fix: 修复 WSL 会话 Linux 路径解析 (#399)
This commit is contained in:
committed by
Menghuan1918
parent
dde0923980
commit
180b16de1b
+2
-1
@@ -30,6 +30,7 @@ import {
|
||||
type SidebarPrefs,
|
||||
} from './config.ts'
|
||||
import { parentOf, requireAbsolute, listDirectory, rootLabel } from './fs-tree.ts'
|
||||
import { resolveSessionPath } from './session-path.ts'
|
||||
import { writeWorkspaceUpload } from './fs-operations.ts'
|
||||
import { ensureWorkspacePath, ensureWorkspaceWritePath } from './path-security.ts'
|
||||
import { searchFiles } from './fs-search.ts'
|
||||
@@ -156,7 +157,7 @@ function selectedRepoOf(payload: unknown): string | undefined {
|
||||
* cwd when the root cannot be resolved, e.g. a bare directory).
|
||||
*/
|
||||
async function resolveGitPath(cwd: string, raw: string, selected?: string): Promise<string> {
|
||||
if (isAbsolute(raw)) return requireAbsolute(raw)
|
||||
if (isAbsolute(raw)) return requireAbsolute(resolveSessionPath(cwd, raw))
|
||||
// Prefer the session-relative interpretation when it names an existing
|
||||
// path. Git status reports repository-root-relative names, but the sidebar
|
||||
// security boundary is the session workspace; this preference keeps files
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { realpath } from 'node:fs/promises'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import { isWithin, requireAbsolute } from './fs-tree.ts'
|
||||
import { resolveSessionPath } from './session-path.ts'
|
||||
import { SidebarError } from './wire.ts'
|
||||
|
||||
/** Resolve a path and convert filesystem resolution failures to an API error. */
|
||||
@@ -25,14 +26,14 @@ function assertWithinWorkspace(workspace: string, target: string): void {
|
||||
* enforce containment.
|
||||
*
|
||||
* @param cwd - Session workspace directory.
|
||||
* @param target - Client-supplied absolute path.
|
||||
* @param target - Client-supplied absolute path in the session's namespace.
|
||||
* @param fence - Whether containment is enforced (the settings-page
|
||||
* `workspaceFence` switch). Even when false the paths are still resolved
|
||||
* through symlinks so callers always receive the canonical target.
|
||||
* @returns The canonical absolute path used for the filesystem operation.
|
||||
*/
|
||||
export async function ensureWorkspacePath(cwd: string, target: string, fence = true): Promise<string> {
|
||||
const absolute = requireAbsolute(target)
|
||||
const absolute = requireAbsolute(resolveSessionPath(cwd, target))
|
||||
const [realCwd, realTarget] = await Promise.all([
|
||||
resolveRealPath(cwd, 'workspace'),
|
||||
resolveRealPath(absolute, 'target'),
|
||||
@@ -49,13 +50,13 @@ export async function ensureWorkspacePath(cwd: string, target: string, fence = t
|
||||
* symlink is never left in the path passed to the write operation.
|
||||
*
|
||||
* @param cwd - Session workspace directory.
|
||||
* @param target - Client-supplied absolute destination path.
|
||||
* @param target - Client-supplied absolute destination path in the session's namespace.
|
||||
* @param fence - Whether containment is enforced (the settings-page
|
||||
* `workspaceFence` switch). Resolution/canonicalization is identical either way.
|
||||
* @returns A canonical path for an existing target or its nearest existing ancestor.
|
||||
*/
|
||||
export async function ensureWorkspaceWritePath(cwd: string, target: string, fence = true): Promise<string> {
|
||||
const absolute = requireAbsolute(target)
|
||||
const absolute = requireAbsolute(resolveSessionPath(cwd, target))
|
||||
const realCwd = await resolveRealPath(cwd, 'workspace')
|
||||
let existingPath = absolute
|
||||
const missingSegments: string[] = []
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { win32 } from 'node:path'
|
||||
|
||||
/** `\\wsl.localhost\<distro>` root of a Windows-hosted WSL workspace. */
|
||||
const WSL_LOCALHOST_ROOT = /^\\\\wsl\.localhost\\([^\\]+)(?:\\|$)/i
|
||||
|
||||
/**
|
||||
* Reinterpret an already-absolute path in the namespace of one session.
|
||||
*
|
||||
* Windows treats `/foo` as rooted on the current drive, so `path.resolve()`
|
||||
* turns it into e.g. `C:\\foo`. That is wrong for a session whose cwd is a
|
||||
* WSL UNC path: in that namespace `/foo` means the distro's Linux `/foo`.
|
||||
* Project only that unambiguous combination onto the matching distro root;
|
||||
* drive paths, UNC paths, ordinary Windows sessions and non-Windows hosts
|
||||
* keep their existing semantics.
|
||||
*
|
||||
* Workspace containment is intentionally NOT handled here. A projected path
|
||||
* such as `/tmp/x` can still be rejected later for lying outside the session
|
||||
* workspace; the important part is that it is checked as WSL `/tmp/x`, not
|
||||
* silently redirected to `C:\\tmp\\x`.
|
||||
*/
|
||||
export function resolveSessionPath(
|
||||
cwd: string,
|
||||
target: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string {
|
||||
if (platform !== 'win32' || !/^\/(?!\/)/.test(target)) return target
|
||||
|
||||
// Session cwd can be spelled with either separator style. Normalize only
|
||||
// for detection; the returned host path is always canonical win32 UNC.
|
||||
const normalizedCwd = cwd.replace(/\//g, '\\')
|
||||
const match = WSL_LOCALHOST_ROOT.exec(normalizedCwd)
|
||||
if (match === null) return target
|
||||
|
||||
const distroRoot = `\\\\wsl.localhost\\${match[1]}`
|
||||
const relative = target.slice(1).replace(/\//g, '\\')
|
||||
return win32.resolve(distroRoot, relative)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveSessionPath } from '../src/session-path.ts'
|
||||
|
||||
const WSL_CWD = '\\\\wsl.localhost\\archlinux\\home\\zdaar\\project'
|
||||
|
||||
describe('resolveSessionPath', () => {
|
||||
it('maps a Linux absolute path into the WSL distro root on win32', () => {
|
||||
expect(resolveSessionPath(WSL_CWD, '/home/zdaar/project/src/a.ts', 'win32'))
|
||||
.toBe('\\\\wsl.localhost\\archlinux\\home\\zdaar\\project\\src\\a.ts')
|
||||
})
|
||||
|
||||
it('maps a path outside the workspace into the same distro before containment checks', () => {
|
||||
expect(resolveSessionPath(WSL_CWD, '/tmp/dsh-issue.md', 'win32'))
|
||||
.toBe('\\\\wsl.localhost\\archlinux\\tmp\\dsh-issue.md')
|
||||
})
|
||||
|
||||
it('normalizes Linux dot segments without escaping the distro share root', () => {
|
||||
expect(resolveSessionPath(WSL_CWD, '/home/zdaar/project/../other/a.ts', 'win32'))
|
||||
.toBe('\\\\wsl.localhost\\archlinux\\home\\zdaar\\other\\a.ts')
|
||||
expect(resolveSessionPath(WSL_CWD, '/../etc/hosts', 'win32'))
|
||||
.toBe('\\\\wsl.localhost\\archlinux\\etc\\hosts')
|
||||
})
|
||||
|
||||
it('preserves Windows drive paths in a WSL session', () => {
|
||||
const target = 'C:\\Users\\zdaar\\file.md'
|
||||
expect(resolveSessionPath(WSL_CWD, target, 'win32')).toBe(target)
|
||||
})
|
||||
|
||||
it('preserves UNC paths in a WSL session', () => {
|
||||
const backslash = '\\\\server\\share\\file.md'
|
||||
const forwardSlash = '//server/share/file.md'
|
||||
expect(resolveSessionPath(WSL_CWD, backslash, 'win32')).toBe(backslash)
|
||||
expect(resolveSessionPath(WSL_CWD, forwardSlash, 'win32')).toBe(forwardSlash)
|
||||
})
|
||||
|
||||
it('does not reinterpret slash-rooted paths for ordinary Windows sessions', () => {
|
||||
expect(resolveSessionPath('C:\\Users\\zdaar\\project', '/tmp/a.ts', 'win32')).toBe('/tmp/a.ts')
|
||||
expect(resolveSessionPath('\\\\server\\share\\project', '/tmp/a.ts', 'win32')).toBe('/tmp/a.ts')
|
||||
})
|
||||
|
||||
it('does not reinterpret paths on non-Windows hosts', () => {
|
||||
expect(resolveSessionPath('/home/zdaar/project', '/tmp/a.ts', 'linux')).toBe('/tmp/a.ts')
|
||||
expect(resolveSessionPath('/Users/zdaar/project', '/tmp/a.ts', 'darwin')).toBe('/tmp/a.ts')
|
||||
})
|
||||
|
||||
it('recognizes wsl.localhost case-insensitively and with forward-slash cwd spelling', () => {
|
||||
expect(resolveSessionPath('//WSL.LOCALHOST/Ubuntu/home/me/project', '/home/me/project/a.ts', 'win32'))
|
||||
.toBe('\\\\wsl.localhost\\Ubuntu\\home\\me\\project\\a.ts')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user