Merge variants C0: every reader refuses what C1 will write, by name
C#, TypeScript, Python, Rust and CultMesh refuse an unknown cultcache.store.* header and a fifth record slot, naming the version or the key and schemaId. The silent slot-skipping and C#'s ignored header are deleted, and CultMesh's second record decoder is gone. Shared vectors are in tests/vectors/document-variants-c0/. v1 stores stay byte-identical. At base, the single-file commit's read-modify-write rewrote a v2 store as v1, which lost data; it now refuses. Soul passed it. Cross-runtime interop was run and passes. Three C# Caching tests fail on Linux at both base and tip: they are Windows share-mode tests (a FileShare.Read handle blocking delete/replace), which cannot pass on Linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
e382bb4e20
20 changed files with 471 additions and 44 deletions
|
|
@ -30,6 +30,7 @@ public sealed class CultDocumentResolver : IFormatterResolver
|
|||
|
||||
public static class CultDocumentMessagePackSerialization
|
||||
{
|
||||
private const string SingleFileFormatVersion = "cultcache.store.v1";
|
||||
private const int PersistedRecordFieldCount = 4;
|
||||
private const int SchemaCatalogEntryFieldCount = 7;
|
||||
private const int SchemaCatalogMemberFieldCount = 8;
|
||||
|
|
@ -139,7 +140,8 @@ public static class CultDocumentMessagePackSerialization
|
|||
|
||||
if (fieldCount > 0)
|
||||
{
|
||||
snapshot.FormatVersion = reader.ReadString() ?? "cultcache.store.v1";
|
||||
snapshot.FormatVersion = reader.ReadString()
|
||||
?? throw new NotSupportedException("Store snapshot declares no format version; this runtime reads " + SingleFileFormatVersion + " only.");
|
||||
}
|
||||
|
||||
if (fieldCount > 1)
|
||||
|
|
@ -170,6 +172,17 @@ public static class CultDocumentMessagePackSerialization
|
|||
return snapshot;
|
||||
}
|
||||
|
||||
/// <summary>Refuses a single-file snapshot this runtime cannot read, naming the format it found.</summary>
|
||||
public static void RequireSingleFileFormat(CultPersistedStoreSnapshot snapshot)
|
||||
{
|
||||
if (!string.Equals(snapshot.FormatVersion, SingleFileFormatVersion, StringComparison.Ordinal))
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Store format {snapshot.FormatVersion} is not readable; this runtime reads {SingleFileFormatVersion} only. " +
|
||||
"The store needs a runtime that resolves document variants.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WritePersistedRecord(ref MessagePackWriter writer, CultPersistedRecord record)
|
||||
{
|
||||
writer.WriteArrayHeader(PersistedRecordFieldCount);
|
||||
|
|
@ -179,7 +192,7 @@ public static class CultDocumentMessagePackSerialization
|
|||
writer.Write(record.Payload);
|
||||
}
|
||||
|
||||
private static CultPersistedRecord ReadPersistedRecord(ref MessagePackReader reader)
|
||||
public static CultPersistedRecord ReadPersistedRecord(ref MessagePackReader reader)
|
||||
{
|
||||
var fieldCount = reader.ReadArrayHeader();
|
||||
var record = new CultPersistedRecord();
|
||||
|
|
@ -204,9 +217,11 @@ public static class CultDocumentMessagePackSerialization
|
|||
record.Payload = reader.ReadBytes()?.ToArray() ?? Array.Empty<byte>();
|
||||
}
|
||||
|
||||
for (var index = PersistedRecordFieldCount; index < fieldCount; index++)
|
||||
if (fieldCount > PersistedRecordFieldCount)
|
||||
{
|
||||
reader.Skip();
|
||||
throw new NotSupportedException(
|
||||
$"Record '{record.Key}' (schema '{record.SchemaId}') has {fieldCount} slots; this runtime reads {PersistedRecordFieldCount}. " +
|
||||
"The store needs a runtime that resolves document variants.");
|
||||
}
|
||||
|
||||
return record;
|
||||
|
|
@ -371,7 +386,9 @@ public class SingleFileMessagePackBackingStore : SingleFileBackingStore
|
|||
|
||||
protected override CultPersistedStoreSnapshot DeserializeSnapshot(byte[] data)
|
||||
{
|
||||
return CultDocumentMessagePackSerialization.DeserializeSnapshot(data);
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(data);
|
||||
CultDocumentMessagePackSerialization.RequireSingleFileFormat(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
protected override byte[] SerializePayload(object document)
|
||||
|
|
|
|||
|
|
@ -212,6 +212,11 @@ Example:
|
|||
The payload is the domain object only. It does not repeat the key, schema id,
|
||||
or stored timestamp.
|
||||
|
||||
Under `cultcache.store.v1` a record has exactly these four slots. A reader refuses any
|
||||
other `cultcache.store.*` header and any record with more than four slots, naming the
|
||||
version or the record's key and schema id. It never skips a slot it does not
|
||||
understand.
|
||||
|
||||
## Canonical Semantic Schema Hash
|
||||
|
||||
`schemaId` should be derived from a canonical semantic schema hash.
|
||||
|
|
|
|||
|
|
@ -236,14 +236,18 @@ namespace GameCult.Mesh
|
|||
private static CultPersistedStoreSnapshot ReadSingleFileSnapshot(string path)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
CultPersistedStoreSnapshot snapshot;
|
||||
try
|
||||
{
|
||||
return CultDocumentMessagePackSerialization.DeserializeSnapshot(bytes);
|
||||
snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(bytes);
|
||||
}
|
||||
catch (Exception ex) when (ex is MessagePackSerializationException or InvalidOperationException)
|
||||
{
|
||||
return ReadLegacySingleFileSnapshot(bytes);
|
||||
snapshot = ReadLegacySingleFileSnapshot(bytes);
|
||||
}
|
||||
|
||||
CultDocumentMessagePackSerialization.RequireSingleFileFormat(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static CultPersistedStoreSnapshot ReadLegacySingleFileSnapshot(byte[] bytes)
|
||||
|
|
@ -252,7 +256,8 @@ namespace GameCult.Mesh
|
|||
var fieldCount = reader.ReadArrayHeader();
|
||||
var snapshot = new CultPersistedStoreSnapshot();
|
||||
if (fieldCount > 0)
|
||||
snapshot.FormatVersion = reader.ReadString() ?? "cultcache.store.v1";
|
||||
snapshot.FormatVersion = reader.ReadString()
|
||||
?? throw new NotSupportedException("Store snapshot declares no format version.");
|
||||
|
||||
if (fieldCount > 1)
|
||||
{
|
||||
|
|
@ -267,7 +272,7 @@ namespace GameCult.Mesh
|
|||
var recordCount = reader.ReadArrayHeader();
|
||||
snapshot.Records = new CultPersistedRecord[recordCount];
|
||||
for (var index = 0; index < recordCount; index++)
|
||||
snapshot.Records[index] = ReadLegacyPersistedRecord(ref reader);
|
||||
snapshot.Records[index] = CultDocumentMessagePackSerialization.ReadPersistedRecord(ref reader);
|
||||
}
|
||||
|
||||
for (var index = 3; index < fieldCount; index++)
|
||||
|
|
@ -303,23 +308,6 @@ namespace GameCult.Mesh
|
|||
return entry;
|
||||
}
|
||||
|
||||
private static CultPersistedRecord ReadLegacyPersistedRecord(ref MessagePackReader reader)
|
||||
{
|
||||
var fieldCount = reader.ReadArrayHeader();
|
||||
var record = new CultPersistedRecord();
|
||||
if (fieldCount > 0) record.Key = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 1) record.SchemaId = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 2) record.StoredAt = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 3)
|
||||
{
|
||||
var payload = reader.ReadBytes();
|
||||
record.Payload = payload.HasValue ? payload.Value.ToArray() : Array.Empty<byte>();
|
||||
}
|
||||
for (var index = 4; index < fieldCount; index++)
|
||||
reader.Skip();
|
||||
return record;
|
||||
}
|
||||
|
||||
private static bool PublishesSchema(CultSchemaCatalogEntry[] catalog, string schemaId)
|
||||
{
|
||||
return catalog.Any(entry =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue