1
0
Fork 0

feat(okf): path/reserved-name validation gate — traversal + index.md/log.md shadow (T4, TDD, +10)

This commit is contained in:
Kjell Tore Guttormsen 2026-07-06 07:40:56 +02:00
commit ec121f3259
2 changed files with 104 additions and 0 deletions

View file

@ -30,8 +30,10 @@ from .report import Report, Source
__all__ = [
"parse_frontmatter",
"scan_concept",
"validate_concept_path",
"OKFError",
"OKFFrontmatterError",
"OKFPathError",
]
_FENCE = "---"
@ -52,6 +54,15 @@ class OKFFrontmatterError(OKFError):
"""Frontmatter violates the strict, reject-by-default OKF subset."""
class OKFPathError(OKFError):
"""A concept path is unsafe (traversal, absolute, or reserved-name shadow)."""
# `index.md` (directory listing) and `log.md` (update history) are reserved by
# the OKF spec and MUST NOT name concept documents — at any directory level.
_RESERVED_BASENAMES = frozenset({"index.md", "log.md"})
def parse_frontmatter(document):
"""Split leading OKF frontmatter from the body and parse it strictly.
@ -110,6 +121,44 @@ def _scannable_regions(frontmatter, body):
return regions
def validate_concept_path(path):
"""Validate a bundle-relative concept path and return its concept-ID.
T4 path / reserved-name gate. The concept-ID is the path with the ``.md``
suffix removed (OKF spec). Rejects, before the path is ever used to write:
- ``..`` traversal at any segment (escape the bundle);
- absolute paths (``/...``) and backslashes (platform-separator ambiguity);
- the reserved basenames ``index.md`` / ``log.md`` (shadow the directory
listing / update log), case-insensitively a case-insensitive filesystem
lets ``Index.md`` shadow ``index.md``;
- non-``.md`` files (not a concept document).
Raises :class:`OKFPathError` on any of these; returns the concept-ID string.
"""
if not path or not isinstance(path, str):
raise OKFPathError("empty or non-string concept path: %r" % (path,))
if path.startswith("/"):
raise OKFPathError("concept path must be bundle-relative, not absolute: %r" % path)
if "\\" in path:
raise OKFPathError("backslashes are not permitted in a concept path: %r" % path)
segments = path.split("/")
for seg in segments:
if seg == "..":
raise OKFPathError("path traversal ('..') is not permitted: %r" % path)
if seg == "" or seg == ".":
raise OKFPathError("malformed path segment in %r" % path)
basename = segments[-1]
if basename.lower() in _RESERVED_BASENAMES:
raise OKFPathError("reserved filename may not name a concept: %r" % basename)
if not basename.lower().endswith(".md"):
raise OKFPathError("a concept document must be a .md file: %r" % path)
return path[: -len(".md")]
def _parse_flat(fm_lines):
result = {}
i = 0