From 7fd7f33cc25e249a642d77050156cc52dc985a9e Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Thu, 24 Sep 2026 13:24:23 -0400 Subject: [PATCH] fix(desktop): collapse ../ in the renderer preview path fallback A note linking ../other.md resolved to /notes/../other.md when the Electron normalizer is unavailable, a path the fs bridge then rejects. --- apps/desktop/src/lib/local-preview.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/local-preview.ts b/apps/desktop/src/lib/local-preview.ts index 9eb7ff8a26..9c3c598ce3 100644 --- a/apps/desktop/src/lib/local-preview.ts +++ b/apps/desktop/src/lib/local-preview.ts @@ -57,12 +57,32 @@ function extension(value: string) { return idx >= 0 ? clean.slice(idx).toLowerCase() : '' } +// Collapses `.`/`..` so a note's `../other.md` lands on the sibling, not on a +// path the fs bridge rejects. Never climbs above the root of `base`. function joinPath(base: string, rel: string) { if (!base) { return rel } - return `${base.replace(/\/+$/, '')}/${rel.replace(/^\.?\//, '')}` + const normalizedBase = base.replace(/\\/g, '/') + const root = normalizedBase.match(/^(?:\/\/|(?:[A-Za-z]:)?\/)/)?.[0] ?? '' + const parts = normalizedBase.slice(root.length).split('/').filter(Boolean) + + for (const part of rel.replace(/\\/g, '/').split('/')) { + if (!part || part === '.') { + continue + } + + if (part === '..') { + parts.pop() + + continue + } + + parts.push(part) + } + + return `${root}${parts.join('/')}` } function pathToFileUrl(path: string) {