Browse Source

Fix getting paths from Uri

pull/7831/head
Rustam Sayfutdinov 4 years ago
parent
commit
04f9c0b5b6
  1. 27
      Avalonia.sln
  2. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  3. 70
      src/Avalonia.Base/Utilities/UriExtensions.cs
  4. 241
      src/Avalonia.PlatformSupport/AssetLoader.cs
  5. 5
      src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj
  6. 42
      src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs
  7. 42
      src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs
  8. 52
      src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs
  9. 6
      src/Avalonia.PlatformSupport/Internal/Constants.cs
  10. 59
      src/Avalonia.PlatformSupport/Internal/SlicedStream.cs
  11. 150
      src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
  12. 29
      tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs
  13. 62
      tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs
  14. 24
      tests/Avalonia.PlatformSupport.UnitTests/Avalonia.PlatformSupport.UnitTests.csproj
  15. 5
      tests/Avalonia.UnitTests/MockAssetLoader.cs
  16. 45
      tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

27
Avalonia.sln

@ -234,6 +234,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@ -2212,6 +2214,30 @@ Global
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhone.Build.0 = Release|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.Build.0 = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.ActiveCfg = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -2276,6 +2302,7 @@ Global
{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -11,4 +11,5 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.PlatformSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

70
src/Avalonia.Base/Utilities/UriExtensions.cs

@ -0,0 +1,70 @@
using System;
namespace Avalonia.Utilities;
internal static class UriExtensions
{
public static bool IsAbsoluteResm(this Uri uri) =>
uri.IsAbsoluteUri && uri.IsResm();
public static bool IsResm(this Uri uri) => uri.Scheme == "resm";
public static bool IsAvares(this Uri uri) => uri.Scheme == "avares";
public static Uri EnsureAbsolute(this 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.IsResm())
throw new ArgumentException(
$"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm");
return new Uri(baseUri, uri);
}
public static string GetUnescapeAbsolutePath(this Uri uri) =>
Uri.UnescapeDataString(uri.AbsolutePath);
public static string GetUnescapeAbsoluteUri(this Uri uri) =>
Uri.UnescapeDataString(uri.AbsoluteUri);
public static string GetAssemblyNameFromQuery(this Uri uri)
{
const string assembly = "assembly";
var query = Uri.UnescapeDataString(uri.Query);
// Skip the '?'
var currentIndex = 1;
while (currentIndex < query.Length)
{
var isFind = false;
for (var i = 0; i < assembly.Length; ++currentIndex, ++i)
if (query[currentIndex] == assembly[i])
{
isFind = i == assembly.Length - 1;
}
else
{
break;
}
// Skip the '='
++currentIndex;
var beginIndex = currentIndex;
while (currentIndex < query.Length && query[currentIndex] != '&')
++currentIndex;
if (isFind)
return query.Substring(beginIndex, currentIndex - beginIndex);
++currentIndex;
}
return "";
}
}

241
src/Avalonia.PlatformSupport/AssetLoader.cs

@ -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"))

5
src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj

@ -16,4 +16,9 @@
<Import Project="..\..\build\NetCore.props" />
<Import Project="..\..\build\NetFX.props" />
<Import Project="..\..\build\NullableEnable.props" />
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87" />
</ItemGroup>
</Project>

42
src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Utilities;
namespace Avalonia.PlatformSupport.Internal;
internal 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(Constants.AvaloniaResourceName))
{
if (resources != null)
{
Resources.Remove(Constants.AvaloniaResourceName);
var indexLength = new BinaryReader(resources).ReadInt32();
var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength));
var baseOffset = indexLength + 4;
AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), 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; }
private static string GetPathRooted(AvaloniaResourcesIndexEntry r) =>
r.Path![0] == '/' ? r.Path : '/' + r.Path;
}

42
src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Avalonia.PlatformSupport.Internal;
internal class AssemblyDescriptorResolver
{
private readonly Dictionary<string, AssemblyDescriptor> _assemblyNameCache = new();
public 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;
}
}

52
src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs

@ -0,0 +1,52 @@
using System;
using System.IO;
using System.Reflection;
namespace Avalonia.PlatformSupport.Internal;
internal interface IAssetDescriptor
{
Stream GetStream();
Assembly Assembly { get; }
}
internal 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;
}
internal 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(Constants.AvaloniaResourceName) ??
throw new InvalidOperationException($"Could not find manifest resource stream '{Constants.AvaloniaResourceName}',");
return new SlicedStream(s, _offset, _length);
}
}

6
src/Avalonia.PlatformSupport/Internal/Constants.cs

@ -0,0 +1,6 @@
namespace Avalonia.PlatformSupport.Internal;
internal static class Constants
{
public static string AvaloniaResourceName => "!AvaloniaResources";
}

59
src/Avalonia.PlatformSupport/Internal/SlicedStream.cs

@ -0,0 +1,59 @@
using System;
using System.IO;
namespace Avalonia.PlatformSupport.Internal;
internal 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();
}

150
src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
@ -12,18 +13,10 @@ namespace Avalonia.Media.Fonts
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey)
{
var sourceWithoutArguments = fontFamilyKey.Source.OriginalString.Split('?').First();
if (sourceWithoutArguments.EndsWith(".ttf")
|| sourceWithoutArguments.EndsWith(".otf"))
{
return GetFontAssetsByExpression(fontFamilyKey);
}
return GetFontAssetsBySource(fontFamilyKey);
}
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
IsFontTtfOrOtf(fontFamilyKey.Source) ?
GetFontAssetsByExpression(fontFamilyKey) :
GetFontAssetsBySource(fontFamilyKey);
/// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets
@ -34,11 +27,7 @@ namespace Avalonia.Media.Fonts
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var matchingAssets =
availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf"));
return matchingAssets;
return availableAssets.Where(x => IsFontTtfOrOtf(x));
}
/// <summary>
@ -49,71 +38,124 @@ namespace Avalonia.Media.Fonts
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
{
var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location);
var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location);
return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
}
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
private static (string fileNameWithoutExtension, string extension) GetFileName(
FontFamilyKey fontFamilyKey, out Uri location)
{
if (fontFamilyKey.Source.IsAbsoluteResm())
{
var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.');
var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri()
.Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
return fileName;
}
string compareTo;
var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString);
var fullFilename = filename.fileNameWithoutExtension + filename.extension;
if (fontFamilyKey.Source.IsAbsoluteUri)
if (fontFamilyKey.BaseUri != null)
{
if (fontFamilyKey.Source.Scheme == "resm")
{
compareTo = location.AbsolutePath + "." + fileName.Split('*').First();
}
else
{
compareTo = location.AbsolutePath + fileName.Split('*').First();
}
var relativePath = fontFamilyKey.Source.OriginalString
.Replace(fullFilename, string.Empty);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
}
else
{
compareTo = location.AbsolutePath + fileName.Split('*').First();
var uriString = fontFamilyKey.Source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
}
var matchingResources = availableResources.Where(
x => x.AbsolutePath.Contains(compareTo)
&& x.AbsolutePath.EndsWith(fileExtension));
return filename;
}
return matchingResources;
private static string CreateFilePattern(
FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension)
{
var path = location.GetUnescapeAbsolutePath();
var file = GetSubString(fileNameWithoutExtension, '*');
return fontFamilyKey.Source.IsAbsoluteResm()
? path + "." + file
: path + file;
}
private static string GetFileName(FontFamilyKey fontFamilyKey, out string fileExtension, out Uri location)
private static bool IsContainsFile(Uri x, string filePattern, string fileExtension)
{
if (fontFamilyKey.Source.IsAbsoluteUri && fontFamilyKey.Source.Scheme == "resm")
{
fileExtension = "." + fontFamilyKey.Source.AbsolutePath.Split('.').LastOrDefault();
var path = x.GetUnescapeAbsolutePath();
return path.IndexOf(filePattern, StringComparison.Ordinal) >= 0
&& path.EndsWith(fileExtension, StringComparison.Ordinal);
}
var fileName = fontFamilyKey.Source.LocalPath.Replace(fileExtension, string.Empty).Split('.').Last();
private static bool IsFontTtfOrOtf(Uri uri)
{
var sourceWithoutArguments = GetSubString(uri.OriginalString, '?');
return sourceWithoutArguments.EndsWith(".ttf", StringComparison.Ordinal)
|| sourceWithoutArguments.EndsWith(".otf", StringComparison.Ordinal);
}
location = new Uri(fontFamilyKey.Source.AbsoluteUri.Replace("." + fileName + fileExtension, string.Empty), UriKind.RelativeOrAbsolute);
private static (string fileNameWithoutExtension, string extension) GetFileNameAndExtension(
string path, char directorySeparator = '/')
{
var pathAsSpan = path.AsSpan();
pathAsSpan = IsPathRooted(pathAsSpan, directorySeparator) ?
pathAsSpan.Slice(1, path.Length - 1) :
pathAsSpan;
return fileName;
}
var extension = GetFileExtension(pathAsSpan);
if (extension.Length == pathAsSpan.Length)
return (extension.ToString(), string.Empty);
var pathSegments = fontFamilyKey.Source.OriginalString.Split('/');
var fileName = GetFileName(pathAsSpan, directorySeparator, extension.Length);
return (fileName.ToString(), extension.ToString());
}
var fileNameWithExtension = pathSegments.Last();
private static bool IsPathRooted(ReadOnlySpan<char> path, char directorySeparator) =>
path.Length > 0 && path[0] == directorySeparator;
var fileNameSegments = fileNameWithExtension.Split('.');
private static ReadOnlySpan<char> GetFileExtension(ReadOnlySpan<char> path)
{
for (var i = path.Length - 1; i > 0; --i)
{
if (path[i] == '.')
return path.Slice(i, path.Length - i);
}
fileExtension = "." + fileNameSegments.Last();
return path;
}
if (fontFamilyKey.BaseUri != null)
private static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> path, char directorySeparator, int extensionLength)
{
for (var i = path.Length - extensionLength - 1; i >= 0; --i)
{
var relativePath = fontFamilyKey.Source.OriginalString
.Replace(fileNameWithExtension, string.Empty);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
if (path[i] == directorySeparator)
return path.Slice(i + 1, path.Length - i - extensionLength - 1);
}
else
return path.Slice(0, path.Length - extensionLength);
}
private static string GetSubString(string path, char separator)
{
for (var i = 0; i < path.Length; i++)
{
location = new Uri(fontFamilyKey.Source.AbsolutePath.Replace(fileNameWithExtension, string.Empty));
if (path[i] == separator)
return path.Substring(0, i);
}
return fileNameSegments.First();
return path;
}
}
}

29
tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs

@ -0,0 +1,29 @@
using System;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Base.UnitTests.Utilities;
public class UriExtensionsTests
{
[Fact]
public void Assembly_Name_From_Query_Parsed()
{
const string key = "assembly";
const string value = "Avalonia.Themes.Default";
var uri = new Uri($"resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?{key}={value}");
var name = uri.GetAssemblyNameFromQuery();
Assert.Equal(value, name);
}
[Fact]
public void Assembly_Name_From_Empty_Query_Not_Parsed()
{
var uri = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml");
var name = uri.GetAssemblyNameFromQuery();
Assert.Equal(string.Empty, name);
}
}

62
tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs

@ -0,0 +1,62 @@
using System;
using System.Reflection;
using Avalonia.PlatformSupport.Internal;
using Moq;
using Xunit;
namespace Avalonia.PlatformSupport.UnitTests;
public class AssetLoaderTests
{
public class MockAssembly : Assembly {}
private const string AssemblyNameWithWhitespace = "Awesome Library";
private const string AssemblyNameWithNonAscii = "Какое-то-название";
static AssetLoaderTests()
{
var resolver = Mock.Of<AssemblyDescriptorResolver>();
var descriptor = CreateAssemblyDescriptor(AssemblyNameWithWhitespace);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor);
descriptor = CreateAssemblyDescriptor(AssemblyNameWithNonAscii);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithNonAscii)).Returns(descriptor);
AssetLoader.SetAssemblyDescriptorResolver(resolver);
}
[Fact]
public void AssemblyName_With_Whitespace_Should_Load_Resm()
{
var uri = new Uri($"resm:Avalonia.Base.UnitTests.Assets.something?assembly={AssemblyNameWithWhitespace}");
var loader = new AssetLoader();
var assemblyActual = loader.GetAssembly(uri, null);
Assert.Equal(AssemblyNameWithWhitespace, assemblyActual?.FullName);
}
[Fact]
public void AssemblyName_With_Non_ASCII_Should_Load_Avares()
{
var uri = new Uri($"avares://{AssemblyNameWithNonAscii}/Assets/something");
var loader = new AssetLoader();
var assemblyActual = loader.GetAssembly(uri, null);
Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName);
}
private static AssemblyDescriptor CreateAssemblyDescriptor(string assemblyName)
{
var assembly = Mock.Of<MockAssembly>();
Mock.Get(assembly).Setup(x => x.GetName()).Returns(new AssemblyName(assemblyName));
Mock.Get(assembly).Setup(x => x.FullName).Returns(assemblyName);
var descriptor = Mock.Of<AssemblyDescriptor>();
Mock.Get(descriptor).Setup(x => x.Assembly).Returns(assembly);
return descriptor;
}
}

24
tests/Avalonia.PlatformSupport.UnitTests/Avalonia.PlatformSupport.UnitTests.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
</Project>

5
tests/Avalonia.UnitTests/MockAssetLoader.cs

@ -5,6 +5,7 @@ using System.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.UnitTests
{
@ -39,7 +40,9 @@ namespace Avalonia.UnitTests
public IEnumerable<Uri> GetAssets(Uri uri, Uri baseUri)
{
return _assets.Keys.Where(x => x.AbsolutePath.Contains(uri.AbsolutePath));
var absPath = uri.GetUnescapeAbsolutePath();
return _assets.Keys.Where(
x => x.GetUnescapeAbsolutePath().IndexOf(absPath, StringComparison.Ordinal) >= 0);
}
public void SetDefaultAssembly(Assembly asm)

45
tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

@ -12,24 +12,28 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts
public class FontFamilyLoaderTests : IDisposable
{
private const string AssetMyFontRegular = AssetLocation + ".MyFont Regular.ttf" + Assembly + FontName;
private const string FontName = "#MyFont";
private const string Assembly = "?assembly=Avalonia.Visuals.UnitTests";
private const string AssetLocation = "resm:Avalonia.Visuals.UnitTests.Assets";
private const string AssetLocationAvares = "avares://Avalonia.Visuals.UnitTests";
private const string AssetYourFileName = "/Assets/YourFont.ttf";
private const string AssetYourFontAvares = AssetLocationAvares + AssetYourFileName;
private readonly IDisposable _testApplication;
public FontFamilyLoaderTests()
{
const string AssetMyFontRegular = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName;
const string AssetMyFontBold = AssetLocation + ".MyFont-Bold.ttf" + Assembly + FontName;
const string AssetMyFontBold = AssetLocation + ".MyFont Bold.ttf" + Assembly + FontName;
const string AssetYourFont = AssetLocation + ".YourFont.ttf" + Assembly + FontName;
var fontAssets = new[]
{
(AssetMyFontRegular, "AssetData"),
(AssetMyFontBold, "AssetData"),
(AssetYourFont, "AssetData")
};
{
(AssetMyFontRegular, "AssetData"),
(AssetMyFontBold, "AssetData"),
(AssetYourFont, "AssetData"),
(AssetYourFontAvares, "AssetData")
};
_testApplication = StartWithResources(fontAssets);
}
@ -42,10 +46,18 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts
[Fact]
public void Should_Load_Single_FontAsset()
{
const string FontAsset = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName;
var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var source = new Uri(FontAsset, UriKind.RelativeOrAbsolute);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
Assert.Single(fontAssets);
}
[Fact]
public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
{
var source = new Uri(AssetYourFontAvares);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
@ -54,10 +66,21 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts
}
[Fact]
public void Should_Load_Matching_Assets()
public void Should_Load_Single_FontAsset_Avares_With_BaseUri()
{
var source = new Uri(AssetLocation + ".MyFont-*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
var baseUri = new Uri(AssetLocationAvares);
var key = new FontFamilyKey(source, baseUri);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
Assert.Single(fontAssets);
}
[Fact]
public void Should_Load_Matching_Assets()
{
var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray();

Loading…
Cancel
Save