CultMesh: a raw write refuses a file whose header it cannot read

ExistingHeader read the whole snapshot and swallowed every failure into "no header": a
truncated v3 file got a v1 raw write and an unknown v9 file was overwritten, where
TypeScript, Python and Rust refuse. CultDocumentMessagePackSerialization.ReadStoreHeader
reads the prefix header, refuses a file that is not one complete MessagePack value or whose
format this runtime cannot read, and the raw write keeps a whole marked file marked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-29 22:53:13 +02:00
commit aa6b11006f
3 changed files with 58 additions and 8 deletions

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -240,6 +241,36 @@ 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.
/// </summary>
public static string? ReadStoreHeader(byte[] bytes)
{
if (bytes.Length == 0)
return null;
try
{
var whole = new MessagePackReader(bytes);
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);
}
var reader = new MessagePackReader(bytes);
if (reader.NextMessagePackType != MessagePackType.Array || 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 });
return header;
}
private static void WritePersistedRecord(ref MessagePackWriter writer, CultPersistedRecord record)
{
writer.WriteArrayHeader(record.Variant == null ? PersistedRecordFieldCount : VariantRecordFieldCount);