From 6b4fed10100fa014f685bd77c1872b71a03adda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=B3=BD=E9=98=B3?= <1834413471@qq.com> Date: Fri, 14 Aug 2026 16:44:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(html):=20Windows=20=E7=9B=98=E7=AC=A6?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=A7=A3=E7=A0=81=E5=8E=BB=E6=8E=89=E5=89=8D?= =?UTF-8?q?=E5=AF=BC=E6=96=9C=E6=9D=A0=EF=BC=8C=E4=BF=AE=E5=A4=8D=20html?= =?UTF-8?q?=20=E9=A2=84=E8=A7=88=E8=A2=AB=20isWithin=20=E6=8B=92=E7=BB=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decodeHtmlUrl 对 'D:\' 盘符路径重拼为 '/D:/...',node path.resolve() 在 Windows 上会变成 'D:\D:\...'(盘符重复),html 路由的 isWithin(cwd) 围栏因此拒绝所有盘 符路径——打开 html 报 "html path outside the session working directory"。 修复:首段匹配 /^[A-Za-z]:$/(盘符段)时改回无前导斜杠的 'D:/...' 形式, requireAbsolute 可原样解析回盘符路径;POSIX 路径行为不变。 测试:html-route.spec 更新 Windows round-trip 断言,新增小写盘符用例与 resolve() 回归护栏(win32 平台守卫)。 --- src/html-route.ts | 11 ++++++++++- tests/html-route.spec.ts | 19 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/html-route.ts b/src/html-route.ts index 13df112..b81a556 100644 --- a/src/html-route.ts +++ b/src/html-route.ts @@ -66,5 +66,14 @@ export function decodeHtmlUrl(pathname: string): HtmlDecodeResult { if (sessionId === undefined || sessionId === '' || pathSegments.length === 0 || pathSegments.some(segment => segment === '')) { return { ok: false, status: 400, message: 'sessionId and file path are required' } } - return { ok: true, ref: { sessionId, path: `/${pathSegments.join('/')}` } } + // A Windows drive segment ('D:') is the FIRST path segment of an encoded + // drive path. Rejoining it with a leading slash would yield '/D:/work/...' + // which node's path.resolve() mangles into 'D:\D:\work\...' on Windows — + // the html route's isWithin(cwd) fence would then reject every drive path. + // Keep the drive form slash-free so requireAbsolute() resolves it verbatim. + const first = pathSegments[0] ?? '' + const path = /^[A-Za-z]:$/.test(first) + ? pathSegments.join('/') + : `/${pathSegments.join('/')}` + return { ok: true, ref: { sessionId, path } } } diff --git a/tests/html-route.spec.ts b/tests/html-route.spec.ts index 312037d..95b5d22 100644 --- a/tests/html-route.spec.ts +++ b/tests/html-route.spec.ts @@ -6,6 +6,7 @@ * the session scope intact (the WHY of the path-encoding design). */ import { describe, expect, it } from 'vitest' +import { resolve } from 'node:path' import { decodeHtmlUrl, encodeHtmlUrl } from '../src/html-route.ts' describe('encodeHtmlUrl', () => { @@ -38,11 +39,25 @@ describe('decodeHtmlUrl', () => { }) }) - it('decodes a Windows round-trip', () => { + it('decodes a Windows round-trip (drive path WITHOUT a leading slash, so node resolve() keeps the drive)', () => { const url = encodeHtmlUrl('sess-1', 'C:\\Users\\me\\a.html') expect(decodeHtmlUrl(url)).toEqual({ ok: true, - ref: { sessionId: 'sess-1', path: '/C:/Users/me/a.html' }, + ref: { sessionId: 'sess-1', path: 'C:/Users/me/a.html' }, + }) + // The host runs requireAbsolute() over the decoded path: with the old + // '/C:/...' form node's resolve() produced 'C:\C:\Users\...' (drive + // doubled) on Windows and the isWithin(cwd) fence rejected every drive + // path. The slash-free form must resolve back to the original path. + if (process.platform === 'win32') { + expect(resolve('C:/Users/me/a.html')).toBe('C:\\Users\\me\\a.html') + } + }) + + it('decodes a lowercase-drive Windows path without a leading slash', () => { + expect(decodeHtmlUrl('/sidebar/html/s/d%3A/work/x.html')).toEqual({ + ok: true, + ref: { sessionId: 's', path: 'd:/work/x.html' }, }) })