From e04696fe13e5251b4658da4499ffa7ad38aeed59 Mon Sep 17 00:00:00 2001 From: Simone <185146821+Lucenx9@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:18:58 +0100 Subject: [PATCH 1/2] fix(shared): allow atomic saves through 40 symlinks --- packages/shared/src/atomicWrite.test.ts | 31 +++++++++++++++++++++++++ packages/shared/src/symlink.ts | 3 ++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/atomicWrite.test.ts b/packages/shared/src/atomicWrite.test.ts index 366009f833132..a094f8ac72dae 100644 --- a/packages/shared/src/atomicWrite.test.ts +++ b/packages/shared/src/atomicWrite.test.ts @@ -14,6 +14,37 @@ import { writeFileStringAtomically } from "./atomicWrite.ts"; import * as HostProcess from "./HostProcess.ts"; it.layer(NodeServices.layer)("writeFileStringAtomically", (it) => { + for (const hops of [39, 40, 41]) { + it.effect.skipIf(HostProcess.Platform.defaultValue() !== "linux")( + `matches native file access for a chain of ${hops} symlinks`, + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-atomic-write-" }); + const destination = path.join(root, "settings.json"); + const first = path.join(root, "link-0"); + yield* fs.writeFileString(destination, "before"); + for (let index = hops - 1; index >= 0; index--) { + const target = index === hops - 1 ? "settings.json" : `link-${index + 1}`; + yield* fs.symlink(target, path.join(root, `link-${index}`)); + } + + const read = yield* Effect.exit(fs.readFileString(first)); + const write = yield* Effect.exit( + writeFileStringAtomically({ filePath: first, contents: "after" }), + ); + + assert.strictEqual(Exit.isSuccess(write), Exit.isSuccess(read)); + assert.strictEqual( + yield* fs.readFileString(destination), + Exit.isSuccess(read) ? "after" : "before", + ); + assert.strictEqual(yield* fs.readLink(first), "link-1"); + }), + ); + } + it.effect("keeps a symlinked file linked and rewrites its destination", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/packages/shared/src/symlink.ts b/packages/shared/src/symlink.ts index 2dcb9631c3862..9d71d2a687d93 100644 --- a/packages/shared/src/symlink.ts +++ b/packages/shared/src/symlink.ts @@ -23,7 +23,8 @@ export const resolveSymlinkTarget = Effect.fn("symlink.resolveSymlinkTarget")(fu const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; let current = path.resolve(filePath); - for (let hop = 0; hop < MAX_SYMLINK_HOPS; hop++) { + // The terminal file needs a probe after the last permitted link is followed. + for (let hop = 0; hop <= MAX_SYMLINK_HOPS; hop++) { const link = yield* fs.readLink(current).pipe( Effect.asSome, Effect.catchIf(isNotASymlink, () => Effect.succeedNone), From 9b313886c7d45911413c1a08a8d4435188c6e27f Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 07:48:26 +0000 Subject: [PATCH 2/2] test(shared): simplify symlink boundary cases --- packages/shared/src/atomicWrite.test.ts | 59 ++++++++++++------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/packages/shared/src/atomicWrite.test.ts b/packages/shared/src/atomicWrite.test.ts index a094f8ac72dae..3b0a99ec0d2d2 100644 --- a/packages/shared/src/atomicWrite.test.ts +++ b/packages/shared/src/atomicWrite.test.ts @@ -14,36 +14,35 @@ import { writeFileStringAtomically } from "./atomicWrite.ts"; import * as HostProcess from "./HostProcess.ts"; it.layer(NodeServices.layer)("writeFileStringAtomically", (it) => { - for (const hops of [39, 40, 41]) { - it.effect.skipIf(HostProcess.Platform.defaultValue() !== "linux")( - `matches native file access for a chain of ${hops} symlinks`, - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-atomic-write-" }); - const destination = path.join(root, "settings.json"); - const first = path.join(root, "link-0"); - yield* fs.writeFileString(destination, "before"); - for (let index = hops - 1; index >= 0; index--) { - const target = index === hops - 1 ? "settings.json" : `link-${index + 1}`; - yield* fs.symlink(target, path.join(root, `link-${index}`)); - } - - const read = yield* Effect.exit(fs.readFileString(first)); - const write = yield* Effect.exit( - writeFileStringAtomically({ filePath: first, contents: "after" }), - ); - - assert.strictEqual(Exit.isSuccess(write), Exit.isSuccess(read)); - assert.strictEqual( - yield* fs.readFileString(destination), - Exit.isSuccess(read) ? "after" : "before", - ); - assert.strictEqual(yield* fs.readLink(first), "link-1"); - }), - ); - } + it.effect.each(HostProcess.Platform.defaultValue() === "linux" ? [39, 40, 41] : [])( + "matches native file access for a chain of %i symlinks", + (hops) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-atomic-write-" }); + const destination = path.join(root, "settings.json"); + const first = path.join(root, "link-0"); + yield* fs.writeFileString(destination, "before"); + for (let index = hops - 1; index >= 0; index--) { + const target = index === hops - 1 ? "settings.json" : `link-${index + 1}`; + yield* fs.symlink(target, path.join(root, `link-${index}`)); + } + + const read = yield* Effect.exit(fs.readFileString(first)); + const write = yield* Effect.exit( + writeFileStringAtomically({ filePath: first, contents: "after" }), + ); + + assert.strictEqual(Exit.isSuccess(read), hops <= 40); + assert.strictEqual(Exit.isSuccess(write), Exit.isSuccess(read)); + assert.strictEqual( + yield* fs.readFileString(destination), + Exit.isSuccess(read) ? "after" : "before", + ); + assert.strictEqual(yield* fs.readLink(first), "link-1"); + }), + ); it.effect("keeps a symlinked file linked and rewrites its destination", () => Effect.gen(function* () {