Skip to content

Commit 880c0a7

Browse files
JosXaJosXaHona
authored
fix: normalize filepath in FileTime to prevent Windows path mismatch (#20367)
Co-authored-by: JosXa <info@josxa.dev> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
1 parent eabf3ca commit 880c0a7

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

packages/opencode/src/file/time.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { makeRuntime } from "@/effect/run-service"
44
import { AppFileSystem } from "@/filesystem"
55
import { Flag } from "@/flag/flag"
66
import type { SessionID } from "@/session/schema"
7+
import { Filesystem } from "@/util/filesystem"
78
import { Log } from "../util/log"
89

910
export namespace FileTime {
@@ -62,6 +63,7 @@ export namespace FileTime {
6263
)
6364

6465
const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
66+
filepath = Filesystem.normalizePath(filepath)
6567
const locks = (yield* InstanceState.get(state)).locks
6668
const lock = locks.get(filepath)
6769
if (lock) return lock
@@ -72,18 +74,21 @@ export namespace FileTime {
7274
})
7375

7476
const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
77+
file = Filesystem.normalizePath(file)
7578
const reads = (yield* InstanceState.get(state)).reads
7679
log.info("read", { sessionID, file })
7780
session(reads, sessionID).set(file, yield* stamp(file))
7881
})
7982

8083
const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
84+
file = Filesystem.normalizePath(file)
8185
const reads = (yield* InstanceState.get(state)).reads
8286
return reads.get(sessionID)?.get(file)?.read
8387
})
8488

8589
const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
8690
if (disableCheck) return
91+
filepath = Filesystem.normalizePath(filepath)
8792

8893
const reads = (yield* InstanceState.get(state)).reads
8994
const time = reads.get(sessionID)?.get(filepath)

packages/opencode/test/file/time.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,97 @@ describe("file/time", () => {
306306
})
307307
})
308308

309+
describe("path normalization", () => {
310+
test("read with forward slashes, assert with backslashes", async () => {
311+
await using tmp = await tmpdir()
312+
const filepath = path.join(tmp.path, "file.txt")
313+
await fs.writeFile(filepath, "content", "utf-8")
314+
await touch(filepath, 1_000)
315+
316+
const forwardSlash = filepath.replaceAll("\\", "/")
317+
318+
await Instance.provide({
319+
directory: tmp.path,
320+
fn: async () => {
321+
await FileTime.read(sessionID, forwardSlash)
322+
// assert with the native backslash path should still work
323+
await FileTime.assert(sessionID, filepath)
324+
},
325+
})
326+
})
327+
328+
test("read with backslashes, assert with forward slashes", async () => {
329+
await using tmp = await tmpdir()
330+
const filepath = path.join(tmp.path, "file.txt")
331+
await fs.writeFile(filepath, "content", "utf-8")
332+
await touch(filepath, 1_000)
333+
334+
const forwardSlash = filepath.replaceAll("\\", "/")
335+
336+
await Instance.provide({
337+
directory: tmp.path,
338+
fn: async () => {
339+
await FileTime.read(sessionID, filepath)
340+
// assert with forward slashes should still work
341+
await FileTime.assert(sessionID, forwardSlash)
342+
},
343+
})
344+
})
345+
346+
test("get returns timestamp regardless of slash direction", async () => {
347+
await using tmp = await tmpdir()
348+
const filepath = path.join(tmp.path, "file.txt")
349+
await fs.writeFile(filepath, "content", "utf-8")
350+
351+
const forwardSlash = filepath.replaceAll("\\", "/")
352+
353+
await Instance.provide({
354+
directory: tmp.path,
355+
fn: async () => {
356+
await FileTime.read(sessionID, forwardSlash)
357+
const result = await FileTime.get(sessionID, filepath)
358+
expect(result).toBeInstanceOf(Date)
359+
},
360+
})
361+
})
362+
363+
test("withLock serializes regardless of slash direction", async () => {
364+
await using tmp = await tmpdir()
365+
const filepath = path.join(tmp.path, "file.txt")
366+
367+
const forwardSlash = filepath.replaceAll("\\", "/")
368+
369+
await Instance.provide({
370+
directory: tmp.path,
371+
fn: async () => {
372+
const order: number[] = []
373+
const hold = gate()
374+
const ready = gate()
375+
376+
const op1 = FileTime.withLock(filepath, async () => {
377+
order.push(1)
378+
ready.open()
379+
await hold.wait
380+
order.push(2)
381+
})
382+
383+
await ready.wait
384+
385+
// Use forward-slash variant -- should still serialize against op1
386+
const op2 = FileTime.withLock(forwardSlash, async () => {
387+
order.push(3)
388+
order.push(4)
389+
})
390+
391+
hold.open()
392+
393+
await Promise.all([op1, op2])
394+
expect(order).toEqual([1, 2, 3, 4])
395+
},
396+
})
397+
})
398+
})
399+
309400
describe("stat() Filesystem.stat pattern", () => {
310401
test("reads file modification time via Filesystem.stat()", async () => {
311402
await using tmp = await tmpdir()

0 commit comments

Comments
 (0)