Add durable portable graph publication
This commit is contained in:
parent
96e3965855
commit
1134c2d375
19 changed files with 2542 additions and 13 deletions
|
|
@ -3,7 +3,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import stat
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
from .errors import DocForgeError
|
||||
|
|
@ -69,3 +72,177 @@ def require_bound_directory(path: Path, directory_fd: int) -> None:
|
|||
"path_escape",
|
||||
"Derived cache root disappeared during publication",
|
||||
) from error
|
||||
|
||||
|
||||
def open_confined_directory(root: Path, path: Path, *, create: bool) -> int:
|
||||
"""Open a descendant directory through stable no-follow directory descriptors."""
|
||||
|
||||
try:
|
||||
unsafe = (
|
||||
root.is_symlink()
|
||||
or root.resolve(strict=True) != root
|
||||
or not path.is_relative_to(root)
|
||||
or path == root
|
||||
)
|
||||
except OSError as error:
|
||||
raise DocForgeError("path_escape", "Project root cannot be resolved safely") from error
|
||||
if unsafe:
|
||||
raise DocForgeError("path_escape", "Derived output directory is not confined")
|
||||
relative = path.relative_to(root)
|
||||
try:
|
||||
descriptor = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
||||
except OSError as error:
|
||||
raise DocForgeError("path_escape", "Project root cannot be opened safely") from error
|
||||
try:
|
||||
for part in relative.parts:
|
||||
if part in {"", ".", ".."}:
|
||||
raise DocForgeError("path_escape", "Derived output directory is not confined")
|
||||
if create:
|
||||
try:
|
||||
os.mkdir(part, mode=0o700, dir_fd=descriptor)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as error:
|
||||
raise DocForgeError(
|
||||
"publication_failure",
|
||||
"Derived output directory could not be created",
|
||||
) from error
|
||||
try:
|
||||
next_descriptor = os.open(
|
||||
part,
|
||||
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||
dir_fd=descriptor,
|
||||
)
|
||||
except OSError as error:
|
||||
raise DocForgeError(
|
||||
"path_escape",
|
||||
"Derived output directory is missing or unsafe",
|
||||
) from error
|
||||
os.close(descriptor)
|
||||
descriptor = next_descriptor
|
||||
require_bound_directory(path, descriptor)
|
||||
return descriptor
|
||||
except Exception:
|
||||
os.close(descriptor)
|
||||
raise
|
||||
|
||||
|
||||
def safe_file_identity_at(
|
||||
directory: Path,
|
||||
directory_fd: int,
|
||||
name: str,
|
||||
) -> dict[str, object] | None:
|
||||
"""Return one no-follow regular-file identity relative to a bound directory."""
|
||||
|
||||
del directory
|
||||
if not name or "/" in name or name in {".", ".."}:
|
||||
raise DocForgeError("path_escape", "Derived artifact name is unsafe")
|
||||
try:
|
||||
current = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as error:
|
||||
raise DocForgeError("path_escape", "Derived artifact cannot be inspected") from error
|
||||
if not stat.S_ISREG(current.st_mode):
|
||||
raise DocForgeError("path_escape", "Derived artifact is not a safe regular file")
|
||||
return {
|
||||
"path": name,
|
||||
"device": current.st_dev,
|
||||
"inode": current.st_ino,
|
||||
"mode": current.st_mode,
|
||||
"size": current.st_size,
|
||||
"mtime_ns": current.st_mtime_ns,
|
||||
"ctime_ns": current.st_ctime_ns,
|
||||
}
|
||||
|
||||
|
||||
def read_bounded_file_at(
|
||||
directory_fd: int,
|
||||
name: str,
|
||||
maximum_bytes: int,
|
||||
) -> bytes | None:
|
||||
"""Read one regular file through a bound directory without following links."""
|
||||
|
||||
try:
|
||||
descriptor = os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=directory_fd)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as error:
|
||||
raise DocForgeError("path_escape", "Derived artifact cannot be opened safely") from error
|
||||
with os.fdopen(descriptor, "rb") as handle:
|
||||
current = os.fstat(handle.fileno())
|
||||
if not stat.S_ISREG(current.st_mode) or current.st_size > maximum_bytes:
|
||||
raise DocForgeError("invalid_projection", "Derived artifact is invalid or oversized")
|
||||
content = handle.read(maximum_bytes + 1)
|
||||
if len(content) > maximum_bytes:
|
||||
raise DocForgeError("invalid_projection", "Derived artifact is oversized")
|
||||
return content
|
||||
|
||||
|
||||
def atomic_replace_bytes_at(
|
||||
path: Path,
|
||||
directory_fd: int,
|
||||
name: str,
|
||||
content: bytes,
|
||||
*,
|
||||
verify: Callable[[], None],
|
||||
) -> dict[str, object]:
|
||||
"""Durably replace one file inside an already bound directory."""
|
||||
|
||||
if not name or "/" in name or name in {".", ".."}:
|
||||
raise DocForgeError("path_escape", "Derived artifact name is unsafe")
|
||||
existing = safe_file_identity_at(path, directory_fd, name)
|
||||
del existing
|
||||
temporary = f".docforge-projection-{secrets.token_hex(12)}"
|
||||
descriptor: int | None = None
|
||||
committed = False
|
||||
try:
|
||||
descriptor = os.open(
|
||||
temporary,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
||||
0o600,
|
||||
dir_fd=directory_fd,
|
||||
)
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
descriptor = None
|
||||
handle.write(content)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
verify()
|
||||
require_bound_directory(path, directory_fd)
|
||||
os.replace(
|
||||
temporary,
|
||||
name,
|
||||
src_dir_fd=directory_fd,
|
||||
dst_dir_fd=directory_fd,
|
||||
)
|
||||
committed = True
|
||||
os.fsync(directory_fd)
|
||||
identity = safe_file_identity_at(path, directory_fd, name)
|
||||
if identity is None:
|
||||
raise DocForgeError(
|
||||
"publication_failure",
|
||||
"Derived artifact disappeared after publication",
|
||||
mutation_committed=True,
|
||||
)
|
||||
return identity
|
||||
except DocForgeError as error:
|
||||
if committed:
|
||||
raise DocForgeError(
|
||||
"publication_failure",
|
||||
"Derived artifact was replaced but final publication verification failed",
|
||||
mutation_committed=True,
|
||||
cause=error.code,
|
||||
) from error
|
||||
raise
|
||||
except OSError as error:
|
||||
raise DocForgeError(
|
||||
"publication_failure",
|
||||
"Derived artifact publication failed",
|
||||
mutation_committed=committed,
|
||||
) from error
|
||||
finally:
|
||||
if descriptor is not None:
|
||||
os.close(descriptor)
|
||||
with suppress(OSError):
|
||||
os.unlink(temporary, dir_fd=directory_fd)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue