Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### BREAKING CHANGES

- This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461))
- Added support for v3 of the Android AssemblyStore format that is used in .NET 10 and dropped support for v1 that was used in .NET 8 ([#4576](https://github.com/getsentry/sentry-dotnet/pull/4576))

## Unreleased

### Features
Expand Down
22 changes: 3 additions & 19 deletions src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Sentry.Android.AssemblyReader.V1;
using Sentry.Android.AssemblyReader.V2;

namespace Sentry.Android.AssemblyReader;
Expand All @@ -19,28 +18,13 @@ public static IAndroidAssemblyReader Open(string apkPath, IList<string> supporte
{
logger?.Invoke(DebugLoggerLevel.Debug, "Opening APK: {0}", apkPath);

#if NET9_0
logger?.Invoke(DebugLoggerLevel.Debug, "Reading files using V2 APK layout.");
if (AndroidAssemblyStoreReaderV2.TryReadStore(apkPath, supportedAbis, logger, out var readerV2))
if (AndroidAssemblyStoreReader.TryReadStore(apkPath, supportedAbis, logger, out var readerV2))
{
logger?.Invoke(DebugLoggerLevel.Debug, "APK uses AssemblyStore V2");
logger?.Invoke(DebugLoggerLevel.Debug, "APK uses AssemblyStore");
return readerV2;
}

logger?.Invoke(DebugLoggerLevel.Debug, "APK doesn't use AssemblyStore");
return new AndroidAssemblyDirectoryReaderV2(apkPath, supportedAbis, logger);
#else
logger?.Invoke(DebugLoggerLevel.Debug, "Reading files using V1 APK layout.");

var zipArchive = ZipFile.OpenRead(apkPath);
if (zipArchive.GetEntry("assemblies/assemblies.manifest") is not null)
{
logger?.Invoke(DebugLoggerLevel.Debug, "APK uses AssemblyStore V1");
return new AndroidAssemblyStoreReaderV1(zipArchive, supportedAbis, logger);
}

logger?.Invoke(DebugLoggerLevel.Debug, "APK doesn't use AssemblyStore");
return new AndroidAssemblyDirectoryReaderV1(zipArchive, supportedAbis, logger);
#endif
return new AndroidAssemblyDirectoryReader(apkPath, supportedAbis, logger);
}
}
301 changes: 301 additions & 0 deletions src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
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 AndroidAssemblyDirectoryReader : IAndroidAssemblyReader
{
private DebugLogger? Logger { get; }
private HashSet<AndroidTargetArch> SupportedArchitectures { get; } = new();
private readonly ArchiveAssemblyHelper _archiveAssemblyHelper;

public AndroidAssemblyDirectoryReader(string apkPath, IList<string> supportedAbis, DebugLogger? logger)
{
Logger = logger;
foreach (var abi in supportedAbis)
{
logger?.Invoke(DebugLoggerLevel.Debug, "Adding {0} to supported architectures for Directory Reader", abi);
SupportedArchitectures.Add(abi.AbiToDeviceArchitecture());
}
_archiveAssemblyHelper = new ArchiveAssemblyHelper(apkPath, logger, supportedAbis);
}

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);
}
Logger?.Invoke(DebugLoggerLevel.Debug, "File {0} does not exist in the APK", name);

foreach (var arch in SupportedArchitectures)
{
if (_archiveAssemblyHelper.ReadEntry($"assemblies/{name}", arch) is not { } memStream)
{
Logger?.Invoke(DebugLoggerLevel.Debug, "Couldn't find entry {0} in the APK for the {1} architecture", name, arch);
continue;
}

Logger?.Invoke(DebugLoggerLevel.Debug, "Resolved assembly {0} in the APK", name);
return ArchiveUtils.CreatePEReader(name, memStream, Logger);
}

Logger?.Invoke(DebugLoggerLevel.Debug, "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<byte> Buffers = ArrayPool<byte>.Shared;

private readonly string _archivePath;
private readonly DebugLogger? _logger;
private readonly IList<string> _supportedAbis;

public ArchiveAssemblyHelper(string archivePath, DebugLogger? logger, IList<string> supportedAbis)
{
if (string.IsNullOrEmpty(archivePath))
{
throw new ArgumentException("must not be null or empty", nameof(archivePath));
}

_archivePath = archivePath;
_logger = logger;
_supportedAbis = supportedAbis;
}

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(DebugLoggerLevel.Debug, message);
}
else
{
_logger?.Invoke(DebugLoggerLevel.Debug, $"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)
{
_logger?.Invoke(DebugLoggerLevel.Debug, "No potential entries for path '{0}' with arch '{1}'", path, arch);
return null;
}

// First we check the base.apk
if (ReadEntryFromApk(_archivePath) is { } baseEntry)
{
_logger?.Invoke(DebugLoggerLevel.Debug, "Found entry '{0}' in base archive '{1}'", path, _archivePath);
return baseEntry;
}

// Otherwise check in the device specific APKs
foreach (var supportedAbi in _supportedAbis)
{
var splitFilePath = _archivePath.GetArchivePathForAbi(supportedAbi, _logger);
if (!File.Exists(splitFilePath))
{
_logger?.Invoke(DebugLoggerLevel.Debug, "No split config detected at: '{0}'", splitFilePath);
}
else if (ReadEntryFromApk(splitFilePath) is { } splitEntry)
{
return splitEntry;
}
}

// Finally admit defeat
return null;

MemoryStream? ReadEntryFromApk(string archivePath)
{
using var zip = ZipFile.OpenRead(archivePath);
foreach (var assemblyPath in potentialEntries)
{
if (zip.GetEntry(assemblyPath) is not { } entry)
{
_logger?.Invoke(DebugLoggerLevel.Debug, "No entry found for path '{0}' in archive '{1}'", assemblyPath, archivePath);
continue;
}

var ret = entry.Extract();
ret.Flush();
return ret;
}

return null;
}
}

/// <summary>
/// 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.
/// </summary>
private static List<string>? 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<string>();
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<string>();
var newParts = new List<string> {
string.Empty, // ABI placeholder
$"{fileTypeMarker}{fileName}.so",
};

foreach (var a in abis)
{
newParts[0] = a;
ret.Add(MonoAndroidHelper.MakeZipArchivePath("lib", newParts));
}

return ret;
}
}
}
Loading
Loading