|
|
@@ -7,6 +7,7 @@ using System.IO;
|
|
|
using System.Linq;
|
|
|
using System.Reflection;
|
|
|
using Avalonia.Platform;
|
|
|
+using Avalonia.Utilities;
|
|
|
|
|
|
namespace Avalonia.Shared.PlatformSupport
|
|
|
{
|
|
|
@@ -15,23 +16,24 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
/// </summary>
|
|
|
public class AssetLoader : IAssetLoader
|
|
|
{
|
|
|
+ private const string AvaloniaResourceName = "!AvaloniaResources";
|
|
|
private static readonly Dictionary<string, AssemblyDescriptor> AssemblyNameCache
|
|
|
= new Dictionary<string, AssemblyDescriptor>();
|
|
|
|
|
|
- private AssemblyDescriptor _defaultAssembly;
|
|
|
+ private AssemblyDescriptor _defaultResmAssembly;
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes a new instance of the <see cref="AssetLoader"/> class.
|
|
|
/// </summary>
|
|
|
/// <param name="assembly">
|
|
|
- /// The default assembly from which to load assets for which no assembly is specified.
|
|
|
+ /// The default assembly from which to load resm: assets for which no assembly is specified.
|
|
|
/// </param>
|
|
|
public AssetLoader(Assembly assembly = null)
|
|
|
{
|
|
|
if (assembly == null)
|
|
|
assembly = Assembly.GetEntryAssembly();
|
|
|
if (assembly != null)
|
|
|
- _defaultAssembly = new AssemblyDescriptor(assembly);
|
|
|
+ _defaultResmAssembly = new AssemblyDescriptor(assembly);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -40,7 +42,7 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
/// <param name="assembly">The default assembly.</param>
|
|
|
public void SetDefaultAssembly(Assembly assembly)
|
|
|
{
|
|
|
- _defaultAssembly = new AssemblyDescriptor(assembly);
|
|
|
+ _defaultResmAssembly = new AssemblyDescriptor(assembly);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -99,21 +101,59 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
/// Gets all assets of a folder and subfolders that match specified uri.
|
|
|
/// </summary>
|
|
|
/// <param name="uri">The URI.</param>
|
|
|
+ /// <param name="baseUri">Base URI that is used if <paramref name="uri"/> is relative.</param>
|
|
|
/// <returns>All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset</returns>
|
|
|
- public IEnumerable<(string absolutePath, Assembly assembly)> GetAssets(Uri uri)
|
|
|
+ public IEnumerable<Uri> GetAssets(Uri uri, Uri baseUri)
|
|
|
{
|
|
|
- var assembly = GetAssembly(uri);
|
|
|
+ if (uri.IsAbsoluteUri && uri.Scheme == "resm")
|
|
|
+ {
|
|
|
+ var assembly = GetAssembly(uri);
|
|
|
+
|
|
|
+ return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath))
|
|
|
+ .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ??
|
|
|
+ Enumerable.Empty<Uri>();
|
|
|
+ }
|
|
|
|
|
|
- return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath))
|
|
|
- .Select(x => (x.Key, x.Value.Assembly)) ??
|
|
|
- Enumerable.Empty<(string AbsolutePath, Assembly Assembly)>();
|
|
|
+ uri = EnsureAbsolute(uri, baseUri);
|
|
|
+ if (uri.Scheme == "avares")
|
|
|
+ {
|
|
|
+ var (asm, path) = GetResAsmAndPath(uri);
|
|
|
+ if (asm == null)
|
|
|
+ {
|
|
|
+ throw new ArgumentException(
|
|
|
+ "No default assembly, entry assembly or explicit assembly specified; " +
|
|
|
+ "don't know where to look up for the resource, try specifying assembly explicitly.");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (asm?.AvaloniaResources == null)
|
|
|
+ return Enumerable.Empty<Uri>();
|
|
|
+ path = path.TrimEnd('/') + '/';
|
|
|
+ return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path))
|
|
|
+ .Select(x => new Uri($"avares://{asm.Name}{x.Key}"));
|
|
|
+ }
|
|
|
+
|
|
|
+ return Enumerable.Empty<Uri>();
|
|
|
}
|
|
|
|
|
|
- private IAssetDescriptor GetAsset(Uri uri, Uri baseUri)
|
|
|
+ private Uri EnsureAbsolute(Uri uri, Uri baseUri)
|
|
|
{
|
|
|
- if (!uri.IsAbsoluteUri || uri.Scheme == "resm")
|
|
|
+ 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")
|
|
|
{
|
|
|
- var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultAssembly;
|
|
|
+ var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly;
|
|
|
|
|
|
if (asm == null)
|
|
|
{
|
|
|
@@ -128,19 +168,45 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
asm.Resources.TryGetValue(resourceKey, out rv);
|
|
|
return rv;
|
|
|
}
|
|
|
- throw new ArgumentException($"Invalid uri, see https://github.com/AvaloniaUI/Avalonia/issues/282#issuecomment-166982104", nameof(uri));
|
|
|
+
|
|
|
+ uri = EnsureAbsolute(uri, baseUri);
|
|
|
+
|
|
|
+ if (uri.Scheme == "avares")
|
|
|
+ {
|
|
|
+ var (asm, path) = GetResAsmAndPath(uri);
|
|
|
+ if (asm.AvaloniaResources == null)
|
|
|
+ return null;
|
|
|
+ asm.AvaloniaResources.TryGetValue(path, out var desc);
|
|
|
+ return desc;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri));
|
|
|
}
|
|
|
|
|
|
+ private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri)
|
|
|
+ {
|
|
|
+ var asm = GetAssembly(uri.Authority);
|
|
|
+ return (asm, uri.AbsolutePath);
|
|
|
+ }
|
|
|
+
|
|
|
private AssemblyDescriptor GetAssembly(Uri uri)
|
|
|
{
|
|
|
if (uri != null)
|
|
|
{
|
|
|
- var qs = ParseQueryString(uri);
|
|
|
- string assemblyName;
|
|
|
+ if (!uri.IsAbsoluteUri)
|
|
|
+ return null;
|
|
|
+ if (uri.Scheme == "avares")
|
|
|
+ return GetResAsmAndPath(uri).asm;
|
|
|
|
|
|
- if (qs.TryGetValue("assembly", out assemblyName))
|
|
|
+ if (uri.Scheme == "resm")
|
|
|
{
|
|
|
- return GetAssembly(assemblyName);
|
|
|
+ var qs = ParseQueryString(uri);
|
|
|
+ string assemblyName;
|
|
|
+
|
|
|
+ if (qs.TryGetValue("assembly", out assemblyName))
|
|
|
+ {
|
|
|
+ return GetAssembly(assemblyName);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -150,9 +216,7 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
private AssemblyDescriptor GetAssembly(string name)
|
|
|
{
|
|
|
if (name == null)
|
|
|
- {
|
|
|
- return _defaultAssembly;
|
|
|
- }
|
|
|
+ throw new ArgumentNullException(nameof(name));
|
|
|
|
|
|
AssemblyDescriptor rv;
|
|
|
if (!AssemblyNameCache.TryGetValue(name, out rv))
|
|
|
@@ -167,9 +231,7 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
{
|
|
|
// iOS does not support loading assemblies dynamically!
|
|
|
//
|
|
|
-#if NETCOREAPP1_0
|
|
|
- AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(new AssemblyName(name)));
|
|
|
-#elif __IOS__
|
|
|
+#if __IOS__
|
|
|
throw new InvalidOperationException(
|
|
|
$"Assembly {name} needs to be referenced and explicitly loaded before loading resources");
|
|
|
#else
|
|
|
@@ -213,6 +275,80 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
|
|
|
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()
|
|
|
+ {
|
|
|
+ return new SlicedStream(Assembly.GetManifestResourceStream(AvaloniaResourceName), _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
|
|
|
{
|
|
|
@@ -225,12 +361,37 @@ namespace Avalonia.Shared.PlatformSupport
|
|
|
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"))
|
|
|
+ UriParser.Register(new GenericUriParser(
|
|
|
+ GenericUriParserOptions.GenericAuthority |
|
|
|
+ GenericUriParserOptions.NoUserInfo |
|
|
|
+ GenericUriParserOptions.NoPort |
|
|
|
+ GenericUriParserOptions.NoQuery |
|
|
|
+ GenericUriParserOptions.NoFragment), "avares", -1);
|
|
|
+ }
|
|
|
}
|
|
|
}
|