Skip to content

Commit d18d462

Browse files
Tests pass on net8.0 and net9.0
1 parent 339f4ef commit d18d462

11 files changed

+197
-38
lines changed

src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReader.cs renamed to src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Sentry.Android.AssemblyReader.V1;
1+
using Sentry.Android.AssemblyReader.V1;
2+
3+
namespace Sentry.Android.AssemblyReader;
24

35
// The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file.
46
internal sealed class AndroidAssemblyDirectoryReader : AndroidAssemblyReader, IAndroidAssemblyReader
@@ -31,7 +33,7 @@ public AndroidAssemblyDirectoryReader(ZipArchive zip, IList<string> supportedAbi
3133
zipStream.CopyTo(memStream);
3234
memStream.Position = 0;
3335
}
34-
return CreatePEReader(name, memStream);
36+
return CreatePEReader(name, memStream, Logger);
3537
}
3638

3739
private ZipArchiveEntry? FindAssembly(string name)

src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyReader.cs renamed to src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Sentry.Android.AssemblyReader.V1;
1+
namespace Sentry.Android.AssemblyReader;
22

33
internal abstract class AndroidAssemblyReader : IDisposable
44
{
@@ -18,9 +18,9 @@ public void Dispose()
1818
ZipArchive.Dispose();
1919
}
2020

21-
protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream)
21+
internal static PEReader CreatePEReader(string assemblyName, MemoryStream inputStream, DebugLogger? logger)
2222
{
23-
var decompressedStream = TryDecompressLZ4(assemblyName, inputStream);
23+
var decompressedStream = TryDecompressLZ4(assemblyName, inputStream, logger);
2424

2525
// Use the decompressed stream, or if null, i.e. it wasn't compressed, use the original.
2626
return new PEReader(decompressedStream ?? inputStream);
@@ -35,7 +35,7 @@ protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream)
3535
/// [rest: lz4 compressed payload]
3636
/// </summary>
3737
/// <seealso href="https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/decompress-assemblies/main.cs#L26" />
38-
private Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream)
38+
private static Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream, DebugLogger? logger)
3939
{
4040
const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian
4141
const int payloadOffset = 12;
@@ -51,7 +51,7 @@ protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream)
5151
Debug.Assert(inputStream.Position == payloadOffset);
5252
var inputLength = (int)(inputStream.Length - payloadOffset);
5353

54-
Logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength);
54+
logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength);
5555

5656
var outputStream = new MemoryStream(decompressedLength);
5757

src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Sentry.Android.AssemblyReader.V1;
2+
using Sentry.Android.AssemblyReader.V2;
23

34
namespace Sentry.Android.AssemblyReader;
45

@@ -17,18 +18,23 @@ public static class AndroidAssemblyReaderFactory
1718
public static IAndroidAssemblyReader Open(string apkPath, IList<string> supportedAbis, DebugLogger? logger = null)
1819
{
1920
logger?.Invoke("Opening APK: {0}", apkPath);
20-
var zipArchive = ZipFile.Open(apkPath, ZipArchiveMode.Read);
2121

22-
// TODO: Try to read using the v2 store format
22+
// Try to read using the v2 store format
23+
if (AndroidAssemblyStoreReaderV2.TryReadStore(apkPath, supportedAbis, logger, out var readerV2))
24+
{
25+
logger?.Invoke("APK uses AssemblyStore V2");
26+
return readerV2;
27+
}
2328

2429
// Try to read using the v1 store format
30+
var zipArchive = ZipFile.Open(apkPath, ZipArchiveMode.Read);
2531
if (zipArchive.GetEntry("assemblies/assemblies.manifest") is not null)
2632
{
27-
logger?.Invoke("APK uses AssemblyStore");
28-
return new AndroidAssemblyStoreReader(zipArchive, supportedAbis, logger);
33+
logger?.Invoke("APK uses AssemblyStore V1");
34+
return new AndroidAssemblyStoreReaderV1(zipArchive, supportedAbis, logger);
2935
}
3036

31-
// Try to read from file system
37+
// Finally, try to read from file system
3238
logger?.Invoke("APK doesn't use AssemblyStore");
3339
return new AndroidAssemblyDirectoryReader(zipArchive, supportedAbis, logger);
3440
}

src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
4+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
55
<Description>.NET assembly reader for Android</Description>
66
</PropertyGroup>
77

8-
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
9-
<PackageReference Include="System.Reflection.Metadata" Version="5.0.0" />
10-
</ItemGroup>
11-
128
<ItemGroup>
139
<PackageReference Include="ELFSharp" Version="2.17.3" />
1410
<PackageReference Include="K4os.Compression.LZ4" Version="1.3.5" />

src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReader.cs renamed to src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
namespace Sentry.Android.AssemblyReader.V1;
22

33
// See https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores
4-
internal sealed class AndroidAssemblyStoreReader : AndroidAssemblyReader, IAndroidAssemblyReader
4+
internal sealed class AndroidAssemblyStoreReaderV1 : AndroidAssemblyReader, IAndroidAssemblyReader
55
{
66
private readonly AssemblyStoreExplorer _explorer;
77

8-
public AndroidAssemblyStoreReader(ZipArchive zip, IList<string> supportedAbis, DebugLogger? logger)
8+
public AndroidAssemblyStoreReaderV1(ZipArchive zip, IList<string> supportedAbis, DebugLogger? logger)
99
: base(zip, supportedAbis, logger)
1010
{
1111
_explorer = new(zip, supportedAbis, logger);
@@ -29,7 +29,7 @@ public AndroidAssemblyStoreReader(ZipArchive zip, IList<string> supportedAbis, D
2929
return null;
3030
}
3131

32-
return CreatePEReader(name, stream);
32+
return CreatePEReader(name, stream, Logger);
3333
}
3434

3535
private AssemblyStoreAssembly? TryFindAssembly(string name)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
namespace Sentry.Android.AssemblyReader.V2;
2+
3+
internal class AndroidAssemblyStoreReaderV2 : IAndroidAssemblyReader
4+
{
5+
private readonly IList<AssemblyStoreExplorer> _explorers;
6+
private readonly DebugLogger? _logger;
7+
8+
private AndroidAssemblyStoreReaderV2(IList<AssemblyStoreExplorer> explorers, DebugLogger? logger)
9+
{
10+
_explorers = explorers;
11+
_logger = logger;
12+
}
13+
14+
public static bool TryReadStore(string inputFile, IList<string> supportedAbis, DebugLogger? logger, [NotNullWhen(true)] out AndroidAssemblyStoreReaderV2? reader)
15+
{
16+
var (explorers, errorMessage) = AssemblyStoreExplorer.Open(inputFile, logger);
17+
if (errorMessage != null)
18+
{
19+
logger?.Invoke(errorMessage);
20+
reader = null;
21+
return false;
22+
}
23+
24+
List<AssemblyStoreExplorer> supportedExplorers = [];
25+
if (explorers is not null)
26+
{
27+
foreach (var explorer in explorers)
28+
{
29+
if (explorer.TargetArch is null)
30+
{
31+
continue;
32+
}
33+
34+
foreach (var supportedAbi in supportedAbis)
35+
{
36+
if (supportedAbi.AbiToDeviceArchitecture() == explorer.TargetArch)
37+
{
38+
supportedExplorers.Add(explorer);
39+
}
40+
}
41+
}
42+
}
43+
44+
if (supportedExplorers.Count == 0)
45+
{
46+
logger?.Invoke("Could not find V2 AssemblyStoreExplorer for the supported ABIs: {0}", string.Join(", ", supportedAbis));
47+
reader = null;
48+
return false;
49+
}
50+
51+
reader = new AndroidAssemblyStoreReaderV2(supportedExplorers, logger);
52+
return true;
53+
}
54+
55+
public PEReader? TryReadAssembly(string name)
56+
{
57+
var explorerAssembly = TryFindAssembly(name);
58+
if (explorerAssembly is null)
59+
{
60+
_logger?.Invoke("Couldn't find assembly {0} in the APK AssemblyStore", name);
61+
return null;
62+
}
63+
64+
var (explorer, storeItem) = explorerAssembly;
65+
_logger?.Invoke("Resolved assembly {0} in the APK {1} AssemblyStore", name, storeItem.TargetArch);
66+
67+
var stream = explorer.ReadImageData(storeItem, false);
68+
if (stream is null)
69+
{
70+
_logger?.Invoke("Couldn't access assembly {0} image stream", name);
71+
return null;
72+
}
73+
74+
return AndroidAssemblyReader.CreatePEReader(name, stream, _logger);
75+
}
76+
77+
private ExplorerStoreItem? TryFindAssembly(string name)
78+
{
79+
if (FindBestAssembly(name, out var assembly))
80+
{
81+
return assembly;
82+
}
83+
84+
if (name.EndsWith(".dll", ignoreCase: true, CultureInfo.InvariantCulture) ||
85+
name.EndsWith(".exe", ignoreCase: true, CultureInfo.InvariantCulture))
86+
{
87+
if (FindBestAssembly(name.Substring(0, name.Length - 4), out assembly))
88+
{
89+
return assembly;
90+
}
91+
}
92+
93+
return null;
94+
}
95+
96+
private bool FindBestAssembly(string name, out ExplorerStoreItem? explorerAssembly)
97+
{
98+
foreach (var explorer in _explorers)
99+
{
100+
if (explorer.AssembliesByName?.TryGetValue(name, out var assembly) is true)
101+
{
102+
explorerAssembly = new(explorer,assembly);
103+
return true;
104+
}
105+
}
106+
explorerAssembly = null;
107+
return false;
108+
}
109+
110+
private record ExplorerStoreItem(AssemblyStoreExplorer Explorer, AssemblyStoreItem StoreItem);
111+
112+
public void Dispose()
113+
{
114+
// No-op
115+
}
116+
}
117+
118+
internal static class DeviceArchitectureExtensions
119+
{
120+
public static bool IsSupportedBy(this Span<string> abis, AndroidTargetArch targetArch)
121+
{
122+
foreach (var abi in abis)
123+
{
124+
if (abi.AbiToDeviceArchitecture() == targetArch)
125+
{
126+
return true;
127+
}
128+
}
129+
130+
return false;
131+
}
132+
133+
public static AndroidTargetArch AbiToDeviceArchitecture(this string abi) =>
134+
abi switch
135+
{
136+
"armeabi-v7a" => AndroidTargetArch.Arm,
137+
"arm64-v8a" => AndroidTargetArch.Arm64,
138+
"x86" => AndroidTargetArch.X86,
139+
"x86_64" => AndroidTargetArch.X86_64,
140+
"mips" => AndroidTargetArch.Mips,
141+
_ => AndroidTargetArch.Other,
142+
};
143+
}

src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,7 @@ protected AssemblyStoreExplorer (Stream storeStream, string path, DebugLogger? l
4444
dict.Add(item.Name, item);
4545
}
4646
}
47-
#if NETSTANDARD
48-
// ReadOnlyDictionary is not available in netstandard2.0
49-
AssembliesByName = dict;
50-
#else
5147
AssembliesByName = dict.AsReadOnly ();
52-
#endif
5348
}
5449

5550
protected AssemblyStoreExplorer (FileInfo storeInfo, DebugLogger? logger)
@@ -150,7 +145,7 @@ public static (IList<AssemblyStoreExplorer>? explorers, string? errorMessage) Op
150145
continue;
151146
}
152147

153-
ZipEntry entry = zip.ReadEntry (path);
148+
var entry = zip.ReadEntry (path);
154149
var stream = new MemoryStream ();
155150
entry.Extract (stream);
156151
ret.Add (new AssemblyStoreExplorer (stream, $"{fi.FullName}!{path}", logger));
@@ -163,12 +158,12 @@ public static (IList<AssemblyStoreExplorer>? explorers, string? errorMessage) Op
163158
return (ret, null, true);
164159
}
165160

166-
public Stream? ReadImageData (AssemblyStoreItem item, bool uncompressIfNeeded = false)
161+
public MemoryStream? ReadImageData (AssemblyStoreItem item, bool uncompressIfNeeded = false)
167162
{
168163
return reader.ReadEntryImageData (item, uncompressIfNeeded);
169164
}
170165

171-
string EnsureCorrectAssemblyName (string assemblyName)
166+
private string EnsureCorrectAssemblyName (string assemblyName)
172167
{
173168
assemblyName = Path.GetFileName (assemblyName);
174169
if (reader.NeedsExtensionInName) {

src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected AssemblyStoreReader (Stream store, string path, DebugLogger? logger)
6161
protected abstract void Prepare ();
6262
protected abstract ulong GetStoreStartDataOffset ();
6363

64-
public Stream ReadEntryImageData (AssemblyStoreItem entry, bool uncompressIfNeeded = false)
64+
public MemoryStream ReadEntryImageData (AssemblyStoreItem entry, bool uncompressIfNeeded = false)
6565
{
6666
ulong startOffset = GetStoreStartDataOffset ();
6767
StoreStream.Seek ((uint)startOffset + entry.DataOffset, SeekOrigin.Begin);

test/AndroidTestApp/AndroidTestApp.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFramework>net8.0-android34.0</TargetFramework>
3+
<TargetFrameworks>net8.0-android;net9.0-android</TargetFrameworks>
4+
<IsAotCompatible>false</IsAotCompatible>
45
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
56
<OutputType>Exe</OutputType>
67
<Nullable>enable</Nullable>

test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
using Sentry.Android.AssemblyReader.V1;
2-
31
namespace Sentry.Android.AssemblyReader.Tests;
42

53
public class AndroidAssemblyReaderTests
64
{
75
private readonly ITestOutputHelper _output;
86

7+
#if NET9_0
8+
private static string TargetFramework => "net9.0";
9+
#elif NET8_0
10+
private static string TargetFramework => "net8.0";
11+
#else
12+
// Adding a new TFM to the project? Include it above
13+
#error "Target Framework not yet supported for AndroidAssemblyReader"
14+
#endif
15+
916
public AndroidAssemblyReaderTests(ITestOutputHelper output)
1017
{
1118
_output = output;
@@ -21,7 +28,7 @@ private IAndroidAssemblyReader GetSut(bool isAssemblyStore, bool isCompressed)
2128
Path.GetFullPath(Path.Combine(
2229
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
2330
"..", "..", "..", "TestAPKs",
24-
$"android-Store={isAssemblyStore}-Compressed={isCompressed}.apk"));
31+
$"{TargetFramework}-android-Store={isAssemblyStore}-Compressed={isCompressed}.apk"));
2532

2633
_output.WriteLine($"Checking if APK exists: {apkPath}");
2734
File.Exists(apkPath).Should().BeTrue();
@@ -41,9 +48,13 @@ public void CreatesCorrectReader(bool isAssemblyStore)
4148
Skip.If(true, "It's unknown whether the current Android app APK is an assembly store or not.");
4249
#endif
4350
using var sut = GetSut(isAssemblyStore, isCompressed: true);
44-
if (isAssemblyStore)
51+
if (isAssemblyStore && TargetFramework == "net9.0")
4552
{
46-
Assert.IsType<AndroidAssemblyStoreReader>(sut);
53+
Assert.IsType<V2.AndroidAssemblyStoreReaderV2>(sut);
54+
}
55+
else if (isAssemblyStore && TargetFramework == "net8.0")
56+
{
57+
Assert.IsType<V1.AndroidAssemblyStoreReaderV1>(sut);
4758
}
4859
else
4960
{
@@ -75,6 +86,10 @@ public void ReadsAssembly(bool isAssemblyStore, bool isCompressed, string assemb
7586
// No need to run all combinations - we only test the current APK which is (likely) compressed assembly store.
7687
Skip.If(!isAssemblyStore);
7788
Skip.If(!isCompressed);
89+
#endif
90+
#if NET9_0_OR_GREATER
91+
// Building without an assembly store is not yet supported in net9.0 and above
92+
Skip.If(!isAssemblyStore);
7893
#endif
7994
using var sut = GetSut(isAssemblyStore, isCompressed);
8095

0 commit comments

Comments
 (0)