Skip to content
Merged
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
18 changes: 14 additions & 4 deletions packages/preview3-shim/lib/nodejs/filesystem/descriptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class Descriptor {
* @throws {FSError} `payload.tag` contains mapped WASI error code.
*/
async writeViaStream(data, offset) {
this.#ensureHandle();
this.#ensureWritable(data);
const stream = readableByteStreamFromReader(data, { name: "file write data" });

try {
Expand All @@ -154,7 +154,7 @@ class Descriptor {
* @throws {FSError} `payload.tag` contains mapped WASI error code.
*/
async appendViaStream(data) {
this.#ensureHandle();
this.#ensureWritable(data);
const offset = await this.#handle.stat().then((s) => s.size);
return this.writeViaStream(data, offset);
}
Expand Down Expand Up @@ -310,7 +310,8 @@ class Descriptor {
* @throws {FSError} `payload.tag` contains mapped WASI error code.
*/
readDirectory() {
if (!this.#fullPath) {
const fullPath = this.#fullPath ?? this.#hostPreopen;
if (!fullPath) {
throw new FSError("invalid");
}

Expand All @@ -321,7 +322,7 @@ class Descriptor {
.run(
{
op: "readDir",
fullPath: this.#fullPath,
fullPath,
stream: transform.writable,
preopens,
},
Expand Down Expand Up @@ -928,6 +929,15 @@ class Descriptor {
throw new FSError("bad-descriptor");
}
}

#ensureWritable(data) {
this.#ensureHandle();
if (!this.#mode.write) {
data?.[symbolDispose]?.();
data?.close?.();
throw new FSError("bad-descriptor");
}
}
}

const descriptorCreatePreopen = Descriptor._createPreopen;
Expand Down
12 changes: 10 additions & 2 deletions packages/preview3-shim/lib/nodejs/workers/filesystem-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { opendir } = fs.promises;
const CHUNK_BYTES = 64 * 1024;
// Reuse a single buffer for all read/write ops
const BUFFER = Buffer.alloc(CHUNK_BYTES);
const MAX_SAFE_FILE_POSITION = BigInt(Number.MAX_SAFE_INTEGER);

/** Auto‐dispatch all ops */
Router().op("read", handleRead).op("write", handleWrite).op("readDir", handleReadDir);
Expand All @@ -32,7 +33,7 @@ async function handleRead({ fd, offset, stream }) {
const start = pos;

while (true) {
const { bytesRead } = await readAsync(fd, BUFFER, 0, CHUNK_BYTES, pos);
const { bytesRead } = await readAsync(fd, BUFFER, 0, CHUNK_BYTES, filePosition(pos));
if (bytesRead === 0) {
break;
}
Expand Down Expand Up @@ -70,7 +71,7 @@ async function handleWrite({ fd, offset, stream }) {
break;
}
const buf = Buffer.from(value);
const { bytesWritten } = await writeAsync(fd, buf, 0, buf.length, pos);
const { bytesWritten } = await writeAsync(fd, buf, 0, buf.length, filePosition(pos));
pos += BigInt(bytesWritten);
}
return { bytesWritten: pos - start };
Expand All @@ -80,6 +81,13 @@ async function handleWrite({ fd, offset, stream }) {
}
}

function filePosition(position) {
if (position < 0n || position > MAX_SAFE_FILE_POSITION) {
throw new FSError("invalid");
}
return Number(position);
}

async function handleReadDir({ fullPath, stream, preopens }) {
if (!stream || typeof stream.getWriter !== "function") {
throw new TypeError("stream must have a getWriter() method");
Expand Down
88 changes: 88 additions & 0 deletions packages/preview3-shim/test/filesystem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,73 @@ describe("Descriptor with os.tmpdir()", () => {
child[Symbol.dispose]?.();
});

test("writeViaStream accepts non zero offsets", async () => {
const sub = `${relBase}/file-write-offset.txt`;
const child = await rootDescriptor.openAt(
{},
sub,
{ create: true },
{ read: true, write: true },
);

const { tx, rx } = stream();
await tx.write("Hello, World!");
await tx.close();
await child.writeViaStream(rx, 5n);

const [sr, fr] = child.readViaStream(0n);
const buf = await sr.readAll();
await fr.read();

expect(buf).toEqual(Buffer.from("\0\0\0\0\0Hello, World!"));

child[Symbol.dispose]?.();
});

test("readViaStream accepts non zero offsets", async () => {
const sub = `${relBase}/file-read-offset.txt`;
const child = await rootDescriptor.openAt(
{},
sub,
{ create: true },
{ read: true, write: true },
);

const { tx, rx } = stream();
await tx.write("skip-read");
await tx.close();
await child.writeViaStream(rx, 5n);

const [sr, fr] = child.readViaStream(5n);
const buf = await sr.readAll();
await fr.read();

expect(new TextDecoder().decode(buf)).toBe("skip-read");

child[Symbol.dispose]?.();
});

test("writeViaStream rejects read-only descriptors before consuming data", async () => {
const sub = `${relBase}/file-read-only-write.txt`;
const writable = await rootDescriptor.openAt(
{},
sub,
{ create: true },
{ read: true, write: true },
);
writable[Symbol.dispose]?.();

const readOnly = await rootDescriptor.openAt({}, sub, {}, { read: true });
const reader = {
read: vi.fn(async () => Uint8Array.of(1)),
};

await expect(readOnly.writeViaStream(reader, 0n)).rejects.toThrow();
expect(reader.read).not.toHaveBeenCalled();

readOnly[Symbol.dispose]?.();
});

test("appendViaStream <=> readViaStream round-trip", async () => {
const sub = `${relBase}/file-append.txt`;
const child = await rootDescriptor.openAt(
Expand Down Expand Up @@ -131,6 +198,27 @@ describe("Descriptor with os.tmpdir()", () => {
dirDesc[Symbol.dispose]?.();
});

test("readDirectory works on preopen descriptors", async () => {
const entryName = "preopen-entry.txt";
await fs.writeFile(path.join(tmpDir, entryName), "");

const virtualPath = `/preview3-test-${path.basename(tmpDir)}`;
filesystem._addPreopen(virtualPath, tmpDir);
const [preopen] = filesystem.preopens
.getDirectories()
.find(([_desc, path]) => path === virtualPath);

const [stream, future] = preopen.readDirectory();
const entries = [];
let entry;
while ((entry = await stream.read()) !== null) {
entries.push(entry);
}
await future.read();

expect(entries.some((e) => e.name === entryName)).toBe(true);
});

test("isSameObject & metadataHash vs metadataHashAt", async () => {
const sub = `${relBase}/file3.txt`;
const child = await rootDescriptor.openAt(
Expand Down
Loading