using System; using System.Diagnostics; using System.IO; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; /// /// Block of obtained from a encrypted block storage binary. /// public sealed class SCBlock { /// /// Used to encrypt the rest of the block. /// public readonly uint Key; /// /// What kind of block is it? /// public SCTypeCode Type { get; private set; } /// /// For : What kind of array is it? /// public readonly SCTypeCode SubType; /// /// Decrypted data for this block. /// public readonly byte[] Data; /// /// Changes the block's Boolean type. Will throw if the old / new is not boolean. /// /// New boolean type to set. /// Will throw if the requested block state changes are incorrect. public void ChangeBooleanType(SCTypeCode value) { if (Type is not (SCTypeCode.Bool1 or SCTypeCode.Bool2) || value is not (SCTypeCode.Bool1 or SCTypeCode.Bool2)) throw new InvalidOperationException($"Cannot change {Type} to {value}."); Type = value; } /// /// Replaces the current with a same-sized array . /// /// New array to load (copy from). /// Will throw if the requested block state changes are incorrect. public void ChangeData(ReadOnlySpan value) { if (value.Length != Data.Length) throw new InvalidOperationException($"Cannot change size of {Type} block from {Data.Length} to {value.Length}."); value.CopyTo(Data); } /// /// Creates a new block reference to indicate a boolean value via the (no data). /// /// Hash key /// Value the block has internal SCBlock(uint key, SCTypeCode type) : this(key, type, Array.Empty()) { } /// /// Creates a new block reference to indicate an object or single primitive value. /// /// Hash key /// Type of data that can be read /// Backing byte array to interpret as a typed value internal SCBlock(uint key, SCTypeCode type, byte[] arr) { Key = key; Type = type; Data = arr; } /// /// Creates a new block reference to indicate an array of primitive values. /// /// Hash key /// Backing byte array to read primitives from /// Primitive value type internal SCBlock(uint key, byte[] arr, SCTypeCode subType) { Key = key; Type = SCTypeCode.Array; Data = arr; SubType = subType; } /// Indicates if the block represents a single primitive value. public bool HasValue() => Type > SCTypeCode.Array; /// Returns a boxed reference to a single primitive value. /// Throws an exception if the block does not represent a single primitive value. public object GetValue() => Type.GetValue(Data); /// Sets a boxed primitive value to the block data. /// Throws an exception if the block does not represent a single primitive value, or if the primitive type does not match. /// Boxed primitive value to be set to the block public void SetValue(object value) => Type.SetValue(Data, value); /// /// Creates a deep copy of the block. /// public SCBlock Clone() { if (Data.Length == 0) return new SCBlock(Key, Type); var clone = Data.AsSpan().ToArray(); if (SubType == 0) return new SCBlock(Key, Type, clone); return new SCBlock(Key, clone, SubType); } /// /// Encrypts the according to the and . /// public void WriteBlock(BinaryWriter bw, bool writeKey = true) { if (writeKey) bw.Write(Key); var xk = new SCXorShift32(Key); bw.Write((byte)((byte)Type ^ xk.Next())); if (Type == SCTypeCode.Object) { bw.Write(Data.Length ^ xk.Next32()); } else if (Type == SCTypeCode.Array) { var entries = Data.Length / SubType.GetTypeSize(); bw.Write(entries ^ xk.Next32()); bw.Write((byte)((byte)SubType ^ xk.Next())); } foreach (ref var b in Data.AsSpan()) bw.Write((byte)(b ^ xk.Next())); } /// public static int GetTotalLength(ReadOnlySpan data) { int offset = 0; var key = ReadUInt32LittleEndian(data); offset += 4; return GetTotalLength(data, key, offset); } /// /// Gets the total length of an encoded data block. The input must be at least 10 bytes long to ensure all block types are correctly parsed. /// /// Data the header exists in. /// Key to decrypt with /// Offset the block is to be read from (modified to offset by the amount of bytes consumed). /// This method is useful if you do not know the exact size of a block yet; e.g. fetching the data is an expensive operation. public static int GetTotalLength(ReadOnlySpan data, uint key, int offset = 0) { var xk = new SCXorShift32(key); var type = (SCTypeCode)(data[offset++] ^ xk.Next()); switch (type) { case SCTypeCode.Bool1: case SCTypeCode.Bool2: case SCTypeCode.Bool3: Debug.Assert(type != SCTypeCode.Bool3); // invalid type, haven't seen it used yet return offset; case SCTypeCode.Object: // Cast raw bytes to Object var length = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; return offset + length; case SCTypeCode.Array: // Cast raw bytes to SubType[] var count = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; type = (SCTypeCode)(data[offset++] ^ xk.Next()); return offset + (type.GetTypeSize() * count); default: // Single Value Storage return offset + type.GetTypeSize(); } } /// public static SCBlock ReadFromOffset(ReadOnlySpan data, ref int offset) { // Get key var key = ReadUInt32LittleEndian(data[offset..]); offset += 4; return ReadFromOffset(data, key, ref offset); } /// /// Reads a new object from the , determining the and during read. /// /// Decrypted data /// Key to decrypt with /// Offset the block is to be read from (modified to offset by the amount of bytes consumed). /// New object containing all info for the block. public static SCBlock ReadFromOffset(ReadOnlySpan data, uint key, ref int offset) { // initialize xorshift to decrypt var xk = new SCXorShift32(key); // Parse the block's type var type = (SCTypeCode)(data[offset++] ^ xk.Next()); switch (type) { case SCTypeCode.Bool1: case SCTypeCode.Bool2: case SCTypeCode.Bool3: // Block types are empty, and have no extra data. Debug.Assert(type != SCTypeCode.Bool3); // invalid type, haven't seen it used yet return new SCBlock(key, type); case SCTypeCode.Object: // Cast raw bytes to Object { var num_bytes = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; var arr = data.Slice(offset, num_bytes).ToArray(); offset += num_bytes; for (int i = 0; i < arr.Length; i++) arr[i] ^= xk.Next(); return new SCBlock(key, type, arr); } case SCTypeCode.Array: // Cast raw bytes to SubType[] { var num_entries = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; var sub = (SCTypeCode)(data[offset++] ^ xk.Next()); var num_bytes = num_entries * sub.GetTypeSize(); var arr = data.Slice(offset, num_bytes).ToArray(); offset += num_bytes; for (int i = 0; i < arr.Length; i++) arr[i] ^= xk.Next(); EnsureArrayIsSane(sub, arr); return new SCBlock(key, arr, sub); } default: // Single Value Storage { var num_bytes = type.GetTypeSize(); var arr = data.Slice(offset, num_bytes).ToArray(); offset += num_bytes; for (int i = 0; i < arr.Length; i++) arr[i] ^= xk.Next(); return new SCBlock(key, type, arr); } } } [Conditional("DEBUG")] private static void EnsureArrayIsSane(SCTypeCode sub, ReadOnlySpan arr) { if (sub == SCTypeCode.Bool3) Debug.Assert(arr.IndexOfAnyExcept(0, 1, 2) == -1); else Debug.Assert(sub > SCTypeCode.Array); } /// /// Merges the properties from into this object. /// /// Block to copy all values from. public void CopyFrom(SCBlock other) { if (Type.IsBoolean()) ChangeBooleanType(other.Type); else ChangeData(other.Data); } }