Files
OpenMaidEngine/engine/Age.Engine.Tests/NativeSaveContainerTests.cs
2026-07-24 14:00:33 -04:00

178 lines
8.3 KiB
C#

using System.Buffers.Binary;
using System.Text;
using Age.Engine.Persistence;
using Age.Engine.Sys4;
public class NativeSaveContainerTests
{
private static readonly NativeSystemTime Timestamp =
new(2026, 7, 5, 24, 13, 42, 17, 321);
[Fact]
public void NativeCrcVariantsMatchIndependentCheckVectors()
{
byte[] input = Encoding.ASCII.GetBytes("123456789");
Assert.Equal(0xfc891918u, NativeSaveContainerCodec.Crc32Msb(input));
Assert.Equal(0xcbf43926u, NativeSaveContainerCodec.Crc32Reflected(input));
}
[Fact]
public void LzssEncoderRoundTripsTheNativeRingDialect()
{
byte[] input = Encoding.ASCII.GetBytes(string.Concat(
Enumerable.Repeat("HIMEGARI-HIMEGARI-0000000000000000-", 80)));
byte[] encoded = LzssEncoder.EncodeOrVerbatim(input);
Assert.True(encoded.Length < input.Length);
Assert.Equal(input, LzssDecoder.Decode(encoded, input.Length, "save test"));
byte[] incompressible = [1, 9, 2, 8, 3, 7, 4, 6];
Assert.Equal(incompressible, LzssEncoder.EncodeOrVerbatim(incompressible));
}
[Fact]
public void VersionTwoContainerWritesNativeHeaderFrameAndRoundTrips()
{
byte[] payload = Enumerable.Range(0, 256)
.SelectMany(i => BitConverter.GetBytes(i % 7))
.ToArray();
var metadata = new NativeSaveMetadata(
NativeSaveMagic.S4SD, 0x10203040, "姫狩りDM", Timestamp, 54321, 3, 2);
var options = new NativeSaveEncodingOptions(0x78563412, 0x1357);
byte[] file = NativeSaveContainerCodec.Encode(payload, metadata, options);
Assert.Equal("S4SD", Encoding.ASCII.GetString(file, 0, 4));
Assert.Equal(0x10203040u, BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(4)));
Assert.Equal((ushort)2026, BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(0x108)));
Assert.Equal((ushort)7, BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(0x10a)));
Assert.Equal((ushort)24, BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(0x10e)));
Assert.Equal(54321u, BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(0x118)));
Assert.Equal(3, BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(0x11c)));
Assert.Equal(2, BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(0x120)));
Assert.Equal(0x78563412u, BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(0x130)));
Assert.Equal(0x1357u, BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(0x134)));
byte[] checkedLogical = new byte[payload.Length + 8];
payload.CopyTo(checkedLogical.AsSpan(8));
BinaryPrimitives.WriteUInt32LittleEndian(
checkedLogical, NativeSaveContainerCodec.Crc32Msb(payload));
BinaryPrimitives.WriteUInt32LittleEndian(
checkedLogical.AsSpan(4), NativeSaveContainerCodec.Crc32Reflected(payload));
byte[] stored = LzssEncoder.EncodeOrVerbatim(checkedLogical);
int expectedTransformDwords = stored.Length / 4 + 0x0d;
Assert.Equal(
checked((uint)(expectedTransformDwords * 2)),
BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(0x124)));
NativeSaveDocument decoded = NativeSaveContainerCodec.Decode(file);
Assert.Equal(metadata, decoded.Metadata);
Assert.Equal(payload, decoded.Payload);
Assert.Equal(file.Length, decoded.BytesConsumed);
Assert.Equal(metadata, NativeSaveContainerCodec.ReadMetadata(file.AsSpan(0, 0x124)));
}
[Fact]
public void LegacyUncompressedContainerRoundTrips()
{
byte[] payload = Enumerable.Range(0, 64).Select(i => (byte)(i * 37)).ToArray();
var metadata = new NativeSaveMetadata(
NativeSaveMagic.S3SD, 77, "legacy", Timestamp, 9, 1, 1);
byte[] file = NativeSaveContainerCodec.Encode(
payload, metadata, new NativeSaveEncodingOptions(0xabcdef01, 3));
NativeSaveDocument decoded = NativeSaveContainerCodec.Decode(file);
Assert.Equal(metadata, decoded.Metadata);
Assert.Equal(payload, decoded.Payload);
Assert.Equal((uint)((payload.Length + 8) / 4 * 2),
BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(0x124)));
uint mixed = NativeSaveContainerCodec.Crc32Msb(payload) ^ 0xabcdef01;
Assert.Equal((mixed >> 16) * 3,
BinaryPrimitives.ReadUInt32LittleEndian(
file.AsSpan(NativeSaveContainerCodec.FixedPrefixSize)));
Assert.Equal((mixed & 0xffff) * 3,
BinaryPrimitives.ReadUInt32LittleEndian(
file.AsSpan(NativeSaveContainerCodec.FixedPrefixSize + 4)));
}
[Fact]
public void EncodedChecksumAndExactDivisionRejectCorruption()
{
byte[] payload = Enumerable.Range(0, 16).SelectMany(BitConverter.GetBytes).ToArray();
var metadata = new NativeSaveMetadata(
NativeSaveMagic.S4SD, 1, "test", Timestamp, 0, 1, 1);
byte[] file = NativeSaveContainerCodec.Encode(
payload, metadata, new NativeSaveEncodingOptions(0x12345678, 3));
byte[] crcFailure = file.ToArray();
crcFailure[^1] ^= 0x80;
Assert.Contains("checksum", Assert.Throws<InvalidDataException>(
() => NativeSaveContainerCodec.Decode(crcFailure)).Message);
byte[] divisionFailure = file.ToArray();
int encodedOffset = NativeSaveContainerCodec.FixedPrefixSize;
uint firstProduct = BinaryPrimitives.ReadUInt32LittleEndian(divisionFailure.AsSpan(encodedOffset));
BinaryPrimitives.WriteUInt32LittleEndian(divisionFailure.AsSpan(encodedOffset), firstProduct + 1);
ReadOnlySpan<byte> encoded = divisionFailure.AsSpan(encodedOffset);
BinaryPrimitives.WriteUInt32LittleEndian(
divisionFailure.AsSpan(0x128), NativeSaveContainerCodec.Crc32Msb(encoded));
BinaryPrimitives.WriteUInt32LittleEndian(
divisionFailure.AsSpan(0x12c), NativeSaveContainerCodec.Crc32Reflected(encoded));
Assert.Contains("divisible", Assert.Throws<InvalidDataException>(
() => NativeSaveContainerCodec.Decode(divisionFailure)).Message);
}
[Fact]
public void IdentityUsesNativeLayoutTwoVersionCompatibility()
{
var identity = new NativeSaveIdentity(NativeSaveMagic.S4SD, 9, "game", 2, 10);
identity.Validate(identity.CreateMetadata(Timestamp, 0) with { SaveVersion2 = 20 });
Assert.Throws<InvalidDataException>(() =>
identity.Validate(identity.CreateMetadata(Timestamp, 0) with { SaveVersion1 = 3 }));
Assert.Throws<InvalidDataException>(() =>
identity.Validate(identity.CreateMetadata(Timestamp, 0) with { GameId = "other" }));
}
[Fact]
public void DirectoryStoreKeepsSharedBackupFallbackAndDirectNumberedFiles()
{
string root = Path.Combine(Path.GetTempPath(), "age-native-save-" + Guid.NewGuid().ToString("N"));
try
{
var identity = new NativeSaveIdentity(NativeSaveMagic.S4SD, 123, "himegari-test", 3, 2);
var store = new DirectoryNativeDatStore(root, identity);
byte[] first = Enumerable.Repeat((byte)0x11, 64).ToArray();
byte[] second = Enumerable.Repeat((byte)0x22, 64).ToArray();
byte[] numbered = Enumerable.Repeat((byte)0x33, 64).ToArray();
store.SaveShared(first, Timestamp, 10);
Assert.True(File.Exists(Path.Combine(root, "SAVE.DAT")));
Assert.False(File.Exists(Path.Combine(root, "$$SAVE.DAT")));
Assert.False(File.Exists(Path.Combine(root, "SAVE.BAK")));
store.SaveShared(second, Timestamp, 20);
Assert.Equal(second, store.LoadShared()!.Payload);
Assert.True(File.Exists(Path.Combine(root, "SAVE.BAK")));
string primary = Path.Combine(root, "SAVE.DAT");
byte[] corrupt = File.ReadAllBytes(primary);
corrupt[^1] ^= 1;
File.WriteAllBytes(primary, corrupt);
Assert.Equal(first, store.LoadShared()!.Payload);
store.SaveNumbered(4, numbered, Timestamp, 30);
Assert.True(File.Exists(Path.Combine(root, "SAVE04.DAT")));
Assert.Equal(numbered, store.LoadNumbered(4)!.Payload);
Assert.Null(store.LoadNumbered(5));
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
}