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 bd7277914e..83528a33df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,15 +2,16 @@
## 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))
- 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))
-### 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.42.4 ([#4036](https://github.com/getsentry/sentry-dotnet/pull/4036), [#4049](https://github.com/getsentry/sentry-dotnet/pull/4049))
diff --git a/ELFPayloadError.cs b/ELFPayloadError.cs
new file mode 100644
index 0000000000..932f151c80
--- /dev/null
+++ b/ELFPayloadError.cs
@@ -0,0 +1,11 @@
+namespace Xamarin.Android.AssemblyStore;
+
+enum ELFPayloadError
+{
+ None,
+ NotELF,
+ LoadFailed,
+ NotSharedLibrary,
+ NotLittleEndian,
+ NoPayloadSection,
+}
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..728c07e4ed 100644
--- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs
+++ b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs
@@ -18,9 +18,9 @@ public void Dispose()
ZipArchive.Dispose();
}
- protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream)
+ internal static PEReader CreatePEReader(string assemblyName, MemoryStream inputStream, DebugLogger? logger)
{
- var decompressedStream = TryDecompressLZ4(assemblyName, inputStream);
+ var decompressedStream = TryDecompressLZ4(assemblyName, inputStream, logger);
// Use the decompressed stream, or if null, i.e. it wasn't compressed, use the original.
return new PEReader(decompressedStream ?? inputStream);
@@ -35,7 +35,7 @@ protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream)
/// [rest: lz4 compressed payload]
///
///
- private Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream)
+ private static Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream, DebugLogger? logger)
{
const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian
const int payloadOffset = 12;
@@ -51,7 +51,7 @@ protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream)
Debug.Assert(inputStream.Position == payloadOffset);
var inputLength = (int)(inputStream.Length - payloadOffset);
- Logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength);
+ logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength);
var outputStream = new MemoryStream(decompressedLength);
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/Sentry.Android.AssemblyReader.csproj b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj
index f64bf0f045..dac014317f 100644
--- a/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj
+++ b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj
@@ -1,15 +1,12 @@
- netstandard2.0;net8.0;net9.0
+ net8.0;net9.0
.NET assembly reader for Android
-
-
-
-
+
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..eaa000638f 100644
--- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs
+++ b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs
@@ -1,9 +1,11 @@
-namespace Sentry.Android.AssemblyReader;
+using Sentry.Android.AssemblyReader.V2;
+
+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 +27,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 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 98%
rename from src/Sentry.Android.AssemblyReader/AndroidAssemblyStoreReader.cs
rename to src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs
index dbdb5451ba..914fe5c97a 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 CreatePEReader(name, stream, Logger);
}
private AssemblyStoreAssembly? TryFindAssembly(string name)
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..1ea64b2471
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs
@@ -0,0 +1,50 @@
+using Sentry.Android.AssemblyReader.V2;
+
+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, false);
+ }
+
+ 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 AndroidAssemblyReader.CreatePEReader(name, memStream, Logger);
+ }
+
+ Logger?.Invoke("Couldn't find assembly {0} in the APK", name);
+ return null;
+ }
+
+ public void Dispose()
+ {
+ // No-op
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs
new file mode 100644
index 0000000000..edcbd1e51e
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs
@@ -0,0 +1,143 @@
+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 AndroidAssemblyReader.CreatePEReader(name, stream, _logger);
+ }
+
+ private ExplorerStoreItem? TryFindAssembly(string name)
+ {
+ if (FindBestAssembly(name, out var assembly))
+ {
+ return assembly;
+ }
+
+ if (name.EndsWith(".dll", ignoreCase: true, CultureInfo.InvariantCulture) ||
+ name.EndsWith(".exe", ignoreCase: true, CultureInfo.InvariantCulture))
+ {
+ if (FindBestAssembly(name.Substring(0, name.Length - 4), out assembly))
+ {
+ return assembly;
+ }
+ }
+
+ return null;
+ }
+
+ 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
+ }
+}
+
+internal static class DeviceArchitectureExtensions
+{
+ public static bool IsSupportedBy(this Span abis, AndroidTargetArch targetArch)
+ {
+ foreach (var abi in abis)
+ {
+ if (abi.AbiToDeviceArchitecture() == targetArch)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ 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/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/ArchiveAssemblyHelper.cs b/src/Sentry.Android.AssemblyReader/V2/ArchiveAssemblyHelper.cs
new file mode 100644
index 0000000000..7dae377b06
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/ArchiveAssemblyHelper.cs
@@ -0,0 +1,709 @@
+/*
+ * 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)
+ */
+namespace Sentry.Android.AssemblyReader.V2;
+
+internal class ArchiveAssemblyHelper
+{
+ private const string DefaultAssemblyStoreEntryPrefix = "{storeReader}";
+
+ private static readonly HashSet SpecialExtensions = new(StringComparer.OrdinalIgnoreCase) {
+ ".dll",
+ ".config",
+ ".pdb",
+ };
+
+ private static readonly ArrayPool Buffers = ArrayPool.Shared;
+
+ private readonly string _archivePath;
+ private readonly DebugLogger? _logger;
+ private readonly string _assembliesRootDir;
+ private readonly bool _useAssemblyStores;
+ private List? _archiveContents;
+
+ public ArchiveAssemblyHelper(string archivePath, DebugLogger? logger, bool useAssemblyStores = true)
+ {
+ if (string.IsNullOrEmpty(archivePath))
+ {
+ throw new ArgumentException("must not be null or empty", nameof(archivePath));
+ }
+
+ _archivePath = archivePath;
+ _logger = logger;
+ _useAssemblyStores = useAssemblyStores;
+
+ var extension = Path.GetExtension(archivePath) ?? string.Empty;
+ if (string.Compare(".aab", extension, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ _assembliesRootDir = "base/lib/";
+ }
+ else if (string.Compare(".apk", extension, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ _assembliesRootDir = "lib/";
+ }
+ else if (string.Compare(".zip", extension, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ _assembliesRootDir = "lib/";
+ }
+ else
+ {
+ _assembliesRootDir = string.Empty;
+ }
+ }
+
+ public MemoryStream? ReadEntry(string path, AndroidTargetArch arch = AndroidTargetArch.None, bool uncompressIfNecessary = false)
+ {
+ var ret = _useAssemblyStores
+ ? ReadStoreEntry(path, arch, uncompressIfNecessary)
+ : 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}"
+ };
+ Console.WriteLine(message);
+ }
+ else
+ {
+ Console.WriteLine($"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;
+ }
+
+ private MemoryStream? ReadStoreEntry(string path, AndroidTargetArch arch, bool uncompressIfNecessary)
+ {
+ var name = Path.GetFileNameWithoutExtension(path);
+ var (explorers, errorMessage) = AssemblyStoreExplorer.Open(_archivePath, _logger);
+ var explorer = SelectExplorer(explorers, arch);
+ if (explorer == null)
+ {
+ Console.WriteLine($"Failed to read assembly '{name}' from '{_archivePath}'. {errorMessage}");
+ return null;
+ }
+
+ if (arch == AndroidTargetArch.None)
+ {
+ if (explorer.TargetArch == null)
+ {
+ throw new InvalidOperationException($"Internal error: explorer should not have its TargetArch unset");
+ }
+
+ arch = (AndroidTargetArch)explorer.TargetArch;
+ }
+
+ Console.WriteLine($"Trying to read store entry: {name}");
+ var assemblies = explorer.Find(name, arch);
+ if (assemblies == null)
+ {
+ Console.WriteLine($"Failed to locate assembly '{name}' in assembly store for architecture '{arch}', in archive '{_archivePath}'");
+ return null;
+ }
+
+ AssemblyStoreItem? assembly = null;
+ foreach (var item in assemblies)
+ {
+ if (arch == AndroidTargetArch.None || item.TargetArch == arch)
+ {
+ assembly = item;
+ break;
+ }
+ }
+
+ if (assembly == null)
+ {
+ Console.WriteLine($"Failed to find assembly '{name}' in assembly store for architecture '{arch}', in archive '{_archivePath}'");
+ return null;
+ }
+
+ return explorer.ReadImageData(assembly, uncompressIfNecessary);
+ }
+
+ public List ListArchiveContents(string storeEntryPrefix = DefaultAssemblyStoreEntryPrefix, bool forceRefresh = false, AndroidTargetArch arch = AndroidTargetArch.None)
+ {
+ if (!forceRefresh && _archiveContents != null)
+ {
+ return _archiveContents;
+ }
+
+ if (string.IsNullOrEmpty(storeEntryPrefix))
+ {
+ throw new ArgumentException(nameof(storeEntryPrefix), "must not be null or empty");
+ }
+
+ var entries = new List();
+ using (var zip = ZipFile.OpenRead(_archivePath))
+ {
+ foreach (var entry in zip.Entries)
+ {
+ entries.Add(entry.FullName);
+ }
+ }
+
+ _archiveContents = entries;
+ if (!_useAssemblyStores)
+ {
+ Console.WriteLine("Not using assembly stores");
+ return entries;
+ }
+
+ Console.WriteLine($"Creating AssemblyStoreExplorer for archive '{_archivePath}'");
+ var (explorers, errorMessage) = AssemblyStoreExplorer.Open(_archivePath, _logger);
+
+ if (arch == AndroidTargetArch.None)
+ {
+ if (explorers == null || explorers.Count == 0)
+ {
+ return entries;
+ }
+
+ foreach (var explorer in explorers)
+ {
+ SynthetizeAssemblies(explorer);
+ }
+ }
+ else
+ {
+ SynthetizeAssemblies(SelectExplorer(explorers, arch));
+ }
+
+ Console.WriteLine("Archive entries with synthetised assembly storeReader entries:");
+ foreach (string e in entries)
+ {
+ Console.WriteLine($" {e}");
+ }
+
+ return entries;
+
+ void SynthetizeAssemblies(AssemblyStoreExplorer? explorer)
+ {
+ if (explorer == null)
+ {
+ return;
+ }
+
+ Console.WriteLine($"Explorer for {explorer.TargetArch} found {explorer.AssemblyCount} assemblies");
+ if (explorer.Assemblies is null)
+ {
+ return;
+ }
+
+ foreach (var asm in explorer.Assemblies)
+ {
+ var prefix = storeEntryPrefix;
+ var abi = MonoAndroidHelper.ArchToAbi(asm.TargetArch);
+ prefix = $"{prefix}{abi}/";
+
+ var cultureIndex = asm.Name.IndexOf('/');
+ string? culture = null;
+ string name;
+
+ if (cultureIndex > 0)
+ {
+ culture = asm.Name.Substring(0, cultureIndex);
+ name = asm.Name.Substring(cultureIndex + 1);
+ }
+ else
+ {
+ name = asm.Name;
+ }
+
+ // Mangle name in the same fashion the discrete assembly entries are named, makes other
+ // code in this class simpler.
+ var mangledName = MonoAndroidHelper.MakeDiscreteAssembliesEntryName(name, culture);
+ entries.Add($"{prefix}{mangledName}");
+ if (asm.DebugOffset > 0)
+ {
+ mangledName = MonoAndroidHelper.MakeDiscreteAssembliesEntryName(Path.ChangeExtension(name, "pdb"));
+ entries.Add($"{prefix}{mangledName}");
+ }
+
+ if (asm.ConfigOffset > 0)
+ {
+ mangledName = MonoAndroidHelper.MakeDiscreteAssembliesEntryName(Path.ChangeExtension(name, "config"));
+ entries.Add($"{prefix}{mangledName}");
+ }
+ }
+ }
+ }
+
+ internal AssemblyStoreExplorer? SelectExplorer(IList? explorers, string rid)
+ {
+ return SelectExplorer(explorers, MonoAndroidHelper.RidToArch(rid));
+ }
+
+ internal AssemblyStoreExplorer? SelectExplorer(IList? explorers, AndroidTargetArch arch)
+ {
+ if (explorers == null || explorers.Count == 0)
+ {
+ return null;
+ }
+
+ // If we don't care about target architecture, we check the first store, since all of them will have the same
+ // assemblies. Otherwise, we try to locate the correct store.
+ if (arch == AndroidTargetArch.None)
+ {
+ return explorers[0];
+ }
+
+ foreach (var e in explorers)
+ {
+ if (e.TargetArch == null || e.TargetArch != arch)
+ {
+ continue;
+ }
+ return e;
+ }
+
+ Console.WriteLine($"Failed to find assembly store for architecture '{arch}' in archive '{_archivePath}'");
+ 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 new List { 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;
+ }
+
+ internal static bool ArchiveContains(List archiveContents, string entryPath, AndroidTargetArch arch)
+ {
+ if (archiveContents.Count == 0)
+ {
+ return false;
+ }
+
+ var potentialEntries = TransformArchiveAssemblyPath(entryPath, arch);
+ if (potentialEntries == null || potentialEntries.Count == 0)
+ {
+ return false;
+ }
+
+ foreach (var wantedEntry in potentialEntries)
+ {
+ Console.WriteLine($"Wanted entry: {wantedEntry}");
+ foreach (var existingEntry in archiveContents)
+ {
+ if (string.Compare(existingEntry, wantedEntry, StringComparison.Ordinal) == 0)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks whether exists in the archive or assembly store. The path should use the
+ /// "old style" `assemblies/{ABI}/assembly.dll` format.
+ ///
+ public bool Exists(string entryPath, bool forceRefresh = false, AndroidTargetArch arch = AndroidTargetArch.None)
+ {
+ return ArchiveContains(ListArchiveContents(_assembliesRootDir, forceRefresh), entryPath, arch);
+ }
+
+ public void Contains(ICollection fileNames, out List existingFiles, out List missingFiles, out List additionalFiles, IEnumerable? targetArches = null)
+ {
+ if (fileNames == null)
+ {
+ throw new ArgumentNullException(nameof(fileNames));
+ }
+
+ if (fileNames.Count == 0)
+ {
+ throw new ArgumentException("must not be empty", nameof(fileNames));
+ }
+
+ if (_useAssemblyStores)
+ {
+ StoreContains(fileNames, out existingFiles, out missingFiles, out additionalFiles, targetArches);
+ }
+ else
+ {
+ ArchiveContains(fileNames, out existingFiles, out missingFiles, out additionalFiles, targetArches);
+ }
+ }
+
+ private List GetSupportedArches(IEnumerable? runtimeIdentifiers)
+ {
+ var rids = new List();
+ if (runtimeIdentifiers != null)
+ {
+ rids.AddRange(runtimeIdentifiers);
+ }
+
+ if (rids.Count == 0)
+ {
+ rids.AddRange(MonoAndroidHelper.SupportedTargetArchitectures);
+ }
+
+ return rids;
+
+<<<<<<< TODO: Unmerged change from project 'Sentry.Android.AssemblyReader(net9.0)', Before:
+ void ListFiles(List existingFiles, List missingFiles, List additionalFiles)
+=======
+ private void ListFiles (List existingFiles, List missingFiles, List additionalFiles)
+>>>>>>> After
+ }
+
+ private void ListFiles(List existingFiles, List missingFiles, List additionalFiles)
+ {
+ Console.WriteLine("Archive contents:");
+ ListFiles("existing files", existingFiles);
+ ListFiles("missing files", missingFiles);
+ ListFiles("additional files", additionalFiles);
+
+ void ListFiles(string label, List list)
+ {
+ Console.WriteLine($" {label}:");
+ if (list.Count == 0)
+ {
+ Console.WriteLine(" none");
+ return;
+ }
+
+ foreach (string file in list)
+ {
+ Console.WriteLine($" {file}");
+
+<<<<<<< TODO: Unmerged change from project 'Sentry.Android.AssemblyReader(net9.0)', Before:
+ (string prefixAssemblies, string prefixLib) GetArchivePrefixes(string abi) => ($"{MonoAndroidHelper.MakeZipArchivePath(_assembliesRootDir, abi)}/", $"lib/{abi}/");
+=======
+ private (string prefixAssemblies, string prefixLib) GetArchivePrefixes (string abi) => ($"{MonoAndroidHelper.MakeZipArchivePath (_assembliesRootDir, abi)}/", $"lib/{abi}/");
+>>>>>>> After
+ }
+ }
+ }
+
+ private (string prefixAssemblies, string prefixLib) GetArchivePrefixes(string abi) => ($"{MonoAndroidHelper.MakeZipArchivePath(_assembliesRootDir, abi)}/", $"lib/{abi}/");
+
+ internal void ArchiveContains(ICollection fileNames, out List existingFiles, out List missingFiles, out List additionalFiles, IEnumerable? targetArches = null)
+ {
+ using var zip = ZipFile.OpenRead(_archivePath);
+ existingFiles = zip.Entries.Where(a => a.FullName.StartsWith(_assembliesRootDir, StringComparison.InvariantCultureIgnoreCase)).Select(a => a.FullName).ToList();
+ existingFiles.AddRange(zip.Entries.Where(a => a.FullName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)).Select(a => a.FullName));
+
+ var arches = GetSupportedArches(targetArches);
+
+ missingFiles = [];
+ additionalFiles = [];
+ foreach (var arch in arches)
+ {
+ string abi = MonoAndroidHelper.ArchToAbi(arch);
+ missingFiles.AddRange(GetMissingFilesForAbi(abi));
+ additionalFiles.AddRange(GetAdditionalFilesForAbi(abi, existingFiles));
+ }
+ ListFiles(existingFiles, missingFiles, additionalFiles);
+ return;
+
+ IEnumerable GetMissingFilesForAbi(string abi)
+ {
+ var (prefixAssemblies, prefixLib) = GetArchivePrefixes(abi);
+ return fileNames.Where(x =>
+ {
+ string? culture = null;
+ var fileName = x;
+ var slashIndex = x.IndexOf('/');
+ if (slashIndex > 0)
+ {
+ culture = x.Substring(0, slashIndex);
+ fileName = x.Substring(slashIndex + 1);
+ }
+
+ return !zip.ContainsEntry(MonoAndroidHelper.MakeZipArchivePath(prefixAssemblies, x)) &&
+ !zip.ContainsEntry(MonoAndroidHelper.MakeZipArchivePath(prefixLib, x)) &&
+ !zip.ContainsEntry(MonoAndroidHelper.MakeZipArchivePath(prefixAssemblies, MonoAndroidHelper.MakeDiscreteAssembliesEntryName(fileName, culture))) &&
+ !zip.ContainsEntry(MonoAndroidHelper.MakeZipArchivePath(prefixLib, MonoAndroidHelper.MakeDiscreteAssembliesEntryName(fileName, culture)));
+ });
+ }
+
+ IEnumerable GetAdditionalFilesForAbi(string abi, List existingFiles)
+ {
+ var (prefixAssemblies, prefixLib) = GetArchivePrefixes(abi);
+ return existingFiles.Where(x => !fileNames.Contains(x.Replace(prefixAssemblies, string.Empty)) && !fileNames.Contains(x.Replace(prefixLib, string.Empty)));
+ }
+ }
+
+ internal void StoreContains(ICollection fileNames, out List existingFiles, out List missingFiles, out List additionalFiles, IEnumerable? targetArches = null)
+ {
+ var assemblyNames = fileNames.Where(x => x.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)).ToList();
+ var configFiles = fileNames.Where(x => x.EndsWith(".config", StringComparison.OrdinalIgnoreCase)).ToList();
+ var debugFiles = fileNames.Where(x => x.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)).ToList();
+ var otherFiles = fileNames.Where(x => !SpecialExtensions.Contains(Path.GetExtension(x))).ToList();
+
+ existingFiles = new List();
+ missingFiles = new List();
+ additionalFiles = new List();
+
+ using var zip = ZipFile.OpenRead(_archivePath);
+
+ var arches = GetSupportedArches(targetArches);
+ var (explorers, errorMessage) = AssemblyStoreExplorer.Open(_archivePath, _logger);
+
+ foreach (var arch in arches)
+ {
+ var explorer = SelectExplorer(explorers, arch);
+ if (explorer == null)
+ {
+ continue;
+ }
+
+ if (otherFiles.Count > 0)
+ {
+ var (prefixAssemblies, prefixLib) = GetArchivePrefixes(MonoAndroidHelper.ArchToAbi(arch));
+
+ foreach (string file in otherFiles)
+ {
+ var fullPath = prefixAssemblies + file;
+ if (zip.ContainsEntry(fullPath))
+ {
+ existingFiles.Add(file);
+ }
+
+ fullPath = prefixLib + file;
+ if (zip.ContainsEntry(fullPath))
+ {
+ existingFiles.Add(file);
+ }
+ }
+ }
+
+ if (explorer.AssembliesByName is not null)
+ {
+ foreach (var f in explorer.AssembliesByName)
+ {
+ Console.WriteLine($"DEBUG!\tKey:{f.Key}");
+ }
+
+ if (explorer.AssembliesByName.Count != 0)
+ {
+ existingFiles.AddRange(explorer.AssembliesByName.Keys);
+
+ // We need to fake config and debug files since they have no named entries in the storeReader
+ foreach (var file in configFiles)
+ {
+ var asm = GetStoreAssembly(explorer, file);
+ if (asm == null)
+ {
+ continue;
+ }
+
+ if (asm.ConfigOffset > 0)
+ {
+ existingFiles.Add(file);
+ }
+ }
+
+ foreach (string file in debugFiles)
+ {
+ var asm = GetStoreAssembly(explorer, file);
+ if (asm == null)
+ {
+ continue;
+ }
+
+ if (asm.DebugOffset > 0)
+ {
+ existingFiles.Add(file);
+ }
+ }
+ }
+ }
+ }
+
+ foreach (string file in fileNames)
+ {
+ if (existingFiles.Contains(file))
+ {
+ continue;
+ }
+ missingFiles.Add(file);
+ }
+
+ additionalFiles = existingFiles.Where(x => !fileNames.Contains(x)).ToList();
+ ListFiles(existingFiles, missingFiles, additionalFiles);
+ return;
+
+ AssemblyStoreItem? GetStoreAssembly(AssemblyStoreExplorer explorer, string file)
+ {
+ var assemblyName = Path.GetFileNameWithoutExtension(file);
+ return explorer.AssembliesByName?.TryGetValue(assemblyName, out var asm) is true
+ ? asm
+ : null;
+ }
+ }
+}
diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs
new file mode 100644
index 0000000000..01cd3e37fb
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs
@@ -0,0 +1,229 @@
+/*
+ * 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 string StorePath { get; }
+ public AndroidTargetArch? TargetArch { get; }
+ public uint AssemblyCount { get; }
+ public uint IndexEntryCount { get; }
+ public IList? Assemblies { get; }
+ public IDictionary? AssembliesByName { get; }
+ public bool Is64Bit { get; }
+
+ protected AssemblyStoreExplorer(Stream storeStream, string path, DebugLogger? logger)
+ {
+ StorePath = path;
+ 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;
+ AssemblyCount = reader.AssemblyCount;
+ IndexEntryCount = reader.IndexEntryCount;
+ 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();
+ }
+
+ protected AssemblyStoreExplorer(FileInfo storeInfo, DebugLogger? logger)
+ : this(storeInfo.OpenRead(), storeInfo.FullName, logger)
+ { }
+
+ public static (IList? explorers, string? errorMessage) Open(string inputFile, DebugLogger? logger)
+ {
+ (FileFormat format, FileInfo? 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)
+ {
+ return OpenCommon(
+ fi,
+ new List> {
+ StoreReaderV2.AabPaths,
+ StoreReader_V1.AabPaths,
+ },
+ logger
+ );
+ }
+
+ private static (IList? explorers, string? errorMessage) OpenAabBase(FileInfo fi, DebugLogger? logger)
+ {
+ return OpenCommon(
+ fi,
+ new List> {
+ StoreReaderV2.AabBasePaths,
+ StoreReader_V1.AabBasePaths,
+ },
+ logger
+ );
+ }
+
+ private static (IList? explorers, string? errorMessage) OpenApk(FileInfo fi, DebugLogger? logger)
+ {
+ return OpenCommon(
+ fi,
+ new List> {
+ 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);
+ }
+
+ private string EnsureCorrectAssemblyName(string assemblyName)
+ {
+ assemblyName = Path.GetFileName(assemblyName);
+ if (reader.NeedsExtensionInName)
+ {
+ if (!assemblyName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+ {
+ return $"{assemblyName}.dll";
+ }
+ }
+ else
+ {
+ if (assemblyName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+ {
+ return Path.GetFileNameWithoutExtension(assemblyName);
+ }
+ }
+
+ return assemblyName;
+ }
+
+ public IList? Find(string assemblyName, AndroidTargetArch? targetArch = null)
+ {
+ if (Assemblies == null)
+ {
+ return null;
+ }
+
+ assemblyName = EnsureCorrectAssemblyName(assemblyName);
+ var items = new List();
+ foreach (AssemblyStoreItem item in Assemblies)
+ {
+ if (string.CompareOrdinal(assemblyName, item.Name) != 0)
+ {
+ continue;
+ }
+
+ if (targetArch != null && item.TargetArch != targetArch)
+ {
+ continue;
+ }
+
+ items.Add(item);
+ }
+
+ if (items.Count == 0)
+ {
+ return null;
+ }
+
+ return items;
+ }
+
+ public bool Contains(string assemblyName, AndroidTargetArch? targetArch = null)
+ {
+ IList? items = Find(assemblyName, targetArch);
+ if (items == null || items.Count == 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
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..856f7c35ea
--- /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)
+ {
+ AssemblyStoreReader? 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/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..aa665e5dd9
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/MonoAndroidHelper.Basic.cs
@@ -0,0 +1,245 @@
+/*
+ * 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 partial class MonoAndroidHelper
+{
+ public 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";
+ }
+
+ public 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 = new HashSet {
+ AndroidTargetArch.Arm,
+ AndroidTargetArch.Arm64,
+ AndroidTargetArch.X86,
+ AndroidTargetArch.X86_64,
+ };
+ private static readonly char[] ZipPathTrimmedChars = { '/', '\\' };
+ private static readonly Dictionary ClangAbiMap = new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ {"arm64-v8a", "aarch64"},
+ {"armeabi-v7a", "arm"},
+ {"x86", "i686"},
+ {"x86_64", "x86_64"}
+ };
+ private static readonly Dictionary AbiToArchMap = new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ { AndroidAbi.Arm32, AndroidTargetArch.Arm },
+ { AndroidAbi.Arm64, AndroidTargetArch.Arm64 },
+ { AndroidAbi.X86, AndroidTargetArch.X86 },
+ { AndroidAbi.X64, AndroidTargetArch.X86_64 },
+ };
+ private static readonly Dictionary AbiToRidMap = new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ { AndroidAbi.Arm32, RuntimeIdentifier.Arm32 },
+ { AndroidAbi.Arm64, RuntimeIdentifier.Arm64 },
+ { AndroidAbi.X86, RuntimeIdentifier.X86 },
+ { AndroidAbi.X64, RuntimeIdentifier.X64 },
+ };
+ private static readonly Dictionary RidToAbiMap = new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ { RuntimeIdentifier.Arm32, AndroidAbi.Arm32 },
+ { RuntimeIdentifier.Arm64, AndroidAbi.Arm64 },
+ { RuntimeIdentifier.X86, AndroidAbi.X86 },
+ { RuntimeIdentifier.X64, AndroidAbi.X64 },
+ };
+ private static readonly Dictionary RidToArchMap = new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ { RuntimeIdentifier.Arm32, AndroidTargetArch.Arm },
+ { RuntimeIdentifier.Arm64, AndroidTargetArch.Arm64 },
+ { RuntimeIdentifier.X86, AndroidTargetArch.X86 },
+ { RuntimeIdentifier.X64, AndroidTargetArch.X86_64 },
+ };
+ private static readonly Dictionary ArchToRidMap = new Dictionary {
+ { AndroidTargetArch.Arm, RuntimeIdentifier.Arm32 },
+ { AndroidTargetArch.Arm64, RuntimeIdentifier.Arm64 },
+ { AndroidTargetArch.X86, RuntimeIdentifier.X86 },
+ { AndroidTargetArch.X86_64, 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 AndroidTargetArch AbiToTargetArch(string abi)
+ {
+ if (!AbiToArchMap.TryGetValue(abi, out AndroidTargetArch arch))
+ {
+ throw new NotSupportedException($"Internal error: unsupported ABI '{abi}'");
+ }
+ ;
+
+ return arch;
+ }
+
+ public static string AbiToRid(string abi)
+ {
+ if (!AbiToRidMap.TryGetValue(abi, out var rid))
+ {
+ throw new NotSupportedException($"Internal error: unsupported ABI '{abi}'");
+ }
+ ;
+
+ return rid;
+ }
+
+ public static string RidToAbi(string rid)
+ {
+ if (!RidToAbiMap.TryGetValue(rid, out var abi))
+ {
+ throw new NotSupportedException($"Internal error: unsupported Runtime Identifier '{rid}'");
+ }
+ ;
+
+ return abi;
+ }
+
+ public static AndroidTargetArch RidToArchMaybe(string rid)
+ {
+ if (!RidToArchMap.TryGetValue(rid, out AndroidTargetArch arch))
+ {
+ return AndroidTargetArch.None;
+ }
+ ;
+
+ return arch;
+ }
+
+ public static AndroidTargetArch RidToArch(string rid)
+ {
+ AndroidTargetArch arch = RidToArchMaybe(rid);
+ if (arch == AndroidTargetArch.None)
+ {
+ throw new NotSupportedException($"Internal error: unsupported Runtime Identifier '{rid}'");
+ }
+ ;
+
+ return arch;
+ }
+
+ public static string ArchToRid(AndroidTargetArch arch)
+ {
+ if (!ArchToRidMap.TryGetValue(arch, out var rid))
+ {
+ throw new InvalidOperationException($"Internal error: unsupported architecture '{arch}'");
+ }
+ ;
+
+ return rid;
+ }
+
+ 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 bool IsValidRID(string rid) => RidToAbiMap.ContainsKey(rid);
+
+ public static string? CultureInvariantToString(object? obj)
+ {
+ if (obj == null)
+ {
+ return null;
+ }
+
+ return Convert.ToString(obj, CultureInfo.InvariantCulture);
+ }
+
+ public static string? MapAndroidAbiToClang(string androidAbi)
+ {
+ if (ClangAbiMap.TryGetValue(androidAbi, out var clangAbi))
+ {
+ return clangAbi;
+ }
+ return null;
+ }
+
+ public static string MakeZipArchivePath(string part1, params string[]? pathParts)
+ {
+ return MakeZipArchivePath(part1, (ICollection?)pathParts);
+ }
+
+ 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 = "_";
+
+ ///
+ /// Mangles APK/AAB entry name for assembly and their associated pdb and config entries in the
+ /// way expected by our native runtime. Must **NOT** be used to mangle names when assembly stores
+ /// are used. Must **NOT** be used for entries other than assemblies and their associated files.
+ ///
+ public static string MakeDiscreteAssembliesEntryName(string name, string? culture = null)
+ {
+ if (!string.IsNullOrEmpty(culture))
+ {
+ return $"{MANGLED_ASSEMBLY_SATELLITE_ASSEMBLY_MARKER}{culture}_{name}{MANGLED_ASSEMBLY_NAME_EXT}";
+ }
+
+ return $"{MANGLED_ASSEMBLY_REGULAR_ASSEMBLY_MARKER}{name}{MANGLED_ASSEMBLY_NAME_EXT}";
+ }
+
+ ///
+ /// Returns size of the extension + length of the prefix for mangled assembly names. This is
+ /// used to pre-allocate space for assembly names in `libxamarin-app.so`
+ ///
+ ///
+ public static ulong GetMangledAssemblyNameSizeOverhead()
+ {
+ // Satellite marker is one character more, for the `-` closing the culture part
+ return (ulong)MANGLED_ASSEMBLY_NAME_EXT.Length +
+ (ulong)Math.Max(MANGLED_ASSEMBLY_SATELLITE_ASSEMBLY_MARKER.Length + 1, MANGLED_ASSEMBLY_REGULAR_ASSEMBLY_MARKER.Length);
+ }
+
+ public static byte[] Utf8StringToBytes(string str) => Encoding.UTF8.GetBytes(str);
+ public static byte[] Utf16StringToBytes(string str) => Encoding.Unicode.GetBytes(str);
+}
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..b1ed2e392e
--- /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 (IndexEntry 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..a60ade7e82
--- /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.ELF_MAGIC)
+ {
+ 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.ASSEMBLY_STORE_MAGIC)
+ {
+ 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..a8978e78e4
--- /dev/null
+++ b/src/Sentry.Android.AssemblyReader/V2/Utils.cs
@@ -0,0 +1,180 @@
+/*
+ * 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 ZIP_MAGIC = 0x4034b50;
+ public const uint ASSEMBLY_STORE_MAGIC = 0x41424158;
+ public const uint ELF_MAGIC = 0x464c457f;
+
+ public static readonly ArrayPool BytePool = ArrayPool.Shared;
+
+ public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadSectionOffsetAndSize(Stream stream)
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ Class 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);
+ }
+
+ bool 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
+ FileFormat format = reader.ReadUInt32() switch
+ {
+ Utils.ZIP_MAGIC => FileFormat.Zip,
+ Utils.ELF_MAGIC => FileFormat.ELF,
+ Utils.ASSEMBLY_STORE_MAGIC => 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;
+ }
+
+ private static bool HasAllEntries(ZipArchive zip, string[] entries)
+ {
+ foreach (var entry in entries)
+ {
+ if (zip.GetEntry(entry) is null)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ 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;
+ }
+
+ internal static bool ContainsEntry(this ZipArchive zip, string entry) => zip.GetEntry(entry) is not null;
+}
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..7eae0392ce 100644
--- a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs
+++ b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs
@@ -1,9 +1,20 @@
+using Sentry.Android.AssemblyReader.V1;
+
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;
@@ -19,7 +30,7 @@ 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-Store={isAssemblyStore}-Compressed={isCompressed}.apk"));
_output.WriteLine($"Checking if APK exists: {apkPath}");
File.Exists(apkPath).Should().BeTrue();
@@ -39,13 +50,17 @@ public void CreatesCorrectReader(bool isAssemblyStore)
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)
+ if (isAssemblyStore && TargetFramework == "net9.0")
+ {
+ Assert.IsType(sut);
+ }
+ else if (isAssemblyStore && TargetFramework == "net8.0")
{
- Assert.IsType(sut);
+ Assert.IsType(sut);
}
else
{
- Assert.IsType(sut);
+ Assert.IsType(sut);
}
}
diff --git a/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.verify.cs b/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.verify.cs
index 32bcf57cb7..53706496bf 100644
--- a/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.verify.cs
+++ b/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.verify.cs
@@ -1,3 +1,5 @@
+using Sentry.Android.AssemblyReader.V1;
+
namespace Sentry.Android.AssemblyReader.Tests;
public class ApiApprovalTests
diff --git a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj
index d6dd9fc202..a160cd85cd 100644
--- a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj
+++ b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj
@@ -2,7 +2,8 @@
net9.0;net8.0
- $(TargetFrameworks);net8.0-android34.0
+
+ $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0
enable
@@ -13,10 +14,11 @@
+
+
-
@@ -26,8 +28,8 @@
<_ConfigString>Store=$(_Store)-Compressed=$(_Compressed)
-
-
+
+
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