51 changed files with 1064 additions and 406 deletions
@ -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 ""; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Platform |
||||
|
{ |
||||
|
public interface IPlatformNativeSurfaceHandle : IPlatformHandle |
||||
|
{ |
||||
|
PixelSize Size { get; } |
||||
|
double Scaling { get; } |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Avalonia.PlatformSupport.Internal; |
||||
|
|
||||
|
internal static class Constants |
||||
|
{ |
||||
|
public static string AvaloniaResourceName => "!AvaloniaResources"; |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
// This source file is adapted from the Windows Presentation Foundation project.
|
||||
|
// (https://github.com/dotnet/wpf/)
|
||||
|
//
|
||||
|
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Controls.Metadata |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines a control template part referenced by name in code.
|
||||
|
/// Template part names should begin with the "PART_" prefix.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Style authors should be able to identify the part type used for styling the specific control.
|
||||
|
/// The part is usually required in the style and should have a specific predefined name.
|
||||
|
/// </remarks>
|
||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] |
||||
|
public sealed class TemplatePartAttribute : Attribute |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="TemplatePartAttribute"/> class.
|
||||
|
/// </summary>
|
||||
|
public TemplatePartAttribute() |
||||
|
{ |
||||
|
Name = string.Empty; |
||||
|
Type = typeof(object); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="TemplatePartAttribute"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="name">The part name used by the class to identify a required element in the style.</param>
|
||||
|
/// <param name="type">The type of the element that should be used as a part with name.</param>
|
||||
|
public TemplatePartAttribute(string name, Type type) |
||||
|
{ |
||||
|
Name = name; |
||||
|
Type = type; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the part name used by the class to identify a required element in the style.
|
||||
|
/// Template part names should begin with the "PART_" prefix.
|
||||
|
/// </summary>
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the type of the element that should be used as a part with name specified
|
||||
|
/// in <see cref="Name"/>.
|
||||
|
/// </summary>
|
||||
|
public Type Type { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
Loading…
Reference in new issue