2026-07-29 08:23:04 -04:00
|
|
|
"""Internal directory binding helpers for disposable publication paths."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-29 16:03:14 -04:00
|
|
|
import ctypes
|
|
|
|
|
import errno
|
2026-07-29 08:23:04 -04:00
|
|
|
import os
|
2026-07-29 11:37:33 -04:00
|
|
|
import secrets
|
2026-07-29 08:23:04 -04:00
|
|
|
import stat
|
2026-07-29 11:37:33 -04:00
|
|
|
from collections.abc import Callable
|
|
|
|
|
from contextlib import suppress
|
2026-07-29 08:23:04 -04:00
|
|
|
from pathlib import Path
|
2026-07-29 16:03:14 -04:00
|
|
|
from typing import Protocol, cast
|
2026-07-29 08:23:04 -04:00
|
|
|
|
|
|
|
|
from .errors import DocForgeError
|
|
|
|
|
|
2026-07-29 16:03:14 -04:00
|
|
|
RENAME_EXCHANGE = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _RenameAt2(Protocol):
|
|
|
|
|
argtypes: list[object]
|
|
|
|
|
restype: object
|
|
|
|
|
|
|
|
|
|
def __call__(
|
|
|
|
|
self,
|
|
|
|
|
old_directory_fd: int,
|
|
|
|
|
old_name: bytes,
|
|
|
|
|
new_directory_fd: int,
|
|
|
|
|
new_name: bytes,
|
|
|
|
|
flags: int,
|
|
|
|
|
/,
|
|
|
|
|
) -> int: ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rename_exchange_at(directory_fd: int, first: str, second: str) -> None:
|
|
|
|
|
"""Atomically exchange two names inside one already bound directory."""
|
|
|
|
|
|
|
|
|
|
library = ctypes.CDLL(None, use_errno=True)
|
|
|
|
|
try:
|
|
|
|
|
rename_at2 = cast(_RenameAt2, library.renameat2)
|
|
|
|
|
except AttributeError as error:
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"atomic_exchange_unavailable",
|
|
|
|
|
"Atomic exchange is unavailable on this platform",
|
|
|
|
|
) from error
|
|
|
|
|
rename_at2.argtypes = [
|
|
|
|
|
ctypes.c_int,
|
|
|
|
|
ctypes.c_char_p,
|
|
|
|
|
ctypes.c_int,
|
|
|
|
|
ctypes.c_char_p,
|
|
|
|
|
ctypes.c_uint,
|
|
|
|
|
]
|
|
|
|
|
rename_at2.restype = ctypes.c_int
|
|
|
|
|
ctypes.set_errno(0)
|
|
|
|
|
result = rename_at2(
|
|
|
|
|
directory_fd,
|
|
|
|
|
os.fsencode(first),
|
|
|
|
|
directory_fd,
|
|
|
|
|
os.fsencode(second),
|
|
|
|
|
RENAME_EXCHANGE,
|
|
|
|
|
)
|
|
|
|
|
if result == 0:
|
|
|
|
|
return
|
|
|
|
|
error_number = ctypes.get_errno()
|
|
|
|
|
if error_number in {errno.ENOSYS, errno.EINVAL, errno.EOPNOTSUPP}:
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"atomic_exchange_unavailable",
|
|
|
|
|
"Atomic exchange is unavailable on this filesystem",
|
|
|
|
|
)
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"publication_failure",
|
|
|
|
|
"Could not exchange atomic publication paths",
|
|
|
|
|
error_number=error_number,
|
|
|
|
|
) from OSError(error_number, os.strerror(error_number))
|
|
|
|
|
|
2026-07-29 08:23:04 -04:00
|
|
|
|
|
|
|
|
def open_bound_directory(path: Path) -> int:
|
|
|
|
|
"""Open one real directory and bind its current inode for later operations."""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
path_status = path.lstat()
|
|
|
|
|
if (
|
|
|
|
|
stat.S_ISLNK(path_status.st_mode)
|
|
|
|
|
or not stat.S_ISDIR(path_status.st_mode)
|
|
|
|
|
or path.resolve(strict=True) != path
|
|
|
|
|
):
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"path_escape",
|
|
|
|
|
"Derived cache root is not a safe real directory",
|
|
|
|
|
)
|
|
|
|
|
directory_fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
|
|
|
except FileNotFoundError as error:
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"missing_index",
|
|
|
|
|
"Derived cache root does not exist",
|
|
|
|
|
) from error
|
|
|
|
|
except OSError as error:
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"path_escape",
|
|
|
|
|
"Derived cache root cannot be opened safely",
|
|
|
|
|
) from error
|
|
|
|
|
try:
|
|
|
|
|
opened_status = os.fstat(directory_fd)
|
|
|
|
|
if opened_status.st_dev != path_status.st_dev or opened_status.st_ino != path_status.st_ino:
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"path_escape",
|
|
|
|
|
"Derived cache root changed while opening",
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
os.close(directory_fd)
|
|
|
|
|
raise
|
|
|
|
|
return directory_fd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_bound_directory(path: Path, directory_fd: int) -> None:
|
|
|
|
|
"""Require a path to still name the exact opened real directory."""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
path_status = path.lstat()
|
|
|
|
|
opened_status = os.fstat(directory_fd)
|
|
|
|
|
if (
|
|
|
|
|
stat.S_ISLNK(path_status.st_mode)
|
|
|
|
|
or not stat.S_ISDIR(path_status.st_mode)
|
|
|
|
|
or path.resolve(strict=True) != path
|
|
|
|
|
or opened_status.st_dev != path_status.st_dev
|
|
|
|
|
or opened_status.st_ino != path_status.st_ino
|
|
|
|
|
):
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"path_escape",
|
|
|
|
|
"Derived cache root changed during publication",
|
|
|
|
|
)
|
|
|
|
|
except FileNotFoundError as error:
|
|
|
|
|
raise DocForgeError(
|
|
|
|
|
"path_escape",
|
|
|
|
|
"Derived cache root disappeared during publication",
|
|
|
|
|
) from error
|
2026-07-29 11:37:33 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|