Преглед на файлове

fix(ssh): preserve remote filenames in file URLs

Tianyi Cui преди 6 дни
родител
ревизия
d951a49038

+ 2 - 2
packages/ssh/fs-ssh/README.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 packages/ssh/fs-ssh/README.md
-README.md: e53556a5d1961727d82b9df49d832fa7c79e0977
-README.zh.md: 3ea096d8a91ba04612697709f0b390fdb9b859cd
+README.md: 1f403796792611abc438f140356e442fbb357cde
+README.zh.md: 7e77afe6c87ed7725dea87215c9a3647d650ae95

+ 1 - 1
packages/ssh/fs-ssh/README.md

@@ -27,7 +27,7 @@ English | [中文](README.zh.md)
 
 Mount this provider with [`dsh-ssh`](../ssh/README.md) and `sandboxPolicy`; use its paired SSH subprocess and sandbox providers for execution. This provider has no configuration fields: connection identity and the default workspace belong to `dsh-ssh`, while file-effect mode belongs to `sandboxPolicy`.
 
-`resolve()` canonicalizes paths on the remote host. `processPath()` and `fileUrl()` name files in that same remote namespace; they do not grant host-side access. `processPathFromHostPath()` returns `undefined`, so consumers requiring an installed executable or bootstrap must supply a remote artifact explicitly.
+`resolve()` canonicalizes paths on the remote host. `processPath()` and `fileUrl()` name files in that same remote namespace; they do not grant host-side access. File URLs encode literal percent signs, backslashes and newlines without changing the filename. `processPathFromHostPath()` returns `undefined`, so consumers requiring an installed executable or bootstrap must supply a remote artifact explicitly.
 
 Reads preserve the shared filesystem error codes. Writes and edits send the resolved per-call policy to the helper, which canonicalizes the workspace and enforces it beside the atomic mutation. Lost transport reports an I/O failure; a mutation may already have committed and is not retried automatically.
 

+ 1 - 1
packages/ssh/fs-ssh/README.zh.md

@@ -27,7 +27,7 @@ kind: "package-reference"
 
 将本提供方与 [`dsh-ssh`](../ssh/README.zh.md) 及 `sandboxPolicy` 一同挂载,并使用配套 SSH 子进程与沙箱提供方执行程序。本提供方没有配置字段:连接身份和默认工作区属于 `dsh-ssh`,文件效果模式属于 `sandboxPolicy`。
 
-`resolve()` 在远端主机上规范化路径。`processPath()` 与 `fileUrl()` 在同一个远端命名空间中标识文件,并不授予主机侧访问能力。`processPathFromHostPath()` 返回 `undefined`,因此需要已安装可执行文件或引导程序的消费方必须显式提供远端产物。
+`resolve()` 在远端主机上规范化路径。`processPath()` 与 `fileUrl()` 在同一个远端命名空间中标识文件,并不授予主机侧访问能力。文件 URL 对字面的百分号、反斜杠和换行进行编码,保留原文件名。`processPathFromHostPath()` 返回 `undefined`,因此需要已安装可执行文件或引导程序的消费方必须显式提供远端产物。
 
 读取保留共享文件系统错误码。写入和编辑将已解析的逐次调用策略发送给辅助程序,由其规范化工作区并在原子修改所在位置执行策略。传输丢失报告 I/O 失败;修改可能已经提交,不会自动重试。
 

+ 2 - 3
packages/ssh/fs-ssh/src/index.ts

@@ -1,5 +1,6 @@
 /** Filesystem provider preserving remote identities and helper-owned atomic mutations. */
 import { posix } from 'node:path'
+import { pathToFileURL } from 'node:url'
 import { FileSystem, FsError } from '@deepseek-ai/dsh-fs'
 import type { FsDirEntry, FsEditOutcome, FsEditRequest, FsErrorCode, FsInfo, FsPathInfo, FsTarget, FsVersion, FsWriteIntent, FsWriteOutcome } from '@deepseek-ai/dsh-fs'
 import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
@@ -28,9 +29,7 @@ export class SshFileSystem extends FileSystem {
   override processPath(target: FsTarget): string { return String(target.targetKey) }
 
   override fileUrl(target: FsTarget): string {
-    const url = new URL('file:///')
-    url.pathname = this.processPath(target)
-    return url.href
+    return pathToFileURL(this.processPath(target)).href
   }
 
   override contains(parent: FsTarget, child: FsTarget): boolean {

+ 13 - 0
packages/ssh/fs-ssh/tests/provider.spec.ts

@@ -1,4 +1,5 @@
 import { Context, Service } from '@deepseek-ai/cordis'
+import { fileURLToPath } from 'node:url'
 import { FsError, FsTargetKey, FsVersion, type FsTarget } from '@deepseek-ai/dsh-fs'
 import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
 import { RemoteOperationError } from '@deepseek-ai/dsh-ssh/protocol'
@@ -30,6 +31,18 @@ async function setup() {
 }
 
 describe('SSH filesystem provider', () => {
+  it.each([
+    ['literal%20name.ts', 'literal%2520name.ts'],
+    ['back\\slash.ts', 'back%5Cslash.ts'],
+    ['line\nfeed.ts', 'line%0Afeed.ts'],
+  ])('preserves the POSIX filename %j in a file URL', async (name, encoded) => {
+    const { fs } = await setup()
+    const path = `/remote/work/${name}`
+    const url = fs.fileUrl({ targetKey: FsTargetKey(path), displayPath: path })
+    expect(url).toBe(`file:///remote/work/${encoded}`)
+    expect(fileURLToPath(url)).toBe(path)
+  })
+
   it('keeps remote canonical paths and sends relative spelling to the remote resolver', async () => {
     const { fs, dispatch } = await setup()
     dispatch.mockResolvedValue({ targetKey: '/remote/physical/file #?.txt', displayPath: 'link/../file #?.txt' })