Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions src/js/node/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1054,8 +1054,9 @@ const kAlreadyValidated = Symbol("kAlreadyValidated");

class Dir {
/**
* `-1` when closed. stdio handles (0, 1, 2) don't actually get closed by
* {@link close} or {@link closeSync}.
* Open/closed state sentinel (`-1` once closed). This implementation is
* path-bound and never owns a real fd, so close() only flips this marker;
* it must not `fs.closeSync` a value the constructor was handed.
*/
#handle: number;
#path: PathLike;
Expand Down Expand Up @@ -1174,9 +1175,7 @@ class Dir {
}

#closeOp() {
const handle = this.#handle;
if (handle < 0) throw $ERR_DIR_CLOSED();
if (handle > 2) fs.closeSync(handle);
if (this.#handle < 0) throw $ERR_DIR_CLOSED();
this.#handle = -1;
}

Expand All @@ -1190,10 +1189,8 @@ class Dir {
}

closeSync() {
const handle = this.#handle;
if (handle < 0) throw $ERR_DIR_CLOSED();
if (this.#handle < 0) throw $ERR_DIR_CLOSED();
if (this.#pendingCount > 0) throw this.#dirConcurrentError();
if (handle > 2) fs.closeSync(handle);
this.#handle = -1;
}

Expand Down
48 changes: 48 additions & 0 deletions test/js/node/fs/dir.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test";
import { tempDir } from "harness";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
Expand Down Expand Up @@ -241,3 +242,50 @@ describe("Dir explicit resource management", () => {
await expect(dir[Symbol.asyncDispose]()).resolves.toBeUndefined();
});
});

// A Dir constructed directly with a raw integer must not treat that integer as
// an fd it owns: node's handle is an opaque DirHandle and calling close() on an
// integer throws there without touching any descriptor.
describe("new fs.Dir with a foreign integer handle", () => {
function openProbe(cwd: string) {
const fd = fs.openSync(path.join(cwd, "probe.txt"), "r");
return {
fd,
[Symbol.dispose]() {
try {
fs.closeSync(fd);
} catch {}
},
};
}

it("closeSync does not close the unrelated fd", () => {
using cwd = tempDir("dir-foreign-fd", { "probe.txt": "x" });
using probe = openProbe(String(cwd));
const d = new fs.Dir(probe.fd, String(cwd));
try {
d.closeSync();
} catch {
// node throws TypeError here; either way the fd must survive
}
expect(() => fs.fstatSync(probe.fd)).not.toThrow();
});

it("async close does not close the unrelated fd", async () => {
using cwd = tempDir("dir-foreign-fd", { "probe.txt": "x" });
using probe = openProbe(String(cwd));
const d = new fs.Dir(probe.fd, String(cwd));
await d.close().catch(() => {});
expect(() => fs.fstatSync(probe.fd)).not.toThrow();
});

it("Symbol.dispose does not close the unrelated fd", () => {
using cwd = tempDir("dir-foreign-fd", { "probe.txt": "x" });
using probe = openProbe(String(cwd));
{
using d = new fs.Dir(probe.fd, String(cwd));
void d;
}
expect(() => fs.fstatSync(probe.fd)).not.toThrow();
});
});
Loading