diff --git a/src/browser/tests/file.html b/src/browser/tests/file.html
index 3db5fdfe..0b301b79 100644
--- a/src/browser/tests/file.html
+++ b/src/browser/tests/file.html
@@ -1,12 +1,52 @@
- Test Document Title
+ Test File Web API
-
+
+
+
+
diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig
index 2bead21d..9cc0b4f6 100644
--- a/src/browser/webapi/Blob.zig
+++ b/src/browser/webapi/Blob.zig
@@ -113,7 +113,7 @@ pub fn initFromBytes(data: []const u8, content_type: []const u8, validate_mime:
}
/// Validates and normalizes MIME type according to spec.
-fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bool) ![]const u8 {
+pub fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bool) ![]const u8 {
if (mime_type.len == 0) {
return "";
}
@@ -171,7 +171,7 @@ const vector_sizes = blk: {
};
/// Writes a single part with optional line ending normalization.
-fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Writer) !void {
+pub fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Writer) !void {
// Transparent - no conversion needed.
if (!use_native_endings) {
try writer.writeAll(part);
diff --git a/src/browser/webapi/File.zig b/src/browser/webapi/File.zig
index a0dce126..a740ed04 100644
--- a/src/browser/webapi/File.zig
+++ b/src/browser/webapi/File.zig
@@ -1,4 +1,4 @@
-// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier
// Pierre Tachoire
@@ -27,13 +27,61 @@ const Blob = @import("Blob.zig");
const File = @This();
_proto: *Blob,
+_name: []const u8,
+_last_modified: i64,
-// TODO: Implement File API.
-pub fn init(page: *Page) !*File {
+pub const InitOptions = struct {
+ type: []const u8 = "",
+ endings: []const u8 = "transparent",
+ lastModified: ?i64 = null,
+};
+
+pub fn init(
+ parts_: ?[]const js.Value,
+ name_: []const u8,
+ opts_: ?InitOptions,
+ page: *Page,
+) !*File {
const session = page.session;
- const arena = try session.getArena(.tiny, "File");
+ const arena = try session.getArena(.large, "File");
errdefer session.releaseArena(arena);
- return page.factory.blob(arena, File{ ._proto = undefined });
+
+ const opts = opts_ orelse InitOptions{};
+ const mime = try Blob.validateMimeType(arena, opts.type, false);
+
+ const data = blk: {
+ if (parts_) |blob_parts| {
+ const use_native_endings = std.mem.eql(u8, opts.endings, "native");
+ var w: std.Io.Writer.Allocating = .init(arena);
+ for (blob_parts) |js_val| {
+ const part = try js_val.toStringSmart();
+ try Blob.writePartWithEndings(part, use_native_endings, &w.writer);
+ }
+ break :blk w.written();
+ }
+
+ break :blk "";
+ };
+
+ const last_modified = opts.lastModified orelse std.time.milliTimestamp();
+
+ const file = try page.factory.blob(arena, File{
+ ._proto = undefined,
+ ._name = try arena.dupe(u8, name_),
+ ._last_modified = last_modified,
+ });
+ file._proto._slice = data;
+ file._proto._mime = mime;
+
+ return file;
+}
+
+pub fn getName(self: *const File) []const u8 {
+ return self._name;
+}
+
+pub fn getLastModified(self: *const File) f64 {
+ return @floatFromInt(self._last_modified);
}
pub const JsApi = struct {
@@ -46,6 +94,8 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(File.init, .{});
+ pub const name = bridge.accessor(File.getName, null, .{});
+ pub const lastModified = bridge.accessor(File.getLastModified, null, .{});
};
const testing = @import("../../testing.zig");