|
| 1 | +namespace Sentry.Android.AssemblyReader; |
| 2 | + |
| 3 | +internal static class ArchiveUtils |
| 4 | +{ |
| 5 | + internal static PEReader CreatePEReader(string assemblyName, MemoryStream inputStream, DebugLogger? logger) |
| 6 | + { |
| 7 | + var decompressedStream = TryDecompressLZ4(assemblyName, inputStream, logger); // Returns null if not compressed |
| 8 | + return new PEReader(decompressedStream ?? inputStream); |
| 9 | + } |
| 10 | + |
| 11 | + internal static MemoryStream Extract(this ZipArchiveEntry zipEntry) |
| 12 | + { |
| 13 | + var memStream = new MemoryStream((int)zipEntry.Length); |
| 14 | + using var zipStream = zipEntry.Open(); |
| 15 | + zipStream.CopyTo(memStream); |
| 16 | + memStream.Position = 0; |
| 17 | + return memStream; |
| 18 | + } |
| 19 | + |
| 20 | + /// <summary> |
| 21 | + /// The DLL may be LZ4 compressed, see https://github.com/xamarin/xamarin-android/pull/4686 |
| 22 | + /// In particular: https://github.com/dotnet/android/blob/44c5c30d3da692c54ca27d4a41571ef20b73670f/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs#L96-L104 |
| 23 | + /// The format is: |
| 24 | + /// [ 4 byte magic header ] (XALZ) |
| 25 | + /// [ 4 byte descriptor header index ] |
| 26 | + /// [ 4 byte uncompressed payload length ] |
| 27 | + /// [rest: lz4 compressed payload] |
| 28 | + /// </summary> |
| 29 | + /// <seealso href="https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/decompress-assemblies/main.cs#L26" /> |
| 30 | + private static Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream, DebugLogger? logger) |
| 31 | + { |
| 32 | + const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian |
| 33 | + const int payloadOffset = 12; |
| 34 | + var reader = new BinaryReader(inputStream); |
| 35 | + if (reader.ReadUInt32() != compressedDataMagic) |
| 36 | + { |
| 37 | + // Restore the input stream to the beginning if we're not decompressing. |
| 38 | + inputStream.Position = 0; |
| 39 | + return null; |
| 40 | + } |
| 41 | + reader.ReadUInt32(); // ignore descriptor index, we don't need it |
| 42 | + var decompressedLength = reader.ReadInt32(); |
| 43 | + Debug.Assert(inputStream.Position == payloadOffset); |
| 44 | + var inputLength = (int)(inputStream.Length - payloadOffset); |
| 45 | + |
| 46 | + logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength); |
| 47 | + |
| 48 | + var outputStream = new MemoryStream(decompressedLength); |
| 49 | + |
| 50 | + // We're writing to the underlying array manually, so we need to set the length. |
| 51 | + outputStream.SetLength(decompressedLength); |
| 52 | + var outputBuffer = outputStream.GetBuffer(); |
| 53 | + |
| 54 | + var inputBuffer = inputStream is MemorySlice slice ? slice.FullBuffer : inputStream.GetBuffer(); |
| 55 | + var offset = inputStream is MemorySlice memorySlice ? memorySlice.Offset + payloadOffset : payloadOffset; |
| 56 | + var decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength); |
| 57 | + if (decoded != decompressedLength) |
| 58 | + { |
| 59 | + throw new Exception($"Failed to decompress LZ4 data of assembly {assemblyName} - decoded {decoded} instead of expected {decompressedLength} bytes"); |
| 60 | + } |
| 61 | + return outputStream; |
| 62 | + } |
| 63 | + |
| 64 | + // Allows consumer to access the underlying buffer even if the MemoryStream is created as a slice over another. |
| 65 | + // Plain MemoryStream would throw "MemoryStream's internal buffer cannot be accessed." |
| 66 | + internal class MemorySlice(MemoryStream other, int offset, int size) |
| 67 | + : MemoryStream(other.GetBuffer(), offset, size, writable: false) |
| 68 | + { |
| 69 | + public readonly int Offset = offset; |
| 70 | + public readonly byte[] FullBuffer = other.GetBuffer(); |
| 71 | + } |
| 72 | +} |
0 commit comments