Skip to content

scripts/repo_shape.py: the double-quoted reader that #247 adds (fix/adopt-plan-linebreak-names, not yet on main) resolves \x00 inside a block-scalar KEY, and _parse_mapping splits the NUL-joined sentinel record, so "a\x00b": | reads as the key a with the body b<NUL>text where main reads the literal key a\x00b #248

Closed

Description

@brettheap

Lane: openreposhape-5 (openRepoShape-5)

Found by this lane's adversarial review of #199's fix as N4, and left out of that fix on purpose (PR #247's body says "Filed after this PR opens"). It exists only on #199's branch until #247 lands: fix/adopt-plan-linebreak-names at a02240a (the head of #247). On main 54aa69a the reader does not resolve \x00 in a key, so the escaped form below reads as the literal text. Latent: no writer in this repository emits such a key (see Reach), so this is a reader that mis-parses a document it was just taught to read, not a live failure.

What is wrong. #247 makes the reader resolve \x00, \u0000 and \U00000000 inside a double-quoted scalar, a key included (_unquote_key :244 -> _scalar :356 -> _unescape :328). A block scalar (key: | or key: >) is kept as ONE _Rec whose text is _BLOCK_SENTINEL + key + "\x00" + body (scripts/repo_shape.py:618), and _parse_mapping takes it apart with rec.text.split("\x00", 3) (:673). A key that now holds a NUL adds a fourth field, so the key is cut short at the NUL and the rest of it lands in the body. The review found the comment on _BLOCK_SENTINEL saying it "cannot collide with a real key" (d3023fb); the fix round rewrote it (:175-183) to name the collision as a known limit and left the reader unchanged.

SHAPE=<openRepoShape checkout of fix/adopt-plan-linebreak-names at a02240a, then of main>
SHAPE=$SHAPE python3 - <<'PY'
import os, sys
sys.path.insert(0, os.environ["SHAPE"] + "/scripts")
from repo_shape import parse_yaml
for text in ('"a\\x00b": |\n  text\n',       # the key a, NUL, b, written as an escape
             '"a\\u0000b": >\n  text\n',
             '"a\\x00b": text\n',             # the same key with a plain value: the control
             'a\x00b: |\n  text\n'):          # a RAW NUL in the key: YAML forbids it
    print(f"{text!r:30} ->", repr(parse_yaml(text)))
PY

Observed on a02240a (the branch):

'"a\\x00b": |\n  text\n'       -> {'a': 'b\x00text\n'}
'"a\\u0000b": >\n  text\n'     -> {'a': 'b\x00text\n'}
'"a\\x00b": text\n'            -> {'a\x00b': 'text'}
'a\x00b: |\n  text\n'          -> {'a': 'b\x00text\n'}

Observed on main 54aa69a:

'"a\\x00b": |\n  text\n'       -> {'a\\x00b': 'text\n'}
'"a\\u0000b": >\n  text\n'     -> {'a\\u0000b': 'text\n'}
'"a\\x00b": text\n'            -> {'a\\x00b': 'text'}
'a\x00b: |\n  text\n'          -> {'a': 'b\x00text\n'}

Row 1 is the report: the key is a, the value is b<NUL>text, and nothing refuses it. Row 3 is the same key with a value that is not a block scalar, which reads right, so the fault is in the sentinel record and not in _unescape. Row 4, a raw NUL in the key, is the older half of the same defect: main misreads it too, because YAML forbids a raw NUL and the reader does not refuse one. The escape form is what #247 adds.

Expected. "a\x00b": | reads as {'a\x00b': 'text\n'}, the key its non-block control reads, or is refused by name. The block-scalar record carries its key and its body as separate _Rec fields (or a tuple) instead of NUL-joined text, so no key can collide with it, and the _BLOCK_SENTINEL comment says so again. A test pins the four rows above.

Reach. None of the repository's writers emits a block scalar or a key built from data: the plan writer writes no block scalar and every key it writes is a literal (emit, adopt-project.py:251, whose one variable key, at :945, comes from summary_of's fixed files, bytes, file_share and byte_share; Entry.write; render_plan; the leg names). The review read shape-doctor.py's writers the same way at d3023fb. So it shows only on a file a person edits or another tool writes, a double-quoted key holding \x00, \u0000 or \U00000000 above a | or > value. Every YAML file every tool reads goes through this reader, not only plans.

Found by. The review of #199's fix, N4: handoffs/openRepoShape/attachments/lane-openRepoShape-5/review199-report.md in opensoft/brett-wip (reproduction: review199-tools/unescape_probe.py, line BLOCK-KEY-NUL). Reproduced again on a02240a for this issue.

Not claimed.

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 11, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-5, session e62c2fd7-176d-429d-89fb-0653dd08ad5f@Eagle, 2026-10-11T21:44:33Z, for #248

    Logged in lanes/log/openRepoShape-5.md at 8209b4ee1066db30309d7eb881f2c512ab896132.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#248:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#248"`, kept where the row or its body names `#248`:
    247	adopt-project.py: the plan writer escapes every character that is not printable from the one table `repo_shape`'s reader resolves, so a source holding the macOS `Icon\\r` no longer writes a plan `check` refuses, and a non-UTF-8 name no longer empties the plan (#199)	fix/adopt-plan-linebreak-names	MERGED
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `248` as a whole token:
       none
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions