139 lines
5.5 KiB
Python
139 lines
5.5 KiB
Python
"""Parity coverage for an absolute ``--temp-dir`` that lies inside the receive root.
|
|
|
|
FastSync confines the ``--temp-dir`` scratch directory to the receive root. It
|
|
previously rejected *every* absolute path; it now canonicalizes an absolute path
|
|
with ``realpath(3)`` and accepts it when it resolves inside the canonical receive
|
|
root (the destination is identical, so this is a pure parity win), while an
|
|
absolute path that escapes the root stays rejected with a clear error.
|
|
|
|
Two paths exercise the same receiver-side resolution:
|
|
|
|
* the local ``--read-batch`` apply (no network; the batch destination is the
|
|
receive root), and
|
|
* a real TCP transfer against the shared server (the destination root is the
|
|
client-supplied absolute path).
|
|
|
|
The out-of-root case asserts the run fails without writing a single scratch
|
|
file, so the confinement invariant is preserved.
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from common import CLIENT_CMD, TEST_DATA_DIR, clean_dir, get_dest_received_dir, run_client
|
|
|
|
FILES = {
|
|
"top.txt": b"top level\n",
|
|
"sub/nested.txt": b"nested file\n" * 16,
|
|
}
|
|
|
|
|
|
def _run(args):
|
|
return subprocess.run(CLIENT_CMD + args, capture_output=True, text=True, timeout=180)
|
|
|
|
|
|
def _seed_source(root):
|
|
clean_dir(root)
|
|
for rel, data in FILES.items():
|
|
full = os.path.join(root, rel)
|
|
os.makedirs(os.path.dirname(full), exist_ok=True)
|
|
with open(full, "wb") as fh:
|
|
fh.write(data)
|
|
|
|
|
|
def _make_batch(tmp, source):
|
|
batch = os.path.join(tmp, "tree.batch")
|
|
result = _run(["--only-write-batch", batch, source])
|
|
assert result.returncode == 0, (result.stdout, result.stderr)
|
|
return batch
|
|
|
|
|
|
def _read(path):
|
|
with open(path, "rb") as fh:
|
|
return fh.read()
|
|
|
|
|
|
@pytest.mark.ci
|
|
def test_read_batch_absolute_temp_dir_inside_root_accepted(tmp_path):
|
|
source = os.path.join(tmp_path, "src")
|
|
dest = os.path.join(tmp_path, "dst")
|
|
_seed_source(source)
|
|
clean_dir(dest)
|
|
scratch = os.path.join(dest, "scratch")
|
|
os.makedirs(scratch)
|
|
# Stamp the scratch dir with an old mtime so the test can prove the receiver
|
|
# really created (and then removed) its temp file there: the directory mtime
|
|
# changes when an entry is created/removed, so a silently-ignored --temp-dir
|
|
# would leave the stamp untouched. An empty scratch dir alone does not
|
|
# distinguish "used and cleaned up" from "never used".
|
|
stale_mtime = 946684800 # 2000-01-01
|
|
os.utime(scratch, (stale_mtime, stale_mtime))
|
|
|
|
batch = _make_batch(str(tmp_path), source)
|
|
result = _run(["--read-batch", batch, dest, "--temp-dir", scratch])
|
|
assert result.returncode == 0, (result.stdout, result.stderr)
|
|
|
|
received = get_dest_received_dir(dest, source)
|
|
for rel, data in FILES.items():
|
|
assert _read(os.path.join(received, rel)) == data, f"content mismatch for {rel}"
|
|
assert os.listdir(scratch) == [], "scratch dir was not left clean"
|
|
assert os.stat(scratch).st_mtime != stale_mtime, (
|
|
"--temp-dir scratch dir was never written to (temp file not created there)"
|
|
)
|
|
|
|
|
|
@pytest.mark.ci
|
|
def test_read_batch_absolute_temp_dir_outside_root_rejected(tmp_path):
|
|
source = os.path.join(tmp_path, "src")
|
|
dest = os.path.join(tmp_path, "dst")
|
|
_seed_source(source)
|
|
clean_dir(dest)
|
|
outside = os.path.join(tmp_path, "outside")
|
|
os.makedirs(outside)
|
|
|
|
batch = _make_batch(str(tmp_path), source)
|
|
result = _run(["--read-batch", batch, dest, "--temp-dir", outside])
|
|
assert result.returncode != 0, "an absolute temp dir outside the receive root must be rejected"
|
|
assert os.listdir(outside) == [], "receiver wrote into an unconfined temp dir"
|
|
assert "temp-dir" in (result.stdout + result.stderr), (result.stdout, result.stderr)
|
|
|
|
|
|
def test_tcp_absolute_temp_dir_inside_root_accepted(shared_server):
|
|
source = os.path.join(TEST_DATA_DIR, "tempdir_abs_in_src")
|
|
dest = os.path.join(TEST_DATA_DIR, "tempdir_abs_in_dst")
|
|
_seed_source(source)
|
|
clean_dir(dest)
|
|
scratch = os.path.join(dest, "scratch")
|
|
os.makedirs(scratch)
|
|
# See test_read_batch_absolute_temp_dir_inside_root_accepted: the stale
|
|
# mtime makes actual scratch usage observable (the temp file creation and
|
|
# removal bump the directory mtime).
|
|
stale_mtime = 946684800 # 2000-01-01
|
|
os.utime(scratch, (stale_mtime, stale_mtime))
|
|
|
|
result, _ = run_client(source, dest, flags=["--temp-dir", scratch], port=shared_server.port)
|
|
assert result.returncode == 0, (result.stdout, result.stderr)[:300]
|
|
received = get_dest_received_dir(dest, source)
|
|
for rel, data in FILES.items():
|
|
assert _read(os.path.join(received, rel)) == data, f"content mismatch for {rel}"
|
|
assert os.listdir(scratch) == [], "scratch dir was not left clean"
|
|
assert os.stat(scratch).st_mtime != stale_mtime, (
|
|
"--temp-dir scratch dir was never written to (temp file not created there)"
|
|
)
|
|
|
|
|
|
def test_tcp_absolute_temp_dir_outside_root_rejected(shared_server):
|
|
source = os.path.join(TEST_DATA_DIR, "tempdir_abs_out_src")
|
|
dest = os.path.join(TEST_DATA_DIR, "tempdir_abs_out_dst")
|
|
_seed_source(source)
|
|
clean_dir(dest)
|
|
outside = os.path.join(TEST_DATA_DIR, "tempdir_abs_out_scratch")
|
|
clean_dir(outside)
|
|
|
|
result, _ = run_client(source, dest, flags=["--temp-dir", outside], port=shared_server.port)
|
|
assert result.returncode != 0, "an absolute temp dir outside the receive root must be rejected"
|
|
assert os.listdir(outside) == [], "receiver wrote into an unconfined temp dir"
|