@@ -199,6 +199,180 @@ public void CalcAssemblyCacheChecksums(List<string> checksums, string path, stri
199199 throw new UnreachableException ( ) ;
200200 }
201201
202+ private string CalcChecksumForFile ( string path ) {
203+ if ( ! string . IsNullOrEmpty ( ModuleMeta . PathArchive ) ) {
204+ return ModuleMeta . Hash . ToHexadecimalString ( ) ;
205+ } else if ( ! string . IsNullOrEmpty ( ModuleMeta . PathDirectory ) ) {
206+ return Everest . GetChecksum ( path ) . ToHexadecimalString ( ) ;
207+ } else throw new UnreachableException ( ) ;
208+ }
209+
210+ /// <summary>
211+ /// Calculates the checksums used to cache the assembly in any relevant
212+ /// caches (like the relinker cache).
213+ /// </summary>
214+ /// <param name="path">The path of the assembly inside of the mod.</param>
215+ /// <param name="symPath">The path of the assembly's symbols inside of mod, or null.</param>
216+ /// <returns>The calculated checksums for the file.</returns>
217+ public AsmChecksums CalcAssemblyCacheChecksums ( string path , string symPath ) {
218+ Dictionary < string , string > depChecksums = new ( ) ;
219+ FillChecksums ( depChecksums , ModuleMeta . Dependencies ) ;
220+
221+ Dictionary < string , string > optDepChecksums = new ( ) ;
222+ FillChecksums ( optDepChecksums , ModuleMeta . OptionalDependencies ) ;
223+
224+ return new AsmChecksums (
225+ Everest . Relinker . GameChecksum ,
226+ CalcChecksumForFile ( path ) ,
227+ symPath != null ? CalcChecksumForFile ( symPath ) : "" ,
228+ depChecksums ,
229+ optDepChecksums ) ;
230+
231+ static void FillChecksums ( Dictionary < string , string > dict , List < EverestModuleMetadata > deps ) {
232+ foreach ( EverestModuleMetadata depMeta in deps ) {
233+ if ( depMeta . Name == CoreModule . NETCoreMetaName || depMeta . Name == "Everest" ) continue ;
234+ EverestModule actualModule = Everest . Modules . FirstOrDefault ( m => m . Metadata . Name == depMeta . Name , null ) ;
235+ if ( actualModule == null ) continue ;
236+ dict [ depMeta . Name ] = actualModule . Metadata . Hash . ToHexadecimalString ( ) ;
237+ }
238+ }
239+ }
240+
241+ /// <summary>
242+ /// Represents all the checksum data for an assembly, either from the cache or
243+ /// from one that's about to be loaded.
244+ /// </summary>
245+ public sealed record AsmChecksums (
246+ string GameChecksum ,
247+ string FileChecksum ,
248+ string SymFileChecksum = "" ,
249+ Dictionary < string , string > DepHashes = null ,
250+ Dictionary < string , string > OptDepHashes = null ) {
251+
252+ public static readonly AsmChecksums Empty = new ( "" , "" ) ;
253+
254+ /// <summary>
255+ /// Writes the data to a file, see <see cref="ReadFromFile"/> for a description
256+ /// of the file format.
257+ /// </summary>
258+ /// <param name="path">The path to the file to write to.</param>
259+ public void WriteToFile ( string path ) {
260+ List < string > lines = new ( ) ;
261+ lines . Add ( GameChecksum ) ;
262+ lines . Add ( FileChecksum ) ;
263+ lines . Add ( SymFileChecksum ?? "" ) ;
264+
265+ WriteAsLines ( DepHashes ) ;
266+
267+ lines . Add ( "" ) ;
268+
269+ WriteAsLines ( OptDepHashes ) ;
270+
271+ File . WriteAllLines ( path , lines ) ;
272+ return ;
273+
274+ void WriteAsLines ( Dictionary < string , string > dict ) {
275+ foreach ( ( string metaName , string hash ) in dict ) {
276+ lines . Add ( $ "{ metaName } :{ hash } ") ;
277+ }
278+ }
279+ }
280+
281+ /// <summary>
282+ /// Creates an instance from a file, format is as following:
283+ /// We take all the data and partition it on each new line character into strings, keeping empty strings.
284+ /// Then, the first element will be the game checksum, the second one, the file checksum,
285+ /// the third one, the symbol file (pdb) checksum, or empty an empty string if it doesn't exist or we are working with
286+ /// archives (zips).
287+ /// After the third element, each element is of the format `Name:Hash`, where Name is the meta name and
288+ /// Hash is the hash for that meta, this represents a dependency. Until an empty element, which marks the
289+ /// end of the dependencies list. After this element and until the end of file, the optional dependencies list follow
290+ /// in the same format.
291+ /// </summary>
292+ /// <param name="path">Path to read from.</param>
293+ /// <returns>A populated instance.</returns>
294+ public static AsmChecksums ReadFromFile ( string path ) {
295+ string [ ] data = File . ReadAllLines ( path ) ;
296+ if ( data . Length < 2 ) return Empty ;
297+
298+ string gameChecksum = data [ 0 ] ;
299+ string fileChecksum = data [ 1 ] ;
300+ if ( data . Length <= 2 ) return new AsmChecksums ( gameChecksum , fileChecksum ) ;
301+ string symChecksum = data [ 2 ] ;
302+ if ( data . Length <= 3 ) return new AsmChecksums ( gameChecksum , fileChecksum , symChecksum ) ;
303+ Dictionary < string , string > depChecksums = new ( ) ;
304+ Dictionary < string , string > optDepChecksums = new ( ) ;
305+
306+ Dictionary < string , string > currentDict = depChecksums ;
307+ int index = 3 ;
308+ while ( index < data . Length ) {
309+ if ( data [ index ] == "" ) {
310+ if ( currentDict == depChecksums ) {
311+ currentDict = optDepChecksums ;
312+ index ++ ;
313+ continue ;
314+ } else {
315+ // Malformed cache, return minimum data
316+ return new AsmChecksums ( gameChecksum , fileChecksum , symChecksum ) ;
317+ }
318+ }
319+ ReadOnlySpan < char > curr = data [ index ] ;
320+ int sepIdx = curr . LastIndexOf ( ':' ) ;
321+ if ( sepIdx == - 1 ) {
322+ // Malformed cache, return minimum data
323+ return new AsmChecksums ( gameChecksum , fileChecksum , symChecksum ) ;
324+ }
325+
326+ ReadOnlySpan < char > metaName = curr [ ..sepIdx ] ;
327+ ReadOnlySpan < char > metaHash = curr [ ( sepIdx + 1 ) ..] ;
328+ currentDict [ metaName . ToString ( ) ] = metaHash . ToString ( ) ;
329+
330+ index ++ ;
331+ }
332+ return new AsmChecksums ( gameChecksum , fileChecksum , symChecksum , depChecksums , optDepChecksums ) ;
333+ }
334+
335+ /// <summary>
336+ /// Verifies whether the current instance is a valid in respect to the one passed in:
337+ /// Game, file, and symbol checksums must always match for it to be valid, but
338+ /// the requirements for dependencies and optional dependencies are different.
339+ /// We consider this instance valid with respect to the one passed in, if for every dependency D present
340+ /// in this instance, D is also present in <paramref name="other"/>, and it is associated with the same checksum.
341+ /// The same reasoning goes with optional dependencies.
342+ /// </summary>
343+ /// <param name="other">The other cache to check with.</param>
344+ /// <returns>Whether this is compatible with <paramref name="other"/></returns>
345+ public bool IsValidWith ( AsmChecksums other ) {
346+ if ( other == null ) return false ;
347+ if ( GameChecksum != other . GameChecksum ) return false ;
348+ if ( FileChecksum != other . FileChecksum ) return false ;
349+ if ( SymFileChecksum != other . SymFileChecksum ) return false ;
350+
351+ if ( ! Check ( DepHashes , other . DepHashes ) ) return false ;
352+
353+ if ( ! Check ( OptDepHashes , other . OptDepHashes ) ) return false ;
354+
355+ return true ;
356+
357+ static bool Check ( Dictionary < string , string > dict , Dictionary < string , string > otherDict ) {
358+ // A null list indicates no dependencies, thus, it always satisfies the requirements
359+ if ( dict != null ) {
360+ if ( otherDict != null ) {
361+ foreach ( ( string metaName , string hash ) in dict ) {
362+ if ( ! otherDict . TryGetValue ( metaName , out string otherHash ) ) {
363+ return false ;
364+ }
365+ if ( hash != otherHash ) return false ;
366+ }
367+ } else if ( dict . Count != 0 )
368+ // Having an empty and null list should be equivalent
369+ return false ;
370+ }
371+ return true ;
372+ }
373+ }
374+ }
375+
202376 /// <summary>
203377 /// Tries to load an assembly from a given path inside the mod.
204378 /// This path is an absolute path if the the mod was loaded from a directory, or a path into the mod ZIP otherwise.
0 commit comments