Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions packages/core/src/filesystem.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node"
import { dirname, join, relative, resolve as pathResolve, win32 } from "path"
import { existsSync, realpathSync } from "fs"
import * as fs from "fs"
import * as NFS from "fs/promises"
import { lookup } from "mime-types"
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
Expand Down Expand Up @@ -190,7 +190,7 @@ export namespace AppFileSystem {
if (process.platform !== "win32") return path
const resolved = normalizeWindowsPath(path, options)
try {
return realpathSync.native(resolved)
return fs.realpathSync.native(resolved)
} catch {
return resolved
}
Expand All @@ -202,13 +202,13 @@ export namespace AppFileSystem {
const match = path.match(/^(.*)[\\/]\*$/)
if (!match) return normalizePath(path, options)
const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1]
return join(normalizePath(dir, options), "*")
return win32.join(normalizePath(dir, options), "*")
}

export function resolve(path: string, options?: WindowsPathOptions): string {
const resolved = process.platform === "win32" ? normalizeWindowsPath(path, options) : pathResolve(path)
try {
return normalizePath(realpathSync(resolved))
return normalizePath(fs.realpathSync(resolved))
} catch (error: any) {
if (error?.code === "ENOENT") return normalizePath(resolved)
throw error
Expand All @@ -232,8 +232,12 @@ export namespace AppFileSystem {
base?: string
driveRoots?: string[]
exists?: (path: string) => boolean
cache?: Map<string, string>
}

const rootedWindowsVariantCache = new Map<string, string>()
let rootedWindowsVariantCacheEnv = ""

// Rooted but driveless paths (e.g. "/users/runner/...") arrive when callers
// strip the drive letter before handing the path back. path.resolve would
// bind such a path to the cwd's drive, which silently mismatches when the
Expand All @@ -256,16 +260,52 @@ export namespace AppFileSystem {

function resolveRootedWindowsVariant(p: string, options: WindowsPathOptions): string | undefined {
if (!isRootedDriveless(p)) return
const useDefaultCache = !options.driveRoots && !options.exists
const cacheKey = rootedWindowsVariantCacheKey(p)
const cache = options.cache ?? (useDefaultCache ? rootedWindowsVariantCache : undefined)
if (useDefaultCache) {
refreshRootedWindowsVariantCache()
}
if (cache) {
const cached = cache.get(cacheKey)
if (cached && (options.exists ?? fs.existsSync)(cached)) return cached
if (cached) cache.delete(cacheKey)
}
const suffix = p.replace(/^[\\/]+/, "").replaceAll("/", "\\")
const matches: string[] = []
const exists = options.exists ?? fs.existsSync
for (const root of uniqueWindowsDriveRoots(options.driveRoots ?? windowsDriveRoots())) {
const candidate = win32.join(root, suffix)
if ((options.exists ?? existsSync)(candidate)) matches.push(candidate)
if (exists(candidate)) matches.push(candidate)
}
if (matches.length > 1) {
throw new Error(`Ambiguous Windows path ${p}; use a drive-qualified path.`)
}
return matches[0]
const match = matches[0]
if (cache && match) cache.set(cacheKey, match)
return match
}

function rootedWindowsVariantCacheKey(p: string): string {
return win32.normalize(p).toUpperCase()
}

function rootedWindowsVariantCacheEnvKey(): string {
const cwdRoot = uppercaseDriveRoot(win32.parse(process.cwd()).root || "")
const systemDrive = (process.env.SystemDrive || "").toUpperCase()
return `${cwdRoot}|${systemDrive}`
}

function refreshRootedWindowsVariantCache() {
const next = rootedWindowsVariantCacheEnvKey()
if (next === rootedWindowsVariantCacheEnv) return
rootedWindowsVariantCacheEnv = next
rootedWindowsVariantCache.clear()
}

export function resetWindowsRootedVariantCacheForTest() {
rootedWindowsVariantCacheEnv = ""
rootedWindowsVariantCache.clear()
}

function uniqueWindowsDriveRoots(roots: string[]): string[] {
Expand Down
53 changes: 53 additions & 0 deletions packages/core/test/filesystem/normalize-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ test("normalizePath folds extended-length UNC paths on Windows", () => {
})
})

test("normalizePathPattern preserves UNC glob roots on Windows", () => {
withWin32Platform(() => {
expect(AppFileSystem.normalizePathPattern("\\\\?\\UNC\\server\\share\\dir\\*")).toBe(
"\\\\server\\share\\dir\\*",
)
})
})

test("normalizeWindowsPath resolves non-existing rooted-driveless paths from an explicit base drive", () => {
expect(AppFileSystem.normalizeWindowsPath("\\future\\file.txt", { base: "D:\\project\\work" })).toBe(
"D:\\future\\file.txt",
Expand Down Expand Up @@ -67,6 +75,51 @@ test("normalizeWindowsPath de-duplicates caller-supplied drive roots before ambi
).toBe("C:\\shared\\file.txt")
})

test("normalizeWindowsPath caches rooted-driveless matches until the cached path disappears", () => {
withWin32Platform(() => {
AppFileSystem.resetWindowsRootedVariantCacheForTest()
const cache = new Map<string, string>()
const live = new Map<string, boolean>([
["C:\\shared\\file.txt", false],
["D:\\shared\\file.txt", true],
])
let probes = 0
const exists = (candidate: string) => {
probes += 1
return live.get(candidate) ?? false
}

const first = AppFileSystem.normalizeWindowsPath("\\shared\\file.txt", {
cache,
exists,
driveRoots: ["C:\\", "D:\\"],
})
const probesAfterFirst = probes
const second = AppFileSystem.normalizeWindowsPath("\\shared\\file.txt", {
cache,
exists,
driveRoots: ["C:\\", "D:\\"],
})

expect(first).toBe("D:\\shared\\file.txt")
expect(second).toBe("D:\\shared\\file.txt")
expect(probesAfterFirst).toBe(2)
expect(probes - probesAfterFirst).toBe(1)

live.set("D:\\shared\\file.txt", false)
live.set("E:\\shared\\file.txt", true)

const refreshed = AppFileSystem.normalizeWindowsPath("\\shared\\file.txt", {
cache,
exists,
driveRoots: ["C:\\", "D:\\", "E:\\"],
})

expect(refreshed).toBe("E:\\shared\\file.txt")
expect(probes - probesAfterFirst).toBe(5)
})
})

test.skipIf(process.platform !== "win32")(
"normalizePath probes drive roots for rooted-but-driveless paths to existing files",
() => {
Expand Down
9 changes: 7 additions & 2 deletions packages/opencode/src/tool/external-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,15 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
if (Instance.containsPath(resolved, scope)) return full

const kind = options?.kind ?? "file"
const dir = kind === "directory" ? resolved : path.dirname(resolved)
const dir =
kind === "directory"
? resolved
: process.platform === "win32"
? path.win32.dirname(resolved)
: path.dirname(resolved)
const glob =
process.platform === "win32"
? AppFileSystem.normalizePathPattern(path.join(dir, "*"), { base: ins.directory })
? AppFileSystem.normalizePathPattern(path.win32.join(dir, "*"), { base: ins.directory })
: path.join(dir, "*").replaceAll("\\", "/")

yield* ctx.ask({
Expand Down
35 changes: 35 additions & 0 deletions packages/opencode/test/tool/external-directory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,27 @@ describe("tool.assertExternalDirectory", () => {
})
})

test("returns the canonical UNC target used for permission metadata", async () => {
await withWin32Platform(async () => {
const { requests, ctx } = makeCtx()

await Instance.provide({
directory: "D:\\project",
fn: async () => {
const target = await assertExternalDirectory(ctx, "\\\\?\\UNC\\server\\share\\outside\\file.txt")
expect(target).toBe("\\\\server\\share\\outside\\file.txt")
},
})

const req = requests.find((r) => r.permission === "external_directory")
expect(req).toBeDefined()
expect(req!.patterns).toEqual(["\\\\server\\share\\outside\\*"])
expect(req!.always).toEqual(["\\\\server\\share\\outside\\*"])
expect(req!.metadata.filepath).toBe("\\\\server\\share\\outside\\file.txt")
expect(req!.metadata.realpath).toBe("\\\\server\\share\\outside\\file.txt")
})
})

test("resolves Windows junction traversal before dot-dot normalization", async () => {
await withWin32Platform(async () => {
const junction = "C:\\project\\tmp\\link"
Expand Down Expand Up @@ -280,6 +301,20 @@ describe("tool.assertExternalDirectory", () => {
})
})

test("resolves extended UNC share paths without dropping the share root", async () => {
await withWin32Platform(async () => {
const resolved = resolveExternalPathForPermission("\\\\?\\UNC\\server\\share\\dir\\file.txt", "D:\\project", {
lstat: (_candidate) =>
({
isSymbolicLink: () => false,
}) as ReturnType<typeof import("fs").lstatSync>,
realpath: (candidate) => candidate,
})

expect(resolved).toBe("\\\\server\\share\\dir\\file.txt")
})
})

if (process.platform === "win32") {
test("normalizes Windows path variants to one glob", async () => {
const { requests, ctx } = makeCtx()
Expand Down
Loading