diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln
index f672d86ece7..80f728c8abf 100644
--- a/Xamarin.Android.sln
+++ b/Xamarin.Android.sln
@@ -144,6 +144,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "apksigner", "src\apksigner\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "java-source-utils", "external\Java.Interop\tools\java-source-utils\java-source-utils.csproj", "{37FCD325-1077-4603-98E7-4509CAD648D6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "decompress-assemblies", "tools\decompress-assemblies\decompress-assemblies.csproj", "{88B746FF-8D6E-464D-9D66-FF2ECCF148E0}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Xamarin.Android.NamingCustomAttributes\Xamarin.Android.NamingCustomAttributes.projitems*{3f1f2f50-af1a-4a5a-bedb-193372f068d7}*SharedItemsImports = 4
@@ -395,6 +397,10 @@ Global
{37FCD325-1077-4603-98E7-4509CAD648D6}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{37FCD325-1077-4603-98E7-4509CAD648D6}.Release|AnyCPU.ActiveCfg = Release|Any CPU
{37FCD325-1077-4603-98E7-4509CAD648D6}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {88B746FF-8D6E-464D-9D66-FF2ECCF148E0}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {88B746FF-8D6E-464D-9D66-FF2ECCF148E0}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {88B746FF-8D6E-464D-9D66-FF2ECCF148E0}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {88B746FF-8D6E-464D-9D66-FF2ECCF148E0}.Release|AnyCPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -461,6 +467,7 @@ Global
{86A8DEFE-7ABB-4097-9389-C249581E243D} = {04E3E11E-B47D-4599-8AFC-50515A95E715}
{9A9EF774-6EA6-414F-9D2F-DCD66C56B92A} = {04E3E11E-B47D-4599-8AFC-50515A95E715}
{37FCD325-1077-4603-98E7-4509CAD648D6} = {864062D3-A415-4A6F-9324-5820237BA058}
+ {88B746FF-8D6E-464D-9D66-FF2ECCF148E0} = {864062D3-A415-4A6F-9324-5820237BA058}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {53A1F287-EFB2-4D97-A4BB-4A5E145613F6}
diff --git a/tools/decompress-assemblies/decompress-assemblies.csproj b/tools/decompress-assemblies/decompress-assemblies.csproj
new file mode 100644
index 00000000000..7e179f5c21c
--- /dev/null
+++ b/tools/decompress-assemblies/decompress-assemblies.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Microsoft Corporation
+ 2021 Microsoft Corporation
+ 0.0.1
+ net472
+ false
+ Xamarin.Android.Tools.DecompressAssemblies
+ decompress-assemblies
+ ../../bin/$(Configuration)/bin
+ Exe
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+ decompress-assemblies
+ PreserveNewest
+
+
+
diff --git a/tools/decompress-assemblies/main.cs b/tools/decompress-assemblies/main.cs
new file mode 100644
index 00000000000..749c3a691db
--- /dev/null
+++ b/tools/decompress-assemblies/main.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Buffers;
+using System.IO;
+
+using K4os.Compression.LZ4;
+using Xamarin.Tools.Zip;
+
+namespace Xamarin.Android.Tools.DecompressAssemblies
+{
+ class App
+ {
+ const uint CompressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian
+
+ static readonly ArrayPool bytePool = ArrayPool.Shared;
+
+ static int Usage ()
+ {
+ Console.WriteLine ("Usage: decompress-assemblies {file.{dll,apk,aab}} [{file.{dll,apk,aab} ...]");
+ Console.WriteLine ();
+ Console.WriteLine ("DLL files passed on command are uncompressed to the current directory with the `uncompressed-` prefix added to their name.");
+ Console.WriteLine ("DLL files from AAB/APK archives are uncompressed to a subdirectory of the current directory named after the archive with extension removed");
+ return 1;
+ }
+
+ static bool UncompressDLL (Stream inputStream, string fileName, string filePath, string prefix)
+ {
+ string outputFile = $"{prefix}{Path.GetFileName (filePath)}";
+ bool retVal = true;
+
+ Console.WriteLine ($"Processing {fileName}");
+ //
+ // LZ4 compressed assembly header format:
+ // uint magic; // 0x5A4C4158; 'XALZ', 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 = LZ4Codec.Decode (sourceBytes, 0, inputLength, assemblyBytes, 0, (int)decompressedLength);
+ if (decoded != (int)decompressedLength) {
+ Console.Error.WriteLine ($" Failed to decompress LZ4 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, assemblyBytes.Length);
+ fs.Flush ();
+ }
+ Console.WriteLine ($" uncompressed to: {outputFile}");
+ }
+
+ bytePool.Return (sourceBytes);
+ bytePool.Return (assemblyBytes);
+ } else {
+ Console.WriteLine ($" assembly is not compressed");
+ }
+ }
+
+ return retVal;
+ }
+
+ static bool UncompressDLL (string filePath, string prefix)
+ {
+ using (var fs = File.Open (filePath, FileMode.Open, FileAccess.Read)) {
+ return UncompressDLL (fs, filePath, filePath, prefix);
+ }
+ }
+
+ static bool UncompressFromAPK (string filePath, string assembliesPath)
+ {
+ string prefix = $"uncompressed-{Path.GetFileNameWithoutExtension (filePath)}{Path.DirectorySeparatorChar}";
+ using (ZipArchive apk = ZipArchive.Open (filePath, FileMode.Open)) {
+ foreach (ZipEntry entry in apk) {
+ if (!entry.FullName.StartsWith (assembliesPath, StringComparison.Ordinal)) {
+ continue;
+ }
+
+ if (!entry.FullName.EndsWith (".dll", StringComparison.Ordinal)) {
+ continue;
+ }
+
+ using (var stream = new MemoryStream ()) {
+ entry.Extract (stream);
+ stream.Seek (0, SeekOrigin.Begin);
+ UncompressDLL (stream, $"{filePath}!{entry.FullName}", entry.FullName, prefix);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ static int Main (string[] args)
+ {
+ if (args.Length == 0) {
+ return Usage ();
+ }
+
+ bool haveErrors = false;
+ foreach (string file in args) {
+ string ext = Path.GetExtension (file);
+ if (String.Compare (".dll", ext, StringComparison.OrdinalIgnoreCase) == 0) {
+ if (!UncompressDLL (file, "uncompressed-")) {
+ haveErrors = true;
+ }
+ continue;
+ }
+
+ if (String.Compare (".apk", ext, StringComparison.OrdinalIgnoreCase) == 0) {
+ if (!UncompressFromAPK (file, "assemblies/")) {
+ haveErrors = true;
+ }
+ continue;
+ }
+
+ if (String.Compare (".aab", ext, StringComparison.OrdinalIgnoreCase) == 0) {
+ if (!UncompressFromAPK (file, "base/root/assemblies/")) {
+ haveErrors = true;
+ }
+ continue;
+ }
+ }
+
+ return haveErrors ? 1 : 0;
+ }
+ }
+}
diff --git a/tools/scripts/decompress-assemblies b/tools/scripts/decompress-assemblies
new file mode 100755
index 00000000000..8897f401de3
--- /dev/null
+++ b/tools/scripts/decompress-assemblies
@@ -0,0 +1,3 @@
+#!/bin/bash
+truepath=$(readlink "$0" || echo "$0")
+exec mono --debug "${truepath}.exe" "$@"