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)
{
var xk = new SCXorShift32(Key);
bw.Write(Key);
bw.Write((byte)((byte)Type ^ xk.Next()));
if (Type == SCTypeCode.Object)
{
bw.Write((uint)Data.Length ^ xk.Next32());
}
else if (Type == SCTypeCode.Array)
{
var entries = (uint)(Data.Length / SubType.GetTypeSize());
bw.Write(entries ^ xk.Next32());
bw.Write((byte)((byte)SubType ^ xk.Next()));
}
foreach (var b in Data)
bw.Write((byte)(b ^ xk.Next()));
}
///
/// 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.
/// 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)
{
int offset = 0;
var key = ReadUInt32LittleEndian(data[offset..]);
offset += 4;
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..]) ^ (int)xk.Next32();
offset += 4;
return offset + length;
case SCTypeCode.Array: // Cast raw bytes to SubType[]
var count = ReadInt32LittleEndian(data[offset..]) ^ (int)xk.Next32();
offset += 4;
type = (SCTypeCode)(data[offset++] ^ xk.Next());
return offset + (type.GetTypeSize() * count);
default: // Single Value Storage
return offset + type.GetTypeSize();
}
}
///
/// Reads a new object from the , determining the and during read.
///
/// Decrypted data
/// 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, ref int offset)
{
// Get key, initialize xorshift to decrypt
var key = ReadUInt32LittleEndian(data[offset..]);
offset += 4;
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..]) ^ (int)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] ^= (byte)xk.Next();
return new SCBlock(key, type, arr);
}
case SCTypeCode.Array: // Cast raw bytes to SubType[]
{
var num_entries = ReadInt32LittleEndian(data[offset..]) ^ (int)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] ^= (byte)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] ^= (byte)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);
}
}