/* Copyright 2017 Perfare - https://github.com/Perfare/Il2CppDumper Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty All rights reserved. */ using System.Collections.Immutable; using System.Runtime.CompilerServices; using Il2CppInspector.Next; using Il2CppInspector.Next.Metadata; using VersionedSerialization; namespace Il2CppInspector { public class Metadata : BinaryObjectStreamReader { public Il2CppGlobalMetadataHeader Header { get; set; } public ImmutableArray Assemblies { get; set; } public ImmutableArray Images { get; set; } public ImmutableArray Types { get; set; } public ImmutableArray Methods { get; set; } public ImmutableArray Params { get; set; } public ImmutableArray Fields { get; set; } public ImmutableArray FieldDefaultValues { get; set; } public ImmutableArray ParameterDefaultValues { get; set; } public ImmutableArray Properties { get; set; } public ImmutableArray Events { get; set; } public ImmutableArray GenericContainers { get; set; } public ImmutableArray GenericParameters { get; set; } public ImmutableArray AttributeTypeRanges { get; set; } public ImmutableArray AttributeDataRanges { get; set; } public ImmutableArray InterfaceOffsets { get; set; } public ImmutableArray MetadataUsageLists { get; set; } public ImmutableArray MetadataUsagePairs { get; set; } public ImmutableArray FieldRefs { get; set; } public ImmutableArray InterfaceUsageIndices { get; set; } public ImmutableArray NestedTypeIndices { get; set; } public ImmutableArray AttributeTypeIndices { get; set; } public ImmutableArray GenericConstraintIndices { get; set; } public ImmutableArray VTableMethodIndices { get; set; } public string[] StringLiterals { get; set; } public int FieldAndParameterDefaultValueDataOffset => Version >= MetadataVersions.V380 ? Header.FieldAndParameterDefaultValueData.Offset : Header.FieldAndParameterDefaultValueDataOffset; public int AttributeDataOffset => Version >= MetadataVersions.V380 ? Header.AttributeData.Offset : Header.AttributeDataOffset; public Dictionary Strings { get; private set; } = []; // Set if something in the metadata has been modified / decrypted public bool IsModified { get; private set; } = false; // Status update callback private EventHandler OnStatusUpdate { get; set; } private void StatusUpdate(string status) => OnStatusUpdate?.Invoke(this, status); // Initialize metadata object from a stream public static Metadata FromStream(MemoryStream stream, EventHandler statusCallback = null) { // TODO: This should really be placed before the Metadata object is created, // but for now this ensures it is called regardless of which client is in use PluginHooks.LoadPipelineStarting(); var metadata = new Metadata(statusCallback); stream.Position = 0; stream.CopyTo(metadata); metadata.Position = 0; metadata.Initialize(); return metadata; } private Metadata(EventHandler statusCallback = null) : base() => OnStatusUpdate = statusCallback; private void Initialize() { // Pre-processing hook var pluginResult = PluginHooks.PreProcessMetadata(this); IsModified = pluginResult.IsStreamModified; StatusUpdate("Processing metadata"); // Read metadata header Header = ReadVersionedObject(0); // Check for correct magic bytes if (!Header.SanityValid) { throw new InvalidOperationException("The supplied metadata file is not valid."); } // Set object versioning for Bin2Object from metadata version Version = new StructVersion(Header.Version); if (Version < MetadataVersions.V160 || Version > MetadataVersions.V380) { throw new InvalidOperationException($"The supplied metadata file is not of a supported version ({Header.Version})."); } // Rewind and read metadata header with the correct version settings Header = ReadVersionedObject(0); // Setup the proper index sizes for metadata v38+ if (Version >= MetadataVersions.V380) { static int GetIndexSize(int elementCount) { return elementCount switch { <= byte.MaxValue => sizeof(byte), <= ushort.MaxValue => sizeof(ushort), _ => sizeof(int) }; } var typeDefinitionIndexSize = GetIndexSize(Header.TypeDefinitions.Count); var genericContainerIndexSize = GetIndexSize(Header.GenericContainers.Count); var tag = $"{TypeDefinitionIndex.TagPrefix}{typeDefinitionIndexSize}" + $"_{GenericContainerIndex.TagPrefix}{genericContainerIndexSize}"; var tempVersion = new StructVersion(Version.Major, Version.Minor, tag); // now we need to derive the size for TypeIndex. // this is normally done through s_Il2CppMetadataRegistration->typesCount, but we don't want to use the binary for this // as we do not have it available at this point. // thankfully, we can just guess the size based off the three available options and the known total size of // a type entry that uses TypeIndex. var expectedEventDefinitionSize = Header.Events.SectionSize / Header.Events.Count; var maxEventDefinitionSize = Il2CppEventDefinition.Size(tempVersion); int typeIndexSize; if (expectedEventDefinitionSize == maxEventDefinitionSize) typeIndexSize = sizeof(int); else if (expectedEventDefinitionSize == maxEventDefinitionSize - 2) typeIndexSize = sizeof(ushort); else if (expectedEventDefinitionSize == maxEventDefinitionSize - 3) typeIndexSize = sizeof(byte); else throw new InvalidOperationException("Could not determine TypeIndex size based on the metadata header"); var fullTag = $"{tag}_{TypeIndex.TagPrefix}{typeIndexSize}"; Version = new StructVersion(Version.Major, Version.Minor, fullTag); } // Sanity checking // Unity.IL2CPP.MetadataCacheWriter.WriteLibIl2CppMetadata always writes the metadata information in the same order it appears in the header, // with each block always coming directly after the previous block, 4-byte aligned. We can use this to check the integrity of the data and // detect sub-versions. // For metadata v24.0, the header can either be either 0x110 (24.0, 24.1) or 0x108 (24.2) bytes long. Since 'stringLiteralOffset' is the first thing // in the header after the sanity and version fields, and since it will always point directly to the first byte after the end of the header, // we can use this value to determine the actual header length and therefore narrow down the metadata version to 24.0/24.1 or 24.2. if (!pluginResult.SkipValidation) { var realHeaderLength = Header.StringLiteralOffset; if (realHeaderLength != Sizeof()) { if (Version == MetadataVersions.V240) { Version = MetadataVersions.V242; Header = ReadVersionedObject(0); } } if (realHeaderLength != Sizeof()) { throw new InvalidOperationException("Could not verify the integrity of the metadata file or accurately identify the metadata sub-version"); } } // Load all the relevant metadata using offsets provided in the header if (Version >= MetadataVersions.V160) Images = ReadMetadataArray(Header.ImagesOffset, Header.ImagesSize, Header.Images); // As an additional sanity check, all images in the metadata should have Mono.Cecil.MetadataToken == 1 // In metadata v24.1, two extra fields were added which will cause the below test to fail. // In that case, we can then adjust the version number and reload // Tokens were introduced in v19 - we don't bother testing earlier versions if (Version >= MetadataVersions.V190 && Images.Any(x => x.Token != 1)) if (Version == MetadataVersions.V240) { Version = MetadataVersions.V241; // No need to re-read the header, it's the same for both sub-versions Images = ReadMetadataArray(Header.ImagesOffset, Header.ImagesSize, Header.Images); if (Images.Any(x => x.Token != 1)) throw new InvalidOperationException("Could not verify the integrity of the metadata file image list"); } Types = ReadMetadataArray(Header.TypeDefinitionsOffset, Header.TypeDefinitionsSize, Header.TypeDefinitions); Methods = ReadMetadataArray(Header.MethodsOffset, Header.MethodsSize, Header.Methods); Params = ReadMetadataArray(Header.ParametersOffset, Header.ParametersSize, Header.Parameters); Fields = ReadMetadataArray(Header.FieldsOffset, Header.FieldsSize, Header.Fields); FieldDefaultValues = ReadMetadataArray(Header.FieldDefaultValuesOffset, Header.FieldDefaultValuesSize, Header.FieldDefaultValues); Properties = ReadMetadataArray(Header.PropertiesOffset, Header.PropertiesSize, Header.Properties); Events = ReadMetadataArray(Header.EventsOffset, Header.EventsSize, Header.Events); InterfaceUsageIndices = ReadMetadataPrimitiveArray(Header.InterfacesOffset, Header.InterfacesSize, Header.Interfaces); NestedTypeIndices = ReadMetadataPrimitiveArray(Header.NestedTypesOffset, Header.NestedTypesSize, Header.NestedTypes); GenericContainers = ReadMetadataArray(Header.GenericContainersOffset, Header.GenericContainersSize, Header.GenericContainers); GenericParameters = ReadMetadataArray(Header.GenericParametersOffset, Header.GenericParametersSize, Header.GenericParameters); GenericConstraintIndices = ReadMetadataPrimitiveArray(Header.GenericParameterConstraintsOffset, Header.GenericParameterConstraintsSize, Header.GenericParameterConstraints); InterfaceOffsets = ReadMetadataArray(Header.InterfaceOffsetsOffset, Header.InterfaceOffsetsSize, Header.InterfaceOffsets); VTableMethodIndices = ReadMetadataPrimitiveArray(Header.VTableMethodsOffset, Header.VTableMethodsSize, Header.VtableMethods); if (Version >= MetadataVersions.V160) { // In v24.4 hashValueIndex was removed from Il2CppAssemblyNameDefinition, which is a field in Il2CppAssemblyDefinition // The number of images and assemblies should be the same. If they are not, we deduce that we are using v24.4 // Note the version comparison matches both 24.2 and 24.3 here since 24.3 is tested for during binary loading var assemblyCount = Header.AssembliesSize / Sizeof(); var changedAssemblyDefStruct = false; if ((Version == MetadataVersions.V241 || Version == MetadataVersions.V242 || Version == MetadataVersions.V243) && assemblyCount < Images.Length) { if (Version == MetadataVersions.V241) changedAssemblyDefStruct = true; Version = MetadataVersions.V244; } Assemblies = ReadMetadataArray(Header.AssembliesOffset, Header.AssembliesSize, Header.Assemblies); if (changedAssemblyDefStruct) Version = MetadataVersions.V241; ParameterDefaultValues = ReadMetadataArray(Header.ParameterDefaultValuesOffset, Header.ParameterDefaultValuesSize, Header.ParameterDefaultValues); } if (Version >= MetadataVersions.V190 && Version < MetadataVersions.V270) { MetadataUsageLists = ReadMetadataArray(Header.MetadataUsageListsOffset, Header.MetadataUsageListsCount, default); MetadataUsagePairs = ReadMetadataArray(Header.MetadataUsagePairsOffset, Header.MetadataUsagePairsCount, default); } if (Version >= MetadataVersions.V190) { FieldRefs = ReadMetadataArray(Header.FieldRefsOffset, Header.FieldRefsSize, Header.FieldRefs); } if (Version >= MetadataVersions.V210 && Version < MetadataVersions.V290) { AttributeTypeIndices = ReadMetadataPrimitiveArray(Header.AttributesTypesOffset, Header.AttributesTypesCount, default); AttributeTypeRanges = ReadMetadataArray(Header.AttributesInfoOffset, Header.AttributesInfoCount, default); } if (Version >= MetadataVersions.V290) { AttributeDataRanges = ReadMetadataArray(Header.AttributeDataRangeOffset, Header.AttributeDataRangeSize, Header.AttributeDataRanges); } // Get all metadata strings var pluginGetStringsResult = PluginHooks.GetStrings(this); if (pluginGetStringsResult.IsDataModified && !pluginGetStringsResult.IsInvalid) Strings = pluginGetStringsResult.Strings; else { Position = Header.StringOffset; while (Position < Header.StringOffset + Header.StringSize) Strings.Add((int) Position - Header.StringOffset, ReadNullTerminatedString()); } // Get all string literals var pluginGetStringLiteralsResult = PluginHooks.GetStringLiterals(this); if (pluginGetStringLiteralsResult.IsDataModified) StringLiterals = pluginGetStringLiteralsResult.StringLiterals.ToArray(); else { var stringLiteralList = ReadMetadataArray(Header.StringLiteralOffset, Header.StringLiteralSize, Header.StringLiterals); var dataOffset = Version >= MetadataVersions.V380 ? Header.StringLiteralData.Offset : Header.StringLiteralDataOffset; if (Version >= MetadataVersions.V350) { StringLiterals = new string[stringLiteralList.Length - 1]; for (var i = 0; i < stringLiteralList.Length; i++) { var currentStringDataIndex = stringLiteralList[i].DataIndex; var nextStringDataIndex = stringLiteralList[i + 1].DataIndex; var stringLength = nextStringDataIndex - currentStringDataIndex; StringLiterals[i] = ReadFixedLengthString(dataOffset + currentStringDataIndex, stringLength); } } else { StringLiterals = new string[stringLiteralList.Length]; for (var i = 0; i < stringLiteralList.Length; i++) StringLiterals[i] = ReadFixedLengthString(dataOffset + stringLiteralList[i].DataIndex, (int)stringLiteralList[i].Length); } } // Post-processing hook IsModified |= PluginHooks.PostProcessMetadata(this).IsStreamModified; return; } public ImmutableArray ReadMetadataPrimitiveArray(int oldOffset, int oldSize, Il2CppSectionMetadata newMetadata) where T : unmanaged { return Version >= MetadataVersions.V380 ? ReadPrimitiveArray(newMetadata.Offset, newMetadata.Count) : ReadPrimitiveArray(oldOffset, oldSize / Unsafe.SizeOf()); } public ImmutableArray ReadMetadataArray(int oldOffset, int oldSize, Il2CppSectionMetadata newMetadata) where T : IReadable, new() { return Version >= MetadataVersions.V380 ? ReadVersionedObjectArray(newMetadata.Offset, newMetadata.Count) : ReadVersionedObjectArray(oldOffset, oldSize / Sizeof()); } // Save metadata to file, overwriting if necessary public void SaveToFile(string pathname) { Position = 0; using var outFile = new FileStream(pathname, FileMode.Create, FileAccess.Write); CopyTo(outFile); } public int Sizeof() where T : IReadable => T.Size(Version, Is32Bit); } }