fix: symlink 探测有界并发 + UNC 标记平台中性化(回应 CR/自审)
- fs-tree: symlink 目标 stat 从串行改为 32 并发工作池(readdir 流关闭后统一 探测),symlink 密集/网络目标目录不再累加延迟;顺序与分类语义不变 - html-route: 解码不再重建反斜杠 UNC,统一重建为平台中性的 //server/share/... 前斜杠形态(win32/POSIX 均正确 resolve),从而删除 windowsPathStyle 平台信号 与 client isWindowsStylePath 助手;同时解决无 cwd 时正斜杠 UNC 的往返盲点 - 测试/文档同步;移除未使用的 isPosix
This commit is contained in:
1 parent
9652bbda59
commit
be95787c00
10 files changed
+104
-114
No files matched your search
@@ -86,4 +86,28 @@ describe.skipIf(!canSymlink)('fs-tree symlink listing', () => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('classifies every row correctly across a symlink-heavy level (bounded probe)', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-sidebar-symlink-many-'))
|
||||
mkdirSync(join(dir, 'real-dir'))
|
||||
writeFileSync(join(dir, 'real-file.txt'), 'content')
|
||||
try {
|
||||
// More links than the probe concurrency cap exercises the worker pool;
|
||||
// every directory link must still classify as a directory and every
|
||||
// file link as a file (order is already checked by the sort assertion).
|
||||
for (let index = 0; index < 48; index += 1) {
|
||||
symlinkSync(join(dir, 'real-dir'), join(dir, `dir-link-${index}`))
|
||||
symlinkSync(join(dir, 'real-file.txt'), join(dir, `file-link-${index}`))
|
||||
}
|
||||
const listing = await listDirectory(dir)
|
||||
for (let index = 0; index < 48; index += 1) {
|
||||
expect(listing.entries.find(entry => entry.name === `dir-link-${index}`))
|
||||
.toMatchObject({ isDir: true, isSymlink: true, broken: false })
|
||||
expect(listing.entries.find(entry => entry.name === `file-link-${index}`))
|
||||
.toMatchObject({ isDir: false, isSymlink: true, broken: false })
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolve } from 'node:path'
|
||||
import { compareEntries, isWithin, parentOf, requireAbsolute, rootLabel } from '../src/fs-tree.ts'
|
||||
import { isPosix, isWin32 } from './platform.ts'
|
||||
import { isWin32 } from './platform.ts'
|
||||
|
||||
describe('fs-tree', () => {
|
||||
it('sorts directories first, then names case-insensitively', () => {
|
||||
|
||||
+17
-18
@@ -21,22 +21,19 @@ describe('encodeHtmlUrl', () => {
|
||||
.toBe('/sidebar/html/sess-1/C%3A/Users/me/a.html')
|
||||
})
|
||||
|
||||
it('encodes a Windows UNC path with the // marker only for Windows-style sessions', () => {
|
||||
// Backslash UNC is unambiguous: the marker is emitted whenever the
|
||||
// caller declares the session Windows-style.
|
||||
expect(encodeHtmlUrl('sess-1', '\\\\server\\share\\proj\\a.html', true))
|
||||
it('encodes a UNC path with the // marker (backslash and forward-slash forms)', () => {
|
||||
expect(encodeHtmlUrl('sess-1', '\\\\server\\share\\proj\\a.html'))
|
||||
.toBe('/sidebar/html/sess-1//server/share/proj/a.html')
|
||||
// Forward-slash UNC needs the same marker on a Windows-style session.
|
||||
expect(encodeHtmlUrl('sess-1', '//server/share/proj/a.html', true))
|
||||
expect(encodeHtmlUrl('sess-1', '//server/share/proj/a.html'))
|
||||
.toBe('/sidebar/html/sess-1//server/share/proj/a.html')
|
||||
})
|
||||
|
||||
it('keeps a POSIX // path marker-free (the platform guard)', () => {
|
||||
// A leading // is a legitimate POSIX absolute path: without the
|
||||
// Windows-style flag it must NOT be marked as UNC, or the host would
|
||||
// decode it back into a backslash form it then rejects.
|
||||
it('keeps a POSIX // path marker-encoded (the marker is platform-neutral)', () => {
|
||||
// The decoder rebuilds '//server/share/...', which node:path resolves to
|
||||
// '/server/share/...' on POSIX and '\\server\share\...' on win32 — so the
|
||||
// leading double slash round-trips without any platform signal.
|
||||
expect(encodeHtmlUrl('s-1', '//server/share/a.html'))
|
||||
.toBe('/sidebar/html/s-1/server/share/a.html')
|
||||
.toBe('/sidebar/html/s-1//server/share/a.html')
|
||||
})
|
||||
|
||||
it('percent-encodes special characters in segments', () => {
|
||||
@@ -74,19 +71,21 @@ describe('decodeHtmlUrl', () => {
|
||||
expect(resolve('C:/Users/me/a.html')).toBe('C:\\Users\\me\\a.html')
|
||||
})
|
||||
|
||||
it('decodes a UNC round-trip back to the network path', () => {
|
||||
const url = encodeHtmlUrl('sess-1', '\\\\server\\share\\proj\\a.html', true)
|
||||
it('decodes a UNC round-trip back to the platform-neutral forward-slash form', () => {
|
||||
const url = encodeHtmlUrl('sess-1', '\\\\server\\share\\proj\\a.html')
|
||||
expect(decodeHtmlUrl(url)).toEqual({
|
||||
ok: true,
|
||||
ref: { sessionId: 'sess-1', path: '\\\\server\\share\\proj\\a.html' },
|
||||
ref: { sessionId: 'sess-1', path: '//server/share/proj/a.html' },
|
||||
})
|
||||
// The host's requireAbsolute resolves that form per-platform:
|
||||
// '\\server\share\proj\a.html' on win32, '/server/share/proj/a.html' on POSIX.
|
||||
})
|
||||
|
||||
it('decodes a marker-free POSIX // path as a plain absolute path', () => {
|
||||
it('round-trips a POSIX // path through the same marker', () => {
|
||||
const url = encodeHtmlUrl('s-1', '//server/share/a.html')
|
||||
expect(decodeHtmlUrl(url)).toEqual({
|
||||
ok: true,
|
||||
ref: { sessionId: 's-1', path: '/server/share/a.html' },
|
||||
ref: { sessionId: 's-1', path: '//server/share/a.html' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -167,12 +166,12 @@ describe('relative asset resolution stays in-route', () => {
|
||||
it('relative assets of a UNC document stay inside the same route', () => {
|
||||
// The WHATWG URL preserves the '//' marker during relative resolution,
|
||||
// so ./style.css lands back on the route with the UNC prefix intact.
|
||||
const doc = `http://h${encodeHtmlUrl('s', '\\\\server\\share\\proj\\index.html', true)}`
|
||||
const doc = `http://h${encodeHtmlUrl('s', '\\\\server\\share\\proj\\index.html')}`
|
||||
expect(new URL('./style.css', doc).pathname)
|
||||
.toBe('/sidebar/html/s//server/share/proj/style.css')
|
||||
expect(decodeHtmlUrl(new URL('./style.css', doc).pathname)).toEqual({
|
||||
ok: true,
|
||||
ref: { sessionId: 's', path: '\\\\server\\share\\proj\\style.css' },
|
||||
ref: { sessionId: 's', path: '//server/share/proj/style.css' },
|
||||
})
|
||||
})
|
||||
})
|
||||
+5
-25
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isAbsolutePath, isWindowsStylePath, relativeTo } from '../src/client/paths.ts'
|
||||
import { isAbsolutePath, relativeTo } from '../src/client/paths.ts'
|
||||
import { resolveSidebarPath } from '../src/client/produced-files.ts'
|
||||
import { htmlUrl } from '../src/client/api.ts'
|
||||
|
||||
@@ -56,33 +56,13 @@ describe('path helpers', () => {
|
||||
expect(isAbsolutePath('rel/x.ts')).toBe(false)
|
||||
})
|
||||
|
||||
it('detects windows-style session paths (platform signal for the html route)', () => {
|
||||
expect(isWindowsStylePath('C:\\work\\proj')).toBe(true)
|
||||
expect(isWindowsStylePath('C:/work/proj')).toBe(true)
|
||||
expect(isWindowsStylePath('\\\\server\\share\\proj')).toBe(true)
|
||||
expect(isWindowsStylePath('//server/share/proj')).toBe(false) // ambiguous form needs the drive/backslash signal
|
||||
expect(isWindowsStylePath('/Users/me/code')).toBe(false)
|
||||
})
|
||||
|
||||
it('htmlUrl marks UNC paths from the file path itself (cwd-independent signal)', () => {
|
||||
// cwd not hydrated yet: the unambiguous backslash UNC path must still
|
||||
// keep its '//' marker, or win32 would decode /server/... against the
|
||||
// current drive and the isWithin fence would reject the preview (403).
|
||||
it('htmlUrl always marks UNC paths (platform-neutral marker)', () => {
|
||||
// The marker is platform-neutral now: the host resolves the decoded
|
||||
// '//server/share/...' form per-platform, so no cwd/OS signal is needed.
|
||||
expect(htmlUrl({ sessionId: 's' }, '\\\\server\\share\\proj\\a.html'))
|
||||
.toBe('/sidebar/html/s//server/share/proj/a.html')
|
||||
// A forward-slash-form cwd (still Windows) does not mask the backslash path.
|
||||
expect(htmlUrl({ sessionId: 's', cwd: '//server/share/proj' }, '\\\\server\\share\\proj\\a.html'))
|
||||
.toBe('/sidebar/html/s//server/share/proj/a.html')
|
||||
// Both cwd and path in the forward-slash UNC form: the '//' path matches
|
||||
// the '//' cwd, so it is UNC and keeps the marker (win32 round-trips).
|
||||
expect(htmlUrl({ sessionId: 's', cwd: '//server/share/proj' }, '//server/share/proj/a.html'))
|
||||
.toBe('/sidebar/html/s//server/share/proj/a.html')
|
||||
// A Windows drive cwd marks forward-slash UNC paths.
|
||||
expect(htmlUrl({ sessionId: 's', cwd: 'C:\\work' }, '//server/share/proj/a.html'))
|
||||
.toBe('/sidebar/html/s//server/share/proj/a.html')
|
||||
// A POSIX session keeps forward-slash paths marker-free.
|
||||
expect(htmlUrl({ sessionId: 's', cwd: '/home/me' }, '//server/share/a.html'))
|
||||
.toBe('/sidebar/html/s/server/share/a.html')
|
||||
.toBe('/sidebar/html/s//server/share/a.html')
|
||||
expect(htmlUrl({ sessionId: 's', cwd: '/home/me' }, '/home/me/index.html'))
|
||||
.toBe('/sidebar/html/s/home/me/index.html')
|
||||
})
|
||||
|
||||
@@ -8,6 +8,3 @@
|
||||
|
||||
/** True when running on Windows (drive letters / UNC path semantics apply). */
|
||||
export const isWin32 = process.platform === 'win32'
|
||||
|
||||
/** True on every non-Windows host (POSIX path semantics). */
|
||||
export const isPosix = !isWin32
|
||||
Reference in new issue
Block a user