Contracts: whole-value shapes, closed list of owned shapes, door only verifies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
19f475ca92
commit
19963293ae
2 changed files with 43 additions and 36 deletions
|
|
@ -408,12 +408,18 @@ fifth slot:
|
|||
|
||||
## Element Ids
|
||||
|
||||
Every object that sits in a list, array or dictionary value (`IDictionary<,>` or
|
||||
`IReadOnlyDictionary<,>`) of a registered document type, or of a keyed struct a document
|
||||
holds, union subtypes included, carries an id: one string member with an integer `[Key]`
|
||||
and `[CultElementId]`. The id identifies the element inside its list, so an override can
|
||||
name it (a path step is `[slot, elementId]`) and a base edit that reorders or inserts
|
||||
elements retargets nothing.
|
||||
Every object that sits in a list, array or dictionary value of a shape the element-id walk
|
||||
owns carries an id: one string member with an integer `[Key]` and `[CultElementId]`. The
|
||||
owned shapes are arrays, `IEnumerable<T>` classes and interfaces, `IDictionary<,>` and
|
||||
`IReadOnlyDictionary<,>` values, and `[MessagePackObject]` or `[Union]` types with integer
|
||||
keys (a keyed struct included), read through the members MessagePack writes. Union subtypes
|
||||
are included. The id identifies the element inside its list, so an override can name it (a
|
||||
path step is `[slot, elementId]`) and a base edit that reorders or inserts elements
|
||||
retargets nothing.
|
||||
|
||||
Any other shape is one whole value. Its elements carry no element ids: a `[CultElementId]`
|
||||
member inside it is a plain string, never minted or checked and never counted for `v3`. A
|
||||
variant replaces it whole (operator, 2026-10-16).
|
||||
|
||||
- A random id is 12 lowercase hex characters. A derived id
|
||||
(`[CultElementId(nameof(Source))]`, for content-addressed elements) is the
|
||||
|
|
@ -432,28 +438,27 @@ elements retargets nothing.
|
|||
Python and Rust read `v3` and keep a `v3` header they found when they rewrite a
|
||||
store; they carry ids and do not decide when a document carries them.
|
||||
- An object list's elements are written by their `[Key]` layout, because the id is a
|
||||
keyed member of the element. A list whose element type is not keyed
|
||||
(`[MessagePackObject]` or `[Union]`) is a value list and is replaced whole; so is a list
|
||||
of keyed structs, since a struct is a value and never an element, but an object list
|
||||
inside a keyed struct is walked like any other. The walk follows declared member types:
|
||||
a member declared `object` is untyped on disk, and a subclass's extra members under a
|
||||
non-union base-typed member are not written, so neither is walked. The walk reads the
|
||||
members MessagePack writes: an integer `[Key]`, no `[IgnoreMember]`, a public field or a
|
||||
property whose getter is public. Any other `[Key]` member on the way to an object list
|
||||
(non-public, or a property with a non-public getter) is refused at registration, because
|
||||
MessagePack writes it only under `AllowPrivate` and the element-id walk reads public
|
||||
members. An id member needs a public setter or `init`, so the id MessagePack writes is read
|
||||
back; one with a non-public setter, or one MessagePack does not write, is refused. A
|
||||
`[MessagePackObject]` type is read by key even when it is `IEnumerable<T>`; MessagePack
|
||||
writes a keyed type that is also a collection (`ICollection<T>`, `IList`, `IDictionary`)
|
||||
as a collection, so C# refuses such a type at the first encode or decode when it is on the
|
||||
path to an object list. A `[DataContract]` type the walk reads as a collection, which
|
||||
MessagePack writes by its members, is refused the same way. A formatter MessagePack's
|
||||
source generator emits is not refused. For an
|
||||
object list, C# refuses at registration a concrete element type that is not keyed and
|
||||
`[MessagePackFormatter]` on any type, collection type or member on the way to the list or on the id
|
||||
member, and refuses at the first encode or decode of the document a formatter
|
||||
resolver that claims one of those types.
|
||||
keyed member of the element. Value lists are replaced whole: a list of a non-keyed
|
||||
class or struct, or of primitives. So is a list of keyed structs, since a struct is a
|
||||
value and never an element, but an object list inside a keyed struct is walked like any
|
||||
other. The walk follows declared member types: a member declared `object` is untyped on
|
||||
disk, and a subclass's extra members under a non-union base-typed member are not written,
|
||||
so neither is walked. Whole values are `ImmutableArray<T>`, `ArraySegment<T>`,
|
||||
`Memory<T>`, `KeyValuePair`, tuples, `Lazy<T>`, a class or struct without
|
||||
`[MessagePackObject]`, a `[DataContract]` type, a string-keyed type, a type or member
|
||||
declaring `[MessagePackFormatter]`, and a non-public member of a type without
|
||||
`AllowPrivate`. The members read are an integer `[Key]` with no `[IgnoreMember]`: a
|
||||
public field or a property whose getter is public, or any visibility up the base chain
|
||||
under the most derived `[MessagePackObject(AllowPrivate = true)]` declaration. An id member
|
||||
needs a setter MessagePack calls (public or `init`; any under `AllowPrivate`), so the id
|
||||
MessagePack writes is read back. Still refused: a bad id member (missing, duplicate, not
|
||||
a string, no integer `[Key]`, `[MessagePackFormatter]` on it, one MessagePack does not
|
||||
write or cannot assign, a key shared with another member, a bad derived source), a
|
||||
concrete union arm that is not keyed, a keyed type that is also a collection
|
||||
(`ICollection<T>`, `IList`, `IDictionary`: MessagePack writes it as a collection, so C#
|
||||
refuses it at the first encode or decode when it is on the path to an object list), a
|
||||
type MessagePack has no formatter for, and a formatter resolver that claims a type on the
|
||||
path. A formatter MessagePack's source generator emits is not refused.
|
||||
|
||||
## Canonical Semantic Schema Hash
|
||||
|
||||
|
|
|
|||
|
|
@ -300,17 +300,19 @@ exclusive open of the same lock file exclude each other is not established.
|
|||
Those are written by their `[Key]` layout so each element's id reaches disk, and the
|
||||
codec refuses the document on first encode or decode, naming the resolver and the
|
||||
type. The formatter `StandardResolver` or `StandardResolverAllowPrivate` hands out for a
|
||||
type does not claim it (registration refuses a non-public `[Key]` member on the way to
|
||||
an object list, so the private-member formatter writes every id). A value list (`List<float2>`, a list of a non-keyed class) is not on the
|
||||
type does not claim it: the extra non-public members the `AllowPrivate` formatter writes
|
||||
are whole values the walk does not own. A type or member declaring
|
||||
`[MessagePackFormatter]` is a whole value off the path, the supported way to give a type its
|
||||
own formatter. A value list (`List<float2>`, a list of a non-keyed class) is not on the
|
||||
path, so a resolver may claim it.
|
||||
- The codec also checks, for every type on that path, that `StandardResolver` does not write it
|
||||
against the walk. A type read by key may not be written by the generic collection resolver
|
||||
(a `[MessagePackObject]` class that is also an `ICollection<T>` or dictionary), and a type
|
||||
read as a collection may not be written by the object resolver (a `[DataContract]`
|
||||
enumerable); either is refused on first encode or decode, naming the type. Any other
|
||||
formatter, such as one MessagePack's source generator emits, passes: the walk decides which
|
||||
types carry ids. It also refuses, typed, a type MessagePack cannot build a formatter for, and a
|
||||
resolver that throws while asked about one.
|
||||
(a `[MessagePackObject]` class that is also an `ICollection<T>` or dictionary); it is
|
||||
refused on first encode or decode, naming the type. Any other formatter, such as one
|
||||
MessagePack's source generator emits, passes: the walk decides which types carry ids and
|
||||
the door only verifies. It also refuses, typed, a type MessagePack cannot build a formatter
|
||||
for (it throws, or `StandardResolver` returns none), and a resolver that throws while asked
|
||||
about one.
|
||||
- Deserialization runs under untrusted-data security. `CultRecordRef<T>` is a
|
||||
permitted dictionary key, compared by its key string.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue