diff --git a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
index aaa3802f..82d27db8 100644
--- a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
+++ b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
@@ -185,13 +185,7 @@ public static class CultDocumentMessagePackSerialization
/// , so a reader never takes part of a file for the whole and never skips a slot
/// it does not understand. The rewriting paths use this same reader as their verdict on whether a file may be replaced.
///
- public static CultPersistedStoreSnapshot DeserializeSnapshot(byte[] payload) => ReadStore(payload, ReadSchemaCatalogEntry);
-
- internal delegate CultSchemaCatalogEntry CatalogEntryReader(ref MessagePackReader reader);
-
- // The one store reader. A caller with an older catalog layout supplies its own entry reader; the framing, the slot count
- // and the records are judged here, once.
- internal static CultPersistedStoreSnapshot ReadStore(byte[] payload, CatalogEntryReader readCatalogEntry)
+ public static CultPersistedStoreSnapshot DeserializeSnapshot(byte[] payload)
{
try
{
@@ -215,7 +209,7 @@ public static class CultDocumentMessagePackSerialization
snapshot.SchemaCatalog = new CultSchemaCatalogEntry[catalogCount];
for (var index = 0; index < catalogCount; index++)
{
- snapshot.SchemaCatalog[index] = readCatalogEntry(ref reader);
+ snapshot.SchemaCatalog[index] = ReadSchemaCatalogEntry(ref reader);
}
var recordCount = reader.ReadArrayHeader();
diff --git a/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs b/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs
index 486ecb29..f5c7836a 100644
--- a/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs
+++ b/src/GameCult.Mesh/CultMeshSingleFileDocuments.cs
@@ -1,11 +1,9 @@
using System;
-using System.Buffers;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
-using MessagePack;
namespace GameCult.Mesh
{
@@ -241,9 +239,7 @@ namespace GameCult.Mesh
throw new ArgumentException("Catalog entry must include a schema id.", nameof(catalogEntry));
payload ??= Array.Empty();
- // The file being replaced is read first, by the reader every read of it uses: one this runtime cannot read refuses the
- // write instead of being overwritten.
- var existing = File.Exists(path) ? ReadSingleFileSnapshot(path) : null;
+ CultPersistedStoreSnapshot? existing = null;
// A typed write sees the document it replaces the file with, so its ids decide. A raw payload is opaque: the writer
// cannot see whether it holds ids, so a file already marked stays marked.
@@ -263,96 +259,6 @@ namespace GameCult.Mesh
}
};
- var directory = Path.GetDirectoryName(path);
- if (!string.IsNullOrWhiteSpace(directory))
- Directory.CreateDirectory(directory);
-
- WriteFileAtomically(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
- }
-
- // Every refusal is a CultStoreUnreadableException naming the file. A store whose catalog entries are in the older layout is
- // read by the same store reader with the older entry reader, so it is judged by the same framing, slot count and records.
- // Only a store that is in the older layout goes there; any other refusal stands as the current reader gave it.
- private static CultPersistedStoreSnapshot ReadSingleFileSnapshot(string path)
- {
- var bytes = File.ReadAllBytes(path);
- CultPersistedStoreSnapshot snapshot;
- try
- {
- try
- {
- snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(bytes);
- }
- catch (CultStoreUnreadableException) when (HasOlderCatalogLayout(bytes))
- {
- snapshot = CultDocumentMessagePackSerialization.ReadStore(bytes, ReadLegacySchemaCatalogEntry);
- }
-
- CultDocumentMessagePackSerialization.RequireSingleFileFormat(snapshot);
- }
- catch (CultStoreUnreadableException ex) when (ex.Path == null)
- {
- throw new CultStoreUnreadableException(ex.Message, path, ex.InnerException);
- }
-
- return snapshot;
- }
-
- // The older layout puts the content hash, a string, at catalog entry slot 5, where the current layout has the array of
- // compatible schema ids.
- private static bool HasOlderCatalogLayout(byte[] bytes)
- {
- try
- {
- var reader = new MessagePackReader(bytes);
- if (reader.ReadArrayHeader() != 3)
- return false;
- reader.Skip();
- var entries = reader.ReadArrayHeader();
- for (var entry = 0; entry < entries; entry++)
- {
- var fields = reader.ReadArrayHeader();
- for (var field = 0; field < fields; field++)
- {
- if (field == 5 && reader.NextMessagePackType == MessagePackType.String)
- return true;
- reader.Skip();
- }
- }
-
- return false;
- }
- catch (Exception ex) when (ex is EndOfStreamException or MessagePackSerializationException or InvalidOperationException)
- {
- return false;
- }
- }
-
- private static CultSchemaCatalogEntry ReadLegacySchemaCatalogEntry(ref MessagePackReader reader)
- {
- var fieldCount = reader.ReadArrayHeader();
- var entry = new CultSchemaCatalogEntry();
- if (fieldCount > 0) entry.SchemaId = reader.ReadString() ?? string.Empty;
- if (fieldCount > 1) entry.SchemaName = reader.ReadString() ?? string.Empty;
- if (fieldCount > 2) entry.SchemaVersion = reader.ReadString() ?? string.Empty;
- if (fieldCount > 3) entry.CanonicalSchemaJson = reader.ReadString() ?? string.Empty;
- if (fieldCount > 4) reader.Skip();
- if (fieldCount > 5) entry.ContentHash = reader.ReadString() ?? string.Empty;
- if (fieldCount > 6)
- {
- var memberCount = reader.ReadArrayHeader();
- for (var index = 0; index < memberCount; index++)
- reader.Skip();
- }
-
- for (var index = 7; index < fieldCount; index++)
- reader.Skip();
-
- entry.CompatibleSchemaIds = string.IsNullOrWhiteSpace(entry.SchemaId)
- ? Array.Empty()
- : new[] { entry.SchemaId };
- entry.Members = Array.Empty();
- return entry;
}
private static bool PublishesSchema(CultSchemaCatalogEntry[] catalog, string schemaId)
@@ -361,14 +267,5 @@ namespace GameCult.Mesh
string.Equals(entry.SchemaId, schemaId, StringComparison.Ordinal) ||
entry.CompatibleSchemaIds.Any(candidate => string.Equals(candidate, schemaId, StringComparison.Ordinal)));
}
-
- private static void WriteFileAtomically(string path, byte[] bytes)
- {
- var tempPath = path + ".tmp";
- File.WriteAllBytes(tempPath, bytes);
- if (File.Exists(path))
- File.Delete(path);
- File.Move(tempPath, path);
- }
}
}
diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
index 0e4dfa79..5a9838c8 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
@@ -2585,34 +2585,6 @@ public sealed class CultMeshStreamingTests
cache.Get(key)!.Text.Should().Be("published-directly");
}
- [Test]
- public void SingleFileDocumentHelpers_ReadLegacySingleDocumentSnapshots()
- {
- using var directory = new TemporaryDirectory();
- var filePath = Path.Combine(directory.Path, "legacy-single-document.ccmp");
- var key = new CultRecordKey("mesh-note:legacy-single-document");
- var descriptor = CultDocumentRegistry.Shared.GetRequired();
- var payload = CultDocumentMessagePackSerialization.SerializeUntyped(new MeshPublicationNoteDocument
- {
- Schema = "tests.mesh_publication_note.v1",
- Text = "legacy-published",
- Revision = 4
- }, typeof(MeshPublicationNoteDocument));
-
- File.WriteAllBytes(filePath, WriteLegacySingleDocumentSnapshot(
- key.Value,
- descriptor.SchemaId,
- descriptor.SchemaName,
- descriptor.SchemaVersion,
- "2026-06-27T12:00:00.0000000Z",
- payload));
-
- var document = CultMesh.ReadSingleFileDocument(filePath, key);
-
- document.Text.Should().Be("legacy-published");
- document.Revision.Should().Be(4);
- }
-
[Test]
public async Task DocumentHandle_ReadsRemotePeerSnapshotsAsTypedDocuments()
{
@@ -3853,39 +3825,6 @@ public sealed class CultMeshStreamingTests
ring.Stats().UnavoidableCopyCount.Should().Be(1);
}
- private static byte[] WriteLegacySingleDocumentSnapshot(
- string key,
- string schemaId,
- string schemaName,
- string schemaVersion,
- string storedAt,
- byte[] payload)
- {
- var buffer = new ArrayBufferWriter();
- var writer = new MessagePackWriter(buffer);
-
- writer.WriteArrayHeader(3);
- writer.Write("cultcache.store.v1");
- writer.WriteArrayHeader(1);
- writer.WriteArrayHeader(7);
- writer.Write(schemaId);
- writer.Write(schemaName);
- writer.Write(schemaVersion);
- writer.Write("Legacy Aetheria single document");
- writer.Write(storedAt);
- writer.Write("legacy-hash");
- writer.WriteArrayHeader(0);
- writer.WriteArrayHeader(1);
- writer.WriteArrayHeader(4);
- writer.Write(key);
- writer.Write(schemaId);
- writer.Write(storedAt);
- writer.Write(payload);
- writer.Flush();
-
- return buffer.WrittenSpan.ToArray();
- }
-
private static CultMeshStreamCatalog CatalogWithByteStream()
{
var catalog = CultMesh.CreateStreamCatalog();
diff --git a/tests/GameCult.Mesh.Tests/CultMeshVariantRefusalTests.cs b/tests/GameCult.Mesh.Tests/CultMeshVariantRefusalTests.cs
index 44d71051..bd387498 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshVariantRefusalTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshVariantRefusalTests.cs
@@ -38,26 +38,7 @@ public sealed class CultMeshVariantRefusalTests
message.Should().Contain("item:anvil").And.Contain(ItemSchemaId);
}
- [Test]
- public void ReadSingleFileDocument_RefusesExtraRecordSlotInTheLegacyCatalogLayoutToo()
- {
- var message = Refusal("legacy-catalog-extra-slot.msgpack").Message;
- message.Should().Contain("item:anvil").And.Contain(ItemSchemaId);
- }
-
- // The older catalog layout is read by the same store reader, so it is held to one array too: the same store with bytes after it
- // is refused, and only the bytes make the difference.
- [Test]
- public void ReadSingleFileDocument_ReadsTheLegacyCatalogLayoutButNotWithBytesAfterIt()
- {
- CultMesh.ReadSingleFileDocumentPayload(VectorPath("legacy-catalog-plain.msgpack"), new CultRecordKey("item:bellows"), ItemSchemaId)
- .Should().NotBeEmpty();
-
- Refusal("legacy-catalog-trailing.msgpack").Message.Should().Contain("after its MessagePack array");
- }
-
- // A store in the current layout with one catalog slot malformed is refused as CultCache refuses it. It is not the older layout,
- // so it is not read with the older reader, which would take the malformed canonical schema slot for a slot to skip.
+ // A store in the current layout with one catalog slot malformed is refused as CultCache refuses it.
[TestCase("current-catalog-bad-canonical.msgpack")]
[TestCase("current-catalog-bad-members.msgpack")]
[TestCase("current-catalog-bad-compat.msgpack")]