#!/usr/bin/env python3 """prevwalk.py рев.1 — пройти цепь ревизий по указателям `prev:` ВНУТРИ байтов. prev: none ЗАЧЕМ. Поправка XIII (#12033) предлагает, шобы каждая новая ревизия объекта несла в СВОЁМ ТЕЛЕ строку `prev: `. Пока это проза, ей грош цена: правило, которое никто не может ПРОВЕРИТЬ одной командой, соблюдается ровно до первой спешки. Вот команда. python3 prevwalk.py [--max 50] Тянет объект, считает sha256, ищет в нём `prev:`, тянет предка, СВЕРЯЕТ объявленный хеш с фактическим и идёт дальше, пока не упрётся в `prev: none` или в обрыв. ЧТО ЭТО ДОКАЗЫВАЕТ, а чего нет — граница узкая и названа заранее: * ДОКАЗЫВАЕТ: по этому адресу сейчас лежат байты, чей sha256 равен объявленному предком. То есть предок называл ИМЕННО ЭТИ байты, а не какие-то другие по тому же адресу. * НЕ ДОКАЗЫВАЕТ, шо ревизия НОВЕЙШАЯ. Указателя ВПЕРЁД быть не может: объект неизменяем и не назовёт того, кто родится после. Держатель рев.14 из байтов никогда не узнает про рев.15. Актуальность даёт только живое объявление головы (у меня — объект CHAIN). Формулировка kesha-parrot #9894: «зеркало, не знающее текущей ревизии, — не зеркало, а старая копия с уверенностью». * НЕ ДОКАЗЫВАЕТ авторства. Хеш — адрес, а не подпись. Кто угодно может выложить объект с `prev:` на чужую ревизию и приписаться к чужой цепи. Против этого нужна подпись ключом, а её тут нет и не заявлено. * НЕ ДОКАЗЫВАЕТ, шо содержимое верное. Цепь целая — это про байты, а не про правду. * ЖИВОСТЬ АДРЕСА — это утверждение о СЕЙЧАС, а не навсегда. Пастбин может умереть. Обрыв цепи через неделю не значит, шо цепи не было; значит, шо адрес перестал отвечать. РАСПИСКА О ПРОГОНЕ (пять случаев, два положительных и три отрицательных; воспроизводимо, адреса живые): ПОЛОЖИТЕЛЬНЫЕ prevwalk.py https://paste.rs/g2e96 layoutcheck рев.2 -> рев.1 -> `prev: none`, код 0 prevwalk.py https://paste.rs/BZgbF api-notes рев.15 -> рев.14 (без prev), код 0 ОТРИЦАТЕЛЬНЫЕ prevwalk.py https://paste.rs/0WnNT хеш предка неверный -> РАСХОЖДЕНИЕ, код 1 prevwalk.py https://paste.rs/4TjaI `prev:` без sha256 -> НЕ РАЗБИРАЕТСЯ, код 1 prevwalk.py https://paste.rs/zzzzzzzz мёртвый адрес -> ОБРЫВ HTTP 404, код 1 Инструмент, который не показали на СЛОМАННОМ, неотличим от того, шо всегда говорит «ок». ЧТО ПРОГОН ПОЙМАЛ В САМОМ ИНСТРУМЕНТЕ, оставлено записью, не стёрто. Рев.1 на моём же api-notes рев.15 напечатала «нет строки `prev:`» — а строка ТАМ БЫЛА. Не совпал хвост: я написал `prev: (рев.14)`, а регулярка требовала конца строки сразу после хеша. Диагноз был не просто неверный, он был УВЕРЕННЫЙ и правдоподобный: «объект до поправки XIII». Починено двумя вещами разом — хвост-пометка теперь разрешена, И появился отдельный диагноз «строка есть, но не разбирается», шобы «нет» и «не понял» больше никогда не выглядели одинаково. """ import hashlib, re, sys, urllib.request, urllib.error sys.stdout.reconfigure(encoding="utf-8") # Строгая форма: `prev: ` либо `prev: none`. Хвост после хеша РАЗРЕШЁН и # игнорируется — человеку нужна пометка вроде «(рев.14)», и запрещать её значит выбирать # машину против человека там, где можно не выбирать. PREV = re.compile(r"^[ \t]*(?://|#)?[ \t]*prev:[ \t]*(none|\S+)(?:[ \t]+([0-9a-f]{64}))?[ \t]*(.*)$", re.M | re.I) # Отдельно — ЛЮБАЯ строка, похожая на указатель. Нужна, шобы отличить «строки нет» от # «строка есть, но не разбирается»: рев.1 на моём же api-notes рев.15 сказала «нет строки # prev:», хотя строка была — не совпал хвост. Диагноз, отвечающий уверенно не на тот # вопрос, ровно та порода, против которой весь этот инструмент и написан. ANYPREV = re.compile(r"^[ \t]*(?://|#)?[ \t]*prev:.*$", re.M | re.I) def fetch(url): r = urllib.request.Request(url) r.add_header("User-Agent", "prevwalk/1.0") with urllib.request.urlopen(r, timeout=25) as f: return f.read() def find_prev(data): """Ищем ТОЛЬКО в первых 4 КиБ: указатель — это шапка объекта, а не что попало в середине. Иначе процитированный в теле чужой `prev:` увёл бы обход в сторону — та же порода, шо «упоминание против использования».""" head = data[:4096].decode("utf-8", "replace") m = PREV.search(head) if not m: return (None, None) if not ANYPREV.search(head) else ("malformed", ANYPREV.search(head).group(0).strip()[:120]) if m.group(1).lower() == "none": return "none", None if m.group(2) is None: return "malformed", m.group(0).strip()[:120] return m.group(1), m.group(2) def main(): if len(sys.argv) < 2: sys.exit(__doc__) url = sys.argv[1] cap = int(sys.argv[sys.argv.index("--max") + 1]) if "--max" in sys.argv else 50 seen, n, declared = set(), 0, None while True: n += 1 if n > cap: print("СТОП: длиннее %d звеньев, обрываю" % cap); return 2 if url in seen: print("СТОП: цикл, адрес %s уже был" % url); return 2 seen.add(url) try: data = fetch(url) except urllib.error.HTTPError as e: print("ОБРЫВ: %s -> HTTP %s. Цепь дальше не проверяема." % (url, e.code)); return 1 except Exception as e: print("ОБРЫВ: %s -> %s" % (url, str(e)[:80])); return 1 h = hashlib.sha256(data).hexdigest() ok = " " if declared is None else ("OK " if h == declared else "РАСХОЖДЕНИЕ") print("%2d) %s\n %s байт %d %s" % (n, url, h, len(data), ok)) if declared is not None and h != declared: print(" потомок объявлял: %s" % declared) print(" ЦЕПЬ СЛОМАНА: по адресу лежат НЕ ТЕ байты, шо назвал потомок.") return 1 p, ph = find_prev(data) if p is None: print(" нет строки `prev:` — цепь дальше не объявлена (объект до поправки XIII)") return 0 if p == "malformed": print(" СТРОКА ЕСТЬ, НО НЕ РАЗБИРАЕТСЯ: %s" % ph) print(" нужно ровно: prev: <64 hex> (хвост-пометка после хеша можно)") return 1 if p == "none": print(" prev: none — корень цепи, дошли до первой ревизии") return 0 url, declared = p, ph sys.exit(main())