diff --git a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
index 579eddb6..319d8361 100644
--- a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
+++ b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs
@@ -180,7 +180,8 @@ public static class CultDocumentMessagePackSerialization
///
/// Reads a store file: exactly one complete MessagePack array of three slots (header, schema catalog, records), or an empty
/// array, which is an empty store. A truncated file, bytes after the array, a value that is not an array, a first slot that
- /// is not a header string, a store with fewer or more slots, or a body that does not decode are refused with
+ /// is not a header string, a store with fewer or more slots, a record whose schema the catalog does not publish, or a body
+ /// that does not decode are refused with
/// , 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.
///
@@ -224,6 +225,7 @@ public static class CultDocumentMessagePackSerialization
snapshot.Records[index] = ReadPersistedRecord(ref reader);
}
+ RequirePublishedSchemas(snapshot);
return snapshot;
}
catch (CultStoreUnreadableException)
@@ -236,6 +238,25 @@ public static class CultDocumentMessagePackSerialization
}
}
+ // A record whose schema the store's catalog does not publish cannot be read by any runtime that has only the file.
+ private static void RequirePublishedSchemas(CultPersistedStoreSnapshot snapshot)
+ {
+ var published = new HashSet(StringComparer.Ordinal);
+ foreach (var entry in snapshot.SchemaCatalog)
+ {
+ published.Add(entry.SchemaId);
+ foreach (var compatible in entry.CompatibleSchemaIds)
+ published.Add(compatible);
+ }
+
+ var unpublished = snapshot.Records.FirstOrDefault(record => !published.Contains(record.SchemaId));
+ if (unpublished != null)
+ {
+ throw new CultStoreUnreadableException(
+ $"Record '{unpublished.Key}' names schema '{unpublished.SchemaId}', which the store's schema catalog does not publish.");
+ }
+ }
+
private static void RequireOneArray(byte[] bytes)
{
var whole = new MessagePackReader(bytes);
diff --git a/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs b/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs
index 7abee9a5..7a7ddb55 100644
--- a/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs
+++ b/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs
@@ -15,7 +15,7 @@ namespace GameCult.Caching.Tests
public class StoreReadabilityTests
{
private const int CSharp = 1;
- private static readonly CultDocumentRegistry Registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
+ private static readonly CultDocumentRegistry Registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck), typeof(VectorItem) });
private string _directory = "";
[SetUp]
@@ -66,12 +66,7 @@ namespace GameCult.Caching.Tests
var path = Path.Combine(_directory, "open.cc");
File.WriteAllBytes(path, File.ReadAllBytes(Path.Combine(VectorRoot(), vector)));
- using var cache = new CultCache();
- void Open()
- {
- cache.AddBackingStore(new SingleFileMessagePackBackingStore(path));
- cache.PullAllBackingStoresAsync().GetAwaiter().GetResult();
- }
+ void Open() => CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry }).Dispose();
if (reads)
Assert.DoesNotThrow(Open);