fix(okf): tolerate embedded-NUL cross-link target — skip, never raise
navigate_bundle's out-of-bundle filter drops every '/'- and '\'-bearing target before resolution, but a NUL-byte target carries no path separator and slipped through to (bundle_dir / target).resolve(), which raises ValueError: embedded null character — propagating instead of being skipped. method-spec §72 requires a broken cross-link to be tolerated (skipped, never raised). Wrap resolve/is_file in `except ValueError: continue`; parse_concept_file stays outside the guard so malformed concept files still raise. Load-bearing test drives the first dangerous target THROUGH the filter into resolution; detach-proved RED (ValueError propagates) when the guard is removed. Closes the nullbyte item reported OPEN in OKF trinn E. 442→443 green, golden byte-exact, full gate clean (ruff+format+mypy strict). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
This commit is contained in:
parent
9bae4fb563
commit
613b00f882
2 changed files with 27 additions and 2 deletions
|
|
@ -112,8 +112,15 @@ def navigate_bundle(bundle_dir: Path) -> list[ConceptFile]:
|
||||||
if "/" in target or "\\" in target or target in seen:
|
if "/" in target or "\\" in target or target in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(target)
|
seen.add(target)
|
||||||
resolved = (bundle_dir / target).resolve()
|
try:
|
||||||
if not resolved.is_relative_to(bundle_root) or not resolved.is_file():
|
resolved = (bundle_dir / target).resolve()
|
||||||
|
if not resolved.is_relative_to(bundle_root) or not resolved.is_file():
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
# An unrepresentable target (e.g. an embedded NUL byte, which carries no
|
||||||
|
# path separator and so slips past the out-of-bundle filter) makes
|
||||||
|
# ``resolve``/``is_file`` raise ``ValueError``. That is a broken link, not
|
||||||
|
# a fatal error — skip it, never raise (method-spec §72, OKF robustness).
|
||||||
continue
|
continue
|
||||||
concepts.append(parse_concept_file(resolved))
|
concepts.append(parse_concept_file(resolved))
|
||||||
return concepts
|
return concepts
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,24 @@ class TestNavigation:
|
||||||
names = [c.path.name for c in navigate_bundle(bundle)]
|
names = [c.path.name for c in navigate_bundle(bundle)]
|
||||||
assert names == ["index.md", "a.md"]
|
assert names == ["index.md", "a.md"]
|
||||||
|
|
||||||
|
def test_null_byte_target_is_tolerated_never_raised(self, tmp_path: Path) -> None:
|
||||||
|
# LOAD-BEARING (§11, method-spec §72: a broken cross-link MUST be "skipped,
|
||||||
|
# never raised"). A NUL-byte target carries no path separator, so it slips
|
||||||
|
# past the out-of-bundle filter and reaches resolution, where
|
||||||
|
# ``(bundle_dir / target).resolve()`` raises ``ValueError: embedded null
|
||||||
|
# byte``. That ValueError MUST be caught and the link skipped — a NUL target
|
||||||
|
# is a broken link, not a fatal error. This is the FIRST test that drives a
|
||||||
|
# dangerous target THROUGH the filter into resolution (the ``/``-filter
|
||||||
|
# short-circuits every ``/``-bearing and scheme-bearing case before it).
|
||||||
|
# RED (ValueError propagates) if the resolve guard is detached.
|
||||||
|
bundle = _make_bundle(
|
||||||
|
tmp_path,
|
||||||
|
"Links: [bad](ba\x00d.md), [ok](a.md).",
|
||||||
|
{"a.md": "---\ntype: project\ntitle: A\n---\nBody A."},
|
||||||
|
)
|
||||||
|
names = [c.path.name for c in navigate_bundle(bundle)]
|
||||||
|
assert names == ["index.md", "a.md"]
|
||||||
|
|
||||||
def test_shared_bundle_navigates_all_linked_concepts(self) -> None:
|
def test_shared_bundle_navigates_all_linked_concepts(self) -> None:
|
||||||
# Integration on the shared example bundle: every index-linked file is
|
# Integration on the shared example bundle: every index-linked file is
|
||||||
# reached exactly once; the out-of-bundle ../../README.md link is skipped.
|
# reached exactly once; the out-of-bundle ../../README.md link is skipped.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue