lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | import { createHash } from 'node:crypto'; |
| 2 | import { readFile, readdir, writeFile } from 'node:fs/promises'; |
| 3 | import { fileURLToPath, URL } from 'node:url'; |
| 4 | import { join } from 'node:path'; |
| 5 | |
| 6 | /** |
| 7 | * Pruefsummen der Auslieferungspakete. |
| 8 | * |
| 9 | * Wozu das gut ist: Wer die Anwendung weitergibt oder herunterlaedt, kann damit |
| 10 | * feststellen, dass die Datei unterwegs nicht beschaedigt oder veraendert |
| 11 | * wurde. Bei einem Programm, dessen Ergebnisse in ein verkehrsbehoerdliches |
| 12 | * Verfahren gehen, ist das kein Beiwerk: Ein Pruefer muss sagen koennen, mit |
| 13 | * welchem Programmstand eine Unterlage erstellt wurde. |
| 14 | * |
| 15 | * Wogegen es NICHT hilft, und das steht auch in der erzeugten Datei: Wer die |
| 16 | * Pakete austauschen kann, kann die Prueflistendatei daneben genauso |
| 17 | * austauschen. Eine Prueflistendatei belegt Unversehrtheit nur, soweit sie |
| 18 | * selbst aus einer Quelle stammt, der man traut. Gegen einen Angreifer, der den |
| 19 | * Auslieferungsweg beherrscht, hilft ausschliesslich eine Codesignatur. |
| 20 | */ |
| 21 | |
| 22 | const WURZEL = fileURLToPath(new URL('..', import.meta.url)); |
| 23 | const AUSGABE = join(WURZEL, 'release'); |
| 24 | |
| 25 | /** Endungen, die ausgeliefert werden - Zwischenstaende bleiben aussen vor. */ |
| 26 | const AUSGELIEFERT = /\.(exe|msi|dmg|zip|AppImage|deb|rpm|snap)$/i; |
| 27 | |
| 28 | const paket = JSON.parse(await readFile(join(WURZEL, 'package.json'), 'utf8')); |
| 29 | const version = paket.version; |
| 30 | |
| 31 | let eintraege; |
| 32 | try { |
| 33 | eintraege = await readdir(AUSGABE, { withFileTypes: true }); |
| 34 | } catch { |
| 35 | console.error(`Kein Ordner "release". Bauen Sie zuerst ein Paket, etwa mit "npm run dist:win".`); |
| 36 | process.exit(1); |
| 37 | } |
| 38 | |
| 39 | /* |
| 40 | * Nur die Pakete DIESER Version. |
| 41 | * |
| 42 | * Im Ordner liegen erfahrungsgemaess auch aeltere Staende. Eine Liste, die |
| 43 | * fremde Versionen mitfuehrt, ist schlimmer als keine: Sie verleitet dazu, die |
| 44 | * falsche Zeile zu vergleichen. |
| 45 | */ |
| 46 | const dateien = eintraege |
| 47 | .filter((e) => e.isFile() && AUSGELIEFERT.test(e.name) && e.name.includes(version)) |
| 48 | .map((e) => e.name) |
| 49 | .sort(); |
| 50 | |
| 51 | if (dateien.length === 0) { |
| 52 | console.error(`Keine Pakete der Version ${version} in "release" gefunden.`); |
| 53 | process.exit(1); |
| 54 | } |
| 55 | |
| 56 | const zeilen = []; |
| 57 | for (const name of dateien) { |
| 58 | const inhalt = await readFile(join(AUSGABE, name)); |
| 59 | const summe = createHash('sha256').update(inhalt).digest('hex'); |
| 60 | zeilen.push({ name, summe, bytes: inhalt.byteLength }); |
| 61 | } |
| 62 | |
| 63 | /* |
| 64 | * Oertliches Datum, nicht Weltzeit. |
| 65 | * |
| 66 | * Hier stand `new Date().toISOString().slice(0, 10)`. `toISOString` liefert |
| 67 | * Weltzeit, und der Abschnitt schnitt das 'Z' ab, das den Wert als solche |
| 68 | * ausgewiesen haette. Uebrig blieb ein nackter Kalendertag unter einer |
| 69 | * deutschen Beschriftung - nach ISO 8601 als Ortszeit zu lesen. Deutschland |
| 70 | * liegt oestlich von Greenwich: Zwischen 00:00 und 02:00 Ortszeit (Sommerzeit) |
| 71 | * beziehungsweise 00:00 und 01:00 (Winterzeit) haette die Datei den Vortag |
| 72 | * getragen, waehrend die Pakete daneben den laufenden Tag tragen. Bei einer |
| 73 | * Datei, die als Nachweis dient, laeuft ein solcher Abgleich sonst ins Leere. |
| 74 | */ |
| 75 | const jetzt = new Date(); |
| 76 | const zweistellig = (zahl) => String(zahl).padStart(2, '0'); |
| 77 | const heute = `${jetzt.getFullYear()}-${zweistellig(jetzt.getMonth() + 1)}-${zweistellig(jetzt.getDate())}`; |
| 78 | const text = [ |
| 79 | `LSA-Planer Professional ${version} – Prüfsummen`, |
| 80 | `Erstellt am ${heute}`, |
| 81 | '', |
| 82 | 'So prüfen Sie eine Datei unter Windows (PowerShell, im Ordner der Datei):', |
| 83 | '', |
| 84 | ' Get-FileHash "<Dateiname>" -Algorithm SHA256', |
| 85 | '', |
| 86 | 'Die ausgegebene Zeichenkette muss mit der unten stehenden übereinstimmen.', |
| 87 | 'Groß- und Kleinschreibung spielt dabei keine Rolle.', |
| 88 | '', |
| 89 | 'Was eine Prüfsumme leistet – und was nicht:', |
| 90 | 'Sie belegt, dass die Datei genau die ist, die hier aufgeführt wurde. Damit', |
| 91 | 'lassen sich Übertragungsfehler und nachträgliche Veränderungen erkennen.', |
| 92 | 'Sie belegt das aber nur, soweit diese Liste selbst aus einer Quelle stammt,', |
| 93 | 'der Sie trauen – wer die Pakete austauschen kann, kann diese Datei daneben', |
| 94 | 'ebenso austauschen. Gegen einen beherrschten Auslieferungsweg hilft allein', |
| 95 | 'eine Codesignatur.', |
| 96 | '', |
| 97 | 'SHA-256:', |
| 98 | '', |
| 99 | ...zeilen.map((z) => `${z.summe} ${z.name}`), |
| 100 | '', |
| 101 | 'Größen in Byte:', |
| 102 | '', |
| 103 | ...zeilen.map((z) => `${String(z.bytes).padStart(12)} ${z.name}`), |
| 104 | '', |
| 105 | ].join('\n'); |
| 106 | |
| 107 | /* |
| 108 | * Mit Bytefolgemarke, aus demselben Grund wie beim CSV-Export: Der Text |
| 109 | * enthaelt Umlaute, und ein Windows-Editor, der die Datei ohne Marke als |
| 110 | * ANSI liest, zeigt daraus Buchstabensalat. Der Dateiname bleibt ASCII. |
| 111 | */ |
| 112 | const ziel = join(AUSGABE, `PRUEFSUMMEN-${version}.txt`); |
| 113 | await writeFile(ziel, `${text}`, 'utf8'); |
| 114 | |
| 115 | console.log(`Prüfsummen für ${dateien.length} Paket(e) der Version ${version}:`); |
| 116 | for (const z of zeilen) console.log(` ${z.summe} ${z.name}`); |
| 117 | console.log(`geschrieben: release/PRUEFSUMMEN-${version}.txt`); |