mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
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:
@@ -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;
|
||||
|
||||
75
test/js/web/fetch/blob-write.test.ts
Normal file
75
test/js/web/fetch/blob-write.test.ts
Normal 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
|
||||
});
|
||||
Reference in New Issue
Block a user