From 72083cd0b0d54d33aefe411c90d7bc65b8515ccf Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Fri, 22 Jan 2021 17:00:36 +0100 Subject: [PATCH] [tools] Add a tool to decompress XA-compressed assemblies The utility is named `decompress-assemblies` and takes as its parameters a list of files to decompress. The files might be either `.apk` archives or `.dll` files. I case of `.apk`, managed assemblies stored in the `assemblies/` directory inside the APK will be read and decompressed. The uncompressed result is stored in the current directory with the original assembly name modified to contain the `uncompressed-` prefix, in order to prevent accidental overwriting of assemblies found in the current directory. --- Xamarin.Android.sln | 7 + .../decompress-assemblies.csproj | 30 ++++ tools/decompress-assemblies/main.cs | 139 ++++++++++++++++++ tools/scripts/decompress-assemblies | 3 + 4 files changed, 179 insertions(+) create mode 100644 tools/decompress-assemblies/decompress-assemblies.csproj create mode 100644 tools/decompress-assemblies/main.cs create mode 100755 tools/scripts/decompress-assemblies 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" "$@"