diff --git a/app/src/lib/url/legacy.ts b/app/src/lib/url/legacy.ts index 5e3cbd3..8ffdbee 100644 --- a/app/src/lib/url/legacy.ts +++ b/app/src/lib/url/legacy.ts @@ -1,11 +1,19 @@ /** * Erkennt Legacy-Hugo-URLs der Form /YYYY/MM/DD/.html oder .../.html/ * und gibt den dtag-Teil zurück. Für alle anderen Pfade: null. + * + * Erwartet nur den Pfad ohne Query/Fragment — wenn vorhanden vom Aufrufer + * trennen. `decodeURIComponent` wird defensiv gekapselt, damit malformed + * Percent-Encoding die SPA beim Boot nicht crasht. */ export function parseLegacyUrl(path: string): string | null { const match = path.match(/^\/\d{4}\/\d{2}\/\d{2}\/([^/]+?)\.html\/?$/); if (!match) return null; - return decodeURIComponent(match[1]); + try { + return decodeURIComponent(match[1]); + } catch { + return null; + } } /** diff --git a/app/tests/unit/legacy-url.test.ts b/app/tests/unit/legacy-url.test.ts index a1fd8b3..b3f5ef8 100644 --- a/app/tests/unit/legacy-url.test.ts +++ b/app/tests/unit/legacy-url.test.ts @@ -32,6 +32,14 @@ describe('parseLegacyUrl', () => { 'mit leerzeichen', ); }); + + it('gibt null zurück bei malformed percent-encoding (crash-sicher)', () => { + expect(parseLegacyUrl('/2024/01/26/%E0.html/')).toBeNull(); + }); + + it('gibt null zurück für leeren dtag', () => { + expect(parseLegacyUrl('/2024/01/26/.html/')).toBeNull(); + }); }); describe('canonicalPostPath', () => { @@ -43,3 +51,11 @@ describe('canonicalPostPath', () => { expect(canonicalPostPath('mit leerzeichen')).toBe('/mit%20leerzeichen/'); }); }); + +describe('round-trip parseLegacyUrl → canonicalPostPath', () => { + it('Legacy-URL wird zur kanonischen kurzen Form', () => { + const dtag = parseLegacyUrl('/2025/03/04/dezentrale-oep-oer.html/'); + expect(dtag).not.toBeNull(); + expect(canonicalPostPath(dtag!)).toBe('/dezentrale-oep-oer/'); + }); +});