Bytes after the value: a payload is one MessagePack value (C#) (#125)
This commit is contained in:
parent
a8c2825745
commit
5e10909fe9
21 changed files with 441 additions and 30 deletions
|
|
@ -7,11 +7,12 @@ namespace GameCult.Caching.MessagePack;
|
|||
internal enum PayloadDirection { Read, Write }
|
||||
|
||||
/// <summary>
|
||||
/// The one payload nesting count. Walks the MessagePack headers of a document payload iteratively, counting every array and
|
||||
/// The one walk of a payload's shape: its depth and its one value. Walks the MessagePack headers of a document payload iteratively, counting every array and
|
||||
/// map (read by a formatter or skipped past it; the outermost array is depth 1) and refusing the first container past
|
||||
/// <see cref="CultMessagePackSecurity.MaximumPayloadDepth"/>. Scalars are skipped by length, nothing is allocated per
|
||||
/// element, and the count stack is bounded by the limit. A refusal names the type, the depth reached and the limit,
|
||||
/// never payload bytes. A payload that ends early is the deserializer's to refuse.
|
||||
/// never payload bytes. A payload that ends early is the deserializer's to refuse; bytes after the first value are refused
|
||||
/// with their count and never walked.
|
||||
/// </summary>
|
||||
internal static class CultPayloadDepth
|
||||
{
|
||||
|
|
@ -73,7 +74,13 @@ internal static class CultPayloadDepth
|
|||
|
||||
// One element is complete; close every container it was the last element of.
|
||||
while (depth > 0 && --remaining[depth - 1] == 0) depth--;
|
||||
if (depth == 0) return;
|
||||
if (depth == 0)
|
||||
{
|
||||
if (position < bytes.Length)
|
||||
throw new MessagePackSerializationException(
|
||||
$"{type.FullName} payload has {bytes.Length - position} bytes after its first MessagePack value (refused on {(direction == PayloadDirection.Read ? "read" : "write")}).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -403,6 +403,8 @@ runtime enforces the same bound on read and on write, and the refusal names the
|
|||
the depth and the limit, never payload bytes. The vectors are in
|
||||
`tests/vectors/union-parity` (`bounds-outcomes.msgpack`).
|
||||
|
||||
A record's payload is exactly one MessagePack value: every runtime refuses a payload with bytes after its first value wherever it reads a payload under the nesting bound, and on write where a write takes bytes the library did not encode itself (a C# type's own formatter, a Rust raw envelope), without walking those bytes, so a tail past the limit is refused as bytes after the value and not as depth; the refusal names the type and the count of bytes after the value, never the bytes (vectors: `tests/vectors/union-parity/bytes-after-outcomes.msgpack`). A CDN manifest is stored and keyed as a record and follows the same rule (`contracts/cultmesh/content-acceptance-vectors.json`: `trailing_nil`, `trailing_reserved_code` and `trailing_truncated_array` are refused). Streams of values (CultNet frames, a store's record array) are different contracts and are not covered.
|
||||
|
||||
## Variant Records
|
||||
|
||||
A variant record is a record whose document is a base record's document with some
|
||||
|
|
|
|||
|
|
@ -284,7 +284,10 @@ namespace GameCult.Mesh
|
|||
throw new ArgumentException($"Expected provider command schema '{CultMeshProviderSessionWireContract.CommandSchema}'.", nameof(document));
|
||||
if (!string.Equals(document.PayloadEncoding, "messagepack", StringComparison.Ordinal))
|
||||
throw new ArgumentException("Provider command document must use MessagePack.", nameof(document));
|
||||
var command = MessagePackSerializer.Deserialize<CultMeshProviderCommandWire>(document.Payload, Options);
|
||||
// A raw record's payload is one MessagePack value (ruling trailing-bytes-refuse): a tail is refused by count, never walked.
|
||||
var command = MessagePackSerializer.Deserialize<CultMeshProviderCommandWire>(new ReadOnlyMemory<byte>(document.Payload), Options, out var read);
|
||||
if (read < document.Payload.Length)
|
||||
throw new MessagePackSerializationException($"Provider command document: {document.Payload.Length - read} bytes after its first MessagePack value (refused on read).");
|
||||
if (command == null) throw new MessagePackSerializationException("Provider command decoded to null.");
|
||||
Validate(command);
|
||||
return command;
|
||||
|
|
|
|||
|
|
@ -641,23 +641,19 @@ namespace GameCult.Networking
|
|||
string storedAt,
|
||||
CultNetDocumentMessageOptions? options = null)
|
||||
{
|
||||
var binding = GetByDocumentType(descriptor.DocumentType) ??
|
||||
new CultNetDocumentBinding(
|
||||
descriptor.DocumentType,
|
||||
descriptor.SchemaId,
|
||||
value => CultDocumentMessagePackSerialization.SerializeUntyped(value, value.GetType(), _documents),
|
||||
payload => CultDocumentMessagePackSerialization.DeserializeUntyped(descriptor.DocumentType, payload, _documents));
|
||||
var binding = GetByDocumentType(descriptor.DocumentType);
|
||||
|
||||
return new CultNetRawDocumentRecord
|
||||
{
|
||||
SchemaId = binding.SchemaId,
|
||||
SchemaId = binding?.SchemaId ?? descriptor.SchemaId,
|
||||
SchemaName = descriptor.SchemaName,
|
||||
SchemaVersion = descriptor.SchemaVersion,
|
||||
SchemaContentHash = descriptor.ContentHash,
|
||||
RecordKey = key.Value,
|
||||
StoredAt = storedAt,
|
||||
PayloadEncoding = "messagepack",
|
||||
Payload = binding.PayloadSerializer(document),
|
||||
Payload = binding?.PayloadSerializer(document) ??
|
||||
CultDocumentMessagePackSerialization.SerializeUntyped(document, document.GetType(), _documents),
|
||||
SourceRuntimeId = options?.SourceRuntimeId,
|
||||
SourceAgentId = options?.SourceAgentId,
|
||||
SourceRole = options?.SourceRole,
|
||||
|
|
|
|||
Loading…
Reference in a new issue