#!/usr/bin/env python3
"""rrsync must not let a peer turn on the server's --debug output.

Defence in depth, not the fix: what actually stops a merge file's contents
coming back is the redaction in exclude.c (see filter-merge-content-echo).

This closes ONE spelling of the control -- an explicit -M--debug -- and no
more.  It does not close the FILTER trace family: options.c maps verbosity
onto the debug flags, so -vvv still raises a restricted server to FILTER2 and
forwards it, and the trace metadata (merge-file openability, the server's
absolute paths, mergelist bookkeeping) comes back with it.  Rule TEXT stays
redacted at every verbosity, which is the property that matters and which
filter-merge-content-echo pins.  Do not read this test as sealing -vvv.

It costs nothing: server_options() forwards --info but never --debug, so no
stock client sends one, and the only way it reaches a server is a peer asking
for it by name with -M.
"""

import os
import signal
import subprocess
import sys

signal.signal(signal.SIGUSR1, signal.SIG_IGN)
signal.signal(signal.SIGUSR2, signal.SIG_IGN)
if '--shell' in sys.argv:
    index = sys.argv.index('--shell') + 2
    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[index:])}
    signal.signal(signal.SIGUSR1, signal.SIG_DFL)
    signal.signal(signal.SIGUSR2, signal.SIG_DFL)
    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']
    os.execve(wrapper, [wrapper, '-wo', root], env)

from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv

base = SCRATCHDIR / 'rrsync-debug-denied'
rmtree(base)
src = base / 'src'
root = base / 'root'
makepath(src, root)
(src / 'f').write_bytes(b'hi\n')

# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)
# while rrsync hands its RSYNC to execlp() as a single executable name, so it
# has to be wrapped before patched_rrsync() sees it.
shim = base / 'rsync-shim'
shim.write_text('#!/bin/sh\nexec ' + RSYNC + ' "$@"\n')
shim.chmod(0o755)
wrapper = patched_rrsync(base, rsync_path=str(shim))
rsh = f'{sys.executable} {os.path.abspath(__file__)} --shell'
env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}

for spelling in ('-M--debug=FILTER2', '--remote-option=--debug=ALL3'):
    got = subprocess.run(
        rsync_argv('-r', spelling, '-e', rsh, f'{src}/', 'ignored:'),
        env=env, capture_output=True, text=True, timeout=20)
    out = got.stdout + got.stderr
    assert got.returncode != 0 and 'option --debug has been disabled' in out, (
        f'rrsync accepted a peer-selected {spelling}: rc={got.returncode}, '
        f'out={out!r}')

# The option table is generated by the cull-options script, so the entry this
# relies on can come back on a regeneration.  Check the value, not just the
# behaviour, so that is unambiguous when it happens.
assert "'debug': -1," in wrapper.read_text(), (
    "support/rrsync no longer disables 'debug' -- a cull-options regeneration "
    'probably restored it')

# Ordinary transfers must be unaffected -- including -vvv, which reaches
# FILTER2 by verbosity alone and so must NOT be caught by the refusal above.
for extra in ([], ['-vv'], ['-vvv']):
    ok = subprocess.run(
        rsync_argv('-r', *extra, '-e', rsh, f'{src}/', 'ignored:'),
        env=env, capture_output=True, text=True, timeout=20)
    assert ok.returncode == 0 and (root / 'f').read_bytes() == b'hi\n', (
        f'an ordinary transfer broke with {extra}: rc={ok.returncode}, '
        f'stderr={ok.stderr!r}')
