Every runtime rewrites only a file that is exactly one store
A rewrite read the header of the file it replaces but not whether the file was a store: C# ReadStoreHeader skipped one value and ignored bytes after it, and took a non-array value or an array whose first slot is a string that is not a store header for a headerless legacy file (so did the C# Mesh raw write, which uses it); TypeScript's pushAll took any non-array value or non-header first slot for v1; Rust ignored bytes after the array. The rule, one per runtime: a file is a store on rewrite only when it is exactly one complete MessagePack array. Bytes after it, a value that is not an array, and a first slot that is a string but not a header this runtime reads are refused, and the file is left untouched. An empty file and a legacy envelope array (first slot not a string, empty included) carry no header and are still rewritten. - C#: ReadStoreHeader refuses non-array values, trailing bytes and a non-header first string, with a NotSupportedException naming which. - TypeScript: storeHeader (store-format.ts) decides the rewrite header; @msgpack/msgpack's decode already refuses trailing bytes. - Rust: store_header(bytes, whole) walks with a cursor; the rewrite path asks for the whole file, the read paths keep reading as before. - Python: push_all already reads the whole file (unpackb refuses trailing bytes, the legacy decoder refuses anything but a list of maps); pinned by a test. Tests in each runtime run the same file matrix (truncated, trailing, scalar, map, string first; empty, empty array, legacy), plus a C# unconditional commit over a store with trailing bytes and the C# Mesh raw write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4f742d05c3
commit
ea1bd6a797
8 changed files with 211 additions and 36 deletions
|
|
@ -243,31 +243,37 @@ public static class CultDocumentMessagePackSerialization
|
|||
|
||||
/// <summary>
|
||||
/// The format header of a store file about to be replaced by a writer that does not read the store's records: null for
|
||||
/// an empty file or a legacy envelope array, the header string otherwise. The header is read from the prefix, but a file
|
||||
/// that is not one complete MessagePack value or whose header this runtime cannot read is refused, never taken for a
|
||||
/// legacy file, so a writer cannot overwrite what it cannot see.
|
||||
/// an empty file or a legacy envelope array (an array whose first slot is not a string), the header string otherwise.
|
||||
/// A file is a store only when it is exactly one complete MessagePack array: a truncated file, bytes after the array, a
|
||||
/// value that is not an array, or a first slot that is a string but not a header this runtime reads are all refused,
|
||||
/// never taken for a legacy file, so a writer cannot overwrite what it cannot see.
|
||||
/// </summary>
|
||||
public static string? ReadStoreHeader(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length == 0)
|
||||
return null;
|
||||
var whole = new MessagePackReader(bytes);
|
||||
try
|
||||
{
|
||||
var whole = new MessagePackReader(bytes);
|
||||
if (whole.NextMessagePackType != MessagePackType.Array)
|
||||
throw new NotSupportedException($"The store is a MessagePack {whole.NextMessagePackType}, not an array; it is not rewritten.");
|
||||
whole.Skip();
|
||||
}
|
||||
catch (Exception ex) when (ex is EndOfStreamException or MessagePackSerializationException or InvalidOperationException)
|
||||
{
|
||||
throw new NotSupportedException("The store is not one complete MessagePack value; it is not rewritten.", ex);
|
||||
throw new NotSupportedException("The store is not one complete MessagePack array; it is not rewritten.", ex);
|
||||
}
|
||||
|
||||
if (!whole.End)
|
||||
throw new NotSupportedException("The store has bytes after its MessagePack array; it is not rewritten.");
|
||||
|
||||
var reader = new MessagePackReader(bytes);
|
||||
if (reader.NextMessagePackType != MessagePackType.Array || reader.ReadArrayHeader() == 0 ||
|
||||
reader.NextMessagePackType != MessagePackType.String)
|
||||
if (reader.ReadArrayHeader() == 0 || reader.NextMessagePackType != MessagePackType.String)
|
||||
return null;
|
||||
var header = reader.ReadString();
|
||||
if (header != null && header.StartsWith("cultcache.store.", StringComparison.Ordinal))
|
||||
RequireSingleFileFormat(new CultPersistedStoreSnapshot { FormatVersion = header });
|
||||
var header = reader.ReadString() ?? string.Empty;
|
||||
if (!header.StartsWith("cultcache.store.", StringComparison.Ordinal))
|
||||
throw new NotSupportedException($"The store's first slot is the string '{header}', not a store header; it is not rewritten.");
|
||||
RequireSingleFileFormat(new CultPersistedStoreSnapshot { FormatVersion = header });
|
||||
return header;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue