@@ -5,21 +5,134 @@ namespace ApplicationUtility;
55
66public class ApplicationAssembly : IAspect
77{
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+
813 public static string AspectName { get ; } = "Application assembly" ;
914
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+ }
1551
1652 public static IAspect LoadAspect ( Stream stream , IAspectState state , string ? description )
1753 {
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 ) ;
1960 }
2061
2162 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 )
22120 {
23121 throw new NotImplementedException ( ) ;
24122 }
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+ }
25138}
0 commit comments