@@ -5,21 +5,134 @@ namespace ApplicationUtility;
5
5
6
6
public class ApplicationAssembly : IAspect
7
7
{
8
+ const string LogTag = "ApplicationAssembly" ;
9
+ const uint COMPRESSED_MAGIC = 0x5A4C4158 ; // 'XALZ', little-endian
10
+ const ushort MSDOS_EXE_MAGIC = 0x5A4D ; // 'MZ'
11
+ const uint PE_EXE_MAGIC = 0x00004550 ; // 'PE\0\0'
12
+
8
13
public static string AspectName { get ; } = "Application assembly" ;
9
14
10
- public bool IsCompressed { get ; private set ; }
11
- public string Name { get ; private set ; } = "" ;
12
- public ulong CompressedSize { get ; private set ; }
13
- public ulong Size { get ; private set ; }
14
- public bool IgnoreOnLoad { get ; private set ; }
15
+ public bool IsCompressed { get ; }
16
+ public string Name { get ; }
17
+ public ulong CompressedSize { get ; }
18
+ public ulong Size { get ; }
19
+ public bool IgnoreOnLoad { get ; }
20
+ public ulong NameHash { get ; internal set ; }
21
+
22
+ readonly Stream ? assemblyStream ;
23
+
24
+ ApplicationAssembly ( Stream stream , uint uncompressedSize , string ? description , bool isCompressed )
25
+ {
26
+ assemblyStream = stream ;
27
+ Size = uncompressedSize ;
28
+ CompressedSize = isCompressed ? ( ulong ) stream . Length : 0 ;
29
+ IsCompressed = isCompressed ;
30
+ Name = NameMe ( description ) ;
31
+ }
32
+
33
+ ApplicationAssembly ( string ? description , bool isIgnored )
34
+ {
35
+ IgnoreOnLoad = isIgnored ;
36
+ Name = NameMe ( description ) ;
37
+ }
38
+
39
+ static string NameMe ( string ? description ) => String . IsNullOrEmpty ( description ) ? "Unnamed" : description ;
40
+
41
+ // This is a special case, as much as I hate to have one. Ignored assemblies exist only in the assembly store's
42
+ // index. They have an associated descriptor, but no data whatsoever. For that reason, we can't go the `ProbeAspect`
43
+ // + `LoadAspect` route, so `AssemblyStore` will call this method for them.
44
+ public static IAspect CreateIgnoredAssembly ( string ? description , ulong nameHash )
45
+ {
46
+ Log . Debug ( $ "{ LogTag } : stream ('{ description } ') is an ignored assembly.") ;
47
+ return new ApplicationAssembly ( description , isIgnored : true ) {
48
+ NameHash = nameHash ,
49
+ } ;
50
+ }
15
51
16
52
public static IAspect LoadAspect ( Stream stream , IAspectState state , string ? description )
17
53
{
18
- throw new NotImplementedException ( ) ;
54
+ using var reader = Utilities . GetReaderAndRewindStream ( stream ) ;
55
+ if ( ReadCompressedHeader ( reader , out uint uncompressedLength ) ) {
56
+ return new ApplicationAssembly ( stream , uncompressedLength , description , isCompressed : true ) ;
57
+ }
58
+
59
+ return new ApplicationAssembly ( stream , ( uint ) stream . Length , description , isCompressed : false ) ;
19
60
}
20
61
21
62
public static IAspectState ProbeAspect ( Stream stream , string ? description )
63
+ {
64
+ Log . Debug ( $ "{ LogTag } : probing stream ('{ description } ')") ;
65
+ if ( stream . Length == 0 ) {
66
+ // It can happen if the assembly store index or name table are corrupted and we cannot
67
+ // determine if an assembly is ignored or not. If it is ignored, it will have no data
68
+ // available and so the stream will have length of 0
69
+ return new BasicAspectState ( false ) ;
70
+ }
71
+
72
+ // If we detect compressed assembly signature, we won't proceed with checking whether
73
+ // the rest of data is actually a valid managed assembly. This is to avoid doing a
74
+ // costly operation of decompressing when e.g. loading data from an assemblystore, when
75
+ // we potentially create a lot of `ApplicationAssembly` instances. Presence of the compression
76
+ // header is enough for the probing stage.
77
+
78
+ using var reader = Utilities . GetReaderAndRewindStream ( stream ) ;
79
+ if ( ReadCompressedHeader ( reader , out _ ) ) {
80
+ Log . Debug ( $ "{ LogTag } : stream ('{ description } ') is a compressed assembly.") ;
81
+ return new BasicAspectState ( true ) ;
82
+ }
83
+
84
+ // We could use PEReader (https://learn.microsoft.com/en-us/dotnet/api/system.reflection.portableexecutable.pereader)
85
+ // but it would be too heavy for our purpose here.
86
+ reader . BaseStream . Seek ( 0 , SeekOrigin . Begin ) ;
87
+ ushort mzExeMagic = reader . ReadUInt16 ( ) ;
88
+ if ( mzExeMagic != MSDOS_EXE_MAGIC ) {
89
+ return Utilities . GetFailureAspectState ( $ "{ LogTag } : stream doesn't have MS-DOS executable signature.") ;
90
+ }
91
+
92
+ const long PE_HEADER_OFFSET = 0x3c ;
93
+ if ( reader . BaseStream . Length <= PE_HEADER_OFFSET ) {
94
+ return Utilities . GetFailureAspectState ( $ "{ LogTag } : stream contains a corrupted MS-DOS executable image (too short, offset { PE_HEADER_OFFSET } is bigger than stream size).") ;
95
+ }
96
+
97
+ // Offset at 0x3C is where we can read the 32-bit offset to the PE header
98
+ reader . BaseStream . Seek ( PE_HEADER_OFFSET , SeekOrigin . Begin ) ;
99
+ uint uintVal = reader . ReadUInt32 ( ) ;
100
+ if ( reader . BaseStream . Length <= ( long ) uintVal ) {
101
+ return Utilities . GetFailureAspectState ( $ "{ LogTag } : stream contains a corrupted PE executable image (too short, offset { uintVal } is bigger than stream size).") ;
102
+ }
103
+
104
+ reader . BaseStream . Seek ( ( long ) uintVal , SeekOrigin . Begin ) ;
105
+ uintVal = reader . ReadUInt32 ( ) ;
106
+ if ( uintVal != PE_EXE_MAGIC ) {
107
+ return Utilities . GetFailureAspectState ( $ "{ LogTag } : stream doesn't have PE executable signature.") ;
108
+ }
109
+ // This is good enough for us
110
+
111
+ Log . Debug ( $ "{ LogTag } : stream ('{ description } ') appears to be a PE image.") ;
112
+ return new BasicAspectState ( true ) ;
113
+ }
114
+
115
+ /// <summary>
116
+ /// Writes assembly data to the indicated file, uncompressing it if necessary. If the destination
117
+ /// file exists, it will be overwritten.
118
+ /// </summary>
119
+ public void SaveToFile ( string filePath )
22
120
{
23
121
throw new NotImplementedException ( ) ;
24
122
}
123
+
124
+ // We don't care about the descriptor index here, it's only needed during the run time
125
+ static bool ReadCompressedHeader ( BinaryReader reader , out uint uncompressedLength )
126
+ {
127
+ uncompressedLength = 0 ;
128
+
129
+ uint uintVal = reader . ReadUInt32 ( ) ;
130
+ if ( uintVal != COMPRESSED_MAGIC ) {
131
+ return false ;
132
+ }
133
+
134
+ uintVal = reader . ReadUInt32 ( ) ; // descriptor index
135
+ uncompressedLength = reader . ReadUInt32 ( ) ;
136
+ return true ;
137
+ }
25
138
}
0 commit comments