diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 72b2e92f51..277ff99842 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -117,7 +117,13 @@ jobs:
dotnet build src/Sentry/Sentry.csproj -t:InstallAndroidDependencies -f:net8.0-android34.0 -p:AcceptAndroidSDKLicenses=True -p:AndroidSdkPath="/usr/local/lib/android/sdk/"
- name: Build
- run: dotnet build Sentry-CI-Build-${{ runner.os }}.slnf -c Release --no-restore --nologo -v:minimal -flp:logfile=build.log -p:CopyLocalLockFileAssemblies=true
+ run: dotnet build Sentry-CI-Build-${{ runner.os }}.slnf -c Release --no-restore --nologo -v:minimal -flp:logfile=build.log -p:CopyLocalLockFileAssemblies=true -bl:build.binlog
+
+ - name: Upload build logs
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ runner.os }}-build-logs
+ path: build.binlog
- name: Test
run: dotnet test Sentry-CI-Build-${{ runner.os }}.slnf -c Release --no-build --nologo -l GitHubActions -l "trx;LogFilePrefix=testresults_${{ runner.os }}" --collect "XPlat Code Coverage"
diff --git a/.github/workflows/device-tests-android.yml b/.github/workflows/device-tests-android.yml
index 64749e1b8f..9a00195412 100644
--- a/.github/workflows/device-tests-android.yml
+++ b/.github/workflows/device-tests-android.yml
@@ -11,7 +11,12 @@ on:
jobs:
build:
+ name: Build (${{ matrix.tfm }})
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ tfm: [net8.0, net9.0]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: 1
@@ -32,18 +37,27 @@ jobs:
uses: ./.github/actions/buildnative
- name: Build Android Test App
- run: pwsh ./scripts/device-test.ps1 android -Build
+ run: pwsh ./scripts/device-test.ps1 android -Build -Tfm ${{ matrix.tfm }}
+
+ - name: Upload Android Test App (net8.0)
+ if: matrix.tfm == 'net8.0'
+ uses: actions/upload-artifact@v4
+ with:
+ name: device-test-android-net8.0
+ if-no-files-found: error
+ path: test/Sentry.Maui.Device.TestApp/bin/Release/net8.0-android/android-x64/io.sentry.dotnet.maui.device.testapp-Signed.apk
- - name: Upload Android Test App
+ - name: Upload Android Test App (net9.0)
+ if: matrix.tfm == 'net9.0'
uses: actions/upload-artifact@v4
with:
- name: device-test-android
+ name: device-test-android-net9.0
if-no-files-found: error
- path: test/Sentry.Maui.Device.TestApp/bin/Release/net8.0-android34.0/android-x64/io.sentry.dotnet.maui.device.testapp-Signed.apk
+ path: test/Sentry.Maui.Device.TestApp/bin/Release/net9.0-android/android-x64/io.sentry.dotnet.maui.device.testapp-Signed.apk
android:
needs: [build]
- name: Run Android API-${{ matrix.api-level }} Test
+ name: Run Android API-${{ matrix.api-level }} Test (${{ matrix.tfm }})
# Requires a "larger runner", for nested virtualization support
runs-on: ubuntu-latest-4-cores
@@ -51,6 +65,7 @@ jobs:
strategy:
fail-fast: false
matrix:
+ tfm: [net8.0, net9.0]
# We run against both an older and a newer API
api-level: [27, 33]
env:
@@ -70,7 +85,7 @@ jobs:
- name: Download test app artifact
uses: actions/download-artifact@v4
with:
- name: device-test-android
+ name: device-test-android-${{ matrix.tfm }}
path: bin
- name: Setup Gradle
@@ -78,7 +93,6 @@ jobs:
# Cached AVD setup per https://github.com/ReactiveCircus/android-emulator-runner/blob/main/README.md
-
- name: Run Tests
timeout-minutes: 40
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # Tag: v2.33.0
@@ -92,11 +106,11 @@ jobs:
disk-size: 4096M
emulator-options: -no-snapshot-save -no-window -accel on -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
- script: pwsh scripts/device-test.ps1 android -Run
+ script: pwsh scripts/device-test.ps1 android -Run -Tfm ${{ matrix.tfm }}
- name: Upload results
if: success() || failure()
uses: actions/upload-artifact@v4
with:
- name: device-test-android-${{ matrix.api-level }}-results
+ name: device-test-android-${{ matrix.api-level }}-${{ matrix.tfm }}-results
path: test_output
diff --git a/CHANGELOG.md b/CHANGELOG.md
index efc1251bc8..0573ad4806 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,17 +2,18 @@
## Unreleased
+### Features
+
+- Exception.HResult is now included in the mechanism data for all exceptions ([#4029](https://github.com/getsentry/sentry-dotnet/pull/4029))
+
### Fixes
+- Fixed symbolication and source context for net9.0-android ([#4033](https://github.com/getsentry/sentry-dotnet/pull/4033))
- Single quotes added to the release name when using MS Build to create Sentry releases on Windows ([#4015](https://github.com/getsentry/sentry-dotnet/pull/4015))
- Target `net9.0` on Sentry.Google.Cloud.Functions to avoid conflict with Sentry.AspNetCore ([#4039](https://github.com/getsentry/sentry-dotnet/pull/4039))
- Changed default value for `SentryOptions.EnableAppHangTrackingV2` to `false` ([#4042](https://github.com/getsentry/sentry-dotnet/pull/4042))
- Missing MAUI `Shell` navigation breadcrumbs on iOS ([#4006](https://github.com/getsentry/sentry-dotnet/pull/4006))
-### Features
-
-- Exception.HResult is now included in the mechanism data for all exceptions ([#4029](https://github.com/getsentry/sentry-dotnet/pull/4029))
-
### Dependencies
- Bump CLI from v2.42.2 to v2.43.0 ([#4036](https://github.com/getsentry/sentry-dotnet/pull/4036), [#4049](https://github.com/getsentry/sentry-dotnet/pull/4049), [#4060](https://github.com/getsentry/sentry-dotnet/pull/4060), [#4062](https://github.com/getsentry/sentry-dotnet/pull/4062))
diff --git a/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj
index 6cf009ec38..bdb8ed918a 100644
--- a/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj
+++ b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/scripts/device-test.ps1 b/scripts/device-test.ps1
index 6812c42dff..c9e03ffdd8 100644
--- a/scripts/device-test.ps1
+++ b/scripts/device-test.ps1
@@ -5,7 +5,8 @@ param(
[String] $Platform,
[Switch] $Build,
- [Switch] $Run
+ [Switch] $Run,
+ [String] $Tfm
)
Set-StrictMode -Version latest
@@ -21,13 +22,16 @@ $CI = Test-Path env:CI
Push-Location $PSScriptRoot/..
try
{
- $tfm = 'net8.0-'
+ if (!$Tfm)
+ {
+ $Tfm = 'net8.0'
+ }
$arch = (!$IsWindows -and $(uname -m) -eq 'arm64') ? 'arm64' : 'x64'
if ($Platform -eq 'android')
{
- $tfm += 'android34.0'
+ $Tfm += '-android'
$group = 'android'
- $buildDir = $CI ? 'bin' : "test/Sentry.Maui.Device.TestApp/bin/Release/$tfm/android-$arch"
+ $buildDir = $CI ? 'bin' : "test/Sentry.Maui.Device.TestApp/bin/Release/$Tfm/android-$arch"
$arguments = @(
'--app', "$buildDir/io.sentry.dotnet.maui.device.testapp-Signed.apk",
'--package-name', 'io.sentry.dotnet.maui.device.testapp',
@@ -43,11 +47,11 @@ try
}
elseif ($Platform -eq 'ios')
{
- $tfm += 'ios17.0'
+ $Tfm += '-ios'
$group = 'apple'
# Always use x64 on iOS, since arm64 doesn't support JIT, which is required for tests using NSubstitute
$arch = 'x64'
- $buildDir = "test/Sentry.Maui.Device.TestApp/bin/Release/$tfm/iossimulator-$arch"
+ $buildDir = "test/Sentry.Maui.Device.TestApp/bin/Release/$Tfm/iossimulator-$arch"
$envValue = $CI ? 'true' : 'false'
$arguments = @(
'--app', "$buildDir/Sentry.Maui.Device.TestApp.app",
@@ -60,7 +64,7 @@ try
if ($Build)
{
# We disable AOT for device tests: https://github.com/nsubstitute/NSubstitute/issues/834
- dotnet build -f $tfm -c Release -p:EnableAot=false -p:NoSymbolStrip=true test/Sentry.Maui.Device.TestApp
+ dotnet build -f $Tfm -c Release -p:EnableAot=false -p:NoSymbolStrip=true test/Sentry.Maui.Device.TestApp
if ($LASTEXITCODE -ne 0)
{
throw 'Failed to build Sentry.Maui.Device.TestApp'
diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs
index 746b11cbe2..addd6633ff 100644
--- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs
+++ b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs
@@ -17,69 +17,4 @@ public void Dispose()
{
ZipArchive.Dispose();
}
-
- protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream)
- {
- var decompressedStream = TryDecompressLZ4(assemblyName, inputStream);
-
- // Use the decompressed stream, or if null, i.e. it wasn't compressed, use the original.
- return new PEReader(decompressedStream ?? inputStream);
- }
-
- ///
- /// The DLL may be LZ4 compressed, see https://github.com/xamarin/xamarin-android/pull/4686
- /// The format is:
- /// [ 4 byte magic header ] (XALZ)
- /// [ 4 byte header index ]
- /// [ 4 byte uncompressed payload length ]
- /// [rest: lz4 compressed payload]
- ///
- ///
- private Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream)
- {
- const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian
- const int payloadOffset = 12;
- var reader = new BinaryReader(inputStream);
- if (reader.ReadUInt32() != compressedDataMagic)
- {
- // Restore the input stream to the beginning if we're not decompressing.
- inputStream.Position = 0;
- return null;
- }
- reader.ReadUInt32(); // ignore descriptor index, we don't need it
- var decompressedLength = reader.ReadInt32();
- Debug.Assert(inputStream.Position == payloadOffset);
- var inputLength = (int)(inputStream.Length - payloadOffset);
-
- Logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength);
-
- var outputStream = new MemoryStream(decompressedLength);
-
- // We're writing to the underlying array manually, so we need to set the length.
- outputStream.SetLength(decompressedLength);
- var outputBuffer = outputStream.GetBuffer();
-
- var inputBuffer = inputStream is MemorySlice slice ? slice.FullBuffer : inputStream.GetBuffer();
- var offset = inputStream is MemorySlice memorySlice ? memorySlice.Offset + payloadOffset : payloadOffset;
- var decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength);
- if (decoded != decompressedLength)
- {
- throw new Exception($"Failed to decompress LZ4 data of assembly {assemblyName} - decoded {decoded} instead of expected {decompressedLength} bytes");
- }
- return outputStream;
- }
-
- // Allows consumer to access the underlying buffer even if the MemoryStream is created as a slice over another.
- // Plain MemoryStream would throw "MemoryStream's internal buffer cannot be accessed."
- protected class MemorySlice : MemoryStream
- {
- public readonly int Offset;
- public readonly byte[] FullBuffer;
-
- public MemorySlice(MemoryStream other, int offset, int size) : base(other.GetBuffer(), offset, size, writable: false)
- {
- Offset = offset;
- FullBuffer = other.GetBuffer();
- }
- }
}
diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs
index 399f8622e3..62670899a9 100644
--- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs
+++ b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs
@@ -1,3 +1,6 @@
+using Sentry.Android.AssemblyReader.V1;
+using Sentry.Android.AssemblyReader.V2;
+
namespace Sentry.Android.AssemblyReader;
///
@@ -15,15 +18,29 @@ public static class AndroidAssemblyReaderFactory
public static IAndroidAssemblyReader Open(string apkPath, IList supportedAbis, DebugLogger? logger = null)
{
logger?.Invoke("Opening APK: {0}", apkPath);
- var zipArchive = ZipFile.Open(apkPath, ZipArchiveMode.Read);
+#if NET9_0
+ logger?.Invoke("Reading files using V2 APK layout.");
+ if (AndroidAssemblyStoreReaderV2.TryReadStore(apkPath, supportedAbis, logger, out var readerV2))
+ {
+ logger?.Invoke("APK uses AssemblyStore V2");
+ return readerV2;
+ }
+
+ logger?.Invoke("APK doesn't use AssemblyStore");
+ return new AndroidAssemblyDirectoryReaderV2(apkPath, supportedAbis, logger);
+#else
+ logger?.Invoke("Reading files using V1 APK layout.");
+
+ var zipArchive = ZipFile.OpenRead(apkPath);
if (zipArchive.GetEntry("assemblies/assemblies.manifest") is not null)
{
- logger?.Invoke("APK uses AssemblyStore");
- return new AndroidAssemblyStoreReader(zipArchive, supportedAbis, logger);
+ logger?.Invoke("APK uses AssemblyStore V1");
+ return new AndroidAssemblyStoreReaderV1(zipArchive, supportedAbis, logger);
}
logger?.Invoke("APK doesn't use AssemblyStore");
- return new AndroidAssemblyDirectoryReader(zipArchive, supportedAbis, logger);
+ return new AndroidAssemblyDirectoryReaderV1(zipArchive, supportedAbis, logger);
+#endif
}
}
diff --git a/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs b/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs
new file mode 100644
index 0000000000..0613eed1f9
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs
@@ -0,0 +1,72 @@
+namespace Sentry.Android.AssemblyReader;
+
+internal static class ArchiveUtils
+{
+ internal static PEReader CreatePEReader(string assemblyName, MemoryStream inputStream, DebugLogger? logger)
+ {
+ var decompressedStream = TryDecompressLZ4(assemblyName, inputStream, logger); // Returns null if not compressed
+ return new PEReader(decompressedStream ?? inputStream);
+ }
+
+ internal static MemoryStream Extract(this ZipArchiveEntry zipEntry)
+ {
+ var memStream = new MemoryStream((int)zipEntry.Length);
+ using var zipStream = zipEntry.Open();
+ zipStream.CopyTo(memStream);
+ memStream.Position = 0;
+ return memStream;
+ }
+
+ ///
+ /// The DLL may be LZ4 compressed, see https://github.com/xamarin/xamarin-android/pull/4686
+ /// In particular: https://github.com/dotnet/android/blob/44c5c30d3da692c54ca27d4a41571ef20b73670f/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs#L96-L104
+ /// The format is:
+ /// [ 4 byte magic header ] (XALZ)
+ /// [ 4 byte descriptor header index ]
+ /// [ 4 byte uncompressed payload length ]
+ /// [rest: lz4 compressed payload]
+ ///
+ ///
+ private static Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream, DebugLogger? logger)
+ {
+ const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian
+ const int payloadOffset = 12;
+ var reader = new BinaryReader(inputStream);
+ if (reader.ReadUInt32() != compressedDataMagic)
+ {
+ // Restore the input stream to the beginning if we're not decompressing.
+ inputStream.Position = 0;
+ return null;
+ }
+ reader.ReadUInt32(); // ignore descriptor index, we don't need it
+ var decompressedLength = reader.ReadInt32();
+ Debug.Assert(inputStream.Position == payloadOffset);
+ var inputLength = (int)(inputStream.Length - payloadOffset);
+
+ logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength);
+
+ var outputStream = new MemoryStream(decompressedLength);
+
+ // We're writing to the underlying array manually, so we need to set the length.
+ outputStream.SetLength(decompressedLength);
+ var outputBuffer = outputStream.GetBuffer();
+
+ var inputBuffer = inputStream is MemorySlice slice ? slice.FullBuffer : inputStream.GetBuffer();
+ var offset = inputStream is MemorySlice memorySlice ? memorySlice.Offset + payloadOffset : payloadOffset;
+ var decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength);
+ if (decoded != decompressedLength)
+ {
+ throw new Exception($"Failed to decompress LZ4 data of assembly {assemblyName} - decoded {decoded} instead of expected {decompressedLength} bytes");
+ }
+ return outputStream;
+ }
+
+ // Allows consumer to access the underlying buffer even if the MemoryStream is created as a slice over another.
+ // Plain MemoryStream would throw "MemoryStream's internal buffer cannot be accessed."
+ internal class MemorySlice(MemoryStream other, int offset, int size)
+ : MemoryStream(other.GetBuffer(), offset, size, writable: false)
+ {
+ public readonly int Offset = offset;
+ public readonly byte[] FullBuffer = other.GetBuffer();
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/ELFSharp/ATTRIBUTION.txt
new file mode 100644
index 0000000000..1c47bdece1
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ATTRIBUTION.txt
@@ -0,0 +1,76 @@
+The code in this subdirectory was adapted from:
+https://github.com/konrad-kruczynski/elfsharp/tree/0c859b7b4b8c73bd1194021672681586c1b7139e
+
+The only changes to the code are:
+- Public members have been made internal
+- Nullability warnings have been disabled
+
+The original license is as follows:
+
+Copyright (c) 2011 Konrad KruczyĆski and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+This software uses ELF machine constants from the LLVM projects, whose license
+is provided below:
+
+==============================================================================
+LLVM Release License
+==============================================================================
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2003-2010 University of Illinois at Urbana-Champaign.
+All rights reserved.
+
+Developed by:
+
+ LLVM Team
+
+ University of Illinois at Urbana-Champaign
+
+ http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimers.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimers in the
+ documentation and/or other materials provided with the distribution.
+
+ * Neither the names of the LLVM Team, University of Illinois at
+ Urbana-Champaign, nor the names of its contributors may be used to
+ endorse or promote products derived from this Software without specific
+ prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Class.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Class.cs
new file mode 100644
index 0000000000..ca60eb04f5
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Class.cs
@@ -0,0 +1,9 @@
+namespace ELFSharp.ELF
+{
+ internal enum Class
+ {
+ Bit32,
+ Bit64,
+ NotELF
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Consts.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Consts.cs
new file mode 100644
index 0000000000..83c44d49c1
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Consts.cs
@@ -0,0 +1,11 @@
+namespace ELFSharp.ELF
+{
+ internal static class Consts
+ {
+ public const string ObjectsStringTableName = ".strtab";
+ public const string DynamicStringTableName = ".dynstr";
+ public const int SymbolEntrySize32 = 16;
+ public const int SymbolEntrySize64 = 24;
+ public const int MinimalELFSize = 16;
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELF.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELF.cs
new file mode 100644
index 0000000000..19eb3dfbda
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELF.cs
@@ -0,0 +1,411 @@
+using ELFSharp.ELF.Sections;
+using ELFSharp.ELF.Segments;
+using ELFSharp.Utilities;
+using SectionHeader = ELFSharp.ELF.Sections.SectionHeader;
+
+#nullable disable
+
+namespace ELFSharp.ELF
+{
+ internal sealed class ELF : IELF where T : struct
+ {
+ private const int SectionNameNotUniqueMarker = -1;
+ private readonly bool ownsStream;
+
+ private readonly SimpleEndianessAwareReader reader;
+ private Stage currentStage;
+ private StringTable dynamicStringTable;
+ private StringTable objectsStringTable;
+ private uint sectionHeaderEntryCount;
+ private ushort sectionHeaderEntrySize;
+ private long sectionHeaderOffset;
+ private List sectionHeaders;
+ private Dictionary sectionIndicesByName;
+ private List> sections;
+ private ushort segmentHeaderEntryCount;
+ private ushort segmentHeaderEntrySize;
+ private long segmentHeaderOffset;
+ private List> segments;
+ private uint stringTableIndex;
+
+ internal ELF(Stream stream, bool ownsStream)
+ {
+ this.ownsStream = ownsStream;
+ reader = ObtainEndianessAwareReader(stream);
+ ReadFields();
+ ReadStringTable();
+ ReadSections();
+ ReadSegmentHeaders();
+ }
+
+ public T EntryPoint { get; private set; }
+
+ public T MachineFlags { get; private set; }
+
+ public IReadOnlyList> Segments => segments.AsReadOnly();
+
+ public IReadOnlyList> Sections => sections.AsReadOnly();
+
+ public Endianess Endianess { get; private set; }
+
+ public Class Class { get; private set; }
+
+ public FileType Type { get; private set; }
+
+ public Machine Machine { get; private set; }
+
+ public bool HasSegmentHeader => segmentHeaderOffset != 0;
+
+ public bool HasSectionHeader => sectionHeaderOffset != 0;
+
+ public bool HasSectionsStringTable => stringTableIndex != 0;
+
+ IReadOnlyList IELF.Segments => Segments;
+
+ public IStringTable SectionsStringTable { get; private set; }
+
+ IEnumerable IELF.GetSections()
+ {
+ return Sections.Where(x => x is TSectionType).Cast();
+ }
+
+ IReadOnlyList IELF.Sections => Sections;
+
+ bool IELF.TryGetSection(string name, out ISection section)
+ {
+ var result = TryGetSection(name, out var concreteSection);
+ section = concreteSection;
+ return result;
+ }
+
+ ISection IELF.GetSection(string name)
+ {
+ return GetSection(name);
+ }
+
+ bool IELF.TryGetSection(int index, out ISection section)
+ {
+ var result = TryGetSection(index, out var sectionConcrete);
+ section = sectionConcrete;
+ return result;
+ }
+
+ ISection IELF.GetSection(int index)
+ {
+ return GetSection(index);
+ }
+
+ public void Dispose()
+ {
+ if (ownsStream)
+ reader.BaseStream.Dispose();
+ }
+
+ public IEnumerable GetSections() where TSection : Section
+ {
+ return Sections.Where(x => x is TSection).Cast();
+ }
+
+ public bool TryGetSection(string name, out Section section)
+ {
+ return TryGetSectionInner(name, out section) == GetSectionResult.Success;
+ }
+
+ public Section GetSection(string name)
+ {
+ var result = TryGetSectionInner(name, out var section);
+
+ switch (result)
+ {
+ case GetSectionResult.Success:
+ return section;
+ case GetSectionResult.SectionNameNotUnique:
+ throw new InvalidOperationException("Given section name is not unique, order is ambigous.");
+ case GetSectionResult.NoSectionsStringTable:
+ throw new InvalidOperationException(
+ "Given ELF does not contain section header string table, therefore names of sections cannot be obtained.");
+ case GetSectionResult.NoSuchSection:
+ throw new KeyNotFoundException(string.Format("Given section {0} could not be found in the file.",
+ name));
+ default:
+ throw new InvalidOperationException("Unhandled error.");
+ }
+ }
+
+ public Section GetSection(int index)
+ {
+ var result = TryGetSectionInner(index, out var section);
+ switch (result)
+ {
+ case GetSectionResult.Success:
+ return section;
+ case GetSectionResult.NoSuchSection:
+ throw new IndexOutOfRangeException(string.Format("Given section index {0} is out of range.",
+ index));
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("[ELF: Endianess={0}, Class={1}, Type={2}, Machine={3}, EntryPoint=0x{4:X}, " +
+ "NumberOfSections={5}, NumberOfSegments={6}]", Endianess, Class, Type, Machine,
+ EntryPoint, sections.Count, segments.Count);
+ }
+
+ private bool TryGetSection(int index, out Section section)
+ {
+ return TryGetSectionInner(index, out section) == GetSectionResult.Success;
+ }
+
+ private Section GetSectionFromSectionHeader(SectionHeader header)
+ {
+ Section returned;
+ switch (header.Type)
+ {
+ case SectionType.Null:
+ goto default;
+ case SectionType.ProgBits:
+ returned = new ProgBitsSection(header, reader);
+ break;
+ case SectionType.SymbolTable:
+ returned = new SymbolTable(header, reader, objectsStringTable, this);
+ break;
+ case SectionType.StringTable:
+ returned = new StringTable(header, reader);
+ break;
+ case SectionType.RelocationAddends:
+ goto default;
+ case SectionType.HashTable:
+ goto default;
+ case SectionType.Dynamic:
+ returned = new DynamicSection(header, reader, this);
+ break;
+ case SectionType.Note:
+ returned = new NoteSection(header, reader);
+ break;
+ case SectionType.NoBits:
+ goto default;
+ case SectionType.Relocation:
+ goto default;
+ case SectionType.Shlib:
+ goto default;
+ case SectionType.DynamicSymbolTable:
+ returned = new SymbolTable(header, reader, dynamicStringTable, this);
+ break;
+ default:
+ returned = new Section(header, reader);
+ break;
+ }
+
+ return returned;
+ }
+
+ private void ReadSegmentHeaders()
+ {
+ segments = new List>(segmentHeaderEntryCount);
+
+ for (var i = 0u; i < segmentHeaderEntryCount; i++)
+ {
+ var seekTo = segmentHeaderOffset + i * segmentHeaderEntrySize;
+ reader.BaseStream.Seek(seekTo, SeekOrigin.Begin);
+ var segmentType = Segment.ProbeType(reader);
+
+ Segment segment;
+ if (segmentType == SegmentType.Note)
+ segment = new NoteSegment(segmentHeaderOffset + i * segmentHeaderEntrySize, Class, reader);
+ else
+ segment = new Segment(segmentHeaderOffset + i * segmentHeaderEntrySize, Class, reader);
+
+ segments.Add(segment);
+ }
+ }
+
+ private void ReadSections()
+ {
+ sectionHeaders = new List();
+ if (HasSectionsStringTable)
+ sectionIndicesByName = new Dictionary();
+
+ for (var i = 0; i < sectionHeaderEntryCount; i++)
+ {
+ var header = ReadSectionHeader(i);
+ sectionHeaders.Add(header);
+ if (HasSectionsStringTable)
+ {
+ var name = header.Name;
+ if (!sectionIndicesByName.ContainsKey(name))
+ sectionIndicesByName.Add(name, i);
+ else
+ sectionIndicesByName[name] = SectionNameNotUniqueMarker;
+ }
+ }
+
+ sections = new List>(Enumerable.Repeat>(
+ null,
+ sectionHeaders.Count
+ ));
+ FindStringTables();
+ for (var i = 0; i < sectionHeaders.Count; i++)
+ TouchSection(i);
+ sectionHeaders = null;
+ currentStage = Stage.AfterSectionsAreRead;
+ }
+
+ private void TouchSection(int index)
+ {
+ if (currentStage != Stage.Initalizing)
+ throw new InvalidOperationException("TouchSection invoked in improper state.");
+ if (sections[index] != null)
+ return;
+ var section = GetSectionFromSectionHeader(sectionHeaders[index]);
+ sections[index] = section;
+ }
+
+ private void FindStringTables()
+ {
+ TryGetSection(Consts.ObjectsStringTableName, out var section);
+ objectsStringTable = (StringTable)section;
+ TryGetSection(Consts.DynamicStringTableName, out section);
+
+ // It might happen that the section is not really available, represented as a NoBits one.
+ dynamicStringTable = section as StringTable;
+ }
+
+ private void ReadStringTable()
+ {
+ if (!HasSectionHeader || !HasSectionsStringTable)
+ return;
+
+ var header = ReadSectionHeader(checked((int)stringTableIndex));
+ if (header.Type != SectionType.StringTable)
+ throw new InvalidOperationException(
+ "Given index of section header does not point at string table which was expected.");
+
+ SectionsStringTable = new StringTable(header, reader);
+ }
+
+ private SectionHeader ReadSectionHeader(int index, bool ignoreUpperLimit = false)
+ {
+ if (index < 0 || (!ignoreUpperLimit && index >= sectionHeaderEntryCount))
+ throw new ArgumentOutOfRangeException(nameof(index));
+
+ reader.BaseStream.Seek(
+ sectionHeaderOffset + index * sectionHeaderEntrySize,
+ SeekOrigin.Begin
+ );
+
+ return new SectionHeader(reader, Class, SectionsStringTable);
+ }
+
+ private SimpleEndianessAwareReader ObtainEndianessAwareReader(Stream stream)
+ {
+ var reader = new BinaryReader(stream);
+ reader.ReadBytes(4); // ELF magic
+ var classByte = reader.ReadByte();
+
+ Class = classByte switch
+ {
+ 1 => Class.Bit32,
+ 2 => Class.Bit64,
+ _ => throw new ArgumentException($"Given ELF file is of unknown class {classByte}.")
+ };
+
+ var endianessByte = reader.ReadByte();
+
+ Endianess = endianessByte switch
+ {
+ 1 => Endianess.LittleEndian,
+ 2 => Endianess.BigEndian,
+ _ => throw new ArgumentException($"Given ELF file uses unknown endianess {endianessByte}.")
+ };
+
+ reader.ReadBytes(10); // padding bytes of section e_ident
+ return new SimpleEndianessAwareReader(stream, Endianess);
+ }
+
+ private void ReadFields()
+ {
+ Type = (FileType)reader.ReadUInt16();
+ Machine = (Machine)reader.ReadUInt16();
+ var version = reader.ReadUInt32();
+ if (version != 1)
+ throw new ArgumentException(string.Format(
+ "Given ELF file is of unknown version {0}.",
+ version
+ ));
+ EntryPoint = (Class == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To();
+ // TODO: assertions for (u)longs
+ segmentHeaderOffset = Class == Class.Bit32 ? reader.ReadUInt32() : reader.ReadInt64();
+ sectionHeaderOffset = Class == Class.Bit32 ? reader.ReadUInt32() : reader.ReadInt64();
+ MachineFlags = reader.ReadUInt32().To(); // TODO: always 32bit?
+ reader.ReadUInt16(); // elf header size
+ segmentHeaderEntrySize = reader.ReadUInt16();
+ segmentHeaderEntryCount = reader.ReadUInt16();
+ sectionHeaderEntrySize = reader.ReadUInt16();
+ sectionHeaderEntryCount = reader.ReadUInt16();
+ stringTableIndex = reader.ReadUInt16();
+
+ // If the number of sections is greater than or equal to SHN_LORESERVE (0xff00), this member has the
+ // value zero and the actual number of section header table entries is contained in the sh_size field
+ // of the section header at index 0. (Otherwise, the sh_size member of the initial entry contains 0.)
+ if (sectionHeaderEntryCount == 0)
+ {
+ var firstSectionHeader = ReadSectionHeader(0, true);
+ sectionHeaderEntryCount = checked((uint)firstSectionHeader.Size);
+
+ // If the index of the string table is larger than or equal to SHN_LORESERVE (0xff00), this member holds SHN_XINDEX (0xffff)
+ // and the real index of the section name string table section is held in the sh_link member of the initial entry in section
+ // header table. Otherwise, the sh_link member of the initial entry in section header table contains the value zero.
+ if (stringTableIndex == 0xffff)
+ stringTableIndex = checked(firstSectionHeader.Link);
+ }
+ }
+
+ private GetSectionResult TryGetSectionInner(string name, out Section section)
+ {
+ section = default;
+ if (!HasSectionsStringTable)
+ return GetSectionResult.NoSectionsStringTable;
+ if (!sectionIndicesByName.TryGetValue(name, out var index))
+ return GetSectionResult.NoSuchSection;
+ if (index == SectionNameNotUniqueMarker)
+ return GetSectionResult.SectionNameNotUnique;
+ return TryGetSectionInner(index, out section);
+ }
+
+ private GetSectionResult TryGetSectionInner(int index, out Section section)
+ {
+ section = default;
+ if (index >= sections.Count)
+ return GetSectionResult.NoSuchSection;
+ if (sections[index] != null)
+ {
+ section = sections[index];
+ return GetSectionResult.Success;
+ }
+
+ if (currentStage != Stage.Initalizing)
+ throw new InvalidOperationException(
+ "Assert not met: null section by proper index in not initializing stage.");
+ TouchSection(index);
+ section = sections[index];
+ return GetSectionResult.Success;
+ }
+
+ private enum Stage
+ {
+ Initalizing,
+ AfterSectionsAreRead
+ }
+
+ private enum GetSectionResult
+ {
+ Success,
+ SectionNameNotUnique,
+ NoSectionsStringTable,
+ NoSuchSection
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELFReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELFReader.cs
new file mode 100644
index 0000000000..13521bb3ae
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELFReader.cs
@@ -0,0 +1,116 @@
+using System;
+using System.IO;
+using System.Text;
+
+#nullable disable
+
+namespace ELFSharp.ELF
+{
+ internal static class ELFReader
+ {
+ private const string NotELFMessage = "Given stream is not a proper ELF file.";
+
+ private static readonly byte[] Magic =
+ {
+ 0x7F,
+ 0x45,
+ 0x4C,
+ 0x46
+ }; // 0x7F 'E' 'L' 'F'
+
+ public static IELF Load(Stream stream, bool shouldOwnStream)
+ {
+ if (!TryLoad(stream, shouldOwnStream, out var elf))
+ throw new ArgumentException(NotELFMessage);
+
+ return elf;
+ }
+
+ public static IELF Load(string fileName)
+ {
+ return Load(File.OpenRead(fileName), true);
+ }
+
+ public static bool TryLoad(Stream stream, bool shouldOwnStream, out IELF elf)
+ {
+ switch (CheckELFType(stream))
+ {
+ case Class.Bit32:
+ elf = new ELF(stream, shouldOwnStream);
+ return true;
+ case Class.Bit64:
+ elf = new ELF(stream, shouldOwnStream);
+ return true;
+ default:
+ elf = null;
+ stream.Close();
+ return false;
+ }
+ }
+
+ public static bool TryLoad(string fileName, out IELF elf)
+ {
+ return TryLoad(File.OpenRead(fileName), true, out elf);
+ }
+
+ public static Class CheckELFType(Stream stream)
+ {
+ var currentStreamPosition = stream.Position;
+
+ if (stream.Length < Consts.MinimalELFSize)
+ return Class.NotELF;
+
+ using (var reader = new BinaryReader(stream, Encoding.UTF8, true))
+ {
+ var magic = reader.ReadBytes(4);
+ for (var i = 0; i < 4; i++)
+ if (magic[i] != Magic[i])
+ return Class.NotELF;
+
+ var value = reader.ReadByte();
+ stream.Position = currentStreamPosition;
+ return value == 1 ? Class.Bit32 : Class.Bit64;
+ }
+ }
+
+ public static Class CheckELFType(string fileName)
+ {
+ using (var stream = File.OpenRead(fileName))
+ {
+ return CheckELFType(stream);
+ }
+ }
+
+ public static ELF Load(Stream stream, bool shouldOwnStream) where T : struct
+ {
+ if (CheckELFType(stream) == Class.NotELF)
+ throw new ArgumentException(NotELFMessage);
+
+ return new ELF(stream, shouldOwnStream);
+ }
+
+ public static ELF Load(string fileName) where T : struct
+ {
+ return Load(File.OpenRead(fileName), true);
+ }
+
+ public static bool TryLoad(Stream stream, bool shouldOwnStream, out ELF elf) where T : struct
+ {
+ switch (CheckELFType(stream))
+ {
+ case Class.Bit32:
+ case Class.Bit64:
+ elf = new ELF(stream, shouldOwnStream);
+ return true;
+ default:
+ elf = null;
+ return false;
+ }
+ }
+
+ public static bool TryLoad(string fileName, out ELF elf) where T : struct
+ {
+ return TryLoad(File.OpenRead(fileName), true, out elf);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/FileType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/FileType.cs
new file mode 100644
index 0000000000..9e04bf8ab9
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/FileType.cs
@@ -0,0 +1,11 @@
+namespace ELFSharp.ELF
+{
+ internal enum FileType : ushort
+ {
+ None = 0,
+ Relocatable,
+ Executable,
+ SharedObject,
+ Core
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/IELF.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/IELF.cs
new file mode 100644
index 0000000000..9310425cdd
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/IELF.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using ELFSharp.ELF.Sections;
+using ELFSharp.ELF.Segments;
+
+namespace ELFSharp.ELF
+{
+ internal interface IELF : IDisposable
+ {
+ public Endianess Endianess { get; }
+ public Class Class { get; }
+ public FileType Type { get; }
+ public Machine Machine { get; }
+ public bool HasSegmentHeader { get; }
+ public bool HasSectionHeader { get; }
+ public bool HasSectionsStringTable { get; }
+ public IReadOnlyList Segments { get; }
+ public IStringTable SectionsStringTable { get; }
+ public IReadOnlyList Sections { get; }
+ public IEnumerable GetSections() where T : ISection;
+ public bool TryGetSection(string name, out ISection section);
+ public ISection GetSection(string name);
+ public bool TryGetSection(int index, out ISection section);
+ public ISection GetSection(int index);
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Machine.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Machine.cs
new file mode 100644
index 0000000000..42e823288a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Machine.cs
@@ -0,0 +1,169 @@
+/*
+ * This file is based on LLVM's elf.h file. You can find its license
+ * in the LICENSE file.
+ *
+ */
+
+namespace ELFSharp.ELF
+{
+ internal enum Machine : ushort
+ {
+ None = 0, // No machine
+ M32 = 1, // AT&T WE 32100
+ SPARC = 2, // SPARC
+ Intel386 = 3, // Intel 386
+ M68K = 4, // Motorola 68000
+ M88K = 5, // Motorola 88000
+ Intel486 = 6, // Intel 486 (deprecated)
+ Intel860 = 7, // Intel 80860
+ MIPS = 8, // MIPS R3000
+ S370 = 9, // IBM System/370
+ MIPSRS3LE = 10, // MIPS RS3000 Little-endian
+ PARISC = 15, // Hewlett-Packard PA-RISC
+ VPP500 = 17, // Fujitsu VPP500
+ SPARC32Plus = 18, // Enhanced instruction set SPARC
+ Intel960 = 19, // Intel 80960
+ PPC = 20, // PowerPC
+ PPC64 = 21, // PowerPC64
+ S390 = 22, // IBM System/390
+ SPU = 23, // IBM SPU/SPC
+ V800 = 36, // NEC V800
+ FR20 = 37, // Fujitsu FR20
+ RH32 = 38, // TRW RH-32
+ RCE = 39, // Motorola RCE
+ ARM = 40, // ARM
+ Alpha = 41, // DEC Alpha
+ SuperH = 42, // Hitachi SH
+ SPARCv9 = 43, // SPARC V9
+ TriCore = 44, // Siemens TriCore
+ ARC = 45, // Argonaut RISC Core
+ H8300 = 46, // Hitachi H8/300
+ H8300H = 47, // Hitachi H8/300H
+ H8S = 48, // Hitachi H8S
+ H8500 = 49, // Hitachi H8/500
+ IA64 = 50, // Intel IA-64 processor architecture
+ MIPSX = 51, // Stanford MIPS-X
+ ColdFire = 52, // Motorola ColdFire
+ M68HC12 = 53, // Motorola M68HC12
+ MMA = 54, // Fujitsu MMA Multimedia Accelerator
+ PCP = 55, // Siemens PCP
+ NCPU = 56, // Sony nCPU embedded RISC processor
+ NDR1 = 57, // Denso NDR1 microprocessor
+ StarCore = 58, // Motorola Star*Core processor
+ ME16 = 59, // Toyota ME16 processor
+ ST100 = 60, // STMicroelectronics ST100 processor
+ TinyJ = 61, // Advanced Logic Corp. TinyJ embedded processor family
+ AMD64 = 62, // AMD x86-64 architecture
+ PDSP = 63, // Sony DSP Processor
+ PDP10 = 64, // Digital Equipment Corp. PDP-10
+ PDP11 = 65, // Digital Equipment Corp. PDP-11
+ FX66 = 66, // Siemens FX66 microcontroller
+ ST9PLUS = 67, // STMicroelectronics ST9+ 8/16 bit microcontroller
+ ST7 = 68, // STMicroelectronics ST7 8-bit microcontroller
+ M68HC16 = 69, // Motorola MC68HC16 Microcontroller
+ M68HC11 = 70, // Motorola MC68HC11 Microcontroller
+ M68HC08 = 71, // Motorola MC68HC08 Microcontroller
+ M68HC05 = 72, // Motorola MC68HC05 Microcontroller
+ SVX = 73, // Silicon Graphics SVx
+ ST19 = 74, // STMicroelectronics ST19 8-bit microcontroller
+ VAX = 75, // Digital VAX
+ CRIS = 76, // Axis Communications 32-bit embedded processor
+ Javelin = 77, // Infineon Technologies 32-bit embedded processor
+ FirePath = 78, // Element 14 64-bit DSP Processor
+ ZSP = 79, // LSI Logic 16-bit DSP Processor
+ MMIX = 80, // Donald Knuth's educational 64-bit processor
+ HUANY = 81, // Harvard University machine-independent object files
+ PRISM = 82, // SiTera Prism
+ AVR = 83, // Atmel AVR 8-bit microcontroller
+ FR30 = 84, // Fujitsu FR30
+ D10V = 85, // Mitsubishi D10V
+ D30V = 86, // Mitsubishi D30V
+ V850 = 87, // NEC v850
+ M32R = 88, // Mitsubishi M32R
+ MN10300 = 89, // Matsushita MN10300
+ MN10200 = 90, // Matsushita MN10200
+ PicoJava = 91, // picoJava
+ OpenRISC = 92, // OpenRISC 32-bit embedded processor
+ ARCompact = 93, // ARC International ARCompact processo
+ Xtensa = 94, // Tensilica Xtensa Architecture
+ VideoCore = 95, // Alphamosaic VideoCore processor
+ TMMGPP = 96, // Thompson Multimedia General Purpose Processor
+ NS32K = 97, // National Semiconductor 32000 series
+ TPC = 98, // Tenor Network TPC processor
+ SNP1k = 99, // Trebia SNP 1000 processor
+ ST200 = 100, // STMicroelectronics (www.st.com) ST200
+ IP2K = 101, // Ubicom IP2xxx microcontroller family
+ MAX = 102, // MAX Processor
+ CompactRISC = 103, // National Semiconductor CompactRISC microprocessor
+ F2MC16 = 104, // Fujitsu F2MC16
+ MSP430 = 105, // Texas Instruments embedded microcontroller msp430
+ Blackfin = 106, // Analog Devices Blackfin (DSP) processor
+ S1C33 = 107, // S1C33 Family of Seiko Epson processors
+ SEP = 108, // Sharp embedded microprocessor
+ ArcaRISC = 109, // Arca RISC Microprocessor
+ UNICORE = 110, // Microprocessor series from PKU-Unity Ltd. and MPRC of Peking University
+ Excess = 111, // eXcess: 16/32/64-bit configurable embedded CPU
+ DXP = 112, // Icera Semiconductor Inc. Deep Execution Processor
+ AlteraNios2 = 113, // Altera Nios II soft-core processor
+ CRX = 114, // National Semiconductor CompactRISC CRX
+ XGATE = 115, // Motorola XGATE embedded processor
+ C166 = 116, // Infineon C16x/XC16x processor
+ M16C = 117, // Renesas M16C series microprocessors
+ DSPIC30F = 118, // Microchip Technology dsPIC30F Digital Signal
+
+ // Controller
+ EngineRISC = 119, // Freescale Communication Engine RISC core
+ M32C = 120, // Renesas M32C series microprocessors
+ TSK3000 = 131, // Altium TSK3000 core
+ RS08 = 132, // Freescale RS08 embedded processor
+ SHARC = 133, // Analog Devices SHARC family of 32-bit DSP processors
+ ECOG2 = 134, // Cyan Technology eCOG2 microprocessor
+ Score7 = 135, // Sunplus S+core7 RISC processor
+ DSP24 = 136, // New Japan Radio (NJR) 24-bit DSP Processor
+ VideoCore3 = 137, // Broadcom VideoCore III processor
+ LatticeMico32 = 138, // RISC processor for Lattice FPGA architecture
+ SeikoEpsonC17 = 139, // Seiko Epson C17 family
+ TIC6000 = 140, // The Texas Instruments TMS320C6000 DSP family
+ TIC2000 = 141, // The Texas Instruments TMS320C2000 DSP family
+ TIC5500 = 142, // The Texas Instruments TMS320C55x DSP family
+ MMDSPPlus = 160, // STMicroelectronics 64bit VLIW Data Signal Processor
+ CypressM8C = 161, // Cypress M8C microprocessor
+ R32C = 162, // Renesas R32C series microprocessors
+ TriMedia = 163, // NXP Semiconductors TriMedia architecture family
+ Hexagon = 164, // Qualcomm Hexagon processor
+ Intel8051 = 165, // Intel 8051 and variants
+ STxP7x = 166, // STMicroelectronics STxP7x family of configurable and extensible RISC processors
+ NDS32 = 167, // Andes Technology compact code size embedded RISC processor family
+ ECOG1 = 168, // Cyan Technology eCOG1X family
+ ECOG1X = 168, // Cyan Technology eCOG1X family
+ MAXQ30 = 169, // Dallas Semiconductor MAXQ30 Core Micro-controllers
+ XIMO16 = 170, // New Japan Radio (NJR) 16-bit DSP Processor
+ MANIK = 171, // M2000 Reconfigurable RISC Microprocessor
+ CrayNV2 = 172, // Cray Inc. NV2 vector architecture
+ RX = 173, // Renesas RX family
+ METAG = 174, // Imagination Technologies META processor architecture
+ MCSTElbrus = 175, // MCST Elbrus general purpose hardware architecture
+ ECOG16 = 176, // Cyan Technology eCOG16 family
+ CR16 = 177, // National Semiconductor CompactRISC CR16 16-bit microprocessor
+ ETPU = 178, // Freescale Extended Time Processing Unit
+ SLE9X = 179, // Infineon Technologies SLE9X core
+ L10M = 180, // Intel L10M
+ K10M = 181, // Intel K10M
+ AArch64 = 183, // ARM AArch64
+ AVR32 = 185, // Atmel Corporation 32-bit microprocessor family
+ STM8 = 186, // STMicroeletronics STM8 8-bit microcontroller
+ TILE64 = 187, // Tilera TILE64 multicore architecture family
+ TILEPro = 188, // Tilera TILEPro multicore architecture family
+ CUDA = 190, // NVIDIA CUDA architecture
+ TILEGx = 191, // Tilera TILE-Gx multicore architecture family
+ CloudShield = 192, // CloudShield architecture family
+ CoreA1st = 193, // KIPO-KAIST Core-A 1st generation processor family
+ CoreA2nd = 194, // KIPO-KAIST Core-A 2nd generation processor family
+ ARCompact2 = 195, // Synopsys ARCompact V2
+ Open8 = 196, // Open8 8-bit RISC soft processor core
+ RL78 = 197, // Renesas RL78 family
+ VideoCore5 = 198, // Broadcom VideoCore V processor
+ R78KOR = 199, // Renesas 78KOR family
+ F56800EX = 200 // Freescale 56800EX Digital Signal Controller (DSC)
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicEntry.cs
new file mode 100644
index 0000000000..64d2c50b6a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicEntry.cs
@@ -0,0 +1,27 @@
+#nullable disable
+
+namespace ELFSharp.ELF.Sections
+{
+ ///
+ /// Dynamic table entries are made up of a 32 bit or 64 bit "tag"
+ /// and a 32 bit or 64 bit union (val/pointer in 64 bit, val/pointer/offset in 32 bit).
+ /// See LLVM elf.h file for the C/C++ version.
+ ///
+ internal class DynamicEntry : IDynamicEntry
+ {
+ public DynamicEntry(T tagValue, T value)
+ {
+ Tag = (DynamicTag)tagValue.To();
+ Value = value;
+ }
+
+ public T Value { get; }
+
+ public DynamicTag Tag { get; }
+
+ public override string ToString()
+ {
+ return string.Format("{0} \t 0x{1:X}", Tag, Value);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicSection.cs
new file mode 100644
index 0000000000..00c6ad3424
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicSection.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using ELFSharp.Utilities;
+
+#nullable disable
+
+namespace ELFSharp.ELF.Sections
+{
+ internal sealed class DynamicSection : Section, IDynamicSection where T : struct
+ {
+ private readonly ELF elf;
+
+ private List> entries;
+
+ internal DynamicSection(SectionHeader header, SimpleEndianessAwareReader reader, ELF elf) : base(header,
+ reader)
+ {
+ this.elf = elf;
+ ReadEntries();
+ }
+
+ public IEnumerable> Entries => new ReadOnlyCollection>(entries);
+
+ IEnumerable IDynamicSection.Entries => entries;
+
+ public override string ToString()
+ {
+ return string.Format("{0}: {2}, load @0x{4:X}, {5} entries", Name, NameIndex, Type, RawFlags, LoadAddress,
+ Entries.Count());
+ }
+
+ private void ReadEntries()
+ {
+ // "Kind-of" Bug:
+ // So, this winds up with "extra" DT_NULL entries for some executables. The issue
+ // is basically that sometimes the .dynamic section's size (and # of entries) per the
+ // header is higher than the actual # of entries. The extra space gets filled with null
+ // entries in all of the ELF files I tested, so we shouldn't end up with any 'incorrect' entries
+ // here unless someone is messing with the ELF structure.
+
+ SeekToSectionBeginning();
+ var entryCount = elf.Class == Class.Bit32 ? Header.Size / 8 : Header.Size / 16;
+ entries = new List>();
+
+ for (ulong i = 0; i < entryCount; i++)
+ if (elf.Class == Class.Bit32)
+ entries.Add(new DynamicEntry(Reader.ReadUInt32().To(), Reader.ReadUInt32().To()));
+ else if (elf.Class == Class.Bit64)
+ entries.Add(new DynamicEntry(Reader.ReadUInt64().To(), Reader.ReadUInt64().To()));
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicTag.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicTag.cs
new file mode 100644
index 0000000000..716874df4c
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicTag.cs
@@ -0,0 +1,64 @@
+namespace ELFSharp.ELF.Sections
+{
+ ///
+ /// This enum holds some of the possible values for the DynamicTag value (dropping platform
+ /// specific contents, such as MIPS flags.)
+ /// Values are coming from LLVM's elf.h headers.
+ /// File can be found in LLVM 3.8.1 source at:
+ /// ../include/llvm/support/elf.h
+ /// License of the original C code is LLVM license.
+ ///
+ internal enum DynamicTag : ulong
+ {
+ Null = 0, // Marks end of dynamic array.
+ Needed = 1, // String table offset of needed library.
+ PLTRelSz = 2, // Size of relocation entries in PLT.
+ PLTGOT = 3, // Address associated with linkage table.
+ Hash = 4, // Address of symbolic hash table.
+ StrTab = 5, // Address of dynamic string table.
+ SymTab = 6, // Address of dynamic symbol table.
+ RelA = 7, // Address of relocation table (Rela entries).
+ RelASz = 8, // Size of Rela relocation table.
+ RelAEnt = 9, // Size of a Rela relocation entry.
+ StrSz = 10, // Total size of the string table.
+ SymEnt = 11, // Size of a symbol table entry.
+ Init = 12, // Address of initialization function.
+ Fini = 13, // Address of termination function.
+ SoName = 14, // String table offset of a shared objects name.
+ RPath = 15, // String table offset of library search path.
+ Symbolic = 16, // Changes symbol resolution algorithm.
+ Rel = 17, // Address of relocation table (Rel entries).
+ RelSz = 18, // Size of Rel relocation table.
+ RelEnt = 19, // Size of a Rel relocation entry.
+ PLTRel = 20, // Type of relocation entry used for linking.
+ Debug = 21, // Reserved for debugger.
+ TextRel = 22, // Relocations exist for non-writable segments.
+ JmpRel = 23, // Address of relocations associated with PLT.
+ BindNow = 24, // Process all relocations before execution.
+ InitArray = 25, // Pointer to array of initialization functions.
+ FiniArray = 26, // Pointer to array of termination functions.
+ InitArraySz = 27, // Size of DT_INIT_ARRAY.
+ FiniArraySz = 28, // Size of DT_FINI_ARRAY.
+ RunPath = 29, // String table offset of lib search path.
+ Flags = 30, // Flags.
+ Encoding = 32, // Values from here to DT_LOOS follow the rules for the interpretation of the d_un union.
+
+ PreInitArray = 32, // Pointer to array of preinit functions.
+ PreInitArraySz = 33, // Size of the DT_PREINIT_ARRAY array.
+ LoOS = 0x60000000, // Start of environment specific tags.
+ HiOS = 0x6FFFFFFF, // End of environment specific tags.
+ LoProc = 0x70000000, // Start of processor specific tags.
+ HiProc = 0x7FFFFFFF, // End of processor specific tags.
+ GNUHash = 0x6FFFFEF5, // Reference to the GNU hash table.
+ TLSDescPLT = 0x6FFFFEF6, // Location of PLT entry for TLS descriptor resolver calls.
+ TLSDescGOT = 0x6FFFFEF7, // Location of GOT entry used by TLS descriptor resolver PLT entry.
+ RelACount = 0x6FFFFFF9, // ELF32_Rela count.
+ RelCount = 0x6FFFFFFA, // ELF32_Rel count.
+ Flags1 = 0X6FFFFFFB, // Flags_1.
+ VerSym = 0x6FFFFFF0, // The address of .gnu.version section.
+ VerDef = 0X6FFFFFFC, // The address of the version definition table.
+ VerDefNum = 0X6FFFFFFD, // The number of entries in DT_VERDEF.
+ VerNeed = 0X6FFFFFFE, // The address of the version Dependency table.
+ VerNeedNum = 0X6FFFFFFF // The number of entries in DT_VERNEED.
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicEntry.cs
new file mode 100644
index 0000000000..40fd1d65c3
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicEntry.cs
@@ -0,0 +1,12 @@
+namespace ELFSharp.ELF.Sections
+{
+ ///
+ /// Represents an entry in the dynamic table.
+ /// Interface--because this is a union type in C, if we want more detail at some point on the values in the Union type,
+ /// we can have separate classes.
+ ///
+ internal interface IDynamicEntry
+ {
+ public DynamicTag Tag { get; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicSection.cs
new file mode 100644
index 0000000000..1b88de7dc9
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicSection.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace ELFSharp.ELF.Sections
+{
+ internal interface IDynamicSection : ISection
+ {
+ public IEnumerable Entries { get; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/INoteSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/INoteSection.cs
new file mode 100644
index 0000000000..82516c4653
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/INoteSection.cs
@@ -0,0 +1,8 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal interface INoteSection : ISection
+ {
+ public string NoteName { get; }
+ public byte[] Description { get; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IProgBitsSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IProgBitsSection.cs
new file mode 100644
index 0000000000..e98415ceaf
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IProgBitsSection.cs
@@ -0,0 +1,7 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal interface IProgBitsSection : ISection
+ {
+ public void WriteContents(byte[] destination, int offset, int length = 0);
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISection.cs
new file mode 100644
index 0000000000..23f812c7e7
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISection.cs
@@ -0,0 +1,11 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal interface ISection
+ {
+ public string Name { get; }
+ public uint NameIndex { get; }
+ public SectionType Type { get; }
+ public SectionFlags Flags { get; }
+ public byte[] GetContents();
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IStringTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IStringTable.cs
new file mode 100644
index 0000000000..f529c273c5
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IStringTable.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace ELFSharp.ELF.Sections
+{
+ internal interface IStringTable : ISection
+ {
+ public string this[long index] { get; }
+ public IEnumerable Strings { get; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolEntry.cs
new file mode 100644
index 0000000000..ac1ced6a9b
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolEntry.cs
@@ -0,0 +1,13 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal interface ISymbolEntry
+ {
+ public string Name { get; }
+ public SymbolBinding Binding { get; }
+ public SymbolType Type { get; }
+ public SymbolVisibility Visibility { get; }
+ public bool IsPointedIndexSpecial { get; }
+ public ISection PointedSection { get; }
+ public ushort PointedSectionIndex { get; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolTable.cs
new file mode 100644
index 0000000000..da9763727d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolTable.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace ELFSharp.ELF.Sections
+{
+ internal interface ISymbolTable : ISection
+ {
+ public IEnumerable Entries { get; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteData.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteData.cs
new file mode 100644
index 0000000000..e26b4f4ee3
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteData.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Text;
+using ELFSharp.ELF.Segments;
+using ELFSharp.Utilities;
+
+#nullable disable
+
+namespace ELFSharp.ELF.Sections
+{
+ internal class NoteData : INoteData
+ {
+ public const ulong NoteDataHeaderSize = 12; // name size + description size + field
+
+ private readonly SimpleEndianessAwareReader reader;
+
+ internal NoteData(ulong sectionOffset, ulong sectionSize, SimpleEndianessAwareReader reader)
+ {
+ this.reader = reader;
+ var sectionEnd = (long)(sectionOffset + sectionSize);
+ reader.BaseStream.Seek((long)sectionOffset, SeekOrigin.Begin);
+ var nameSize = ReadSize();
+ var descriptionSize = ReadSize();
+ Type = ReadField();
+ int remainder;
+ var fields = Math.DivRem(nameSize, FieldSize, out remainder);
+ var alignedNameSize = FieldSize * (remainder > 0 ? fields + 1 : fields);
+
+ fields = Math.DivRem(descriptionSize, FieldSize, out remainder);
+ var alignedDescriptionSize = FieldSize * (remainder > 0 ? fields + 1 : fields);
+
+ // We encountered binaries where nameSize and descriptionSize are
+ // invalid (i.e. significantly larger than the size of the binary itself).
+ // To avoid throwing on such binaries, we only read in name and description
+ // if the sizes are within range of the containing section.
+ if (reader.BaseStream.Position + alignedNameSize <= sectionEnd)
+ {
+ var name = reader.ReadBytes(alignedNameSize);
+ if (nameSize > 0)
+ Name = Encoding.UTF8.GetString(name, 0, nameSize - 1); // minus one to omit terminating NUL
+ if (reader.BaseStream.Position + descriptionSize <= sectionEnd)
+ DescriptionBytes = descriptionSize > 0 ? reader.ReadBytes(descriptionSize) : new byte[0];
+ }
+
+ // If there are multiple notes inside one segment, keep track of the end position so we can read them
+ // all when parsing the segment
+ NoteOffset = sectionOffset;
+ NoteFileSize = (ulong)alignedNameSize + (ulong)alignedDescriptionSize + NoteDataHeaderSize;
+ }
+
+ internal byte[] DescriptionBytes { get; }
+
+ internal ulong NoteOffset { get; }
+ internal ulong NoteFileSize { get; }
+ internal ulong NoteFileEnd => NoteOffset + NoteFileSize;
+
+ private int FieldSize => 4;
+
+ public string Name { get; }
+
+ public ReadOnlyCollection Description => new ReadOnlyCollection(DescriptionBytes);
+
+ public ulong Type { get; }
+
+ public Stream ToStream()
+ {
+ return new MemoryStream(DescriptionBytes);
+ }
+
+ public override string ToString()
+ {
+ return $"Name={Name} DataSize=0x{DescriptionBytes.Length.ToString("x8")}";
+ }
+
+ private int ReadSize()
+ {
+ /*
+ * According to some versions of ELF64 specfication, in 64-bit ELF files words, of which
+ * such section consists, should have 8 byte length. However, this is not the case in
+ * some other specifications (some of theme contradicts with themselves like the 64bit MIPS
+ * one). In real life scenarios I also observed that note sections are identical in both
+ * ELF classes. There is also only one structure (i.e. Elf_External_Note) in existing and
+ * well tested GNU tools.
+ *
+ * Nevertheless I leave here the whole machinery as it is already written and may be useful
+ * some day.
+ */
+ return reader.ReadInt32();
+ }
+
+ private ulong ReadField()
+ {
+ // see comment above
+ return reader.ReadUInt32();
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteSection.cs
new file mode 100644
index 0000000000..145c21daaa
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteSection.cs
@@ -0,0 +1,25 @@
+using ELFSharp.Utilities;
+
+namespace ELFSharp.ELF.Sections
+{
+ internal sealed class NoteSection : Section, INoteSection where T : struct
+ {
+ private readonly NoteData data;
+
+ internal NoteSection(SectionHeader header, SimpleEndianessAwareReader reader) : base(header, reader)
+ {
+ data = new NoteData(header.Offset, header.Size, reader);
+ }
+
+ public T NoteType => data.Type.To();
+
+ public string NoteName => data.Name;
+
+ public byte[] Description => data.DescriptionBytes;
+
+ public override string ToString()
+ {
+ return string.Format("{0}: {2}, Type={1}", Name, NoteType, Type);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ProgBitsSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ProgBitsSection.cs
new file mode 100644
index 0000000000..4d1f39ea2c
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ProgBitsSection.cs
@@ -0,0 +1,29 @@
+using System;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.ELF.Sections
+{
+ internal sealed class ProgBitsSection : Section, IProgBitsSection where T : struct
+ {
+ private const int BufferSize = 10 * 1024;
+
+ internal ProgBitsSection(SectionHeader header, SimpleEndianessAwareReader reader) : base(header, reader)
+ {
+ }
+
+
+ public void WriteContents(byte[] destination, int offset, int length = 0)
+ {
+ SeekToSectionBeginning();
+ if (length == 0 || (ulong)length > Header.Size)
+ length = Convert.ToInt32(Header.Size);
+ var remaining = length;
+ while (remaining > 0)
+ {
+ var buffer = Reader.ReadBytes(Math.Min(BufferSize, remaining));
+ buffer.CopyTo(destination, offset + (length - remaining));
+ remaining -= buffer.Length;
+ }
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/Section.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/Section.cs
new file mode 100644
index 0000000000..5c88fa1a1d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/Section.cs
@@ -0,0 +1,58 @@
+using System;
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.ELF.Sections
+{
+ internal class Section : ISection where T : struct
+ {
+ protected readonly SimpleEndianessAwareReader Reader;
+
+ internal Section(SectionHeader header, SimpleEndianessAwareReader reader)
+ {
+ Header = header;
+ Reader = reader;
+ }
+
+ public T RawFlags => Header.RawFlags.To();
+
+ public T LoadAddress => Header.LoadAddress.To();
+
+ public T Alignment => Header.Alignment.To();
+
+ public T EntrySize => Header.EntrySize.To();
+
+ public T Size => Header.Size.To();
+
+ public T Offset => Header.Offset.To();
+
+ internal SectionHeader Header { get; }
+
+ public virtual byte[] GetContents()
+ {
+ if (Type == SectionType.NoBits)
+ return Array.Empty();
+
+ Reader.BaseStream.Seek((long)Header.Offset, SeekOrigin.Begin);
+ return Reader.ReadBytes(Convert.ToInt32(Header.Size));
+ }
+
+ public string Name => Header.Name;
+
+ public uint NameIndex => Header.NameIndex;
+
+ public SectionType Type => Header.Type;
+
+ public SectionFlags Flags => Header.Flags;
+
+ public override string ToString()
+ {
+ return Header.ToString();
+ }
+
+ protected void SeekToSectionBeginning()
+ {
+ Reader.BaseStream.Seek((long)Header.Offset, SeekOrigin.Begin);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionFlags.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionFlags.cs
new file mode 100644
index 0000000000..7abd030186
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionFlags.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ELFSharp.ELF.Sections
+{
+ [Flags]
+ internal enum SectionFlags
+ {
+ Writable = 1,
+ Allocatable = 2,
+ Executable = 4
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionHeader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionHeader.cs
new file mode 100644
index 0000000000..54ea890727
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionHeader.cs
@@ -0,0 +1,70 @@
+using ELFSharp.Utilities;
+
+#nullable disable
+
+namespace ELFSharp.ELF.Sections
+{
+ internal sealed class SectionHeader
+ {
+ private readonly Class elfClass;
+
+ private readonly SimpleEndianessAwareReader reader;
+
+ private readonly IStringTable table;
+
+ // TODO: make elf consts file with things like SHT_LOUSER
+ internal SectionHeader(SimpleEndianessAwareReader reader, Class elfClass, IStringTable table = null)
+ {
+ this.reader = reader;
+ this.table = table;
+ this.elfClass = elfClass;
+ ReadSectionHeader();
+ }
+
+ internal string Name { get; private set; }
+ internal uint NameIndex { get; private set; }
+ internal SectionType Type { get; private set; }
+ internal SectionFlags Flags { get; private set; }
+ internal ulong RawFlags { get; private set; }
+ internal ulong LoadAddress { get; private set; }
+ internal ulong Alignment { get; private set; }
+ internal ulong EntrySize { get; private set; }
+ internal ulong Size { get; private set; }
+ internal ulong Offset { get; private set; }
+ internal uint Link { get; private set; }
+ internal uint Info { get; private set; }
+
+ public override string ToString()
+ {
+ return string.Format("{0}: {2}, load @0x{4:X}, {5} bytes long", Name, NameIndex, Type, RawFlags,
+ LoadAddress, Size);
+ }
+
+ private void ReadSectionHeader()
+ {
+ NameIndex = reader.ReadUInt32();
+ if (table != null)
+ Name = table[NameIndex];
+ Type = (SectionType)reader.ReadUInt32();
+ RawFlags = ReadAddress();
+ Flags = unchecked((SectionFlags)RawFlags);
+ LoadAddress = ReadAddress();
+ Offset = ReadOffset();
+ Size = ReadOffset();
+ Link = reader.ReadUInt32();
+ Info = reader.ReadUInt32();
+ Alignment = ReadAddress();
+ EntrySize = ReadAddress();
+ }
+
+ private ulong ReadAddress()
+ {
+ return elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64();
+ }
+
+ private ulong ReadOffset()
+ {
+ return elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64();
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionType.cs
new file mode 100644
index 0000000000..c1b3b006a1
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionType.cs
@@ -0,0 +1,18 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal enum SectionType : uint
+ {
+ Null = 0,
+ ProgBits,
+ SymbolTable,
+ StringTable,
+ RelocationAddends,
+ HashTable,
+ Dynamic,
+ Note,
+ NoBits,
+ Relocation,
+ Shlib,
+ DynamicSymbolTable
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionIndex.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionIndex.cs
new file mode 100644
index 0000000000..ca51e9e648
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionIndex.cs
@@ -0,0 +1,9 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal enum SpecialSectionIndex : ushort
+ {
+ Absolute = 0,
+ Common = 0xFFF1,
+ Undefined = 0xFFF2
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionType.cs
new file mode 100644
index 0000000000..710773e6cc
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionType.cs
@@ -0,0 +1,12 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal enum SpecialSectionType
+ {
+ Null,
+ ProgBits,
+ NoBits,
+ Shlib,
+ ProcessorSpecific,
+ UserSpecific
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/StringTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/StringTable.cs
new file mode 100644
index 0000000000..c1d710e708
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/StringTable.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.ELF.Sections
+{
+ internal sealed class StringTable : Section, IStringTable where T : struct
+ {
+ private readonly byte[] stringBlob;
+
+ private readonly Dictionary stringCache;
+ private bool cachePopulated;
+
+ internal StringTable(SectionHeader header, SimpleEndianessAwareReader reader) : base(header, reader)
+ {
+ stringCache = new Dictionary
+ {
+ { 0, string.Empty }
+ };
+ stringBlob = ReadStringData();
+ }
+
+ public IEnumerable Strings
+ {
+ get
+ {
+ if (!cachePopulated)
+ PrepopulateCache();
+ return stringCache.Values;
+ }
+ }
+
+ public string this[long index]
+ {
+ get
+ {
+ if (stringCache.TryGetValue(index, out var result))
+ return result;
+ return HandleUnexpectedIndex(index);
+ }
+ }
+
+ private string HandleUnexpectedIndex(long index)
+ {
+ var stringStart = (int)index;
+ for (var i = stringStart; i < stringBlob.Length; ++i)
+ if (stringBlob[i] == 0)
+ {
+ var str = Encoding.UTF8.GetString(stringBlob, stringStart, i - stringStart);
+ stringCache.Add(stringStart, str);
+ return str;
+ }
+
+ throw new IndexOutOfRangeException();
+ }
+
+ private void PrepopulateCache()
+ {
+ cachePopulated = true;
+
+ var stringStart = 1;
+ for (var i = 1; i < stringBlob.Length; ++i)
+ if (stringBlob[i] == 0)
+ {
+ if (!stringCache.ContainsKey(stringStart))
+ stringCache.Add(stringStart, Encoding.UTF8.GetString(stringBlob, stringStart, i - stringStart));
+ stringStart = i + 1;
+ }
+ }
+
+ private byte[] ReadStringData()
+ {
+ SeekToSectionBeginning();
+ var blob = Reader.ReadBytes((int)Header.Size);
+ Debug.Assert(blob.Length == 0 || (blob[0] == 0 && blob[blob.Length - 1] == 0),
+ "First and last bytes must be the null character (except for empty string tables)");
+ return blob;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolBinding.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolBinding.cs
new file mode 100644
index 0000000000..3f447e97b1
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolBinding.cs
@@ -0,0 +1,10 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal enum SymbolBinding
+ {
+ Local,
+ Global,
+ Weak,
+ ProcessorSpecific
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolEntry.cs
new file mode 100644
index 0000000000..445b9a445a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolEntry.cs
@@ -0,0 +1,60 @@
+using System;
+
+#nullable disable
+
+namespace ELFSharp.ELF.Sections
+{
+ internal class SymbolEntry : ISymbolEntry where T : struct
+ {
+ private readonly ELF elf;
+
+ public SymbolEntry(string name, T value, T size, SymbolVisibility visibility,
+ SymbolBinding binding, SymbolType type, ELF elf, ushort sectionIdx)
+ {
+ Name = name;
+ Value = value;
+ Size = size;
+ Binding = binding;
+ Type = type;
+ Visibility = visibility;
+ this.elf = elf;
+ PointedSectionIndex = sectionIdx;
+ }
+
+ public T Value { get; }
+
+ public T Size { get; }
+
+ public Section PointedSection => IsPointedIndexSpecial ? null : elf.GetSection(PointedSectionIndex);
+
+ public SpecialSectionIndex SpecialPointedSectionIndex
+ {
+ get
+ {
+ if (IsPointedIndexSpecial)
+ return (SpecialSectionIndex)PointedSectionIndex;
+ throw new InvalidOperationException("Given pointed section index does not have special meaning.");
+ }
+ }
+
+ public string Name { get; }
+
+ public SymbolBinding Binding { get; }
+
+ public SymbolType Type { get; }
+
+ public SymbolVisibility Visibility { get; }
+
+ public bool IsPointedIndexSpecial => Enum.IsDefined(typeof(SpecialSectionIndex), PointedSectionIndex);
+
+ ISection ISymbolEntry.PointedSection => PointedSection;
+
+ public ushort PointedSectionIndex { get; }
+
+ public override string ToString()
+ {
+ return string.Format("[{3} {4} {0}: 0x{1:X}, size: {2}, section idx: {5}]",
+ Name, Value, Size, Binding, Type, (SpecialSectionIndex)PointedSectionIndex);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolTable.cs
new file mode 100644
index 0000000000..86b2cab0b2
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolTable.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using ELFSharp.Utilities;
+
+#nullable disable
+
+namespace ELFSharp.ELF.Sections
+{
+ internal sealed class SymbolTable : Section, ISymbolTable where T : struct
+ {
+ private readonly ELF elf;
+ private readonly IStringTable table;
+
+ private List> entries;
+
+ internal SymbolTable(SectionHeader header, SimpleEndianessAwareReader Reader, IStringTable table, ELF elf) :
+ base(header, Reader)
+ {
+ this.table = table;
+ this.elf = elf;
+ ReadSymbols();
+ }
+
+ public IEnumerable> Entries => new ReadOnlyCollection>(entries);
+
+ IEnumerable ISymbolTable.Entries => Entries;
+
+ private void ReadSymbols()
+ {
+ SeekToSectionBeginning();
+ entries = new List>();
+ var adder = (ulong)(elf.Class == Class.Bit32 ? Consts.SymbolEntrySize32 : Consts.SymbolEntrySize64);
+ for (var i = 0UL; i < Header.Size; i += adder)
+ {
+ var value = 0UL;
+ var size = 0UL;
+ var nameIdx = Reader.ReadUInt32();
+
+ if (elf.Class == Class.Bit32)
+ {
+ value = Reader.ReadUInt32();
+ size = Reader.ReadUInt32();
+ }
+
+ var info = Reader.ReadByte();
+ var other = Reader.ReadByte();
+ var visibility = (SymbolVisibility)(other & 3); // Only three lowest bits are meaningful.
+ var sectionIdx = Reader.ReadUInt16();
+
+ if (elf.Class == Class.Bit64)
+ {
+ value = Reader.ReadUInt64();
+ size = Reader.ReadUInt64();
+ }
+
+ var name = table == null ? "" : table[nameIdx];
+ var binding = (SymbolBinding)(info >> 4);
+ var type = (SymbolType)(info & 0x0F);
+ entries.Add(new SymbolEntry(name, value.To(), size.To(), visibility, binding, type, elf,
+ sectionIdx));
+ }
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolType.cs
new file mode 100644
index 0000000000..ce2fa60b6f
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolType.cs
@@ -0,0 +1,12 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal enum SymbolType
+ {
+ NotSpecified,
+ Object,
+ Function,
+ Section,
+ File,
+ ProcessorSpecific
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolVisibility.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolVisibility.cs
new file mode 100644
index 0000000000..535762ece8
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolVisibility.cs
@@ -0,0 +1,10 @@
+namespace ELFSharp.ELF.Sections
+{
+ internal enum SymbolVisibility : byte
+ {
+ Default = 0,
+ Internal = 1,
+ Hidden = 2,
+ Protected = 3
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteData.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteData.cs
new file mode 100644
index 0000000000..c8e560581a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteData.cs
@@ -0,0 +1,31 @@
+using System.Collections.ObjectModel;
+using System.IO;
+
+namespace ELFSharp.ELF.Segments
+{
+ internal interface INoteData
+ {
+ ///
+ /// Owner of the note.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Data contents of the note. The format of this depends on the combination of the Name and Type properties and often
+ /// corresponds to a struct.
+ /// For example, see elf.h in the Linux kernel source tree.
+ ///
+ public ReadOnlyCollection Description { get; }
+
+ ///
+ /// Data type
+ ///
+ public ulong Type { get; }
+
+ ///
+ /// Returns the Description byte[] as a Stream
+ ///
+ ///
+ public Stream ToStream();
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteSegment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteSegment.cs
new file mode 100644
index 0000000000..fba41c72cc
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteSegment.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace ELFSharp.ELF.Segments
+{
+ internal interface INoteSegment : ISegment
+ {
+ public string NoteName { get; }
+ public ulong NoteType { get; }
+ public byte[] NoteDescription { get; }
+
+ ///
+ /// Returns all notes within the segment
+ ///
+ public IReadOnlyList Notes { get; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/ISegment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/ISegment.cs
new file mode 100644
index 0000000000..2d9b00b608
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/ISegment.cs
@@ -0,0 +1,11 @@
+namespace ELFSharp.ELF.Segments
+{
+ internal interface ISegment
+ {
+ public SegmentType Type { get; }
+ public SegmentFlags Flags { get; }
+ public byte[] GetRawHeader();
+ public byte[] GetFileContents();
+ public byte[] GetMemoryContents();
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/NoteSegment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/NoteSegment.cs
new file mode 100644
index 0000000000..2085294e52
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/NoteSegment.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using ELFSharp.ELF.Sections;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.ELF.Segments
+{
+ internal sealed class NoteSegment : Segment, INoteSegment
+ {
+ private readonly NoteData data;
+
+ private readonly List notes = new List();
+
+ internal NoteSegment(long headerOffset, Class elfClass, SimpleEndianessAwareReader reader)
+ : base(headerOffset, elfClass, reader)
+ {
+ var offset = (ulong)Offset;
+ var fileSize = (ulong)FileSize;
+ var remainingSize = fileSize;
+
+ // Keep the first NoteData as a property for backwards compatibility
+ data = new NoteData(offset, remainingSize, reader);
+ notes.Add(data);
+
+ offset += data.NoteFileSize;
+
+ // Read all additional notes within the segment
+ // Multiple notes are common in ELF core files
+ if (data.NoteFileSize < remainingSize)
+ {
+ remainingSize -= data.NoteFileSize;
+
+ while (remainingSize > NoteData.NoteDataHeaderSize)
+ {
+ var note = new NoteData(offset, remainingSize, reader);
+ notes.Add(note);
+ offset += note.NoteFileSize;
+ if (note.NoteFileSize <= remainingSize)
+ remainingSize -= note.NoteFileSize;
+ else
+ // File is damaged
+ throw new IndexOutOfRangeException("NoteSegment internal note-data is out of bounds");
+ }
+ }
+ }
+
+ public string NoteName => data.Name;
+
+ public ulong NoteType => data.Type;
+
+ public byte[] NoteDescription => data.DescriptionBytes;
+ public IReadOnlyList Notes => notes.AsReadOnly();
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/Segment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/Segment.cs
new file mode 100644
index 0000000000..91d28c38d4
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/Segment.cs
@@ -0,0 +1,114 @@
+using System;
+using System.IO;
+using ELFSharp.Utilities;
+
+#nullable disable
+
+namespace ELFSharp.ELF.Segments
+{
+ internal class Segment : ISegment
+ {
+ private readonly Class elfClass;
+
+ private readonly long headerOffset;
+ private readonly SimpleEndianessAwareReader reader;
+
+ internal Segment(long headerOffset, Class elfClass, SimpleEndianessAwareReader reader)
+ {
+ this.reader = reader;
+ this.headerOffset = headerOffset;
+ this.elfClass = elfClass;
+ ReadHeader();
+ }
+
+ public T Address { get; private set; }
+
+ public T PhysicalAddress { get; private set; }
+
+ public T Size { get; private set; }
+
+ public T Alignment { get; private set; }
+
+ public long FileSize { get; private set; }
+
+ public long Offset { get; private set; }
+
+ public SegmentType Type { get; private set; }
+
+ public SegmentFlags Flags { get; private set; }
+
+ ///
+ /// Returns content of the section as it is given in the file.
+ /// Note that it may be an array of length 0.
+ ///
+ /// Segment contents as byte array.
+ public byte[] GetFileContents()
+ {
+ if (FileSize == 0)
+ return new byte[0];
+
+ SeekTo(Offset);
+ var result = new byte[checked((int)FileSize)];
+ var fileImage = reader.ReadBytes(result.Length);
+ fileImage.CopyTo(result, 0);
+ return result;
+ }
+
+ ///
+ /// Returns content of the section, possibly padded or truncated to the memory size.
+ /// Note that it may be an array of length 0.
+ ///
+ /// Segment image as a byte array.
+ public byte[] GetMemoryContents()
+ {
+ var sizeAsInt = Size.To();
+ if (sizeAsInt == 0)
+ return new byte[0];
+
+ SeekTo(Offset);
+ var result = new byte[sizeAsInt];
+ var fileImage = reader.ReadBytes(Math.Min(result.Length, checked((int)FileSize)));
+ fileImage.CopyTo(result, 0);
+ return result;
+ }
+
+ public byte[] GetRawHeader()
+ {
+ SeekTo(headerOffset);
+ return reader.ReadBytes(elfClass == Class.Bit32 ? 32 : 56);
+ }
+
+ public static SegmentType ProbeType(SimpleEndianessAwareReader reader)
+ {
+ return (SegmentType)reader.ReadUInt32();
+ }
+
+ public override string ToString()
+ {
+ return string.Format("{2}: size {3}, @ 0x{0:X}", Address, PhysicalAddress, Type, Size);
+ }
+
+ private void ReadHeader()
+ {
+ SeekTo(headerOffset);
+ Type = (SegmentType)reader.ReadUInt32();
+ if (elfClass == Class.Bit64)
+ Flags = (SegmentFlags)reader.ReadUInt32();
+ // TODO: some functions?s
+ Offset = elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadInt64();
+ Address = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To();
+ PhysicalAddress = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To();
+ FileSize = elfClass == Class.Bit32 ? reader.ReadInt32() : reader.ReadInt64();
+ Size = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To();
+ if (elfClass == Class.Bit32)
+ Flags = (SegmentFlags)reader.ReadUInt32();
+
+ Alignment = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To();
+ }
+
+ private void SeekTo(long givenOffset)
+ {
+ reader.BaseStream.Seek(givenOffset, SeekOrigin.Begin);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentFlags.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentFlags.cs
new file mode 100644
index 0000000000..4d10a6589d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentFlags.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ELFSharp.ELF.Segments
+{
+ [Flags]
+ internal enum SegmentFlags : uint
+ {
+ Execute = 1,
+ Write = 2,
+ Read = 4
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentType.cs
new file mode 100644
index 0000000000..f9c681aa54
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentType.cs
@@ -0,0 +1,13 @@
+namespace ELFSharp.ELF.Segments
+{
+ internal enum SegmentType : uint
+ {
+ Null = 0,
+ Load,
+ Dynamic,
+ Interpreter,
+ Note,
+ SharedLibrary,
+ ProgramHeader
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Utilities.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Utilities.cs
new file mode 100644
index 0000000000..8d84290714
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Utilities.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ELFSharp.ELF
+{
+ internal static class Utilities
+ {
+ internal static T To(this object source)
+ {
+ return (T)Convert.ChangeType(source, typeof(T));
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Endianess.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Endianess.cs
new file mode 100644
index 0000000000..a2c0aadb0d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Endianess.cs
@@ -0,0 +1,8 @@
+namespace ELFSharp
+{
+ internal enum Endianess
+ {
+ LittleEndian,
+ BigEndian
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Command.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Command.cs
new file mode 100644
index 0000000000..01ae9069bf
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Command.cs
@@ -0,0 +1,17 @@
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal class Command
+ {
+ protected readonly SimpleEndianessAwareReader Reader;
+ protected readonly Stream Stream;
+
+ internal Command(SimpleEndianessAwareReader reader, Stream stream)
+ {
+ Stream = stream;
+ Reader = reader;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/CommandType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/CommandType.cs
new file mode 100644
index 0000000000..1e5acb136d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/CommandType.cs
@@ -0,0 +1,15 @@
+namespace ELFSharp.MachO
+{
+ internal enum CommandType : uint
+ {
+ Segment = 0x1,
+ SymbolTable = 0x2,
+ LoadDylib = 0xc,
+ IdDylib = 0xd,
+ LoadWeakDylib = 0x80000018u,
+ Segment64 = 0x19,
+ ReexportDylib = 0x8000001fu,
+ Main = 0x80000028u,
+ UUID = 0x1b
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Dylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Dylib.cs
new file mode 100644
index 0000000000..de9e5f12b8
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Dylib.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using System.Text;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal abstract class Dylib : Command
+ {
+ internal Dylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream)
+ {
+ var offset = reader.ReadUInt32();
+ var timestamp = reader.ReadInt32();
+ var currentVersion = reader.ReadUInt32();
+ var compatibilityVersion = reader.ReadUInt32();
+ Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime;
+ CurrentVersion = GetVersion(currentVersion);
+ CompatibilityVersion = GetVersion(compatibilityVersion);
+ Name = GetString(reader.ReadBytes((int)(commandSize - offset)));
+ }
+
+ public string Name { get; }
+ public DateTime Timestamp { get; }
+ public Version CurrentVersion { get; }
+ public Version CompatibilityVersion { get; }
+
+ private static Version GetVersion(uint version)
+ {
+ return new Version((int)(version >> 16), (int)((version >> 8) & 0xff), (int)(version & 0xff));
+ }
+
+ private static string GetString(byte[] bytes)
+ {
+ var nullTerminatorIndex = Array.FindIndex(bytes, e => e == '\0');
+ return Encoding.ASCII.GetString(bytes, 0, nullTerminatorIndex >= 0 ? nullTerminatorIndex : bytes.Length);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/EntryPoint.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/EntryPoint.cs
new file mode 100644
index 0000000000..0d41fc1a34
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/EntryPoint.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal class EntryPoint : Command
+ {
+ public EntryPoint(SimpleEndianessAwareReader reader, Stream stream) : base(reader, stream)
+ {
+ Value = Reader.ReadInt64();
+ StackSize = Reader.ReadInt64();
+ }
+
+ public long Value { get; private set; }
+
+ public long StackSize { get; private set; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FatArchiveReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FatArchiveReader.cs
new file mode 100644
index 0000000000..401382c02a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FatArchiveReader.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal static class FatArchiveReader
+ {
+ public static IEnumerable Enumerate(Stream stream, bool shouldOwnStream)
+ {
+ // Fat header is always big endian.
+ var reader = new SimpleEndianessAwareReader(stream, Endianess.BigEndian, !shouldOwnStream);
+
+ // We assume that fat magic has been already read.
+ var machOCount = reader.ReadInt32();
+ var alreadyRead = 0;
+ var fatEntriesBegin = stream.Position;
+
+ while (alreadyRead < machOCount)
+ {
+ // We're only interested in offset and size.
+ stream.Seek(fatEntriesBegin + 20 * alreadyRead + 8, SeekOrigin.Begin);
+ var offset = reader.ReadInt32();
+ var size = reader.ReadInt32();
+ var substream = new SubStream(stream, offset, size);
+ yield return MachOReader.Load(substream, false);
+
+ alreadyRead++;
+ }
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FileType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FileType.cs
new file mode 100644
index 0000000000..515717389a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FileType.cs
@@ -0,0 +1,17 @@
+namespace ELFSharp.MachO
+{
+ internal enum FileType : uint
+ {
+ Object = 0x1,
+ Executable = 0x2,
+ FixedVM = 0x3,
+ Core = 0x4,
+ Preload = 0x5,
+ DynamicLibrary = 0x6,
+ DynamicLinker = 0x7,
+ Bundle = 0x8,
+ DynamicLibraryStub = 0x9,
+ Debug = 0xA,
+ Kext = 0xB
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/HeaderFlags.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/HeaderFlags.cs
new file mode 100644
index 0000000000..1866e4569c
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/HeaderFlags.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace ELFSharp.MachO
+{
+ [Flags]
+ internal enum HeaderFlags : uint
+ {
+ NoUndefs = 0x1, /* the object file has no undefined references */
+
+ IncrLink = 0x2, /* the object file is the output of an incremental link against a base file
+ * and can't be link edited again */
+
+ DyldLink = 0x4, /* the object file is input for the dynamic linker and can't be staticly link edited again */
+
+ BindAtLoad = 0x8, /* the object file's undefined references are bound by the dynamic
+ * linker when loaded. */
+
+ Prebound = 0x10, /* the file has its dynamic undefined references prebound. */
+
+ SplitSeg = 0x20, /* the file has its read-only and read-write segments split */
+
+ LazyInit = 0x40, /* the shared library init routine is to be run lazily via catching memory
+ * faults to its writeable segments (obsolete) */
+
+ TwoLevel = 0x80, /* the image is using two-level name space bindings */
+
+ ForceFlat = 0x100, /* the executable is forcing all images to use flat name space bindings */
+
+ NoMultiDefs = 0x200, /* this umbrella guarantees no multiple defintions of symbols in its
+ * sub-images so the two-level namespace hints can always be used. */
+
+ NoFixPrebinding = 0x400, /* do not have dyld notify the prebinding agent about this executable*/
+
+ Prebindable = 0x800, /* the binary is not prebound but can have its prebinding redone. only used
+ * when MH_PREBOUND is not set. */
+
+ AllModsBound = 0x1000, /* indicates that this binary binds to all two-level namespace modules of
+ * its dependent libraries.only used when MH_PREBINDABLE and MH_TWOLEVEL are both set. */
+
+ SubsectionsViaSymbols = 0x2000, /* safe to divide up the sections into sub-sections via symbols for dead
+ * code stripping*/
+
+ Canonical = 0x4000, /* the binary has been canonicalized via the unprebind operation */
+
+ WeakDefines = 0x8000, /* the final linked image contains external weak symbols*/
+
+ BindsToWeak = 0x10000, /* the final linked image uses weak symbols */
+
+ AllowStackExecution = 0x20000, /* When this bit is set, all stacks in the task will be given stack
+ * execution privilege. Only used in MH_EXECUTE filetypes. */
+
+ RootSafe =
+ 0x40000, /* When this bit is set, the binary declares it is safe for use in processes with uid zero */
+
+ SetuidSafe =
+ 0x80000, /* When this bit is set, the binary declares it is safe for use in processes when issetugid() is true */
+
+ NoReexportedDylibs = 0x100000, /* When this bit is set on a dylib, the static linker does not need to
+ * examine dependent dylibs to see if any are re-exported */
+
+ PIE = 0x200000, /* When this bit is set, the OS will load the main executable at a random
+ * address.Only used in MH_EXECUTE filetypes. */
+
+ DeadStrippableDylib = 0x400000, /* Only for use on dylibs. When linking against a dylib that has this bit
+ * set, the static linker will automatically not create a LC_LOAD_DYLIB
+ * load command to the dylib if no symbols are being referenced from the dylib. */
+
+ HasTLVDescriptors = 0x800000, /* Contains a section of type S_THREAD_LOCAL_VARIABLES*/
+
+ NoHeapExecution = 0x1000000, /* When this bit is set, the OS will run the main executable with a
+ * non-executable heap even on platforms (e.g.i386) that don't require
+ * it. Only used in MH_EXECUTE filetypes. */
+
+ AppExtensionSafe = 0x02000000 /* The code was linked for use in an application extension. */
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/IdDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/IdDylib.cs
new file mode 100644
index 0000000000..c57ae7b66b
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/IdDylib.cs
@@ -0,0 +1,13 @@
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal class IdDylib : Dylib
+ {
+ public IdDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream,
+ commandSize)
+ {
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadDylib.cs
new file mode 100644
index 0000000000..339bc62a0e
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadDylib.cs
@@ -0,0 +1,13 @@
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal class LoadDylib : Dylib
+ {
+ public LoadDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream,
+ commandSize)
+ {
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadWeakDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadWeakDylib.cs
new file mode 100644
index 0000000000..aa92ebf5d2
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadWeakDylib.cs
@@ -0,0 +1,13 @@
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal class LoadWeakDylib : Dylib
+ {
+ public LoadWeakDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream,
+ commandSize)
+ {
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachO.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachO.cs
new file mode 100644
index 0000000000..a900c167e0
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachO.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal sealed class MachO
+ {
+ internal const int Architecture64 = 0x1000000;
+ private readonly Command[] commands;
+
+ internal MachO(Stream stream, bool is64, Endianess endianess, bool ownsStream)
+ {
+ Is64 = is64;
+
+ using var reader = new SimpleEndianessAwareReader(stream, endianess, !ownsStream);
+
+ Machine = (Machine)reader.ReadInt32();
+ reader.ReadBytes(4); // we don't support the cpu subtype now
+ FileType = (FileType)reader.ReadUInt32();
+ var noOfCommands = reader.ReadInt32();
+ reader.ReadInt32(); // size of commands
+ Flags = (HeaderFlags)reader.ReadUInt32();
+ if (is64)
+ reader.ReadBytes(4); // reserved
+ commands = new Command[noOfCommands];
+ ReadCommands(noOfCommands, stream, reader);
+ }
+
+ public Machine Machine { get; private set; }
+
+ public FileType FileType { get; private set; }
+
+ public HeaderFlags Flags { get; private set; }
+
+ public bool Is64 { get; }
+
+ public IEnumerable GetCommandsOfType() where T : Command
+ {
+ return commands.Where(x => x != null).OfType();
+ }
+
+ private void ReadCommands(int noOfCommands, Stream stream, SimpleEndianessAwareReader reader)
+ {
+ for (var i = 0; i < noOfCommands; i++)
+ {
+ var loadCommandType = reader.ReadUInt32();
+ var commandSize = reader.ReadUInt32();
+ switch ((CommandType)loadCommandType)
+ {
+ case CommandType.SymbolTable:
+ commands[i] = new SymbolTable(reader, stream, Is64,
+ commands.OfType().SelectMany(e => e.Sections).ToList());
+ break;
+ case CommandType.IdDylib:
+ commands[i] = new IdDylib(reader, stream, commandSize);
+ break;
+ case CommandType.LoadDylib:
+ commands[i] = new LoadDylib(reader, stream, commandSize);
+ break;
+ case CommandType.LoadWeakDylib:
+ commands[i] = new LoadWeakDylib(reader, stream, commandSize);
+ break;
+ case CommandType.ReexportDylib:
+ commands[i] = new ReexportDylib(reader, stream, commandSize);
+ break;
+ case CommandType.Main:
+ commands[i] = new EntryPoint(reader, stream);
+ break;
+ case CommandType.Segment:
+ case CommandType.Segment64:
+ commands[i] = new Segment(reader, stream, this);
+ break;
+ case CommandType.UUID:
+ commands[i] = new UUID(reader, stream);
+ break;
+ default:
+ reader.ReadBytes((int)commandSize - 8); // 8 bytes is the size of the common command header
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOReader.cs
new file mode 100644
index 0000000000..3ce1e516ec
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOReader.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+#nullable disable
+
+namespace ELFSharp.MachO
+{
+ internal static class MachOReader
+ {
+ private const uint FatMagic = 0xBEBAFECA;
+
+ private const string FatArchiveErrorMessage =
+ "Given file is a fat archive, contains more than one MachO binary. Use (Try)LoadFat to handle it.";
+
+ private const string NotMachOErrorMessage = "Given file is not a Mach-O file.";
+
+ private static readonly IReadOnlyDictionary MagicToMachOType =
+ new Dictionary
+ {
+ { 0xFEEDFACE, (false, Endianess.LittleEndian) },
+ { 0xFEEDFACF, (true, Endianess.LittleEndian) },
+ { 0xCEFAEDFE, (false, Endianess.BigEndian) },
+ { 0xCFFAEDFE, (true, Endianess.BigEndian) }
+ };
+
+ public static MachO Load(string fileName)
+ {
+ return Load(File.OpenRead(fileName), true);
+ }
+
+ public static MachO Load(Stream stream, bool shouldOwnStream)
+ {
+ return TryLoad(stream, shouldOwnStream, out var result) switch
+ {
+ MachOResult.OK => result,
+ MachOResult.NotMachO => throw new InvalidOperationException(NotMachOErrorMessage),
+ MachOResult.FatMachO => throw new InvalidOperationException(FatArchiveErrorMessage),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public static IReadOnlyList LoadFat(Stream stream, bool shouldOwnStream)
+ {
+ var result = TryLoadFat(stream, shouldOwnStream, out var machOs);
+ if (result == MachOResult.OK || result == MachOResult.FatMachO)
+ return machOs;
+
+ throw new InvalidOperationException(NotMachOErrorMessage);
+ }
+
+ public static MachOResult TryLoad(string fileName, out MachO machO)
+ {
+ return TryLoad(File.OpenRead(fileName), true, out machO);
+ }
+
+ public static MachOResult TryLoad(Stream stream, bool shouldOwnStream, out MachO machO)
+ {
+ var result = TryLoadFat(stream, shouldOwnStream, out var machOs);
+
+ if (result == MachOResult.OK)
+ machO = machOs.SingleOrDefault();
+ else
+ machO = null;
+
+ return result;
+ }
+
+ public static MachOResult TryLoadFat(Stream stream, bool shouldOwnStream, out IReadOnlyList machOs)
+ {
+ machOs = null;
+
+ using var reader = new BinaryReader(stream, Encoding.UTF8, true);
+ var magic = reader.ReadUInt32();
+
+ if (magic == FatMagic)
+ {
+ machOs = FatArchiveReader.Enumerate(stream, shouldOwnStream).ToArray();
+ return MachOResult.FatMachO;
+ }
+
+ if (!MagicToMachOType.TryGetValue(magic, out var machOType))
+ return MachOResult.NotMachO;
+
+ var machO = new MachO(stream, machOType.Is64Bit, machOType.Endianess, shouldOwnStream);
+ machOs = new[] { machO };
+ return MachOResult.OK;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOResult.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOResult.cs
new file mode 100644
index 0000000000..1ea75cc7a1
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOResult.cs
@@ -0,0 +1,9 @@
+namespace ELFSharp.MachO
+{
+ internal enum MachOResult
+ {
+ OK,
+ NotMachO,
+ FatMachO
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Machine.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Machine.cs
new file mode 100644
index 0000000000..55545e5a28
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Machine.cs
@@ -0,0 +1,20 @@
+namespace ELFSharp.MachO
+{
+ internal enum Machine
+ {
+ Any = -1,
+ Vax = 1,
+ M68k = 6,
+ X86 = 7,
+ X86_64 = X86 | MachO.Architecture64,
+ M98k = 10,
+ PaRisc = 11,
+ Arm = 12,
+ Arm64 = Arm | MachO.Architecture64,
+ M88k = 13,
+ Sparc = 14,
+ I860 = 15,
+ PowerPc = 18,
+ PowerPc64 = PowerPc | MachO.Architecture64
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Protection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Protection.cs
new file mode 100644
index 0000000000..f50d18fcdf
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Protection.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ELFSharp.MachO
+{
+ [Flags]
+ internal enum Protection
+ {
+ Read = 1,
+ Write = 2,
+ Execute = 4
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/ReexportDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/ReexportDylib.cs
new file mode 100644
index 0000000000..b187cf6b19
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/ReexportDylib.cs
@@ -0,0 +1,13 @@
+using System.IO;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal class ReexportDylib : Dylib
+ {
+ public ReexportDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream,
+ commandSize)
+ {
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Section.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Section.cs
new file mode 100644
index 0000000000..2cbd673dff
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Section.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Diagnostics;
+
+namespace ELFSharp.MachO
+{
+ [DebuggerDisplay("Section({segment.Name,nq},{Name,nq})")]
+ internal sealed class Section
+ {
+ private readonly Segment segment;
+
+ public Section(string name, string segmentName, ulong address, ulong size, uint offset, uint alignExponent,
+ uint relocOffset, uint numberOfReloc, uint flags, Segment segment)
+ {
+ Name = name;
+ SegmentName = segmentName;
+ Address = address;
+ Size = size;
+ Offset = offset;
+ AlignExponent = alignExponent;
+ RelocOffset = relocOffset;
+ RelocCount = numberOfReloc;
+ Flags = flags;
+ this.segment = segment;
+ }
+
+ public string Name { get; private set; }
+ public string SegmentName { get; private set; }
+ public ulong Address { get; private set; }
+ public ulong Size { get; }
+ public uint Offset { get; }
+ public uint AlignExponent { get; private set; }
+ public uint RelocOffset { get; private set; }
+ public uint RelocCount { get; private set; }
+ public uint Flags { get; private set; }
+
+ public byte[] GetData()
+ {
+ if (Offset < segment.FileOffset || Offset + Size > segment.FileOffset + segment.Size)
+ return new byte[0];
+ var result = new byte[Size];
+ Array.Copy(segment.GetData(), (int)(Offset - segment.FileOffset), result, 0, (int)Size);
+ return result;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Segment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Segment.cs
new file mode 100644
index 0000000000..fd755fc74a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Segment.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using ELFSharp.Utilities;
+
+#nullable disable
+
+namespace ELFSharp.MachO
+{
+ [DebuggerDisplay("{Type}({Name,nq})")]
+ internal sealed class Segment : Command
+ {
+ private readonly byte[] data;
+
+ private readonly bool is64;
+
+ public Segment(SimpleEndianessAwareReader reader, Stream stream, MachO machO) : base(reader, stream)
+ {
+ is64 = machO.Is64;
+ Name = ReadSectionOrSegmentName();
+ Address = ReadUInt32OrUInt64();
+ Size = ReadUInt32OrUInt64();
+ FileOffset = ReadUInt32OrUInt64();
+ var fileSize = ReadUInt32OrUInt64();
+ MaximalProtection = ReadProtection();
+ InitialProtection = ReadProtection();
+ var numberOfSections = Reader.ReadInt32();
+ Reader.ReadInt32(); // we ignore flags for now
+
+ if (fileSize > 0)
+ {
+ var streamPosition = Stream.Position;
+ Stream.Seek((long)FileOffset, SeekOrigin.Begin);
+ data = new byte[Size];
+ var buffer = stream.ReadBytesOrThrow(checked((int)fileSize));
+ Array.Copy(buffer, data, buffer.Length);
+ Stream.Position = streamPosition;
+ }
+
+ var sections = new List();
+ for (var i = 0; i < numberOfSections; i++)
+ {
+ var sectionName = ReadSectionOrSegmentName();
+ var segmentName = ReadSectionOrSegmentName();
+
+ // An intermediate object file contains only one segment.
+ // This segment name is empty, its sections segment names are not empty.
+ if (machO.FileType != FileType.Object && segmentName != Name)
+ throw new InvalidOperationException("Unexpected name of the section's segment.");
+
+ var sectionAddress = ReadUInt32OrUInt64();
+ var sectionSize = ReadUInt32OrUInt64();
+ var offset = Reader.ReadUInt32();
+ var alignExponent = Reader.ReadUInt32();
+ var relocOffset = Reader.ReadUInt32();
+ var numberOfReloc = Reader.ReadUInt32();
+ var flags = Reader.ReadUInt32();
+ _ = Reader.ReadUInt32(); // reserved1
+ _ = Reader.ReadUInt32(); // reserved2
+ _ = is64 ? Reader.ReadUInt32() : 0; // reserved3
+
+ var section = new Section(sectionName, segmentName, sectionAddress, sectionSize, offset, alignExponent,
+ relocOffset, numberOfReloc, flags, this);
+ sections.Add(section);
+ }
+
+ Sections = new ReadOnlyCollection(sections);
+ }
+
+ public string Name { get; private set; }
+ public ulong Address { get; private set; }
+ public ulong Size { get; }
+ public ulong FileOffset { get; }
+ public Protection InitialProtection { get; private set; }
+ public Protection MaximalProtection { get; private set; }
+ public ReadOnlyCollection Sections { get; private set; }
+ private CommandType Type => is64 ? CommandType.Segment64 : CommandType.Segment;
+
+ public byte[] GetData()
+ {
+ if (data == null)
+ return new byte[Size];
+ return data.ToArray();
+ }
+
+ private ulong ReadUInt32OrUInt64()
+ {
+ return is64 ? Reader.ReadUInt64() : Reader.ReadUInt32();
+ }
+
+ private Protection ReadProtection()
+ {
+ return (Protection)Reader.ReadInt32();
+ }
+
+ private string ReadSectionOrSegmentName()
+ {
+ var nameAsBytes = Reader.ReadBytes(16).TakeWhile(x => x != 0).ToArray();
+ return Encoding.UTF8.GetString(nameAsBytes);
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Symbol.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Symbol.cs
new file mode 100644
index 0000000000..94715b7c02
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Symbol.cs
@@ -0,0 +1,19 @@
+using System.Diagnostics;
+
+namespace ELFSharp.MachO
+{
+ [DebuggerDisplay("Symbol({Name,nq},{Value}) in {Section}")]
+ internal struct Symbol
+ {
+ public Symbol(string name, long value, Section section) : this()
+ {
+ Name = name;
+ Value = value;
+ Section = section;
+ }
+
+ public string Name { get; private set; }
+ public long Value { get; private set; }
+ public Section Section { get; private set; }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/SymbolTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/SymbolTable.cs
new file mode 100644
index 0000000000..c8293f0263
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/SymbolTable.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using ELFSharp.Utilities;
+
+#nullable disable
+
+namespace ELFSharp.MachO
+{
+ internal class SymbolTable : Command
+ {
+ private readonly bool is64;
+
+ private Symbol[] symbols;
+
+ public SymbolTable(SimpleEndianessAwareReader reader, Stream stream, bool is64, IReadOnlyList sections)
+ : base(reader, stream)
+ {
+ this.is64 = is64;
+ ReadSymbols(sections);
+ }
+
+ public IEnumerable Symbols
+ {
+ get { return symbols.Select(x => x); }
+ }
+
+ private void ReadSymbols(IReadOnlyList sections)
+ {
+ var symbolTableOffset = Reader.ReadInt32();
+ var numberOfSymbols = Reader.ReadInt32();
+ symbols = new Symbol[numberOfSymbols];
+ var stringTableOffset = Reader.ReadInt32();
+ Reader.ReadInt32(); // string table size
+
+ var streamPosition = Stream.Position;
+ Stream.Seek(symbolTableOffset, SeekOrigin.Begin);
+
+ try
+ {
+ for (var i = 0; i < numberOfSymbols; i++)
+ {
+ var nameOffset = Reader.ReadInt32();
+ var name = ReadStringFromOffset(stringTableOffset + nameOffset);
+ var type = Reader.ReadByte();
+ var sect = Reader.ReadByte();
+ var desc = Reader.ReadInt16();
+ var value = is64 ? Reader.ReadInt64() : Reader.ReadInt32();
+ var symbol = new Symbol(name, value,
+ sect > 0 && sect <= sections.Count ? sections[sect - 1] : null);
+ symbols[i] = symbol;
+ }
+ }
+ finally
+ {
+ Stream.Position = streamPosition;
+ }
+ }
+
+ private string ReadStringFromOffset(int offset)
+ {
+ var streamPosition = Stream.Position;
+ Stream.Seek(offset, SeekOrigin.Begin);
+ try
+ {
+ var asBytes = new List();
+ int readByte;
+ while ((readByte = Stream.ReadByte()) != 0)
+ {
+ if (readByte == -1)
+ throw new EndOfStreamException("Premature end of the stream while reading string.");
+ asBytes.Add((byte)readByte);
+ }
+
+ return Encoding.UTF8.GetString(asBytes.ToArray());
+ }
+ finally
+ {
+ Stream.Position = streamPosition;
+ }
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/UUID.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/UUID.cs
new file mode 100644
index 0000000000..893cd4c325
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/UUID.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using System.Linq;
+using ELFSharp.Utilities;
+
+namespace ELFSharp.MachO
+{
+ internal class UUID : Command
+ {
+ internal UUID(SimpleEndianessAwareReader reader, Stream stream) : base(reader, stream)
+ {
+ ID = ReadUUID();
+ }
+
+ public Guid ID { get; }
+
+ private Guid ReadUUID()
+ {
+ var rawBytes = Reader.ReadBytes(16).ToArray();
+
+ // Deal here with UUID endianess. Switch scheme is 4(r)-2(r)-2(r)-8(o)
+ // where r is reverse, o is original order.
+ Array.Reverse(rawBytes, 0, 4);
+ Array.Reverse(rawBytes, 4, 2);
+ Array.Reverse(rawBytes, 6, 2);
+
+ var guid = new Guid(rawBytes);
+ return guid;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/Architecture.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/Architecture.cs
new file mode 100644
index 0000000000..8d320d7b15
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/Architecture.cs
@@ -0,0 +1,27 @@
+namespace ELFSharp.UImage
+{
+ internal enum Architecture : byte
+ {
+ Invalid = 0,
+ Alpha = 1,
+ ARM = 2,
+ Ix86 = 3,
+ Itanium = 4,
+ MIPS = 5,
+ MIPS64 = 6,
+ PowerPC = 7,
+ S390 = 8,
+ SuperH = 9,
+ SPARC = 10,
+ SPARC64 = 11,
+ M68k = 12,
+ MicroBlaze = 14,
+ Nios2 = 15,
+ Blackfin = 16,
+ AVR32 = 17,
+ ST200 = 18,
+ Sandbox = 19,
+ NDS32 = 20,
+ OpenRISC = 21
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/CompressionType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/CompressionType.cs
new file mode 100644
index 0000000000..555c3aa9a3
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/CompressionType.cs
@@ -0,0 +1,11 @@
+namespace ELFSharp.UImage
+{
+ internal enum CompressionType : byte
+ {
+ None = 0,
+ Gzip = 1,
+ Bzip2 = 2,
+ Lzma = 3,
+ Lzo = 4
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageDataResult.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageDataResult.cs
new file mode 100644
index 0000000000..acea570b1d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageDataResult.cs
@@ -0,0 +1,10 @@
+namespace ELFSharp.UImage
+{
+ internal enum ImageDataResult
+ {
+ OK,
+ BadChecksum,
+ UnsupportedCompressionFormat,
+ InvalidIndex
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageType.cs
new file mode 100644
index 0000000000..f54c189990
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageType.cs
@@ -0,0 +1,10 @@
+namespace ELFSharp.UImage
+{
+ // here only supported image types are listed
+ internal enum ImageType : byte
+ {
+ Standalone = 1,
+ Kernel = 2,
+ MultiFileImage = 4
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/OS.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/OS.cs
new file mode 100644
index 0000000000..6fa0e6118a
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/OS.cs
@@ -0,0 +1,30 @@
+namespace ELFSharp.UImage
+{
+ internal enum OS : byte
+ {
+ Invalid = 0,
+ OpenBSD = 1,
+ NetBSD = 2,
+ FreeBSD = 3,
+ BSD44 = 4,
+ Linux = 5,
+ SVR4 = 6,
+ Esix = 7,
+ Solaris = 8,
+ Irix = 9,
+ SCO = 10,
+ Dell = 11,
+ NCR = 12,
+ LynxOS = 13,
+ VxWorks = 14,
+ PSOS = 15,
+ QNX = 16,
+ Firmware = 17,
+ RTEMS = 18,
+ ARTOS = 19,
+ UnityOS = 20,
+ INTEGRITY = 21,
+ OSE = 22,
+ Plan9 = 23
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImage.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImage.cs
new file mode 100644
index 0000000000..680ca7e6fb
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImage.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text;
+
+#nullable disable
+
+namespace ELFSharp.UImage
+{
+ internal sealed class UImage
+ {
+ private const int MaximumNameLength = 32;
+ private readonly List imageSizes;
+ private readonly byte[] rawImage;
+ private readonly bool shouldOwnStream;
+
+ internal UImage(Stream stream, bool multiFileImage, bool ownsStream)
+ {
+ shouldOwnStream = ownsStream;
+ imageSizes = new List();
+
+ using var reader = new BinaryReader(stream, Encoding.UTF8, !ownsStream);
+
+ reader.ReadBytes(8); // magic and CRC, already checked
+ Timestamp = (new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) +
+ TimeSpan.FromSeconds(reader.ReadInt32BigEndian())).ToLocalTime();
+ Size = reader.ReadUInt32BigEndian();
+ LoadAddress = reader.ReadUInt32BigEndian();
+ EntryPoint = reader.ReadUInt32BigEndian();
+ CRC = reader.ReadUInt32BigEndian();
+ OperatingSystem = (OS)reader.ReadByte();
+ Architecture = (Architecture)reader.ReadByte();
+ Type = (ImageType)reader.ReadByte();
+ Compression = (CompressionType)reader.ReadByte();
+ var nameAsBytes = reader.ReadBytes(32);
+ Name = Encoding.UTF8.GetString(nameAsBytes.Reverse().SkipWhile(x => x == 0).Reverse().ToArray());
+
+ if (multiFileImage)
+ {
+ var startingPosition = stream.Position;
+
+ int nextImageSize;
+ do
+ {
+ nextImageSize = reader.ReadInt32BigEndian();
+ imageSizes.Add(nextImageSize);
+ } while (nextImageSize != 0);
+
+ // Last image size is actually a terminator.
+ imageSizes.RemoveAt(imageSizes.Count - 1);
+ ImageCount = imageSizes.Count;
+ stream.Position = startingPosition;
+ }
+
+ rawImage = reader.ReadBytes((int)Size);
+ }
+
+ public uint CRC { get; }
+ public bool IsChecksumOK { get; private set; }
+ public uint Size { get; }
+ public uint LoadAddress { get; private set; }
+ public uint EntryPoint { get; private set; }
+ public string Name { get; private set; }
+ public DateTime Timestamp { get; private set; }
+ public CompressionType Compression { get; }
+ public ImageType Type { get; private set; }
+ public OS OperatingSystem { get; private set; }
+ public Architecture Architecture { get; private set; }
+ public int ImageCount { get; }
+
+ public ImageDataResult TryGetImageData(int imageIndex, out byte[] result)
+ {
+ result = null;
+
+ if (imageIndex > ImageCount - 1 || imageIndex < 0)
+ return ImageDataResult.InvalidIndex;
+
+ if (ImageCount == 1)
+ return TryGetImageData(out result);
+
+ if (Compression != CompressionType.None)
+ // We only support multi file images without compression
+ return ImageDataResult.UnsupportedCompressionFormat;
+
+ if (CRC != UImageReader.GzipCrc32(rawImage))
+ return ImageDataResult.BadChecksum;
+
+ // Images sizes * 4 + terminator (which also takes 4 bytes).
+ var startingOffset = 4 * (ImageCount + 1) + imageSizes.Take(imageIndex).Sum();
+ result = new byte[imageSizes[imageIndex]];
+ Array.Copy(rawImage, startingOffset, result, 0, result.Length);
+
+ return ImageDataResult.OK;
+ }
+
+ public ImageDataResult TryGetImageData(out byte[] result)
+ {
+ result = null;
+
+ if (ImageCount > 1)
+ return TryGetImageData(0, out result);
+
+ if (Compression != CompressionType.None && Compression != CompressionType.Gzip)
+ return ImageDataResult.UnsupportedCompressionFormat;
+
+ if (CRC != UImageReader.GzipCrc32(rawImage))
+ return ImageDataResult.BadChecksum;
+
+ result = new byte[rawImage.Length];
+ Array.Copy(rawImage, result, result.Length);
+ if (Compression == CompressionType.Gzip)
+ using (var stream = new GZipStream(new MemoryStream(result), CompressionMode.Decompress))
+ {
+ using (var decompressed = new MemoryStream())
+ {
+ stream.CopyTo(decompressed);
+ result = decompressed.ToArray();
+ }
+ }
+
+ return ImageDataResult.OK;
+ }
+
+ public byte[] GetImageData(int imageIndex)
+ {
+ byte[] result;
+ var imageDataResult = TryGetImageData(imageIndex, out result);
+ return InterpretImageResult(result, imageDataResult);
+ }
+
+ public byte[] GetImageData()
+ {
+ byte[] result;
+ var imageDataResult = TryGetImageData(out result);
+ return InterpretImageResult(result, imageDataResult);
+ }
+
+ private byte[] InterpretImageResult(byte[] result, ImageDataResult imageDataResult)
+ {
+ return imageDataResult switch
+ {
+ ImageDataResult.OK => result,
+ ImageDataResult.BadChecksum => throw new InvalidOperationException(
+ "Bad checksum of the image, probably corrupted image."),
+ ImageDataResult.UnsupportedCompressionFormat => throw new InvalidOperationException(
+ string.Format("Unsupported compression format '{0}'.", Compression)),
+ ImageDataResult.InvalidIndex => throw new ArgumentException("Invalid image index."),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public byte[] GetRawImageData()
+ {
+ var result = new byte[rawImage.Length];
+ Array.Copy(rawImage, result, result.Length);
+ return result;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageReader.cs
new file mode 100644
index 0000000000..b81c887415
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageReader.cs
@@ -0,0 +1,102 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+
+#nullable disable
+
+namespace ELFSharp.UImage
+{
+ internal static class UImageReader
+ {
+ private const uint Magic = 0x27051956;
+ private const uint Polynomial = 0xEDB88320;
+ private const uint Seed = 0xFFFFFFFF;
+
+ public static UImage Load(string fileName)
+ {
+ return Load(File.OpenRead(fileName), true);
+ }
+
+ public static UImage Load(Stream stream, bool shouldOwnStream)
+ {
+ return TryLoad(stream, shouldOwnStream, out var result) switch
+ {
+ UImageResult.OK => result,
+ UImageResult.NotUImage => throw new InvalidOperationException("Given file is not an UBoot image."),
+ UImageResult.BadChecksum => throw new InvalidOperationException(
+ "Wrong header checksum of the given UImage file."),
+ UImageResult.NotSupportedImageType => throw new InvalidOperationException(
+ "Given image type is not supported."),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public static UImageResult TryLoad(string fileName, out UImage uImage)
+ {
+ return TryLoad(File.OpenRead(fileName), true, out uImage);
+ }
+
+ public static UImageResult TryLoad(Stream stream, bool shouldOwnStream, out UImage uImage)
+ {
+ var startingStreamPosition = stream.Position;
+
+ uImage = null;
+ if (stream.Length < 64)
+ return UImageResult.NotUImage;
+
+ using var reader = new BinaryReader(stream, Encoding.UTF8, true);
+
+ var headerForCrc = reader.ReadBytes(64);
+ // we need to zero crc part
+ for (var i = 4; i < 8; i++)
+ headerForCrc[i] = 0;
+
+ stream.Position = startingStreamPosition;
+
+ var magic = reader.ReadUInt32BigEndian();
+ if (magic != Magic)
+ return UImageResult.NotUImage;
+
+ var crc = reader.ReadUInt32BigEndian();
+ if (crc != GzipCrc32(headerForCrc))
+ return UImageResult.BadChecksum;
+
+ reader.ReadBytes(22);
+ var imageType = (ImageType)reader.ReadByte();
+ if (!Enum.IsDefined(typeof(ImageType), imageType))
+ return UImageResult.NotSupportedImageType;
+
+ var multiFileImage = imageType == ImageType.MultiFileImage;
+ stream.Position = startingStreamPosition;
+ uImage = new UImage(stream, multiFileImage, shouldOwnStream);
+ return UImageResult.OK;
+ }
+
+ internal static uint GzipCrc32(byte[] data)
+ {
+ var remainder = Seed;
+ for (var i = 0; i < data.Length; i++)
+ {
+ remainder ^= data[i];
+ for (var j = 0; j < 8; j++)
+ if ((remainder & 1) != 0)
+ remainder = (remainder >> 1) ^ Polynomial;
+ else
+ remainder >>= 1;
+ }
+
+ return remainder ^ Seed;
+ }
+
+ internal static uint ReadUInt32BigEndian(this BinaryReader reader)
+ {
+ return (uint)IPAddress.HostToNetworkOrder(reader.ReadInt32());
+ }
+
+ internal static int ReadInt32BigEndian(this BinaryReader reader)
+ {
+ return IPAddress.HostToNetworkOrder(reader.ReadInt32());
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageResult.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageResult.cs
new file mode 100644
index 0000000000..c84858d27f
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageResult.cs
@@ -0,0 +1,10 @@
+namespace ELFSharp.UImage
+{
+ internal enum UImageResult
+ {
+ OK,
+ NotUImage,
+ BadChecksum,
+ NotSupportedImageType
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/Extensions.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/Extensions.cs
new file mode 100644
index 0000000000..3ccdd3a478
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/Extensions.cs
@@ -0,0 +1,21 @@
+using System.IO;
+
+namespace ELFSharp.Utilities
+{
+ internal static class Extensions
+ {
+ public static byte[] ReadBytesOrThrow(this Stream stream, int count)
+ {
+ var result = new byte[count];
+ while (count > 0)
+ {
+ var readThisTurn = stream.Read(result, result.Length - count, count);
+ if (readThisTurn == 0)
+ throw new EndOfStreamException($"End of stream reached while {count} bytes more expected.");
+ count -= readThisTurn;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SimpleEndianessAwareReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SimpleEndianessAwareReader.cs
new file mode 100644
index 0000000000..74df4c4521
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SimpleEndianessAwareReader.cs
@@ -0,0 +1,81 @@
+using System;
+using System.IO;
+using System.Net;
+
+namespace ELFSharp.Utilities
+{
+ internal sealed class SimpleEndianessAwareReader : IDisposable
+ {
+ private readonly bool beNonClosing;
+
+ private readonly bool needsAdjusting;
+
+ public SimpleEndianessAwareReader(Stream stream, Endianess endianess, bool beNonClosing = false)
+ {
+ this.beNonClosing = beNonClosing;
+ BaseStream = stream;
+ needsAdjusting = (endianess == Endianess.LittleEndian) ^ BitConverter.IsLittleEndian;
+ }
+
+ public Stream BaseStream { get; }
+
+ public void Dispose()
+ {
+ if (beNonClosing)
+ return;
+ BaseStream.Dispose();
+ }
+
+ public byte[] ReadBytes(int count)
+ {
+ return BaseStream.ReadBytesOrThrow(count);
+ }
+
+ public byte ReadByte()
+ {
+ var result = BaseStream.ReadByte();
+ if (result == -1)
+ throw new EndOfStreamException("End of stream reached while trying to read one byte.");
+ return (byte)result;
+ }
+
+ public short ReadInt16()
+ {
+ var value = BitConverter.ToInt16(ReadBytes(2), 0);
+ if (needsAdjusting)
+ value = IPAddress.NetworkToHostOrder(value);
+ return value;
+ }
+
+ public ushort ReadUInt16()
+ {
+ return (ushort)ReadInt16();
+ }
+
+ public int ReadInt32()
+ {
+ var value = BitConverter.ToInt32(ReadBytes(4), 0);
+ if (needsAdjusting)
+ value = IPAddress.NetworkToHostOrder(value);
+ return value;
+ }
+
+ public uint ReadUInt32()
+ {
+ return (uint)ReadInt32();
+ }
+
+ public long ReadInt64()
+ {
+ var value = BitConverter.ToInt64(ReadBytes(8), 0);
+ if (needsAdjusting)
+ value = IPAddress.NetworkToHostOrder(value);
+ return value;
+ }
+
+ public ulong ReadUInt64()
+ {
+ return (ulong)ReadInt64();
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SubStream.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SubStream.cs
new file mode 100644
index 0000000000..3bedf6d06b
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SubStream.cs
@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+
+namespace ELFSharp.Utilities
+{
+ internal sealed class SubStream : Stream
+ {
+ private const string NegativeArgumentMessage = "The argument cannot be negative.";
+ private const string OutsideStreamMessage = "The argument must be within the wrapped stream.";
+ private readonly long startingPosition;
+
+ private readonly Stream wrappedStream;
+
+ public SubStream(Stream wrappedStream, long startingPosition, long length)
+ {
+ if (startingPosition < 0)
+ throw new ArgumentException(nameof(startingPosition), NegativeArgumentMessage);
+
+ if (startingPosition > wrappedStream.Length)
+ throw new ArgumentException(nameof(startingPosition), OutsideStreamMessage);
+
+ if (length < 0)
+ throw new ArgumentException(nameof(length), NegativeArgumentMessage);
+
+ if (startingPosition + length > wrappedStream.Length)
+ throw new ArgumentException(nameof(startingPosition), OutsideStreamMessage);
+
+ if (!wrappedStream.CanSeek)
+ throw new ArgumentException(nameof(wrappedStream), "Wrapped streem has to be seekable.");
+ ;
+ this.wrappedStream = wrappedStream;
+ this.startingPosition = startingPosition;
+ Length = length;
+
+ wrappedStream.Seek(startingPosition, SeekOrigin.Begin);
+ }
+
+ public override bool CanRead => wrappedStream.CanRead;
+
+ public override bool CanSeek => wrappedStream.CanSeek;
+
+ public override bool CanWrite => false;
+
+ public override long Length { get; }
+
+ public override long Position
+ {
+ get => wrappedStream.Position - startingPosition;
+
+ set => wrappedStream.Position = value + startingPosition;
+ }
+
+ public override void Flush()
+ {
+ wrappedStream.Flush();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ count = (int)Math.Min(count, Length - Position);
+ return wrappedStream.Read(buffer, offset, count);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ // All offsets are adjusted to represent a begin-based offset in
+ // the original stream, so that we can simplify sanity checks.
+
+ var adjustedOffset = origin switch
+ {
+ SeekOrigin.Begin => offset + startingPosition,
+ SeekOrigin.End => wrappedStream.Length - offset,
+ SeekOrigin.Current => wrappedStream.Position + offset,
+ _ => throw new InvalidOperationException("Should never reach here.")
+ };
+
+ if (adjustedOffset < startingPosition || adjustedOffset > startingPosition + Length)
+ throw new ArgumentException(nameof(offset), "Effective offset cannot move outside of the substream.");
+
+ return wrappedStream.Seek(adjustedOffset, SeekOrigin.Begin) - startingPosition;
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException($"Setting length is not available for {nameof(SubStream)}.");
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException($"Writing is not available for {nameof(SubStream)}.");
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/make-internal.sh b/src/Sentry.Android.AssemblyReader/ELFSharp/make-internal.sh
new file mode 100755
index 0000000000..4d04f6dd78
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/ELFSharp/make-internal.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+set -e
+
+find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public sealed class/internal sealed class/g'
+find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public static class/internal static class/g'
+find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public partial class/internal partial class/g'
+find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public class/internal class/g'
+find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public struct/internal struct/g'
+find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public enum/internal enum/g'
+find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public interface/internal interface/g'
diff --git a/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj
index f64bf0f045..2c6dcde33e 100644
--- a/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj
+++ b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj
@@ -1,16 +1,12 @@
- netstandard2.0;net8.0;net9.0
+ net8.0;net9.0
.NET assembly reader for Android
-
-
-
-
-
+
@@ -30,4 +26,8 @@
+
+
+
+
diff --git a/src/Sentry.Android.AssemblyReader/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt
similarity index 100%
rename from src/Sentry.Android.AssemblyReader/ATTRIBUTION.txt
rename to src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt
diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs
similarity index 73%
rename from src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs
rename to src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs
index b32a11112f..30314a5546 100644
--- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs
+++ b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs
@@ -1,9 +1,9 @@
-namespace Sentry.Android.AssemblyReader;
+namespace Sentry.Android.AssemblyReader.V1;
// The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file.
-internal sealed class AndroidAssemblyDirectoryReader : AndroidAssemblyReader, IAndroidAssemblyReader
+internal sealed class AndroidAssemblyDirectoryReaderV1 : AndroidAssemblyReader, IAndroidAssemblyReader
{
- public AndroidAssemblyDirectoryReader(ZipArchive zip, IList supportedAbis, DebugLogger? logger)
+ public AndroidAssemblyDirectoryReaderV1(ZipArchive zip, IList supportedAbis, DebugLogger? logger)
: base(zip, supportedAbis, logger) { }
public PEReader? TryReadAssembly(string name)
@@ -25,13 +25,8 @@ public AndroidAssemblyDirectoryReader(ZipArchive zip, IList supportedAbi
Logger?.Invoke("Resolved assembly {0} in the APK at {1}", name, zipEntry.FullName);
// We need a seekable stream for the PEReader (or even to check whether the DLL is compressed), so make a copy.
- var memStream = new MemoryStream((int)zipEntry.Length);
- using (var zipStream = zipEntry.Open())
- {
- zipStream.CopyTo(memStream);
- memStream.Position = 0;
- }
- return CreatePEReader(name, memStream);
+ var memStream = zipEntry.Extract();
+ return ArchiveUtils.CreatePEReader(name, memStream, Logger);
}
private ZipArchiveEntry? FindAssembly(string name)
diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyStoreReader.cs b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs
similarity index 97%
rename from src/Sentry.Android.AssemblyReader/AndroidAssemblyStoreReader.cs
rename to src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs
index dbdb5451ba..3073cc2095 100644
--- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyStoreReader.cs
+++ b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs
@@ -1,11 +1,11 @@
-namespace Sentry.Android.AssemblyReader;
+namespace Sentry.Android.AssemblyReader.V1;
// See https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores
-internal sealed class AndroidAssemblyStoreReader : AndroidAssemblyReader, IAndroidAssemblyReader
+internal sealed class AndroidAssemblyStoreReaderV1 : AndroidAssemblyReader, IAndroidAssemblyReader
{
private readonly AssemblyStoreExplorer _explorer;
- public AndroidAssemblyStoreReader(ZipArchive zip, IList supportedAbis, DebugLogger? logger)
+ public AndroidAssemblyStoreReaderV1(ZipArchive zip, IList supportedAbis, DebugLogger? logger)
: base(zip, supportedAbis, logger)
{
_explorer = new(zip, supportedAbis, logger);
@@ -29,7 +29,7 @@ public AndroidAssemblyStoreReader(ZipArchive zip, IList supportedAbis, D
return null;
}
- return CreatePEReader(name, stream);
+ return ArchiveUtils.CreatePEReader(name, stream, Logger);
}
private AssemblyStoreAssembly? TryFindAssembly(string name)
@@ -409,7 +409,7 @@ public AssemblyStoreReader(MemoryStream store, string? arch = null)
assembly.ConfigDataOffset == 0 ? null : GetDataSlice(assembly.ConfigDataOffset, assembly.ConfigDataSize);
private MemoryStream? GetDataSlice(uint offset, uint size) =>
- size == 0 ? null : new MemorySlice(_storeData, (int)offset, (int)size);
+ size == 0 ? null : new ArchiveUtils.MemorySlice(_storeData, (int)offset, (int)size);
public bool HasIdenticalContent(AssemblyStoreReader other)
{
diff --git a/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt
new file mode 100644
index 0000000000..e782059c7d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt
@@ -0,0 +1,28 @@
+Parts of the code in this subdirectory have been adapted from
+https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/assembly-store-reader.csproj
+
+The original license is as follows:
+
+The MIT License (MIT)
+
+Copyright (c) .NET Foundation and Contributors
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs
new file mode 100644
index 0000000000..d89f81da79
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs
@@ -0,0 +1,267 @@
+namespace Sentry.Android.AssemblyReader.V2;
+
+// The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file.
+internal sealed class AndroidAssemblyDirectoryReaderV2 : IAndroidAssemblyReader
+{
+ private DebugLogger? Logger { get; }
+ private HashSet SupportedArchitectures { get; } = new();
+ private readonly ArchiveAssemblyHelper _archiveAssemblyHelper;
+
+ public AndroidAssemblyDirectoryReaderV2(string apkPath, IList supportedAbis, DebugLogger? logger)
+ {
+ Logger = logger;
+ foreach (var abi in supportedAbis)
+ {
+ SupportedArchitectures.Add(abi.AbiToDeviceArchitecture());
+ }
+ _archiveAssemblyHelper = new ArchiveAssemblyHelper(apkPath, logger);
+ }
+
+ public PEReader? TryReadAssembly(string name)
+ {
+ if (File.Exists(name))
+ {
+ // The assembly is already extracted to the file system. Just read it.
+ var stream = File.OpenRead(name);
+ return new PEReader(stream);
+ }
+
+ foreach (var arch in SupportedArchitectures)
+ {
+ if (_archiveAssemblyHelper.ReadEntry($"assemblies/{name}", arch) is not { } memStream)
+ {
+ continue;
+ }
+
+ Logger?.Invoke("Resolved assembly {0} in the APK", name);
+ return ArchiveUtils.CreatePEReader(name, memStream, Logger);
+ }
+
+ Logger?.Invoke("Couldn't find assembly {0} in the APK", name);
+ return null;
+ }
+
+ public void Dispose()
+ {
+ // No-op
+ }
+
+ /*
+ * Adapted from https://github.com/dotnet/android/blob/6394773fad5108b0d7b4e6f087dc3e6ea997401a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ArchiveAssemblyHelper.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android-tools/blob/ab2165daf27d4fcb29e88bc022e0ab0be33aff69/LICENSE)
+ */
+ internal class ArchiveAssemblyHelper
+ {
+ private static readonly ArrayPool Buffers = ArrayPool.Shared;
+
+ private readonly string _archivePath;
+ private readonly DebugLogger? _logger;
+
+ public ArchiveAssemblyHelper(string archivePath, DebugLogger? logger)
+ {
+ if (string.IsNullOrEmpty(archivePath))
+ {
+ throw new ArgumentException("must not be null or empty", nameof(archivePath));
+ }
+
+ _archivePath = archivePath;
+ _logger = logger;
+ }
+
+ public MemoryStream? ReadEntry(string path, AndroidTargetArch arch = AndroidTargetArch.None, bool uncompressIfNecessary = false)
+ {
+ var ret = ReadZipEntry(path, arch);
+ if (ret == null)
+ {
+ return null;
+ }
+
+ ret.Flush();
+ ret.Seek(0, SeekOrigin.Begin);
+ var (elfPayloadOffset, elfPayloadSize, error) = Utils.FindELFPayloadSectionOffsetAndSize(ret);
+
+ if (error != ELFPayloadError.None)
+ {
+ var message = error switch
+ {
+ ELFPayloadError.NotELF => $"Entry '{path}' is not a valid ELF binary",
+ ELFPayloadError.LoadFailed => $"Entry '{path}' could not be loaded",
+ ELFPayloadError.NotSharedLibrary => $"Entry '{path}' is not a shared ELF library",
+ ELFPayloadError.NotLittleEndian => $"Entry '{path}' is not a little-endian ELF image",
+ ELFPayloadError.NoPayloadSection => $"Entry '{path}' does not contain the 'payload' section",
+ _ => $"Unknown ELF payload section error for entry '{path}': {error}"
+ };
+ _logger?.Invoke(message);
+ }
+ else
+ {
+ _logger?.Invoke($"Extracted content from ELF image '{path}'");
+ }
+
+ if (elfPayloadOffset == 0)
+ {
+ ret.Seek(0, SeekOrigin.Begin);
+ return ret;
+ }
+
+ // Make a copy of JUST the payload section, so that it contains only the data the tests expect and support
+ var payload = new MemoryStream();
+ var data = Buffers.Rent(16384);
+ var toRead = data.Length;
+ var nRead = 0;
+ var remaining = elfPayloadSize;
+
+ ret.Seek((long)elfPayloadOffset, SeekOrigin.Begin);
+ while (remaining > 0 && (nRead = ret.Read(data, 0, toRead)) > 0)
+ {
+ payload.Write(data, 0, nRead);
+ remaining -= (ulong)nRead;
+
+ if (remaining < (ulong)data.Length)
+ {
+ // Make sure the last chunk doesn't gobble in more than we need
+ toRead = (int)remaining;
+ }
+ }
+ Buffers.Return(data);
+
+ payload.Flush();
+ ret.Dispose();
+
+ payload.Seek(0, SeekOrigin.Begin);
+ return payload;
+ }
+
+ private MemoryStream? ReadZipEntry(string path, AndroidTargetArch arch)
+ {
+ var potentialEntries = TransformArchiveAssemblyPath(path, arch);
+ if (potentialEntries == null || potentialEntries.Count == 0)
+ {
+ return null;
+ }
+
+ using var zip = ZipFile.OpenRead(_archivePath);
+ foreach (var assemblyPath in potentialEntries)
+ {
+ if (zip.GetEntry(assemblyPath) is not { } entry)
+ {
+ continue;
+ }
+
+ var ret = entry.Extract();
+ ret.Flush();
+ return ret;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Takes "old style" `assemblies/assembly.dll` path and returns (if possible) a set of paths that reflect the new
+ /// location of `lib/{ARCH}/assembly.dll.so`. A list is returned because, if `arch` is `None`, we'll return all
+ /// the possible architectural paths.
+ /// An exception is thrown if we cannot transform the path for some reason. It should **not** be handled.
+ ///
+ private static List? TransformArchiveAssemblyPath(string path, AndroidTargetArch arch)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException(nameof(path), "must not be null or empty");
+ }
+
+ if (!path.StartsWith("assemblies/", StringComparison.Ordinal))
+ {
+ return [path];
+ }
+
+ var parts = path.Split('/');
+ if (parts.Length < 2)
+ {
+ throw new InvalidOperationException($"Path '{path}' must consist of at least two segments separated by `/`");
+ }
+
+ // We accept:
+ // assemblies/assembly.dll
+ // assemblies/{CULTURE}/assembly.dll
+ // assemblies/{ABI}/assembly.dll
+ // assemblies/{ABI}/{CULTURE}/assembly.dll
+ if (parts.Length > 4)
+ {
+ throw new InvalidOperationException($"Path '{path}' must not consist of more than 4 segments separated by `/`");
+ }
+
+ string? fileName = null;
+ string? culture = null;
+ string? abi = null;
+
+ switch (parts.Length)
+ {
+ // Full satellite assembly path, with abi
+ case 4:
+ abi = parts[1];
+ culture = parts[2];
+ fileName = parts[3];
+ break;
+
+ // Assembly path with abi or culture
+ case 3:
+ // If the middle part isn't a valid abi, we treat it as a culture name
+ if (MonoAndroidHelper.IsValidAbi(parts[1]))
+ {
+ abi = parts[1];
+ }
+ else
+ {
+ culture = parts[1];
+ }
+ fileName = parts[2];
+ break;
+
+ // Assembly path without abi or culture
+ case 2:
+ fileName = parts[1];
+ break;
+ }
+
+ var fileTypeMarker = MonoAndroidHelper.MANGLED_ASSEMBLY_REGULAR_ASSEMBLY_MARKER;
+ var abis = new List();
+ if (!string.IsNullOrEmpty(abi))
+ {
+ abis.Add(abi);
+ }
+ else if (arch == AndroidTargetArch.None)
+ {
+ foreach (AndroidTargetArch targetArch in MonoAndroidHelper.SupportedTargetArchitectures)
+ {
+ abis.Add(MonoAndroidHelper.ArchToAbi(targetArch));
+ }
+ }
+ else
+ {
+ abis.Add(MonoAndroidHelper.ArchToAbi(arch));
+ }
+
+ if (!string.IsNullOrEmpty(culture))
+ {
+ // Android doesn't allow us to put satellite assemblies in lib/{CULTURE}/assembly.dll.so, we must instead
+ // mangle the name.
+ fileTypeMarker = MonoAndroidHelper.MANGLED_ASSEMBLY_SATELLITE_ASSEMBLY_MARKER;
+ fileName = $"{culture}{MonoAndroidHelper.SATELLITE_CULTURE_END_MARKER_CHAR}{fileName}";
+ }
+
+ var ret = new List();
+ var newParts = new List {
+ string.Empty, // ABI placeholder
+ $"{fileTypeMarker}{fileName}.so",
+ };
+
+ foreach (var a in abis)
+ {
+ newParts[0] = a;
+ ret.Add(MonoAndroidHelper.MakeZipArchivePath("lib", newParts));
+ }
+
+ return ret;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs
new file mode 100644
index 0000000000..73b170a0c5
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs
@@ -0,0 +1,117 @@
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal class AndroidAssemblyStoreReaderV2 : IAndroidAssemblyReader
+{
+ private readonly IList _explorers;
+ private readonly DebugLogger? _logger;
+
+ private AndroidAssemblyStoreReaderV2(IList explorers, DebugLogger? logger)
+ {
+ _explorers = explorers;
+ _logger = logger;
+ }
+
+ public static bool TryReadStore(string inputFile, IList supportedAbis, DebugLogger? logger, [NotNullWhen(true)] out AndroidAssemblyStoreReaderV2? reader)
+ {
+ var (explorers, errorMessage) = AssemblyStoreExplorer.Open(inputFile, logger);
+ if (errorMessage != null)
+ {
+ logger?.Invoke(errorMessage);
+ reader = null;
+ return false;
+ }
+
+ List supportedExplorers = [];
+ if (explorers is not null)
+ {
+ foreach (var explorer in explorers)
+ {
+ if (explorer.TargetArch is null)
+ {
+ continue;
+ }
+
+ foreach (var supportedAbi in supportedAbis)
+ {
+ if (supportedAbi.AbiToDeviceArchitecture() == explorer.TargetArch)
+ {
+ supportedExplorers.Add(explorer);
+ }
+ }
+ }
+ }
+
+ if (supportedExplorers.Count == 0)
+ {
+ logger?.Invoke("Could not find V2 AssemblyStoreExplorer for the supported ABIs: {0}", string.Join(", ", supportedAbis));
+ reader = null;
+ return false;
+ }
+
+ reader = new AndroidAssemblyStoreReaderV2(supportedExplorers, logger);
+ return true;
+ }
+
+ public PEReader? TryReadAssembly(string name)
+ {
+ var explorerAssembly = TryFindAssembly(name);
+ if (explorerAssembly is null)
+ {
+ _logger?.Invoke("Couldn't find assembly {0} in the APK AssemblyStore", name);
+ return null;
+ }
+
+ var (explorer, storeItem) = explorerAssembly;
+ _logger?.Invoke("Resolved assembly {0} in the APK {1} AssemblyStore", name, storeItem.TargetArch);
+
+ var stream = explorer.ReadImageData(storeItem, false);
+ if (stream is null)
+ {
+ _logger?.Invoke("Couldn't access assembly {0} image stream", name);
+ return null;
+ }
+
+ return ArchiveUtils.CreatePEReader(name, stream, _logger);
+ }
+
+ private ExplorerStoreItem? TryFindAssembly(string name)
+ {
+ if (FindBestAssembly(name, out var assembly))
+ {
+ return assembly;
+ }
+
+ if ((IsFileType(".dll") || IsFileType(".exe")) && FindBestAssembly(name[..^4], out assembly))
+ {
+ return assembly;
+ }
+
+ return null;
+
+ bool IsFileType(string extension)
+ {
+ return name.EndsWith(extension, ignoreCase: true, CultureInfo.InvariantCulture);
+ }
+ }
+
+ private bool FindBestAssembly(string name, out ExplorerStoreItem? explorerAssembly)
+ {
+ foreach (var explorer in _explorers)
+ {
+ if (explorer.AssembliesByName?.TryGetValue(name, out var assembly) is true)
+ {
+ explorerAssembly = new(explorer, assembly);
+ return true;
+ }
+ }
+ explorerAssembly = null;
+ return false;
+ }
+
+ private record ExplorerStoreItem(AssemblyStoreExplorer Explorer, AssemblyStoreItem StoreItem);
+
+ public void Dispose()
+ {
+ // No-op
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidTargetArch.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidTargetArch.cs
new file mode 100644
index 0000000000..8564e04e6f
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AndroidTargetArch.cs
@@ -0,0 +1,18 @@
+/*
+ * Adapted from https://github.com/dotnet/android-tools/blob/ab2165daf27d4fcb29e88bc022e0ab0be33aff69/src/Xamarin.Android.Tools.AndroidSdk/AndroidTargetArch.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android-tools/blob/ab2165daf27d4fcb29e88bc022e0ab0be33aff69/LICENSE)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+[Flags]
+internal enum AndroidTargetArch
+{
+ None = 0,
+ Arm = 1,
+ X86 = 2,
+ Mips = 4,
+ Arm64 = 8,
+ X86_64 = 16,
+ Other = 0x10000 // hope it's not too optimistic
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs
new file mode 100644
index 0000000000..de2148ddeb
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs
@@ -0,0 +1,132 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreExplorer.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal class AssemblyStoreExplorer
+{
+ private readonly AssemblyStoreReader _reader;
+
+ public AndroidTargetArch? TargetArch { get; }
+ public IList? Assemblies { get; }
+ public IDictionary? AssembliesByName { get; }
+ public bool Is64Bit { get; }
+
+ private AssemblyStoreExplorer(Stream storeStream, string path, DebugLogger? logger)
+ {
+ var storeReader = AssemblyStoreReader.Create(storeStream, path, logger);
+ if (storeReader == null)
+ {
+ storeStream.Dispose();
+ throw new NotSupportedException($"Format of assembly store '{path}' is unsupported");
+ }
+
+ _reader = storeReader;
+ TargetArch = _reader.TargetArch;
+ Assemblies = _reader.Assemblies;
+ Is64Bit = _reader.Is64Bit;
+
+ var dict = new Dictionary(StringComparer.Ordinal);
+ if (Assemblies is not null)
+ {
+ foreach (var item in Assemblies)
+ {
+ dict.Add(item.Name, item);
+ }
+ }
+ AssembliesByName = dict.AsReadOnly();
+ }
+
+ private AssemblyStoreExplorer(FileInfo storeInfo, DebugLogger? logger)
+ : this(storeInfo.OpenRead(), storeInfo.FullName, logger)
+ { }
+
+ public static (IList? explorers, string? errorMessage) Open(string inputFile, DebugLogger? logger)
+ {
+ var (format, info) = Utils.DetectFileFormat(inputFile);
+ if (info == null)
+ {
+ return (null, $"File '{inputFile}' does not exist.");
+ }
+
+ switch (format)
+ {
+ case FileFormat.Unknown:
+ return (null, $"File '{inputFile}' has an unknown format.");
+
+ case FileFormat.Zip:
+ return (null, $"File '{inputFile}' is a ZIP archive, but not an Android one.");
+
+ case FileFormat.AssemblyStore:
+ case FileFormat.ELF:
+ return (new List { new AssemblyStoreExplorer(info, logger) }, null);
+
+ case FileFormat.Aab:
+ return OpenAab(info, logger);
+
+ case FileFormat.AabBase:
+ return OpenAabBase(info, logger);
+
+ case FileFormat.Apk:
+ return OpenApk(info, logger);
+
+ default:
+ return (null, $"File '{inputFile}' has an unsupported format '{format}'");
+ }
+ }
+
+ private static (IList? explorers, string? errorMessage) OpenAab(FileInfo fi, DebugLogger? logger)
+ => OpenCommon(fi, [StoreReaderV2.AabPaths, StoreReader_V1.AabPaths], logger);
+
+ private static (IList? explorers, string? errorMessage) OpenAabBase(FileInfo fi, DebugLogger? logger)
+ => OpenCommon(fi, [StoreReaderV2.AabBasePaths, StoreReader_V1.AabBasePaths], logger);
+
+ private static (IList? explorers, string? errorMessage) OpenApk(FileInfo fi, DebugLogger? logger)
+ => OpenCommon(fi, [StoreReaderV2.ApkPaths, StoreReader_V1.ApkPaths], logger);
+
+ private static (IList? explorers, string? errorMessage) OpenCommon(FileInfo fi, List> pathLists, DebugLogger? logger)
+ {
+ using var zip = ZipFile.OpenRead(fi.FullName);
+
+ foreach (var paths in pathLists)
+ {
+ var (explorers, errorMessage, pathsFound) = TryLoad(fi, zip, paths, logger);
+ if (pathsFound)
+ {
+ return (explorers, errorMessage);
+ }
+ }
+
+ return (null, "Unable to find any blob entries");
+ }
+
+ private static (IList? explorers, string? errorMessage, bool pathsFound) TryLoad(FileInfo fi, ZipArchive zip, IList paths, DebugLogger? logger)
+ {
+ var ret = new List();
+
+ foreach (var path in paths)
+ {
+ if (zip.GetEntry(path) is not { } entry)
+ {
+ continue;
+ }
+
+ var stream = entry.Extract();
+ ret.Add(new AssemblyStoreExplorer(stream, $"{fi.FullName}!{path}", logger));
+ }
+
+ if (ret.Count == 0)
+ {
+ return (null, null, false);
+ }
+
+ return (ret, null, true);
+ }
+
+ public MemoryStream? ReadImageData(AssemblyStoreItem item, bool uncompressIfNeeded = false)
+ {
+ return _reader.ReadEntryImageData(item, uncompressIfNeeded);
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs
new file mode 100644
index 0000000000..a132f51ead
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs
@@ -0,0 +1,27 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/86260ed36dfe1a90c8ed6a2bb1cd0607d637f403/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal abstract class AssemblyStoreItem
+{
+ public string Name { get; }
+ public IList Hashes { get; }
+ public bool Is64Bit { get; }
+ public uint DataOffset { get; protected set; }
+ public uint DataSize { get; protected set; }
+ public uint DebugOffset { get; protected set; }
+ public uint DebugSize { get; protected set; }
+ public uint ConfigOffset { get; protected set; }
+ public uint ConfigSize { get; protected set; }
+ public AndroidTargetArch TargetArch { get; protected set; }
+
+ protected AssemblyStoreItem(string name, bool is64Bit, List hashes)
+ {
+ Name = name;
+ Hashes = hashes.AsReadOnly();
+ Is64Bit = is64Bit;
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs
new file mode 100644
index 0000000000..834cb2c36d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs
@@ -0,0 +1,93 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreReader.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal abstract class AssemblyStoreReader
+{
+ protected DebugLogger? Logger { get; }
+
+ private static readonly UTF8Encoding ReaderEncoding = new UTF8Encoding(false);
+
+ protected Stream StoreStream { get; }
+
+ public abstract string Description { get; }
+ public abstract bool NeedsExtensionInName { get; }
+ public string StorePath { get; }
+
+ public AndroidTargetArch TargetArch { get; protected set; } = AndroidTargetArch.Arm;
+ public uint AssemblyCount { get; protected set; }
+ public uint IndexEntryCount { get; protected set; }
+ public IList? Assemblies { get; protected set; }
+ public bool Is64Bit { get; protected set; }
+
+ protected AssemblyStoreReader(Stream store, string path, DebugLogger? logger)
+ {
+ StoreStream = store;
+ StorePath = path;
+ Logger = logger;
+ }
+
+ public static AssemblyStoreReader? Create(Stream store, string path, DebugLogger? logger)
+ {
+ var reader = MakeReaderReady(new StoreReader_V1(store, path, logger));
+ if (reader != null)
+ {
+ return reader;
+ }
+
+ reader = MakeReaderReady(new StoreReaderV2(store, path, logger));
+ if (reader != null)
+ {
+ return reader;
+ }
+
+ return null;
+ }
+
+ private static AssemblyStoreReader? MakeReaderReady(AssemblyStoreReader reader)
+ {
+ if (!reader.IsSupported())
+ {
+ return null;
+ }
+
+ reader.Prepare();
+ return reader;
+ }
+
+ protected BinaryReader CreateReader() => new BinaryReader(StoreStream, ReaderEncoding, leaveOpen: true);
+
+ protected abstract bool IsSupported();
+ protected abstract void Prepare();
+ protected abstract ulong GetStoreStartDataOffset();
+
+ public MemoryStream ReadEntryImageData(AssemblyStoreItem entry, bool uncompressIfNeeded = false)
+ {
+ ulong startOffset = GetStoreStartDataOffset();
+ StoreStream.Seek((uint)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;
+ }
+ stream.Flush();
+ stream.Seek(0, SeekOrigin.Begin);
+
+ return stream;
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/DeviceArchitectureExtensions.cs b/src/Sentry.Android.AssemblyReader/V2/DeviceArchitectureExtensions.cs
new file mode 100644
index 0000000000..692789686f
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/DeviceArchitectureExtensions.cs
@@ -0,0 +1,15 @@
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal static class DeviceArchitectureExtensions
+{
+ public static AndroidTargetArch AbiToDeviceArchitecture(this string abi) =>
+ abi switch
+ {
+ "armeabi-v7a" => AndroidTargetArch.Arm,
+ "arm64-v8a" => AndroidTargetArch.Arm64,
+ "x86" => AndroidTargetArch.X86,
+ "x86_64" => AndroidTargetArch.X86_64,
+ "mips" => AndroidTargetArch.Mips,
+ _ => AndroidTargetArch.Other,
+ };
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/ELFPayloadError.cs b/src/Sentry.Android.AssemblyReader/V2/ELFPayloadError.cs
new file mode 100644
index 0000000000..b7644d68af
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/ELFPayloadError.cs
@@ -0,0 +1,16 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/ELFPayloadError.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal enum ELFPayloadError
+{
+ None,
+ NotELF,
+ LoadFailed,
+ NotSharedLibrary,
+ NotLittleEndian,
+ NoPayloadSection,
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/FileFormat.cs b/src/Sentry.Android.AssemblyReader/V2/FileFormat.cs
new file mode 100644
index 0000000000..d1cef2ad05
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/FileFormat.cs
@@ -0,0 +1,17 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/FileFormat.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal enum FileFormat
+{
+ Aab,
+ AabBase,
+ Apk,
+ AssemblyStore,
+ ELF,
+ Zip,
+ Unknown,
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/MonoAndroidHelper.Basic.cs b/src/Sentry.Android.AssemblyReader/V2/MonoAndroidHelper.Basic.cs
new file mode 100644
index 0000000000..fda2ff8ad1
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/MonoAndroidHelper.Basic.cs
@@ -0,0 +1,92 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/3822f2b1ee7061813b1d456af22e043e66e2f698/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.Basic.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal static class MonoAndroidHelper
+{
+ private static class AndroidAbi
+ {
+ public const string Arm32 = "armeabi-v7a";
+ public const string Arm64 = "arm64-v8a";
+ public const string X86 = "x86";
+ public const string X64 = "x86_64";
+ }
+
+ private static class RuntimeIdentifier
+ {
+ public const string Arm32 = "android-arm";
+ public const string Arm64 = "android-arm64";
+ public const string X86 = "android-x86";
+ public const string X64 = "android-x64";
+ }
+
+ public static readonly HashSet SupportedTargetArchitectures =
+ [
+ AndroidTargetArch.Arm,
+ AndroidTargetArch.Arm64,
+ AndroidTargetArch.X86,
+ AndroidTargetArch.X86_64
+ ];
+ private static readonly char[] ZipPathTrimmedChars = { '/', '\\' };
+ private static readonly Dictionary AbiToRidMap = new(StringComparer.OrdinalIgnoreCase) {
+ { AndroidAbi.Arm32, RuntimeIdentifier.Arm32 },
+ { AndroidAbi.Arm64, RuntimeIdentifier.Arm64 },
+ { AndroidAbi.X86, RuntimeIdentifier.X86 },
+ { AndroidAbi.X64, RuntimeIdentifier.X64 },
+ };
+ private static readonly Dictionary ArchToAbiMap = new Dictionary {
+ { AndroidTargetArch.Arm, AndroidAbi.Arm32 },
+ { AndroidTargetArch.Arm64, AndroidAbi.Arm64 },
+ { AndroidTargetArch.X86, AndroidAbi.X86 },
+ { AndroidTargetArch.X86_64, AndroidAbi.X64 },
+ };
+
+ public static string ArchToAbi(AndroidTargetArch arch)
+ {
+ if (!ArchToAbiMap.TryGetValue(arch, out var abi))
+ {
+ throw new InvalidOperationException($"Internal error: unsupported architecture '{arch}'");
+ }
+
+ return abi;
+ }
+
+ public static bool IsValidAbi(string abi) => AbiToRidMap.ContainsKey(abi);
+
+ public static string MakeZipArchivePath(string part1, ICollection? pathParts)
+ {
+ var parts = new List();
+ if (!string.IsNullOrEmpty(part1))
+ {
+ parts.Add(part1.TrimEnd(ZipPathTrimmedChars));
+ }
+
+ if (pathParts != null && pathParts.Count > 0)
+ {
+ foreach (string p in pathParts)
+ {
+ if (string.IsNullOrEmpty(p))
+ {
+ continue;
+ }
+ parts.Add(p.TrimEnd(ZipPathTrimmedChars));
+ }
+ }
+
+ if (parts.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ return string.Join("/", parts);
+ }
+
+ // These 3 MUST be the same as the like-named constants in src/monodroid/jni/shared-constants.hh
+ public const string MANGLED_ASSEMBLY_NAME_EXT = ".so";
+ public const string MANGLED_ASSEMBLY_REGULAR_ASSEMBLY_MARKER = "lib_";
+ public const string MANGLED_ASSEMBLY_SATELLITE_ASSEMBLY_MARKER = "lib-";
+ public const string SATELLITE_CULTURE_END_MARKER_CHAR = "_";
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs
new file mode 100644
index 0000000000..c1a3fe3c2d
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs
@@ -0,0 +1,38 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V1.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal 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; }
+
+ static StoreReader_V1()
+ {
+ ApkPaths = new List().AsReadOnly();
+ AabPaths = new List().AsReadOnly();
+ AabBasePaths = new List().AsReadOnly();
+ }
+
+ public StoreReader_V1(Stream store, string path, DebugLogger? logger)
+ : base(store, path, logger)
+ { }
+
+ protected override bool IsSupported()
+ {
+ return false;
+ }
+
+ protected override void Prepare()
+ {
+ }
+
+ protected override ulong GetStoreStartDataOffset() => 0;
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs
new file mode 100644
index 0000000000..85aa91ba89
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs
@@ -0,0 +1,96 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal partial class StoreReaderV2
+{
+ private sealed class Header
+ {
+ public const uint NativeSize = 5 * sizeof(uint);
+
+ public readonly uint magic;
+ public readonly uint version;
+ public readonly uint entry_count;
+ public readonly uint index_entry_count;
+
+ // Index size in bytes
+ public readonly uint index_size;
+
+ public Header(uint magic, uint version, uint entry_count, uint index_entry_count, uint index_size)
+ {
+ this.magic = magic;
+ this.version = version;
+ this.entry_count = entry_count;
+ this.index_entry_count = index_entry_count;
+ this.index_size = index_size;
+ }
+ }
+
+ private sealed class IndexEntry
+ {
+ public readonly ulong name_hash;
+ public readonly uint descriptor_index;
+
+ public IndexEntry(ulong name_hash, uint descriptor_index)
+ {
+ this.name_hash = name_hash;
+ this.descriptor_index = descriptor_index;
+ }
+ }
+
+ private sealed class EntryDescriptor
+ {
+ public uint mapping_index;
+
+ public uint data_offset;
+ public uint data_size;
+
+ public uint debug_data_offset;
+ public uint debug_data_size;
+
+ public uint config_data_offset;
+ public uint config_data_size;
+ }
+
+ private sealed class StoreItemV2 : AssemblyStoreItem
+ {
+ public StoreItemV2(AndroidTargetArch targetArch, string name, bool is64Bit, List indexEntries, EntryDescriptor descriptor)
+ : base(name, is64Bit, IndexToHashes(indexEntries))
+ {
+ DataOffset = descriptor.data_offset;
+ DataSize = descriptor.data_size;
+ DebugOffset = descriptor.debug_data_offset;
+ DebugSize = descriptor.debug_data_size;
+ ConfigOffset = descriptor.config_data_offset;
+ ConfigSize = descriptor.config_data_size;
+ TargetArch = targetArch;
+ }
+
+ private static List IndexToHashes(List indexEntries)
+ {
+ var ret = new List();
+ foreach (var ie in indexEntries)
+ {
+ ret.Add(ie.name_hash);
+ }
+
+ return ret;
+ }
+ }
+
+ private sealed class TemporaryItem
+ {
+ public readonly string Name;
+ public readonly List IndexEntries = new List();
+ public readonly EntryDescriptor Descriptor;
+
+ public TemporaryItem(string name, EntryDescriptor descriptor)
+ {
+ Name = name;
+ Descriptor = descriptor;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs
new file mode 100644
index 0000000000..6a40e05bc0
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs
@@ -0,0 +1,241 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal partial class StoreReaderV2 : AssemblyStoreReader
+{
+ // Bit 31 is set for 64-bit platforms, cleared for the 32-bit ones
+ private const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000002; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant
+ private const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000002;
+ private const uint ASSEMBLY_STORE_FORMAT_VERSION_MASK = 0xF0000000;
+ private const uint ASSEMBLY_STORE_ABI_AARCH64 = 0x00010000;
+ private const uint ASSEMBLY_STORE_ABI_ARM = 0x00020000;
+ private const uint ASSEMBLY_STORE_ABI_X64 = 0x00030000;
+ private const uint ASSEMBLY_STORE_ABI_X86 = 0x00040000;
+ private const uint ASSEMBLY_STORE_ABI_MASK = 0x00FF0000;
+
+ public override string Description => "Assembly store v2";
+ public override bool NeedsExtensionInName => true;
+
+ public static IList ApkPaths { get; }
+ public static IList AabPaths { get; }
+ public static IList AabBasePaths { get; }
+
+ private readonly HashSet supportedVersions;
+ private Header? header;
+ private ulong elfOffset = 0;
+
+ static StoreReaderV2()
+ {
+ var paths = new List {
+ GetArchPath (AndroidTargetArch.Arm64),
+ GetArchPath (AndroidTargetArch.Arm),
+ GetArchPath (AndroidTargetArch.X86_64),
+ GetArchPath (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),
+ };
+ AabPaths = paths.AsReadOnly();
+
+ string GetArchPath(AndroidTargetArch arch, string? root = null)
+ {
+ const string LibDirName = "lib";
+
+ string abi = MonoAndroidHelper.ArchToAbi(arch);
+ var parts = new List();
+ if (!string.IsNullOrEmpty(root))
+ {
+ parts.Add(LibDirName);
+ }
+ else
+ {
+ root = LibDirName;
+ }
+ parts.Add(abi);
+ parts.Add(GetBlobName(abi));
+
+ return MonoAndroidHelper.MakeZipArchivePath(root, parts);
+ }
+ }
+
+ public StoreReaderV2(Stream store, string path, DebugLogger? logger)
+ : base(store, path, logger)
+ {
+ 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,
+ };
+ }
+
+ private static string GetBlobName(string abi) => $"libassemblies.{abi}.blob.so";
+
+ protected override ulong GetStoreStartDataOffset() => elfOffset;
+
+ protected override bool IsSupported()
+ {
+ StoreStream.Seek(0, SeekOrigin.Begin);
+ using var reader = CreateReader();
+
+ uint magic = reader.ReadUInt32();
+ if (magic == Utils.ELFMagic)
+ {
+ ELFPayloadError error;
+ (elfOffset, _, error) = Utils.FindELFPayloadSectionOffsetAndSize(StoreStream);
+
+ if (error != ELFPayloadError.None)
+ {
+ string message = error switch
+ {
+ ELFPayloadError.NotELF => $"Store '{StorePath}' is not a valid ELF binary",
+ 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",
+ _ => $"Unknown ELF payload section error for store '{StorePath}': {error}"
+ };
+ Logger?.Invoke(message);
+ // Was originally:
+ // ```
+ // } else if (elfOffset >= 0) {
+ // ```
+ // However since elfOffset is an ulong, it will never be less than 0
+ }
+ else
+ {
+ StoreStream.Seek((long)elfOffset, SeekOrigin.Begin);
+ magic = reader.ReadUInt32();
+ }
+ }
+
+ if (magic != Utils.AssemblyStoreMagic)
+ {
+ Logger?.Invoke("Store '{0}' has invalid header magic number.", StorePath);
+ return false;
+ }
+
+ uint version = reader.ReadUInt32();
+ if (!supportedVersions.Contains(version))
+ {
+ Logger?.Invoke("Store '{0}' has unsupported version 0x{1:x}", StorePath, version);
+ return false;
+ }
+
+ uint entry_count = reader.ReadUInt32();
+ uint index_entry_count = reader.ReadUInt32();
+ uint index_size = reader.ReadUInt32();
+
+ header = new Header(magic, version, entry_count, index_entry_count, index_size);
+ return true;
+ }
+
+ protected override void Prepare()
+ {
+ if (header == null)
+ {
+ throw new InvalidOperationException("Internal error: header not set, was IsSupported() called?");
+ }
+
+ TargetArch = (header.version & ASSEMBLY_STORE_ABI_MASK) switch
+ {
+ ASSEMBLY_STORE_ABI_AARCH64 => AndroidTargetArch.Arm64,
+ ASSEMBLY_STORE_ABI_ARM => AndroidTargetArch.Arm,
+ ASSEMBLY_STORE_ABI_X64 => AndroidTargetArch.X86_64,
+ ASSEMBLY_STORE_ABI_X86 => AndroidTargetArch.X86,
+ _ => throw new NotSupportedException($"Unsupported ABI in store version: 0x{header.version:x}")
+ };
+
+ Is64Bit = (header.version & ASSEMBLY_STORE_FORMAT_VERSION_MASK) != 0;
+ AssemblyCount = header.entry_count;
+ IndexEntryCount = header.index_entry_count;
+
+ StoreStream.Seek((long)elfOffset + Header.NativeSize, SeekOrigin.Begin);
+ using var reader = CreateReader();
+
+ var index = new List();
+ for (uint i = 0; i < header.index_entry_count; i++)
+ {
+ ulong name_hash;
+ if (Is64Bit)
+ {
+ name_hash = reader.ReadUInt64();
+ }
+ else
+ {
+ name_hash = (ulong)reader.ReadUInt32();
+ }
+
+ uint descriptor_index = reader.ReadUInt32();
+ index.Add(new IndexEntry(name_hash, descriptor_index));
+ }
+
+ var descriptors = new List();
+ for (uint i = 0; i < header.entry_count; i++)
+ {
+ uint mapping_index = reader.ReadUInt32();
+ uint data_offset = reader.ReadUInt32();
+ uint data_size = reader.ReadUInt32();
+ uint debug_data_offset = reader.ReadUInt32();
+ uint debug_data_size = reader.ReadUInt32();
+ uint config_data_offset = reader.ReadUInt32();
+ uint config_data_size = reader.ReadUInt32();
+
+ var desc = new EntryDescriptor
+ {
+ mapping_index = mapping_index,
+ data_offset = data_offset,
+ data_size = data_size,
+ debug_data_offset = debug_data_offset,
+ debug_data_size = debug_data_size,
+ config_data_offset = config_data_offset,
+ config_data_size = config_data_size,
+ };
+ descriptors.Add(desc);
+ }
+
+ var names = new List();
+ for (uint i = 0; i < header.entry_count; i++)
+ {
+ uint name_length = reader.ReadUInt32();
+ byte[] name_bytes = reader.ReadBytes((int)name_length);
+ names.Add(Encoding.UTF8.GetString(name_bytes));
+ }
+
+ var tempItems = new Dictionary();
+ foreach (IndexEntry ie in index)
+ {
+ if (!tempItems.TryGetValue(ie.descriptor_index, out TemporaryItem? item))
+ {
+ item = new TemporaryItem(names[(int)ie.descriptor_index], descriptors[(int)ie.descriptor_index]);
+ tempItems.Add(ie.descriptor_index, item);
+ }
+ item.IndexEntries.Add(ie);
+ }
+
+ if (tempItems.Count != descriptors.Count)
+ {
+ throw new InvalidOperationException($"Assembly store '{StorePath}' index is corrupted.");
+ }
+
+ var storeItems = new List();
+ foreach (var kvp in tempItems)
+ {
+ TemporaryItem ti = kvp.Value;
+ var item = new StoreItemV2(TargetArch, ti.Name, Is64Bit, ti.IndexEntries, ti.Descriptor);
+ storeItems.Add(item);
+ }
+ Assemblies = storeItems.AsReadOnly();
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/Utils.cs b/src/Sentry.Android.AssemblyReader/V2/Utils.cs
new file mode 100644
index 0000000000..750f0eea62
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/Utils.cs
@@ -0,0 +1,169 @@
+/*
+ * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/Utils.cs
+ * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT)
+ */
+using ELFSharp.ELF;
+using ELFSharp.ELF.Sections;
+using Machine = ELFSharp.ELF.Machine;
+
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal static class Utils
+{
+ private static readonly string[] AabZipEntries = {
+ "base/manifest/AndroidManifest.xml",
+ "BundleConfig.pb",
+ };
+ private static readonly string[] AabBaseZipEntries = {
+ "manifest/AndroidManifest.xml",
+ };
+ private static readonly string[] ApkZipEntries = {
+ "AndroidManifest.xml",
+ };
+
+ public const uint ZipMagic = 0x4034b50;
+ public const uint AssemblyStoreMagic = 0x41424158;
+ public const uint ELFMagic = 0x464c457f;
+
+ public static readonly ArrayPool BytePool = ArrayPool.Shared;
+
+ public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadSectionOffsetAndSize(Stream stream)
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ var elfClass = ELFReader.CheckELFType(stream);
+ if (elfClass == Class.NotELF)
+ {
+ return ReturnError(null, ELFPayloadError.NotELF);
+ }
+
+ if (!ELFReader.TryLoad(stream, shouldOwnStream: false, out IELF? elf))
+ {
+ return ReturnError(elf, ELFPayloadError.LoadFailed);
+ }
+
+ if (elf.Type != FileType.SharedObject)
+ {
+ return ReturnError(elf, ELFPayloadError.NotSharedLibrary);
+ }
+
+ if (elf.Endianess != ELFSharp.Endianess.LittleEndian)
+ {
+ return ReturnError(elf, ELFPayloadError.NotLittleEndian);
+ }
+
+ if (!elf.TryGetSection("payload", out ISection? payloadSection))
+ {
+ return ReturnError(elf, ELFPayloadError.NoPayloadSection);
+ }
+
+ var is64 = elf.Machine switch
+ {
+ Machine.ARM => false,
+ Machine.Intel386 => false,
+
+ Machine.AArch64 => true,
+ Machine.AMD64 => true,
+
+ _ => throw new NotSupportedException($"Unsupported ELF architecture '{elf.Machine}'")
+ };
+
+ ulong offset;
+ ulong size;
+
+ if (is64)
+ {
+ (offset, size) = GetOffsetAndSize64((Section)payloadSection);
+ }
+ else
+ {
+ (offset, size) = GetOffsetAndSize32((Section)payloadSection);
+ }
+
+ elf.Dispose();
+ return (offset, size, ELFPayloadError.None);
+
+ (ulong offset, ulong size) GetOffsetAndSize64(Section payload)
+ {
+ return (payload.Offset, payload.Size);
+ }
+
+ (ulong offset, ulong size) GetOffsetAndSize32(Section payload)
+ {
+ return ((ulong)payload.Offset, (ulong)payload.Size);
+ }
+
+ (ulong offset, ulong size, ELFPayloadError error) ReturnError(IELF? elf, ELFPayloadError error)
+ {
+ elf?.Dispose();
+
+ return (0, 0, error);
+ }
+ }
+
+ public static (FileFormat format, FileInfo? info) DetectFileFormat(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return (FileFormat.Unknown, null);
+ }
+
+ var info = new FileInfo(path);
+ if (!info.Exists)
+ {
+ return (FileFormat.Unknown, null);
+ }
+
+ using var reader = new BinaryReader(info.OpenRead());
+
+ // ATM, all formats we recognize have 4-byte magic at the start
+ var format = reader.ReadUInt32() switch
+ {
+ ZipMagic => FileFormat.Zip,
+ ELFMagic => FileFormat.ELF,
+ AssemblyStoreMagic => FileFormat.AssemblyStore,
+ _ => FileFormat.Unknown
+ };
+
+ if (format == FileFormat.Unknown || format != FileFormat.Zip)
+ {
+ return (format, info);
+ }
+
+ return (DetectAndroidArchive(info, format), info);
+ }
+
+ private static FileFormat DetectAndroidArchive(FileInfo info, FileFormat defaultFormat)
+ {
+ using var zip = ZipFile.OpenRead(info.FullName);
+
+ if (HasAllEntries(zip, AabZipEntries))
+ {
+ return FileFormat.Aab;
+ }
+
+ if (HasAllEntries(zip, ApkZipEntries))
+ {
+ return FileFormat.Apk;
+ }
+
+ if (HasAllEntries(zip, AabBaseZipEntries))
+ {
+ return FileFormat.AabBase;
+ }
+
+ return defaultFormat;
+
+ static bool HasAllEntries(ZipArchive zip, string[] entries)
+ {
+ foreach (var entry in entries)
+ {
+ if (zip.GetEntry(entry) is null)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/test/AndroidTestApp/AndroidTestApp.csproj b/test/AndroidTestApp/AndroidTestApp.csproj
index 15dedbe4c9..3d8a1981e1 100644
--- a/test/AndroidTestApp/AndroidTestApp.csproj
+++ b/test/AndroidTestApp/AndroidTestApp.csproj
@@ -1,6 +1,7 @@
- net8.0-android34.0
+ net8.0-android;net9.0-android
+ false
21
Exe
enable
diff --git a/test/Directory.Build.props b/test/Directory.Build.props
index d6cc562cef..4cc80884d9 100644
--- a/test/Directory.Build.props
+++ b/test/Directory.Build.props
@@ -46,7 +46,7 @@
-
+
diff --git a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs
index 4b98a43257..4a9985f827 100644
--- a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs
+++ b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs
@@ -1,15 +1,27 @@
+using Sentry.Android.AssemblyReader.V1;
+using Sentry.Android.AssemblyReader.V2;
+
namespace Sentry.Android.AssemblyReader.Tests;
public class AndroidAssemblyReaderTests
{
private readonly ITestOutputHelper _output;
+#if NET9_0
+ private static string TargetFramework => "net9.0";
+#elif NET8_0
+ private static string TargetFramework => "net8.0";
+#else
+ // Adding a new TFM to the project? Include it above
+#error "Target Framework not yet supported for AndroidAssemblyReader"
+#endif
+
public AndroidAssemblyReaderTests(ITestOutputHelper output)
{
_output = output;
}
- private IAndroidAssemblyReader GetSut(bool isAssemblyStore, bool isCompressed)
+ private IAndroidAssemblyReader GetSut(bool isAot, bool isAssemblyStore, bool isCompressed)
{
#if ANDROID
var logger = new TestOutputDiagnosticLogger(_output);
@@ -19,33 +31,55 @@ private IAndroidAssemblyReader GetSut(bool isAssemblyStore, bool isCompressed)
Path.GetFullPath(Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
"..", "..", "..", "TestAPKs",
- $"android-Store={isAssemblyStore}-Compressed={isCompressed}.apk"));
+ $"{TargetFramework}-android-A={isAot}-S={isAssemblyStore}-C={isCompressed}.apk"));
_output.WriteLine($"Checking if APK exists: {apkPath}");
File.Exists(apkPath).Should().BeTrue();
+ // Note: This needs to match the RID used when publishing the test APK
string[] supportedAbis = { "x86_64" };
return AndroidAssemblyReaderFactory.Open(apkPath, supportedAbis,
logger: (message, args) => _output.WriteLine(message, args));
#endif
}
- [SkippableTheory]
- [InlineData(false)]
- [InlineData(true)]
- public void CreatesCorrectReader(bool isAssemblyStore)
+ [SkippableFact]
+ public void CreatesCorrectStoreReader()
{
#if ANDROID
Skip.If(true, "It's unknown whether the current Android app APK is an assembly store or not.");
#endif
- using var sut = GetSut(isAssemblyStore, isCompressed: true);
- if (isAssemblyStore)
+ using var sut = GetSut(isAot: false, isAssemblyStore: true, isCompressed: true);
+ switch (TargetFramework)
{
- Assert.IsType(sut);
+ case "net9.0":
+ Assert.IsType(sut);
+ break;
+ case "net8.0":
+ Assert.IsType(sut);
+ break;
+ default:
+ throw new NotSupportedException($"Unsupported target framework: {TargetFramework}");
}
- else
+ }
+
+ [SkippableFact]
+ public void CreatesCorrectArchiveReader()
+ {
+#if ANDROID
+ Skip.If(true, "It's unknown whether the current Android app APK is an assembly store or not.");
+#endif
+ using var sut = GetSut(isAot: false, isAssemblyStore: false, isCompressed: true);
+ switch (TargetFramework)
{
- Assert.IsType(sut);
+ case "net9.0":
+ Assert.IsType(sut);
+ break;
+ case "net8.0":
+ Assert.IsType(sut);
+ break;
+ default:
+ throw new NotSupportedException($"Unsupported target framework: {TargetFramework}");
}
}
@@ -54,27 +88,32 @@ public void CreatesCorrectReader(bool isAssemblyStore)
[InlineData(true)]
public void ReturnsNullIfAssemblyDoesntExist(bool isAssemblyStore)
{
- using var sut = GetSut(isAssemblyStore, isCompressed: true);
+ using var sut = GetSut(isAot: false, isAssemblyStore, isCompressed: true);
Assert.Null(sut.TryReadAssembly("NonExistent.dll"));
}
+ public static IEnumerable
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
- <_ConfigString>Store=$(_Store)-Compressed=$(_Compressed)
+ <_ConfigString>A=$(_Aot)-S=$(_Store)-C=$(_Compressed)
+ ..\AndroidTestApp\bin\$(TargetFramework)\$(_ConfigString)\com.companyname.AndroidTestApp-Signed.apk
+ TestAPKs\$(TargetFramework)-$(_ConfigString).apk
+
+
+
+
+
+
+
+ True
+ False
-
-
+
diff --git a/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj b/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj
index 947d10b537..7b0d03edad 100644
--- a/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj
+++ b/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj
@@ -2,9 +2,9 @@
net9.0;net8.0;net48
- $(TargetFrameworks);net8.0-android34.0
- $(TargetFrameworks);net8.0-ios17.0
- $(TargetFrameworks);net8.0-maccatalyst17.0
+ $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0
+ $(TargetFrameworks);net8.0-ios17.0;net9.0-ios18.0
+ $(TargetFrameworks);net8.0-maccatalyst17.0;net9.0-maccatalyst18.0
diff --git a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj
index 5953b65d70..f08b24c8c4 100644
--- a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj
+++ b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj
@@ -2,8 +2,8 @@
- $(TargetFrameworks);net8.0-android34.0
- $(TargetFrameworks);net8.0-ios17.0
+ $(TargetFrameworks);net8.0-android;net9.0-android
+ $(TargetFrameworks);net8.0-ios;net9.0-ios
true