Compare commits

...

3 Commits

Author SHA1 Message Date
Claude
72c643b3a1 Add hardlink test for bun shell rm command
This adds a test to verify that removing hardlinked files works correctly:
- Removing a hardlink doesn't delete the original file
- Removing a directory containing hardlinks preserves the original files
- File contents remain intact after hardlink removal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 15:34:02 -08:00
Arnold
4867f49c8a Merge branch 'main' into fix/rm-links 2025-11-28 19:51:04 +01:00
Arnold Filip
aa2dbc6149 fix #20633 - bun shell rm -r safe on windows
Enhance rm command to support symlink handling on Windows
2025-11-28 17:47:44 +01:00
2 changed files with 84 additions and 2 deletions

View File

@@ -830,7 +830,7 @@ pub const ShellRmTask = struct {
return Maybe(void).initErr(Syscall.Error.fromCode(bun.sys.E.ISDIR, .TODO).withPath(bun.handleOom(bun.default_allocator.dupeZ(u8, dir_task.path))));
}
const flags = bun.O.DIRECTORY | bun.O.RDONLY;
const flags = bun.O.DIRECTORY | bun.O.RDONLY | if (bun.Environment.isWindows) bun.O.NOFOLLOW else 0;
const fd = switch (ShellSyscall.openat(dirfd, path, flags, 0)) {
.result => |fd| fd,
.err => |e| {

View File

@@ -7,7 +7,7 @@
import { $ } from "bun";
import { beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { tempDirWithFiles } from "harness";
import { mkdirSync, writeFileSync } from "node:fs";
import { linkSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
import path from "path";
import { createTestBuilder, sortedShellOutput } from "../util";
const TestBuilder = createTestBuilder(import.meta.path);
@@ -144,6 +144,88 @@ foo/
expect(await fileExists(`${tempdir}/sub_dir_files`)).toBeTrue();
}
});
test("symbolic links", async () => {
const files = {
"keep_file.txt": "",
"keep_dir": {},
"keep_dir/file.txt": "",
"sub_dir": {},
};
const tempdir = tempDirWithFiles("rmsymlinks", files);
// Create file symlink and remove it
{
symlinkSync(`${tempdir}/keep_file.txt`, `${tempdir}/file_symlink.txt`);
expect(await fileExists(`${tempdir}/file_symlink.txt`)).toBeTrue();
await $`rm file_symlink.txt`.cwd(tempdir);
expect(await fileExists(`${tempdir}/file_symlink.txt`)).toBeFalse();
expect(await fileExists(`${tempdir}/keep_file.txt`)).toBeTrue();
}
// Create directory symlink and remove it
{
symlinkSync(`${tempdir}/keep_dir`, `${tempdir}/dir_symlink`);
expect(await fileExists(`${tempdir}/dir_symlink`)).toBeTrue();
expect(await fileExists(`${tempdir}/dir_symlink/file.txt`)).toBeTrue();
await $`rm -r dir_symlink`.cwd(tempdir);
expect(await fileExists(`${tempdir}/dir_symlink`)).toBeFalse();
expect(await fileExists(`${tempdir}/keep_dir`)).toBeTrue();
expect(await fileExists(`${tempdir}/keep_dir/file.txt`)).toBeTrue();
}
// Create symlinks in sub_dir and remove it
{
symlinkSync(`${tempdir}/keep_file.txt`, `${tempdir}/sub_dir/file_symlink.txt`);
symlinkSync(`${tempdir}/keep_dir`, `${tempdir}/sub_dir/dir_symlink`);
expect(await fileExists(`${tempdir}/sub_dir`)).toBeTrue();
expect(await fileExists(`${tempdir}/sub_dir/file_symlink.txt`)).toBeTrue();
expect(await fileExists(`${tempdir}/sub_dir/dir_symlink`)).toBeTrue();
expect(await fileExists(`${tempdir}/sub_dir/dir_symlink/file.txt`)).toBeTrue();
await $`rm -r sub_dir`.cwd(tempdir);
expect(await fileExists(`${tempdir}/sub_dir`)).toBeFalse();
expect(await fileExists(`${tempdir}/keep_dir`)).toBeTrue();
expect(await fileExists(`${tempdir}/keep_dir/file.txt`)).toBeTrue();
}
});
test("hard links", async () => {
const files = {
"keep_file.txt": "original content",
"sub_dir": {},
};
const tempdir = tempDirWithFiles("rmhardlinks", files);
// Create file hardlink and remove it - original should still exist
{
linkSync(`${tempdir}/keep_file.txt`, `${tempdir}/file_hardlink.txt`);
expect(await fileExists(`${tempdir}/file_hardlink.txt`)).toBeTrue();
expect(await fileExists(`${tempdir}/keep_file.txt`)).toBeTrue();
await $`rm file_hardlink.txt`.cwd(tempdir);
expect(await fileExists(`${tempdir}/file_hardlink.txt`)).toBeFalse();
expect(await fileExists(`${tempdir}/keep_file.txt`)).toBeTrue();
// Verify content is still intact in the original file
expect(await Bun.file(`${tempdir}/keep_file.txt`).text()).toBe("original content");
}
// Create hardlinks in sub_dir and remove the directory
{
linkSync(`${tempdir}/keep_file.txt`, `${tempdir}/sub_dir/file_hardlink.txt`);
expect(await fileExists(`${tempdir}/sub_dir`)).toBeTrue();
expect(await fileExists(`${tempdir}/sub_dir/file_hardlink.txt`)).toBeTrue();
await $`rm -r sub_dir`.cwd(tempdir);
expect(await fileExists(`${tempdir}/sub_dir`)).toBeFalse();
expect(await fileExists(`${tempdir}/keep_file.txt`)).toBeTrue();
// Verify content is still intact in the original file
expect(await Bun.file(`${tempdir}/keep_file.txt`).text()).toBe("original content");
}
});
});
function packagejson() {