Forráskód Böngészése

fix: handle special characters in paths and git snapshot reading logic(#9804) (#9807)

shirukai 1 hónapja
szülő
commit
cf6ad4c407
2 módosított fájl, 94 hozzáadás és 14 törlés
  1. 16 2
      packages/app/src/context/file.tsx
  2. 78 12
      packages/opencode/src/snapshot/index.ts

+ 16 - 2
packages/app/src/context/file.tsx

@@ -195,7 +195,20 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
       const root = directory()
       const prefix = root.endsWith("/") ? root : root + "/"
 
-      let path = stripQueryAndHash(stripFileProtocol(input))
+      let path = input
+
+      // Only strip protocol and decode if it's a file URI
+      if (path.startsWith("file://")) {
+        const raw = stripQueryAndHash(stripFileProtocol(path))
+        try {
+          // Attempt to treat as a standard URI
+          path = decodeURIComponent(raw)
+        } catch {
+          // Fallback for legacy paths that might contain invalid URI sequences (e.g. "100%")
+          // In this case, we treat the path as raw, but still strip the protocol
+          path = raw
+        }
+      }
 
       if (path.startsWith(prefix)) {
         path = path.slice(prefix.length)
@@ -218,7 +231,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
 
     function tab(input: string) {
       const path = normalize(input)
-      return `file://${path}`
+      const encoded = path.split("/").map(encodeURIComponent).join("/")
+      return `file://${encoded}`
     }
 
     function pathFromTab(tabValue: string) {

+ 78 - 12
packages/opencode/src/snapshot/index.ts

@@ -104,6 +104,7 @@ export namespace Snapshot {
         .split("\n")
         .map((x) => x.trim())
         .filter(Boolean)
+        .map((x) => unquote(x))
         .map((x) => path.join(Instance.worktree, x)),
     }
   }
@@ -151,7 +152,7 @@ export namespace Snapshot {
             })
           } else {
             log.info("file did not exist in snapshot, deleting", { file })
-            await fs.unlink(file).catch(() => {})
+            await fs.unlink(file).catch(() => { })
           }
         }
         files.add(file)
@@ -202,20 +203,22 @@ export namespace Snapshot {
       .nothrow()
       .lines()) {
       if (!line) continue
-      const [additions, deletions, file] = line.split("\t")
+      const [additions, deletions, rawFile] = line.split("\t")
+      const file = unquote(rawFile)
       const isBinaryFile = additions === "-" && deletions === "-"
-      const before = isBinaryFile
-        ? ""
+      const beforeResult = isBinaryFile
+        ? { exitCode: 0, text: () => "", stderr: Buffer.from("") }
         : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
-            .quiet()
-            .nothrow()
-            .text()
-      const after = isBinaryFile
-        ? ""
+          .quiet()
+          .nothrow()
+      const before = beforeResult.exitCode === 0 ? beforeResult.text() : `[DEBUG ERROR] git show ${from}:${file} failed: ${beforeResult.stderr.toString()}`
+
+      const afterResult = isBinaryFile
+        ? { exitCode: 0, text: () => "", stderr: Buffer.from("") }
         : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
-            .quiet()
-            .nothrow()
-            .text()
+          .quiet()
+          .nothrow()
+      const after = afterResult.exitCode === 0 ? afterResult.text() : `[DEBUG ERROR] git show ${to}:${file} failed: ${afterResult.stderr.toString()}`
       const added = isBinaryFile ? 0 : parseInt(additions)
       const deleted = isBinaryFile ? 0 : parseInt(deletions)
       result.push({
@@ -229,6 +232,69 @@ export namespace Snapshot {
     return result
   }
 
+  export function unquote(path: string): string {
+    // If the path is wrapped in quotes, it might contain octal escapes
+    if (path.startsWith('"') && path.endsWith('"')) {
+      const quoted = path.slice(1, -1)
+      // Decode escaped characters
+      const buffer: number[] = []
+      for (let i = 0; i < quoted.length; i++) {
+        if (quoted[i] === "\\") {
+          i++
+          // Check for octal escape (e.g. \344)
+          if (i + 2 < quoted.length && /^[0-7]{3}$/.test(quoted.slice(i, i + 3))) {
+            const octal = quoted.slice(i, i + 3)
+            buffer.push(parseInt(octal, 8))
+            i += 2
+          } else {
+            // Handle standard escapes
+            switch (quoted[i]) {
+              case "b":
+                buffer.push(8)
+                break
+              case "t":
+                buffer.push(9)
+                break
+              case "n":
+                buffer.push(10)
+                break
+              case "v":
+                buffer.push(11)
+                break
+              case "f":
+                buffer.push(12)
+                break
+              case "r":
+                buffer.push(13)
+                break
+              case '"':
+                buffer.push(34)
+                break
+              case "\\":
+                buffer.push(92)
+                break
+              default:
+                // If unknown escape, keep original (or char code of escaped char)
+                buffer.push(quoted.charCodeAt(i))
+            }
+          }
+        } else {
+          const charCode = quoted.charCodeAt(i)
+          if (charCode < 128) {
+            buffer.push(charCode)
+          } else {
+            const charBuffer = Buffer.from(quoted[i])
+            for (const byte of charBuffer) {
+              buffer.push(byte)
+            }
+          }
+        }
+      }
+      return Buffer.from(buffer).toString("utf8")
+    }
+    return path
+  }
+
   function gitdir() {
     const project = Instance.project
     return path.join(Global.Path.data, "snapshot", project.id)