diff --git a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
index d7ed650e..4071a42a 100644
--- a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
+++ b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
@@ -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
}
}
+ ///
+ /// 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.
+ ///
+ 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);
diff --git a/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs b/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs
index 5dbd7839..22aaf951 100644
--- a/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs
+++ b/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs
@@ -268,14 +268,8 @@ namespace GameCult.Mesh
private static string? ExistingHeader(string path)
{
- try
- {
- return CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).FormatVersion;
- }
- catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or MessagePackSerializationException or EndOfStreamException or InvalidOperationException or NotSupportedException)
- {
- return null;
- }
+ // A file that is gone has no header; one that cannot be read refuses the write rather than being overwritten.
+ return File.Exists(path) ? CultDocumentMessagePackSerialization.ReadStoreHeader(File.ReadAllBytes(path)) : null;
}
private static CultPersistedStoreSnapshot ReadSingleFileSnapshot(string path)
diff --git a/tests/GameCult.Mesh.Tests/CultMeshSingleFileElementIdTests.cs b/tests/GameCult.Mesh.Tests/CultMeshSingleFileElementIdTests.cs
index a59677cb..4636059f 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshSingleFileElementIdTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshSingleFileElementIdTests.cs
@@ -93,6 +93,31 @@ public sealed class CultMeshSingleFileElementIdTests
HeaderOf(path).Should().Be("cultcache.store.v3", "the payload's ids are not the writer's to see");
}
+ // A raw write does not read the store's records, but it does not overwrite a file it cannot read either: the runtimes agree.
+ [Test]
+ public void RawPayloadWriteRefusesAFileItCannotReadAndLeavesItUntouched()
+ {
+ var schema = new CultMeshSingleFileDocumentSchema("raw:schema", "RawSchema", "1");
+ var marked = Path_("marked.cc");
+ CultMesh.WriteSingleFileDocument(marked, Key, Publication(Descriptor()));
+ var whole = File.ReadAllBytes(marked);
+
+ var truncated = Path_("truncated.cc");
+ File.WriteAllBytes(truncated, whole[..^1]);
+ var unknown = Path_("v9.cc");
+ File.WriteAllBytes(unknown, MessagePackSerializer.Serialize(new object[] { "cultcache.store.v9", Array.Empty