diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 3c7e82f0802..452898937dc 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; namespace Avalonia.Utilities @@ -66,20 +67,45 @@ private static void WriteIndex(BinaryWriter writer, List Open)> resources) { - var entries = new List(resources.Count); + WriteResources(output, + resources.Select(r => new AvaloniaResourcesEntry { Path = r.Path, Open = r.Open, Size = r.Size }) + .ToList()); + } + + public static void WriteResources(Stream output, IReadOnlyList resources) + { + var entries = new List(); + var index = new Dictionary open)>(); var offset = 0; foreach (var resource in resources) { - entries.Add(new AvaloniaResourcesIndexEntry + // Try to combine resources with the same system path, if present. + if (!string.IsNullOrEmpty(resource.SystemPath) + && index.TryGetValue(resource.SystemPath, out var existingResource)) { - Path = resource.Path, - Offset = offset, - Size = resource.Size - }); - offset += resource.Size; + entries.Add(new AvaloniaResourcesIndexEntry + { + Path = resource.Path, + Offset = existingResource.entry.Offset, + Size = existingResource.entry.Size + }); + } + else + { + var entry = new AvaloniaResourcesIndexEntry + { + Path = resource.Path, + Offset = offset, + Size = resource.Size + }; + index[resource.SystemPath ?? offset.ToString()] = (entry, resource.Open!); + entries.Add(entry); + offset += resource.Size; + } } using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true); @@ -94,9 +120,9 @@ public static void WriteResources(Stream output, List<(string Path, int Size, Fu writer.Write(indexSize); output.Position = posAfterEntries; - foreach (var resource in resources) + foreach (var pair in index) { - using var resourceStream = resource.Open(); + using var resourceStream = pair.Value.open(); resourceStream.CopyTo(output); } } @@ -113,4 +139,15 @@ class AvaloniaResourcesIndexEntry public int Size { get; set; } } + +#if !BUILDTASK + public +#endif + class AvaloniaResourcesEntry + { + public string? Path { get; init; } + public Func? Open { get; init; } + public int Size { get; init; } + public string? SystemPath { get; init; } + } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 6aa815a28b2..665d84778a0 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -80,7 +80,13 @@ private void Pack(Stream output, List sources) { AvaloniaResourcesIndexReaderWriter.WriteResources( output, - sources.Select(source => (source.Path, source.Size, (Func) source.Open)).ToList()); + sources.Select(source => new AvaloniaResourcesEntry + { + Path = source.Path, + Size = source.Size, + SystemPath = source.SystemPath, + Open = source.Open + }).ToList()); } private bool PreProcessXamlFiles(List sources) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs index 079ea91db17..5a70cde4751 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Platform.Internal; using Avalonia.Utilities; using Mono.Cecil; @@ -67,11 +68,13 @@ public void Save() AvaloniaResourcesIndexReaderWriter.WriteResources( output, - _resources.Select(x => ( - Path: x.Key, - Size: x.Value.FileContents.Length, - Open: (Func) (() => new MemoryStream(x.Value.FileContents)) - )).ToList()); + _resources.Select(x => new AvaloniaResourcesEntry + { + Path = x.Key, + Size = x.Value.FileContents.Length, + SystemPath = x.Value.FilePath, + Open = () => new MemoryStream(x.Value.FileContents) + }).ToList()); output.Position = 0L; _embedded = new EmbeddedResource(Constants.AvaloniaResourceName, ManifestResourceAttributes.Public, output); diff --git a/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaResourcesIndexTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaResourcesIndexTests.cs new file mode 100644 index 00000000000..7e850cd3d96 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaResourcesIndexTests.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Text; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests; + +public class AvaloniaResourcesIndexTests +{ + [Fact] + public void Should_Write_And_Read_The_Same_Resources() + { + using var memoryStream = new MemoryStream(); + + var fooBytes = Encoding.UTF8.GetBytes("foo"); + var booBytes = Encoding.UTF8.GetBytes("boo"); + AvaloniaResourcesIndexReaderWriter.WriteResources(memoryStream, + new[] + { + new AvaloniaResourcesEntry + { + Path = "foo.xaml", Size = fooBytes.Length, Open = () => new MemoryStream(fooBytes) + }, + new AvaloniaResourcesEntry + { + Path = "boo.xaml", Size = booBytes.Length, Open = () => new MemoryStream(booBytes) + } + }); + + memoryStream.Seek(4, SeekOrigin.Begin); // skip 4 bytes for "index size" field. + + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(memoryStream); + var resourcesBasePosition = memoryStream.Position; + + Span buffer = stackalloc byte[index[0].Size]; + + Assert.Equal("foo.xaml", index[0].Path); + Assert.Equal(0, index[0].Offset); + Assert.Equal(fooBytes.Length, index[0].Size); + + memoryStream.Seek(resourcesBasePosition + index[0].Offset, SeekOrigin.Begin); + memoryStream.ReadExactly(buffer); + Assert.Equal(fooBytes, buffer.ToArray()); + + Assert.Equal("boo.xaml", index[1].Path); + Assert.Equal(fooBytes.Length, index[1].Offset); + Assert.Equal(booBytes.Length, index[1].Size); + + memoryStream.Seek(resourcesBasePosition + index[1].Offset, SeekOrigin.Begin); + memoryStream.ReadExactly(buffer); + Assert.Equal(booBytes, buffer.ToArray()); + } + + [Fact] + public void Should_Combined_Same_Physical_Path_Resources() + { + using var memoryStream = new MemoryStream(); + + var resourceBytes = Encoding.UTF8.GetBytes("resource-data"); + AvaloniaResourcesIndexReaderWriter.WriteResources(memoryStream, new[] + { + new AvaloniaResourcesEntry + { + Path = "app.xaml", + SystemPath = "app.ico", + Size = resourceBytes.Length, + Open = () => new MemoryStream(resourceBytes) + }, + new AvaloniaResourcesEntry + { + Path = "!__AvaloniaDefaultWindowIcon", + SystemPath = "app.ico", + Size = resourceBytes.Length, + Open = () => new MemoryStream(resourceBytes) + } + }); + + memoryStream.Seek(4, SeekOrigin.Begin); // skip 4 bytes for "index size" field. + + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(memoryStream); + + Assert.Equal("app.xaml", index[0].Path); + Assert.Equal(0, index[0].Offset); + Assert.Equal(resourceBytes.Length, index[0].Size); + + Assert.Equal("!__AvaloniaDefaultWindowIcon", index[1].Path); + Assert.Equal(0, index[1].Offset); + Assert.Equal(resourceBytes.Length, index[1].Size); + } +}