From ae82d8c0fdff76ff7ba282028710964b5425b3f9 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 12 Sep 2026 21:43:13 +0200 Subject: [PATCH] =?UTF-8?q?fix(v5):=20MakeMKV-Installer=20aus=20dem=20Inte?= =?UTF-8?q?rnet=20Archive=20nur=20nach=20R=C3=BCckfrage,=20mit=20Gr=C3=B6?= =?UTF-8?q?=C3=9Fe=20und=20SHA-256?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WAS: makemkvInstallerHolen fragt vor der Archiv-Quelle über eine Freigabe-Funktion; ohne Freigabe wird das Archiv übersprungen und im Bericht genannt. Der Kern stellt die Frage ans Fenster (beschaffung-status mit `frage`), die MakeMKV-Karte zeigt sie mit „Archiv-Kopie laden" / „Lieber nicht" (beschaffung-antwort); ohne Antwort in zehn Minuten gilt Nein. Die geladene Datei wird mit Größe und SHA-256 gemeldet. WARUM: Durchsicht 12.09.2026, Fund F16 (Commander-Entscheid: Archiv nur nach Rückfrage) — eine unsignierte EXE von fremder Hand, die mit Adminrechten läuft, entscheidet der Nutzer, nicht Rippy. Co-Authored-By: Claude Opus 5 --- .../src/fenster/einstellungen/Karten.tsx | 14 +++++ rippy-windows/src/fenster/kernverbindung.ts | 5 ++ rippy-windows/src/gemeinsam/nachrichten.ts | 8 +++ rippy-windows/src/kern/dienste/werkzeuge.ts | 61 +++++++++++++++++-- rippy-windows/src/kern/index.ts | 3 + .../src/kern/werkzeuge/beschaffen.ts | 32 +++++++++- rippy-windows/test/beschaffung.test.ts | 26 +++++++- 7 files changed, 140 insertions(+), 9 deletions(-) diff --git a/rippy-windows/src/fenster/einstellungen/Karten.tsx b/rippy-windows/src/fenster/einstellungen/Karten.tsx index f61d27d..9aa41de 100644 --- a/rippy-windows/src/fenster/einstellungen/Karten.tsx +++ b/rippy-windows/src/fenster/einstellungen/Karten.tsx @@ -22,6 +22,7 @@ import { } from '../bausteine' import { benachrichtigungTesten, + beschaffungAntworten, einstellungSetzen, fremdenOrdnerLoeschen, kompressionAbbrechen, @@ -480,6 +481,19 @@ export function MakemkvKarte({ verbindung }: { verbindung: Verbindung }) { {b !== null && (

{b.text}

+ {b.frage !== undefined && b.frage.length > 0 && ( +
+

{b.frage}

+
+ + +
+
+ )} {b.laeuft && b.anteil !== null && (
diff --git a/rippy-windows/src/fenster/kernverbindung.ts b/rippy-windows/src/fenster/kernverbindung.ts index 4a71a28..f933502 100644 --- a/rippy-windows/src/fenster/kernverbindung.ts +++ b/rippy-windows/src/fenster/kernverbindung.ts @@ -381,6 +381,11 @@ export function schluesselPruefen(): void { senden({ art: 'schluessel-pruefen' }) } +/** 5.7.0 (F16): Ja/Nein auf eine Frage der MakeMKV-Beschaffung. */ +export function beschaffungAntworten(ja: boolean): void { + anKern({ art: 'beschaffung-antwort', ja }, 'Antwort an die Beschaffung') +} + export function einstellungSetzen(schluessel: string, wert: string): void { anKern({ art: 'einstellung-setzen', schluessel, wert }, `Einstellung ${schluessel}`) } diff --git a/rippy-windows/src/gemeinsam/nachrichten.ts b/rippy-windows/src/gemeinsam/nachrichten.ts index 6ebb971..02abd51 100644 --- a/rippy-windows/src/gemeinsam/nachrichten.ts +++ b/rippy-windows/src/gemeinsam/nachrichten.ts @@ -257,6 +257,9 @@ export interface BeschaffungStand { /** 0–1 während des Downloads; null wenn gerade nicht bezifferbar. */ anteil: number | null fehler: boolean + /** 5.7.0 (F16): Rippy wartet auf ein Ja/Nein — z. B. „Archiv-Kopie laden?". + * Fehlt: keine Frage offen. */ + frage?: string } // ── Vorgänge und Kompressions-Warteschlange (5.4.0) ───────────────────── @@ -450,6 +453,8 @@ export type FensterNachricht = | { art: 'makemkv-update' } /** Schlüsselkette sofort neu laufen lassen (z. B. nach einer Installation). */ | { art: 'schluessel-pruefen' } + /** 5.7.0 (F16): Antwort auf eine Frage der MakeMKV-Beschaffung. */ + | { art: 'beschaffung-antwort'; ja: boolean } // ── 5.4.0: Vorgänge, Roh-Dateien, Warteschlange, Laufwerks-Gesundheit ── | { art: 'vorgaenge-laden' } /** Den Roh-Ordner EINES Vorgangs jetzt löschen (von Hand). */ @@ -712,6 +717,9 @@ export function istFensterNachricht(wert: unknown): wert is FensterNachricht { if (wert.art === 'kompression-pause') { return typeof wert.an === 'boolean' } + if (wert.art === 'beschaffung-antwort') { + return typeof wert.ja === 'boolean' + } if (wert.art === 'einstellung-setzen') { return istKennung(wert.schluessel) && typeof wert.wert === 'string' } diff --git a/rippy-windows/src/kern/dienste/werkzeuge.ts b/rippy-windows/src/kern/dienste/werkzeuge.ts index 3004a1f..4386c51 100644 --- a/rippy-windows/src/kern/dienste/werkzeuge.ts +++ b/rippy-windows/src/kern/dienste/werkzeuge.ts @@ -15,6 +15,9 @@ export class WerkzeugDienst { schluesselStand: SchluesselStand | null = null private beschaffungLaeuft = false private installerAntwort: ((fehler: string | null) => void) | null = null + /** 5.7.0 (F16): Die offene Ja/Nein-Frage der Beschaffung — null, wenn keine. */ + private frageAntwort: ((ja: boolean) => void) | null = null + private letzterBeschaffungsText = '' constructor( private readonly kontext: KernKontext, @@ -115,9 +118,40 @@ export class WerkzeugDienst { this.installerAntwort = null } - private beschaffungMelden(text: string, anteil: number | null, laeuft: boolean, fehler = false): void { - this.kontext.anFenster({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft, text, anteil, fehler } }) - this.kontext.protokoll.schreiben(`MakeMKV-Update: ${text}`) + private beschaffungMelden(text: string, anteil: number | null, laeuft: boolean, fehler = false, frage?: string): void { + this.letzterBeschaffungsText = text + this.kontext.anFenster({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft, text, anteil, fehler, ...(frage !== undefined ? { frage } : {}) } }) + this.kontext.protokoll.schreiben(`MakeMKV-Update: ${text}${frage !== undefined ? ` — FRAGE: ${frage}` : ''}`) + } + + /** Antwort des Fensters auf eine offene Frage (beschaffung-antwort). */ + beschaffungAntwort(ja: boolean): void { + const antwort = this.frageAntwort + this.frageAntwort = null + if (antwort === null) { + this.kontext.hinweis('Es ist keine Frage der MakeMKV-Beschaffung offen.') + return + } + antwort(ja) + } + + /** Eine Ja/Nein-Frage ans Fenster; ohne Antwort in zehn Minuten gilt Nein. */ + private frage(text: string): Promise { + return new Promise((fertig) => { + this.frageAntwort = (ja) => { + clearTimeout(zeiger) + this.beschaffungMelden(ja ? 'Freigegeben — die Archiv-Kopie wird geladen.' : 'Nicht freigegeben.', null, true) + fertig(ja) + } + const zeiger = setTimeout(() => { + if (this.frageAntwort === null) return + this.frageAntwort = null + this.beschaffungMelden('Keine Antwort — die Archiv-Kopie wird nicht geladen.', null, true) + fertig(false) + }, 10 * 60_000) + zeiger.unref() + this.beschaffungMelden(this.letzterBeschaffungsText, null, true, false, text) + }) } // ── MakeMKV-Update (§ 9): holen, prüfen, installieren lassen ──────────── @@ -168,8 +202,25 @@ export class WerkzeugDienst { const alteExe = this.makemkvcon() const alteVersion = alteExe.length > 0 ? versionsangaben(alteExe).version : '' - const ergebnis = await makemkvInstallerHolen(neueste, (text, anteil) => this.beschaffungMelden(text, anteil, true)) - this.beschaffungMelden(`Geprüft (Quelle: ${ergebnis.quelle}) — der Installer wird gestartet …`, null, true) + const ergebnis = await makemkvInstallerHolen( + neueste, + (text, anteil) => this.beschaffungMelden(text, anteil, true), + '', + undefined, + undefined, + undefined, + // F16: Das Archiv liefert eine unsignierte EXE, die gleich mit + // Adminrechten läuft — das entscheidet der Nutzer, nicht Rippy. + (quelle) => + this.frage( + `makemkv.com antwortet nicht. Soll Rippy den MakeMKV-Installer ${neueste} aus dem ${quelle} laden? Das ist eine Kopie von fremder Hand, geprüft wird nur, dass sie ein MakeMKV-Programm ist — er läuft danach mit Adminrechten. Wer sicher gehen will, lädt später selbst von makemkv.com.`, + ), + ) + this.beschaffungMelden( + `Geprüft (Quelle: ${ergebnis.quelle}, ${(ergebnis.bytes / 1024 ** 2).toFixed(1)} MB, SHA-256 ${ergebnis.sha256}) — der Installer wird gestartet …`, + null, + true, + ) const startFehler = await new Promise((fertig) => { this.installerAntwort = fertig diff --git a/rippy-windows/src/kern/index.ts b/rippy-windows/src/kern/index.ts index 8a6f792..467f3a0 100644 --- a/rippy-windows/src/kern/index.ts +++ b/rippy-windows/src/kern/index.ts @@ -288,6 +288,9 @@ function fensterNachricht(frage: unknown): void { case 'schluessel-pruefen': void werkzeuge.schluesselLauf().then(() => werkzeuge.senden()) return + case 'beschaffung-antwort': + werkzeuge.beschaffungAntwort(frage.ja) + return case 'vorgaenge-laden': vorgaenge.neuMessen() return diff --git a/rippy-windows/src/kern/werkzeuge/beschaffen.ts b/rippy-windows/src/kern/werkzeuge/beschaffen.ts index d4dae50..64aa683 100644 --- a/rippy-windows/src/kern/werkzeuge/beschaffen.ts +++ b/rippy-windows/src/kern/werkzeuge/beschaffen.ts @@ -22,7 +22,8 @@ // 3. NIE eine gute Datei durch eine halbe ersetzen: Geladen wird in // `.neu`, geprüft, erst dann getauscht. import { spawnSync } from 'node:child_process' -import { createWriteStream, openSync, readSync, closeSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs' +import { createHash } from 'node:crypto' +import { createWriteStream, openSync, readFileSync, readSync, closeSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs' import { win32 as pfad } from 'node:path' import { werkzeugOrdner } from './katalog' @@ -285,6 +286,22 @@ export interface BeschaffungErgebnis { pfad: string quelle: string version: string + /** 5.7.0 (F16): Größe und SHA-256 der geladenen Datei — damit man sie + * gegen die Herstellerangabe halten kann, sobald die Seite wieder da ist. */ + bytes: number + sha256: string +} + +/** SHA-256 einer Datei als Hex — für die Anzeige neben dem Installer. */ +export function pruefsumme(dateiPfad: string): string { + return createHash('sha256').update(readFileSync(dateiPfad)).digest('hex') +} + +/** Welche Quelle ein Rückfrage braucht: alles außer Hersteller und eigener + * Basis. Das Internet Archive liefert eine unsignierte EXE, die mit + * Adminrechten läuft — das entscheidet der Nutzer, nicht Rippy (F16). */ +export function brauchtFreigabe(quellenName: string): boolean { + return quellenName === 'Internet Archive' } /** Lädt den MakeMKV-Installer über die Quellenkette in Rippys @@ -297,6 +314,9 @@ export async function makemkvInstallerHolen( holen: typeof fetch = fetch, ordner: string = pfad.join(werkzeugOrdner(), 'downloads'), pruefen: typeof dateiPruefen = dateiPruefen, + /** 5.7.0 (F16): Darf eine Quelle mit Rückfrage benutzt werden? Ohne + * diese Funktion wird sie übersprungen — nie still geladen. */ + freigabe?: (quellenName: string, url: string) => Promise, ): Promise { mkdirSync(ordner, { recursive: true }) const ziel = pfad.join(ordner, `Setup_MakeMKV_v${version}.exe`) @@ -304,6 +324,14 @@ export async function makemkvInstallerHolen( const fehlschlaege: string[] = [] for (const [name, url] of makemkvQuellen(version, basis)) { + if (brauchtFreigabe(name)) { + const erlaubt = freigabe !== undefined && (await freigabe(name, url)) + if (!erlaubt) { + fehlschlaege.push(`${name}: nicht freigegeben`) + melden(`${name} wurde nicht freigegeben.`, null) + continue + } + } melden(`MakeMKV ${version} wird geladen (${name}) …`, 0) try { await dateiLaden(url, vorlaeufig, melden, holen) @@ -312,7 +340,7 @@ export async function makemkvInstallerHolen( // Erst tauschen, wenn geprüft — ein halber Download darf eine gute // Datei nicht ersetzen. renameSync(vorlaeufig, ziel) - return { pfad: ziel, quelle: name, version } + return { pfad: ziel, quelle: name, version, bytes: statSync(ziel).size, sha256: pruefsumme(ziel) } } catch (fehler) { fehlschlaege.push(`${name}: ${fehler instanceof Error ? fehler.message : String(fehler)}`) melden(`${name} hat nicht geliefert.`, null) diff --git a/rippy-windows/test/beschaffung.test.ts b/rippy-windows/test/beschaffung.test.ts index 9adb5de..f414422 100644 --- a/rippy-windows/test/beschaffung.test.ts +++ b/rippy-windows/test/beschaffung.test.ts @@ -136,10 +136,11 @@ describe('dateiPruefen', () => { describe('makemkvInstallerHolen', () => { const GROSS = Buffer.concat([Buffer.from('MZ'), Buffer.alloc(1024 * 1024 + 100, 7)]) - it('Fehlerseite mit HTTP 200 fällt durch die Mindestgröße — die nächste Quelle gewinnt', async () => { + it('Fehlerseite mit HTTP 200 fällt durch die Mindestgröße — die nächste Quelle gewinnt (mit Freigabe)', async () => { const ordner = tempOrdner() const holen = (async (url: string | URL | Request) => String(url).includes('web.archive.org') ? antwort(GROSS) : antwort('kaputt')) as typeof fetch + const gefragt: string[] = [] const ergebnis = await makemkvInstallerHolen( '1.18.4', () => {}, @@ -147,16 +148,35 @@ describe('makemkvInstallerHolen', () => { holen, ordner, (p, v) => dateiPruefen(p, v, () => ({ firma: 'GuinpinSoft inc', produkt: 'MakeMKV', version: 'v1.18.4' }), 'win32'), + async (quelle) => { + gefragt.push(quelle) + return true + }, ) + expect(gefragt).toEqual(['Internet Archive']) expect(ergebnis.quelle).toBe('Internet Archive') expect(readFileSync(ergebnis.pfad).length).toBe(GROSS.length) + expect(ergebnis.bytes).toBe(GROSS.length) + expect(ergebnis.sha256).toMatch(/^[0-9a-f]{64}$/) expect(existsSync(`${ergebnis.pfad}.neu`)).toBe(false) }) + // 5.7.0 (Durchsicht F16): Ohne Freigabe wird das Archiv NIE benutzt — es + // liefert eine unsignierte EXE, die mit Adminrechten läuft. + it('ohne Freigabe bleibt das Archiv aus — und der Bericht sagt es', async () => { + const ordner = tempOrdner() + const holen = (async (url: string | URL | Request) => + String(url).includes('web.archive.org') ? antwort(GROSS) : antwort('kaputt')) as typeof fetch + await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner)).rejects.toThrow(/Internet Archive: nicht freigegeben/) + await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner, undefined, async () => false)).rejects.toThrow( + /Internet Archive: nicht freigegeben/, + ) + }) + it('liefert keine Quelle, steht im Wurf der Bericht ALLER Versuche', async () => { const ordner = tempOrdner() const holen = (async () => antwort('weg', 404)) as typeof fetch - await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner)).rejects.toThrow( + await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner, undefined, async () => true)).rejects.toThrow( /Hersteller: HTTP 404[\s\S]*Internet Archive: HTTP 404[\s\S]*makemkv\.com/, ) }) @@ -166,6 +186,8 @@ describe('Nachrichten-Schema der Update-Strecke', () => { it('Fenster → Kern: makemkv-update und schluessel-pruefen', () => { expect(istFensterNachricht({ art: 'makemkv-update' })).toBe(true) expect(istFensterNachricht({ art: 'schluessel-pruefen' })).toBe(true) + expect(istFensterNachricht({ art: 'beschaffung-antwort', ja: true })).toBe(true) + expect(istFensterNachricht({ art: 'beschaffung-antwort' })).toBe(false) }) it('Haupt → Kern: installer-ergebnis', () => { expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: null })).toBe(true)