🔒 Repository is read-only – file editing is disabled.

PaganLinux/tmp-apply-https.py main

205 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
#!/usr/bin/env python3
"""Punkt 7: http:// -> https:// w polach source/sources/url receptur.

- Podmienia TYLKO wystapienia zweryfikowane jako SAFE_HTTPS / SAFE_HTTPS_UNVERIFIED.
- Granica dopasowania: po URL musi byc cudzyslow/bialy znak/przecinek/nawias/koniec,
  zeby nie trafic w prefiks dluzszego URL-a (np. url: 'http://x/' + source http://x/files/...).
- Jesli ten sam URL wystepuje tez w build/hooks, plik obslugiwany jest zakresowo
  (tylko blok klucza), a nie globalnie.
- Zachowuje CRLF (odczyt/zapis z newline='').
- Waliduje YAML przed i po; sprawdza, ze zmienily sie dokladnie oczekiwane wartosci.
"""
import collections
import json
import os
import re
import sys

import yaml

R = "/var/lib/pagan-sync/recipes"
AUDIT = "/tmp/https-audit.json"
CONV = ("SAFE_HTTPS", "SAFE_HTTPS_UNVERIFIED")
LIST_KEYS = ("source", "sources")
SCALAR_KEYS = ("url",)
APPLY = "--apply" in sys.argv


def as_list(v):
    if v is None:
        return []
    return v if isinstance(v, list) else [v]


def url_pat(url):
    return re.compile(re.escape(url) + r'(?=["\'\s\],]|$)')


def block_span(text, key, top_only=True):
    """Zwraca (start, end) zakresu bloku listy dla klucza (razem z linia klucza)."""
    m = re.search(r"(?m)^%s:" % re.escape(key), text)
    if not m:
        return None
    line_end = text.find("\n", m.start())
    if line_end == -1:
        line_end = len(text)
    rest = text[m.end():line_end].replace("\r", "").strip()
    if rest and not rest.startswith("["):
        return (m.start(), line_end)  # skalarny/nie-listowy
    pos = line_end + 1
    end = line_end
    item_indent = None
    while pos < len(text):
        nl = text.find("\n", pos)
        if nl == -1:
            nl = len(text)
        raw = text[pos:nl].replace("\r", "")
        if raw.strip() == "":
            break
        mi = re.match(r"^(\s*)-\s", raw)
        if mi:
            if item_indent is None or len(mi.group(1)) == item_indent:
                item_indent = len(mi.group(1))
                end = nl
                pos = nl + 1
                continue
        if raw[0] in " \t":
            end = nl
            pos = nl + 1
            continue
        break
    return (m.start(), end)


def in_other_value(d, urls):
    """Czy ktorys URL wystepuje w build/hooks (poza polami zrodel)?"""
    hits = []
    for k in ("build", "hooks", "pre-install", "post-install", "prepare"):
        v = d.get(k)
        if v is None:
            continue
        s = json.dumps(v) if not isinstance(v, str) else v
        for u in urls:
            if u in s:
                hits.append((k, u))
    return hits


def main():
    audit = json.load(open(AUDIT))
    occ = [i for c in CONV for i in audit.get(c, [])]
    by_file = collections.defaultdict(list)
    for i in occ:
        by_file[i["recipe"]].append(i)

    changed_files = 0
    changed_occ = 0
    problems = []
    scoped_files = []
    report = []

    for rel in sorted(by_file):
        path = os.path.join(R, rel)
        text = open(path, newline="", encoding="utf-8").read()
        try:
            before = yaml.safe_load(text) or {}
        except Exception as e:
            problems.append(f"{rel}: YAML przed nie parsuje sie: {e}")
            continue

        targets = by_file[rel]
        urls = [t["raw"] for t in targets]
        hits = in_other_value(before, urls)

        new = text
        applied = 0
        if hits:
            scoped_files.append((rel, hits))
            # tryb zakresowy: podmieniaj tylko w blokach kluczy
            for key in LIST_KEYS + SCALAR_KEYS:
                key_urls = [t["raw"] for t in targets if t["key"] == key]
                if not key_urls:
                    continue
                sp = block_span(new, key)
                if not sp:
                    problems.append(f"{rel}: zakresowy, brak bloku {key}")
                    continue
                s, e = sp
                seg = new[s:e]
                for u in key_urls:
                    seg2, n = url_pat(u).subn("https://" + u[7:], seg, count=1)
                    if n:
                        applied += n
                    else:
                        problems.append(f"{rel}: zakresowo nie znaleziono {u}")
                    seg = seg2
                new = new[:s] + seg + new[e:]
        else:
            for t in targets:
                new2, n = url_pat(t["raw"]).subn("https://" + t["raw"][7:], new, count=1)
                if n:
                    applied += n
                else:
                    problems.append(f"{rel}: nie znaleziono doslownie {t['raw']}")
                new = new2

        if applied == 0:
            continue

        try:
            after = yaml.safe_load(new) or {}
        except Exception as e:
            problems.append(f"{rel}: YAML PO edycji nie parsuje sie: {e}")
            continue

        # walidacja: zmienily sie dokladnie oczekiwane wartosci
        ok = True
        for key in LIST_KEYS + SCALAR_KEYS:
            ob, oa = before.get(key), after.get(key)
            if ob == oa:
                continue
            wb, wa = as_list(ob), as_list(oa)
            if len(wb) != len(wa):
                problems.append(f"{rel}: {key} zmieniona dlugosc listy")
                ok = False
                continue
            for i, (x, y) in enumerate(zip(wb, wa)):
                if x == y:
                    continue
                expect = str(x).replace("http://", "https://", 1)
                if str(y) != expect:
                    problems.append(f"{rel}: {key}[{i}] nieoczekiwana zmiana: {x!r} -> {y!r}")
                    ok = False
        # zadne inne klucze nie moga sie zmienic
        for k in set(before) | set(after):
            if k in LIST_KEYS + SCALAR_KEYS:
                continue
            if before.get(k) != after.get(k):
                problems.append(f"{rel}: nieoczekiwana zmiana klucza {k}")
                ok = False
        if not ok:
            continue

        if APPLY:
            with open(path, "w", newline="", encoding="utf-8") as f:
                f.write(new)
        changed_files += 1
        changed_occ += applied
        report.append(f"{'ZMIENIONO' if APPLY else 'DRY'}: {rel} ({applied})")

    for line in report:
        print(line)
    print()
    print(f"plikow: {changed_files} | wystapien: {changed_occ} | tryb: {'APPLY' if APPLY else 'DRY-RUN'}")
    if scoped_files:
        print(f"\npliki z trybem zakresowym (URL tez w build/hooks): {len(scoped_files)}")
        for rel, hits in scoped_files:
            print(f"   {rel}: {hits}")
    if problems:
        print(f"\nPROBLEMY ({len(problems)}):")
        for p in problems:
            print("   !", p)


if __name__ == "__main__":
    sys.exit(main())