#!/usr/bin/python3 -I
"""Walnut: after a successful apt update, print one notice line when a newer Walnut RELEASE is available (not when a single
package has an update). ps3-system's stanza in stable's Packages list carries a Walnut-Release field (the
top-level VERSION at build time); the notice compares the highest ps3-system version's field with VERSION_ID in
/etc/os-release. Reads only /var/lib/apt/lists/walnut.nanobyte.dev_* of the suites enabled in walnut.sources (streamed, no
apt cache), runs dpkg --compare-versions, writes only to stdout. Exits 0 whatever happens (apt update must never fail
because of it), within 30 s.
Usage: apt-update-notice [LISTS_DIR [SOURCES_FILE [BIN_DIR [OS_RELEASE]]]]  (arguments are for the host test)"""
import sys

try:
    import glob, gzip, lzma, os, re, signal, subprocess
    signal.alarm(30)  # overall deadline: SIGALRM kills the process, apt goes on

    VERS = re.compile(r"^(?:[0-9]+:)?[0-9][A-Za-z0-9.+~-]*$")  # a package version: no leading '-', no spaces
    REL = re.compile(r"^[0-9][0-9.]*$")  # a release (VERSION)
    SUITE = re.compile(r"^[a-z][a-z0-9-]*$")
    MAX_LINE = 65536
    MAX_LINES = 5000000

    a = sys.argv
    lists = a[1] if len(a) > 1 else "/var/lib/apt/lists"
    sources = a[2] if len(a) > 2 else "/etc/apt/sources.list.d/walnut.sources"
    bindir = a[3] if len(a) > 3 else "/usr/bin"
    osrel = a[4] if len(a) > 4 else "/etc/os-release"

    def suites():
        """The suites of the enabled stanzas (a missing Enabled: means yes)."""
        out, cur = [], {}
        with open(sources, encoding="utf-8", errors="replace") as f:
            for line in list(f)[:500] + ["\n"]:
                if line.strip() == "":
                    if cur.get("uris", "").startswith("https://walnut.nanobyte.dev/apt") \
                            and cur.get("enabled", "yes").lower() in ("yes", "true", "1"):
                        out += [s for s in cur.get("suites", "").split() if SUITE.match(s)]
                    cur = {}
                elif not line.startswith("#") and ":" in line:
                    k, v = line.split(":", 1)
                    cur[k.strip().lower()] = v.strip()
        return out

    def installed_release():
        with open(osrel, encoding="utf-8", errors="replace") as f:
            for _ in range(200):
                line = f.readline(4096)
                if not line:
                    break
                if line.startswith("VERSION_ID="):
                    v = line[11:].strip().strip('"')
                    return v if REL.match(v) else None
        return None

    def opener(path):
        if path.endswith(".gz"):
            return gzip.open(path, "rt", encoding="utf-8", errors="replace")
        if path.endswith(".xz"):
            return lzma.open(path, "rt", encoding="utf-8", errors="replace")
        return open(path, encoding="utf-8", errors="replace")

    def stanzas(path):
        """(Version, Walnut-Release or None) of each ps3-system stanza, one pass with bounded line reads."""
        pkg = ver = rel = None
        with opener(path) as f:
            for _ in range(MAX_LINES):
                line = f.readline(MAX_LINE)
                if not line:
                    break
                if line.strip() == "":
                    if pkg == "ps3-system" and ver:
                        yield ver, rel
                    pkg = ver = rel = None
                elif line.startswith("Package:"):
                    pkg = line[8:].strip()
                elif line.startswith("Version:"):
                    ver = line[8:].strip()
                elif line.startswith("Walnut-Release:"):
                    rel = line[15:].strip()
        if pkg == "ps3-system" and ver:
            yield ver, rel

    def cmp_gt(x, y):
        return subprocess.run([os.path.join(bindir, "dpkg"), "--compare-versions", x, "gt", y],
                              stdin=subprocess.DEVNULL, capture_output=True, timeout=20).returncode == 0

    def main():
        have = installed_release()
        if not have:
            return
        files = []
        for s in suites():
            if s != "stable":  # a release is what stable carries; a testing build is no release
                continue
            files += sorted(glob.glob(os.path.join(lists, "walnut.nanobyte.dev_apt_dists_%s_*_Packages*" % s)))
        files = [x for x in files if x.endswith(("_Packages", "_Packages.gz", "_Packages.xz"))]
        best = None  # (package version, release) of the highest ps3-system version
        for fn in files:
            try:
                for ver, rel in stanzas(fn):
                    if not VERS.match(ver) or best is not None and not cmp_gt(ver, best[0]):
                        continue
                    best = (ver, rel)
            except Exception:
                continue
        if best and best[1] and REL.match(best[1]) and cmp_gt(best[1], have):
            print("Notice: A new version of walnut is available, upgrade using `apt full-upgrade`")

    main()
except BaseException:
    pass
sys.exit(0)
