mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
utils/yaml_bootstrap.py promises that load_block(text) either equals
yaml.safe_load(text) or raises BootstrapYamlError. A fuzzer comparing
both found two inputs that broke the promise:
- str.splitlines() also breaks lines at \x0b, \x0c and \x1c to \x1e,
which PyYAML's reader rejects as non-printable. "\x1e/: R" came back
as {"/": "R"}, and a non-printable character inside a comment line was
skipped with the comment.
- An indented " ---" on line 1 was taken as the document marker, while
PyYAML reads it as a plain scalar and rejects the mapping that follows.
load_block now refuses any character outside PyYAML's own printable
class before it splits lines, and takes "---" as the document marker
only at column 0. The line breaks splitlines() still honours (\r, \r\n,
NEL, LS, PS) are line breaks for PyYAML too, so splitting stays as is.
The eight inputs join STRUCTURES in the divergence test; against the
previous reader, four of them diverge and fail it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
136 lines
5.3 KiB
Python
136 lines
5.3 KiB
Python
"""Stdlib reader for the block-YAML subset the bootstrap SPOT files use.
|
|
|
|
This is a deliberate second YAML touchpoint outside ``utils.cache.yaml``:
|
|
the modules that read the distro SPOT must import on the bare bootstrap
|
|
python, which runs the ``.env`` generator before any dependency is
|
|
installed, so they cannot reach the PyYAML-backed cache.
|
|
|
|
Supported: nested block mappings, block sequences of scalars, plain and
|
|
double-quoted string scalars, full-line and trailing comments, a single
|
|
leading document marker. Every other construct raises
|
|
:class:`BootstrapYamlError`, so ``load_block(text)`` either equals
|
|
``yaml.safe_load(text)`` or fails loud.
|
|
|
|
The plain-scalar rule is deliberately narrower than YAML's: a plain scalar
|
|
must start with a letter, an underscore or a slash, and may then carry only
|
|
``[A-Za-z0-9_./+@:-]`` and spaces. That excludes every leading character
|
|
PyYAML's implicit resolvers key off, so no accepted plain scalar can come
|
|
back as an int, float, bool, timestamp or null.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
_PLAIN = re.compile(r"^[A-Za-z_/][A-Za-z0-9_./+@:\- ]*$")
|
|
_RESERVED = re.compile(r"^(?:true|false|yes|no|on|off|y|n|null)$", re.IGNORECASE)
|
|
_NON_PRINTABLE = re.compile(
|
|
"[^\x09\x0a\x0d\x20-\x7e\x85\xa0-\U0000d7ff\U0000e000-\U0000fffd\U00010000-\U0010ffff]"
|
|
)
|
|
|
|
|
|
class BootstrapYamlError(ValueError):
|
|
"""Raised for a construct outside the supported block subset."""
|
|
|
|
|
|
def _plain(text: str, lineno: int) -> str:
|
|
if not _PLAIN.match(text) or ": " in text or text.endswith(":"):
|
|
raise BootstrapYamlError(f"line {lineno}: unsupported plain scalar {text!r}")
|
|
if _RESERVED.match(text):
|
|
raise BootstrapYamlError(f"line {lineno}: {text!r} is not a string, quote it")
|
|
return text
|
|
|
|
|
|
def _scalar(raw: str, lineno: int) -> str:
|
|
if raw.startswith('"'):
|
|
end = raw.find('"', 1)
|
|
if end < 0 or "\\" in raw[1:end]:
|
|
raise BootstrapYamlError(
|
|
f"line {lineno}: unsupported quoted scalar {raw!r}"
|
|
)
|
|
trailer = raw[end + 1 :].strip()
|
|
if trailer and not trailer.startswith("#"):
|
|
raise BootstrapYamlError(f"line {lineno}: trailing text after {raw!r}")
|
|
return raw[1:end]
|
|
return _plain(raw.split(" #", 1)[0].strip(), lineno)
|
|
|
|
|
|
def load_block(text: str) -> dict:
|
|
"""Parse *text* as a block mapping and return it.
|
|
|
|
Args:
|
|
text: YAML source restricted to the supported block subset.
|
|
|
|
Returns:
|
|
The root mapping, with every scalar as a ``str``.
|
|
|
|
Raises:
|
|
BootstrapYamlError: for any construct outside the subset.
|
|
"""
|
|
non_printable = _NON_PRINTABLE.search(text)
|
|
if non_printable:
|
|
raise BootstrapYamlError(
|
|
f"non-printable character {non_printable.group()!r} at offset {non_printable.start()}"
|
|
)
|
|
root: dict = {}
|
|
stack: list[tuple[int, object]] = [(0, root)]
|
|
pending: tuple[dict, str, int] | None = None
|
|
for lineno, line in enumerate(text.splitlines(), 1):
|
|
if "\t" in line:
|
|
raise BootstrapYamlError(f"line {lineno}: tab indentation")
|
|
body = line.strip()
|
|
if not body or body.startswith("#"):
|
|
continue
|
|
if body == "---":
|
|
if lineno != 1:
|
|
raise BootstrapYamlError(f"line {lineno}: second document")
|
|
if line.startswith(" "):
|
|
raise BootstrapYamlError(f"line {lineno}: indented document marker")
|
|
continue
|
|
indent = len(line) - len(line.lstrip(" "))
|
|
if pending is not None:
|
|
parent, key, parent_indent = pending
|
|
if indent <= parent_indent:
|
|
raise BootstrapYamlError(
|
|
f"line {lineno}: {key!r} has neither a value nor a block"
|
|
)
|
|
child: object = [] if body.startswith("- ") else {}
|
|
parent[key] = child
|
|
stack.append((indent, child))
|
|
pending = None
|
|
else:
|
|
while indent < stack[-1][0]:
|
|
stack.pop()
|
|
if indent != stack[-1][0]:
|
|
raise BootstrapYamlError(f"line {lineno}: unexpected indent")
|
|
container = stack[-1][1]
|
|
if body.startswith("- "):
|
|
if not isinstance(container, list):
|
|
raise BootstrapYamlError(f"line {lineno}: sequence entry in a mapping")
|
|
container.append(_scalar(body[2:], lineno))
|
|
continue
|
|
if isinstance(container, list):
|
|
raise BootstrapYamlError(f"line {lineno}: mapping entry in a sequence")
|
|
key, sep, rest = body.partition(": ")
|
|
if not sep:
|
|
if not body.endswith(":"):
|
|
raise BootstrapYamlError(
|
|
f"line {lineno}: not a mapping entry: {body!r}"
|
|
)
|
|
key, rest = body[:-1], ""
|
|
if key != key.strip():
|
|
raise BootstrapYamlError(f"line {lineno}: not a mapping entry: {body!r}")
|
|
_plain(key, lineno)
|
|
if key in container:
|
|
raise BootstrapYamlError(f"line {lineno}: duplicate key {key!r}")
|
|
rest = rest.strip()
|
|
if not rest or rest.startswith("#"):
|
|
pending = (container, key, indent)
|
|
continue
|
|
container[key] = _scalar(rest, lineno)
|
|
if pending is not None:
|
|
raise BootstrapYamlError(f"{pending[1]!r} has neither a value nor a block")
|
|
if not root:
|
|
raise BootstrapYamlError("empty document")
|
|
return root
|