_managed_files owns the file snapshot reader and public metadata helpers used by pinned directories, durable copies, and opaque snapshots; transaction staging owns read_target and same_identity_and_contents; dead helpers removed; hook_template no longer re-exports the guard generator; mypy targets win32 so _windows_security type-checks. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
450 lines
16 KiB
Python
450 lines
16 KiB
Python
"""Pinned physical directory handles for transactional file operations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import os
|
|
import stat
|
|
from typing import Any, Optional, Tuple
|
|
|
|
from ._managed_files import (
|
|
DirectorySnapshot,
|
|
FileSnapshot,
|
|
read_file_snapshot,
|
|
read_physical_directory,
|
|
)
|
|
|
|
|
|
_FILE_ATTRIBUTE_DIRECTORY = 0x00000010
|
|
_FILE_ATTRIBUTE_NORMAL = 0x00000080
|
|
_FILE_ATTRIBUTE_READONLY = 0x00000001
|
|
_FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
|
|
|
|
|
|
def _close_windows_handle(handle: int) -> None:
|
|
import ctypes as ctypes_module
|
|
from ctypes import wintypes
|
|
|
|
ctypes: Any = ctypes_module
|
|
|
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
close_handle = kernel32.CloseHandle
|
|
close_handle.argtypes = [wintypes.HANDLE]
|
|
close_handle.restype = wintypes.BOOL
|
|
if not close_handle(handle):
|
|
error = ctypes.get_last_error()
|
|
raise OSError(f"Close Windows handle: {ctypes.WinError(error)}")
|
|
|
|
|
|
def _open_windows_directory(
|
|
directory: str, label: str, delete_access: bool = False
|
|
) -> Tuple[int, DirectorySnapshot]:
|
|
import ctypes as ctypes_module
|
|
from ctypes import wintypes
|
|
import msvcrt as msvcrt_module
|
|
|
|
ctypes: Any = ctypes_module
|
|
msvcrt: Any = msvcrt_module
|
|
|
|
class ByHandleFileInformation(ctypes.Structure):
|
|
_fields_ = [
|
|
("file_attributes", wintypes.DWORD),
|
|
("creation_time", wintypes.FILETIME),
|
|
("last_access_time", wintypes.FILETIME),
|
|
("last_write_time", wintypes.FILETIME),
|
|
("volume_serial_number", wintypes.DWORD),
|
|
("file_size_high", wintypes.DWORD),
|
|
("file_size_low", wintypes.DWORD),
|
|
("number_of_links", wintypes.DWORD),
|
|
("file_index_high", wintypes.DWORD),
|
|
("file_index_low", wintypes.DWORD),
|
|
]
|
|
|
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
create_file = kernel32.CreateFileW
|
|
create_file.argtypes = [
|
|
wintypes.LPCWSTR,
|
|
wintypes.DWORD,
|
|
wintypes.DWORD,
|
|
wintypes.LPVOID,
|
|
wintypes.DWORD,
|
|
wintypes.DWORD,
|
|
wintypes.HANDLE,
|
|
]
|
|
create_file.restype = wintypes.HANDLE
|
|
get_information = kernel32.GetFileInformationByHandle
|
|
get_information.argtypes = [
|
|
wintypes.HANDLE,
|
|
ctypes.POINTER(ByHandleFileInformation),
|
|
]
|
|
get_information.restype = wintypes.BOOL
|
|
|
|
file_list_directory = 0x00000001
|
|
file_read_attributes = 0x00000080
|
|
delete = 0x00010000
|
|
file_share_read = 0x00000001
|
|
file_share_write = 0x00000002
|
|
open_existing = 3
|
|
file_flag_open_reparse_point = 0x00200000
|
|
file_flag_backup_semantics = 0x02000000
|
|
invalid_handle = ctypes.c_void_p(-1).value
|
|
handle = create_file(
|
|
directory,
|
|
file_list_directory
|
|
| file_read_attributes
|
|
| (delete if delete_access else 0),
|
|
file_share_read | file_share_write,
|
|
None,
|
|
open_existing,
|
|
file_flag_open_reparse_point | file_flag_backup_semantics,
|
|
None,
|
|
)
|
|
if handle == invalid_handle:
|
|
error = ctypes.get_last_error()
|
|
raise OSError(f"Open {label} at {directory}: {ctypes.WinError(error)}")
|
|
handle_value = int(handle)
|
|
descriptor = -1
|
|
try:
|
|
information = ByHandleFileInformation()
|
|
if not get_information(handle_value, ctypes.byref(information)):
|
|
error = ctypes.get_last_error()
|
|
raise OSError(
|
|
f"Inspect pinned {label} at {directory}: {ctypes.WinError(error)}"
|
|
)
|
|
attributes = information.file_attributes
|
|
if not attributes & _FILE_ATTRIBUTE_DIRECTORY:
|
|
raise OSError(f"{label} is not a physical directory: {directory}")
|
|
if attributes & _FILE_ATTRIBUTE_REPARSE_POINT:
|
|
raise OSError(f"{label} is not a physical directory: {directory}")
|
|
descriptor = msvcrt.open_osfhandle(
|
|
handle_value,
|
|
os.O_RDONLY | getattr(os, "O_NOINHERIT", 0),
|
|
)
|
|
handle_value = 0
|
|
metadata = os.fstat(descriptor)
|
|
if not stat.S_ISDIR(metadata.st_mode):
|
|
raise OSError(f"{label} is not a physical directory: {directory}")
|
|
return descriptor, DirectorySnapshot(metadata.st_dev, metadata.st_ino)
|
|
except Exception:
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|
|
elif handle_value:
|
|
_close_windows_handle(handle_value)
|
|
raise
|
|
|
|
|
|
def _chmod_windows_descriptor(descriptor: int, mode: int) -> None:
|
|
import ctypes as ctypes_module
|
|
from ctypes import wintypes
|
|
import msvcrt as msvcrt_module
|
|
|
|
ctypes: Any = ctypes_module
|
|
msvcrt: Any = msvcrt_module
|
|
|
|
class FileBasicInformation(ctypes.Structure):
|
|
_fields_ = [
|
|
("creation_time", ctypes.c_longlong),
|
|
("last_access_time", ctypes.c_longlong),
|
|
("last_write_time", ctypes.c_longlong),
|
|
("change_time", ctypes.c_longlong),
|
|
("file_attributes", wintypes.DWORD),
|
|
]
|
|
|
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
get_information = kernel32.GetFileInformationByHandleEx
|
|
get_information.argtypes = [
|
|
wintypes.HANDLE,
|
|
ctypes.c_int,
|
|
wintypes.LPVOID,
|
|
wintypes.DWORD,
|
|
]
|
|
get_information.restype = wintypes.BOOL
|
|
set_information = kernel32.SetFileInformationByHandle
|
|
set_information.argtypes = [
|
|
wintypes.HANDLE,
|
|
ctypes.c_int,
|
|
wintypes.LPVOID,
|
|
wintypes.DWORD,
|
|
]
|
|
set_information.restype = wintypes.BOOL
|
|
|
|
handle = msvcrt.get_osfhandle(descriptor)
|
|
if handle == -1:
|
|
raise OSError("File descriptor has no Windows handle")
|
|
current = FileBasicInformation()
|
|
file_basic_info = 0
|
|
if not get_information(
|
|
handle,
|
|
file_basic_info,
|
|
ctypes.byref(current),
|
|
ctypes.sizeof(current),
|
|
):
|
|
error = ctypes.get_last_error()
|
|
raise OSError(f"Inspect open file mode: {ctypes.WinError(error)}")
|
|
|
|
attributes = int(current.file_attributes)
|
|
if mode & stat.S_IWRITE:
|
|
attributes &= ~_FILE_ATTRIBUTE_READONLY
|
|
if attributes == 0:
|
|
attributes = _FILE_ATTRIBUTE_NORMAL
|
|
else:
|
|
attributes &= ~_FILE_ATTRIBUTE_NORMAL
|
|
attributes |= _FILE_ATTRIBUTE_READONLY
|
|
if attributes == current.file_attributes:
|
|
return
|
|
update = FileBasicInformation(0, 0, 0, 0, attributes)
|
|
if not set_information(
|
|
handle,
|
|
file_basic_info,
|
|
ctypes.byref(update),
|
|
ctypes.sizeof(update),
|
|
):
|
|
error = ctypes.get_last_error()
|
|
raise OSError(f"Set open file mode: {ctypes.WinError(error)}")
|
|
|
|
|
|
def chmod_open_file(descriptor: int, mode: int) -> None:
|
|
"""Apply a mode to the already-open physical file."""
|
|
if os.name == "nt" and not hasattr(os, "fchmod"):
|
|
_chmod_windows_descriptor(descriptor, mode)
|
|
return
|
|
getattr(os, "fchmod")(descriptor, mode)
|
|
|
|
|
|
@dataclass
|
|
class PinnedDirectory:
|
|
"""Keep one physical directory stable while path mutations run."""
|
|
|
|
path: str
|
|
label: str
|
|
snapshot: DirectorySnapshot
|
|
descriptor: Optional[int] = None
|
|
path_bound: bool = True
|
|
|
|
@classmethod
|
|
def open(
|
|
cls,
|
|
directory: str,
|
|
label: str,
|
|
expected: Optional[DirectorySnapshot] = None,
|
|
delete_access: bool = False,
|
|
) -> "PinnedDirectory":
|
|
normalized = os.path.abspath(directory)
|
|
before = read_physical_directory(normalized, label)
|
|
if before is None:
|
|
raise OSError(f"{label} is missing: {normalized}")
|
|
if expected is not None and before != expected:
|
|
raise OSError(f"{label} changed before pinning: {normalized}")
|
|
|
|
descriptor: Optional[int] = None
|
|
try:
|
|
if os.name == "nt":
|
|
if delete_access:
|
|
descriptor, opened = _open_windows_directory(
|
|
normalized, label, True
|
|
)
|
|
else:
|
|
descriptor, opened = _open_windows_directory(
|
|
normalized, label
|
|
)
|
|
if opened != before:
|
|
raise OSError(f"{label} changed while pinning: {normalized}")
|
|
else:
|
|
flags = (
|
|
os.O_RDONLY
|
|
| getattr(os, "O_CLOEXEC", 0)
|
|
| getattr(os, "O_DIRECTORY", 0)
|
|
| getattr(os, "O_NOFOLLOW", 0)
|
|
)
|
|
descriptor = os.open(normalized, flags)
|
|
metadata = os.fstat(descriptor)
|
|
if (
|
|
not stat.S_ISDIR(metadata.st_mode)
|
|
or (metadata.st_dev, metadata.st_ino)
|
|
!= (before.device, before.inode)
|
|
):
|
|
raise OSError(f"{label} changed while pinning: {normalized}")
|
|
after = read_physical_directory(normalized, label)
|
|
if after != before:
|
|
raise OSError(f"{label} changed while pinning: {normalized}")
|
|
return cls(normalized, label, before, descriptor)
|
|
except Exception:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
raise
|
|
|
|
def close(self) -> None:
|
|
failures = []
|
|
if self.descriptor is not None:
|
|
try:
|
|
os.close(self.descriptor)
|
|
except OSError as error:
|
|
failures.append(str(error))
|
|
self.descriptor = None
|
|
if failures:
|
|
raise OSError(f"Close {self.label}: {'; '.join(failures)}")
|
|
|
|
def _require_open(self) -> None:
|
|
if self.descriptor is None:
|
|
raise OSError(f"Pinned {self.label} is closed")
|
|
|
|
def assert_path_stable(self, operation: str = "transaction") -> None:
|
|
"""Require the visible path to still name the pinned directory."""
|
|
self._require_open()
|
|
assert self.descriptor is not None
|
|
metadata = os.fstat(self.descriptor)
|
|
if (
|
|
not stat.S_ISDIR(metadata.st_mode)
|
|
or (metadata.st_dev, metadata.st_ino)
|
|
!= (self.snapshot.device, self.snapshot.inode)
|
|
):
|
|
raise OSError(f"Pinned {self.label} changed during {operation}")
|
|
if not self.path_bound:
|
|
return
|
|
current = read_physical_directory(self.path, self.label)
|
|
if current != self.snapshot:
|
|
raise OSError(
|
|
f"{self.label} changed during {operation}: {self.path}"
|
|
)
|
|
|
|
def chmod_descriptor(self, descriptor: int, mode: int) -> None:
|
|
"""Apply a mode while this directory remains pinned."""
|
|
self._require_open()
|
|
chmod_open_file(descriptor, mode)
|
|
|
|
def name_for(self, file_path: str) -> str:
|
|
absolute = os.path.abspath(file_path)
|
|
if os.path.normcase(os.path.dirname(absolute)) != os.path.normcase(self.path):
|
|
raise ValueError(f"File is outside pinned {self.label}: {file_path}")
|
|
name = os.path.basename(absolute)
|
|
if not name or name in {".", ".."}:
|
|
raise ValueError(f"Invalid file name in pinned {self.label}: {file_path}")
|
|
return name
|
|
|
|
def path_for(self, name: str) -> str:
|
|
if not name or name in {".", ".."} or os.path.basename(name) != name:
|
|
raise ValueError(f"Invalid file name in pinned {self.label}: {name}")
|
|
return os.path.join(self.path, name)
|
|
|
|
def list_names(self) -> list[str]:
|
|
"""List direct entries while retaining the physical directory handle."""
|
|
self._require_open()
|
|
self.assert_path_stable("directory enumeration")
|
|
try:
|
|
if os.name == "nt":
|
|
names = os.listdir(self.path)
|
|
else:
|
|
assert self.descriptor is not None
|
|
names = os.listdir(self.descriptor)
|
|
except OSError as error:
|
|
raise OSError(f"List {self.label} at {self.path}: {error}") from error
|
|
self.assert_path_stable("directory enumeration")
|
|
return sorted(names)
|
|
|
|
def open_file(self, name: str, flags: int, mode: int = 0o600) -> int:
|
|
self._require_open()
|
|
if os.name == "nt":
|
|
return os.open(self.path_for(name), flags, mode)
|
|
assert self.descriptor is not None
|
|
return os.open(name, flags, mode, dir_fd=self.descriptor)
|
|
|
|
def stat_file(self, name: str) -> os.stat_result:
|
|
self._require_open()
|
|
if os.name == "nt":
|
|
return os.lstat(self.path_for(name))
|
|
assert self.descriptor is not None
|
|
return os.stat(name, dir_fd=self.descriptor, follow_symlinks=False)
|
|
|
|
def remove_file(self, name: str) -> None:
|
|
self._require_open()
|
|
if os.name == "nt":
|
|
os.remove(self.path_for(name))
|
|
return
|
|
assert self.descriptor is not None
|
|
os.unlink(name, dir_fd=self.descriptor)
|
|
|
|
def replace_file(self, source_name: str, destination_name: str) -> None:
|
|
self._require_open()
|
|
if os.name == "nt":
|
|
os.replace(self.path_for(source_name), self.path_for(destination_name))
|
|
return
|
|
assert self.descriptor is not None
|
|
os.replace(
|
|
source_name,
|
|
destination_name,
|
|
src_dir_fd=self.descriptor,
|
|
dst_dir_fd=self.descriptor,
|
|
)
|
|
|
|
def sync(self) -> None:
|
|
"""Persist directory entries where the host exposes directory fsync."""
|
|
self._require_open()
|
|
if os.name == "nt":
|
|
# Windows directory handles do not support FlushFileBuffers.
|
|
return
|
|
if os.name != "posix":
|
|
raise OSError(f"Unsupported transaction platform: {os.name}")
|
|
assert self.descriptor is not None
|
|
os.fsync(self.descriptor)
|
|
|
|
def link_file(self, source_name: str, destination_name: str) -> None:
|
|
"""Create a no-clobber hard link within the pinned directory."""
|
|
self._require_open()
|
|
if os.name == "nt":
|
|
os.link(self.path_for(source_name), self.path_for(destination_name))
|
|
return
|
|
assert self.descriptor is not None
|
|
os.link(
|
|
source_name,
|
|
destination_name,
|
|
src_dir_fd=self.descriptor,
|
|
dst_dir_fd=self.descriptor,
|
|
follow_symlinks=False,
|
|
)
|
|
|
|
def read_file(
|
|
self,
|
|
name: str,
|
|
label: str,
|
|
maximum_bytes: int,
|
|
) -> Optional[FileSnapshot]:
|
|
file_path = self.path_for(name)
|
|
try:
|
|
before = self.stat_file(name)
|
|
except FileNotFoundError:
|
|
return None
|
|
except OSError as error:
|
|
raise OSError(f"Inspect {label} at {file_path}: {error}") from error
|
|
if not stat.S_ISREG(before.st_mode) or stat.S_ISLNK(before.st_mode):
|
|
raise OSError(f"{label} path is not a physical file: {file_path}")
|
|
return read_file_snapshot(
|
|
before,
|
|
lambda flags: self.open_file(name, flags),
|
|
file_path,
|
|
label,
|
|
maximum_bytes,
|
|
)
|
|
|
|
def chmod_file(
|
|
self,
|
|
name: str,
|
|
mode: int,
|
|
expected_identity: Tuple[int, int],
|
|
) -> None:
|
|
flags = os.O_RDWR | getattr(os, "O_BINARY", 0) | getattr(os, "O_CLOEXEC", 0)
|
|
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
descriptor = -1
|
|
try:
|
|
descriptor = self.open_file(name, flags)
|
|
metadata = os.fstat(descriptor)
|
|
if (
|
|
not stat.S_ISREG(metadata.st_mode)
|
|
or (metadata.st_dev, metadata.st_ino) != expected_identity
|
|
):
|
|
raise OSError(f"File changed before chmod: {self.path_for(name)}")
|
|
chmod_open_file(descriptor, mode)
|
|
os.fsync(descriptor)
|
|
finally:
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|