Skip to content

Commit c9caf69

Browse files
committed
dofs, wsd, script: unlink symlinks without following targets
Resolve paths for rm with the final symlink preserved so unlink removes the link entry rather than the target it points at. Treat symlinks as leaves during recursive removal and cover the FUSE unlink path plus the fs-tests rm -rf scenario.
1 parent 5c4c907 commit c9caf69

4 files changed

Lines changed: 50 additions & 2 deletions

File tree

packages/dofs/src/fs/rm.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mkdir } from "./mkdir.js";
55
import { readdir } from "./readdir.js";
66
import { resolveInode } from "./resolve.js";
77
import { rm } from "./rm.js";
8+
import { symlink } from "./symlink.js";
89
import { withDB } from "./with-db.js";
910
import { writeFile } from "./writeFile.js";
1011

@@ -60,6 +61,31 @@ describe("rm", () => {
6061
});
6162
});
6263

64+
it("removes a symlink itself rather than its target", async () => {
65+
await withDB(async (db) => {
66+
await writeFile(db, "/target.txt", "still here", {}, () => 0);
67+
symlink(db, "/target.txt", "/link.txt", () => 0);
68+
69+
rm(db, "/link.txt", {});
70+
71+
expect(resolveInode(db, "/link.txt", { followSymlinks: false })).toBeNull();
72+
expect(resolveInode(db, "/target.txt")).not.toBeNull();
73+
});
74+
});
75+
76+
it("recursive rm does not follow symlinks out of the removed tree", async () => {
77+
await withDB(async (db) => {
78+
await writeFile(db, "/outside.txt", "still here", {}, () => 0);
79+
mkdir(db, "/d", {}, () => 0);
80+
symlink(db, "/outside.txt", "/d/link.txt", () => 0);
81+
82+
rm(db, "/d", { recursive: true });
83+
84+
expect(resolveInode(db, "/d", { followSymlinks: false })).toBeNull();
85+
expect(resolveInode(db, "/outside.txt")).not.toBeNull();
86+
});
87+
});
88+
6389
it("rejects ENOENT for a missing path", async () => {
6490
await withDB((db) => {
6591
expect(() => rm(db, "/missing", {})).toThrowError(

packages/dofs/src/fs/rm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function* walkPostOrder(
3232

3333
while (stack.length > 0) {
3434
const top = stack[stack.length - 1];
35-
if (top.type === "file" || top.expanded) {
35+
if (top.type !== "dir" || top.expanded) {
3636
stack.pop();
3737
yield { path: top.path, inode: top.inode, type: top.type };
3838
continue;
@@ -76,7 +76,7 @@ export function rm(db: Database, path: string, options: RmOptions): void {
7676
const recursive = options.recursive === true;
7777

7878
db.transactionSync(() => {
79-
const node = resolveInode(db, canonical);
79+
const node = resolveInode(db, canonical, { followSymlinks: false });
8080
if (node === null) {
8181
if (force) return;
8282
throw createWorkspaceError("ENOENT", `no such path: ${canonical}`, canonical);

packages/wsd/src/fuse/driver.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,19 @@ test("FUSE ops return errno values instead of throwing for expected filesystem e
199199
expect(await status((cb) => ops.unlink("/missing", cb))).toBe(-2);
200200
});
201201

202+
test("FUSE unlink removes a symlink itself rather than its target", async () => {
203+
const { vfs } = await createNodeVirtualFileSystem();
204+
const ops = makeFUSEOps(vfs);
205+
206+
vfs.writeFileSync("/target.txt", Buffer.from("still here"));
207+
expect(await status((cb) => ops.symlink("/target.txt", "/link.txt", cb))).toBe(0);
208+
209+
expect(await status((cb) => ops.unlink("/link.txt", cb))).toBe(0);
210+
211+
expect(() => vfs.lstatSync("/link.txt")).toThrow();
212+
expect(Buffer.from(vfs.readFileSync("/target.txt")).toString("utf8")).toBe("still here");
213+
});
214+
202215
test("write past the per-file cap returns EFBIG instead of growing unbounded", async () => {
203216
// The driver keeps an in-memory buffer per file and doubles its
204217
// capacity on demand. Without a ceiling, a runaway client can OOM

script/fs-tests.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,15 @@ run "follow symlink" -- 'echo content > target && ln -s target link
141141
run "stat -L follows" -- 'echo x > target && ln -s target link && [ -f link ]'
142142
run "lstat shows symlink" -- 'echo x > target && ln -s target link && [ -L link ]'
143143
run "dangling symlink" -- 'ln -s nowhere d && [ -L d ] && [ ! -e d ]'
144+
run "rm -rf removes symlink entry" -- '
145+
echo content > target
146+
mkdir d
147+
ln -s ../target d/link
148+
rm -rf d
149+
[ ! -e d ]
150+
[ -f target ]
151+
[ "$(cat target)" = "content" ]
152+
'
144153

145154
section "hard links"
146155
run "create hard link" -- 'echo content > a && ln a b && [ "$(cat b)" = "content" ]'

0 commit comments

Comments
 (0)