waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | /** |
| 2 | * Lädt die Datenschutzerklärung aus der mitgelieferten Datei. |
| 3 | * |
| 4 | * ## Warum genau wie die Lizenztexte |
| 5 | * |
| 6 | * Die Anwendung liefert bereits zwei lange Rechtstexte mit und zeigt sie an — |
| 7 | * den EUPL-Volltext und die Übersicht der Fremdkomponenten. Der Weg dorthin |
| 8 | * ist erprobt und wird gegen das **gepackte** Paket geprüft |
| 9 | * (`e2e/gepackt.spec.ts`, „lädt die Lizenztexte aus resources/“): Datei über |
| 10 | * `extraResources` ins Paket, im Hauptprozess über `process.resourcesPath` |
| 11 | * gelesen, im Quellbaum aufwärts gesucht. Diese Datei macht dasselbe und |
| 12 | * erfindet nichts daneben. |
| 13 | * |
| 14 | * ## Was bei fehlender Datei geschieht |
| 15 | * |
| 16 | * Die Anwendung läuft weiter und sagt, was fehlt. Ein leerer Bereich ohne |
| 17 | * Begründung wäre die schlechteste Auskunft — der Nutzende hielte die |
| 18 | * Erklärung für nicht vorhanden statt für nicht ladbar, und ausgerechnet |
| 19 | * beim Datenschutz ist das der falsche Eindruck. |
| 20 | * |
| 21 | * ## Die Prüfsumme wird hier **nicht** geprüft |
| 22 | * |
| 23 | * Sie steht in der Datei und wird von `tests/datenschutz.test.ts` gegen |
| 24 | * `docs/datenschutz.md` gerechnet — also beim Bauen, nicht beim Starten. Zur |
| 25 | * Laufzeit liegt die Quelldatei gar nicht vor: `docs/` wird nicht |
| 26 | * mitgeliefert. Eine Prüfung, die im Paket nie etwas zu vergleichen hat, |
| 27 | * wäre eine Zusage ohne Deckung. |
| 28 | */ |
| 29 | |
| 30 | import { existsSync, readFileSync } from 'node:fs'; |
| 31 | import { join } from 'node:path'; |
| 32 | |
| 33 | import { app } from 'electron'; |
| 34 | |
| 35 | import type { |
| 36 | Datenschutzangaben, |
| 37 | Datenschutzblock, |
| 38 | Datenschutzerklaerung, |
| 39 | } from '../shared/datenschutz'; |
| 40 | import { sucheAufwaerts } from './katalog'; |
| 41 | |
| 42 | const DATEI = 'datenschutz.json'; |
| 43 | const RELATIV = join('content', DATEI); |
| 44 | |
| 45 | /** |
| 46 | * Obergrenze beim Lesen. |
| 47 | * |
| 48 | * Nachgemessen ist die Datei 80.304 Byte groß. Eine Grenze bei zwei Megabyte |
| 49 | * lässt jedes denkbare Wachstum zu und fängt zugleich den Fall ab, dass |
| 50 | * jemand versehentlich etwas anderes unter diesen Namen legt. |
| 51 | */ |
| 52 | const HOECHSTGROESSE = 2 * 1024 * 1024; |
| 53 | |
| 54 | let speicher: Datenschutzangaben | null = null; |
| 55 | |
| 56 | function fehlerText(fehler: unknown): string { |
| 57 | return fehler instanceof Error ? fehler.message : String(fehler); |
| 58 | } |
| 59 | |
| 60 | function pfadSuchen(): string | null { |
| 61 | if (app.isPackaged) { |
| 62 | const pfad = join(process.resourcesPath, DATEI); |
| 63 | return existsSync(pfad) ? pfad : null; |
| 64 | } |
| 65 | |
| 66 | for (const start of [app.getAppPath(), __dirname, process.cwd()]) { |
| 67 | const treffer = sucheAufwaerts(start, RELATIV); |
| 68 | if (treffer !== null) { |
| 69 | return treffer; |
| 70 | } |
| 71 | } |
| 72 | return null; |
| 73 | } |
| 74 | |
| 75 | /** Prüft die Form so weit, dass die Oberfläche sich darauf verlassen kann. */ |
| 76 | function istBlock(wert: unknown): wert is Datenschutzblock { |
| 77 | if (typeof wert !== 'object' || wert === null) { |
| 78 | return false; |
| 79 | } |
| 80 | const art = (wert as { art?: unknown }).art; |
| 81 | return art === 'ueberschrift' || art === 'absatz' || art === 'liste' || art === 'code'; |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Baut die Angaben aus dem Dateiinhalt. |
| 86 | * |
| 87 | * Getrennt von {@link datenschutzLesen}, damit Tests sie ohne Electron und |
| 88 | * ohne Dateisystem prüfen können – dieselbe Aufteilung wie bei |
| 89 | * `lizenzenBilden`. |
| 90 | */ |
| 91 | export function datenschutzBilden(roh: string | null, warnung: string | null): Datenschutzangaben { |
| 92 | if (roh === null) { |
| 93 | return { erklaerung: null, warnung }; |
| 94 | } |
| 95 | |
| 96 | let gelesen: unknown; |
| 97 | try { |
| 98 | gelesen = JSON.parse(roh); |
| 99 | } catch (fehler) { |
| 100 | return { |
| 101 | erklaerung: null, |
| 102 | warnung: `Die Datenschutzerklärung ist nicht lesbar: ${fehlerText(fehler)}`, |
| 103 | }; |
| 104 | } |
| 105 | |
| 106 | const daten = gelesen as Partial<Datenschutzerklaerung>; |
| 107 | const bloecke = Array.isArray(daten.bloecke) ? daten.bloecke.filter(istBlock) : []; |
| 108 | |
| 109 | if ( |
| 110 | typeof daten.fassung !== 'string' || |
| 111 | typeof daten.stand !== 'string' || |
| 112 | typeof daten.quellpruefsumme !== 'string' || |
| 113 | bloecke.length === 0 |
| 114 | ) { |
| 115 | return { |
| 116 | erklaerung: null, |
| 117 | warnung: |
| 118 | 'Die mitgelieferte Datenschutzerklärung ist unvollständig. ' + |
| 119 | 'Die veröffentlichte Fassung gilt unverändert.', |
| 120 | }; |
| 121 | } |
| 122 | |
| 123 | return { |
| 124 | erklaerung: { |
| 125 | quellpruefsumme: daten.quellpruefsumme, |
| 126 | fassung: daten.fassung, |
| 127 | stand: daten.stand, |
| 128 | bloecke, |
| 129 | }, |
| 130 | warnung: null, |
| 131 | }; |
| 132 | } |
| 133 | |
| 134 | /** Liest die Erklärung, einmal je Programmlauf. */ |
| 135 | export function datenschutzLesen(): Datenschutzangaben { |
| 136 | if (speicher !== null) { |
| 137 | return speicher; |
| 138 | } |
| 139 | |
| 140 | const pfad = pfadSuchen(); |
| 141 | if (pfad === null) { |
| 142 | speicher = datenschutzBilden( |
| 143 | null, |
| 144 | `Die Datenschutzerklärung (${DATEI}) wurde nicht gefunden. ` + |
| 145 | 'Bitte fordern Sie sie unter der Adresse an, die unter „Rückmeldung und ' + |
| 146 | 'Kontakt“ steht.', |
| 147 | ); |
| 148 | return speicher; |
| 149 | } |
| 150 | |
| 151 | let roh: string | null = null; |
| 152 | let warnung: string | null = null; |
| 153 | try { |
| 154 | roh = readFileSync(pfad, 'utf8').slice(0, HOECHSTGROESSE); |
| 155 | } catch (fehler) { |
| 156 | warnung = `Die Datei ${DATEI} ist nicht lesbar: ${fehlerText(fehler)}`; |
| 157 | } |
| 158 | |
| 159 | speicher = datenschutzBilden(roh, warnung); |
| 160 | return speicher; |
| 161 | } |
| 162 | |
| 163 | /** Leert den Zwischenspeicher – Gegenstück für Tests. */ |
| 164 | export function datenschutzZuruecksetzen(): void { |
| 165 | speicher = null; |
| 166 | } |