mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-12 01:56:19 -04:00
Merge pull request #2181 from lightpanda-io/blob_constructor
Support more types in new Blob(...)
This commit is contained in:
@@ -163,6 +163,53 @@ pub fn isFloat64Array(self: Value) bool {
|
||||
return v8.v8__Value__IsFloat64Array(self.handle);
|
||||
}
|
||||
|
||||
// A few places in the code take various types, but want a string. This is a
|
||||
// type-aware version of toString(). If you do:
|
||||
// (new ArrayBuffer(100)).toString()
|
||||
// You'll get "[object ArrayBuffer]". But this `toStringSmart()` knows about
|
||||
// buffers, and Blobs, etc and will try to return the real underlying string
|
||||
// value. It _does_ ultimately fallback to toString() - callers should check
|
||||
// for types they _don't_ want before calling this. For example, `Response`
|
||||
// checks for null or undefined before calling this to apply specific handling
|
||||
// to those cases.
|
||||
pub fn toStringSmart(self: Value) ![]const u8 {
|
||||
if (self.isString()) |js_str| {
|
||||
return try js_str.toSlice();
|
||||
}
|
||||
|
||||
const Blob = @import("../webapi/Blob.zig");
|
||||
if (self.local.jsValueToZig(*Blob, self)) |blob_obj| {
|
||||
return blob_obj._slice;
|
||||
} else |_| {}
|
||||
|
||||
var byte_offset: usize = 0;
|
||||
var byte_len: usize = undefined;
|
||||
var array_buffer: ?*const v8.ArrayBuffer = null;
|
||||
|
||||
if (self.isTypedArray() or self.isArrayBufferView()) {
|
||||
const buffer_handle: *const v8.ArrayBufferView = @ptrCast(self.handle);
|
||||
byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle);
|
||||
byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle);
|
||||
array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle);
|
||||
} else if (self.isArrayBuffer()) {
|
||||
array_buffer = @ptrCast(self.handle);
|
||||
byte_len = v8.v8__ArrayBuffer__ByteLength(array_buffer);
|
||||
} else {
|
||||
return self.toStringSlice();
|
||||
}
|
||||
|
||||
const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer orelse return "");
|
||||
if (byte_len == 0) {
|
||||
return &[_]u8{};
|
||||
}
|
||||
|
||||
const backing_store_handle = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr) orelse return "";
|
||||
const data = v8.v8__BackingStore__Data(backing_store_handle) orelse return "";
|
||||
const base = @as([*]const u8, @ptrCast(data)) + byte_offset;
|
||||
|
||||
return base[0..byte_len];
|
||||
}
|
||||
|
||||
pub fn isPromise(self: Value) bool {
|
||||
return v8.v8__Value__IsPromise(self.handle);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,100 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=parts>
|
||||
// Blob as a blob part - contents are copied in, not stringified.
|
||||
{
|
||||
const inner = new Blob(["hello "], { type: "text/plain" });
|
||||
const blob = new Blob([inner, "world"]);
|
||||
testing.expectEqual(11, blob.size);
|
||||
testing.async(async () => {
|
||||
testing.expectEqual("hello world", await blob.text());
|
||||
});
|
||||
}
|
||||
|
||||
// Uint8Array as a blob part.
|
||||
{
|
||||
const bytes = new Uint8Array([104, 101, 108, 108, 111]); // "hello"
|
||||
const blob = new Blob([bytes, " ", "world"]);
|
||||
testing.expectEqual(11, blob.size);
|
||||
testing.async(async () => {
|
||||
testing.expectEqual("hello world", await blob.text());
|
||||
});
|
||||
}
|
||||
|
||||
// Non-byte TypedArrays contribute their raw backing bytes.
|
||||
{
|
||||
// Uint16Array of one element (0x0041 = 'A' little-endian) produces 2 bytes.
|
||||
const u16 = new Uint16Array([0x0041]);
|
||||
const blob = new Blob([u16]);
|
||||
testing.expectEqual(2, blob.size);
|
||||
}
|
||||
|
||||
{
|
||||
// Float32Array: 4 bytes per element.
|
||||
const f32 = new Float32Array([1.0, 2.0, 3.0]);
|
||||
const blob = new Blob([f32]);
|
||||
testing.expectEqual(12, blob.size);
|
||||
}
|
||||
|
||||
// ArrayBuffer as a blob part.
|
||||
{
|
||||
const buf = new Uint8Array([1, 2, 3, 4]).buffer;
|
||||
const blob = new Blob([buf]);
|
||||
testing.expectEqual(4, blob.size);
|
||||
testing.async(async () => {
|
||||
const result = await blob.bytes();
|
||||
testing.expectEqual(new Uint8Array([1, 2, 3, 4]), result);
|
||||
});
|
||||
}
|
||||
|
||||
// DataView (ArrayBufferView) as a blob part.
|
||||
{
|
||||
const buf = new Uint8Array([10, 20, 30, 40, 50]).buffer;
|
||||
const view = new DataView(buf, 1, 3); // bytes [20, 30, 40]
|
||||
const blob = new Blob([view]);
|
||||
testing.expectEqual(3, blob.size);
|
||||
testing.async(async () => {
|
||||
const result = await blob.bytes();
|
||||
testing.expectEqual(new Uint8Array([20, 30, 40]), result);
|
||||
});
|
||||
}
|
||||
|
||||
// Mixed types in a single parts array.
|
||||
{
|
||||
const inner = new Blob(["bb"]);
|
||||
const bytes = new Uint8Array([99, 99]); // "cc"
|
||||
const buf = new Uint8Array([100, 100]).buffer; // "dd"
|
||||
const blob = new Blob(["aa", inner, bytes, buf, "ee"]);
|
||||
testing.expectEqual(10, blob.size);
|
||||
testing.async(async () => {
|
||||
testing.expectEqual("aabbccddee", await blob.text());
|
||||
});
|
||||
}
|
||||
|
||||
// Number coerces to string.
|
||||
{
|
||||
const blob = new Blob([42]);
|
||||
testing.expectEqual(2, blob.size);
|
||||
testing.async(async () => {
|
||||
testing.expectEqual("42", await blob.text());
|
||||
});
|
||||
}
|
||||
|
||||
// Empty parts array.
|
||||
{
|
||||
const blob = new Blob([]);
|
||||
testing.expectEqual(0, blob.size);
|
||||
testing.expectEqual("", blob.type);
|
||||
}
|
||||
|
||||
// No arguments.
|
||||
{
|
||||
const blob = new Blob();
|
||||
testing.expectEqual(0, blob.size);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=stream>
|
||||
{
|
||||
const parts = ["may", "thy", "knife", "chip", "and", "shatter"];
|
||||
|
||||
@@ -59,69 +59,23 @@ const InitOptions = struct {
|
||||
endings: []const u8 = "transparent",
|
||||
};
|
||||
|
||||
/// Creates a new Blob (JS constructor).
|
||||
pub fn init(
|
||||
maybe_blob_parts: ?[]const []const u8,
|
||||
maybe_options: ?InitOptions,
|
||||
page: *Page,
|
||||
) !*Blob {
|
||||
return initWithMimeValidation(maybe_blob_parts, maybe_options, false, page);
|
||||
}
|
||||
|
||||
/// Creates a new Blob with optional MIME validation.
|
||||
/// When validate_mime is true, uses full MIME parsing (for Response/Request).
|
||||
/// When false, uses simple ASCII validation per FileAPI spec (for Blob constructor).
|
||||
pub fn initWithMimeValidation(
|
||||
maybe_blob_parts: ?[]const []const u8,
|
||||
maybe_options: ?InitOptions,
|
||||
validate_mime: bool,
|
||||
page: *Page,
|
||||
) !*Blob {
|
||||
const data_len = blk: {
|
||||
const parts = maybe_blob_parts orelse break :blk 0;
|
||||
var size: usize = 0;
|
||||
for (parts) |p| {
|
||||
size += p.len;
|
||||
}
|
||||
break :blk size;
|
||||
};
|
||||
const arena = try page.getArena(256 + data_len, "Blob");
|
||||
/// Creates a new Blob from JS values with optional MIME validation.
|
||||
/// This is the JS Constructor
|
||||
pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, page: *Page) !*Blob {
|
||||
const arena = try page.getArena(.large, "Blob");
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const options: InitOptions = maybe_options orelse .{};
|
||||
|
||||
const mime: []const u8 = blk: {
|
||||
const t = options.type;
|
||||
if (t.len == 0) {
|
||||
break :blk "";
|
||||
}
|
||||
|
||||
const buf = try arena.dupe(u8, t);
|
||||
|
||||
if (validate_mime) {
|
||||
// Full MIME parsing per MIME sniff spec (for Content-Type headers)
|
||||
_ = Mime.parse(buf) catch break :blk "";
|
||||
} else {
|
||||
// Simple validation per FileAPI spec (for Blob constructor):
|
||||
// - If any char is outside U+0020-U+007E, return empty string
|
||||
// - Otherwise lowercase
|
||||
for (t) |c| {
|
||||
if (c < 0x20 or c > 0x7E) {
|
||||
break :blk "";
|
||||
}
|
||||
}
|
||||
_ = std.ascii.lowerString(buf, buf);
|
||||
}
|
||||
|
||||
break :blk buf;
|
||||
};
|
||||
const opts: InitOptions = opts_ orelse .{};
|
||||
const mime = try validateMimeType(arena, opts.type, false);
|
||||
|
||||
const data = blk: {
|
||||
if (maybe_blob_parts) |blob_parts| {
|
||||
if (parts_) |blob_parts| {
|
||||
const use_native_endings = std.mem.eql(u8, opts.endings, "native");
|
||||
var w: Writer.Allocating = .init(arena);
|
||||
const use_native_endings = std.mem.eql(u8, options.endings, "native");
|
||||
try writeBlobParts(&w.writer, blob_parts, use_native_endings);
|
||||
|
||||
for (blob_parts) |js_val| {
|
||||
const part = try js_val.toStringSmart();
|
||||
try writePartWithEndings(part, use_native_endings, &w.writer);
|
||||
}
|
||||
break :blk w.written();
|
||||
}
|
||||
|
||||
@@ -139,6 +93,50 @@ pub fn initWithMimeValidation(
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Creates a new Blob from raw byte slices (for internal Zig use).
|
||||
pub fn initFromBytes(data: []const u8, content_type: []const u8, validate_mime: bool, page: *Page) !*Blob {
|
||||
const arena = try page.getArena(.large, "Blob");
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const mime = try validateMimeType(arena, content_type, validate_mime);
|
||||
|
||||
const self = try arena.create(Blob);
|
||||
self.* = .{
|
||||
._rc = .{},
|
||||
._arena = arena,
|
||||
._type = .generic,
|
||||
._slice = try arena.dupe(u8, data),
|
||||
._mime = mime,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Validates and normalizes MIME type according to spec.
|
||||
fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bool) ![]const u8 {
|
||||
if (mime_type.len == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const buf = try arena.dupe(u8, mime_type);
|
||||
|
||||
if (full_validation) {
|
||||
// Full MIME parsing per MIME sniff spec (for Content-Type headers)
|
||||
_ = Mime.parse(buf) catch return "";
|
||||
} else {
|
||||
// Simple validation per FileAPI spec (for Blob constructor):
|
||||
// - If any char is outside U+0020-U+007E, return empty string
|
||||
// - Otherwise lowercase
|
||||
for (mime_type) |c| {
|
||||
if (c < 0x20 or c > 0x7E) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
_ = std.ascii.lowerString(buf, buf);
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Blob, session: *Session) void {
|
||||
session.releaseArena(self._arena);
|
||||
}
|
||||
@@ -171,18 +169,11 @@ const vector_sizes = blk: {
|
||||
break :blk items;
|
||||
};
|
||||
|
||||
/// Writes blob parts to given `Writer` with desired endings.
|
||||
fn writeBlobParts(
|
||||
writer: *Writer,
|
||||
blob_parts: []const []const u8,
|
||||
use_native_endings: bool,
|
||||
) !void {
|
||||
// Transparent.
|
||||
/// Writes a single part with optional line ending normalization.
|
||||
fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Writer) !void {
|
||||
// Transparent - no conversion needed.
|
||||
if (!use_native_endings) {
|
||||
for (blob_parts) |part| {
|
||||
try writer.writeAll(part);
|
||||
}
|
||||
|
||||
try writer.writeAll(part);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,68 +195,66 @@ fn writeBlobParts(
|
||||
// ```
|
||||
// "the quick\n\nbrown fox"
|
||||
// ```
|
||||
scan_parts: for (blob_parts) |part| {
|
||||
var end: usize = 0;
|
||||
var end: usize = 0;
|
||||
|
||||
inline for (vector_sizes) |vector_len| {
|
||||
const Vec = @Vector(vector_len, u8);
|
||||
inline for (vector_sizes) |vector_len| {
|
||||
const Vec = @Vector(vector_len, u8);
|
||||
|
||||
while (end + vector_len <= part.len) : (end += vector_len) {
|
||||
const cr: Vec = @splat('\r');
|
||||
// Load chunk as vectors.
|
||||
const data = part[end..][0..vector_len];
|
||||
const chunk: Vec = data.*;
|
||||
// Look for CR.
|
||||
const match = chunk == cr;
|
||||
while (end + vector_len <= part.len) : (end += vector_len) {
|
||||
const cr: Vec = @splat('\r');
|
||||
// Load chunk as vectors.
|
||||
const data = part[end..][0..vector_len];
|
||||
const chunk: Vec = data.*;
|
||||
// Look for CR.
|
||||
const match = chunk == cr;
|
||||
|
||||
// Create a bitset out of match vector.
|
||||
const bitset = std.bit_set.IntegerBitSet(vector_len){
|
||||
.mask = @bitCast(@intFromBool(match)),
|
||||
};
|
||||
// Create a bitset out of match vector.
|
||||
const bitset = std.bit_set.IntegerBitSet(vector_len){
|
||||
.mask = @bitCast(@intFromBool(match)),
|
||||
};
|
||||
|
||||
var iter = bitset.iterator(.{});
|
||||
var relative_start: usize = 0;
|
||||
while (iter.next()) |index| {
|
||||
_ = try writer.writeVec(&.{ data[relative_start..index], "\n" });
|
||||
var iter = bitset.iterator(.{});
|
||||
var relative_start: usize = 0;
|
||||
while (iter.next()) |index| {
|
||||
_ = try writer.writeVec(&.{ data[relative_start..index], "\n" });
|
||||
|
||||
if (index + 1 != data.len and data[index + 1] == '\n') {
|
||||
relative_start = index + 2;
|
||||
} else {
|
||||
relative_start = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
_ = try writer.writeVec(&.{data[relative_start..]});
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar scan fallback.
|
||||
var relative_start: usize = end;
|
||||
while (end < part.len) {
|
||||
if (part[end] == '\r') {
|
||||
_ = try writer.writeVec(&.{ part[relative_start..end], "\n" });
|
||||
|
||||
// Part ends with CR. We can continue to next part.
|
||||
if (end + 1 == part.len) {
|
||||
continue :scan_parts;
|
||||
}
|
||||
|
||||
// If next char is LF, skip it too.
|
||||
if (part[end + 1] == '\n') {
|
||||
relative_start = end + 2;
|
||||
if (index + 1 != data.len and data[index + 1] == '\n') {
|
||||
relative_start = index + 2;
|
||||
} else {
|
||||
relative_start = end + 1;
|
||||
relative_start = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
end += 1;
|
||||
_ = try writer.writeVec(&.{data[relative_start..]});
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar scan fallback.
|
||||
var relative_start: usize = end;
|
||||
while (end < part.len) {
|
||||
if (part[end] == '\r') {
|
||||
_ = try writer.writeVec(&.{ part[relative_start..end], "\n" });
|
||||
|
||||
// Part ends with CR. We need to remember this for next part.
|
||||
if (end + 1 == part.len) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If next char is LF, skip it too.
|
||||
if (part[end + 1] == '\n') {
|
||||
relative_start = end + 2;
|
||||
} else {
|
||||
relative_start = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the remaining. We get this in such situations:
|
||||
// `the quick brown\rfox`
|
||||
// `the quick brown\r\nfox`
|
||||
try writer.writeAll(part[relative_start..end]);
|
||||
end += 1;
|
||||
}
|
||||
|
||||
// Write the remaining. We get this in such situations:
|
||||
// `the quick brown\rfox`
|
||||
// `the quick brown\r\nfox`
|
||||
try writer.writeAll(part[relative_start..end]);
|
||||
}
|
||||
|
||||
/// Returns a Promise that resolves with the contents of the blob
|
||||
@@ -323,7 +312,7 @@ pub fn slice(
|
||||
break :blk @min(data.len, @max(start, @as(u31, @intCast(requested_end))));
|
||||
};
|
||||
|
||||
return Blob.init(&.{data[start..end]}, .{ .type = content_type_ orelse "" }, page);
|
||||
return Blob.initFromBytes(data[start..end], content_type_ orelse "", false, page);
|
||||
}
|
||||
|
||||
/// Returns the size of the Blob in bytes.
|
||||
|
||||
@@ -174,12 +174,7 @@ pub fn blob(self: *Request, page: *Page) !js.Promise {
|
||||
const headers = try self.getHeaders(page);
|
||||
const content_type = try headers.get("content-type", page) orelse "";
|
||||
|
||||
const b = try Blob.initWithMimeValidation(
|
||||
&.{body},
|
||||
.{ .type = content_type },
|
||||
true,
|
||||
page,
|
||||
);
|
||||
const b = try Blob.initFromBytes(body, content_type, true, page);
|
||||
|
||||
return page.js.local.?.resolvePromise(b);
|
||||
}
|
||||
|
||||
@@ -83,29 +83,10 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, page: *Page) !*Response {
|
||||
.bytes => |body_bytes| break :blk .{ .bytes = try arena.dupe(u8, body_bytes) },
|
||||
.stream => |stream| break :blk .{ .stream = stream },
|
||||
.js_val => |js_val| {
|
||||
const local = page.js.local.?;
|
||||
|
||||
if (local.jsValueToZig(*ReadableStream, js_val)) |stream| {
|
||||
break :blk .{ .stream = stream };
|
||||
} else |_| {}
|
||||
|
||||
if (js_val.isString()) |js_str| {
|
||||
break :blk .{ .bytes = try js_str.toSliceWithAlloc(arena) };
|
||||
}
|
||||
|
||||
if (js_val.isArrayBuffer() or js_val.isTypedArray() or js_val.isArrayBufferView()) {
|
||||
if (local.jsValueToZig([]u8, js_val)) |data| {
|
||||
break :blk .{ .bytes = try arena.dupe(u8, data) };
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
if (local.jsValueToZig(*Blob, js_val)) |blob_obj| {
|
||||
break :blk .{ .bytes = try arena.dupe(u8, blob_obj._slice) };
|
||||
} else |_| {}
|
||||
|
||||
if (js_val.isNullOrUndefined() == false) {
|
||||
break :blk .{ .bytes = try js_val.toStringSliceWithAlloc(arena) };
|
||||
if (js_val.isNullOrUndefined()) {
|
||||
break :blk .empty;
|
||||
}
|
||||
break :blk .{ .bytes = try arena.dupe(u8, try js_val.toStringSmart()) };
|
||||
},
|
||||
}
|
||||
break :blk .empty;
|
||||
@@ -335,14 +316,7 @@ pub fn blob(self: *const Response, page: *Page) !js.Promise {
|
||||
.stream => return local.rejectPromise(.{ .type_error = "Cannot read blob from stream body" }),
|
||||
};
|
||||
const content_type = try self._headers.get("content-type", page) orelse "";
|
||||
|
||||
const b = try Blob.initWithMimeValidation(
|
||||
&.{body},
|
||||
.{ .type = content_type },
|
||||
true,
|
||||
page,
|
||||
);
|
||||
|
||||
const b = try Blob.initFromBytes(body, content_type, true, page);
|
||||
return local.resolvePromise(b);
|
||||
}
|
||||
|
||||
|
||||
@@ -466,7 +466,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF
|
||||
switch (self._binary_type) {
|
||||
.arraybuffer => .{ .arraybuffer = .{ .values = data } },
|
||||
.blob => blk: {
|
||||
const blob = try Blob.init(&.{data}, .{}, page);
|
||||
const blob = try Blob.initFromBytes(data, "", false, page);
|
||||
blob.acquireRef();
|
||||
break :blk .{ .blob = blob };
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user