Files
2026-07-13 13:12:33 +08:00

1674 lines
56 KiB
Python

from __future__ import annotations
import ctypes
import errno
import hashlib
import json
import multiprocessing
import os
import shutil
import stat
import struct
import subprocess
import sys
import threading
import uuid
from pathlib import Path
from types import SimpleNamespace
import pytest
from opensquilla.recovery import (
AtomicStateUnknownError,
CrossDeviceMoveError,
DestinationExistsError,
ProfileLockBusyError,
ProfileOperationLock,
UnsafePathError,
move_profile_no_replace,
native_move_no_replace,
profile_lock_path,
)
from opensquilla.recovery.atomic import (
PathIdentity,
_linux_rename_no_replace,
_macos_rename_no_replace,
_manifest_matches_after_move,
_windows_extended_path,
_windows_move_no_replace,
_windows_rename_info,
)
from opensquilla.recovery.config_patch import ConfigSnapshot
from opensquilla.recovery.locking import (
LegacyGatewayLock,
acquire_legacy_gateway_locks,
user_state_dir,
)
def _contend_for_lock(home: str, state_root: str, queue: multiprocessing.Queue) -> None:
import os
os.environ["OPENSQUILLA_USER_STATE_DIR"] = state_root
os.environ["OPENSQUILLA_TEST_PROFILE_LOCK_ROOT"] = "1"
try:
with ProfileOperationLock(home):
queue.put("acquired")
except ProfileLockBusyError:
queue.put("busy")
def test_user_state_override_is_ignored_without_the_explicit_test_gate(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
override = tmp_path / "override"
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(override))
monkeypatch.delenv("OPENSQUILLA_TEST_PROFILE_LOCK_ROOT", raising=False)
assert user_state_dir() != override
def test_user_state_override_is_used_with_the_explicit_test_gate(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
override = tmp_path / "override"
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(override))
monkeypatch.setenv("OPENSQUILLA_TEST_PROFILE_LOCK_ROOT", "1")
assert user_state_dir() == override
def test_profile_lock_does_not_require_descriptor_chmod(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setenv("OPENSQUILLA_TEST_PROFILE_LOCK_ROOT", "1")
monkeypatch.delattr(os, "fchmod", raising=False)
with ProfileOperationLock(tmp_path / "profile"):
assert profile_lock_path(tmp_path / "profile").is_file()
def _contend_for_gateway(state_dir: str, queue: multiprocessing.Queue) -> None:
from opensquilla.gateway.pidlock import GatewayPidLock
lock = GatewayPidLock(state_dir)
try:
lock.acquire()
except SystemExit:
queue.put("busy")
else:
queue.put("acquired")
lock.release()
def test_profile_lock_is_same_process_reentrant_and_external(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
state_root = tmp_path / "user-state"
home = tmp_path / "profile"
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(state_root))
with ProfileOperationLock(home):
with ProfileOperationLock(home):
assert profile_lock_path(home).is_file()
context = multiprocessing.get_context("spawn" if sys.platform == "win32" else "fork")
queue = context.Queue()
process = context.Process(
target=_contend_for_lock,
args=(str(home), str(state_root), queue),
)
process.start()
process.join(timeout=10)
assert process.exitcode == 0
assert queue.get(timeout=1) == "busy"
with ProfileOperationLock(home):
pass
def test_profile_lock_does_not_treat_another_thread_as_reentrant(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
home = tmp_path / "profile"
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(tmp_path / "user-state"))
result: list[str] = []
def contend() -> None:
try:
with ProfileOperationLock(home, timeout=0.05):
result.append("acquired")
except ProfileLockBusyError:
result.append("busy")
with ProfileOperationLock(home):
thread = threading.Thread(target=contend)
thread.start()
thread.join(timeout=2)
assert not thread.is_alive()
assert result == ["busy"]
def test_profile_lock_refuses_app_state_symlink_without_external_write(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
user_state = tmp_path / "user-state"
external = tmp_path / "external"
user_state.mkdir()
external.mkdir()
try:
(user_state / "OpenSquilla").symlink_to(external, target_is_directory=True)
except OSError:
pytest.skip("directory symlinks are unavailable")
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(user_state))
with pytest.raises(UnsafePathError):
ProfileOperationLock(tmp_path / "profile").acquire()
assert not (external / "profile-locks").exists()
def test_config_snapshot_rejects_windows_reparse_attribute_before_open(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
path = tmp_path / "config.toml"
path.write_text("synthetic = true\n", encoding="utf-8")
original_lstat = Path.lstat
original_open = os.open
opened = False
def fake_lstat(candidate: Path):
value = original_lstat(candidate)
if candidate != path:
return value
return SimpleNamespace(
st_mode=stat.S_IFREG | 0o600,
st_nlink=1,
st_file_attributes=0x400,
st_dev=value.st_dev,
st_ino=value.st_ino,
st_size=value.st_size,
st_mtime_ns=value.st_mtime_ns,
)
def track_open(candidate, *args, **kwargs):
nonlocal opened
if Path(candidate) == path:
opened = True
return original_open(candidate, *args, **kwargs)
monkeypatch.setattr(Path, "lstat", fake_lstat)
monkeypatch.setattr(os, "open", track_open)
with pytest.raises(UnsafePathError):
ConfigSnapshot.capture(path)
assert not opened
def test_native_move_never_replaces_existing_destination(tmp_path: Path) -> None:
source = tmp_path / "source"
destination = tmp_path / "destination"
source.mkdir()
destination.mkdir()
(source / "value.txt").write_text("source", encoding="utf-8")
(destination / "value.txt").write_text("destination", encoding="utf-8")
with pytest.raises(DestinationExistsError):
native_move_no_replace(source, destination)
assert (source / "value.txt").read_text(encoding="utf-8") == "source"
assert (destination / "value.txt").read_text(encoding="utf-8") == "destination"
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="requires renameat2")
def test_linux_primitive_collision_preserves_both_trees(tmp_path: Path) -> None:
source = tmp_path / "source"
destination = tmp_path / "destination"
source.mkdir()
destination.mkdir()
(source / "value.txt").write_text("source", encoding="utf-8")
(destination / "value.txt").write_text("destination", encoding="utf-8")
with pytest.raises(DestinationExistsError):
_linux_rename_no_replace(source, destination)
assert (source / "value.txt").read_text(encoding="utf-8") == "source"
assert (destination / "value.txt").read_text(encoding="utf-8") == "destination"
@pytest.mark.skipif(sys.platform != "darwin", reason="requires renameatx_np")
def test_macos_primitive_collision_preserves_both_trees(tmp_path: Path) -> None:
source = tmp_path / "source"
destination = tmp_path / "destination"
source.mkdir()
destination.mkdir()
(source / "value.txt").write_text("source", encoding="utf-8")
(destination / "value.txt").write_text("destination", encoding="utf-8")
with pytest.raises(DestinationExistsError):
_macos_rename_no_replace(source, destination)
assert (source / "value.txt").read_text(encoding="utf-8") == "source"
assert (destination / "value.txt").read_text(encoding="utf-8") == "destination"
@pytest.mark.skipif(sys.platform != "win32", reason="requires native Windows rename")
def test_windows_primitive_collision_preserves_both_trees(tmp_path: Path) -> None:
source = tmp_path / "source"
destination = tmp_path / "destination"
source.mkdir()
destination.mkdir()
(source / "value.txt").write_text("source", encoding="utf-8")
(destination / "value.txt").write_text("destination", encoding="utf-8")
with pytest.raises(DestinationExistsError):
_windows_move_no_replace(source, destination)
assert (source / "value.txt").read_text(encoding="utf-8") == "source"
assert (destination / "value.txt").read_text(encoding="utf-8") == "destination"
def test_native_move_moves_a_regular_tree_between_real_parents(tmp_path: Path) -> None:
source_parent = tmp_path / "source-parent"
destination_parent = tmp_path / "destination-parent"
source_parent.mkdir()
destination_parent.mkdir()
source = source_parent / "source"
destination = destination_parent / "destination"
source.mkdir()
(source / "value.txt").write_text("preserved", encoding="utf-8")
native_move_no_replace(source, destination)
assert not source.exists()
assert (destination / "value.txt").read_text(encoding="utf-8") == "preserved"
def test_native_move_treats_unsafe_post_move_manifest_as_unknown(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.atomic as atomic
source = tmp_path / "source"
destination = tmp_path / "destination"
source.mkdir()
(source / "value.txt").write_text("preserved", encoding="utf-8")
original_manifest = atomic.no_follow_manifest
calls = 0
def fail_post_move_manifest(path: str | Path):
nonlocal calls
calls += 1
if calls == 2:
raise UnsafePathError("synthetic unsafe post-move tree")
return original_manifest(path)
monkeypatch.setattr(atomic, "no_follow_manifest", fail_post_move_manifest)
with pytest.raises(AtomicStateUnknownError):
atomic.native_move_no_replace(source, destination)
assert calls == 2
assert not source.exists()
assert (destination / "value.txt").read_text(encoding="utf-8") == "preserved"
def test_move_manifest_allows_only_selected_lock_mtime_change() -> None:
original = PathIdentity(1, 2, stat.S_IFREG | 0o600, 1, 100)
mtime_changed = PathIdentity(1, 2, stat.S_IFREG | 0o600, 1, 200)
size_changed = PathIdentity(1, 2, stat.S_IFREG | 0o600, 2, 200)
relative = "state/gateway.pid.lock"
assert _manifest_matches_after_move(
{relative: original},
{relative: mtime_changed},
allowed_mtime_changes=frozenset({relative}),
)
assert not _manifest_matches_after_move(
{relative: original},
{relative: mtime_changed},
allowed_mtime_changes=frozenset(),
)
assert not _manifest_matches_after_move(
{relative: original},
{relative: size_changed},
allowed_mtime_changes=frozenset({relative}),
)
directory = PathIdentity(1, 3, stat.S_IFDIR | 0o700, 0, 100)
directory_mtime_changed = PathIdentity(1, 3, stat.S_IFDIR | 0o700, 0, 200)
assert _manifest_matches_after_move(
{"workspace": directory},
{"workspace": directory_mtime_changed},
allowed_mtime_changes=frozenset(),
allow_directory_mtime_changes=True,
)
assert not _manifest_matches_after_move(
{"workspace": directory},
{"workspace": directory_mtime_changed},
allowed_mtime_changes=frozenset(),
)
assert not _manifest_matches_after_move(
{relative: original},
{relative: mtime_changed},
allowed_mtime_changes=frozenset(),
allow_directory_mtime_changes=True,
)
@pytest.mark.skipif(sys.platform != "win32", reason="requires two native Windows volumes")
def test_windows_native_move_refuses_real_cross_volume_move() -> None:
volume_a_value = os.environ.get("OPENSQUILLA_WINDOWS_TEST_VOLUME_A")
volume_b_value = os.environ.get("OPENSQUILLA_WINDOWS_TEST_VOLUME_B")
if not volume_a_value or not volume_b_value:
running_in_ci = os.environ.get("CI", "").strip().lower() not in {
"",
"0",
"false",
}
if running_in_ci:
pytest.fail("Windows CI must provide both synthetic test volume roots")
pytest.skip("requires the two synthetic Windows test volume roots")
volume_a = Path(volume_a_value)
volume_b = Path(volume_b_value)
assert volume_a.stat().st_dev != volume_b.stat().st_dev
token = uuid.uuid4().hex
source_parent = volume_a / f"opensquilla-cross-volume-source-{token}"
destination_parent = volume_b / f"opensquilla-cross-volume-destination-{token}"
try:
source_parent.mkdir(exist_ok=False)
destination_parent.mkdir(exist_ok=False)
assert source_parent.stat().st_dev != destination_parent.stat().st_dev
source = source_parent / "source"
destination = destination_parent / "destination"
payload = source / "payload.bin"
source.mkdir()
payload.write_bytes(b"synthetic-cross-volume-payload")
with pytest.raises(CrossDeviceMoveError):
native_move_no_replace(source, destination)
assert payload.read_bytes() == b"synthetic-cross-volume-payload"
assert not destination.exists()
finally:
for owned_root in (source_parent, destination_parent):
if owned_root.exists():
shutil.rmtree(owned_root)
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="requires renameat2")
def test_linux_cross_filesystem_no_replace_fails_without_copy_delete_fallback(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
source_parent = tmp_path / "source-parent"
destination_parent = tmp_path / "destination-parent"
source_parent.mkdir()
destination_parent.mkdir()
source = source_parent / "source"
destination = destination_parent / "destination"
source.mkdir()
(source / "value.txt").write_text("preserved", encoding="utf-8")
class FakeRenameAt2:
argtypes = None
restype = None
def __call__(self, *_args: object) -> int:
return -1
monkeypatch.setattr(
ctypes,
"CDLL",
lambda *_args, **_kwargs: SimpleNamespace(renameat2=FakeRenameAt2()),
)
monkeypatch.setattr(ctypes, "get_errno", lambda: errno.EXDEV)
with pytest.raises(CrossDeviceMoveError):
_linux_rename_no_replace(source, destination)
assert source.is_dir()
assert (source / "value.txt").read_text(encoding="utf-8") == "preserved"
assert not destination.exists()
def test_native_move_refuses_symlink_in_source_tree(tmp_path: Path) -> None:
source = tmp_path / "source"
destination = tmp_path / "destination"
source.mkdir()
outside = tmp_path / "outside.txt"
outside.write_text("outside", encoding="utf-8")
try:
(source / "link").symlink_to(outside)
except OSError:
pytest.skip("symlink creation is unavailable")
with pytest.raises(UnsafePathError):
native_move_no_replace(source, destination)
assert source.is_dir()
assert not destination.exists()
@pytest.mark.skipif(sys.platform != "win32", reason="requires native Windows long paths")
def test_windows_native_move_handles_real_path_longer_than_260_characters(
tmp_path: Path,
) -> None:
long_root = tmp_path
for index in range(6):
long_root /= f"segment-{index}-" + ("x" * 42)
source_parent = long_root / "source-parent"
destination_parent = long_root / "destination-parent"
source_parent.mkdir(parents=True)
destination_parent.mkdir(parents=True)
source = source_parent / "candidate-profile"
destination = destination_parent / "published-profile"
source.mkdir()
(source / "value.txt").write_text("long-path-preserved", encoding="utf-8")
assert len(str(source)) > 260
assert len(str(destination)) > 260
native_move_no_replace(source, destination)
assert not source.exists()
assert (destination / "value.txt").read_text(encoding="utf-8") == (
"long-path-preserved"
)
@pytest.mark.skipif(sys.platform != "win32", reason="requires a real Windows junction")
def test_windows_native_move_rejects_real_junction_in_source_tree(tmp_path: Path) -> None:
source = tmp_path / "source"
outside = tmp_path / "outside"
destination = tmp_path / "destination"
source.mkdir()
outside.mkdir()
completed = subprocess.run(
["cmd", "/c", "mklink", "/J", str(source / "junction"), str(outside)],
check=False,
capture_output=True,
text=True,
)
assert completed.returncode == 0, completed.stderr or completed.stdout
assert os.path.isjunction(source / "junction")
with pytest.raises(UnsafePathError):
native_move_no_replace(source, destination)
assert source.is_dir()
assert outside.is_dir()
assert not destination.exists()
@pytest.mark.skipif(sys.platform != "darwin", reason="requires renameatx_np")
def test_macos_no_replace_binds_source_and_destination_parent_handles(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
source_parent = tmp_path / "source-parent"
destination_parent = tmp_path / "destination-parent"
source_parent.mkdir()
destination_parent.mkdir()
source = source_parent / "source"
destination = destination_parent / "destination"
source.mkdir()
calls: list[tuple[int, bytes, int, bytes, int]] = []
class FakeRenameAt:
argtypes = None
restype = None
def __call__(self, source_fd, source_name, destination_fd, destination_name, flags):
calls.append(
(source_fd, source_name, destination_fd, destination_name, flags)
)
return 0
monkeypatch.setattr(
"opensquilla.recovery.atomic.ctypes.CDLL",
lambda *_args, **_kwargs: SimpleNamespace(renameatx_np=FakeRenameAt()),
)
_macos_rename_no_replace(source, destination)
assert len(calls) == 1
source_fd, source_name, destination_fd, destination_name, flags = calls[0]
assert source_fd >= 0
assert destination_fd >= 0
assert source_name == b"source"
assert destination_name == b"destination"
assert flags == 0x00000004
@pytest.mark.skipif(sys.platform != "darwin", reason="requires renameatx_np")
def test_macos_no_replace_stops_if_open_parent_identity_changed(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
source_parent = tmp_path / "source-parent"
destination_parent = tmp_path / "destination-parent"
source_parent.mkdir()
destination_parent.mkdir()
source = source_parent / "source"
source.mkdir()
destination = destination_parent / "destination"
called = False
original_fstat = os.fstat
class FakeRenameAt:
argtypes = None
restype = None
def __call__(self, *_args):
nonlocal called
called = True
return 0
def changed_fstat(fd: int):
value = original_fstat(fd)
return SimpleNamespace(
st_mode=value.st_mode,
st_dev=value.st_dev,
st_ino=value.st_ino + 1,
)
monkeypatch.setattr(
"opensquilla.recovery.atomic.ctypes.CDLL",
lambda *_args, **_kwargs: SimpleNamespace(renameatx_np=FakeRenameAt()),
)
monkeypatch.setattr("opensquilla.recovery.atomic.os.fstat", changed_fstat)
with pytest.raises(UnsafePathError, match="identity changed"):
_macos_rename_no_replace(source, destination)
assert not called
def test_legacy_gateway_lock_rejects_broken_state_symlink(tmp_path: Path) -> None:
home = tmp_path / "profile"
home.mkdir()
try:
(home / "state").symlink_to(tmp_path / "missing-state", target_is_directory=True)
except OSError:
pytest.skip("symlink creation is unavailable")
with pytest.raises(UnsafePathError):
LegacyGatewayLock(home).acquire()
def test_legacy_gateway_lock_does_not_follow_implicit_canonical_state_symlink(
tmp_path: Path,
) -> None:
home = tmp_path / "profile"
external = tmp_path / "external-state"
home.mkdir()
external.mkdir()
try:
(home / "state").symlink_to(external, target_is_directory=True)
except OSError:
pytest.skip("directory symlinks are unavailable")
with pytest.raises(UnsafePathError):
LegacyGatewayLock(home).acquire()
assert not (external / "gateway.pid.lock").exists()
def test_legacy_gateway_lock_creates_and_keeps_absent_lock_in_existing_state(
tmp_path: Path,
) -> None:
home = tmp_path / "profile"
state = home / "state"
state.mkdir(parents=True)
lock_path = state / "gateway.pid.lock"
assert not lock_path.exists()
with LegacyGatewayLock(home):
assert lock_path.is_file()
context = multiprocessing.get_context("spawn" if sys.platform == "win32" else "fork")
queue = context.Queue()
process = context.Process(
target=_contend_for_gateway,
args=(str(state), queue),
)
process.start()
process.join(timeout=10)
assert process.exitcode == 0
assert queue.get(timeout=1) == "busy"
# Persistent identity prevents a successor from locking an unlinked inode
# while a third process creates a new lock path.
assert lock_path.is_file()
def test_legacy_gateway_lock_snapshots_through_its_own_descriptor(tmp_path: Path) -> None:
home = tmp_path / "profile"
state = home / "state"
state.mkdir(parents=True)
lock_path = state / "gateway.pid.lock"
lock_path.write_bytes(b"synthetic-lock-authority\n")
with LegacyGatewayLock(home) as lease:
snapshot = lease.snapshot_state_root(state)
assert snapshot is not None
assert snapshot.path == lock_path
assert snapshot.size == len(b"synthetic-lock-authority\n")
assert snapshot.digest == hashlib.sha256(b"synthetic-lock-authority\n").hexdigest()
assert lease.snapshot_state_root(state) == snapshot
assert lease.snapshot_state_root(state) is None
def test_legacy_gateway_lock_does_not_treat_another_thread_as_reentrant(
tmp_path: Path,
) -> None:
home = tmp_path / "profile"
(home / "state").mkdir(parents=True)
result: list[str] = []
def contend() -> None:
from opensquilla.recovery.errors import LegacyGatewayRunningError
try:
with LegacyGatewayLock(home, timeout=0.05):
result.append("acquired")
except LegacyGatewayRunningError:
result.append("busy")
with LegacyGatewayLock(home):
thread = threading.Thread(target=contend)
thread.start()
thread.join(timeout=2)
assert not thread.is_alive()
assert result == ["busy"]
def test_legacy_gateway_lock_never_creates_a_missing_state_directory(tmp_path: Path) -> None:
home = tmp_path / "profile"
home.mkdir()
with LegacyGatewayLock(home):
pass
assert not (home / "state").exists()
def test_legacy_gateway_lock_covers_external_effective_state(
tmp_path: Path,
) -> None:
home = tmp_path / "profile"
external_state = tmp_path / "external-state"
home.mkdir()
external_state.mkdir()
(home / "config.toml").write_text(
f"state_dir = {json.dumps(str(external_state))}\n",
encoding="utf-8",
)
with LegacyGatewayLock(home):
assert (external_state / "gateway.pid.lock").is_file()
context = multiprocessing.get_context("spawn" if sys.platform == "win32" else "fork")
queue = context.Queue()
process = context.Process(
target=_contend_for_gateway,
args=(str(external_state), queue),
)
process.start()
process.join(timeout=10)
assert process.exitcode == 0
assert queue.get(timeout=1) == "busy"
def test_multi_root_legacy_acquisition_is_sorted_and_releases_partial_claims(
tmp_path: Path,
) -> None:
from opensquilla.gateway.pidlock import GatewayPidLock
from opensquilla.recovery.errors import LegacyGatewayRunningError
home = tmp_path / "profile"
canonical_state = home / "state"
external_state = tmp_path / "external-state"
canonical_state.mkdir(parents=True)
external_state.mkdir()
(home / "config.toml").write_text(
f"state_dir = {json.dumps(str(external_state))}\n",
encoding="utf-8",
)
probe = LegacyGatewayLock(home)
assert list(probe.state_roots) == sorted(
probe.state_roots,
key=lambda path: os.path.normcase(os.path.normpath(str(path.resolve()))),
)
blocking_gateway = GatewayPidLock(probe.state_roots[-1])
blocking_gateway.acquire()
try:
with pytest.raises(LegacyGatewayRunningError):
probe.acquire()
finally:
blocking_gateway.release()
# Failure on the later root must have released the earlier root.
successor = GatewayPidLock(probe.state_roots[0])
successor.acquire()
successor.release()
def test_import_source_lock_probe_does_not_create_source_authority_file(
tmp_path: Path,
) -> None:
source = tmp_path / "source"
target = tmp_path / "target"
(source / "state").mkdir(parents=True)
(target / "state").mkdir(parents=True)
with acquire_legacy_gateway_locks(
source,
target,
read_only_homes=(source,),
):
assert not (source / "state" / "gateway.pid.lock").exists()
assert (target / "state" / "gateway.pid.lock").is_file()
def test_import_existing_source_lock_probe_preserves_bytes_and_mode(
tmp_path: Path,
) -> None:
source = tmp_path / "source"
target = tmp_path / "target"
source_state = source / "state"
(source_state).mkdir(parents=True)
(target / "state").mkdir(parents=True)
source_lock = source_state / "gateway.pid.lock"
source_lock.write_bytes(b"synthetic-existing-lock\n")
os.chmod(source_lock, 0o644)
bytes_before = source_lock.read_bytes()
mode_before = stat.S_IMODE(source_lock.stat().st_mode)
with acquire_legacy_gateway_locks(
source,
target,
read_only_homes=(source,),
) as locks:
assert any(lock.holds_state_root(source_state) for lock in locks)
assert source_lock.read_bytes() == bytes_before
assert stat.S_IMODE(source_lock.stat().st_mode) == mode_before
def test_windows_legacy_lock_handle_allows_share_delete_for_profile_swap(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import opensquilla.recovery.locking as locking_module
create_calls: list[tuple[object, ...]] = []
converted: list[tuple[int, int]] = []
class FakeCreateFile:
argtypes = None
restype = None
def __call__(self, *args: object) -> int:
create_calls.append(args)
return 101
kernel32 = SimpleNamespace(
CreateFileW=FakeCreateFile(),
CloseHandle=lambda _handle: 1,
)
monkeypatch.setattr(
locking_module.ctypes,
"WinDLL",
lambda *_args, **_kwargs: kernel32,
raising=False,
)
monkeypatch.setitem(
sys.modules,
"msvcrt",
SimpleNamespace(
open_osfhandle=lambda handle, flags: converted.append((handle, flags)) or 77,
),
)
fd = locking_module._windows_open_legacy_lock_file(
Path("C:/synthetic/state/gateway.pid.lock"),
create_if_missing=True,
)
assert fd == 77
assert len(create_calls) == 1
_path, _access, share_mode, _security, creation, open_flags, _template = create_calls[0]
assert share_mode == 0x00000001 | 0x00000002 | 0x00000004
assert creation == 4 # OPEN_ALWAYS
assert int(open_flags) & 0x00200000 # FILE_FLAG_OPEN_REPARSE_POINT
assert converted and converted[0][0] == 101
@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows share-delete semantics")
def test_windows_real_legacy_lock_survives_profile_move_and_rebind(tmp_path: Path) -> None:
source = tmp_path / "source"
destination = tmp_path / "destination"
(source / "state").mkdir(parents=True)
with LegacyGatewayLock(source):
move_profile_no_replace(source, destination)
with LegacyGatewayLock(destination):
context = multiprocessing.get_context("spawn")
queue = context.Queue()
process = context.Process(
target=_contend_for_gateway,
args=(str(destination / "state"), queue),
)
process.start()
process.join(timeout=15)
assert process.exitcode == 0
assert queue.get(timeout=2) == "busy"
@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows lock handoff")
def test_windows_real_replacement_locks_survive_two_profile_moves(tmp_path: Path) -> None:
target = tmp_path / "target"
staging = tmp_path / "staging"
backup = tmp_path / "backup"
(target / "state").mkdir(parents=True)
(staging / "state").mkdir(parents=True)
with LegacyGatewayLock(target), LegacyGatewayLock(staging):
move_profile_no_replace(target, backup)
move_profile_no_replace(staging, target)
context = multiprocessing.get_context("spawn")
for state_root in (backup / "state", target / "state"):
queue = context.Queue()
process = context.Process(
target=_contend_for_gateway,
args=(str(state_root), queue),
)
process.start()
process.join(timeout=15)
assert process.exitcode == 0
assert queue.get(timeout=2) == "busy"
@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows lock handoff")
def test_windows_real_recent_locked_profile_tree_moves_without_metadata_false_positive(
tmp_path: Path,
) -> None:
source = tmp_path / "profile-staging"
destination = tmp_path / "profile-target"
(source / "state").mkdir(parents=True)
(source / "workspace").mkdir()
with LegacyGatewayLock(source):
(source / "config.toml").write_text("port = 18791\n", encoding="utf-8")
(source / "workspace" / "SOUL.md").write_text("synthetic soul\n", encoding="utf-8")
(source / "profile-migration-report.json").write_text("{}\n", encoding="utf-8")
(source / ".opensquilla-layout-v2.json").write_text("{}\n", encoding="utf-8")
move_profile_no_replace(source, destination)
assert not source.exists()
assert (destination / "config.toml").read_text(encoding="utf-8") == (
"port = 18791\n"
)
assert (destination / "workspace" / "SOUL.md").read_text(
encoding="utf-8"
) == "synthetic soul\n"
def test_windows_handoff_reacquires_source_after_pre_mutation_failure(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
source = tmp_path / "source"
destination = tmp_path / "destination"
(source / "state").mkdir(parents=True)
monkeypatch.setattr(locking_module, "_windows_requires_legacy_lock_handoff", lambda: True)
def fail_move(
_source: Path,
_destination: Path,
*,
_mutation_guard,
**_move_options: object,
) -> None:
with _mutation_guard():
raise DestinationExistsError("synthetic collision")
with LegacyGatewayLock(source):
with pytest.raises(DestinationExistsError, match="synthetic collision"):
move_profile_no_replace(source, destination, move=fail_move)
held = next(iter(locking_module._PROCESS_LEGACY_LOCKS.values()))
assert held.fd is not None
assert held.path.parent == source / "state"
def test_windows_handoff_fails_closed_when_destination_lock_cannot_be_reacquired(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
source = tmp_path / "source"
destination = tmp_path / "destination"
(source / "state").mkdir(parents=True)
original_try_lock = locking_module._try_lock
deny_reacquire = False
monkeypatch.setattr(locking_module, "_windows_requires_legacy_lock_handoff", lambda: True)
def try_lock(fd: int) -> bool:
return False if deny_reacquire else original_try_lock(fd)
def move_then_block_reacquire(
source_path: Path,
destination_path: Path,
*,
_mutation_guard,
**_move_options: object,
) -> None:
nonlocal deny_reacquire
with _mutation_guard():
source_path.rename(destination_path)
deny_reacquire = True
monkeypatch.setattr(locking_module, "_try_lock", try_lock)
with LegacyGatewayLock(source):
with pytest.raises(AtomicStateUnknownError, match="could not be reacquired"):
move_profile_no_replace(
source,
destination,
move=move_then_block_reacquire,
)
assert not source.exists()
assert destination.is_dir()
def test_windows_handoff_treats_move_then_raise_as_unknown(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
source = tmp_path / "source"
destination = tmp_path / "destination"
(source / "state").mkdir(parents=True)
monkeypatch.setattr(locking_module, "_windows_requires_legacy_lock_handoff", lambda: True)
def move_then_raise(
source_path: Path,
destination_path: Path,
*,
_mutation_guard,
**_move_options: object,
) -> None:
with _mutation_guard():
source_path.rename(destination_path)
raise OSError("synthetic post-move failure")
with LegacyGatewayLock(source):
with pytest.raises(AtomicStateUnknownError, match="before reporting failure"):
move_profile_no_replace(source, destination, move=move_then_raise)
held = next(iter(locking_module._PROCESS_LEGACY_LOCKS.values()))
assert held.fd is not None
assert held.path.parent == destination / "state"
assert not source.exists()
def test_windows_handoff_rejects_same_size_lock_content_change(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
source = tmp_path / "source"
destination = tmp_path / "destination"
(source / "state").mkdir(parents=True)
(source / "state" / "gateway.pid.lock").write_bytes(b"a")
monkeypatch.setattr(locking_module, "_windows_requires_legacy_lock_handoff", lambda: True)
def move_then_change_lock(
source_path: Path,
destination_path: Path,
*,
_mutation_guard,
**_move_options: object,
) -> None:
with _mutation_guard():
source_path.rename(destination_path)
(destination_path / "state" / "gateway.pid.lock").write_bytes(b"b")
with LegacyGatewayLock(source):
with pytest.raises(AtomicStateUnknownError, match="could not be reacquired"):
move_profile_no_replace(source, destination, move=move_then_change_lock)
assert destination.is_dir()
assert not source.exists()
def test_windows_handoff_keeps_external_state_lock_open(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
source = tmp_path / "source"
destination = tmp_path / "destination"
external_state = tmp_path / "external-state"
(source / "state").mkdir(parents=True)
external_state.mkdir()
monkeypatch.setattr(locking_module, "_windows_requires_legacy_lock_handoff", lambda: True)
with LegacyGatewayLock(
source,
state_roots=(source / "state", external_state),
) as lock:
external_claim = next(
claim for claim in lock._claims if claim.path.parent == external_state
)
external_fd = external_claim.fd
def guarded_move(
source_path: Path,
destination_path: Path,
*,
_mutation_guard,
**_move_options: object,
) -> None:
assert _move_options["_allowed_manifest_mtime_changes"] == frozenset(
{"state/gateway.pid.lock"}
)
with _mutation_guard():
assert external_claim.fd == external_fd
source_path.rename(destination_path)
move_profile_no_replace(source, destination, move=guarded_move)
assert external_claim.fd == external_fd
def test_moved_legacy_lock_can_be_rebound_without_dropping_exclusion(
tmp_path: Path,
) -> None:
staging = tmp_path / "staging"
target = tmp_path / "target"
(staging / "state").mkdir(parents=True)
with LegacyGatewayLock(staging):
move_profile_no_replace(staging, target)
with LegacyGatewayLock(target):
context = multiprocessing.get_context(
"spawn" if sys.platform == "win32" else "fork"
)
queue = context.Queue()
process = context.Process(
target=_contend_for_gateway,
args=(str(target / "state"), queue),
)
process.start()
process.join(timeout=10)
assert process.exitcode == 0
assert queue.get(timeout=1) == "busy"
@pytest.mark.skipif(
sys.platform == "win32",
reason="manual rebind unit; Windows handoff tamper is covered by native move tests",
)
def test_moved_legacy_lock_rebind_rejects_tampered_destination_state(
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
staging = tmp_path / "staging"
target = tmp_path / "target"
(staging / "state").mkdir(parents=True)
with LegacyGatewayLock(staging):
native_move_no_replace(staging, target)
original_state = target / "state"
parked_state = target / "parked-state"
native_move_no_replace(original_state, parked_state)
original_state.mkdir()
(original_state / "gateway.pid.lock").write_bytes(b"tampered-lock\n")
registry_before = dict(locking_module._PROCESS_LEGACY_LOCKS)
with pytest.raises(UnsafePathError, match="directory identity"):
locking_module.rebind_legacy_gateway_lock(
staging / "state",
target / "state",
)
assert locking_module._PROCESS_LEGACY_LOCKS == registry_before
def test_rebound_target_keeps_displaced_backup_lock_registered_until_release(
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
staging = tmp_path / "staging"
target = tmp_path / "target"
backup = tmp_path / "backup"
(staging / "state").mkdir(parents=True)
(target / "state").mkdir(parents=True)
target_key = locking_module._normalized_path(target / "state" / "gateway.pid.lock")
with LegacyGatewayLock(target):
displaced = locking_module._PROCESS_LEGACY_LOCKS[target_key]
with LegacyGatewayLock(staging):
move_profile_no_replace(target, backup)
move_profile_no_replace(staging, target)
assert displaced in locking_module._PROCESS_LEGACY_LOCKS.values()
def test_windows_nt_create_relative_binds_child_name_to_parent_handle() -> None:
import opensquilla.recovery.locking as locking_module
calls: list[tuple[object, ...]] = []
class FakeNtCreateFile:
argtypes = None
restype = None
def __call__(self, *args: object) -> int:
calls.append(args)
handle_out = args[0]
handle_out._obj.value = 202 # type: ignore[attr-defined]
return 0
handle = locking_module._windows_nt_create_relative(
FakeNtCreateFile(),
parent_handle=101,
name="profile-locks",
desired_access=0x1234,
file_attributes=0x10,
share_access=0x03,
create_disposition=3,
create_options=0x00200021,
)
assert handle == 202
assert len(calls) == 1
(
_handle_out,
desired_access,
object_attributes_pointer,
_io_status,
_allocation_size,
file_attributes,
share_access,
create_disposition,
create_options,
_ea_buffer,
_ea_length,
) = calls[0]
object_attributes = object_attributes_pointer._obj # type: ignore[attr-defined]
unicode_name = object_attributes.object_name.contents
assert object_attributes.root_directory == 101
assert ctypes.wstring_at(unicode_name.buffer, unicode_name.length // 2) == "profile-locks"
assert desired_access == 0x1234
assert file_attributes == 0x10
assert share_access == 0x03
assert create_disposition == 3
assert int(create_options) & 0x00200000 # FILE_OPEN_REPARSE_POINT
def test_windows_profile_lock_native_chain_pins_every_app_owned_component(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
root = tmp_path / "user-state"
path = root / "OpenSquilla" / "profile-locks" / "synthetic.lock"
paths_by_handle: dict[int, Path] = {}
create_calls: list[tuple[object, ...]] = []
relative_calls: list[tuple[int, str, int, int]] = []
closed: list[int] = []
class FakeCreateFile:
argtypes = None
restype = None
def __call__(self, *args: object) -> int:
create_calls.append(args)
paths_by_handle[10] = root
return 10
class FakeNtCreateFile:
argtypes = None
restype = None
def __call__(self, *args: object) -> int:
handle_out = args[0]
object_attributes = args[2]._obj # type: ignore[attr-defined]
unicode_name = object_attributes.object_name.contents
name = ctypes.wstring_at(unicode_name.buffer, unicode_name.length // 2)
parent_handle = int(object_attributes.root_directory)
share_access = int(args[6])
create_options = int(args[8])
candidate = paths_by_handle[parent_handle] / name
if create_options & 0x00000001: # FILE_DIRECTORY_FILE
candidate.mkdir()
else:
candidate.touch()
handle = 10 + len(paths_by_handle)
paths_by_handle[handle] = candidate
handle_out._obj.value = handle # type: ignore[attr-defined]
relative_calls.append((parent_handle, name, share_access, create_options))
return 0
class FakeGetInformation:
argtypes = None
restype = None
def __call__(self, handle: int, info_class: int, output: object, _size: int) -> int:
value = paths_by_handle[int(handle)].lstat()
if info_class == 9:
info = ctypes.cast(
output,
ctypes.POINTER(locking_module._WindowsFileAttributeTagInfo),
).contents
info.file_attributes = 0x10 if stat.S_ISDIR(value.st_mode) else 0x80
return 1
assert info_class == 18
info = ctypes.cast(
output,
ctypes.POINTER(locking_module._WindowsFileIdInfo),
).contents
info.volume_serial_number = value.st_dev
identifier = int(value.st_ino).to_bytes(16, "little")
for index, byte in enumerate(identifier):
info.file_id.identifier[index] = byte
return 1
class FakeCloseHandle:
argtypes = None
restype = None
def __call__(self, handle: int) -> int:
closed.append(int(handle))
return 1
create_file = FakeCreateFile()
get_information = FakeGetInformation()
close_handle = FakeCloseHandle()
nt_create_file = FakeNtCreateFile()
kernel32 = SimpleNamespace(
CreateFileW=create_file,
GetFileInformationByHandleEx=get_information,
CloseHandle=close_handle,
)
ntdll = SimpleNamespace(NtCreateFile=nt_create_file)
monkeypatch.setattr(
locking_module.ctypes,
"WinDLL",
lambda name, **_kwargs: kernel32 if name == "kernel32" else ntdll,
raising=False,
)
converted: list[tuple[int, int]] = []
def open_osfhandle(handle: int, flags: int) -> int:
converted.append((handle, flags))
return os.open(paths_by_handle[handle], os.O_RDWR)
monkeypatch.setitem(
sys.modules,
"msvcrt",
SimpleNamespace(open_osfhandle=open_osfhandle),
)
fd = locking_module._windows_open_profile_lock_file(path, root)
try:
assert os.read(fd, 1) == b"\0"
finally:
os.close(fd)
assert len(create_calls) == 1
assert create_calls[0][2] == 0x00000001 | 0x00000002
assert int(create_calls[0][5]) & 0x00200000
assert [(parent, name) for parent, name, _share, _options in relative_calls] == [
(10, "OpenSquilla"),
(11, "profile-locks"),
(12, "synthetic.lock"),
]
assert all(share == 0x00000001 | 0x00000002 for _, _, share, _ in relative_calls)
assert all(options & 0x00200000 for _, _, _, options in relative_calls)
assert converted and converted[0][0] == 13
assert closed == [12, 11, 10]
def test_windows_profile_lock_preparation_uses_native_handle_relative_opener(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
import opensquilla.recovery.locking as locking_module
root = tmp_path / "user-state"
path = root / "OpenSquilla" / "profile-locks" / "synthetic.lock"
backing = tmp_path / "backing.lock"
backing.write_bytes(b"\0")
backing_fd = os.open(backing, os.O_RDWR)
calls: list[tuple[Path, Path]] = []
def native_open(candidate: Path, candidate_root: Path) -> int:
calls.append((candidate, candidate_root))
return backing_fd
monkeypatch.setattr(
locking_module,
"_windows_open_profile_lock_file",
native_open,
raising=False,
)
def reject_path_open(*_args: object, **_kwargs: object) -> int:
raise AssertionError("Windows profile lock leaf must not be opened by pathname")
monkeypatch.setattr(locking_module.os, "open", reject_path_open)
fd = locking_module._prepare_windows_lock_file(path, root)
try:
assert fd == backing_fd
assert calls == [(path, root)]
finally:
os.close(fd)
@pytest.mark.skipif(sys.platform != "win32", reason="requires native Windows handles")
def test_windows_profile_lock_real_native_chain_and_contention(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
user_state = tmp_path / "user-state"
home = tmp_path / "profile"
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(user_state))
with ProfileOperationLock(home):
lock_path = profile_lock_path(home)
assert lock_path.is_file()
context = multiprocessing.get_context("spawn")
queue = context.Queue()
process = context.Process(
target=_contend_for_lock,
args=(str(home), str(user_state), queue),
)
process.start()
process.join(timeout=15)
assert process.exitcode == 0
assert queue.get(timeout=2) == "busy"
def test_import_source_does_not_inherit_target_process_state_override(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
source = tmp_path / "source"
target = tmp_path / "target"
external_target_state = tmp_path / "active-target-state"
(source / "state").mkdir(parents=True)
target.mkdir()
external_target_state.mkdir()
monkeypatch.setenv(
"OPENSQUILLA_GATEWAY_STATE_DIR",
str(external_target_state),
)
with acquire_legacy_gateway_locks(
source,
target,
read_only_homes=(source,),
):
assert not (source / "state" / "gateway.pid.lock").exists()
assert (external_target_state / "gateway.pid.lock").is_file()
def test_windows_native_move_uses_extended_length_paths() -> None:
deep = "C:\\Users\\synthetic\\" + "nested\\" * 50 + "workspace"
converted = _windows_extended_path(deep)
assert converted.startswith("\\\\?\\C:\\")
assert len(converted) > 260
assert _windows_extended_path(r"\\server\share\workspace").startswith(
"\\\\?\\UNC\\server\\share\\"
)
@pytest.mark.parametrize(
("native_status", "mapped_error", "expected_error"),
[
(0, 0, None),
(ctypes.c_int32(0xC0000035).value, 183, DestinationExistsError),
(ctypes.c_int32(0xC00000D4).value, 17, CrossDeviceMoveError),
(0x00000103, 997, AtomicStateUnknownError),
],
ids=["success", "collision", "cross-volume", "pending"],
)
def test_windows_no_replace_pins_source_parent_source_and_destination_parent_handles(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
native_status: int,
mapped_error: int,
expected_error: type[Exception] | None,
) -> None:
source = tmp_path / "source"
destination_parent = tmp_path / "destination-parent"
source.mkdir()
destination_parent.mkdir()
destination = destination_parent / "destination"
source_parent_stat = source.parent.lstat()
source_stat = source.lstat()
destination_parent_stat = destination_parent.lstat()
created: list[tuple[str, int, int]] = []
closed: list[int] = []
rename_calls: list[tuple[int, int, int, int, int]] = []
mutation_events: list[str] = []
mapped_statuses: list[int] = []
class FakeCall:
argtypes = None
restype = None
def __init__(self, callback):
self.callback = callback
def __call__(self, *args):
return self.callback(*args)
def create_file(path, access, share_mode, *_args):
created.append((path, int(access), int(share_mode)))
return (101, 202, 303)[len(created) - 1]
def get_information(handle, info_class, buffer, _size):
if info_class == 9: # FileAttributeTagInfo
ctypes.memmove(buffer, struct.pack("<II", 0x10, 0), 8)
elif info_class == 18: # FileIdInfo
value = {
101: source_parent_stat,
202: source_stat,
303: destination_parent_stat,
}[handle]
payload = struct.pack("<Q", value.st_dev) + int(value.st_ino).to_bytes(
16, "little"
)
ctypes.memmove(buffer, payload, len(payload))
else: # pragma: no cover - contract assertion
raise AssertionError(f"unexpected info class {info_class}")
return 1
class RenameHeader(ctypes.Structure):
_fields_ = [
("replace_or_flags", ctypes.c_uint32),
("root_directory", ctypes.c_void_p),
("file_name_length", ctypes.c_uint32),
]
def set_information(handle, _io_status, buffer, size, info_class):
mutation_events.append("rename")
header = ctypes.cast(buffer, ctypes.POINTER(RenameHeader)).contents
rename_calls.append(
(
handle,
info_class,
int(header.replace_or_flags),
int(header.root_directory),
int(header.file_name_length),
)
)
assert size >= ctypes.sizeof(RenameHeader)
return native_status
def status_to_error(status):
mapped_statuses.append(status)
return mapped_error
kernel32 = SimpleNamespace(
CreateFileW=FakeCall(create_file),
GetFileInformationByHandleEx=FakeCall(get_information),
CloseHandle=FakeCall(lambda handle: closed.append(handle) or 1),
)
ntdll = SimpleNamespace(
NtSetInformationFile=FakeCall(set_information),
RtlNtStatusToDosError=FakeCall(status_to_error),
)
monkeypatch.setattr(
ctypes,
"WinDLL",
lambda name, **_kwargs: kernel32 if name == "kernel32" else ntdll,
raising=False,
)
monkeypatch.setattr(ctypes, "get_last_error", lambda: 0, raising=False)
class MutationGuard:
def __enter__(self) -> None:
mutation_events.append("guard-enter")
def __exit__(self, *_args: object) -> None:
mutation_events.append("guard-exit")
if expected_error is None:
_windows_move_no_replace(
source,
destination,
_before_mutation=lambda: mutation_events.append("failpoint"),
_mutation_guard=MutationGuard,
)
else:
with pytest.raises(expected_error):
_windows_move_no_replace(
source,
destination,
_before_mutation=lambda: mutation_events.append("failpoint"),
_mutation_guard=MutationGuard,
)
assert created == [
(
_windows_extended_path(source.parent),
0x00000020 | 0x00000080 | 0x00100000,
0x00000001 | 0x00000002,
),
(
_windows_extended_path(source),
0x00010000 | 0x00000080 | 0x00100000,
0x00000001 | 0x00000002 | 0x00000004,
),
(
_windows_extended_path(destination_parent),
0x00000020 | 0x00000080 | 0x00100000,
0x00000001 | 0x00000002,
),
]
assert rename_calls == [
(202, 10, 0, 303, len(destination.name.encode("utf-16-le")))
]
assert mapped_statuses == ([native_status] if native_status < 0 else [])
assert mutation_events == ["guard-enter", "failpoint", "rename", "guard-exit"]
assert closed == [303, 202, 101]
def test_windows_rename_info_keeps_required_nul_terminator_outside_reported_length() -> None:
destination_name = "candidate-\U0001f980-profile"
info = _windows_rename_info(destination_name, 123)
encoded_name = destination_name.encode("utf-16-le")
encoded_buffer = b"".join(int(unit).to_bytes(2, "little") for unit in info.file_name)
assert encoded_buffer.startswith(encoded_name + b"\x00\x00")
assert not encoded_buffer[len(encoded_name) :].strip(b"\x00")
expected_root_offset = 8 if ctypes.sizeof(ctypes.c_void_p) == 8 else 4
assert type(info).root_directory.offset == expected_root_offset
assert type(info).file_name_length.offset == expected_root_offset + ctypes.sizeof(
ctypes.c_void_p
)
assert type(info).file_name.offset == type(info).file_name_length.offset + 4
assert info.replace_or_flags == 0
assert info.root_directory == 123
assert info.file_name_length == len(encoded_name)
assert type(info).file_name.size == len(encoded_name) + 4
class MinimumRenameInformation(ctypes.Structure):
_fields_ = [
("replace_or_flags", ctypes.c_uint32),
("root_directory", ctypes.c_void_p),
("file_name_length", ctypes.c_uint32),
("file_name", ctypes.c_uint16 * 1),
]
assert ctypes.sizeof(info) >= ctypes.sizeof(MinimumRenameInformation) + len(encoded_name)
@pytest.mark.parametrize("destination_name", ["", ".", "..", "unsafe\x00suffix"])
def test_windows_rename_info_rejects_invalid_leaf_names(destination_name: str) -> None:
with pytest.raises(UnsafePathError):
_windows_rename_info(destination_name, 123)
@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows sharing semantics")
def test_windows_no_replace_pins_both_parents_during_real_mutation_window(
tmp_path: Path,
) -> None:
source_parent = tmp_path / "source-parent"
destination_parent = tmp_path / "destination-parent"
source_parent.mkdir()
destination_parent.mkdir()
source = source_parent / "source"
source.mkdir()
(source / "value.txt").write_text("preserved\n", encoding="utf-8")
destination = destination_parent / "destination"
renamed_source_parent = tmp_path / "renamed-source-parent"
renamed_destination_parent = tmp_path / "renamed-destination-parent"
blocked: list[Path] = []
def contend_for_both_parents() -> None:
for current, renamed in (
(source_parent, renamed_source_parent),
(destination_parent, renamed_destination_parent),
):
try:
current.rename(renamed)
except OSError:
blocked.append(current)
_windows_move_no_replace(
source,
destination,
_before_mutation=contend_for_both_parents,
)
assert blocked == [source_parent, destination_parent]
assert source_parent.is_dir()
assert destination_parent.is_dir()
assert not renamed_source_parent.exists()
assert not renamed_destination_parent.exists()
assert not source.exists()
assert (destination / "value.txt").read_text(encoding="utf-8") == "preserved\n"