4
4
using System . IO . Compression ;
5
5
using System . Linq ;
6
6
7
+ using Xamarin . Android . Tasks ;
8
+ using Xamarin . Android . Tools ;
9
+
7
10
namespace ApplicationUtility ;
8
11
9
12
public abstract class ApplicationPackage : IAspect
@@ -24,18 +27,28 @@ public abstract class ApplicationPackage : IAspect
24
27
"dex/classes.dex" ,
25
28
} ;
26
29
30
+ readonly static HashSet < string > KnownSignatureEntries = new ( StringComparer . Ordinal ) {
31
+ "META-INF/BNDLTOOL.RSA" ,
32
+ "META-INF/ANDROIDD.RSA" ,
33
+ } ;
34
+
27
35
public static string AspectName { get ; } = "Application package" ;
28
36
29
37
public abstract string PackageFormat { get ; }
38
+ protected abstract string NativeLibDirBase { get ; }
39
+ protected abstract string AndroidManifestPath { get ; }
30
40
31
41
protected ZipArchive Zip { get ; }
32
42
public string ? Description { get ; }
33
43
34
44
public bool Signed { get ; protected set ; }
45
+ public bool ValidAndroidPackage { get ; protected set ; }
46
+ public bool Debuggable { get ; protected set ; }
35
47
public ApplicationRuntime Runtime { get ; protected set ; } = ApplicationRuntime . Unknown ;
36
48
public string PackageName { get ; protected set ; } = "" ;
49
+ public string MainActivity { get ; protected set ; } = "" ;
37
50
public List < AssemblyStore > ? AssemblyStores { get ; protected set ; }
38
- public NativeArchitecture Architectures { get ; protected set ; }
51
+ public List < AndroidTargetArch > Architectures { get ; protected set ; } = new ( ) ;
39
52
40
53
protected ApplicationPackage ( ZipArchive zip , string ? description )
41
54
{
@@ -61,11 +74,147 @@ public static IAspect LoadAspect (Stream stream, string? description)
61
74
} else {
62
75
throw new InvalidOperationException ( "Stream is not a supported Android ZIP package. Call ProbeAspect first." ) ;
63
76
}
64
-
65
77
Log . Debug ( $ "ApplicationPackage: stream ('{ description } ') is: { ret . PackageFormat } ") ;
78
+
79
+ // TODO: for all of the below, add support for detection of older XA apps (just to warn that this version doesn't support
80
+ // and that people should use older tools)
81
+ ret . TryDetectArchitectures ( ) ; // This must be called first, some further steps depend on it
82
+ ret . TryDetectRuntime ( ) ;
83
+ ret . TryDetectWhetherIsSigned ( ) ;
84
+ ret . TryLoadAssemblyStores ( ) ;
85
+ ret . TryLoadAndroidManifest ( ) ;
86
+
66
87
return ret ;
67
88
}
68
89
90
+ void TryDetectArchitectures ( )
91
+ {
92
+ foreach ( AndroidTargetArch arch in Enum . GetValues < AndroidTargetArch > ( ) ) {
93
+ if ( ! MonoAndroidHelper . SupportedTargetArchitectures . Contains ( arch ) ) {
94
+ continue ;
95
+ }
96
+
97
+ // We can't simply test for presence of the libDir below, because it's possible
98
+ // that a separate entry for the "directory" (they are only a naming convention
99
+ // in the ZIP archive, not a separate entity) won't exist. Instead, we look for
100
+ // any entry starting with the path.
101
+ if ( ! HasEntryStartingWith ( Zip , GetNativeLibDir ( arch ) ) ) {
102
+ continue ;
103
+ }
104
+ Architectures . Add ( arch ) ;
105
+ Log . Debug ( $ "Detected architecture: { arch } ") ;
106
+ }
107
+ }
108
+
109
+ void TryDetectRuntime ( )
110
+ {
111
+ ApplicationRuntime runtime = ApplicationRuntime . Unknown ;
112
+ string runtimePath ;
113
+ foreach ( AndroidTargetArch arch in Architectures ) {
114
+ runtimePath = GetNativeLibFile ( arch , "libcoreclr.so" ) ;
115
+ if ( HasEntry ( Zip , runtimePath ) ) {
116
+ runtime = ApplicationRuntime . CoreCLR ;
117
+ break ;
118
+ }
119
+
120
+ runtimePath = GetNativeLibFile ( arch , "libmonosgen-2.0.so" ) ;
121
+ if ( HasEntry ( Zip , runtimePath ) ) {
122
+ runtime = ApplicationRuntime . MonoVM ;
123
+ break ;
124
+ }
125
+ }
126
+
127
+ if ( runtime != ApplicationRuntime . Unknown || Architectures . Count == 0 ) {
128
+ Log . Debug ( $ "Detected runtime: { runtime } ") ;
129
+ return ;
130
+ }
131
+
132
+ runtimePath = GetNativeLibFile ( Architectures [ 0 ] , "libmonodroid.so" ) ;
133
+ if ( ! HasEntry ( Zip , runtimePath ) ) {
134
+ return ;
135
+ }
136
+
137
+ // TODO: it might be statically linked CoreCLR runtime. Need to check for presence of
138
+ // some public symbols to verify that.
139
+ }
140
+
141
+ void TryDetectWhetherIsSigned ( )
142
+ {
143
+ Signed = HasAnyEntries ( Zip , KnownSignatureEntries ) ;
144
+ Log . Debug ( $ "Signature detected: { Signed } ") ;
145
+ }
146
+
147
+ void TryLoadAssemblyStores ( )
148
+ {
149
+ foreach ( AndroidTargetArch arch in Architectures ) {
150
+ string storePath = GetNativeLibFile ( arch , $ "libassemblies.{ MonoAndroidHelper . ArchToAbi ( arch ) } .blob.so") ;
151
+ Log . Debug ( $ "Trying assembly store: { storePath } ") ;
152
+ if ( ! HasEntry ( Zip , storePath ) ) {
153
+ Log . Debug ( $ "Assembly store '{ storePath } ' not found") ;
154
+ continue ;
155
+ }
156
+
157
+ Log . Debug ( $ "Found assembly store entry for architecture { arch } ") ;
158
+ AssemblyStore ? store = TryLoadAssemblyStore ( storePath ) ;
159
+ if ( store == null ) {
160
+ continue ;
161
+ }
162
+ }
163
+ }
164
+
165
+ AssemblyStore ? TryLoadAssemblyStore ( string storePath )
166
+ {
167
+ // AssemblyStore class owns the stream, don't dispose it here
168
+ Stream ? storeStream = TryGetEntryStream ( storePath ) ;
169
+ if ( storeStream == null ) {
170
+ return null ;
171
+ }
172
+
173
+ try {
174
+ if ( ! AssemblyStore . ProbeAspect ( storeStream , storePath ) ) {
175
+ Log . Debug ( $ "Assembly store '{ storePath } ' is not in a supported format") ;
176
+ return null ;
177
+ }
178
+
179
+ return ( AssemblyStore ) AssemblyStore . LoadAspect ( storeStream , storePath ) ;
180
+ } catch ( Exception ex ) {
181
+ Log . Debug ( $ "Failed to load assembly store '{ storePath } '", ex ) ;
182
+ return null ;
183
+ }
184
+ }
185
+
186
+ void TryLoadAndroidManifest ( )
187
+ {
188
+ ValidAndroidPackage = HasEntry ( Zip , AndroidManifestPath ) ;
189
+ if ( ! ValidAndroidPackage ) {
190
+ Log . Debug ( $ "Package is missing manifest entry '{ AndroidManifestPath } '") ;
191
+ return ;
192
+ }
193
+
194
+ Log . Debug ( $ "Found Android manifest '{ AndroidManifestPath } '") ;
195
+ using Stream ? manifestStream = TryGetEntryStream ( AndroidManifestPath ) ;
196
+ // TODO: parse
197
+ }
198
+
199
+ string GetNativeLibDir ( AndroidTargetArch arch ) => $ "{ NativeLibDirBase } /{ MonoAndroidHelper . ArchToAbi ( arch ) } /";
200
+ string GetNativeLibFile ( AndroidTargetArch arch , string fileName ) => $ "{ GetNativeLibDir ( arch ) } { fileName } ";
201
+
202
+ Stream ? TryGetEntryStream ( string path )
203
+ {
204
+ try {
205
+ ZipArchiveEntry ? entry = Zip . GetEntry ( path ) ;
206
+ if ( entry == null ) {
207
+ Log . Debug ( $ "ZIP entry '{ path } ' could not be loaded.") ;
208
+ return null ;
209
+ }
210
+
211
+ return entry . Open ( ) ;
212
+ } catch ( Exception ex ) {
213
+ Log . Debug ( $ "Failed to load entry '{ path } ' from the archive.", ex ) ;
214
+ return null ;
215
+ }
216
+ }
217
+
69
218
public static bool ProbeAspect ( Stream stream , string ? description )
70
219
{
71
220
Log . Debug ( $ "ApplicationPackage: checking if stream ('{ description } ') is a ZIP archive") ;
@@ -91,15 +240,30 @@ public static bool ProbeAspect (Stream stream, string? description)
91
240
return true ;
92
241
}
93
242
94
- static bool IsAPK ( ZipArchive zip ) => HasEntries ( zip , KnownApkEntries ) ;
95
- static bool IsAAB ( ZipArchive zip ) => HasEntries ( zip , KnownAabEntries ) ;
96
- static bool IsBase ( ZipArchive zip ) => HasEntries ( zip , KnownBaseEntries ) ;
243
+ static bool IsAPK ( ZipArchive zip ) => HasAllEntries ( zip , KnownApkEntries ) ;
244
+ static bool IsAAB ( ZipArchive zip ) => HasAllEntries ( zip , KnownAabEntries ) ;
245
+ static bool IsBase ( ZipArchive zip ) => HasAllEntries ( zip , KnownBaseEntries ) ;
97
246
98
- static bool HasEntries ( ZipArchive zip , HashSet < string > knownEntries )
247
+ static bool HasAnyEntries ( ZipArchive zip , HashSet < string > knownEntries )
99
248
{
100
249
return zip . Entries . Where ( ( ZipArchiveEntry entry ) => knownEntries . Contains ( entry . FullName ) ) . Any ( ) ;
101
250
}
102
251
252
+ static bool HasAllEntries ( ZipArchive zip , HashSet < string > knownEntries )
253
+ {
254
+ return zip . Entries . Where ( ( ZipArchiveEntry entry ) => knownEntries . Contains ( entry . FullName ) ) . Count ( ) == knownEntries . Count ;
255
+ }
256
+
257
+ static bool HasEntry ( ZipArchive zip , string path )
258
+ {
259
+ return zip . Entries . Where ( ( ZipArchiveEntry entry ) => entry . FullName == path ) . Any ( ) ;
260
+ }
261
+
262
+ static bool HasEntryStartingWith ( ZipArchive zip , string path )
263
+ {
264
+ return zip . Entries . Where ( ( ZipArchiveEntry entry ) => entry . FullName . StartsWith ( path , StringComparison . Ordinal ) ) . Any ( ) ;
265
+ }
266
+
103
267
static ZipArchive ? TryOpenAsZip ( Stream stream )
104
268
{
105
269
stream . Seek ( 0 , SeekOrigin . Begin ) ;
0 commit comments