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:
parent
c4fbb53a3b
commit
aa6b11006f
3 changed files with 58 additions and 8 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<object>(), Array.Empty<object>() }));
|
||||
|
||||
foreach (var path in new[] { truncated, unknown })
|
||||
{
|
||||
var before = File.ReadAllBytes(path);
|
||||
Assert.Throws<NotSupportedException>(() => CultMesh.WriteSingleFileDocumentPayload(path, Key, schema, null, new byte[] { 0x90 }), path);
|
||||
File.ReadAllBytes(path).Should().Equal(before, path + " was rewritten");
|
||||
}
|
||||
|
||||
CultMesh.WriteSingleFileDocumentPayload(marked, Key, schema, null, new byte[] { 0x90 });
|
||||
HeaderOf(marked).Should().Be("cultcache.store.v3", "a whole marked file is still written and stays marked");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TypedWriteRefusesADuplicateIdAndAnElementObjectTwiceAndLeavesTheFileAlone()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue