|
|
|
@ -4,6 +4,7 @@ using System.IO; |
|
|
|
using System.Linq; |
|
|
|
using System.Reflection; |
|
|
|
using Avalonia.Platform; |
|
|
|
using Avalonia.PlatformSupport.Internal; |
|
|
|
using Avalonia.Utilities; |
|
|
|
|
|
|
|
namespace Avalonia.PlatformSupport |
|
|
|
@ -13,12 +14,16 @@ namespace Avalonia.PlatformSupport |
|
|
|
/// </summary>
|
|
|
|
public class AssetLoader : IAssetLoader |
|
|
|
{ |
|
|
|
private const string AvaloniaResourceName = "!AvaloniaResources"; |
|
|
|
private static readonly Dictionary<string, AssemblyDescriptor> AssemblyNameCache |
|
|
|
= new Dictionary<string, AssemblyDescriptor>(); |
|
|
|
private static AssemblyDescriptorResolver s_assemblyDescriptorResolver = new(); |
|
|
|
|
|
|
|
private AssemblyDescriptor? _defaultResmAssembly; |
|
|
|
|
|
|
|
/// <remarks>
|
|
|
|
/// Introduced for tests.
|
|
|
|
/// </remarks>
|
|
|
|
internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver resolver) => |
|
|
|
s_assemblyDescriptorResolver = resolver; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="AssetLoader"/> class.
|
|
|
|
/// </summary>
|
|
|
|
@ -109,17 +114,18 @@ namespace Avalonia.PlatformSupport |
|
|
|
/// <returns>All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset</returns>
|
|
|
|
public IEnumerable<Uri> GetAssets(Uri uri, Uri? baseUri) |
|
|
|
{ |
|
|
|
if (uri.IsAbsoluteUri && uri.Scheme == "resm") |
|
|
|
if (uri.IsAbsoluteResm()) |
|
|
|
{ |
|
|
|
var assembly = GetAssembly(uri); |
|
|
|
|
|
|
|
return assembly?.Resources?.Where(x => x.Key.Contains(uri.AbsolutePath)) |
|
|
|
return assembly?.Resources? |
|
|
|
.Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) |
|
|
|
.Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? |
|
|
|
Enumerable.Empty<Uri>(); |
|
|
|
} |
|
|
|
|
|
|
|
uri = EnsureAbsolute(uri, baseUri); |
|
|
|
if (uri.Scheme == "avares") |
|
|
|
uri = uri.EnsureAbsolute(baseUri); |
|
|
|
if (uri.IsAvares()) |
|
|
|
{ |
|
|
|
var (asm, path) = GetResAsmAndPath(uri); |
|
|
|
if (asm == null) |
|
|
|
@ -129,33 +135,23 @@ namespace Avalonia.PlatformSupport |
|
|
|
"don't know where to look up for the resource, try specifying assembly explicitly."); |
|
|
|
} |
|
|
|
|
|
|
|
if (asm?.AvaloniaResources == null) |
|
|
|
if (asm.AvaloniaResources == null) |
|
|
|
return Enumerable.Empty<Uri>(); |
|
|
|
path = path.TrimEnd('/') + '/'; |
|
|
|
return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) |
|
|
|
|
|
|
|
if (path[path.Length - 1] != '/') |
|
|
|
path += '/'; |
|
|
|
|
|
|
|
return asm.AvaloniaResources |
|
|
|
.Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) |
|
|
|
.Select(x => new Uri($"avares://{asm.Name}{x.Key}")); |
|
|
|
} |
|
|
|
|
|
|
|
return Enumerable.Empty<Uri>(); |
|
|
|
} |
|
|
|
|
|
|
|
private Uri EnsureAbsolute(Uri uri, Uri? baseUri) |
|
|
|
{ |
|
|
|
if (uri.IsAbsoluteUri) |
|
|
|
return uri; |
|
|
|
if(baseUri == null) |
|
|
|
throw new ArgumentException($"Relative uri {uri} without base url"); |
|
|
|
if (!baseUri.IsAbsoluteUri) |
|
|
|
throw new ArgumentException($"Base uri {baseUri} is relative"); |
|
|
|
if (baseUri.Scheme == "resm") |
|
|
|
throw new ArgumentException( |
|
|
|
$"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); |
|
|
|
return new Uri(baseUri, uri); |
|
|
|
} |
|
|
|
|
|
|
|
private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) |
|
|
|
{ |
|
|
|
if (uri.IsAbsoluteUri && uri.Scheme == "resm") |
|
|
|
if (uri.IsAbsoluteResm()) |
|
|
|
{ |
|
|
|
var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; |
|
|
|
|
|
|
|
@ -172,9 +168,9 @@ namespace Avalonia.PlatformSupport |
|
|
|
return rv; |
|
|
|
} |
|
|
|
|
|
|
|
uri = EnsureAbsolute(uri, baseUri); |
|
|
|
uri = uri.EnsureAbsolute(baseUri); |
|
|
|
|
|
|
|
if (uri.Scheme == "avares") |
|
|
|
if (uri.IsAvares()) |
|
|
|
{ |
|
|
|
var (asm, path) = GetResAsmAndPath(uri); |
|
|
|
if (asm.AvaloniaResources == null) |
|
|
|
@ -188,8 +184,8 @@ namespace Avalonia.PlatformSupport |
|
|
|
|
|
|
|
private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) |
|
|
|
{ |
|
|
|
var asm = GetAssembly(uri.Authority); |
|
|
|
return (asm, uri.AbsolutePath); |
|
|
|
var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); |
|
|
|
return (asm, uri.GetUnescapeAbsolutePath()); |
|
|
|
} |
|
|
|
|
|
|
|
private AssemblyDescriptor? GetAssembly(Uri? uri) |
|
|
|
@ -198,197 +194,20 @@ namespace Avalonia.PlatformSupport |
|
|
|
{ |
|
|
|
if (!uri.IsAbsoluteUri) |
|
|
|
return null; |
|
|
|
if (uri.Scheme == "avares") |
|
|
|
if (uri.IsAvares()) |
|
|
|
return GetResAsmAndPath(uri).asm; |
|
|
|
|
|
|
|
if (uri.Scheme == "resm") |
|
|
|
if (uri.IsResm()) |
|
|
|
{ |
|
|
|
var qs = ParseQueryString(uri); |
|
|
|
|
|
|
|
if (qs.TryGetValue("assembly", out var assemblyName)) |
|
|
|
{ |
|
|
|
return GetAssembly(assemblyName); |
|
|
|
} |
|
|
|
var assemblyName = uri.GetAssemblyNameFromQuery(); |
|
|
|
if (assemblyName.Length > 0) |
|
|
|
return s_assemblyDescriptorResolver.GetAssembly(assemblyName); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private AssemblyDescriptor GetAssembly(string name) |
|
|
|
{ |
|
|
|
if (name == null) |
|
|
|
throw new ArgumentNullException(nameof(name)); |
|
|
|
|
|
|
|
if (!AssemblyNameCache.TryGetValue(name, out var rv)) |
|
|
|
{ |
|
|
|
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); |
|
|
|
var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); |
|
|
|
if (match != null) |
|
|
|
{ |
|
|
|
AssemblyNameCache[name] = rv = new AssemblyDescriptor(match); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// iOS does not support loading assemblies dynamically!
|
|
|
|
#if NET6_0_OR_GREATER
|
|
|
|
if (OperatingSystem.IsIOS()) |
|
|
|
{ |
|
|
|
throw new InvalidOperationException( |
|
|
|
$"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); |
|
|
|
} |
|
|
|
#endif
|
|
|
|
name = Uri.UnescapeDataString(name); |
|
|
|
AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return rv; |
|
|
|
} |
|
|
|
|
|
|
|
private Dictionary<string, string> ParseQueryString(Uri uri) |
|
|
|
{ |
|
|
|
return uri.Query.TrimStart('?') |
|
|
|
.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) |
|
|
|
.Select(p => p.Split('=')) |
|
|
|
.ToDictionary(p => p[0], p => p[1]); |
|
|
|
} |
|
|
|
|
|
|
|
private interface IAssetDescriptor |
|
|
|
{ |
|
|
|
Stream GetStream(); |
|
|
|
Assembly Assembly { get; } |
|
|
|
} |
|
|
|
|
|
|
|
private class AssemblyResourceDescriptor : IAssetDescriptor |
|
|
|
{ |
|
|
|
private readonly Assembly _asm; |
|
|
|
private readonly string _name; |
|
|
|
|
|
|
|
public AssemblyResourceDescriptor(Assembly asm, string name) |
|
|
|
{ |
|
|
|
_asm = asm; |
|
|
|
_name = name; |
|
|
|
} |
|
|
|
|
|
|
|
public Stream GetStream() |
|
|
|
{ |
|
|
|
var s = _asm.GetManifestResourceStream(_name); |
|
|
|
return s ?? throw new InvalidOperationException($"Could not find manifest resource stream '{_name}',"); |
|
|
|
} |
|
|
|
|
|
|
|
public Assembly Assembly => _asm; |
|
|
|
} |
|
|
|
|
|
|
|
private class AvaloniaResourceDescriptor : IAssetDescriptor |
|
|
|
{ |
|
|
|
private readonly int _offset; |
|
|
|
private readonly int _length; |
|
|
|
public Assembly Assembly { get; } |
|
|
|
|
|
|
|
public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) |
|
|
|
{ |
|
|
|
_offset = offset; |
|
|
|
_length = length; |
|
|
|
Assembly = asm; |
|
|
|
} |
|
|
|
|
|
|
|
public Stream GetStream() |
|
|
|
{ |
|
|
|
var s = Assembly.GetManifestResourceStream(AvaloniaResourceName) ?? |
|
|
|
throw new InvalidOperationException($"Could not find manifest resource stream '{AvaloniaResourceName}',"); |
|
|
|
return new SlicedStream(s, _offset, _length); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class SlicedStream : Stream |
|
|
|
{ |
|
|
|
private readonly Stream _baseStream; |
|
|
|
private readonly int _from; |
|
|
|
|
|
|
|
public SlicedStream(Stream baseStream, int from, int length) |
|
|
|
{ |
|
|
|
Length = length; |
|
|
|
_baseStream = baseStream; |
|
|
|
_from = from; |
|
|
|
_baseStream.Position = from; |
|
|
|
} |
|
|
|
public override void Flush() |
|
|
|
{ |
|
|
|
} |
|
|
|
|
|
|
|
public override int Read(byte[] buffer, int offset, int count) |
|
|
|
{ |
|
|
|
return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); |
|
|
|
} |
|
|
|
|
|
|
|
public override long Seek(long offset, SeekOrigin origin) |
|
|
|
{ |
|
|
|
if (origin == SeekOrigin.Begin) |
|
|
|
Position = offset; |
|
|
|
if (origin == SeekOrigin.End) |
|
|
|
Position = _from + Length + offset; |
|
|
|
if (origin == SeekOrigin.Current) |
|
|
|
Position = Position + offset; |
|
|
|
return Position; |
|
|
|
} |
|
|
|
|
|
|
|
public override void SetLength(long value) => throw new NotSupportedException(); |
|
|
|
|
|
|
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|
|
|
|
|
|
|
public override bool CanRead => true; |
|
|
|
public override bool CanSeek => _baseStream.CanRead; |
|
|
|
public override bool CanWrite => false; |
|
|
|
public override long Length { get; } |
|
|
|
public override long Position |
|
|
|
{ |
|
|
|
get => _baseStream.Position - _from; |
|
|
|
set => _baseStream.Position = value + _from; |
|
|
|
} |
|
|
|
|
|
|
|
protected override void Dispose(bool disposing) |
|
|
|
{ |
|
|
|
if (disposing) |
|
|
|
_baseStream.Dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
public override void Close() => _baseStream.Close(); |
|
|
|
} |
|
|
|
|
|
|
|
private class AssemblyDescriptor |
|
|
|
{ |
|
|
|
public AssemblyDescriptor(Assembly assembly) |
|
|
|
{ |
|
|
|
Assembly = assembly; |
|
|
|
|
|
|
|
if (assembly != null) |
|
|
|
{ |
|
|
|
Resources = assembly.GetManifestResourceNames() |
|
|
|
.ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); |
|
|
|
Name = assembly.GetName().Name; |
|
|
|
using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName)) |
|
|
|
{ |
|
|
|
if (resources != null) |
|
|
|
{ |
|
|
|
Resources.Remove(AvaloniaResourceName); |
|
|
|
|
|
|
|
var indexLength = new BinaryReader(resources).ReadInt32(); |
|
|
|
var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); |
|
|
|
var baseOffset = indexLength + 4; |
|
|
|
AvaloniaResources = index.ToDictionary(r => "/" + r.Path!.TrimStart('/'), r => (IAssetDescriptor) |
|
|
|
new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public Assembly Assembly { get; } |
|
|
|
public Dictionary<string, IAssetDescriptor>? Resources { get; } |
|
|
|
public Dictionary<string, IAssetDescriptor>? AvaloniaResources { get; } |
|
|
|
public string? Name { get; } |
|
|
|
} |
|
|
|
|
|
|
|
public static void RegisterResUriParsers() |
|
|
|
{ |
|
|
|
if (!UriParser.IsKnownScheme("avares")) |
|
|
|
|