diff --git a/.github/skills/tests/references/test-catalog.md b/.github/skills/tests/references/test-catalog.md index ca48bdadc0d..3c76975e1d7 100644 --- a/.github/skills/tests/references/test-catalog.md +++ b/.github/skills/tests/references/test-catalog.md @@ -20,6 +20,7 @@ These tests can be run immediately with `dotnet test` on the `.csproj`, even if | Test Area | Project | Command | |-----------|---------|---------| +| **assembly store reader** | `tools/assembly-store-reader-mk2/AssemblyStore.Tests/` | `dotnet test tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStore.Tests.csproj -v minimal` | | **trimmable type map** (unit) | `tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/` | `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal` | | **aidl** | `tests/Xamarin.Android.Tools.Aidl-Tests/` | `dotnet test tests/Xamarin.Android.Tools.Aidl-Tests/Xamarin.Android.Tools.Aidl-Tests.csproj -v minimal` | | **source writer** | `external/Java.Interop/tests/Xamarin.SourceWriter-Tests/` | `dotnet test external/Java.Interop/tests/Xamarin.SourceWriter-Tests/Xamarin.SourceWriter-Tests.csproj -v minimal` | @@ -205,5 +206,6 @@ Run these tests with `dotnet test` from each test project directory listed above | Test Area | Tier | Assembly / Project | Notes | |-----------|------|--------------------|-------| | **aidl** | **Standalone** | `tests/Xamarin.Android.Tools.Aidl-Tests/` | AIDL compiler tests — `dotnet test` on `.csproj` | +| **assembly store reader** | **Standalone** | `tools/assembly-store-reader-mk2/AssemblyStore.Tests/` | Legacy/current store layouts and LZ4/Zstd decompression | | **api compatibility** | N/A | `tests/api-compatibility/` | Not a test runner — reference data for API surface checks | | **android sdk tools** | **Standalone** | `external/xamarin-android-tools/tests/` | Android SDK helper tooling tests — `dotnet test` on `.csproj` | diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln index cdbcd19f9b5..ea2e1ad545e 100644 --- a/Xamarin.Android.sln +++ b/Xamarin.Android.sln @@ -159,6 +159,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "xamarin-android-tools", "xa EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{65925F91-7BB8-AC56-CADB-FB3DD28A5B5B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyStore.Tests", "tools\assembly-store-reader-mk2\AssemblyStore.Tests\AssemblyStore.Tests.csproj", "{34FEC4BE-168E-460A-B049-94AA4627CE83}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyStore", "tools\assembly-store-reader-mk2\AssemblyStore\AssemblyStore.csproj", "{19DA4CD1-21A7-49FE-9431-4A8DF69A186B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -997,6 +1001,30 @@ Global {4E5C5846-CBFD-4695-B1F0-D48DEC0AF50B}.Release|x64.Build.0 = Release|Any CPU {4E5C5846-CBFD-4695-B1F0-D48DEC0AF50B}.Release|x86.ActiveCfg = Release|Any CPU {4E5C5846-CBFD-4695-B1F0-D48DEC0AF50B}.Release|x86.Build.0 = Release|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Debug|x64.ActiveCfg = Debug|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Debug|x64.Build.0 = Debug|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Debug|x86.ActiveCfg = Debug|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Debug|x86.Build.0 = Debug|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Release|Any CPU.Build.0 = Release|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Release|x64.ActiveCfg = Release|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Release|x64.Build.0 = Release|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Release|x86.ActiveCfg = Release|Any CPU + {34FEC4BE-168E-460A-B049-94AA4627CE83}.Release|x86.Build.0 = Release|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Debug|x64.ActiveCfg = Debug|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Debug|x64.Build.0 = Debug|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Debug|x86.ActiveCfg = Debug|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Debug|x86.Build.0 = Debug|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Release|Any CPU.Build.0 = Release|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Release|x64.ActiveCfg = Release|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Release|x64.Build.0 = Release|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Release|x86.ActiveCfg = Release|Any CPU + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1071,6 +1099,8 @@ Global {4E5C5846-CBFD-4695-B1F0-D48DEC0AF50B} = {FFCF518F-2A4A-40A2-9174-2EE13B76C723} {BB3C8E1B-31A2-1C4E-0B3F-36B3F475D6AA} = {05C3B1D6-A4CE-4534-A9E4-E9117591ADF7} {65925F91-7BB8-AC56-CADB-FB3DD28A5B5B} = {BB3C8E1B-31A2-1C4E-0B3F-36B3F475D6AA} + {34FEC4BE-168E-460A-B049-94AA4627CE83} = {864062D3-A415-4A6F-9324-5820237BA058} + {19DA4CD1-21A7-49FE-9431-4A8DF69A186B} = {864062D3-A415-4A6F-9324-5820237BA058} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {53A1F287-EFB2-4D97-A4BB-4A5E145613F6} diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml index 26c3d3100f7..8c3080f0574 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -105,6 +105,21 @@ steps: testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-tests/*.trx" testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.Tests +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self + parameters: + command: test + project: tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStore.Tests.csproj + arguments: -c $(XA.Build.Configuration) --logger trx --results-directory $(Agent.TempDirectory)/assembly-store-tests + displayName: Test AssemblyStore.Tests $(XA.Build.Configuration) + +- task: PublishTestResults@2 + displayName: publish AssemblyStore.Tests results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: "$(Agent.TempDirectory)/assembly-store-tests/*.trx" + testRunTitle: AssemblyStore.Tests + - template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self parameters: command: test diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj index f99cb2ba1c1..708f81906a5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj @@ -35,6 +35,7 @@ + ..\Expected\GenerateDesignerFileExpected.cs PreserveNewest diff --git a/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj b/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj index aaf12f6298e..59d31a19cc1 100644 --- a/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj +++ b/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj @@ -26,6 +26,7 @@ + diff --git a/tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStore.Tests.csproj b/tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStore.Tests.csproj new file mode 100644 index 00000000000..149f621c0a9 --- /dev/null +++ b/tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStore.Tests.csproj @@ -0,0 +1,19 @@ + + + + + + $(DotNetStableTargetFramework);$(DotNetTargetFramework) + enable + false + Xamarin.Android.AssemblyStore.Tests + + + + + + + + + + diff --git a/tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStoreTests.cs b/tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStoreTests.cs new file mode 100644 index 00000000000..2b84dc8dee4 --- /dev/null +++ b/tools/assembly-store-reader-mk2/AssemblyStore.Tests/AssemblyStoreTests.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +#if NET11_0_OR_GREATER +using K4os.Compression.LZ4; +#endif // NET11_0_OR_GREATER +using NUnit.Framework; +using Xamarin.Android.Tools; +#if NET11_0_OR_GREATER +using ZstandardEncoder = System.IO.Compression.ZstandardEncoder; +#endif // NET11_0_OR_GREATER + +namespace Xamarin.Android.AssemblyStore.Tests; + +[TestFixture] +public class AssemblyStoreTests +{ + static readonly byte[] assemblyData = "Synthetic managed assembly"u8.ToArray (); + +#if NET11_0_OR_GREATER + [TestCase (AssemblyCompressionFormat.Lz4)] + [TestCase (AssemblyCompressionFormat.Zstandard)] + public void DecompressesBothCompressionFormats (AssemblyCompressionFormat format) + { + using Stream input = CreateCompressedAssembly (format, assemblyData); + using var output = new MemoryStream (); + + Assert.IsTrue (AssemblyCompression.TryDecompress (input, output, out AssemblyCompressionFormat detectedFormat)); + Assert.AreEqual (format, detectedFormat); + CollectionAssert.AreEqual (assemblyData, output.ToArray ()); + } + + [Test] + public void LeavesUncompressedAssembliesUntouched () + { + using var input = new MemoryStream (assemblyData); + using var output = new MemoryStream (); + + Assert.IsFalse (AssemblyCompression.TryDecompress (input, output, out _)); + Assert.AreEqual (0, input.Position); + Assert.AreEqual (0, output.Length); + } + + [Test] + public void RejectsUnreasonableUncompressedLength () + { + using var input = new MemoryStream (); + using (var writer = new BinaryWriter (input, System.Text.Encoding.UTF8, leaveOpen: true)) { + writer.Write (0x5A4C4158u); + writer.Write (0u); + writer.Write (1024u * 1024 * 1024); + writer.Write ((byte)0); + } + input.Seek (0, SeekOrigin.Begin); + using var output = new MemoryStream (); + + InvalidDataException? exception = Assert.Throws ( + () => AssemblyCompression.TryDecompress (input, output, out _) + ); + Assert.That (exception?.Message, Does.Contain ("unsupported size")); + } + + [TestCase (AssemblyCompressionFormat.Lz4, "base_assemblies.blob")] + [TestCase (AssemblyCompressionFormat.Zstandard, "base_assemblies.manifest")] + [TestCase (AssemblyCompressionFormat.Lz4, "base")] + public void ReadsLegacyStoreSets (AssemblyCompressionFormat format, string inputName) + { + string directory = CreateTemporaryDirectory (); + try { + using MemoryStream compressedStream = CreateCompressedAssembly (format, assemblyData); + byte[] compressedAssembly = compressedStream.ToArray (); + string indexStore = Path.Combine (directory, "base_assemblies.blob"); + CreateV1StoreSet (indexStore, compressedAssembly); + + (IList? explorers, string? errorMessage) = AssemblyStoreExplorer.Open (Path.Combine (directory, inputName)); + Assert.IsNull (errorMessage); + Assert.IsNotNull (explorers); + + AssemblyStoreExplorer explorer = RequireSingle (explorers, "legacy store explorer"); + Assert.AreEqual (AndroidTargetArch.Arm64, explorer.TargetArch); + + IList? items = explorer.Find ("Test.dll", AndroidTargetArch.Arm64); + Assert.IsNotNull (items); + AssemblyStoreItem item = RequireSingle (items, "legacy store item"); + + using Stream image = explorer.ReadImageData (item, uncompressIfNeeded: true) ?? + throw new InvalidOperationException ("Legacy store image was not returned"); + using var output = new MemoryStream (); + image.CopyTo (output); + CollectionAssert.AreEqual (assemblyData, output.ToArray ()); + } finally { + Directory.Delete (directory, recursive: true); + } + } + + [Test] + public void ReadsLegacyStoreApk () + { + string directory = CreateTemporaryDirectory (); + try { + using MemoryStream compressedStream = CreateCompressedAssembly (AssemblyCompressionFormat.Lz4, assemblyData); + string indexStore = Path.Combine (directory, "assemblies.blob"); + CreateV1StoreSet (indexStore, compressedStream.ToArray ()); + + string apk = Path.Combine (directory, "legacy-v1.apk"); + using (FileStream file = File.Create (apk)) + using (var archive = new ZipArchive (file, ZipArchiveMode.Create)) { + archive.CreateEntry ("AndroidManifest.xml"); + WriteEntry (archive, "assemblies/assemblies.blob", File.ReadAllBytes (indexStore)); + WriteEntry (archive, "assemblies/assemblies.arm64_v8a.blob", File.ReadAllBytes (Path.Combine (directory, "assemblies.arm64_v8a.blob"))); + WriteEntry (archive, "assemblies/assemblies.manifest", File.ReadAllBytes (Path.Combine (directory, "assemblies.manifest"))); + } + + (IList? explorers, string? errorMessage) = AssemblyStoreExplorer.Open (apk); + Assert.IsNull (errorMessage); + AssemblyStoreExplorer explorer = RequireSingle (explorers, "legacy APK store explorer"); + AssemblyStoreItem item = RequireSingle (explorer.Find ("Test.dll", AndroidTargetArch.Arm64), "legacy APK store item"); + using Stream image = explorer.ReadImageData (item, uncompressIfNeeded: true) ?? + throw new InvalidOperationException ("Legacy APK store image was not returned"); + using var output = new MemoryStream (); + image.CopyTo (output); + CollectionAssert.AreEqual (assemblyData, output.ToArray ()); + } finally { + Directory.Delete (directory, recursive: true); + } + } +#endif // NET11_0_OR_GREATER + + [TestCase (2u, "lib/arm64-v8a/libassemblies.arm64-v8a.blob.so")] + [TestCase (3u, "lib/arm64-v8a/libassembly-store.so")] + public void ReadsV2ArchivePathsAndVersions (uint version, string storePath) + { + string directory = CreateTemporaryDirectory (); + try { + string apk = Path.Combine (directory, "legacy-v2.apk"); + using (FileStream file = File.Create (apk)) + using (var archive = new ZipArchive (file, ZipArchiveMode.Create)) { + archive.CreateEntry ("AndroidManifest.xml"); + WriteEntry ( + archive, + storePath, + CreateV2Store (assemblyData, version) + ); + } + + (IList? explorers, string? errorMessage) = AssemblyStoreExplorer.Open (apk); + Assert.IsNull (errorMessage); + Assert.IsNotNull (explorers); + + AssemblyStoreExplorer explorer = RequireSingle (explorers, "v2 store explorer"); + IList? items = explorer.Find ("Test.dll", AndroidTargetArch.Arm64); + Assert.IsNotNull (items); + AssemblyStoreItem item = RequireSingle (items, "v2 store item"); + using Stream image = explorer.ReadImageData (item, uncompressIfNeeded: true) ?? + throw new InvalidOperationException ("V2 store image was not returned"); + using var output = new MemoryStream (); + image.CopyTo (output); + CollectionAssert.AreEqual (assemblyData, output.ToArray ()); + } finally { + Directory.Delete (directory, recursive: true); + } + } + + [Test] + public void IncludesCommonAssembliesInLegacyArchitectureViews () + { + byte[] commonAssemblyData = "Common managed assembly"u8.ToArray (); + string directory = CreateTemporaryDirectory (); + try { + string indexStore = Path.Combine (directory, "assemblies.blob"); + CreateV1StoreSet (indexStore, assemblyData, commonAssemblyData); + + (IList? explorers, string? errorMessage) = AssemblyStoreExplorer.Open (indexStore); + Assert.IsNull (errorMessage); + AssemblyStoreExplorer explorer = RequireSingle (explorers, "legacy store explorer"); + AssemblyStoreItem item = RequireSingle (explorer.Find ("Common.dll", AndroidTargetArch.Arm64), "common legacy store item"); + using Stream image = explorer.ReadImageData (item) ?? + throw new InvalidOperationException ("Common legacy store image was not returned"); + using var output = new MemoryStream (); + image.CopyTo (output); + CollectionAssert.AreEqual (commonAssemblyData, output.ToArray ()); + } finally { + Directory.Delete (directory, recursive: true); + } + } + +#if NET11_0_OR_GREATER + static MemoryStream CreateCompressedAssembly (AssemblyCompressionFormat format, byte[] data) + { + byte[] compressed; + switch (format) { + case AssemblyCompressionFormat.Lz4: { + compressed = new byte [LZ4Codec.MaximumOutputSize (data.Length)]; + int length = LZ4Codec.Encode (data, 0, data.Length, compressed, 0, compressed.Length); + Array.Resize (ref compressed, length); + break; + } + case AssemblyCompressionFormat.Zstandard: { + long maximumLength = ZstandardEncoder.GetMaxCompressedLength (data.Length); + compressed = new byte [checked ((int)maximumLength)]; + Assert.IsTrue (ZstandardEncoder.TryCompress (data, compressed, out int length, 3, 0)); + Array.Resize (ref compressed, length); + break; + } + default: + throw new NotSupportedException ($"Unsupported compression format '{format}'"); + } + + var output = new MemoryStream (); + using (var writer = new BinaryWriter (output, System.Text.Encoding.UTF8, leaveOpen: true)) { + writer.Write (format == AssemblyCompressionFormat.Lz4 ? 0x5A4C4158u : 0x535A4158u); + writer.Write (0u); + writer.Write ((uint)data.Length); + writer.Write (compressed); + } + output.Seek (0, SeekOrigin.Begin); + return output; + } +#endif // NET11_0_OR_GREATER + + static void CreateV1StoreSet (string indexStorePath, byte[] image, byte[]? commonImage = null) + { + const ulong Hash64 = 0x123456789abcdef0; + const uint Hash32 = 0x12345678; + const ulong CommonHash64 = 0xfedcba9876543210; + const uint CommonHash32 = 0x87654321; + string directory = Path.GetDirectoryName (indexStorePath) ?? ""; + string baseName = Path.GetFileNameWithoutExtension (indexStorePath); + uint globalEntryCount = commonImage == null ? 1u : 2u; + uint localEntryCount = commonImage == null ? 0u : 1u; + + using (FileStream file = File.Create (indexStorePath)) + using (var writer = new BinaryWriter (file)) { + WriteV1Header (writer, localEntryCount, globalEntryCount, storeId: 0); + if (commonImage != null) { + uint commonDataOffset = checked ((uint)( + 5 * sizeof (uint) + + 6 * sizeof (uint) + + globalEntryCount * 2 * (sizeof (ulong) + 3 * sizeof (uint)) + )); + writer.Write (commonDataOffset); + writer.Write ((uint)commonImage.Length); + writer.Write (0u); + writer.Write (0u); + writer.Write (0u); + writer.Write (0u); + } + WriteV1IndexEntry (writer, Hash32, storeId: 1); + if (commonImage != null) { + WriteV1IndexEntry (writer, CommonHash32, storeId: 0); + } + WriteV1IndexEntry (writer, Hash64, storeId: 1); + if (commonImage != null) { + WriteV1IndexEntry (writer, CommonHash64, storeId: 0); + writer.Write (commonImage); + } + } + + string archStorePath = Path.Combine (directory, $"{baseName}.arm64_v8a.blob"); + using (FileStream file = File.Create (archStorePath)) + using (var writer = new BinaryWriter (file)) { + const uint DataOffset = 5 * sizeof (uint) + 6 * sizeof (uint); + WriteV1Header (writer, localEntryCount: 1, globalEntryCount: 0, storeId: 1); + writer.Write (DataOffset); + writer.Write ((uint)image.Length); + writer.Write (0u); + writer.Write (0u); + writer.Write (0u); + writer.Write (0u); + writer.Write (image); + } + + string manifestPath = Path.Combine (directory, $"{baseName}.manifest"); + File.WriteAllText ( + manifestPath, + "Hash 32 Hash 64 Store ID Store idx Name\n" + + $"0x{Hash32:x8} 0x{Hash64:x16} 1 0 Test\n" + + (commonImage == null ? "" : $"0x{CommonHash32:x8} 0x{CommonHash64:x16} 0 0 Common\n") + ); + } + + static void WriteV1Header (BinaryWriter writer, uint localEntryCount, uint globalEntryCount, uint storeId) + { + writer.Write (0x41424158u); + writer.Write (1u); + writer.Write (localEntryCount); + writer.Write (globalEntryCount); + writer.Write (storeId); + } + + static void WriteV1IndexEntry (BinaryWriter writer, ulong hash, uint storeId) + { + writer.Write (hash); + writer.Write (0u); + writer.Write (0u); + writer.Write (storeId); + } + + static byte[] CreateV2Store (byte[] image, uint version) + { + byte[] name = "Test.dll"u8.ToArray (); + const int HeaderSize = 5 * sizeof (uint); + int indexSize = sizeof (ulong) + sizeof (uint) + (version >= 3 ? sizeof (byte) : 0); + const int DescriptorSize = 7 * sizeof (uint); + int dataOffset = HeaderSize + indexSize + DescriptorSize + sizeof (uint) + name.Length; + + using var output = new MemoryStream (); + using (var writer = new BinaryWriter (output, System.Text.Encoding.UTF8, leaveOpen: true)) { + writer.Write (0x41424158u); + writer.Write (0x80010000u | version); + writer.Write (1u); + writer.Write (1u); + writer.Write ((uint)indexSize); + writer.Write (0x123456789abcdef0ul); + writer.Write (0u); + if (version >= 3) { + writer.Write (false); + } + writer.Write (0u); + writer.Write ((uint)dataOffset); + writer.Write ((uint)image.Length); + writer.Write (0u); + writer.Write (0u); + writer.Write (0u); + writer.Write (0u); + writer.Write ((uint)name.Length); + writer.Write (name); + writer.Write (image); + } + return output.ToArray (); + } + + static void WriteEntry (ZipArchive archive, string path, byte[] data) + { + ZipArchiveEntry entry = archive.CreateEntry (path, CompressionLevel.NoCompression); + using Stream output = entry.Open (); + output.Write (data); + } + + static string CreateTemporaryDirectory () + { + string directory = Path.Combine (Path.GetTempPath (), $"assembly-store-tests-{Guid.NewGuid ():N}"); + Directory.CreateDirectory (directory); + return directory; + } + + static T RequireSingle (IEnumerable? values, string description) + { + if (values == null) { + throw new InvalidOperationException ($"Missing {description}"); + } + return values.Single (); + } +} diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyCompression.cs b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyCompression.cs new file mode 100644 index 00000000000..802e56d1cde --- /dev/null +++ b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyCompression.cs @@ -0,0 +1,153 @@ +using System; +using System.IO; + +#if NET11_0_OR_GREATER +using System.Buffers; +using K4os.Compression.LZ4; +using ZstandardDecoder = System.IO.Compression.ZstandardDecoder; +#endif // NET11_0_OR_GREATER + +namespace Xamarin.Android.AssemblyStore; + +public enum AssemblyCompressionFormat +{ + Lz4, + Zstandard, +} + +public static class AssemblyCompression +{ + const uint Lz4Magic = 0x5A4C4158; // 'XALZ', little-endian + const uint ZstandardMagic = 0x535A4158; // 'XAZS', little-endian + +#if NET11_0_OR_GREATER + const int HeaderSize = 3 * sizeof (uint); + const uint MaximumUncompressedAssemblySize = 512 * 1024 * 1024; + + static readonly ArrayPool bytePool = ArrayPool.Shared; + + public static bool TryDecompress (Stream input, Stream output, out AssemblyCompressionFormat format) + { + ArgumentNullException.ThrowIfNull (input); + ArgumentNullException.ThrowIfNull (output); + + if (!input.CanRead || !input.CanSeek) { + throw new ArgumentException ("Input stream must be readable and seekable", nameof (input)); + } + if (!output.CanWrite) { + throw new ArgumentException ("Output stream must be writable", nameof (output)); + } + + long start = input.Position; + if (!TryReadFormat (input, out format)) { + return false; + } + + if (input.Length - start < HeaderSize) { + throw new InvalidDataException ($"Truncated {format} assembly header"); + } + + using var reader = new BinaryReader (input, System.Text.Encoding.UTF8, leaveOpen: true); + reader.ReadUInt32 (); // descriptor index + uint uncompressedLength = reader.ReadUInt32 (); + if (uncompressedLength > MaximumUncompressedAssemblySize) { + throw new InvalidDataException ($"{format} assembly expands to an unsupported size of {uncompressedLength} bytes (maximum {MaximumUncompressedAssemblySize} bytes)"); + } + + long compressedLength = input.Length - input.Position; + if (compressedLength > Int32.MaxValue) { + throw new InvalidDataException ($"{format} assembly contains an unsupported compressed size of {compressedLength} bytes"); + } + + byte[]? compressedBytes = null; + byte[]? assemblyBytes = null; + try { + compressedBytes = bytePool.Rent ((int)compressedLength); + ReadFully (reader, compressedBytes, (int)compressedLength); + + assemblyBytes = bytePool.Rent ((int)uncompressedLength); + int decoded = format switch { + AssemblyCompressionFormat.Lz4 => LZ4Codec.Decode ( + compressedBytes, + 0, + (int)compressedLength, + assemblyBytes, + 0, + (int)uncompressedLength + ), + AssemblyCompressionFormat.Zstandard => ZstandardDecoder.TryDecompress ( + compressedBytes.AsSpan (0, (int)compressedLength), + assemblyBytes.AsSpan (0, (int)uncompressedLength), + out int bytesWritten + ) ? bytesWritten : -1, + _ => throw new InvalidOperationException ($"Unsupported compression format '{format}'"), + }; + + if (decoded != (int)uncompressedLength) { + throw new InvalidDataException ($"Failed to decompress {format} assembly data (decoded {decoded} of {uncompressedLength} bytes)"); + } + + output.Write (assemblyBytes, 0, decoded); + output.Flush (); + return true; + } finally { + if (compressedBytes != null) { + bytePool.Return (compressedBytes); + } + if (assemblyBytes != null) { + bytePool.Return (assemblyBytes); + } + } + } + + static void ReadFully (BinaryReader reader, byte[] destination, int count) + { + int totalRead = 0; + while (totalRead < count) { + int read = reader.Read (destination, totalRead, count - totalRead); + if (read == 0) { + throw new EndOfStreamException ("Unexpected end of compressed assembly data"); + } + totalRead += read; + } + } +#endif // NET11_0_OR_GREATER + + internal static bool IsCompressed (Stream input) + { + ArgumentNullException.ThrowIfNull (input); + + if (!input.CanRead || !input.CanSeek) { + throw new ArgumentException ("Input stream must be readable and seekable", nameof (input)); + } + + long start = input.Position; + bool compressed = TryReadFormat (input, out _); + input.Seek (start, SeekOrigin.Begin); + return compressed; + } + + static bool TryReadFormat (Stream input, out AssemblyCompressionFormat format) + { + long start = input.Position; + if (input.Length - start < sizeof (uint)) { + format = default; + return false; + } + + using var reader = new BinaryReader (input, System.Text.Encoding.UTF8, leaveOpen: true); + uint magic = reader.ReadUInt32 (); + switch (magic) { + case Lz4Magic: + format = AssemblyCompressionFormat.Lz4; + return true; + case ZstandardMagic: + format = AssemblyCompressionFormat.Zstandard; + return true; + default: + input.Seek (start, SeekOrigin.Begin); + format = default; + return false; + } + } +} diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStore.csproj b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStore.csproj new file mode 100644 index 00000000000..e15dd161b97 --- /dev/null +++ b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStore.csproj @@ -0,0 +1,28 @@ + + + + + Microsoft Corporation + 2026 Microsoft Corporation + $(DotNetStableTargetFramework);$(DotNetTargetFramework) + Xamarin.Android.AssemblyStore + Xamarin.Android.AssemblyStore + disable + enable + + + + + + + + + + + + + + + + + diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreExplorer.cs b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreExplorer.cs index d6a5a638302..1d6cc65c7ec 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreExplorer.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreExplorer.cs @@ -7,7 +7,7 @@ namespace Xamarin.Android.AssemblyStore; -class AssemblyStoreExplorer +public class AssemblyStoreExplorer { readonly AssemblyStoreReader reader; @@ -20,16 +20,14 @@ class AssemblyStoreExplorer public bool Is64Bit { get; } protected AssemblyStoreExplorer (Stream storeStream, string path) - { - StorePath = path; - var storeReader = AssemblyStoreReader.Create (storeStream, path); - if (storeReader == null) { - storeStream.Dispose (); - throw new NotSupportedException ($"Format of assembly store '{path}' is unsupported"); - } + : this (CreateReader (storeStream, path)) + {} + AssemblyStoreExplorer (AssemblyStoreReader storeReader) + { reader = storeReader; - TargetArch = reader.TargetArch; + StorePath = reader.StorePath; + TargetArch = reader.TargetArch == AndroidTargetArch.None ? null : reader.TargetArch; AssemblyCount = reader.AssemblyCount; IndexEntryCount = reader.IndexEntryCount; Assemblies = reader.Assemblies; @@ -42,6 +40,17 @@ protected AssemblyStoreExplorer (Stream storeStream, string path) AssembliesByName = dict.AsReadOnly (); } + static AssemblyStoreReader CreateReader (Stream storeStream, string path) + { + AssemblyStoreReader? storeReader = AssemblyStoreReader.Create (storeStream, path); + if (storeReader != null) { + return storeReader; + } + + storeStream.Dispose (); + throw new NotSupportedException ($"Format of assembly store '{path}' is unsupported"); + } + protected AssemblyStoreExplorer (FileInfo storeInfo) : this (storeInfo.OpenRead (), storeInfo.FullName) {} @@ -50,17 +59,39 @@ public static (IList? explorers, string? errorMessage) Op { (FileFormat format, FileInfo? info) = Utils.DetectFileFormat (inputFile); if (info == null) { + if (String.IsNullOrEmpty (Path.GetExtension (inputFile))) { + (IList? legacyExplorers, string? legacyError) = OpenV1 (inputFile); + if (legacyExplorers != null || legacyError != null) { + return (legacyExplorers, legacyError); + } + } + return (null, $"File '{inputFile}' does not exist."); } switch (format) { - case FileFormat.Unknown: + case FileFormat.Unknown: { + (IList? legacyExplorers, string? legacyError) = OpenV1 (inputFile); + if (legacyExplorers != null || legacyError != null) { + return (legacyExplorers, legacyError); + } return (null, $"File '{inputFile}' has an unknown format."); + } - case FileFormat.Zip: + case FileFormat.Zip: { + (IList? legacyExplorers, string? legacyError) = OpenV1 (inputFile); + if (legacyExplorers != null || legacyError != null) { + return (legacyExplorers, legacyError); + } return (null, $"File '{inputFile}' is a ZIP archive, but not an Android one."); + } case FileFormat.AssemblyStore: + if (IsV1Store (info)) { + return OpenV1 (inputFile); + } + return (new List { new AssemblyStoreExplorer (info)}, null); + case FileFormat.ELF: return (new List { new AssemblyStoreExplorer (info)}, null); @@ -80,52 +111,60 @@ public static (IList? explorers, string? errorMessage) Op static (IList? explorers, string? errorMessage) OpenAab (FileInfo fi) { - return OpenCommon ( - fi, - new List> { - StoreReader_V2.AabPaths, - StoreReader_V1.AabPaths, - } - ); + return OpenArchive (fi, StoreReader_V2.AabPaths); } static (IList? explorers, string? errorMessage) OpenAabBase (FileInfo fi) { - return OpenCommon ( - fi, - new List> { - StoreReader_V2.AabBasePaths, - StoreReader_V1.AabBasePaths, - } - ); + return OpenArchive (fi, StoreReader_V2.AabBasePaths); } static (IList? explorers, string? errorMessage) OpenApk (FileInfo fi) { - return OpenCommon ( - fi, - new List> { - StoreReader_V2.ApkPaths, - StoreReader_V1.ApkPaths, - } - ); + return OpenArchive (fi, StoreReader_V2.ApkPaths); } - static (IList? explorers, string? errorMessage) OpenCommon (FileInfo fi, List> pathLists) + static (IList? explorers, string? errorMessage) OpenArchive (FileInfo fi, IList paths) { - using var zip = ZipArchive.Open (fi.FullName, FileMode.Open); - IList? explorers; string? errorMessage; - bool pathsFound; - - foreach (IList paths in pathLists) { - (explorers, errorMessage, pathsFound) = TryLoad (fi, zip, paths); + using (var zip = ZipArchive.Open (fi.FullName, FileMode.Open)) { + (IList? explorers, string? loadError, bool pathsFound) = TryLoad (fi, zip, paths); if (pathsFound) { - return (explorers, errorMessage); + return (explorers, loadError); } + errorMessage = loadError; + } + + (IList? legacyExplorers, string? legacyError) = OpenV1 (fi.FullName); + if (legacyExplorers != null || legacyError != null) { + return (legacyExplorers, legacyError); + } + + return (null, errorMessage ?? "Unable to find any assembly store entries"); + } + + static (IList? explorers, string? errorMessage) OpenV1 (string inputFile) + { + (IList? readers, string? errorMessage) = StoreReader_V1.Open (inputFile); + if (readers == null) { + return (null, errorMessage); + } + + var explorers = new List (); + foreach (StoreReader_V1 reader in readers) { + explorers.Add (new AssemblyStoreExplorer (reader)); + } + return (explorers.AsReadOnly (), null); + } + + static bool IsV1Store (FileInfo info) + { + if (info.Length < 2 * sizeof (uint)) { + return false; } - return (null, "Unable to find any blob entries"); + using var reader = new BinaryReader (info.OpenRead ()); + return reader.ReadUInt32 () == Utils.ASSEMBLY_STORE_MAGIC && reader.ReadUInt32 () == 1; } static (IList? explorers, string? errorMessage, bool pathsFound) TryLoad (FileInfo fi, ZipArchive zip, IList paths) diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs index 7e5dbcfd1d5..fee67fdecbe 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs @@ -4,7 +4,7 @@ namespace Xamarin.Android.AssemblyStore; -abstract class AssemblyStoreItem +public abstract class AssemblyStoreItem { public string Name { get; } public IList Hashes { get; } diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreReader.cs b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreReader.cs index 982b62645e9..9174e86119f 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreReader.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreReader.cs @@ -31,12 +31,7 @@ protected AssemblyStoreReader (Stream store, string path) public static AssemblyStoreReader? Create (Stream store, string path) { - AssemblyStoreReader? reader = MakeReaderReady (new StoreReader_V1 (store, path)); - if (reader != null) { - return reader; - } - - reader = MakeReaderReady (new StoreReader_V2 (store, path)); + AssemblyStoreReader? reader = MakeReaderReady (new StoreReader_V2 (store, path)); if (reader != null) { return reader; } @@ -60,28 +55,58 @@ protected AssemblyStoreReader (Stream store, string path) protected abstract void Prepare (); protected abstract ulong GetStoreStartDataOffset (); - public Stream ReadEntryImageData (AssemblyStoreItem entry, bool uncompressIfNeeded = false) + public virtual Stream ReadEntryImageData (AssemblyStoreItem entry, bool uncompressIfNeeded = false) { ulong startOffset = GetStoreStartDataOffset (); - StoreStream.Seek ((uint)startOffset + entry.DataOffset, SeekOrigin.Begin); + StoreStream.Seek (checked ((long)startOffset + entry.DataOffset), SeekOrigin.Begin); var stream = new MemoryStream (); - if (uncompressIfNeeded) { - throw new NotImplementedException (); - } - const long BufferSize = 65535; byte[] buffer = Utils.BytePool.Rent ((int)BufferSize); - long remainingToRead = entry.DataSize; - - while (remainingToRead > 0) { - int nread = StoreStream.Read (buffer, 0, (int)Math.Min (BufferSize, remainingToRead)); - stream.Write (buffer, 0, nread); - remainingToRead -= (long)nread; + try { + long remainingToRead = entry.DataSize; + while (remainingToRead > 0) { + int nread = StoreStream.Read (buffer, 0, (int)Math.Min (BufferSize, remainingToRead)); + if (nread == 0) { + throw new EndOfStreamException ($"Unexpected end of assembly store '{StorePath}' while reading '{entry.Name}'"); + } + + stream.Write (buffer, 0, nread); + remainingToRead -= nread; + } + } finally { + Utils.BytePool.Return (buffer); } + stream.Flush (); stream.Seek (0, SeekOrigin.Begin); + return UncompressIfNeeded (stream, uncompressIfNeeded); + } + protected static Stream UncompressIfNeeded (MemoryStream stream, bool uncompressIfNeeded) + { + if (!uncompressIfNeeded) { + return stream; + } + +#if NET11_0_OR_GREATER + var output = new MemoryStream (); + if (AssemblyCompression.TryDecompress (stream, output, out _)) { + stream.Dispose (); + output.Seek (0, SeekOrigin.Begin); + return output; + } + + output.Dispose (); + stream.Seek (0, SeekOrigin.Begin); return stream; +#else // !NET11_0_OR_GREATER + if (!AssemblyCompression.IsCompressed (stream)) { + return stream; + } + + stream.Dispose (); + throw new NotSupportedException ("Assembly decompression requires .NET 11 or later"); +#endif // !NET11_0_OR_GREATER } } diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/ELFPayloadError.cs b/tools/assembly-store-reader-mk2/AssemblyStore/ELFPayloadError.cs index 932f151c80f..4b2c42127d2 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/ELFPayloadError.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/ELFPayloadError.cs @@ -7,5 +7,6 @@ enum ELFPayloadError LoadFailed, NotSharedLibrary, NotLittleEndian, + InvalidPayloadSymbol, NoPayloadSection, } diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V1.cs b/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V1.cs index f60fa3a067b..3713f997096 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V1.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V1.cs @@ -1,6 +1,10 @@ +using System; using System.Collections.Generic; using System.IO; +using Xamarin.Android.Tools; +using Legacy = Xamarin.Android.AssemblyStore.V1; + namespace Xamarin.Android.AssemblyStore; class StoreReader_V1 : AssemblyStoreReader @@ -8,24 +12,95 @@ class StoreReader_V1 : AssemblyStoreReader public override string Description => "Assembly store v1"; public override bool NeedsExtensionInName => false; - public static IList ApkPaths { get; } - public static IList AabPaths { get; } - public static IList AabBasePaths { get; } + readonly Dictionary sourceAssemblies = new Dictionary (); - static StoreReader_V1 () + StoreReader_V1 (IList stores, AndroidTargetArch targetArch, string storePath) + : base (Stream.Null, storePath) { - ApkPaths = new List ().AsReadOnly (); - AabPaths = new List ().AsReadOnly (); - AabBasePaths = new List ().AsReadOnly (); + TargetArch = targetArch; + Is64Bit = TargetArch == AndroidTargetArch.Arm64 || TargetArch == AndroidTargetArch.X86_64; + + var items = new List (); + uint indexEntryCount = 0; + foreach (Legacy.AssemblyStoreReader store in stores) { + indexEntryCount += (uint)(store.GlobalIndex32.Count + store.GlobalIndex64.Count); + foreach (Legacy.AssemblyStoreAssembly assembly in store.Assemblies) { + var item = new StoreItem_V1 (assembly, TargetArch, Is64Bit); + items.Add (item); + sourceAssemblies.Add (item, assembly); + } + } + + Assemblies = items.AsReadOnly (); + AssemblyCount = (uint)items.Count; + IndexEntryCount = indexEntryCount; } - public StoreReader_V1 (Stream store, string path) - : base (store, path) - {} + public static (IList? readers, string? errorMessage) Open (string inputFile) + { + var errors = new List (); + var explorer = new Legacy.AssemblyStoreExplorer ( + inputFile, + (level, message) => { + if (level == Legacy.AssemblyStoreExplorerLogLevel.Error) { + errors.Add (message); + } + }, + keepStoreInMemory: true + ); + + if (errors.Count > 0) { + return (null, String.Join (Environment.NewLine, errors)); + } + + var commonStores = new List (); + var storesByArch = new Dictionary> (); + foreach (var storeGroup in explorer.Stores) { + foreach (Legacy.AssemblyStoreReader store in storeGroup.Value) { + AndroidTargetArch targetArch = GetTargetArch (store.Arch); + if (targetArch == AndroidTargetArch.None) { + commonStores.Add (store); + } else { + if (!storesByArch.TryGetValue (targetArch, out List? stores)) { + stores = new List (); + storesByArch.Add (targetArch, stores); + } + stores.Add (store); + } + } + } + + var readers = new List (); + foreach (var archStores in storesByArch) { + var stores = new List (commonStores); + stores.AddRange (archStores.Value); + readers.Add (new StoreReader_V1 (stores, archStores.Key, $"{explorer.StorePath}!{archStores.Key}")); + } + + if (storesByArch.Count == 0 && HasAssemblies (commonStores)) { + readers.Add (new StoreReader_V1 (commonStores, AndroidTargetArch.None, $"{explorer.StorePath}!shared")); + } + + if (readers.Count == 0) { + return (null, null); + } + + return (readers.AsReadOnly (), null); + + static bool HasAssemblies (List stores) + { + foreach (Legacy.AssemblyStoreReader store in stores) { + if (store.Assemblies.Count > 0) { + return true; + } + } + return false; + } + } protected override bool IsSupported () { - return false; + return true; } protected override void Prepare () @@ -33,4 +108,64 @@ protected override void Prepare () } protected override ulong GetStoreStartDataOffset () => 0; + + public override Stream ReadEntryImageData (AssemblyStoreItem entry, bool uncompressIfNeeded = false) + { + if (!sourceAssemblies.TryGetValue (entry, out Legacy.AssemblyStoreAssembly? assembly)) { + throw new ArgumentException ($"Assembly '{entry.Name}' does not belong to store '{StorePath}'", nameof (entry)); + } + + var stream = new MemoryStream (); + assembly.ExtractImage (stream); + stream.Seek (0, SeekOrigin.Begin); + return UncompressIfNeeded (stream, uncompressIfNeeded); + } + + static AndroidTargetArch GetTargetArch (string arch) + { + return arch.Replace ('-', '_').ToLowerInvariant () switch { + "" => AndroidTargetArch.None, + "arm64_v8a" => AndroidTargetArch.Arm64, + "armeabi_v7a" => AndroidTargetArch.Arm, + "x86_64" => AndroidTargetArch.X86_64, + "x86" => AndroidTargetArch.X86, + _ => AndroidTargetArch.Other, + }; + } + + sealed class StoreItem_V1 : AssemblyStoreItem + { + public StoreItem_V1 (Legacy.AssemblyStoreAssembly assembly, AndroidTargetArch targetArch, bool is64Bit) + : base (GetName (assembly), is64Bit, GetHashes (assembly), ignore: false) + { + DataOffset = assembly.DataOffset; + DataSize = assembly.DataSize; + DebugOffset = assembly.DebugDataOffset; + DebugSize = assembly.DebugDataSize; + ConfigOffset = assembly.ConfigDataOffset; + ConfigSize = assembly.ConfigDataSize; + TargetArch = targetArch; + } + + static string GetName (Legacy.AssemblyStoreAssembly assembly) + { + if (!String.IsNullOrEmpty (assembly.Name)) { + return assembly.Name; + } + + return $"{assembly.Store.StoreID}_{assembly.DataOffset:x}_{assembly.Hash32:x8}_{assembly.Hash64:x16}"; + } + + static List GetHashes (Legacy.AssemblyStoreAssembly assembly) + { + var hashes = new List (); + if (assembly.Hash32 != 0) { + hashes.Add (assembly.Hash32); + } + if (assembly.Hash64 != 0) { + hashes.Add (assembly.Hash64); + } + return hashes; + } + } } diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs b/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs index e8a77a1e102..be180f517e5 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs @@ -32,6 +32,9 @@ public Header (uint magic, uint version, uint entry_count, uint index_entry_coun sealed class IndexEntry { + public const uint NativeSize32_V2 = 2 * sizeof (uint); + public const uint NativeSize64_V2 = sizeof (ulong) + sizeof (uint); + // We treat `bool` as `byte` here, since that's what gets written to the binary. public const uint NativeSize32 = 2 * sizeof (uint) + sizeof (byte); public const uint NativeSize64 = sizeof (ulong) + sizeof (uint) + sizeof (byte); diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs b/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs index d8de129f727..c8b162e442b 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs @@ -11,10 +11,12 @@ namespace Xamarin.Android.AssemblyStore; partial class StoreReader_V2 : AssemblyStoreReader { // Bit 31 is set for 64-bit platforms, cleared for the 32-bit ones - const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000003; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant - const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000003; - const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT = 0x80000004; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant - const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT = 0x00000004; + const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT_V2 = 0x80000002; + const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT_V2 = 0x00000002; + const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT_V3 = 0x80000003; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant + const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT_V3 = 0x00000003; + const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT_V4 = 0x80000004; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant + const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT_V4 = 0x00000004; const uint ASSEMBLY_STORE_FORMAT_VERSION_MASK = 0xF0000000; const uint ASSEMBLY_STORE_FORMAT_NUMBER_MASK = 0x0000FFFF; @@ -38,38 +40,40 @@ partial class StoreReader_V2 : AssemblyStoreReader static StoreReader_V2 () { - var paths = new List { - GetArchPath (AndroidTargetArch.Arm64), - GetArchPath (AndroidTargetArch.Arm), - GetArchPath (AndroidTargetArch.X86_64), - GetArchPath (AndroidTargetArch.X86), - }; + var paths = new List (); + AddArchPaths (paths, AndroidTargetArch.Arm64); + AddArchPaths (paths, AndroidTargetArch.Arm); + AddArchPaths (paths, AndroidTargetArch.X86_64); + AddArchPaths (paths, AndroidTargetArch.X86); ApkPaths = paths.AsReadOnly (); AabBasePaths = ApkPaths; const string AabBaseDir = "base"; - paths = new List { - GetArchPath (AndroidTargetArch.Arm64, AabBaseDir), - GetArchPath (AndroidTargetArch.Arm, AabBaseDir), - GetArchPath (AndroidTargetArch.X86_64, AabBaseDir), - GetArchPath (AndroidTargetArch.X86, AabBaseDir), - }; + paths = new List (); + AddArchPaths (paths, AndroidTargetArch.Arm64, AabBaseDir); + AddArchPaths (paths, AndroidTargetArch.Arm, AabBaseDir); + AddArchPaths (paths, AndroidTargetArch.X86_64, AabBaseDir); + AddArchPaths (paths, AndroidTargetArch.X86, AabBaseDir); AabPaths = paths.AsReadOnly (); - string GetArchPath (AndroidTargetArch arch, string? root = null) + void AddArchPaths (List targetPaths, AndroidTargetArch arch, string? root = null) { - const string LibDirName = "lib"; - string abi = MonoAndroidHelper.ArchToAbi (arch); - var parts = new List (); + targetPaths.Add (GetArchPath (abi, "libassembly-store.so", root)); + targetPaths.Add (GetArchPath (abi, $"libassemblies.{abi}.blob.so", root)); + } + + string GetArchPath (string abi, string fileName, string? root) + { + const string LibDirName = "lib"; + var parts = new List (); if (!String.IsNullOrEmpty (root)) { parts.Add (LibDirName); } else { root = LibDirName; } parts.Add (abi); - parts.Add ("libassembly-store.so"); - + parts.Add (fileName); return MonoAndroidHelper.MakeZipArchivePath (root, parts); } } @@ -78,14 +82,18 @@ public StoreReader_V2 (Stream store, string path) : base (store, path) { supportedVersions = new HashSet { - ASSEMBLY_STORE_FORMAT_VERSION_64BIT | ASSEMBLY_STORE_ABI_AARCH64, - ASSEMBLY_STORE_FORMAT_VERSION_64BIT | ASSEMBLY_STORE_ABI_X64, - ASSEMBLY_STORE_FORMAT_VERSION_32BIT | ASSEMBLY_STORE_ABI_ARM, - ASSEMBLY_STORE_FORMAT_VERSION_32BIT | ASSEMBLY_STORE_ABI_X86, - ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT | ASSEMBLY_STORE_ABI_AARCH64, - ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT | ASSEMBLY_STORE_ABI_X64, - ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT | ASSEMBLY_STORE_ABI_ARM, - ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT | ASSEMBLY_STORE_ABI_X86, + ASSEMBLY_STORE_FORMAT_VERSION_64BIT_V2 | ASSEMBLY_STORE_ABI_AARCH64, + ASSEMBLY_STORE_FORMAT_VERSION_64BIT_V2 | ASSEMBLY_STORE_ABI_X64, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT_V2 | ASSEMBLY_STORE_ABI_ARM, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT_V2 | ASSEMBLY_STORE_ABI_X86, + ASSEMBLY_STORE_FORMAT_VERSION_64BIT_V3 | ASSEMBLY_STORE_ABI_AARCH64, + ASSEMBLY_STORE_FORMAT_VERSION_64BIT_V3 | ASSEMBLY_STORE_ABI_X64, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT_V3 | ASSEMBLY_STORE_ABI_ARM, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT_V3 | ASSEMBLY_STORE_ABI_X86, + ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT_V4 | ASSEMBLY_STORE_ABI_AARCH64, + ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT_V4 | ASSEMBLY_STORE_ABI_X64, + ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT_V4 | ASSEMBLY_STORE_ABI_ARM, + ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT_V4 | ASSEMBLY_STORE_ABI_X86, }; } @@ -99,7 +107,7 @@ protected override bool IsSupported () uint magic = reader.ReadUInt32 (); if (magic == Utils.ELF_MAGIC) { ELFPayloadError error; - (elfOffset, _, error) = Utils.FindELFPayloadSectionOffsetAndSize (StoreStream); + (elfOffset, _, error) = Utils.FindELFPayloadOffsetAndSize (StoreStream); if (error != ELFPayloadError.None) { string message = error switch { @@ -107,7 +115,8 @@ protected override bool IsSupported () ELFPayloadError.LoadFailed => $"Store '{StorePath}' could not be loaded", ELFPayloadError.NotSharedLibrary => $"Store '{StorePath}' is not a shared ELF library", ELFPayloadError.NotLittleEndian => $"Store '{StorePath}' is not a little-endian ELF image", - ELFPayloadError.NoPayloadSection => $"Store '{StorePath}' does not contain the 'payload' section", + ELFPayloadError.InvalidPayloadSymbol => $"Store '{StorePath}' has an invalid '_assembly_store' symbol", + ELFPayloadError.NoPayloadSection => $"Store '{StorePath}' does not contain the 'payload' section", _ => $"Unknown ELF payload section error for store '{StorePath}': {error}" }; Log.Debug (message); @@ -162,16 +171,25 @@ protected override void Prepare () var index = new List (); for (uint i = 0; i < header.index_entry_count; i++) { ulong name_hash; + bool hasIgnoreFlag; if (indexEntrySize == IndexEntry.NativeSize64) { name_hash = reader.ReadUInt64 (); + hasIgnoreFlag = true; } else if (indexEntrySize == IndexEntry.NativeSize32) { name_hash = (ulong)reader.ReadUInt32 (); + hasIgnoreFlag = true; + } else if (indexEntrySize == IndexEntry.NativeSize64_V2) { + name_hash = reader.ReadUInt64 (); + hasIgnoreFlag = false; + } else if (indexEntrySize == IndexEntry.NativeSize32_V2) { + name_hash = (ulong)reader.ReadUInt32 (); + hasIgnoreFlag = false; } else { throw new InvalidOperationException ($"Assembly store '{StorePath}' index entry size {indexEntrySize} is not supported."); } uint descriptor_index = reader.ReadUInt32 (); - bool ignore = reader.ReadByte () != 0; + bool ignore = hasIgnoreFlag && reader.ReadByte () != 0; index.Add (new IndexEntry (name_hash, descriptor_index, ignore)); } diff --git a/tools/assembly-store-reader-mk2/AssemblyStore/Utils.cs b/tools/assembly-store-reader-mk2/AssemblyStore/Utils.cs index 284f501c9ac..3b502ebaf62 100644 --- a/tools/assembly-store-reader-mk2/AssemblyStore/Utils.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/Utils.cs @@ -10,6 +10,8 @@ namespace Xamarin.Android.AssemblyStore; static class Utils { + const string AssemblyStorePayloadSymbol = "_assembly_store"; + static readonly string[] aabZipEntries = { "base/manifest/AndroidManifest.xml", "BundleConfig.pb", @@ -30,6 +32,9 @@ static class Utils public static readonly ArrayPool BytePool = ArrayPool.Shared; public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadSectionOffsetAndSize (Stream stream) + => FindELFPayloadOffsetAndSize (stream); + + public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadOffsetAndSize (Stream stream) { stream.Seek (0, SeekOrigin.Begin); Class elfClass = ELFReader.CheckELFType (stream); @@ -49,6 +54,15 @@ public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadSe return ReturnError (elf, ELFPayloadError.NotLittleEndian); } + if (TryGetPayloadSymbolOffsetAndSize (elf, out ulong symbolOffset, out ulong symbolSize, out bool symbolFound)) { + elf.Dispose (); + return (symbolOffset, symbolSize, ELFPayloadError.None); + } + + if (symbolFound) { + return ReturnError (elf, ELFPayloadError.InvalidPayloadSymbol); + } + if (!elf.TryGetSection ("payload", out ISection? payloadSection)) { return ReturnError (elf, ELFPayloadError.NoPayloadSection); } @@ -75,6 +89,90 @@ public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadSe elf.Dispose (); return (offset, size, ELFPayloadError.None); + bool TryGetPayloadSymbolOffsetAndSize (IELF image, out ulong payloadOffset, out ulong payloadSize, out bool payloadSymbolFound) + { + payloadOffset = 0; + payloadSize = 0; + payloadSymbolFound = false; + + if (!image.TryGetSection (".dynsym", out ISection? section) || section is not ISymbolTable symbols) { + return false; + } + + foreach (ISymbolEntry symbol in symbols.Entries) { + if (String.CompareOrdinal (symbol.Name, AssemblyStorePayloadSymbol) != 0) { + continue; + } + + payloadSymbolFound = true; + return symbol switch { + SymbolEntry symbol64 => TryGetOffsetAndSize64 (symbol64, out payloadOffset, out payloadSize), + SymbolEntry symbol32 => TryGetOffsetAndSize32 (symbol32, out payloadOffset, out payloadSize), + _ => false, + }; + } + + return false; + } + + bool TryGetOffsetAndSize64 (SymbolEntry symbol, out ulong payloadOffset, out ulong payloadSize) + { + payloadOffset = 0; + payloadSize = 0; + + if (symbol.PointedSection is not Section section || symbol.Value < section.LoadAddress) { + return false; + } + + ulong sectionOffset = symbol.Value - section.LoadAddress; + if (sectionOffset >= section.Size) { + return false; + } + + ulong availableSize = section.Size - sectionOffset; + if (symbol.Size > availableSize) { + return false; + } + + payloadOffset = section.Offset + sectionOffset; + if (symbol.Size == 0) { + Log.Debug ($"ELF symbol '{AssemblyStorePayloadSymbol}' has size 0; using the remaining {availableSize} bytes in section '{section.Name}'"); + payloadSize = availableSize; + } else { + payloadSize = symbol.Size; + } + return true; + } + + bool TryGetOffsetAndSize32 (SymbolEntry symbol, out ulong payloadOffset, out ulong payloadSize) + { + payloadOffset = 0; + payloadSize = 0; + + if (symbol.PointedSection is not Section section || symbol.Value < section.LoadAddress) { + return false; + } + + uint sectionOffset = symbol.Value - section.LoadAddress; + if (sectionOffset >= section.Size) { + return false; + } + + uint availableSize = section.Size - sectionOffset; + if (symbol.Size > availableSize) { + return false; + } + + payloadOffset = (ulong)section.Offset + sectionOffset; + if (symbol.Size == 0) { + Log.Debug ($"ELF symbol '{AssemblyStorePayloadSymbol}' has size 0; using the remaining {availableSize} bytes in section '{section.Name}'"); + payloadSize = availableSize; + } else { + payloadSize = symbol.Size; + } + return true; + } + (ulong offset, ulong size) GetOffsetAndSize64 (Section payload) { return (payload.Offset, payload.Size); @@ -103,6 +201,9 @@ public static (FileFormat format, FileInfo? info) DetectFileFormat (string path) if (!info.Exists) { return (FileFormat.Unknown, null); } + if (info.Length < sizeof (uint)) { + return (FileFormat.Unknown, info); + } using var reader = new BinaryReader (info.OpenRead ()); diff --git a/tools/assembly-store-reader/AssemblyStoreAssembly.cs b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreAssembly.cs similarity index 98% rename from tools/assembly-store-reader/AssemblyStoreAssembly.cs rename to tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreAssembly.cs index b83be2acbf4..9a1edb41eea 100644 --- a/tools/assembly-store-reader/AssemblyStoreAssembly.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreAssembly.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace Xamarin.Android.AssemblyStore +namespace Xamarin.Android.AssemblyStore.V1 { class AssemblyStoreAssembly { diff --git a/tools/assembly-store-reader/AssemblyStoreExplorer.cs b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreExplorer.cs similarity index 99% rename from tools/assembly-store-reader/AssemblyStoreExplorer.cs rename to tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreExplorer.cs index 58afb529ce4..85ebcd22264 100644 --- a/tools/assembly-store-reader/AssemblyStoreExplorer.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreExplorer.cs @@ -4,7 +4,7 @@ using Xamarin.Tools.Zip; -namespace Xamarin.Android.AssemblyStore +namespace Xamarin.Android.AssemblyStore.V1 { class AssemblyStoreExplorer { diff --git a/tools/assembly-store-reader/AssemblyStoreExplorerLogLevel.cs b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreExplorerLogLevel.cs similarity index 65% rename from tools/assembly-store-reader/AssemblyStoreExplorerLogLevel.cs rename to tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreExplorerLogLevel.cs index 19452be507b..37ebe928891 100644 --- a/tools/assembly-store-reader/AssemblyStoreExplorerLogLevel.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreExplorerLogLevel.cs @@ -1,4 +1,4 @@ -namespace Xamarin.Android.AssemblyStore +namespace Xamarin.Android.AssemblyStore.V1 { enum AssemblyStoreExplorerLogLevel { diff --git a/tools/assembly-store-reader/AssemblyStoreHashEntry.cs b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreHashEntry.cs similarity index 92% rename from tools/assembly-store-reader/AssemblyStoreHashEntry.cs rename to tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreHashEntry.cs index b4504778807..3b6b0fc48ff 100644 --- a/tools/assembly-store-reader/AssemblyStoreHashEntry.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreHashEntry.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace Xamarin.Android.AssemblyStore +namespace Xamarin.Android.AssemblyStore.V1 { class AssemblyStoreHashEntry { diff --git a/tools/assembly-store-reader/AssemblyStoreManifestEntry.cs b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreManifestEntry.cs similarity index 97% rename from tools/assembly-store-reader/AssemblyStoreManifestEntry.cs rename to tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreManifestEntry.cs index ac7dc6e8d00..a5195e2ac42 100644 --- a/tools/assembly-store-reader/AssemblyStoreManifestEntry.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreManifestEntry.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; -namespace Xamarin.Android.AssemblyStore +namespace Xamarin.Android.AssemblyStore.V1 { class AssemblyStoreManifestEntry { diff --git a/tools/assembly-store-reader/AssemblyStoreManifestReader.cs b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreManifestReader.cs similarity index 97% rename from tools/assembly-store-reader/AssemblyStoreManifestReader.cs rename to tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreManifestReader.cs index 53a8a3c2e19..5c16800bfdb 100644 --- a/tools/assembly-store-reader/AssemblyStoreManifestReader.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreManifestReader.cs @@ -3,7 +3,7 @@ using System.IO; using System.Text; -namespace Xamarin.Android.AssemblyStore +namespace Xamarin.Android.AssemblyStore.V1 { class AssemblyStoreManifestReader { diff --git a/tools/assembly-store-reader/AssemblyStoreReader.cs b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreReader.cs similarity index 90% rename from tools/assembly-store-reader/AssemblyStoreReader.cs rename to tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreReader.cs index 7ab0e91f0a3..9111c948ace 100644 --- a/tools/assembly-store-reader/AssemblyStoreReader.cs +++ b/tools/assembly-store-reader-mk2/AssemblyStore/V1/AssemblyStoreReader.cs @@ -4,7 +4,7 @@ using System.IO; using System.Text; -namespace Xamarin.Android.AssemblyStore +namespace Xamarin.Android.AssemblyStore.V1 { class AssemblyStoreReader { @@ -104,20 +104,28 @@ void SaveDataToStream (Stream output, uint offset, uint size) EnsureStoreDataAvailable (); ArrayPool pool = ArrayPool.Shared; - storeData!.Seek (offset, SeekOrigin.Begin); + MemoryStream? data = storeData; + if (data == null) { + throw new InvalidOperationException ("Store data not available"); + } + + data.Seek (offset, SeekOrigin.Begin); byte[] buf = pool.Rent (16384); - int nread; - long toRead = size; - while (toRead > 0 && (nread = storeData.Read (buf, 0, buf.Length)) > 0) { - if (nread > toRead) { - nread = (int)toRead; + try { + long toRead = size; + while (toRead > 0) { + int nread = data.Read (buf, 0, (int)Math.Min (buf.Length, toRead)); + if (nread == 0) { + throw new EndOfStreamException ($"Unexpected end of assembly store while reading {size} bytes at offset {offset}"); + } + + output.Write (buf, 0, nread); + toRead -= nread; } - - output.Write (buf, 0, nread); - toRead -= nread; + output.Flush (); + } finally { + pool.Return (buf); } - output.Flush (); - pool.Return (buf); } void EnsureStoreDataAvailable () diff --git a/tools/assembly-store-reader-mk2/Directory.Build.targets b/tools/assembly-store-reader-mk2/Directory.Build.targets index e58eed5ca2c..11acff32947 100644 --- a/tools/assembly-store-reader-mk2/Directory.Build.targets +++ b/tools/assembly-store-reader-mk2/Directory.Build.targets @@ -1,6 +1,7 @@ - + AfterTargets="Build" + Condition="'$(OutputType)' == 'Exe'"> + diff --git a/tools/assembly-store-reader-mk2/Main.cs b/tools/assembly-store-reader-mk2/Main.cs index 89cb2f60bb0..3626e7744cc 100644 --- a/tools/assembly-store-reader-mk2/Main.cs +++ b/tools/assembly-store-reader-mk2/Main.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using Mono.Options; @@ -92,11 +91,6 @@ static int Main (string[] args) } string inputFile = theRest[0]; - (FileFormat format, FileInfo? info) = Utils.DetectFileFormat (inputFile); - if (info == null) { - return WriteErrorAndReturn ($"File '{inputFile}' does not exist."); - } - (IList? explorers, string? errorMessage) = AssemblyStoreExplorer.Open (inputFile); if (explorers == null) { return WriteErrorAndReturn (errorMessage ?? "Unknown error"); diff --git a/tools/assembly-store-reader-mk2/assembly-store-reader.csproj b/tools/assembly-store-reader-mk2/assembly-store-reader.csproj index 1a760876dfc..a70e8504173 100644 --- a/tools/assembly-store-reader-mk2/assembly-store-reader.csproj +++ b/tools/assembly-store-reader-mk2/assembly-store-reader.csproj @@ -8,7 +8,7 @@ false ../../bin/$(Configuration)/bin/assembly-store-reader Exe - $(DotNetStableTargetFramework) + $(DotNetTargetFramework) Xamarin.Android.AssemblyStoreReader disable enable @@ -22,10 +22,13 @@ + + + diff --git a/tools/assembly-store-reader/Program.cs b/tools/assembly-store-reader/Program.cs index 84dbe5703c6..90d1449bc03 100644 --- a/tools/assembly-store-reader/Program.cs +++ b/tools/assembly-store-reader/Program.cs @@ -4,6 +4,7 @@ using System.Text; using Mono.Options; +using Xamarin.Android.AssemblyStore.V1; namespace Xamarin.Android.AssemblyStore { diff --git a/tools/assembly-store-reader/assembly-store-reader.csproj b/tools/assembly-store-reader/assembly-store-reader.csproj index c2b4c58ff21..0e68dc702b6 100644 --- a/tools/assembly-store-reader/assembly-store-reader.csproj +++ b/tools/assembly-store-reader/assembly-store-reader.csproj @@ -14,6 +14,10 @@ enable + + + + diff --git a/tools/decompress-assemblies/decompress-assemblies.csproj b/tools/decompress-assemblies/decompress-assemblies.csproj index cf59128cbc2..458582c7463 100644 --- a/tools/decompress-assemblies/decompress-assemblies.csproj +++ b/tools/decompress-assemblies/decompress-assemblies.csproj @@ -17,7 +17,12 @@ - + + + + + + diff --git a/tools/decompress-assemblies/main.cs b/tools/decompress-assemblies/main.cs index 5efe0837a7e..6a64030454d 100644 --- a/tools/decompress-assemblies/main.cs +++ b/tools/decompress-assemblies/main.cs @@ -1,19 +1,16 @@ using System; -using System.Buffers; +using System.Collections.Generic; using System.IO; -using Xamarin.Tools.Zip; using Xamarin.Android.AssemblyStore; -using ZstandardDecoder = System.IO.Compression.ZstandardDecoder; +using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; +using Xamarin.Tools.Zip; namespace Xamarin.Android.Tools.DecompressAssemblies { class App { - const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian - - static readonly ArrayPool bytePool = ArrayPool.Shared; - static int Usage () { Console.WriteLine ("Usage: decompress-assemblies {file.{dll,apk,aab}} [{file.{dll,apk,aab} ...]"); @@ -26,53 +23,32 @@ static int Usage () static bool UncompressDLL (Stream inputStream, string fileName, string filePath, string prefix) { string outputFile = $"{prefix}{filePath}"; - bool retVal = true; - Console.WriteLine ($"Processing {fileName}"); - // - // Zstd compressed assembly header format: - // uint magic; // 0x535A4158; 'XAZS', little-endian - // uint descriptor_index; // Index into an internal assembly descriptor table - // uint uncompressed_length; // Size of assembly, uncompressed - // - using (var reader = new BinaryReader (inputStream)) { - uint magic = reader.ReadUInt32 (); - if (magic == CompressedDataMagic) { - reader.ReadUInt32 (); // descriptor index, ignore - uint decompressedLength = reader.ReadUInt32 (); - - int inputLength = (int)(inputStream.Length - 12); - byte[] sourceBytes = bytePool.Rent (inputLength); - reader.Read (sourceBytes, 0, inputLength); - - byte[] assemblyBytes = bytePool.Rent ((int)decompressedLength); - int decoded = ZstandardDecoder.TryDecompress ( - sourceBytes.AsSpan (0, inputLength), - assemblyBytes.AsSpan (0, (int)decompressedLength), - out int bytesWritten) ? bytesWritten : -1; - if (decoded != (int)decompressedLength) { - Console.Error.WriteLine ($" Failed to decompress Zstd data of {fileName} (decoded: {decoded})"); - retVal = false; - } else { - string? outputDir = Path.GetDirectoryName (outputFile); - if (!String.IsNullOrEmpty (outputDir)) { - Directory.CreateDirectory (outputDir); - } - using (var fs = File.Open (outputFile, FileMode.Create, FileAccess.Write)) { - fs.Write (assemblyBytes, 0, decoded); - fs.Flush (); - } - Console.WriteLine ($" uncompressed to: {outputFile}"); - } - bytePool.Return (sourceBytes); - bytePool.Return (assemblyBytes); - } else { - Console.WriteLine ($" assembly is not compressed"); + using var assemblyStream = new MemoryStream (); + AssemblyCompressionFormat format; + try { + if (!AssemblyCompression.TryDecompress (inputStream, assemblyStream, out format)) { + Console.WriteLine (" assembly is not compressed"); + return true; } + } catch (InvalidDataException e) { + Console.Error.WriteLine ($" Failed to decompress {fileName}: {e.Message}"); + return false; } - return retVal; + string? outputDir = Path.GetDirectoryName (outputFile); + if (!String.IsNullOrEmpty (outputDir)) { + Directory.CreateDirectory (outputDir); + } + + assemblyStream.Seek (0, SeekOrigin.Begin); + using (var output = File.Open (outputFile, FileMode.Create, FileAccess.Write)) { + assemblyStream.CopyTo (output); + output.Flush (); + } + Console.WriteLine ($" {format} assembly uncompressed to: {outputFile}"); + return true; } static bool UncompressDLL (string filePath, string prefix) @@ -84,6 +60,7 @@ static bool UncompressDLL (string filePath, string prefix) static bool UncompressFromAPK_IndividualEntries (ZipArchive apk, string filePath, string assembliesPath, string prefix) { + bool retVal = true; foreach (ZipEntry entry in apk) { if (!entry.FullName.StartsWith (assembliesPath, StringComparison.Ordinal)) { continue; @@ -97,43 +74,91 @@ static bool UncompressFromAPK_IndividualEntries (ZipArchive apk, string filePath entry.Extract (stream); stream.Seek (0, SeekOrigin.Begin); string fileName = entry.FullName.Substring (assembliesPath.Length); - UncompressDLL (stream, $"{filePath}!{entry.FullName}", fileName, prefix); + retVal &= UncompressDLL (stream, $"{filePath}!{entry.FullName}", fileName, prefix); } } - return true; + return retVal; } static bool UncompressFromAPK_AssemblyStores (string filePath, string prefix) { - var explorer = new AssemblyStoreExplorer (filePath, keepStoreInMemory: true); - foreach (AssemblyStoreAssembly assembly in explorer.Assemblies) { - string assemblyName = assembly.DllName; + (IList? stores, string? errorMessage) = AssemblyStoreExplorer.Open (filePath); + if (stores == null) { + Console.Error.WriteLine (errorMessage ?? $"Unable to read assembly stores from '{filePath}'"); + return false; + } - if (!String.IsNullOrEmpty (assembly.Store.Arch)) { - assemblyName = $"{assembly.Store.Arch}/{assemblyName}"; + bool retVal = true; + foreach (AssemblyStoreExplorer store in stores) { + string? abi = null; + if (store.TargetArch.HasValue && !TryGetAndroidAbi (store.TargetArch.Value, out abi)) { + Console.Error.WriteLine ($"Assembly store '{store.StorePath}' has unsupported target architecture '{store.TargetArch.Value}'"); + retVal = false; + continue; } - using (var stream = new MemoryStream ()) { - assembly.ExtractImage (stream); - stream.Seek (0, SeekOrigin.Begin); - UncompressDLL (stream, $"{filePath}!{assemblyName}", assemblyName, prefix); + foreach (AssemblyStoreItem assembly in store.Assemblies ?? []) { + if (assembly.Ignore) { + continue; + } + + string fileName = assembly.Name.EndsWith (".dll", StringComparison.OrdinalIgnoreCase) ? assembly.Name : $"{assembly.Name}.dll"; + string assemblyName = abi == null ? fileName : $"{abi}/{fileName}"; + using Stream? stream = store.ReadImageData (assembly); + if (stream == null) { + Console.Error.WriteLine ($"Unable to read '{assembly.Name}' from assembly store '{store.StorePath}'"); + retVal = false; + continue; + } + + retVal &= UncompressDLL (stream, $"{filePath}!{assemblyName}", assemblyName, prefix); } } + return retVal; + } + + static bool TryGetAndroidAbi (AndroidTargetArch arch, out string abi) + { + if (!MonoAndroidHelper.SupportedTargetArchitectures.Contains (arch)) { + abi = ""; + return false; + } + + abi = MonoAndroidHelper.ArchToAbi (arch); return true; } - static bool UncompressFromAPK (string filePath, string assembliesPath) + static bool HasAssemblyStore (ZipArchive apk, string assembliesPath, string nativeLibrariesPath) + { + if (apk.ContainsEntry ($"{assembliesPath}assemblies.blob")) { + return true; + } + + foreach (AndroidTargetArch arch in MonoAndroidHelper.SupportedTargetArchitectures) { + string abi = MonoAndroidHelper.ArchToAbi (arch); + if ( + apk.ContainsEntry ($"{nativeLibrariesPath}{abi}/libassembly-store.so") || + apk.ContainsEntry ($"{nativeLibrariesPath}{abi}/libassemblies.{abi}.blob.so") + ) { + return true; + } + } + + return false; + } + + static bool UncompressFromAPK (string filePath, string assembliesPath, string nativeLibrariesPath) { string prefix = $"uncompressed-{Path.GetFileNameWithoutExtension (filePath)}{Path.DirectorySeparatorChar}"; using (ZipArchive apk = ZipArchive.Open (filePath, FileMode.Open)) { - if (!apk.ContainsEntry ($"{assembliesPath}assemblies.blob")) { - return UncompressFromAPK_IndividualEntries (apk, filePath, assembliesPath, prefix); + if (HasAssemblyStore (apk, assembliesPath, nativeLibrariesPath)) { + return UncompressFromAPK_AssemblyStores (filePath, prefix); } - } - return UncompressFromAPK_AssemblyStores (filePath, prefix); + return UncompressFromAPK_IndividualEntries (apk, filePath, assembliesPath, prefix); + } } static int Main (string[] args) @@ -153,14 +178,14 @@ static int Main (string[] args) } if (String.Compare (".apk", ext, StringComparison.OrdinalIgnoreCase) == 0) { - if (!UncompressFromAPK (file, "assemblies/")) { + if (!UncompressFromAPK (file, "assemblies/", "lib/")) { haveErrors = true; } continue; } if (String.Compare (".aab", ext, StringComparison.OrdinalIgnoreCase) == 0) { - if (!UncompressFromAPK (file, "base/root/assemblies/")) { + if (!UncompressFromAPK (file, "base/root/assemblies/", "base/lib/")) { haveErrors = true; } continue; diff --git a/tools/tmt/ApkManagedTypeResolver.cs b/tools/tmt/ApkManagedTypeResolver.cs index 444efcb187d..27ddaf557f3 100644 --- a/tools/tmt/ApkManagedTypeResolver.cs +++ b/tools/tmt/ApkManagedTypeResolver.cs @@ -4,15 +4,13 @@ using Mono.Cecil; using Xamarin.Android.AssemblyStore; +using Xamarin.Android.AssemblyStore.V1; using Xamarin.Tools.Zip; -using ZstandardDecoder = System.IO.Compression.ZstandardDecoder; namespace tmt { class ApkManagedTypeResolver : ManagedTypeResolver { - const uint CompressedDataMagic = 0x535A4158; // 'XAZS', little-endian - readonly Dictionary? individualAssemblies; readonly Dictionary? blobAssemblies; readonly ZipArchive apk; @@ -129,71 +127,17 @@ Stream PrepStream (Stream stream) protected override AssemblyDefinition ReadAssembly (string assemblyPath) { - byte[]? assemblyBytes = null; - byte[]? sourceBytes = null; Stream stream = GetAssemblyStream (assemblyPath); - - // - // Zstd compressed assembly header format: - // uint magic; // 0x535A4158; 'XAZS', little-endian - // uint descriptor_index; // Index into an internal assembly descriptor table - // uint uncompressed_length; // Size of assembly, uncompressed - // - using var reader = new BinaryReader (stream); - uint magic = reader.ReadUInt32 (); - if (magic == CompressedDataMagic) { - reader.ReadUInt32 (); // descriptor index, ignore - uint decompressedLength = reader.ReadUInt32 (); - - int inputLength = (int)(stream.Length - 12); - try { - sourceBytes = Utilities.BytePool.Rent (inputLength); - ReadFully (reader, sourceBytes, inputLength); - - assemblyBytes = Utilities.BytePool.Rent ((int)decompressedLength); - int decoded = ZstandardDecoder.TryDecompress ( - sourceBytes.AsSpan (0, inputLength), - assemblyBytes.AsSpan (0, (int)decompressedLength), - out int bytesWritten) ? bytesWritten : -1; - if (decoded != (int)decompressedLength) { - throw new InvalidOperationException ($"Failed to decompress Zstd data of {assemblyPath} (decoded: {decoded})"); - } - } catch { - if (assemblyBytes != null) { - Utilities.BytePool.Return (assemblyBytes); - assemblyBytes = null; - } - throw; - } finally { - if (sourceBytes != null) { - Utilities.BytePool.Return (sourceBytes); - sourceBytes = null; - } - } - } - - if (assemblyBytes != null) { - stream.Close (); + var decompressed = new MemoryStream (); + if (AssemblyCompression.TryDecompress (stream, decompressed, out _)) { stream.Dispose (); - stream = new MemoryStream (); - stream.Write (assemblyBytes, 0, assemblyBytes.Length); - Utilities.BytePool.Return (assemblyBytes); - stream.Seek (0, SeekOrigin.Begin); + decompressed.Seek (0, SeekOrigin.Begin); + stream = decompressed; + } else { + decompressed.Dispose (); } return AssemblyDefinition.ReadAssembly (stream); } - - static void ReadFully (BinaryReader reader, byte[] destination, int count) - { - int totalRead = 0; - while (totalRead < count) { - int read = reader.Read (destination, totalRead, count - totalRead); - if (read <= 0) - throw new EndOfStreamException ("Unexpected end of stream while reading compressed assembly."); - - totalRead += read; - } - } } } diff --git a/tools/tmt/tmt.csproj b/tools/tmt/tmt.csproj index 4c1ba65babd..83863d30cee 100644 --- a/tools/tmt/tmt.csproj +++ b/tools/tmt/tmt.csproj @@ -16,7 +16,8 @@ - + + @@ -25,6 +26,7 @@ +