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>
This commit is contained in:
co-authored by
JosXa
Luke Parker
parent
eabf3caeb9
commit
880c0a7477
@@ -4,6 +4,7 @@ import { makeRuntime } from "@/effect/run-service"
|
|||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import type { SessionID } from "@/session/schema"
|
import type { SessionID } from "@/session/schema"
|
||||||
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
export namespace FileTime {
|
export namespace FileTime {
|
||||||
@@ -62,6 +63,7 @@ export namespace FileTime {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
|
const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
|
||||||
|
filepath = Filesystem.normalizePath(filepath)
|
||||||
const locks = (yield* InstanceState.get(state)).locks
|
const locks = (yield* InstanceState.get(state)).locks
|
||||||
const lock = locks.get(filepath)
|
const lock = locks.get(filepath)
|
||||||
if (lock) return lock
|
if (lock) return lock
|
||||||
@@ -72,18 +74,21 @@ export namespace FileTime {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
|
const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
|
||||||
|
file = Filesystem.normalizePath(file)
|
||||||
const reads = (yield* InstanceState.get(state)).reads
|
const reads = (yield* InstanceState.get(state)).reads
|
||||||
log.info("read", { sessionID, file })
|
log.info("read", { sessionID, file })
|
||||||
session(reads, sessionID).set(file, yield* stamp(file))
|
session(reads, sessionID).set(file, yield* stamp(file))
|
||||||
})
|
})
|
||||||
|
|
||||||
const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
|
const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
|
||||||
|
file = Filesystem.normalizePath(file)
|
||||||
const reads = (yield* InstanceState.get(state)).reads
|
const reads = (yield* InstanceState.get(state)).reads
|
||||||
return reads.get(sessionID)?.get(file)?.read
|
return reads.get(sessionID)?.get(file)?.read
|
||||||
})
|
})
|
||||||
|
|
||||||
const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
|
const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
|
||||||
if (disableCheck) return
|
if (disableCheck) return
|
||||||
|
filepath = Filesystem.normalizePath(filepath)
|
||||||
|
|
||||||
const reads = (yield* InstanceState.get(state)).reads
|
const reads = (yield* InstanceState.get(state)).reads
|
||||||
const time = reads.get(sessionID)?.get(filepath)
|
const time = reads.get(sessionID)?.get(filepath)
|
||||||
|
|||||||
@@ -306,6 +306,97 @@ describe("file/time", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("path normalization", () => {
|
||||||
|
test("read with forward slashes, assert with backslashes", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
const filepath = path.join(tmp.path, "file.txt")
|
||||||
|
await fs.writeFile(filepath, "content", "utf-8")
|
||||||
|
await touch(filepath, 1_000)
|
||||||
|
|
||||||
|
const forwardSlash = filepath.replaceAll("\\", "/")
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
await FileTime.read(sessionID, forwardSlash)
|
||||||
|
// assert with the native backslash path should still work
|
||||||
|
await FileTime.assert(sessionID, filepath)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("read with backslashes, assert with forward slashes", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
const filepath = path.join(tmp.path, "file.txt")
|
||||||
|
await fs.writeFile(filepath, "content", "utf-8")
|
||||||
|
await touch(filepath, 1_000)
|
||||||
|
|
||||||
|
const forwardSlash = filepath.replaceAll("\\", "/")
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
await FileTime.read(sessionID, filepath)
|
||||||
|
// assert with forward slashes should still work
|
||||||
|
await FileTime.assert(sessionID, forwardSlash)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("get returns timestamp regardless of slash direction", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
const filepath = path.join(tmp.path, "file.txt")
|
||||||
|
await fs.writeFile(filepath, "content", "utf-8")
|
||||||
|
|
||||||
|
const forwardSlash = filepath.replaceAll("\\", "/")
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
await FileTime.read(sessionID, forwardSlash)
|
||||||
|
const result = await FileTime.get(sessionID, filepath)
|
||||||
|
expect(result).toBeInstanceOf(Date)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("withLock serializes regardless of slash direction", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
const filepath = path.join(tmp.path, "file.txt")
|
||||||
|
|
||||||
|
const forwardSlash = filepath.replaceAll("\\", "/")
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const order: number[] = []
|
||||||
|
const hold = gate()
|
||||||
|
const ready = gate()
|
||||||
|
|
||||||
|
const op1 = FileTime.withLock(filepath, async () => {
|
||||||
|
order.push(1)
|
||||||
|
ready.open()
|
||||||
|
await hold.wait
|
||||||
|
order.push(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
await ready.wait
|
||||||
|
|
||||||
|
// Use forward-slash variant -- should still serialize against op1
|
||||||
|
const op2 = FileTime.withLock(forwardSlash, async () => {
|
||||||
|
order.push(3)
|
||||||
|
order.push(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
hold.open()
|
||||||
|
|
||||||
|
await Promise.all([op1, op2])
|
||||||
|
expect(order).toEqual([1, 2, 3, 4])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("stat() Filesystem.stat pattern", () => {
|
describe("stat() Filesystem.stat pattern", () => {
|
||||||
test("reads file modification time via Filesystem.stat()", async () => {
|
test("reads file modification time via Filesystem.stat()", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
|
|||||||
Reference in New Issue
Block a user