Skip to content

Commit 1085950

Browse files
committed
initial Blob support
1 parent f7abf09 commit 1085950

File tree

6 files changed

+176
-7
lines changed

6 files changed

+176
-7
lines changed

src/browser/file/Blob.zig

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <francis@lightpanda.io>
4+
// Pierre Tachoire <pierre@lightpanda.io>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const Writer = std.Io.Writer;
21+
22+
const builtin = @import("builtin");
23+
const is_windows = builtin.os.tag == .windows;
24+
25+
const Page = @import("../page.zig").Page;
26+
const js = @import("../js/js.zig");
27+
28+
/// https://w3c.github.io/FileAPI/#blob-section
29+
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob
30+
const Blob = @This();
31+
32+
/// Immutable slice of blob.
33+
/// Note that another blob may hold a pointer/slice to this,
34+
/// so its better to leave the deallocation of it to arena allocator.
35+
slice: []const u8,
36+
/// MIME attached to blob. Can be an empty string.
37+
mime: []const u8,
38+
39+
const ConstructorOptions = struct {
40+
/// MIME type.
41+
type: []const u8 = "",
42+
/// How to handle newline (LF) characters.
43+
/// `transparent` means do nothing, `native` expects CRLF (\r\n) on Windows.
44+
endings: []const u8 = "transparent",
45+
};
46+
47+
/// Creates a new Blob.
48+
pub fn constructor(
49+
maybe_blob_parts: ?[]const []const u8,
50+
maybe_options: ?ConstructorOptions,
51+
page: *Page,
52+
) !Blob {
53+
const options: ConstructorOptions = maybe_options orelse .{};
54+
55+
if (maybe_blob_parts) |blob_parts| {
56+
var w: Writer.Allocating = .init(page.arena);
57+
const use_native_endings = std.mem.eql(u8, options.endings, "native");
58+
try writeBlobParts(&w.writer, blob_parts, use_native_endings);
59+
60+
const written = w.written();
61+
62+
return .{ .slice = written, .mime = options.type };
63+
}
64+
65+
// We don't have `blob_parts`, why would you want a Blob anyway then?
66+
return .{ .slice = "", .mime = options.type };
67+
}
68+
69+
/// Writes blob parts to given `Writer` by desired encoding.
70+
fn writeBlobParts(
71+
writer: *Writer,
72+
blob_parts: []const []const u8,
73+
use_native_endings: bool,
74+
) !void {
75+
// Transparent.
76+
if (!use_native_endings) {
77+
for (blob_parts) |part| {
78+
try writer.writeAll(part);
79+
}
80+
81+
return;
82+
}
83+
84+
// TODO: Windows support.
85+
// TODO: Vector search.
86+
87+
// Linux & Unix.
88+
// Both Firefox and Chrome implement it as such:
89+
// CRLF => LF
90+
// CR => LF
91+
// So even though CR is not followed by LF, it gets replaced.
92+
//
93+
// I believe this is because such scenario is possible:
94+
// ```
95+
// let parts = [ "the quick\r", "\nbrown fox" ];
96+
// ```
97+
// In the example, one should have to check the part before in order to
98+
// understand that CRLF is being presented in the final buffer.
99+
// So they took a simpler approach, here's what given blob parts produce:
100+
// ```
101+
// "the quick\n\nbrown fox"
102+
// ```
103+
scan_parts: for (blob_parts) |part| {
104+
var end: usize = 0;
105+
var start = end;
106+
while (end < part.len) {
107+
if (part[end] == '\r') {
108+
try writer.writeAll(part[start..end]);
109+
try writer.writeByte('\n');
110+
111+
// Part ends with CR. We can continue to next part.
112+
if (end + 1 == part.len) {
113+
continue :scan_parts;
114+
}
115+
116+
// If next char is LF, skip it too.
117+
if (part[end + 1] == '\n') {
118+
start = end + 2;
119+
} else {
120+
start = end + 1;
121+
}
122+
}
123+
124+
end += 1;
125+
}
126+
127+
// Write the remaining. We get this in such situations:
128+
// `the quick brown\rfox`
129+
// `the quick brown\r\nfox`
130+
try writer.writeAll(part[start..end]);
131+
}
132+
}
133+
134+
pub fn get_size(self: *const Blob) usize {
135+
return self.slice.len;
136+
}
137+
138+
pub fn get_str(self: *const Blob) []const u8 {
139+
return self.slice;
140+
}
141+
142+
const testing = @import("../../testing.zig");
143+
test "Browser: File.Blob" {
144+
try testing.htmlRunner("file/blob.html");
145+
}

src/browser/xhr/File.zig renamed to src/browser/file/File.zig

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@ const std = @import("std");
2121
// https://w3c.github.io/FileAPI/#file-section
2222
const File = @This();
2323

24-
// Very incomplete. The prototype for this is Blob, which we don't have.
25-
// This minimum "implementation" is added because some JavaScript code just
26-
// checks: if (x instanceof File) throw Error(...)
24+
/// TODO: Implement File API.
2725
pub fn constructor() File {
2826
return .{};
2927
}
3028

3129
const testing = @import("../../testing.zig");
32-
test "Browser: File" {
33-
try testing.htmlRunner("xhr/file.html");
30+
test "Browser: File.File" {
31+
try testing.htmlRunner("file/file.html");
3432
}

src/browser/file/root.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! File API.
2+
//! https://developer.mozilla.org/en-US/docs/Web/API/File_API
3+
4+
pub const Interfaces = .{
5+
@import("./Blob.zig"),
6+
@import("./File.zig"),
7+
};

src/browser/js/types.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const Interfaces = generate.Tuple(.{
1717
@import("../url/url.zig").Interfaces,
1818
@import("../xhr/xhr.zig").Interfaces,
1919
@import("../navigation/root.zig").Interfaces,
20+
@import("../file/root.zig").Interfaces,
2021
@import("../xhr/form_data.zig").Interfaces,
21-
@import("../xhr/File.zig"),
2222
@import("../xmlserializer/xmlserializer.zig").Interfaces,
2323
@import("../fetch/fetch.zig").Interfaces,
2424
@import("../streams/streams.zig").Interfaces,

src/tests/file/blob.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<script src="../testing.js"></script>
3+
4+
<script id=Blob>
5+
const parts = ["\r\nthe quick brown\rfo\rx\r", "\njumps over\r\nthe\nlazy\r", "\ndog"];
6+
7+
// "transparent" ending should not modify the final buffer.
8+
let blob1 = new Blob(parts);
9+
let expected = parts.join("");
10+
testing.expectEqual(expected.length, blob1.size);
11+
testing.expectEqual(expected, blob1.str);
12+
13+
// "native" ending should modify the final buffer.
14+
let blob2 = new Blob(parts, { endings: "native" });
15+
expected = "\nthe quick brown\nfo\nx\n\njumps over\nthe\nlazy\n\ndog";
16+
testing.expectEqual(expected.length, blob2.size);
17+
testing.expectEqual(expected, blob2.str);
18+
</script>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<!DOCTYPE html>
22
<script src="../testing.js"></script>
3+
34
<script id=file>
4-
let f = new File()
5+
let f = new File();
56
testing.expectEqual(true, f instanceof File);
67
</script>

0 commit comments

Comments
 (0)