From 1085950b88be3fc9ca6f4bbeb67cd0c179c235f3 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 30 Oct 2025 16:08:03 +0300 Subject: [PATCH 01/11] initial `Blob` support --- src/browser/file/Blob.zig | 145 +++++++++++++++++++++++++++++ src/browser/{xhr => file}/File.zig | 8 +- src/browser/file/root.zig | 7 ++ src/browser/js/types.zig | 2 +- src/tests/file/blob.html | 18 ++++ src/tests/{xhr => file}/file.html | 3 +- 6 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 src/browser/file/Blob.zig rename src/browser/{xhr => file}/File.zig (78%) create mode 100644 src/browser/file/root.zig create mode 100644 src/tests/file/blob.html rename src/tests/{xhr => file}/file.html (84%) diff --git a/src/browser/file/Blob.zig b/src/browser/file/Blob.zig new file mode 100644 index 000000000..faa3648ef --- /dev/null +++ b/src/browser/file/Blob.zig @@ -0,0 +1,145 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const Writer = std.Io.Writer; + +const builtin = @import("builtin"); +const is_windows = builtin.os.tag == .windows; + +const Page = @import("../page.zig").Page; +const js = @import("../js/js.zig"); + +/// https://w3c.github.io/FileAPI/#blob-section +/// https://developer.mozilla.org/en-US/docs/Web/API/Blob +const Blob = @This(); + +/// Immutable slice of blob. +/// Note that another blob may hold a pointer/slice to this, +/// so its better to leave the deallocation of it to arena allocator. +slice: []const u8, +/// MIME attached to blob. Can be an empty string. +mime: []const u8, + +const ConstructorOptions = struct { + /// MIME type. + type: []const u8 = "", + /// How to handle newline (LF) characters. + /// `transparent` means do nothing, `native` expects CRLF (\r\n) on Windows. + endings: []const u8 = "transparent", +}; + +/// Creates a new Blob. +pub fn constructor( + maybe_blob_parts: ?[]const []const u8, + maybe_options: ?ConstructorOptions, + page: *Page, +) !Blob { + const options: ConstructorOptions = maybe_options orelse .{}; + + if (maybe_blob_parts) |blob_parts| { + var w: Writer.Allocating = .init(page.arena); + const use_native_endings = std.mem.eql(u8, options.endings, "native"); + try writeBlobParts(&w.writer, blob_parts, use_native_endings); + + const written = w.written(); + + return .{ .slice = written, .mime = options.type }; + } + + // We don't have `blob_parts`, why would you want a Blob anyway then? + return .{ .slice = "", .mime = options.type }; +} + +/// Writes blob parts to given `Writer` by desired encoding. +fn writeBlobParts( + writer: *Writer, + blob_parts: []const []const u8, + use_native_endings: bool, +) !void { + // Transparent. + if (!use_native_endings) { + for (blob_parts) |part| { + try writer.writeAll(part); + } + + return; + } + + // TODO: Windows support. + // TODO: Vector search. + + // Linux & Unix. + // Both Firefox and Chrome implement it as such: + // CRLF => LF + // CR => LF + // So even though CR is not followed by LF, it gets replaced. + // + // I believe this is because such scenario is possible: + // ``` + // let parts = [ "the quick\r", "\nbrown fox" ]; + // ``` + // In the example, one should have to check the part before in order to + // understand that CRLF is being presented in the final buffer. + // So they took a simpler approach, here's what given blob parts produce: + // ``` + // "the quick\n\nbrown fox" + // ``` + scan_parts: for (blob_parts) |part| { + var end: usize = 0; + var start = end; + while (end < part.len) { + if (part[end] == '\r') { + try writer.writeAll(part[start..end]); + try writer.writeByte('\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') { + start = end + 2; + } else { + start = end + 1; + } + } + + end += 1; + } + + // Write the remaining. We get this in such situations: + // `the quick brown\rfox` + // `the quick brown\r\nfox` + try writer.writeAll(part[start..end]); + } +} + +pub fn get_size(self: *const Blob) usize { + return self.slice.len; +} + +pub fn get_str(self: *const Blob) []const u8 { + return self.slice; +} + +const testing = @import("../../testing.zig"); +test "Browser: File.Blob" { + try testing.htmlRunner("file/blob.html"); +} diff --git a/src/browser/xhr/File.zig b/src/browser/file/File.zig similarity index 78% rename from src/browser/xhr/File.zig rename to src/browser/file/File.zig index ebec7be66..bfec0d4f7 100644 --- a/src/browser/xhr/File.zig +++ b/src/browser/file/File.zig @@ -21,14 +21,12 @@ const std = @import("std"); // https://w3c.github.io/FileAPI/#file-section const File = @This(); -// Very incomplete. The prototype for this is Blob, which we don't have. -// This minimum "implementation" is added because some JavaScript code just -// checks: if (x instanceof File) throw Error(...) +/// TODO: Implement File API. pub fn constructor() File { return .{}; } const testing = @import("../../testing.zig"); -test "Browser: File" { - try testing.htmlRunner("xhr/file.html"); +test "Browser: File.File" { + try testing.htmlRunner("file/file.html"); } diff --git a/src/browser/file/root.zig b/src/browser/file/root.zig new file mode 100644 index 000000000..570cc7d59 --- /dev/null +++ b/src/browser/file/root.zig @@ -0,0 +1,7 @@ +//! File API. +//! https://developer.mozilla.org/en-US/docs/Web/API/File_API + +pub const Interfaces = .{ + @import("./Blob.zig"), + @import("./File.zig"), +}; diff --git a/src/browser/js/types.zig b/src/browser/js/types.zig index 5f843de5e..1849b72e3 100644 --- a/src/browser/js/types.zig +++ b/src/browser/js/types.zig @@ -17,8 +17,8 @@ const Interfaces = generate.Tuple(.{ @import("../url/url.zig").Interfaces, @import("../xhr/xhr.zig").Interfaces, @import("../navigation/root.zig").Interfaces, + @import("../file/root.zig").Interfaces, @import("../xhr/form_data.zig").Interfaces, - @import("../xhr/File.zig"), @import("../xmlserializer/xmlserializer.zig").Interfaces, @import("../fetch/fetch.zig").Interfaces, @import("../streams/streams.zig").Interfaces, diff --git a/src/tests/file/blob.html b/src/tests/file/blob.html new file mode 100644 index 000000000..66298ea6a --- /dev/null +++ b/src/tests/file/blob.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/tests/xhr/file.html b/src/tests/file/file.html similarity index 84% rename from src/tests/xhr/file.html rename to src/tests/file/file.html index 622846028..05f23ad78 100644 --- a/src/tests/xhr/file.html +++ b/src/tests/file/file.html @@ -1,6 +1,7 @@ + From c491648941b4d0fd8fdd3d7f31f327f44992971d Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 30 Oct 2025 22:34:58 +0300 Subject: [PATCH 02/11] implement various `Blob` methods Support for `stream`, `text` and `bytes`. --- src/browser/file/Blob.zig | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/browser/file/Blob.zig b/src/browser/file/Blob.zig index faa3648ef..bd1e07d23 100644 --- a/src/browser/file/Blob.zig +++ b/src/browser/file/Blob.zig @@ -25,6 +25,8 @@ const is_windows = builtin.os.tag == .windows; const Page = @import("../page.zig").Page; const js = @import("../js/js.zig"); +const ReadableStream = @import("../streams/ReadableStream.zig"); + /// https://w3c.github.io/FileAPI/#blob-section /// https://developer.mozilla.org/en-US/docs/Web/API/Blob const Blob = @This(); @@ -131,6 +133,38 @@ fn writeBlobParts( } } +// TODO: Blob.arrayBuffer. +// https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer + +/// Returns a ReadableStream which upon reading returns the data +/// contained within the Blob. +pub fn _stream(self: *const Blob, page: *Page) !*ReadableStream { + const stream = try ReadableStream.constructor(null, null, page); + try stream.queue.append(page.arena, .{ + .uint8array = .{ .values = self.slice }, + }); + return stream; +} + +/// Returns a Promise that resolves with a string containing +/// the contents of the blob, interpreted as UTF-8. +pub fn _text(self: *const Blob, page: *Page) !js.Promise { + const resolver = page.js.createPromiseResolver(.none); + try resolver.resolve(self.slice); + return resolver.promise(); +} + +/// Extension to Blob; works on Firefox and Safari. +/// https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes +/// Returns a Promise that resolves with a Uint8Array containing +/// the contents of the blob as an array of bytes. +pub fn _bytes(self: *const Blob, page: *Page) !js.Promise { + const resolver = page.js.createPromiseResolver(.none); + try resolver.resolve(js.TypedArray(u8){ .values = self.slice }); + return resolver.promise(); +} + +/// Returns the size of the Blob in bytes. pub fn get_size(self: *const Blob) usize { return self.slice.len; } From dd43be4818a93fc0857d22a8c0ba6071045a4e9c Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 30 Oct 2025 22:35:37 +0300 Subject: [PATCH 03/11] remove method added for testing This was only required while testing things, not an actual API. --- src/browser/file/Blob.zig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/browser/file/Blob.zig b/src/browser/file/Blob.zig index bd1e07d23..7ac523918 100644 --- a/src/browser/file/Blob.zig +++ b/src/browser/file/Blob.zig @@ -169,10 +169,6 @@ pub fn get_size(self: *const Blob) usize { return self.slice.len; } -pub fn get_str(self: *const Blob) []const u8 { - return self.slice; -} - const testing = @import("../../testing.zig"); test "Browser: File.Blob" { try testing.htmlRunner("file/blob.html"); From 3307a664c4821391ec87f92f5df1278e85ce0469 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 30 Oct 2025 22:35:58 +0300 Subject: [PATCH 04/11] prefer `writeVec` instead of `writeAll` + `writeByte` --- src/browser/file/Blob.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/browser/file/Blob.zig b/src/browser/file/Blob.zig index 7ac523918..4ffe57eef 100644 --- a/src/browser/file/Blob.zig +++ b/src/browser/file/Blob.zig @@ -107,8 +107,7 @@ fn writeBlobParts( var start = end; while (end < part.len) { if (part[end] == '\r') { - try writer.writeAll(part[start..end]); - try writer.writeByte('\n'); + _ = try writer.writeVec(&.{ part[start..end], "\n" }); // Part ends with CR. We can continue to next part. if (end + 1 == part.len) { From b68675bb94479a24166484a261109079fa1efae0 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 30 Oct 2025 22:36:07 +0300 Subject: [PATCH 05/11] update `Blob` test --- src/tests/file/blob.html | 66 ++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/src/tests/file/blob.html b/src/tests/file/blob.html index 66298ea6a..8087b5698 100644 --- a/src/tests/file/blob.html +++ b/src/tests/file/blob.html @@ -2,17 +2,57 @@ + + + + + From 5785c147da587c2882fde4d513e5f6be99a8a865 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 31 Oct 2025 17:17:02 +0300 Subject: [PATCH 06/11] support `Blob` type --- src/browser/file/Blob.zig | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/browser/file/Blob.zig b/src/browser/file/Blob.zig index 4ffe57eef..bda204946 100644 --- a/src/browser/file/Blob.zig +++ b/src/browser/file/Blob.zig @@ -41,7 +41,7 @@ mime: []const u8, const ConstructorOptions = struct { /// MIME type. type: []const u8 = "", - /// How to handle newline (LF) characters. + /// How to handle line endings (CR and LF). /// `transparent` means do nothing, `native` expects CRLF (\r\n) on Windows. endings: []const u8 = "transparent", }; @@ -53,22 +53,29 @@ pub fn constructor( page: *Page, ) !Blob { const options: ConstructorOptions = maybe_options orelse .{}; + // Setup MIME; This can be any string according to my observations. + const mime: []const u8 = blk: { + const t = options.type; + if (t.len == 0) { + break :blk ""; + } + + break :blk try page.arena.dupe(u8, t); + }; if (maybe_blob_parts) |blob_parts| { var w: Writer.Allocating = .init(page.arena); const use_native_endings = std.mem.eql(u8, options.endings, "native"); try writeBlobParts(&w.writer, blob_parts, use_native_endings); - const written = w.written(); - - return .{ .slice = written, .mime = options.type }; + return .{ .slice = w.written(), .mime = mime }; } // We don't have `blob_parts`, why would you want a Blob anyway then? - return .{ .slice = "", .mime = options.type }; + return .{ .slice = "", .mime = mime }; } -/// Writes blob parts to given `Writer` by desired encoding. +/// Writes blob parts to given `Writer` with desired endings. fn writeBlobParts( writer: *Writer, blob_parts: []const []const u8, @@ -168,6 +175,11 @@ pub fn get_size(self: *const Blob) usize { return self.slice.len; } +/// Returns the type of Blob; likely a MIME type, yet anything can be given. +pub fn get_type(self: *const Blob) []const u8 { + return self.mime; +} + const testing = @import("../../testing.zig"); test "Browser: File.Blob" { try testing.htmlRunner("file/blob.html"); From 4be7fa178cd29fcdc95f89c06a17ef043a861a89 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 31 Oct 2025 17:18:03 +0300 Subject: [PATCH 07/11] support `Blob.arrayBuffer` Also adds support for `ArrayBuffer` to js.zig. --- src/browser/file/Blob.zig | 9 +++++++-- src/browser/js/js.zig | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/browser/file/Blob.zig b/src/browser/file/Blob.zig index bda204946..0d5eb9370 100644 --- a/src/browser/file/Blob.zig +++ b/src/browser/file/Blob.zig @@ -139,8 +139,13 @@ fn writeBlobParts( } } -// TODO: Blob.arrayBuffer. -// https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer +/// Returns a Promise that resolves with the contents of the blob +/// as binary data contained in an ArrayBuffer. +pub fn _arrayBuffer(self: *const Blob, page: *Page) !js.Promise { + const resolver = page.js.createPromiseResolver(.none); + try resolver.resolve(js.ArrayBuffer{ .values = self.slice }); + return resolver.promise(); +} /// Returns a ReadableStream which upon reading returns the data /// contained within the Blob. diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index aeff65409..5471f3000 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -58,6 +58,10 @@ pub fn TypedArray(comptime T: type) type { }; } +pub const ArrayBuffer = struct { + values: []const u8, +}; + pub const PromiseResolver = struct { context: *Context, resolver: v8.PromiseResolver, @@ -324,6 +328,19 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo }, .@"struct" => { const T = @TypeOf(value); + + if (T == ArrayBuffer) { + const values = value.values; + const len = values.len; + var array_buffer: v8.ArrayBuffer = undefined; + const backing_store = v8.BackingStore.init(isolate, len); + const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData())); + @memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]); + array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr()); + + return .{ .handle = array_buffer.handle }; + } + if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) { const values = value.values; const value_type = @typeInfo(@TypeOf(values)).pointer.child; From 93542c97564df3290719485d27b0959fce1c2ee8 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 31 Oct 2025 17:18:22 +0300 Subject: [PATCH 08/11] support `Blob.slice` --- src/browser/file/Blob.zig | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/browser/file/Blob.zig b/src/browser/file/Blob.zig index 0d5eb9370..191bc4a88 100644 --- a/src/browser/file/Blob.zig +++ b/src/browser/file/Blob.zig @@ -175,6 +175,55 @@ pub fn _bytes(self: *const Blob, page: *Page) !js.Promise { return resolver.promise(); } +/// Returns a new Blob object which contains data +/// from a subset of the blob on which it's called. +pub fn _slice( + self: *const Blob, + maybe_start: ?i32, + maybe_end: ?i32, + maybe_content_type: ?[]const u8, + page: *Page, +) !Blob { + const mime: []const u8 = blk: { + if (maybe_content_type) |content_type| { + if (content_type.len == 0) { + break :blk ""; + } + + break :blk try page.arena.dupe(u8, content_type); + } + + break :blk ""; + }; + + const slice = self.slice; + if (maybe_start) |_start| { + const start = blk: { + if (_start < 0) { + break :blk slice.len -| @abs(_start); + } + + break :blk @min(slice.len, @as(u31, @intCast(_start))); + }; + + const end: usize = blk: { + if (maybe_end) |_end| { + if (_end < 0) { + break :blk @max(start, slice.len -| @abs(_end)); + } + + break :blk @min(slice.len, @max(start, @as(u31, @intCast(_end)))); + } + + break :blk slice.len; + }; + + return .{ .slice = slice[start..end], .mime = mime }; + } + + return .{ .slice = slice, .mime = mime }; +} + /// Returns the size of the Blob in bytes. pub fn get_size(self: *const Blob) usize { return self.slice.len; From 9b990da7fa3b4836874af4a7fe718c9572eda7f1 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 31 Oct 2025 17:18:33 +0300 Subject: [PATCH 09/11] update `Blob` test --- src/tests/file/blob.html | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/tests/file/blob.html b/src/tests/file/blob.html index 8087b5698..754861a31 100644 --- a/src/tests/file/blob.html +++ b/src/tests/file/blob.html @@ -1,14 +1,15 @@ - @@ -41,6 +44,34 @@ } + +