Files
rippy/src/rippy/platform/dateiangaben.py
T
HitonabiandClaude Opus 5 95beb91364
Ampel / ampel (push) Successful in 1m1s
feat(tools): Ausweichquellen fuer MakeMKV — recherchiert und gemessen
Commander: "bitte baue fuer MakeMKV fallback seiten ein."

Anlass war ein echter Ausfall. Am 28.08.2026 gemessen:

    https://www.makemkv.com/download/   HTTP 525   (dauerhaft)
    https://makemkv.com/download/       HTTP 525
    http://www.makemkv.com/download/    HTTP 403
    https://forum.makemkv.com/          200 / 522 / Zeitablauf (wechselnd)
    web.archive.org/web/2025id_/...exe  200, 16.432.607 Bytes

525/522 heissen: Cloudflare erreicht den Ursprungsserver nicht. Eine Stoerung
beim Hersteller -- dieselbe, die im Projekt schon einmal jeden Worker-Build
lahmgelegt hat.

## Die Kette

Version:  Hersteller-Seite (massgeblich) -> Forum-Ankuendigungen -> Archiv
Datei:    eigene Quelle -> Hersteller -> Internet Archive

Der Hersteller zuerst, immer. Das Archiv ist Rueckfallebene, und Rippy nennt
hinterher die Quelle, aus der die Datei kam.

Zwei Feinheiten, beide aus Messungen statt aus dem Kopf:

* Die HOECHSTE Nummer gewinnt, nicht die erste. Antwortet das Forum nicht,
  meldet das Archiv einen aelteren Stand (1.18.2 statt 1.18.4) -- "neueste
  Fassung" waere dann eine falsche Aussage.
* Ein zweiter Anlauf, mit knapper Zeitgrenze. Drei Abrufe am Forum:
  TimeoutError, HTTP 522, dann Erfolg. Ohne Wiederholung fiel die Quelle in
  zwei von drei Faellen aus; mit 20-Sekunden-Grenzen dauerte der schlimmste
  Fall 46 Sekunden, in denen das Setup eingefroren aussah. Jetzt acht
  Sekunden je Versuch, schlimmstenfalls gut zwanzig.

## Warum eine fremde Quelle trotzdem sicher ist

Der naheliegende Schutz geht NICHT: MakeMKV signiert seinen Installer nicht
(Get-AuthenticodeSignature -> NotSigned, an der echten Datei gemessen). Eine
Signaturpruefung waere eine, die immer fehlschlaegt -- schlimmer als keine,
weil sie Sicherheit vortaeuscht.

Geprueft wird die Versions-Ressource, ebenfalls gemessen:

    CompanyName      GuinpinSoft inc
    FileDescription  MakeMKV installer
    FileVersion      v1.18.4

Das ersetzt keine Signatur, faengt aber ab, was hier wirklich droht: eine
Fehlerseite mit .exe-Namen, ein abgebrochener Download, eine falsche Fassung.

Ende zu Ende nachgewiesen, ohne etwas zu installieren:

    Version laut Kette: 1.18.4
    Hersteller:         HTTP 525 -> uebersprungen
    Internet Archive:   15,7 MB in 6,1s
    Pruefung:           BESTANDEN (MakeMKV v1.18.4)

Die Grenze aus KONZEPT.md 6 bleibt: Rippy liefert MakeMKV weiterhin NICHT
mit. Es holt die Datei des Herstellers -- im Rueckfall aus einem Archiv, das
genau diese Datei aufbewahrt.

Ampel lokal: 733 gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:19:46 +02:00

125 lines
4.9 KiB
Python

"""Was eine Windows-Datei über sich selbst sagt — und warum das hier zählt.
## Wozu (28.08.2026)
Rippy lädt den MakeMKV-Installer notfalls aus einer Ausweichquelle, weil
makemkv.com tagelang mit HTTP 525 antwortet. Eine Datei von woanders zu
holen und ungeprüft auszuführen wäre leichtsinnig.
Der naheliegende Schutz — die digitale Signatur — geht NICHT:
Get-AuthenticodeSignature Setup_MakeMKV_v1.18.4.exe
Status: NotSigned
Am 28.08.2026 an der echten Datei gemessen: **MakeMKV signiert seinen
Installer nicht.** Eine Signaturprüfung wäre also eine Prüfung, die immer
fehlschlägt — schlimmer als keine, weil sie Sicherheit vortäuscht und dann
den richtigen Weg blockiert.
Prüfbar ist die **Versions-Ressource**. An derselben Datei gemessen:
CompanyName GuinpinSoft inc
FileDescription MakeMKV installer
FileVersion v1.18.4
ProductName MakeMKV
Das ist kein Ersatz für eine Signatur — wer die Datei fälscht, kann auch die
Ressource fälschen. Aber es fängt zuverlässig ab, was hier wirklich droht:
eine Fehlerseite, ein umbenanntes Archiv, eine falsche Version, ein
abgebrochener Download. Und es sagt dem Nutzer, WAS er da bekommen hat.
"""
import os
FELDER = ("CompanyName", "FileDescription", "FileVersion", "ProductName",
"ProductVersion")
def versionsangaben(pfad: str) -> dict:
"""Die Versions-Ressource einer Windows-Datei. Leer, wenn keine da ist.
Wirft nicht: Eine Datei ohne Ressource ist ein gültiges Ergebnis (und
genau das, was eine Fehlerseite mit `.exe`-Namen hätte).
"""
if os.name != "nt" or not os.path.isfile(pfad):
return {}
import ctypes
try:
version = ctypes.WinDLL("version")
groesse = version.GetFileVersionInfoSizeW(pfad, None)
if not groesse:
return {}
puffer = ctypes.create_string_buffer(groesse)
if not version.GetFileVersionInfoW(pfad, 0, groesse, puffer):
return {}
zeiger = ctypes.c_void_p()
laenge = ctypes.c_uint()
if not version.VerQueryValueW(puffer, r"\VarFileInfo\Translation",
ctypes.byref(zeiger), ctypes.byref(laenge)):
return {}
paar = ctypes.cast(zeiger, ctypes.POINTER(ctypes.c_uint16))
sprache = "%04x%04x" % (paar[0], paar[1])
ergebnis = {}
for feld in FELDER:
z = ctypes.c_void_p()
n = ctypes.c_uint()
pfad_im_block = "\\StringFileInfo\\%s\\%s" % (sprache, feld)
if version.VerQueryValueW(puffer, pfad_im_block,
ctypes.byref(z), ctypes.byref(n)) and n.value:
ergebnis[feld] = ctypes.wstring_at(z.value, n.value - 1).strip()
return ergebnis
except (AttributeError, OSError, ValueError):
return {}
def ist_programm(pfad: str) -> bool:
"""Fängt die Datei mit `MZ` an? (Das tut jedes Windows-Programm.)
Der billigste und wirksamste Test gegen das, was wirklich passiert: eine
HTML-Fehlerseite, die unter dem Namen `Setup_MakeMKV_v1.18.4.exe`
gespeichert wurde.
"""
try:
with open(pfad, "rb") as f:
return f.read(2) == b"MZ"
except OSError:
return False
# ── Die Prüfung als reine Funktion ──────────────────────────────────────
def passt(angaben: dict, firma: str = "", produkt: str = "",
version: str = "") -> tuple:
"""Passen die Angaben zu dem, was erwartet wird? (ja/nein, Begründung)
Reine Funktion — damit jeder Fall prüfbar ist, ohne eine Datei zu haben.
Leere Erwartungen werden nicht geprüft: Wer nichts fordert, bekommt kein
Urteil vorgesetzt.
"""
if not angaben:
return False, "Die Datei hat keine Versionsangaben — das ist kein " \
"Installationsprogramm, sondern vermutlich eine Fehlerseite."
gefunden_firma = angaben.get("CompanyName", "")
if firma and firma.lower() not in gefunden_firma.lower():
return False, ("Die Datei stammt laut ihren eigenen Angaben von "
"%r, erwartet war %r." % (gefunden_firma or "niemandem", firma))
text = " ".join(angaben.get(f, "") for f in
("ProductName", "FileDescription")).lower()
if produkt and produkt.lower() not in text:
return False, ("Die Datei bezeichnet sich als %r, erwartet war %r."
% (text.strip() or "nichts", produkt))
if version:
gefunden = (angaben.get("FileVersion") or
angaben.get("ProductVersion") or "").lstrip("vV")
if gefunden and not gefunden.startswith(version):
return False, ("Die Datei ist Fassung %s, erwartet war %s."
% (gefunden, version))
return True, "%s %s" % (angaben.get("ProductName", "?"),
angaben.get("FileVersion", "?"))