Caching: a single-file refusal echoes a header only in its known shape

RequireSingleFileFormat echoed any header string. It now names one only in
the shape cultcache.store.v<digits> and otherwise gives its byte length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 18:35:20 +02:00
commit c996ae7892
2 changed files with 32 additions and 1 deletions

View file

@ -224,7 +224,7 @@ public static class CultDocumentMessagePackSerialization
!string.Equals(version, CultPersistedStoreSnapshot.FormatV2, StringComparison.Ordinal))
{
throw new NotSupportedException(
$"Store format {version} is not readable; this runtime reads {CultPersistedStoreSnapshot.FormatV1} and {CultPersistedStoreSnapshot.FormatV2}.");
$"Store format {DescribeHeader(version)} is not readable; this runtime reads {CultPersistedStoreSnapshot.FormatV1} and {CultPersistedStoreSnapshot.FormatV2}.");
}
// Only a v2 store may hold a variant: a v1 header over a variant record is a store no reader could trust.
@ -239,6 +239,17 @@ public static class CultDocumentMessagePackSerialization
}
}
// A store header as a refusal may show it: echoed in the shape cultcache.store.v<digits>, and otherwise described
// only by its length, since the bytes are the store's.
private static string DescribeHeader(string header)
{
const string prefix = "cultcache.store.v";
var version = header.StartsWith(prefix, StringComparison.Ordinal) ? header.Substring(prefix.Length) : "";
return version.Length > 0 && version.All(character => character >= '0' && character <= '9')
? header
: $"an unrecognised cultcache.store.* header of {System.Text.Encoding.UTF8.GetByteCount(header)} bytes";
}
private static void WritePersistedRecord(ref MessagePackWriter writer, CultPersistedRecord record)
{
writer.WriteArrayHeader(record.Variant == null ? PersistedRecordFieldCount : VariantRecordFieldCount);

View file

@ -202,6 +202,26 @@ namespace GameCult.Caching.Tests
}
}
// A refusal echoes a header only in the shape cultcache.store.v<digits>; any other is described by its length.
[TestCase("cultcache.store.v12", true)]
[TestCase("cultcache.store.SECRET-HEADER", false)]
[TestCase("cultcache.store.v12SECRET", false)]
[TestCase("cultcache.store.v", false)]
public void SingleFileRefusalEchoesAHeaderOnlyInTheKnownShape(string header, bool echoed)
{
var snapshot = new CultPersistedStoreSnapshot { FormatVersion = header };
var error = Assert.Throws<NotSupportedException>(() => CultDocumentMessagePackSerialization.RequireSingleFileFormat(snapshot))!;
if (echoed)
{
Assert.That(error.Message, Does.Contain("format " + header + " is not"));
}
else
{
Assert.That(error.Message, Does.Not.Contain("SECRET").And.Not.Contain("format " + header + " is not"));
Assert.That(error.Message, Does.Contain($"of {header.Length} bytes"));
}
}
[Test]
public void V1StoreWrittenAtTheBaseCommitStillDecodesByteForByte()
{