|
|
@@ -502,5 +502,57 @@ describe("filesystem", () => {
|
|
|
const drive = tmp.path[0].toLowerCase()
|
|
|
expect(Filesystem.resolve(`/mnt/${drive}`)).toBe(Filesystem.resolve(`${drive.toUpperCase()}:/`))
|
|
|
})
|
|
|
+
|
|
|
+ test("resolves symlinked directory to canonical path", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const target = path.join(tmp.path, "real")
|
|
|
+ await fs.mkdir(target)
|
|
|
+ const link = path.join(tmp.path, "link")
|
|
|
+ await fs.symlink(target, link)
|
|
|
+ expect(Filesystem.resolve(link)).toBe(Filesystem.resolve(target))
|
|
|
+ })
|
|
|
+
|
|
|
+ test("returns unresolved path when target does not exist", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const missing = path.join(tmp.path, "does-not-exist-" + Date.now())
|
|
|
+ const result = Filesystem.resolve(missing)
|
|
|
+ expect(result).toBe(Filesystem.normalizePath(path.resolve(missing)))
|
|
|
+ })
|
|
|
+
|
|
|
+ test("throws ELOOP on symlink cycle", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const a = path.join(tmp.path, "a")
|
|
|
+ const b = path.join(tmp.path, "b")
|
|
|
+ await fs.symlink(b, a)
|
|
|
+ await fs.symlink(a, b)
|
|
|
+ expect(() => Filesystem.resolve(a)).toThrow()
|
|
|
+ })
|
|
|
+
|
|
|
+ // Windows: chmod(0o000) is a no-op, so EACCES cannot be triggered
|
|
|
+ test("throws EACCES on permission-denied symlink target", async () => {
|
|
|
+ if (process.platform === "win32") return
|
|
|
+ if (process.getuid?.() === 0) return // skip when running as root
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const dir = path.join(tmp.path, "restricted")
|
|
|
+ await fs.mkdir(dir)
|
|
|
+ const link = path.join(tmp.path, "link")
|
|
|
+ await fs.symlink(dir, link)
|
|
|
+ await fs.chmod(dir, 0o000)
|
|
|
+ try {
|
|
|
+ expect(() => Filesystem.resolve(path.join(link, "child"))).toThrow()
|
|
|
+ } finally {
|
|
|
+ await fs.chmod(dir, 0o755)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // Windows: traversing through a file throws ENOENT (not ENOTDIR),
|
|
|
+ // which resolve() catches as a fallback instead of rethrowing
|
|
|
+ test("rethrows non-ENOENT errors", async () => {
|
|
|
+ if (process.platform === "win32") return
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const file = path.join(tmp.path, "not-a-directory")
|
|
|
+ await fs.writeFile(file, "x")
|
|
|
+ expect(() => Filesystem.resolve(path.join(file, "child"))).toThrow()
|
|
|
+ })
|
|
|
})
|
|
|
})
|