waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src main lizenzen.ts

8,3 KB Rohdatei
app/src/main/lizenzen.ts — 254 Zeilen
1 /**
2 * Lizenzangaben für den Bereich „Über diese Software“.
3 *
4 * Das Anzeigen ist keine Höflichkeit, sondern Pflicht: MIT, BSD und Apache
5 * verlangen, dass Lizenztext und Urheberrechtsvermerk mitgeliefert werden,
6 * und die EUPL verlangt den Hinweis auf sich selbst. Der amtliche
7 * Fragenkatalog bekommt bewusst keine Lizenz – er wird nur mit Quellenangabe
8 * unverändert wiedergegeben.
9 *
10 * Ablageort der Dateien – dieselbe Logik wie beim Fragenkatalog:
11 * - Entwicklung: `<repo>/LICENSE.de.txt` und `<repo>/content/drittlizenzen.json`
12 * - Gepackt: `<app>/resources/LICENSE.de.txt` und
13 * `<app>/resources/drittlizenzen.json` (siehe `extraResources`
14 * in electron-builder.yml)
15 *
16 * Grundsatz: eine fehlende oder beschädigte Datei darf die Anwendung nicht
17 * anhalten. Das betroffene Feld bleibt leer, und im Protokoll steht, was
18 * fehlt – die übrigen Angaben werden trotzdem gezeigt.
19 */
20
21 import { existsSync, readFileSync } from 'node:fs';
22 import { join } from 'node:path';
23
24 import { app } from 'electron';
25
26 import type { Katalog } from '../shared/katalog';
27 import {
28 EUPL_HINWEIS,
29 EUPL_KENNUNG,
30 KATALOG_HINWEIS,
31 type Drittlizenz,
32 type Lizenzangaben,
33 } from '../shared/lizenzen';
34 import { katalogLaden, sucheAufwaerts } from './katalog';
35
36 /** Deutsche Fassung der EUPL 1.2 im Projektwurzelverzeichnis. */
37 const LIZENZ_DATEI = 'LICENSE.de.txt';
38
39 /** Erzeugte Übersicht der mitgelieferten Fremdkomponenten. */
40 const DRITT_DATEI = 'drittlizenzen.json';
41
42 /** Pfad der Drittlizenzen relativ zum Projektwurzelverzeichnis. */
43 const DRITT_RELATIV = join('content', DRITT_DATEI);
44
45 const EUPL_NAME = 'European Union Public Licence, Version 1.2';
46
47 /** Obergrenze je Lizenztext – schützt die Anzeige vor einer verbogenen Datei. */
48 const MAX_TEXT_LAENGE = 200_000;
49
50 // ─── Quellen ────────────────────────────────────────────────────────────────
51
52 /**
53 * Alles, was {@link lizenzenBilden} von außen braucht. Ausdrücklich als
54 * Parameter und nicht als Modulzustand, damit sich die Zusammenstellung ohne
55 * Electron und ohne echte Anwendungspfade prüfen lässt.
56 */
57 export interface LizenzQuellen {
58 /** Pfad zur deutschen EUPL-Fassung; `null`, wenn sie nicht gefunden wurde. */
59 readonly lizenzPfad: string | null;
60 /** Pfad zu `drittlizenzen.json`; `null`, wenn sie nicht gefunden wurde. */
61 readonly drittPfad: string | null;
62 /** Geladener Katalog; `null`, wenn er sich nicht laden ließ. */
63 readonly katalog: Katalog | null;
64 readonly warnen: (meldung: string) => void;
65 }
66
67 function standardWarnung(meldung: string): void {
68 console.warn(`[lizenzen] ${meldung}`);
69 }
70
71 function fehlerText(fehler: unknown): string {
72 return fehler instanceof Error ? fehler.message : String(fehler);
73 }
74
75 /**
76 * Sucht eine Datei des Projektwurzelverzeichnisses.
77 *
78 * Gepackt liegt sie flach unter `resources/`, sonst wird vom
79 * Anwendungspfad aus aufwärts gesucht – im Quellbaum ist nicht bekannt, aus
80 * welchem Verzeichnis gestartet wurde.
81 */
82 function ressourcePfad(gepacktName: string, relativ: string): string | null {
83 if (app.isPackaged) {
84 const pfad = join(process.resourcesPath, gepacktName);
85 return existsSync(pfad) ? pfad : null;
86 }
87
88 for (const start of [app.getAppPath(), __dirname, process.cwd()]) {
89 const treffer = sucheAufwaerts(start, relativ);
90 if (treffer !== null) {
91 return treffer;
92 }
93 }
94 return null;
95 }
96
97 // ─── Einzelteile ────────────────────────────────────────────────────────────
98
99 function lizenztextLesen(pfad: string | null, warnen: (meldung: string) => void): string | null {
100 if (pfad === null) {
101 warnen(
102 `Die deutsche Lizenzfassung (${LIZENZ_DATEI}) wurde nicht gefunden. ` +
103 'Der Lizenztext wird nicht angezeigt; die Lizenz selbst gilt unverändert.',
104 );
105 return null;
106 }
107
108 try {
109 return readFileSync(pfad, 'utf8').slice(0, MAX_TEXT_LAENGE);
110 } catch (fehler) {
111 warnen(`Die Datei ${pfad} ist nicht lesbar: ${fehlerText(fehler)}`);
112 return null;
113 }
114 }
115
116 /** Nimmt einen Eintrag aus `drittlizenzen.json` an – oder verwirft ihn begründet. */
117 function drittlizenz(
118 wert: unknown,
119 stelle: string,
120 warnen: (meldung: string) => void,
121 ): Drittlizenz | null {
122 if (typeof wert !== 'object' || wert === null || Array.isArray(wert)) {
123 warnen(`${stelle} ist kein Objekt und wird übergangen.`);
124 return null;
125 }
126 const roh = wert as Record<string, unknown>;
127
128 const name = roh['name'];
129 const lizenz = roh['lizenz'];
130 if (typeof name !== 'string' || name.trim().length === 0) {
131 warnen(`${stelle} hat keinen Namen und wird übergangen.`);
132 return null;
133 }
134 if (typeof lizenz !== 'string' || lizenz.trim().length === 0) {
135 warnen(`Der Eintrag „${name}“ hat keine Lizenzangabe und wird übergangen.`);
136 return null;
137 }
138
139 const text = (feld: unknown): string | null =>
140 typeof feld === 'string' ? feld.slice(0, MAX_TEXT_LAENGE) : null;
141
142 const hinweis = roh['hinweis'];
143
144 // `exactOptionalPropertyTypes`: `hinweis` nur setzen, wenn es ihn gibt.
145 return {
146 name,
147 version: text(roh['version']),
148 lizenz,
149 url: text(roh['url']),
150 lizenztext: text(roh['lizenztext']),
151 ...(typeof hinweis === 'string' && hinweis.length > 0 ? { hinweis } : {}),
152 };
153 }
154
155 function drittlizenzenLesen(pfad: string | null, warnen: (meldung: string) => void): Drittlizenz[] {
156 if (pfad === null) {
157 warnen(
158 `Die Übersicht der Fremdkomponenten (${DRITT_DATEI}) wurde nicht gefunden. ` +
159 'Es werden keine Drittlizenzen angezeigt.',
160 );
161 return [];
162 }
163
164 let roh: unknown;
165 try {
166 roh = JSON.parse(readFileSync(pfad, 'utf8'));
167 } catch (fehler) {
168 warnen(`Die Datei ${pfad} ist nicht lesbar: ${fehlerText(fehler)}`);
169 return [];
170 }
171
172 if (typeof roh !== 'object' || roh === null || Array.isArray(roh)) {
173 warnen(`Die Datei ${pfad} enthält kein Objekt mit dem Feld „pakete“.`);
174 return [];
175 }
176
177 const pakete = (roh as Record<string, unknown>)['pakete'];
178 if (!Array.isArray(pakete)) {
179 warnen(`Die Datei ${pfad} enthält keine Liste „pakete“.`);
180 return [];
181 }
182
183 const eintraege: Drittlizenz[] = [];
184 (pakete as readonly unknown[]).forEach((eintrag, i) => {
185 const geprueft = drittlizenz(eintrag, `pakete[${String(i)}]`, warnen);
186 if (geprueft !== null) {
187 eintraege.push(geprueft);
188 }
189 });
190 return eintraege;
191 }
192
193 // ─── Zusammenstellung ───────────────────────────────────────────────────────
194
195 /** Baut die Lizenzangaben aus den übergebenen Quellen zusammen. */
196 export function lizenzenBilden(quellen: LizenzQuellen): Lizenzangaben {
197 const { warnen } = quellen;
198
199 if (quellen.katalog === null) {
200 warnen(
201 'Der Fragenkatalog ließ sich nicht laden; Quellenangabe und Quell-URL ' +
202 'bleiben leer. Der Rechtshinweis zum Katalog wird trotzdem angezeigt.',
203 );
204 }
205
206 return {
207 eigene: {
208 kennung: EUPL_KENNUNG,
209 name: EUPL_NAME,
210 hinweis: EUPL_HINWEIS,
211 text: lizenztextLesen(quellen.lizenzPfad, warnen),
212 },
213 katalog: {
214 quellenangabe: quellen.katalog?.meta.quellenangabe ?? '',
215 quelleUrl: quellen.katalog?.meta.quelle_url ?? '',
216 hinweis: KATALOG_HINWEIS,
217 },
218 dritte: drittlizenzenLesen(quellen.drittPfad, warnen),
219 };
220 }
221
222 let speicher: Lizenzangaben | null = null;
223
224 /**
225 * Liefert die Lizenzangaben; Dateien werden nur einmal gelesen.
226 *
227 * Die Angaben ändern sich zur Laufzeit nicht, und der EUPL-Text allein sind
228 * rund 18 kB – ein zweites Lesen wäre reine Verschwendung.
229 */
230 export function lizenzenLesen(): Lizenzangaben {
231 if (speicher !== null) {
232 return speicher;
233 }
234
235 let katalog: Katalog | null = null;
236 try {
237 katalog = katalogLaden();
238 } catch (fehler) {
239 standardWarnung(`Fragenkatalog nicht verfügbar: ${fehlerText(fehler)}`);
240 }
241
242 speicher = lizenzenBilden({
243 lizenzPfad: ressourcePfad(LIZENZ_DATEI, LIZENZ_DATEI),
244 drittPfad: ressourcePfad(DRITT_DATEI, DRITT_RELATIV),
245 katalog,
246 warnen: standardWarnung,
247 });
248 return speicher;
249 }
250
251 /** Leert den Zwischenspeicher – für Tests und einen sauberen Neustart. */
252 export function lizenzenZuruecksetzen(): void {
253 speicher = null;
254 }