Pin the reader against a trimmed trailing dot on the root and the local

Soul's X1 and X11 survived the second fix batch: a reader that trimmed a
trailing dot from the root or from the local before its dotted_text check
passed every test, because the only fixtures with a trailing dot went
through the writer, which never composes one. Four malformed ids now
read through pipeline_id in keys_read_back_as_ids_of_their_kind and are
refused: a trailing dot on the root, a trailing dot, a leading dot and an
empty part on the local. X1 and X11 join the Cut 6b suite as entries,
each killed by that test.

The resolution arm's comment named "the 64-byte bound in local"; the
bound is LOCAL_MAX and the comment now says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-16 15:28:23 +02:00
commit 7cd1a38bc8
2 changed files with 37 additions and 4 deletions

View file

@ -648,8 +648,8 @@ pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefus
// ordinary id, so it composes as a subject like any other; each nesting
// prepends `resolution.` (11 bytes), so a chain `n` deep over a
// depth-one local of `L` bytes composes `11 * (n - 1) + L` bytes
// against the 64-byte bound in `local`, which is the only depth limit
// and needs no guard.
// against `LOCAL_MAX` in `local`, which is the only depth limit and
// needs no guard.
D::Resolution(value) => {
let field = "resolution.subject.id";
let (subject_root, subject_local) = pipeline_id(field, &value.subject.id.0, value.subject.kind)?;
@ -1307,6 +1307,18 @@ mod tests {
"a campaign key is not an instance id"
);
// Soul X1 and X11: the reader's root and local are each `dotted_text`
// whole, so a trailing dot on either is refused by the reader itself,
// not only by the writer that never composes one. A leading dot and
// an empty part are the same rule on the local.
for malformed in ["c.:target:x", "c:target:x.", "c:target:.x", "c:target:a..b"] {
let read = pipeline_id("read_back", malformed, PipelineKind::Target);
assert!(
matches!(&read, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "read_back"),
"{malformed:?} is not an id of any kind, got {read:?}"
);
}
// The instance root's own key segment is validated, not merely bounded.
// Nothing else stands between a `Slug` and a key: a space would compose
// a store key carrying a byte no `Label` may hold.

View file

@ -12,8 +12,11 @@
# pass: N7 and N8 re-anchor Cut 6's M12 and M13 on `key_segment`, N3 is the
# analogue of Cut 6's M5 on `local`, N6 of Cut 6's M15 on the reader, and N9
# and N10 are the dotted-root mutations that survived until
# `dotted_roots_key_and_read_back`. The file holds an `é` literal in
# `bounds_refuse_in_utf8_bytes`, which is why the harness I/O is byte-exact.
# `dotted_roots_key_and_read_back`. X1 and X11 are Soul's third pass, the
# reader trimming a trailing dot from the root or the local, killed by the
# malformed fixtures in `keys_read_back_as_ids_of_their_kind`. The file holds
# an `é` literal in `bounds_refuse_in_utf8_bytes`, which is why the harness
# I/O is byte-exact.
#
# Soul's N4 moved the bound in `local` and both 64s in the depth test to 60
# together and survived: the test restated the literal. `LOCAL_MAX` now names
@ -169,6 +172,24 @@
Test = 'tests::dotted_roots_key_and_read_back'
Old = ' dotted_text(field, root)?;'
New = ' label_text(field, root)?;'
},
# Soul X1 and X11, the reader's halves of S4: a trailing dot on the
# root or on the local was refused only by the writer, which never
# composes one, so a reader that trimmed it survived. The malformed
# fixtures read through `pipeline_id` kill both.
@{
Id = 'X1'
Rule = 'The reader''s root check refuses a trailing dot; it does not trim one.'
Test = 'tests::keys_read_back_as_ids_of_their_kind'
Old = ' dotted_text(field, root)?;'
New = ' dotted_text(field, root.trim_end_matches(''.''))?;'
},
@{
Id = 'X11'
Rule = 'The reader''s local check refuses a trailing dot; it does not trim one.'
Test = 'tests::keys_read_back_as_ids_of_their_kind'
Old = ' dotted_text(field, local)?;'
New = ' dotted_text(field, local.trim_end_matches(''.''))?;'
}
)
}