numos/state.py
back to source
"""Render and parse the line-oriented numos.state format."""
from numos.canonical import content_hash
class StateError(Exception):
"""Raised when a state file is malformed or fails hash verification."""
def _csv(values):
return ",".join(values) if values else "-"
def _uncsv(field):
return [] if field == "-" else field.split(",")
def _opt(value):
return value if value else "-"
def _unopt(field):
return None if field == "-" else field
def render(state):
"""Serialize a state dict to canonical text with the C line stamped."""
lines = ["V %d" % state["version"]]
lines += sorted(
"A %s %s %s %s %s" % (a["canonical"], _csv(a["aliases"]),
_opt(a["zig_triple"]), a["busybox_variant"],
_opt(a["artifact_sha256"]))
for a in state["arch_targets"]
)
lines += sorted(
"P %d %s %s %s" % (p["ordinal"], p["name"], p["on_failure"],
_csv(p["required_units"]))
for p in state["boot_phases"]
)
lines += sorted(
"U %s %s %s %d %d %s %s %s %s %s" % (
u["name"], u["kind"], u["restart"], u["backoff_ms"], u["backoff_max_ms"],
_csv(u["arch_mask"]), _opt(u["health_probe"]),
_csv(u["requires"]), _csv(u["after"]), u["exec"])
for u in state["units"]
)
lines += sorted(
"X %s %d %d %s %s %s" % (h["name"], h["interval_s"], h["threshold"],
h["local_action"], _opt(h["on_fire"]), h["probe"])
for h in state["health"]
)
stamped = ["V %d" % state["version"], "C " + content_hash(lines)] + lines[1:]
return "".join(ln + "\n" for ln in stamped)
def parse(text):
"""Parse canonical text back into a state dict. Ignores the C line."""
state = {"version": None, "arch_targets": [], "boot_phases": [],
"units": [], "health": []}
for raw in text.split("\n"):
if not raw:
continue
tag, _, rest = raw.partition(" ")
if tag == "V":
state["version"] = int(rest)
elif tag == "C":
continue
elif tag == "A":
f = rest.split(" ", 4)
state["arch_targets"].append({
"canonical": f[0], "aliases": _uncsv(f[1]),
"zig_triple": _unopt(f[2]), "busybox_variant": f[3],
"artifact_sha256": _unopt(f[4])})
elif tag == "P":
f = rest.split(" ", 3)
state["boot_phases"].append({
"ordinal": int(f[0]), "name": f[1], "on_failure": f[2],
"required_units": _uncsv(f[3])})
elif tag == "U":
f = rest.split(" ", 9)
state["units"].append({
"name": f[0], "kind": f[1], "restart": f[2],
"backoff_ms": int(f[3]), "backoff_max_ms": int(f[4]),
"arch_mask": _uncsv(f[5]), "health_probe": _unopt(f[6]),
"requires": _uncsv(f[7]), "after": _uncsv(f[8]), "exec": f[9]})
elif tag == "X":
f = rest.split(" ", 5)
state["health"].append({
"name": f[0], "interval_s": int(f[1]), "threshold": int(f[2]),
"local_action": f[3], "on_fire": _unopt(f[4]), "probe": f[5]})
else:
raise StateError("unknown record tag: %r" % tag)
if state["version"] is None:
raise StateError("missing V record")
return state
HEX = "0123456789abcdef"
def _file_lines(text):
"""Split text the way the shell verifier's `grep` sees the file.
Exactly one trailing newline terminates the last line; anything past it
is a real (blank) line that grep would emit and hash. Blank lines are
NOT dropped: boot/bootstrap.sh keeps them in the hashed body, and a
build machine must not certify a file the target would refuse.
"""
lines = text.split("\n")
if lines and lines[-1] == "":
lines.pop()
return lines
def verify(text):
"""Raise StateError unless the stamped C hash matches the body."""
lines = _file_lines(text)
stamped = [ln for ln in lines if ln.startswith("C ")]
if len(stamped) != 1:
raise StateError("expected exactly one C record, found %d" % len(stamped))
want = stamped[0][2:]
# The hash is the whole remainder of the line, and it must be exactly
# 64 lowercase hex characters -- boot/bootstrap.sh applies the identical
# rule, so `C <valid-hash> ANYTHING` is rejected by both verifiers.
if len(want) != 64 or any(c not in HEX for c in want):
raise StateError("C record is not a 64-character lowercase hex sha256: %r" % want)
got = content_hash(lines)
if want != got:
raise StateError("content hash mismatch: stamped %s computed %s" % (want, got))