#!/usr/bin/env python3
# Python rewrite of testsuite/devices.test (and, via a Makefile-built
# symlink, of devices-fake.test).
#
# Test rsync's handling of device nodes (char/block/fifo) plus the
# fake-super variant that encodes device numbers in the
# user.rsync.%stat xattr instead of mknod-ing real devices.

import os
import sys

import rsyncfns
from rsyncfns import (
    CHKDIR, CHKFILE, FROMDIR, OUTFILE, RSYNC_PREFIX, TMPDIR, TODIR,
    all_plus, allspace, dots,
    checkdiff, hands_setup, makepath, rsync_ls_lR, run_rsync,
    test_fail, test_skipped, v_filt, xattr_set, xattrs_supported,
)


script_name = os.path.basename(sys.argv[0] if sys.argv[0] else __file__)
fake_variant = 'fake' in script_name

if fake_variant:
    if not xattrs_supported():
        test_skipped("Rsync needs xattrs for fake device tests")

    rsyncfns.RSYNC = rsyncfns.RSYNC + ' --fake-super'
    rsyncfns.TLS_ARGS = (rsyncfns.TLS_ARGS + ' --fake-super').strip()

    def make_special(path, kind: str, major: int = 0, minor: int = 0) -> bool:
        """Pretend to mknod `path` as kind {'p','c','b'} via rsync's
        fake-super "%stat" xattr (name/namespace handled per-OS by
        xattr_set). Returns True on success, False if the FS rejects it.
        """
        mode = {'p': 0o10644, 'c': 0o20644, 'b': 0o60644}[kind]
        try:
            with open(path, 'w'):
                pass
            xattr_set(f'{RSYNC_PREFIX}.%stat',
                      f"{mode:o} {major},{minor} 0:0", path)
            return True
        except OSError:
            return False
else:
    my_uid = os.getuid()
    if my_uid != 0:
        # Try fakeroot, mirroring the shell test.
        fakeroot_path = os.environ.get('FAKEROOT_PATH')
        if fakeroot_path and os.access(fakeroot_path, os.X_OK):
            print("Let's try re-running the script under fakeroot...")
            os.execv(fakeroot_path, [fakeroot_path, sys.executable, __file__])
        test_skipped("Rsync needs root/fakeroot for device tests")

    def make_special(path, kind: str, major: int = 0, minor: int = 0) -> bool:
        try:
            if kind == 'p':
                os.mkfifo(path)
            else:
                mode = 0o644 | (0o020000 if kind == 'c' else 0o060000)
                os.mknod(path, mode, os.makedev(major, minor))
            return True
        except OSError:
            return False


# Does this build of rsync support hard-linking specials?
vv = run_rsync('-VV', check=True, capture_output=True)
can_hlink_special = '"hardlink_specials": true' in vv.stdout

FROMDIR.mkdir(parents=True, exist_ok=True)
TODIR.mkdir(parents=True, exist_ok=True)

if not make_special(FROMDIR / 'char', 'c', 41, 67):
    test_skipped("Can't create char device node")
if not make_special(FROMDIR / 'char2', 'c', 42, 68):
    test_skipped("Can't create char device node")
if not make_special(FROMDIR / 'char3', 'c', 42, 69):
    test_skipped("Can't create char device node")
if not make_special(FROMDIR / 'block', 'b', 42, 69):
    test_skipped("Can't create block device node")
if not make_special(FROMDIR / 'block2', 'b', 42, 73):
    test_skipped("Can't create block device node")
if not make_special(FROMDIR / 'block3', 'b', 105, 73):
    test_skipped("Can't create block device node")

if can_hlink_special:
    try:
        os.link(FROMDIR / 'block3', FROMDIR / 'block3.5')
    except OSError:
        # The shell test prints a "Skipping hard-linked device test..." line
        # when it can't link the device; let it slide here, too.
        print("Skipping hard-linked device test... (link failed)")
        can_hlink_special = False
else:
    print("Skipping hard-linked device test...")

if not make_special(FROMDIR / 'fifo', 'p'):
    test_skipped("Can't run mkfifo")

# Match block/block2 timestamps so the diff doesn't drift.
ref = (FROMDIR / 'block').stat()
os.utime(FROMDIR / 'block', (ref.st_atime, ref.st_mtime), follow_symlinks=False)
os.utime(FROMDIR / 'block2', (ref.st_atime, ref.st_mtime), follow_symlinks=False)

checkdiff(['-ai', f'{FROMDIR}/block', f'{TODIR}/block2'],
          f"cD{all_plus} block\n")

checkdiff(['-ai', f'{FROMDIR}/block2', f'{TODIR}/block'],
          f"cD{all_plus} block2\n")

import time
time.sleep(1)

checkdiff(['-Di', f'{FROMDIR}/block3', f'{TODIR}/block'],
          f"cDc.T.{dots} block3\n")

# Build the expected -aiHvv listing.
chkfile_lines = [
    f".d..t.{dots} ./",
    f"cDc.t.{dots} block",
    f"cDc...{dots} block2",
    f"cD{all_plus} block3",
]
if can_hlink_special:
    chkfile_lines.append(f"hD{all_plus} block3.5 => block3")
chkfile_lines += [
    f"cD{all_plus} char",
    f"cD{all_plus} char2",
    f"cD{all_plus} char3",
    f"cS{all_plus} fifo",
]
expected = '\n'.join(chkfile_lines) + '\n'

checkdiff(['-aiHvv', f'{FROMDIR}/', f'{TODIR}/'], expected, filter=v_filt)

print("check how the directory listings compare with diff:\n")
ls_from = rsync_ls_lR(FROMDIR)
ls_to = rsync_ls_lR(TODIR)
if ls_from != ls_to:
    from difflib import unified_diff
    sys.stdout.write(''.join(unified_diff(
        ls_from.splitlines(keepends=True),
        ls_to.splitlines(keepends=True),
        fromfile='from', tofile='to',
    )))
    test_fail("from/to listings differ after device transfer")

if can_hlink_special:
    expected = (
        f"created directory {CHKDIR}\n"
        f"cd{allspace} ./\n"
        f"hD{allspace} block\n"
        f"hD{allspace} block2\n"
        f"hD{allspace} block3\n"
        f"hD{allspace} block3.5\n"
        f"hD{allspace} char\n"
        f"hD{allspace} char2\n"
        f"hD{allspace} char3\n"
        f"hS{allspace} fifo\n"
    )
    checkdiff(['-aii', f'--link-dest={TODIR}',
               f'{FROMDIR}/', f'{CHKDIR}/'], expected)
