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 @@
+