/** * Lizenzangaben für den Bereich „Über diese Software“. * * Das Anzeigen ist keine Höflichkeit, sondern Pflicht: MIT, BSD und Apache * verlangen, dass Lizenztext und Urheberrechtsvermerk mitgeliefert werden, * und die EUPL verlangt den Hinweis auf sich selbst. Der amtliche * Fragenkatalog bekommt bewusst keine Lizenz – er wird nur mit Quellenangabe * unverändert wiedergegeben. * * Ablageort der Dateien – dieselbe Logik wie beim Fragenkatalog: * - Entwicklung: `/LICENSE.de.txt` und `/content/drittlizenzen.json` * - Gepackt: `/resources/LICENSE.de.txt` und * `/resources/drittlizenzen.json` (siehe `extraResources` * in electron-builder.yml) * * Grundsatz: eine fehlende oder beschädigte Datei darf die Anwendung nicht * anhalten. Das betroffene Feld bleibt leer, und im Protokoll steht, was * fehlt – die übrigen Angaben werden trotzdem gezeigt. */ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { app } from 'electron'; import type { Katalog } from '../shared/katalog'; import { EUPL_HINWEIS, EUPL_KENNUNG, KATALOG_HINWEIS, type Drittlizenz, type Lizenzangaben, } from '../shared/lizenzen'; import { katalogLaden, sucheAufwaerts } from './katalog'; /** Deutsche Fassung der EUPL 1.2 im Projektwurzelverzeichnis. */ const LIZENZ_DATEI = 'LICENSE.de.txt'; /** Erzeugte Übersicht der mitgelieferten Fremdkomponenten. */ const DRITT_DATEI = 'drittlizenzen.json'; /** Pfad der Drittlizenzen relativ zum Projektwurzelverzeichnis. */ const DRITT_RELATIV = join('content', DRITT_DATEI); const EUPL_NAME = 'European Union Public Licence, Version 1.2'; /** Obergrenze je Lizenztext – schützt die Anzeige vor einer verbogenen Datei. */ const MAX_TEXT_LAENGE = 200_000; // ─── Quellen ──────────────────────────────────────────────────────────────── /** * Alles, was {@link lizenzenBilden} von außen braucht. Ausdrücklich als * Parameter und nicht als Modulzustand, damit sich die Zusammenstellung ohne * Electron und ohne echte Anwendungspfade prüfen lässt. */ export interface LizenzQuellen { /** Pfad zur deutschen EUPL-Fassung; `null`, wenn sie nicht gefunden wurde. */ readonly lizenzPfad: string | null; /** Pfad zu `drittlizenzen.json`; `null`, wenn sie nicht gefunden wurde. */ readonly drittPfad: string | null; /** Geladener Katalog; `null`, wenn er sich nicht laden ließ. */ readonly katalog: Katalog | null; readonly warnen: (meldung: string) => void; } function standardWarnung(meldung: string): void { console.warn(`[lizenzen] ${meldung}`); } function fehlerText(fehler: unknown): string { return fehler instanceof Error ? fehler.message : String(fehler); } /** * Sucht eine Datei des Projektwurzelverzeichnisses. * * Gepackt liegt sie flach unter `resources/`, sonst wird vom * Anwendungspfad aus aufwärts gesucht – im Quellbaum ist nicht bekannt, aus * welchem Verzeichnis gestartet wurde. */ function ressourcePfad(gepacktName: string, relativ: string): string | null { if (app.isPackaged) { const pfad = join(process.resourcesPath, gepacktName); return existsSync(pfad) ? pfad : null; } for (const start of [app.getAppPath(), __dirname, process.cwd()]) { const treffer = sucheAufwaerts(start, relativ); if (treffer !== null) { return treffer; } } return null; } // ─── Einzelteile ──────────────────────────────────────────────────────────── function lizenztextLesen(pfad: string | null, warnen: (meldung: string) => void): string | null { if (pfad === null) { warnen( `Die deutsche Lizenzfassung (${LIZENZ_DATEI}) wurde nicht gefunden. ` + 'Der Lizenztext wird nicht angezeigt; die Lizenz selbst gilt unverändert.', ); return null; } try { return readFileSync(pfad, 'utf8').slice(0, MAX_TEXT_LAENGE); } catch (fehler) { warnen(`Die Datei ${pfad} ist nicht lesbar: ${fehlerText(fehler)}`); return null; } } /** Nimmt einen Eintrag aus `drittlizenzen.json` an – oder verwirft ihn begründet. */ function drittlizenz( wert: unknown, stelle: string, warnen: (meldung: string) => void, ): Drittlizenz | null { if (typeof wert !== 'object' || wert === null || Array.isArray(wert)) { warnen(`${stelle} ist kein Objekt und wird übergangen.`); return null; } const roh = wert as Record; const name = roh['name']; const lizenz = roh['lizenz']; if (typeof name !== 'string' || name.trim().length === 0) { warnen(`${stelle} hat keinen Namen und wird übergangen.`); return null; } if (typeof lizenz !== 'string' || lizenz.trim().length === 0) { warnen(`Der Eintrag „${name}“ hat keine Lizenzangabe und wird übergangen.`); return null; } const text = (feld: unknown): string | null => typeof feld === 'string' ? feld.slice(0, MAX_TEXT_LAENGE) : null; const hinweis = roh['hinweis']; // `exactOptionalPropertyTypes`: `hinweis` nur setzen, wenn es ihn gibt. return { name, version: text(roh['version']), lizenz, url: text(roh['url']), lizenztext: text(roh['lizenztext']), ...(typeof hinweis === 'string' && hinweis.length > 0 ? { hinweis } : {}), }; } function drittlizenzenLesen(pfad: string | null, warnen: (meldung: string) => void): Drittlizenz[] { if (pfad === null) { warnen( `Die Übersicht der Fremdkomponenten (${DRITT_DATEI}) wurde nicht gefunden. ` + 'Es werden keine Drittlizenzen angezeigt.', ); return []; } let roh: unknown; try { roh = JSON.parse(readFileSync(pfad, 'utf8')); } catch (fehler) { warnen(`Die Datei ${pfad} ist nicht lesbar: ${fehlerText(fehler)}`); return []; } if (typeof roh !== 'object' || roh === null || Array.isArray(roh)) { warnen(`Die Datei ${pfad} enthält kein Objekt mit dem Feld „pakete“.`); return []; } const pakete = (roh as Record)['pakete']; if (!Array.isArray(pakete)) { warnen(`Die Datei ${pfad} enthält keine Liste „pakete“.`); return []; } const eintraege: Drittlizenz[] = []; (pakete as readonly unknown[]).forEach((eintrag, i) => { const geprueft = drittlizenz(eintrag, `pakete[${String(i)}]`, warnen); if (geprueft !== null) { eintraege.push(geprueft); } }); return eintraege; } // ─── Zusammenstellung ─────────────────────────────────────────────────────── /** Baut die Lizenzangaben aus den übergebenen Quellen zusammen. */ export function lizenzenBilden(quellen: LizenzQuellen): Lizenzangaben { const { warnen } = quellen; if (quellen.katalog === null) { warnen( 'Der Fragenkatalog ließ sich nicht laden; Quellenangabe und Quell-URL ' + 'bleiben leer. Der Rechtshinweis zum Katalog wird trotzdem angezeigt.', ); } return { eigene: { kennung: EUPL_KENNUNG, name: EUPL_NAME, hinweis: EUPL_HINWEIS, text: lizenztextLesen(quellen.lizenzPfad, warnen), }, katalog: { quellenangabe: quellen.katalog?.meta.quellenangabe ?? '', quelleUrl: quellen.katalog?.meta.quelle_url ?? '', hinweis: KATALOG_HINWEIS, }, dritte: drittlizenzenLesen(quellen.drittPfad, warnen), }; } let speicher: Lizenzangaben | null = null; /** * Liefert die Lizenzangaben; Dateien werden nur einmal gelesen. * * Die Angaben ändern sich zur Laufzeit nicht, und der EUPL-Text allein sind * rund 18 kB – ein zweites Lesen wäre reine Verschwendung. */ export function lizenzenLesen(): Lizenzangaben { if (speicher !== null) { return speicher; } let katalog: Katalog | null = null; try { katalog = katalogLaden(); } catch (fehler) { standardWarnung(`Fragenkatalog nicht verfügbar: ${fehlerText(fehler)}`); } speicher = lizenzenBilden({ lizenzPfad: ressourcePfad(LIZENZ_DATEI, LIZENZ_DATEI), drittPfad: ressourcePfad(DRITT_DATEI, DRITT_RELATIV), katalog, warnen: standardWarnung, }); return speicher; } /** Leert den Zwischenspeicher – für Tests und einen sauberen Neustart. */ export function lizenzenZuruecksetzen(): void { speicher = null; }