#!/usr/bin/python3
"""Eccezione temporanea al solo pam_shells di sddm-autologin; radice alternativa fidata."""
import os
from pathlib import Path
import re
import secrets
import stat
import sys

NAME = 'sddm-autologin'
BEGIN = b'# BEGIN stidda-primo-avvio PAM\n'
END = b'# END stidda-primo-avvio PAM\n'
RULE = b'auth [success=1 default=ignore] pam_succeed_if.so quiet user = stidda-primo-avvio\n'
BLOCK = BEGIN + RULE + END


def verifica_salti_precedenti(lines):
    # Inserire o rimuovere un elemento auth può cambiare il bersaglio di un salto.
    for line in lines:
        active = line.split(b'#', 1)[0].strip().lower()
        # Non interpretiamo parzialmente righe logiche o stack auth esterni.
        if active.endswith(b'\\'):
            raise ValueError('layout_pam_con_continuazioni_non_supportato')
        if re.match(rb'(?:-?auth\s+(?:include|substack)|@include)(?:\s|$)', active):
            raise ValueError('layout_auth_con_inclusioni_non_supportato')
        # Il layout supportato usa controlli semplici: non analizziamo valori numerici PAM.
        if re.match(rb'-?auth\s+\[', active):
            raise ValueError('layout_auth_con_controlli_estesi_non_supportato')


def trasforma(text, mode):
    marked = BEGIN.rstrip(b'\n') in text or END.rstrip(b'\n') in text
    present = text.count(BLOCK) == 1
    if marked and (not present or text.count(BEGIN.rstrip(b'\n')) != 1 or text.count(END.rstrip(b'\n')) != 1):
        raise ValueError('blocco_gestito_alterato')
    base = text.replace(BLOCK, b'', 1) if present else text
    if mode == 'ripristina' and present:
        verifica_salti_precedenti(text[:text.index(BLOCK)].splitlines())
        return base  # Le altre regole, anche aggiunte dall'amministratore, restano identiche.
    for line in base.splitlines():
        active = line.split(b'#', 1)[0]
        if b'pam_succeed_if.so' in active and b'stidda-primo-avvio' in active:
            raise ValueError('regola_kiosk_non_gestita')
    if mode == 'ripristina':
        return base
    lines = base.splitlines(keepends=True)
    shells = []
    for i, line in enumerate(lines):
        fields = line.split(b'#', 1)[0].split()
        if any(word.rsplit(b'/', 1)[-1] == b'pam_shells.so' for word in fields):
            if len(fields) < 3 or fields[:2] != [b'auth', b'required'] or fields[2].rsplit(b'/', 1)[-1] != b'pam_shells.so':
                raise ValueError('layout_pam_shells_non_supportato')
            shells.append(i)
    if len(shells) != 1:
        raise ValueError('pam_shells_non_univoco')
    index = shells[0]
    verifica_salti_precedenti(lines[:index])
    offset = sum(map(len, lines[:index]))
    if present and text.index(BLOCK) != offset:
        raise ValueError('blocco_gestito_non_adiacente_a_pam_shells')
    return base[:offset] + BLOCK + base[offset:]


def apri_directory(root):
    if not root.is_absolute() or '..' in root.parts:
        raise ValueError('radice_non_valida')
    flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
    fd = os.open('/', flags)
    try:
        for part in (*root.parts[1:], 'etc', 'pam.d'):
            next_fd = os.open(part, flags, dir_fd=fd)
            os.close(fd)
            fd = next_fd
        return fd
    except BaseException:
        os.close(fd)
        raise


def identita(s):
    return (s.st_dev, s.st_ino, s.st_mtime_ns, s.st_ctime_ns, s.st_size, s.st_mode, s.st_uid, s.st_gid)


def aggiorna(root, mode):
    directory = apri_directory(root)
    temporary = None
    try:
        fd = os.open(NAME, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=directory)
        with os.fdopen(fd, 'rb') as source:
            original = os.fstat(source.fileno())
            if not stat.S_ISREG(original.st_mode) or original.st_nlink != 1:
                raise ValueError('file_pam_non_regolare_o_con_hardlink')
            text = source.read()
        replacement = trasforma(text, mode)
        if replacement == text:
            return
        temporary = '.stidda-primo-avvio-' + secrets.token_hex(8)
        fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC,
                     0o600, dir_fd=directory)
        with os.fdopen(fd, 'wb') as output:
            os.fchown(output.fileno(), original.st_uid, original.st_gid)
            os.fchmod(output.fileno(), stat.S_IMODE(original.st_mode))
            output.write(replacement)
            output.flush()
            os.fsync(output.fileno())
        current = os.stat(NAME, dir_fd=directory, follow_symlinks=False)
        if identita(current) != identita(original):
            raise ValueError('file_pam_modificato_durante_preparazione')
        os.replace(temporary, NAME, src_dir_fd=directory, dst_dir_fd=directory)
        os.fsync(directory)
    finally:
        if temporary is not None:
            try:
                os.unlink(temporary, dir_fd=directory)
            except FileNotFoundError:
                pass
        os.close(directory)


def main():
    if len(sys.argv) not in (2, 3) or sys.argv[1] not in ('prepara', 'ripristina'):
        print('Uso: pam-primo-avvio prepara|ripristina [RADICE_ASSOLUTA]', file=sys.stderr)
        return 2
    root = Path(sys.argv[2] if len(sys.argv) == 3 else '/')
    if root == Path('/') and os.geteuid() != 0:
        print('pam-primo-avvio: richiede_root', file=sys.stderr)
        return 1
    try:
        aggiorna(root, sys.argv[1])
    except ValueError as error:
        print('pam-primo-avvio: ' + str(error), file=sys.stderr)
        return 1
    except OSError:
        print('pam-primo-avvio: errore_file', file=sys.stderr)
        return 1
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
