Compare commits

...

7 Commits

Author SHA1 Message Date
autofix-ci[bot]
8b79406878 [autofix.ci] apply automated fixes 2026-01-28 21:27:50 +00:00
Claude Bot
fad01ae8d8 Fix promise error handling and improve rename docs
- Add catch {} for promise.reject/resolve in finalize() (API change)
- Expand Bun.rename() docs with function signature and conflict mode table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 21:26:00 +00:00
Claude Bot
f57454afea Merge main into claude/implement-bun-rename
Resolved conflicts in:
- docs/runtime/file-io.mdx - kept Bun.rename() docs section
- src/bun.js/api/BunObject.zig - kept rename implementation with new gc export

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 21:16:35 +00:00
autofix-ci[bot]
1f1fb2c582 [autofix.ci] apply automated fixes 2025-08-23 22:32:23 +00:00
Claude Bot
deae5c3d61 fix: Prevent UAF in Bun.rename error handling
- Switch from AnyTask to task list approach using vm.enqueueTask()
- Clone error in WorkPool task to avoid stale path buffer references
- Properly deinit cloned error in finalize to prevent memory leaks

This fixes potential use-after-free when error contains references
to path buffers that get freed before error is accessed on main thread.
2025-08-23 22:30:17 +00:00
Claude Bot
6c348f4a81 docs: Move Bun.rename documentation to file-io.md
Remove standalone rename.md page and add concise documentation
to existing file I/O docs as requested.
2025-08-23 22:18:08 +00:00
Claude Bot
7e73c0dda8 feat: Add Bun.rename() with advanced conflict resolution
Implement Bun.rename() API that extends Node.js fs.promises.rename() with
an optional third parameter for conflict resolution:

- replace (default): Replace destination if it exists
- swap: Atomically swap files/directories (Unix only, falls back on Windows)
- no-replace: Fail if destination already exists

Features:
- Uses atomic operations (renameat, renameat2) for best performance
- Cross-platform support with graceful Windows fallbacks
- Async implementation using WorkPoolTask and proper promise handling
- Comprehensive input validation and error handling
- Full TypeScript definitions with detailed JSDoc
- PathLike support (strings, Buffers, URLs)

Implementation details:
- Added to BunObject.zig with proper async patterns
- Updated C++ bindings and hash tables
- 32 comprehensive tests covering all modes and edge cases
- Complete documentation with usage examples
- Platform-aware behavior documented for Windows limitations

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 22:15:38 +00:00
6 changed files with 659 additions and 0 deletions

View File

@@ -152,6 +152,31 @@ const response = await fetch("https://bun.com");
await Bun.write("index.html", response);
```
## Renaming files (`Bun.rename()`)
`Bun.rename(from, to, conflict?): Promise<void>`
Atomically rename or move files and directories. Similar to Node.js `fs.promises.rename()` but with an optional third parameter for conflict resolution.
```ts
// Basic rename (replaces destination if exists)
await Bun.rename("old.txt", "new.txt");
// Atomic swap (Linux/macOS only, falls back to replace on Windows)
await Bun.rename("file1.txt", "file2.txt", "swap");
// Fail if destination exists
await Bun.rename("source.txt", "dest.txt", "no-replace");
```
The `conflict` parameter controls behavior when the destination already exists:
| Mode | Description |
| -------------- | -------------------------------------------------------------------------------------- |
| `"replace"` | Replace destination if it exists (default) |
| `"swap"` | Atomically swap the two files (Linux/macOS only, falls back to `"replace"` on Windows) |
| `"no-replace"` | Fail with an error if destination exists |
---
## Incremental writing with `FileSink`

View File

@@ -1163,6 +1163,39 @@ declare module "bun" {
},
): Promise<number>;
/**
* Atomically rename a file or directory from `from` to `to`.
*
* Similar to Node.js's `fs.promises.rename()`, but with an additional optional `conflict` parameter
* to control what happens when the destination already exists.
*
* On Windows, the `conflict` parameter behaves differently due to platform limitations:
* - `"swap"` falls back to `"replace"` (atomic swap is not supported)
* - `"no-replace"` may not be fully atomic (checks existence then renames)
*
* @param from - The current file or directory path
* @param to - The new file or directory path
* @param conflict - How to handle conflicts when destination exists:
* - `"replace"` (default) - Replace the destination if it exists
* - `"swap"` - Atomically swap the files (Linux/macOS only, falls back to replace on Windows)
* - `"no-replace"` - Fail if destination already exists
*
* @returns A promise that resolves when the rename is complete
*
* @example
* ```ts
* // Basic rename (replaces destination if it exists)
* await Bun.rename("old.txt", "new.txt");
*
* // Atomically swap two files (Linux/macOS only)
* await Bun.rename("file1.txt", "file2.txt", "swap");
*
* // Fail if destination exists
* await Bun.rename("source.txt", "dest.txt", "no-replace");
* ```
*/
function rename(from: PathLike, to: PathLike, conflict?: "replace" | "swap" | "no-replace"): Promise<void>;
interface SystemError extends Error {
errno?: number | undefined;
code?: string | undefined;

View File

@@ -28,6 +28,7 @@ pub const BunObject = struct {
pub const nanoseconds = toJSCallback(Bun.nanoseconds);
pub const openInEditor = toJSCallback(Bun.openInEditor);
pub const registerMacro = toJSCallback(Bun.registerMacro);
pub const rename = toJSCallback(Bun.rename);
pub const resolve = toJSCallback(Bun.resolve);
pub const resolveSync = toJSCallback(Bun.resolveSync);
pub const serve = toJSCallback(Bun.serve);
@@ -171,6 +172,7 @@ pub const BunObject = struct {
@export(&BunObject.nanoseconds, .{ .name = callbackName("nanoseconds") });
@export(&BunObject.openInEditor, .{ .name = callbackName("openInEditor") });
@export(&BunObject.registerMacro, .{ .name = callbackName("registerMacro") });
@export(&BunObject.rename, .{ .name = callbackName("rename") });
@export(&BunObject.resolve, .{ .name = callbackName("resolve") });
@export(&BunObject.resolveSync, .{ .name = callbackName("resolveSync") });
@export(&BunObject.serve, .{ .name = callbackName("serve") });
@@ -762,6 +764,219 @@ pub fn sleepSync(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
return .js_undefined;
}
const RenameConflict = enum {
replace,
swap,
no_replace,
pub fn fromJS(globalThis: *jsc.JSGlobalObject, value: jsc.JSValue) bun.JSError!?RenameConflict {
if (value.isEmptyOrUndefinedOrNull()) return null;
if (!value.isString()) {
return globalThis.throwInvalidArgumentType("rename", "conflict", "string");
}
const str = try value.toSlice(globalThis, bun.default_allocator);
defer str.deinit();
if (strings.eqlComptime(str.slice(), "replace")) {
return .replace;
} else if (strings.eqlComptime(str.slice(), "swap")) {
return .swap;
} else if (strings.eqlComptime(str.slice(), "no-replace")) {
return .no_replace;
} else {
return globalThis.throwInvalidArguments("conflict must be 'replace', 'swap', or 'no-replace'", .{});
}
}
};
const RenameResultType = bun.sys.Maybe(void);
const RenameJob = struct {
from_path: [:0]u8,
to_path: [:0]u8,
conflict: RenameConflict,
task: jsc.WorkPoolTask = .{ .callback = &runRenameTask },
promise: jsc.JSPromise.Strong = .{},
vm: *jsc.VirtualMachine,
result: RenameResultType = .{ .result = {} },
completion_task: jsc.AnyTask = undefined,
pub fn create(globalThis: *JSGlobalObject, from_path: [:0]u8, to_path: [:0]u8, conflict: RenameConflict) *RenameJob {
const job = bun.new(RenameJob, .{
.from_path = from_path,
.to_path = to_path,
.conflict = conflict,
.vm = globalThis.bunVM(),
});
job.completion_task = jsc.AnyTask.New(RenameJob, finalize).init(job);
return job;
}
pub fn runRenameTask(task: *jsc.WorkPoolTask) void {
const job: *RenameJob = @fieldParentPtr("task", task);
job.result = performRename(job.from_path, job.to_path, job.conflict);
// Clone the error to avoid UAF when path buffers are freed
switch (job.result) {
.err => |*err| {
job.result = .{ .err = err.clone(bun.default_allocator) };
},
.result => {},
}
bun.default_allocator.free(job.from_path);
bun.default_allocator.free(job.to_path);
// Schedule completion on main thread
job.vm.enqueueTask(jsc.Task.init(&job.completion_task));
}
pub fn finalize(job: *RenameJob) void {
var promise = job.promise.swap();
const globalThis = job.vm.global;
switch (job.result) {
.err => |*err| {
defer err.deinit();
const error_instance = globalThis.createErrorInstance("rename failed: errno {d}", .{err.errno});
promise.reject(globalThis, error_instance) catch {};
},
.result => {
promise.resolve(globalThis, jsc.JSValue.js_undefined) catch {};
},
}
bun.destroy(job);
}
fn performRename(from_path: [:0]const u8, to_path: [:0]const u8, conflict: RenameConflict) RenameResultType {
switch (conflict) {
.replace => {
return bun.sys.renameat(
bun.FD.cwd(),
from_path,
bun.FD.cwd(),
to_path,
);
},
.swap => {
if (comptime Environment.isWindows) {
// Windows doesn't support atomic swap, fall back to replace
return bun.sys.renameat(
bun.FD.cwd(),
from_path,
bun.FD.cwd(),
to_path,
);
} else {
// Try atomic exchange first
const result = bun.sys.renameat2(
bun.FD.cwd(),
from_path,
bun.FD.cwd(),
to_path,
.{ .exchange = true },
);
// If exchange fails because destination doesn't exist, fall back to regular rename
switch (result) {
.err => |err| {
if (err.getErrno() == .NOENT) {
return bun.sys.renameat(
bun.FD.cwd(),
from_path,
bun.FD.cwd(),
to_path,
);
}
return result;
},
.result => return result,
}
}
},
.no_replace => {
if (comptime Environment.isWindows) {
// Windows doesn't have RENAME_NOREPLACE equivalent, so we check first
switch (bun.sys.exists(to_path)) {
.result => |exists| {
if (exists) {
return .{ .err = bun.sys.Error.fromCode(.EXIST, .rename) };
}
},
.err => {},
}
return bun.sys.renameat(
bun.FD.cwd(),
from_path,
bun.FD.cwd(),
to_path,
);
} else {
return bun.sys.renameat2(
bun.FD.cwd(),
from_path,
bun.FD.cwd(),
to_path,
.{ .exclude = true },
);
}
},
}
}
};
pub fn rename(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments();
var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args.deinit();
// Parse from path
var from_path_like = try jsc.Node.PathOrFileDescriptor.fromJS(globalThis, &args, bun.default_allocator) orelse {
return globalThis.throwInvalidArgumentType("rename", "from", "string or Buffer");
};
defer from_path_like.deinit();
// Parse to path
var to_path_like = try jsc.Node.PathOrFileDescriptor.fromJS(globalThis, &args, bun.default_allocator) orelse {
return globalThis.throwInvalidArgumentType("rename", "to", "string or Buffer");
};
defer to_path_like.deinit();
// Parse optional conflict parameter
const conflict_value = args.nextEat();
const conflict = try RenameConflict.fromJS(globalThis, conflict_value orelse jsc.JSValue.js_undefined) orelse .replace;
// Only support path-based operations for now
if (from_path_like != .path or to_path_like != .path) {
return globalThis.throwInvalidArguments("rename only supports string paths currently", .{});
}
// Convert paths to null-terminated strings
var from_buf: bun.PathBuffer = undefined;
var to_buf: bun.PathBuffer = undefined;
const from_slice = from_path_like.path.sliceZ(&from_buf);
const to_slice = to_path_like.path.sliceZ(&to_buf);
// Duplicate the paths for the async task
const from_owned = try bun.default_allocator.dupeZ(u8, from_slice);
const to_owned = try bun.default_allocator.dupeZ(u8, to_slice);
// Create and schedule the rename job
const job = RenameJob.create(globalThis, from_owned, to_owned, conflict);
var promise = JSPromise.create(globalThis);
const promise_value = promise.asValue(globalThis);
promise_value.ensureStillAlive();
job.promise.strong.set(globalThis, promise_value);
jsc.WorkPool.schedule(&job.task);
return promise_value;
}
pub const gc = Bun__gc;
export fn Bun__gc(vm: *jsc.VirtualMachine, sync: bool) callconv(.c) usize {
return vm.garbageCollect(sync);

View File

@@ -62,6 +62,7 @@
macro(nanoseconds) \
macro(openInEditor) \
macro(registerMacro) \
macro(rename) \
macro(resolve) \
macro(resolveSync) \
macro(serve) \

View File

@@ -977,6 +977,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
readableStreamToJSON JSBuiltin Builtin|Function 1
readableStreamToText JSBuiltin Builtin|Function 1
registerMacro BunObject_callback_registerMacro DontEnum|DontDelete|Function 1
rename BunObject_callback_rename DontDelete|Function 1
resolve BunObject_callback_resolve DontDelete|Function 1
resolveSync BunObject_callback_resolveSync DontDelete|Function 1
revision constructBunRevision ReadOnly|DontDelete|PropertyCallback

View File

@@ -0,0 +1,384 @@
import { tmpdirSync } from "harness";
import { promises as fs } from "node:fs";
import path from "node:path";
describe("Bun.rename()", () => {
let tmpdir: string;
beforeAll(() => {
tmpdir = tmpdirSync("bun-rename");
});
it("throws when no arguments are provided", async () => {
// @ts-expect-error
await expect(() => Bun.rename()).toThrowWithCodeAsync(Error, "ERR_INVALID_ARG_TYPE");
});
it("throws when only one argument is provided", async () => {
// @ts-expect-error
await expect(() => Bun.rename("from.txt")).toThrowWithCodeAsync(Error, "ERR_INVALID_ARG_TYPE");
});
it.each([undefined, null, 1, true, Symbol("foo"), {}])("throws when `from` is not a path (%p)", async (from: any) => {
// @ts-expect-error
await expect(() => Bun.rename(from, "to.txt")).toThrowWithCodeAsync(Error, "ERR_INVALID_ARG_TYPE");
});
it.each([undefined, null, 1, true, Symbol("foo"), {}])("throws when `to` is not a path (%p)", async (to: any) => {
// @ts-expect-error
await expect(() => Bun.rename("from.txt", to)).toThrowWithCodeAsync(Error, "ERR_INVALID_ARG_TYPE");
});
it("throws when conflict parameter is invalid", async () => {
const from = path.join(tmpdir, "from.txt");
const to = path.join(tmpdir, "to.txt");
await fs.writeFile(from, "content");
// @ts-expect-error
expect(() => Bun.rename(from, to, "invalid")).toThrow("conflict must be 'replace', 'swap', or 'no-replace'");
// Clean up
await fs.unlink(from).catch(() => {});
});
describe("basic file operations", () => {
it("renames a file successfully", async () => {
const from = path.join(tmpdir, "file-to-rename.txt");
const to = path.join(tmpdir, "renamed-file.txt");
const content = "Hello, rename!";
await fs.writeFile(from, content);
await expect(Bun.rename(from, to)).resolves.toBeUndefined();
// Verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(content);
// Clean up
await fs.unlink(to).catch(() => {});
});
it("renames a directory successfully", async () => {
const from = path.join(tmpdir, "dir-to-rename");
const to = path.join(tmpdir, "renamed-dir");
const fileName = "test-file.txt";
const content = "Directory content";
await fs.mkdir(from);
await fs.writeFile(path.join(from, fileName), content);
await expect(Bun.rename(from, to)).resolves.toBeUndefined();
// Verify the directory was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(path.join(to, fileName), "utf8")).resolves.toBe(content);
// Clean up
await fs.rm(to, { recursive: true }).catch(() => {});
});
it("works with Buffer paths", async () => {
const from = path.join(tmpdir, "buffer-from.txt");
const to = path.join(tmpdir, "buffer-to.txt");
const content = "Buffer path test";
await fs.writeFile(from, content);
await expect(Bun.rename(Buffer.from(from), Buffer.from(to))).resolves.toBeUndefined();
// Verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(content);
// Clean up
await fs.unlink(to).catch(() => {});
});
it("throws when source file doesn't exist", async () => {
const from = path.join(tmpdir, "nonexistent.txt");
const to = path.join(tmpdir, "destination.txt");
await expect(Bun.rename(from, to)).rejects.toThrow();
});
});
describe("conflict resolution", () => {
describe("replace mode (default)", () => {
it("replaces existing destination by default", async () => {
const from = path.join(tmpdir, "replace-source.txt");
const to = path.join(tmpdir, "replace-dest.txt");
const sourceContent = "Source content";
const destContent = "Original dest content";
await fs.writeFile(from, sourceContent);
await fs.writeFile(to, destContent);
await expect(Bun.rename(from, to)).resolves.toBeUndefined();
// Verify the destination has the source content
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(sourceContent);
// Clean up
await fs.unlink(to).catch(() => {});
});
it("replaces existing destination with explicit 'replace'", async () => {
const from = path.join(tmpdir, "explicit-replace-source.txt");
const to = path.join(tmpdir, "explicit-replace-dest.txt");
const sourceContent = "Source content";
const destContent = "Original dest content";
await fs.writeFile(from, sourceContent);
await fs.writeFile(to, destContent);
await expect(Bun.rename(from, to, "replace")).resolves.toBeUndefined();
// Verify the destination has the source content
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(sourceContent);
// Clean up
await fs.unlink(to).catch(() => {});
});
});
describe("no-replace mode", () => {
it("fails when destination exists with 'no-replace'", async () => {
const from = path.join(tmpdir, "no-replace-source.txt");
const to = path.join(tmpdir, "no-replace-dest.txt");
const sourceContent = "Source content";
const destContent = "Original dest content";
await fs.writeFile(from, sourceContent);
await fs.writeFile(to, destContent);
await expect(Bun.rename(from, to, "no-replace")).rejects.toThrow();
// Verify both files still exist with original content
await expect(fs.readFile(from, "utf8")).resolves.toBe(sourceContent);
await expect(fs.readFile(to, "utf8")).resolves.toBe(destContent);
// Clean up
await fs.unlink(from).catch(() => {});
await fs.unlink(to).catch(() => {});
});
it("succeeds when destination doesn't exist with 'no-replace'", async () => {
const from = path.join(tmpdir, "no-replace-success-source.txt");
const to = path.join(tmpdir, "no-replace-success-dest.txt");
const content = "Source content";
await fs.writeFile(from, content);
await expect(Bun.rename(from, to, "no-replace")).resolves.toBeUndefined();
// Verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(content);
// Clean up
await fs.unlink(to).catch(() => {});
});
});
describe("swap mode", () => {
it("atomically swaps two files with 'swap'", async () => {
const file1 = path.join(tmpdir, "swap-file1.txt");
const file2 = path.join(tmpdir, "swap-file2.txt");
const content1 = "Content of file 1";
const content2 = "Content of file 2";
await fs.writeFile(file1, content1);
await fs.writeFile(file2, content2);
await expect(Bun.rename(file1, file2, "swap")).resolves.toBeUndefined();
// Verify the files were swapped
await expect(fs.readFile(file1, "utf8")).resolves.toBe(content2);
await expect(fs.readFile(file2, "utf8")).resolves.toBe(content1);
// Clean up
await fs.unlink(file1).catch(() => {});
await fs.unlink(file2).catch(() => {});
});
it("works when destination doesn't exist with 'swap'", async () => {
const from = path.join(tmpdir, "swap-from-only.txt");
const to = path.join(tmpdir, "swap-to-nonexistent.txt");
const content = "Source content";
await fs.writeFile(from, content);
await expect(Bun.rename(from, to, "swap")).resolves.toBeUndefined();
// Verify the file was moved (not swapped since destination didn't exist)
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(content);
// Clean up
await fs.unlink(to).catch(() => {});
});
it("swaps directories on Unix", async () => {
if (process.platform === "win32") {
// Skip this test on Windows
return;
}
const dir1 = path.join(tmpdir, "swap-dir1");
const dir2 = path.join(tmpdir, "swap-dir2");
const file1Content = "Content in dir 1";
const file2Content = "Content in dir 2";
await fs.mkdir(dir1);
await fs.mkdir(dir2);
await fs.writeFile(path.join(dir1, "file.txt"), file1Content);
await fs.writeFile(path.join(dir2, "file.txt"), file2Content);
await expect(Bun.rename(dir1, dir2, "swap")).resolves.toBeUndefined();
// Verify the directories were swapped
await expect(fs.readFile(path.join(dir1, "file.txt"), "utf8")).resolves.toBe(file2Content);
await expect(fs.readFile(path.join(dir2, "file.txt"), "utf8")).resolves.toBe(file1Content);
// Clean up
await fs.rm(dir1, { recursive: true }).catch(() => {});
await fs.rm(dir2, { recursive: true }).catch(() => {});
});
it("falls back to replace on Windows when using swap", async () => {
const from = path.join(tmpdir, "win-swap-source.txt");
const to = path.join(tmpdir, "win-swap-dest.txt");
const sourceContent = "Source content";
const destContent = "Dest content";
await fs.writeFile(from, sourceContent);
await fs.writeFile(to, destContent);
// On Windows, swap should fall back to replace behavior
// On Unix, it should actually swap the files
await expect(Bun.rename(from, to, "swap")).resolves.toBeUndefined();
if (process.platform === "win32") {
// Verify destination has source content (replace behavior on Windows)
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(sourceContent);
} else {
// Verify files were swapped (Unix behavior)
await expect(fs.readFile(from, "utf8")).resolves.toBe(destContent);
await expect(fs.readFile(to, "utf8")).resolves.toBe(sourceContent);
// Clean up both files
await fs.unlink(from).catch(() => {});
}
// Clean up
await fs.unlink(to).catch(() => {});
});
});
});
describe("edge cases", () => {
it("handles relative paths", async () => {
const originalCwd = process.cwd();
process.chdir(tmpdir);
try {
const from = "relative-from.txt";
const to = "relative-to.txt";
const content = "Relative path test";
await fs.writeFile(from, content);
await expect(Bun.rename(from, to)).resolves.toBeUndefined();
// Verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(content);
// Clean up
await fs.unlink(to).catch(() => {});
} finally {
process.chdir(originalCwd);
}
});
it("handles paths with special characters", async () => {
const from = path.join(tmpdir, "file with spaces & symbols!.txt");
const to = path.join(tmpdir, "renamed file with spaces & symbols!.txt");
const content = "Special chars test";
await fs.writeFile(from, content);
await expect(Bun.rename(from, to)).resolves.toBeUndefined();
// Verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(content);
// Clean up
await fs.unlink(to).catch(() => {});
});
it("handles empty files", async () => {
const from = path.join(tmpdir, "empty-from.txt");
const to = path.join(tmpdir, "empty-to.txt");
await fs.writeFile(from, "");
await expect(Bun.rename(from, to)).resolves.toBeUndefined();
// Verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe("");
// Clean up
await fs.unlink(to).catch(() => {});
});
it("handles large files", async () => {
const from = path.join(tmpdir, "large-from.txt");
const to = path.join(tmpdir, "large-to.txt");
const largeContent = "x".repeat(1024 * 1024); // 1MB
await fs.writeFile(from, largeContent);
await expect(Bun.rename(from, to)).resolves.toBeUndefined();
// Verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(largeContent);
// Clean up
await fs.unlink(to).catch(() => {});
});
});
describe("cross-platform behavior", () => {
it("works across different filesystems when supported", async () => {
// This test may fail on some systems if /tmp and current dir are on different filesystems
// but that's expected behavior
const from = path.join(tmpdir, "cross-fs-source.txt");
const to = "/tmp/cross-fs-dest.txt";
const content = "Cross filesystem test";
await fs.writeFile(from, content);
try {
await Bun.rename(from, to);
// If it succeeds, verify the file was moved
await expect(fs.access(from)).rejects.toThrow();
await expect(fs.readFile(to, "utf8")).resolves.toBe(content);
// Clean up
await fs.unlink(to).catch(() => {});
} catch (error) {
// It's okay if this fails due to cross-filesystem limitations
// Clean up source file
await fs.unlink(from).catch(() => {});
}
});
});
});