Files
RuView/scripts/readme_visuals_xml.py
T

51 lines
1.8 KiB
Python

"""Bounded, hardened XML parsing for the README visual maintenance tools."""
from pathlib import Path, PurePosixPath, PureWindowsPath
from defusedxml import ElementTree
# Keep the visual verifier's existing exclusive per-asset size limit.
MAX_SVG_BYTES = 20_000
def resolve_repo_file(root: Path, relative: str, *, asset: bool = False) -> Path:
"""Resolve a portable manifest path inside its trusted repository boundary."""
if (
not isinstance(relative, str)
or not relative
or "\\" in relative
or ":" in relative
or "\x00" in relative
or PurePosixPath(relative).is_absolute()
or PureWindowsPath(relative).drive
or ".." in relative.split("/")
):
raise ValueError("Expected a repository-relative path without traversal")
repository = root.resolve()
boundary = repository / "assets" / "readme" if asset else repository
resolved = (repository / relative).resolve()
if not resolved.is_relative_to(boundary):
raise ValueError("Manifest path escapes its allowed repository directory")
if not resolved.is_file():
raise ValueError("Manifest path must name an existing file")
return resolved
def parse_svg(raw: bytes):
"""Reject oversized input and DTD/entity declarations before building a tree."""
if not isinstance(raw, bytes):
raise TypeError("SVG input must be bytes")
if len(raw) >= MAX_SVG_BYTES:
raise ValueError(f"SVG must be smaller than {MAX_SVG_BYTES} bytes")
return ElementTree.fromstring(
raw, forbid_dtd=True, forbid_entities=True, forbid_external=True
)
def read_svg(path: Path):
"""Read at most the rejection boundary, including when a file grows mid-read."""
with path.open("rb") as stream:
raw = stream.read(MAX_SVG_BYTES)
return raw, parse_svg(raw)