Răsfoiți Sursa

fix(fs): share native path rooting between resolve and lstat

Tianyi Cui 2 săptămâni în urmă
părinte
comite
4c7b6902e3

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 0899b817e6d73826de170b53581cecfb05cec003
-config-catalog.zh.md: f96f0db17698c1c1e4527e1164bf14b6d494f05b
+config-catalog.md: 0f73ff98450f8036d2ba3d85f7ab1b41bee95ea6
+config-catalog.zh.md: ea864ba538b743fb22718d8079fc11774c4606dd

+ 1 - 1
docs/config-catalog.md

@@ -752,7 +752,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/fs/fs-local/src/index.ts:42`](../packages/fs/fs-local/src/index.ts)
+Source: [`packages/fs/fs-local/src/index.ts:43`](../packages/fs/fs-local/src/index.ts)
 
 <a id="deepseek-aidsh-fs-sandbox"></a>
 

+ 1 - 1
docs/config-catalog.zh.md

@@ -754,7 +754,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/fs/fs-local/src/index.ts:42`](../packages/fs/fs-local/src/index.ts)
+来源:[`packages/fs/fs-local/src/index.ts:43`](../packages/fs/fs-local/src/index.ts)
 
 <a id="deepseek-aidsh-fs-sandbox"></a>
 

+ 15 - 5
packages/fs/fs-local/src/fsio.ts

@@ -136,6 +136,20 @@ export interface LocalDirEntry {
   size?: number
 }
 
+/**
+ * Anchor a path using native drive semantics and POSIX physical parent traversal.
+ * @param cwd - provider base directory for relative paths.
+ * @param path - non-empty requested path.
+ * @returns absolute display spelling shared by target resolution and no-follow metadata.
+ */
+export function localDisplayPath(cwd: string, path: string): string {
+  const absoluteCwd = isAbsolute(cwd) ? cwd : `${process.cwd()}${sep}${cwd}`
+  const raw = isAbsolute(path) ? path : `${absoluteCwd}${sep}${path}`
+  const physicalSpelling = /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(raw) ? raw : resolve(cwd, path)
+  /* v8 ignore next -- Native Windows tests cover DOS drive-relative resolution; POSIX preserves physical traversal. */
+  return process.platform === 'win32' ? resolve(cwd, path) : physicalSpelling
+}
+
 /**
  * Resolve a path to its absolute display path and realpath identity. For a missing target,
  * realpath the nearest existing ancestor and append the missing suffix, preserving identity
@@ -146,11 +160,7 @@ export interface LocalDirEntry {
  */
 export async function resolveLocalTarget(cwd: string, path: string): Promise<LocalTarget> {
   if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
-  const absoluteCwd = isAbsolute(cwd) ? cwd : `${process.cwd()}${sep}${cwd}`
-  const raw = isAbsolute(path) ? path : `${absoluteCwd}${sep}${path}`
-  const physicalSpelling = /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(raw) ? raw : resolve(cwd, path)
-  /* v8 ignore next -- Native Windows tests cover DOS drive-relative resolution; POSIX preserves physical traversal. */
-  const displayPath = process.platform === 'win32' ? resolve(cwd, path) : physicalSpelling
+  const displayPath = localDisplayPath(cwd, path)
   try {
     // Prefer the file's own realpath (resolves a symlinked file to its target).
     return { displayPath, targetKey: FsTargetKey(await realpath(displayPath)) }

+ 2 - 1
packages/fs/fs-local/src/index.ts

@@ -23,6 +23,7 @@ import type {
 import {
   applyLiteralEdit,
   listDirectory,
+  localDisplayPath,
   normalizeLineEndings,
   probe,
   probeNoFollow,
@@ -140,7 +141,7 @@ export class LocalFileSystem extends FileSystem {
     if (signal?.aborted) throw new FsError('lstat aborted', 'FS_ABORTED')
     if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
     const cwd = opts?.cwd ?? this.config.cwd
-    const info = await probeNoFollow(isAbsolute(path) ? path : `${cwd}/${path}`)
+    const info = await probeNoFollow(localDisplayPath(cwd, path))
     if (signal?.aborted) throw new FsError('lstat aborted', 'FS_ABORTED')
     if (!info) return undefined
     return { version: info.version, type: info.type, size: info.size }

+ 12 - 1
packages/fs/fs-local/tests/filesystem.spec.ts

@@ -10,7 +10,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { constants as bufferConstants } from 'node:buffer'
 import { mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, unlink, utimes, writeFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
-import { join } from 'node:path'
+import { join, parse, relative } from 'node:path'
 import { pathToFileURL } from 'node:url'
 import { Context } from '@deepseek-ai/cordis'
 import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
@@ -167,6 +167,17 @@ describe('stat', () => {
 })
 
 describe('lstat', () => {
+  it.skipIf(process.platform !== 'win32')('uses the same native drive-relative paths as resolve', async () => {
+    await writeFile(join(dir, 'created.txt'), 'native')
+    const drive = parse(dir).root.slice(0, 2)
+    for (const cwd of [dir, `${drive}${relative(process.cwd(), dir)}`]) {
+      for (const path of ['created.txt', `${drive}created.txt`]) {
+        const target = await fs.resolve(path, { cwd })
+        expect(await fs.lstat(path, { cwd })).toEqual(await fs.stat(target))
+      }
+    }
+  })
+
   it('reports path metadata without following the final symlink component', async () => {
     await writeFile(join(dir, 'real.txt'), 'hello')
     await symlink(join(dir, 'real.txt'), join(dir, 'link.txt'))