Add missing check for .write() on a data-backed blob (#21552)

### What does this PR do?

Add missing check for .write() on a data-backed blob

### How did you verify your code works?

There is a test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
This commit is contained in:
Jarred Sumner
2025-08-01 20:04:16 -07:00
committed by GitHub
parent bb67f2b345
commit 07ffde8a69
2 changed files with 95 additions and 16 deletions

View File

@@ -1464,6 +1464,15 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
return writeFileWithSourceDestination(globalThis, &source_blob, &destination_blob, options);
}
fn validateWritableBlob(globalThis: *jsc.JSGlobalObject, blob: *Blob) bun.JSError!void {
const store = blob.store orelse {
return globalThis.throw("Cannot write to a detached Blob", .{});
};
if (store.data == .bytes) {
return globalThis.throwInvalidArguments("Cannot write to a Blob backed by bytes, which are always read-only", .{});
}
}
/// `Bun.write(destination, input, options?)`
pub fn writeFile(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments();
@@ -1479,12 +1488,7 @@ pub fn writeFile(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun
}
// "Blob" must actually be a BunFile, not a webcore blob.
if (path_or_blob == .blob) {
const store = path_or_blob.blob.store orelse {
return globalThis.throw("Cannot write to a detached Blob", .{});
};
if (store.data == .bytes) {
return globalThis.throwInvalidArguments("Cannot write to a Blob backed by bytes, which are always read-only", .{});
}
try validateWritableBlob(globalThis, &path_or_blob.blob);
}
const data = args.nextEat() orelse {
@@ -2224,6 +2228,8 @@ pub fn doWrite(this: *Blob, globalThis: *jsc.JSGlobalObject, callframe: *jsc.Cal
var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args.deinit();
try validateWritableBlob(globalThis, this);
const data = args.nextEat() orelse {
return globalThis.throwInvalidArguments("blob.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{});
};
@@ -2275,13 +2281,14 @@ pub fn doUnlink(this: *Blob, globalThis: *jsc.JSGlobalObject, callframe: *jsc.Ca
const arguments = callframe.arguments_old(1).slice();
var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args.deinit();
const store = this.store orelse {
return jsc.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Blob is detached", .{}));
};
try validateWritableBlob(globalThis, this);
const store = this.store.?;
return switch (store.data) {
.s3 => |*s3| try s3.unlink(store, globalThis, args.nextEat()),
.file => |file| file.unlink(globalThis),
else => jsc.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Blob is read-only", .{})),
else => unreachable, // validateWritableBlob should have caught this
};
}
@@ -2569,9 +2576,9 @@ pub fn getWriter(
return globalThis.throwInvalidArguments("options must be an object or undefined", .{});
}
var store = this.store orelse {
return globalThis.throwInvalidArguments("Blob is detached", .{});
};
try validateWritableBlob(globalThis, this);
var store = this.store.?;
if (this.isS3()) {
const s3 = &this.store.?.data.s3;
const path = s3.path();
@@ -2625,9 +2632,6 @@ pub fn getWriter(
null,
);
}
if (store.data != .file) {
return globalThis.throwInvalidArguments("Blob is read-only", .{});
}
if (Environment.isWindows) {
const pathlike = store.data.file.pathlike;

View File

@@ -0,0 +1,75 @@
import { expect, test } from "bun:test";
import { tempDirWithFiles } from "harness";
import path from "path";
test("blob.write() throws for data-backed blob", () => {
const blob = new Blob(["Hello, world!"]);
expect(() => blob.write("test.txt")).toThrowErrorMatchingInlineSnapshot(
`"Cannot write to a Blob backed by bytes, which are always read-only"`,
);
});
test("Bun.file(path).write() does not throw", async () => {
const file = Bun.file(path.join(tempDirWithFiles("bun-write", { a: "Hello, world!" }), "a"));
expect(() => file.write(new Blob(["Hello, world!!"]))).not.toThrow();
expect(await file.text()).toBe("Hello, world!!");
});
test("blob.unlink() throws for data-backed blob", () => {
const blob = new Blob(["Hello, world!"]);
expect(() => blob.unlink()).toThrowErrorMatchingInlineSnapshot(
`"Cannot write to a Blob backed by bytes, which are always read-only"`,
);
});
test("blob.delete() throws for data-backed blob", () => {
const blob = new Blob(["Hello, world!"]);
expect(() => blob.delete()).toThrowErrorMatchingInlineSnapshot(
`"Cannot write to a Blob backed by bytes, which are always read-only"`,
);
});
test("Bun.file(path).unlink() does not throw", async () => {
const dir = tempDirWithFiles("bun-unlink", { a: "Hello, world!" });
const file = Bun.file(path.join(dir, "a"));
expect(file.unlink()).resolves.toBeUndefined();
expect(await Bun.file(path.join(dir, "a")).exists()).toBe(false);
});
test("Bun.file(path).delete() does not throw", async () => {
const dir = tempDirWithFiles("bun-unlink", { a: "Hello, world!" });
const file = Bun.file(path.join(dir, "a"));
expect(file.delete()).resolves.toBeUndefined();
expect(await Bun.file(path.join(dir, "a")).exists()).toBe(false);
});
test("blob.writer() throws for data-backed blob", () => {
const blob = new Blob(["Hello, world!"]);
expect(() => blob.writer()).toThrowErrorMatchingInlineSnapshot(
`"Cannot write to a Blob backed by bytes, which are always read-only"`,
);
});
test("Bun.file(path).writer() does not throw", async () => {
const dir = tempDirWithFiles("bun-writer", {});
const file = Bun.file(path.join(dir, "test.txt"));
const writer = file.writer();
expect(writer).toBeDefined();
writer.write("New content");
await writer.end();
expect(await file.text()).toBe("New content");
});
test("blob.stat() returns undefined for data-backed blob", async () => {
const blob = new Blob(["Hello, world!"]);
const stat = await blob.stat();
expect(stat).toBeUndefined();
});
test("Bun.file(path).stat() returns stats", async () => {
const dir = tempDirWithFiles("bun-stat", { a: "Hello, world!" });
const file = Bun.file(path.join(dir, "a"));
const stat = await file.stat();
expect(stat).toBeDefined();
expect(stat.size).toBe(13); // "Hello, world!" is 13 bytes
});