-
Notifications
You must be signed in to change notification settings - Fork 286
Blob support
#1190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Blob support
#1190
Changes from 9 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
1085950
initial `Blob` support
nikneym c491648
implement various `Blob` methods
nikneym dd43be4
remove method added for testing
nikneym 3307a66
prefer `writeVec` instead of `writeAll` + `writeByte`
nikneym b68675b
update `Blob` test
nikneym 5785c14
support `Blob` type
nikneym 4be7fa1
support `Blob.arrayBuffer`
nikneym 93542c9
support `Blob.slice`
nikneym 9b990da
update `Blob` test
nikneym 10c2d7d
remove unnecessary import and declaration
nikneym 7aafab9
prefer `js.resolvePromise` helper for promise returns
nikneym File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| // Copyright (C) 2023-2025 Lightpanda (Selecy SAS) | ||
| // | ||
| // Francis Bouvier <[email protected]> | ||
| // Pierre Tachoire <[email protected]> | ||
| // | ||
| // 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 <https://www.gnu.org/licenses/>. | ||
|
|
||
| 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"); | ||
|
|
||
| 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(); | ||
|
|
||
| /// 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 line endings (CR and LF). | ||
| /// `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 .{}; | ||
| // 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); | ||
|
|
||
| return .{ .slice = w.written(), .mime = mime }; | ||
| } | ||
|
|
||
| // We don't have `blob_parts`, why would you want a Blob anyway then? | ||
| return .{ .slice = "", .mime = mime }; | ||
| } | ||
|
|
||
| /// Writes blob parts to given `Writer` with desired endings. | ||
| 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.writeVec(&.{ part[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') { | ||
| 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]); | ||
| } | ||
| } | ||
|
|
||
| /// 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); | ||
nikneym marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| try resolver.resolve(js.ArrayBuffer{ .values = self.slice }); | ||
| return resolver.promise(); | ||
| } | ||
|
|
||
| /// 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 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; | ||
| } | ||
|
|
||
| /// 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"); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"), | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| <!DOCTYPE html> | ||
| <script src="../testing.js"></script> | ||
|
|
||
| <script id=Blob/Blob.text> | ||
| { | ||
| const parts = ["\r\nthe quick brown\rfo\rx\r", "\njumps over\r\nthe\nlazy\r", "\ndog"]; | ||
| // "transparent" ending should not modify the final buffer. | ||
| const blob = new Blob(parts, { type: "text/html" }); | ||
|
|
||
| const expected = parts.join(""); | ||
| testing.expectEqual(expected.length, blob.size); | ||
| testing.expectEqual("text/html", blob.type); | ||
| testing.async(blob.text(), result => testing.expectEqual(expected, result)); | ||
| } | ||
|
|
||
| { | ||
| const parts = ["\rhello\r", "\nwor\r\nld"]; | ||
| // "native" ending should modify the final buffer. | ||
| const blob = new Blob(parts, { endings: "native" }); | ||
|
|
||
| const expected = "\nhello\n\nwor\nld"; | ||
| testing.expectEqual(expected.length, blob.size); | ||
| testing.async(blob.text(), result => testing.expectEqual(expected, result)); | ||
|
|
||
| testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer)); | ||
| } | ||
| </script> | ||
|
|
||
| <script id=Blob.stream> | ||
| { | ||
| const parts = ["may", "thy", "knife", "chip", "and", "shatter"]; | ||
| const blob = new Blob(parts); | ||
| const reader = blob.stream().getReader(); | ||
|
|
||
| testing.async(reader.read(), ({ done, value }) => { | ||
| const expected = new Uint8Array([109, 97, 121, 116, 104, 121, 107, 110, | ||
| 105, 102, 101, 99, 104, 105, 112, 97, | ||
| 110, 100, 115, 104, 97, 116, 116, 101, | ||
| 114]); | ||
| testing.expectEqual(false, done); | ||
| testing.expectEqual(true, value instanceof Uint8Array); | ||
| testing.expectEqual(expected, value); | ||
| }); | ||
| } | ||
| </script> | ||
|
|
||
| <script id=Blob.arrayBuffer/Blob.slice> | ||
| { | ||
| const parts = ["la", "symphonie", "des", "éclairs"]; | ||
| const blob = new Blob(parts); | ||
| testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer)); | ||
|
|
||
| let temp = blob.slice(0); | ||
| testing.expectEqual(blob.size, temp.size); | ||
| testing.async(temp.text(), result => { | ||
| testing.expectEqual("lasymphoniedeséclairs", result); | ||
| }); | ||
|
|
||
| temp = blob.slice(-4, -2, "custom"); | ||
| testing.expectEqual(2, temp.size); | ||
| testing.expectEqual("custom", temp.type); | ||
| testing.async(temp.text(), result => testing.expectEqual("ai", result)); | ||
|
|
||
| temp = blob.slice(14); | ||
| testing.expectEqual(8, temp.size); | ||
| testing.async(temp.text(), result => testing.expectEqual("éclairs", result)); | ||
|
|
||
| temp = blob.slice(6, -10, "text/eclair"); | ||
| testing.expectEqual(6, temp.size); | ||
| testing.expectEqual("text/eclair", temp.type); | ||
| testing.async(temp.text(), result => testing.expectEqual("honied", result)); | ||
| } | ||
| </script> | ||
|
|
||
| <!-- Firefox and Safari only --> | ||
| <script id=Blob.bytes> | ||
| { | ||
| const parts = ["light ", "panda ", "rocks ", "!"]; | ||
| const blob = new Blob(parts); | ||
|
|
||
| testing.async(blob.bytes(), result => { | ||
| const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97, | ||
| 110, 100, 97, 32, 114, 111, 99, 107, 115, | ||
| 32, 33]); | ||
| testing.expectEqual(true, result instanceof Uint8Array); | ||
| testing.expectEqual(expected, result); | ||
| }); | ||
| } | ||
| </script> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.