mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
51 lines
1.8 KiB
Python
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)
|