mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 14:43:28 +00:00
@@ -5,6 +5,7 @@ const Console = @import("jsruntime").Console;
|
||||
const DOM = @import("dom/dom.zig");
|
||||
const HTML = @import("html/html.zig");
|
||||
const Events = @import("events/event.zig");
|
||||
const XHR = @import("xhr/xhr.zig");
|
||||
|
||||
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
@@ -14,4 +15,5 @@ pub const Interfaces = generate.Tuple(.{
|
||||
DOM.Interfaces,
|
||||
Events.Interfaces,
|
||||
HTML.Interfaces,
|
||||
XHR.Interfaces,
|
||||
});
|
||||
|
||||
1627
src/async/Client.zig
Normal file
1627
src/async/Client.zig
Normal file
File diff suppressed because it is too large
Load Diff
115
src/async/stream.zig
Normal file
115
src/async/stream.zig
Normal file
@@ -0,0 +1,115 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const os = std.os;
|
||||
const io = std.io;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const tcp = @import("tcp.zig");
|
||||
|
||||
pub const Stream = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
conn: *tcp.Conn,
|
||||
|
||||
handle: std.os.socket_t,
|
||||
|
||||
pub fn close(self: Stream) void {
|
||||
os.closeSocket(self.handle);
|
||||
self.alloc.destroy(self.conn);
|
||||
}
|
||||
|
||||
pub const ReadError = os.ReadError;
|
||||
pub const WriteError = os.WriteError;
|
||||
|
||||
pub const Reader = io.Reader(Stream, ReadError, read);
|
||||
pub const Writer = io.Writer(Stream, WriteError, write);
|
||||
|
||||
pub fn reader(self: Stream) Reader {
|
||||
return .{ .context = self };
|
||||
}
|
||||
|
||||
pub fn writer(self: Stream) Writer {
|
||||
return .{ .context = self };
|
||||
}
|
||||
|
||||
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
|
||||
return self.conn.receive(self.handle, buffer) catch |err| switch (err) {
|
||||
else => return error.Unexpected,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn readv(s: Stream, iovecs: []const os.iovec) ReadError!usize {
|
||||
return os.readv(s.handle, iovecs);
|
||||
}
|
||||
|
||||
/// Returns the number of bytes read. If the number read is smaller than
|
||||
/// `buffer.len`, it means the stream reached the end. Reaching the end of
|
||||
/// a stream is not an error condition.
|
||||
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
|
||||
return readAtLeast(s, buffer, buffer.len);
|
||||
}
|
||||
|
||||
/// Returns the number of bytes read, calling the underlying read function
|
||||
/// the minimal number of times until the buffer has at least `len` bytes
|
||||
/// filled. If the number read is less than `len` it means the stream
|
||||
/// reached the end. Reaching the end of the stream is not an error
|
||||
/// condition.
|
||||
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
|
||||
assert(len <= buffer.len);
|
||||
var index: usize = 0;
|
||||
while (index < len) {
|
||||
const amt = try s.read(buffer[index..]);
|
||||
if (amt == 0) break;
|
||||
index += amt;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
|
||||
/// file system thread instead of non-blocking. It needs to be reworked to properly
|
||||
/// use non-blocking I/O.
|
||||
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
|
||||
return self.conn.send(self.handle, buffer) catch |err| switch (err) {
|
||||
error.AccessDenied => error.AccessDenied,
|
||||
error.WouldBlock => error.WouldBlock,
|
||||
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
|
||||
error.MessageTooBig => error.FileTooBig,
|
||||
error.BrokenPipe => error.BrokenPipe,
|
||||
else => return error.Unexpected,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
|
||||
var index: usize = 0;
|
||||
while (index < bytes.len) {
|
||||
index += try self.write(bytes[index..]);
|
||||
}
|
||||
}
|
||||
|
||||
/// See https://github.com/ziglang/zig/issues/7699
|
||||
/// See equivalent function: `std.fs.File.writev`.
|
||||
pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {
|
||||
if (iovecs.len == 0) return 0;
|
||||
const first_buffer = iovecs[0].iov_base[0..iovecs[0].iov_len];
|
||||
return try self.write(first_buffer);
|
||||
}
|
||||
|
||||
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
|
||||
/// order to handle partial writes from the underlying OS layer.
|
||||
/// See https://github.com/ziglang/zig/issues/7699
|
||||
/// See equivalent function: `std.fs.File.writevAll`.
|
||||
pub fn writevAll(self: Stream, iovecs: []os.iovec_const) WriteError!void {
|
||||
if (iovecs.len == 0) return;
|
||||
|
||||
var i: usize = 0;
|
||||
while (true) {
|
||||
var amt = try self.writev(iovecs[i..]);
|
||||
while (amt >= iovecs[i].iov_len) {
|
||||
amt -= iovecs[i].iov_len;
|
||||
i += 1;
|
||||
if (i >= iovecs.len) return;
|
||||
}
|
||||
iovecs[i].iov_base += amt;
|
||||
iovecs[i].iov_len -= amt;
|
||||
}
|
||||
}
|
||||
};
|
||||
94
src/async/tcp.zig
Normal file
94
src/async/tcp.zig
Normal file
@@ -0,0 +1,94 @@
|
||||
const std = @import("std");
|
||||
const net = std.net;
|
||||
const Stream = @import("stream.zig").Stream;
|
||||
const Loop = @import("jsruntime").Loop;
|
||||
const NetworkImpl = Loop.Network(Conn.Command);
|
||||
|
||||
// Conn is a TCP connection using jsruntime Loop async I/O.
|
||||
// connect, send and receive are blocking, but use async I/O in the background.
|
||||
// Client doesn't own the socket used for the connection, the caller is
|
||||
// responsible for closing it.
|
||||
pub const Conn = struct {
|
||||
const Command = struct {
|
||||
impl: NetworkImpl,
|
||||
|
||||
done: bool = false,
|
||||
err: ?anyerror = null,
|
||||
ln: usize = 0,
|
||||
|
||||
fn ok(self: *Command, err: ?anyerror, ln: usize) void {
|
||||
self.err = err;
|
||||
self.ln = ln;
|
||||
self.done = true;
|
||||
}
|
||||
|
||||
fn wait(self: *Command) !usize {
|
||||
while (!self.done) try self.impl.tick();
|
||||
|
||||
if (self.err) |err| return err;
|
||||
return self.ln;
|
||||
}
|
||||
pub fn onConnect(self: *Command, err: ?anyerror) void {
|
||||
self.ok(err, 0);
|
||||
}
|
||||
pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void {
|
||||
self.ok(err, ln);
|
||||
}
|
||||
pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void {
|
||||
self.ok(err, ln);
|
||||
}
|
||||
};
|
||||
|
||||
loop: *Loop,
|
||||
|
||||
pub fn connect(self: *Conn, socket: std.os.socket_t, address: std.net.Address) !void {
|
||||
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
|
||||
cmd.impl.connect(&cmd, socket, address);
|
||||
_ = try cmd.wait();
|
||||
}
|
||||
|
||||
pub fn send(self: *Conn, socket: std.os.socket_t, buffer: []const u8) !usize {
|
||||
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
|
||||
cmd.impl.send(&cmd, socket, buffer);
|
||||
return try cmd.wait();
|
||||
}
|
||||
|
||||
pub fn receive(self: *Conn, socket: std.os.socket_t, buffer: []u8) !usize {
|
||||
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
|
||||
cmd.impl.receive(&cmd, socket, buffer);
|
||||
return try cmd.wait();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
|
||||
// TODO async resolve
|
||||
const list = try net.getAddressList(alloc, name, port);
|
||||
defer list.deinit();
|
||||
|
||||
if (list.addrs.len == 0) return error.UnknownHostName;
|
||||
|
||||
for (list.addrs) |addr| {
|
||||
return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) {
|
||||
error.ConnectionRefused => {
|
||||
continue;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
return std.os.ConnectError.ConnectionRefused;
|
||||
}
|
||||
|
||||
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
|
||||
const sockfd = try std.os.socket(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP);
|
||||
errdefer std.os.closeSocket(sockfd);
|
||||
|
||||
var conn = try alloc.create(Conn);
|
||||
conn.* = Conn{ .loop = loop };
|
||||
try conn.connect(sockfd, addr);
|
||||
|
||||
return Stream{
|
||||
.alloc = alloc,
|
||||
.conn = conn,
|
||||
.handle = sockfd,
|
||||
};
|
||||
}
|
||||
172
src/async/test.zig
Normal file
172
src/async/test.zig
Normal file
@@ -0,0 +1,172 @@
|
||||
const std = @import("std");
|
||||
const http = std.http;
|
||||
const Client = @import("Client.zig");
|
||||
const Request = @import("Client.zig").Request;
|
||||
|
||||
pub const Loop = @import("jsruntime").Loop;
|
||||
|
||||
const url = "https://w3.org";
|
||||
|
||||
test "blocking mode fetch API" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
var client: Client = .{
|
||||
.allocator = alloc,
|
||||
.loop = &loop,
|
||||
};
|
||||
defer client.deinit();
|
||||
|
||||
// force client's CA cert scan from system.
|
||||
try client.ca_bundle.rescan(client.allocator);
|
||||
|
||||
var res = try client.fetch(alloc, .{
|
||||
.location = .{ .uri = try std.Uri.parse(url) },
|
||||
.payload = .none,
|
||||
});
|
||||
defer res.deinit();
|
||||
|
||||
try std.testing.expect(res.status == .ok);
|
||||
}
|
||||
|
||||
test "blocking mode open/send/wait API" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
var client: Client = .{
|
||||
.allocator = alloc,
|
||||
.loop = &loop,
|
||||
};
|
||||
defer client.deinit();
|
||||
|
||||
// force client's CA cert scan from system.
|
||||
try client.ca_bundle.rescan(client.allocator);
|
||||
|
||||
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{});
|
||||
defer headers.deinit();
|
||||
|
||||
var req = try client.open(.GET, try std.Uri.parse(url), headers, .{});
|
||||
defer req.deinit();
|
||||
|
||||
try req.send(.{});
|
||||
try req.finish();
|
||||
try req.wait();
|
||||
|
||||
try std.testing.expect(req.response.status == .ok);
|
||||
}
|
||||
|
||||
// Example how to write an async http client using the modified standard client.
|
||||
const AsyncClient = struct {
|
||||
cli: Client,
|
||||
|
||||
const YieldImpl = Loop.Yield(AsyncRequest);
|
||||
const AsyncRequest = struct {
|
||||
const State = enum { new, open, send, finish, wait, done };
|
||||
|
||||
cli: *Client,
|
||||
uri: std.Uri,
|
||||
headers: std.http.Headers,
|
||||
|
||||
req: ?Request = undefined,
|
||||
state: State = .new,
|
||||
|
||||
impl: YieldImpl,
|
||||
err: ?anyerror = null,
|
||||
|
||||
pub fn deinit(self: *AsyncRequest) void {
|
||||
if (self.req) |*r| r.deinit();
|
||||
self.headers.deinit();
|
||||
}
|
||||
|
||||
pub fn fetch(self: *AsyncRequest) void {
|
||||
self.state = .new;
|
||||
return self.impl.yield(self);
|
||||
}
|
||||
|
||||
fn onerr(self: *AsyncRequest, err: anyerror) void {
|
||||
self.state = .done;
|
||||
self.err = err;
|
||||
}
|
||||
|
||||
pub fn onYield(self: *AsyncRequest, err: ?anyerror) void {
|
||||
if (err) |e| return self.onerr(e);
|
||||
|
||||
switch (self.state) {
|
||||
.new => {
|
||||
self.state = .open;
|
||||
self.req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e);
|
||||
},
|
||||
.open => {
|
||||
self.state = .send;
|
||||
self.req.?.send(.{}) catch |e| return self.onerr(e);
|
||||
},
|
||||
.send => {
|
||||
self.state = .finish;
|
||||
self.req.?.finish() catch |e| return self.onerr(e);
|
||||
},
|
||||
.finish => {
|
||||
self.state = .wait;
|
||||
self.req.?.wait() catch |e| return self.onerr(e);
|
||||
},
|
||||
.wait => {
|
||||
self.state = .done;
|
||||
return;
|
||||
},
|
||||
.done => return,
|
||||
}
|
||||
|
||||
return self.impl.yield(self);
|
||||
}
|
||||
|
||||
pub fn wait(self: *AsyncRequest) !void {
|
||||
while (self.state != .done) try self.impl.tick();
|
||||
if (self.err) |err| return err;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient {
|
||||
return .{
|
||||
.cli = .{
|
||||
.allocator = alloc,
|
||||
.loop = loop,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *AsyncClient) void {
|
||||
self.cli.deinit();
|
||||
}
|
||||
|
||||
pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest {
|
||||
return .{
|
||||
.impl = YieldImpl.init(self.cli.loop),
|
||||
.cli = &self.cli,
|
||||
.uri = uri,
|
||||
.headers = .{ .allocator = self.cli.allocator, .owned = false },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "non blocking client" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
var client = AsyncClient.init(alloc, &loop);
|
||||
defer client.deinit();
|
||||
|
||||
var reqs: [3]AsyncClient.AsyncRequest = undefined;
|
||||
for (0..reqs.len) |i| {
|
||||
reqs[i] = try client.createRequest(try std.Uri.parse(url));
|
||||
reqs[i].fetch();
|
||||
}
|
||||
for (0..reqs.len) |i| {
|
||||
try reqs[i].wait();
|
||||
reqs[i].deinit();
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,15 @@ pub const Page = struct {
|
||||
log.info("GET {any} {d}", .{ self.uri, req.response.status });
|
||||
|
||||
// TODO handle redirection
|
||||
if (req.response.status != .ok) return error.BadStatusCode;
|
||||
if (req.response.status != .ok) {
|
||||
log.debug("{?} {d} {s}\n{any}", .{
|
||||
req.response.version,
|
||||
req.response.status,
|
||||
req.response.reason,
|
||||
req.response.headers,
|
||||
});
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
|
||||
// TODO handle charset
|
||||
// https://html.spec.whatwg.org/#content-type
|
||||
|
||||
@@ -17,6 +17,7 @@ params: []const u8 = "",
|
||||
charset: ?[]const u8 = null,
|
||||
boundary: ?[]const u8 = null,
|
||||
|
||||
pub const Empty = Self{ .mtype = "", .msubtype = "" };
|
||||
pub const HTML = Self{ .mtype = "text", .msubtype = "html" };
|
||||
pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" };
|
||||
|
||||
|
||||
@@ -13,6 +13,16 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
|
||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Event,
|
||||
ProgressEvent,
|
||||
});
|
||||
const Generated = generate.Union.compile(Interfaces);
|
||||
pub const Union = Generated._union;
|
||||
|
||||
// https://dom.spec.whatwg.org/#event
|
||||
pub const Event = struct {
|
||||
pub const Self = parser.Event;
|
||||
@@ -28,6 +38,13 @@ pub const Event = struct {
|
||||
pub const _AT_TARGET = 2;
|
||||
pub const _BUBBLING_PHASE = 3;
|
||||
|
||||
pub fn toInterface(evt: *parser.Event) !Union {
|
||||
return switch (try parser.eventGetInternalType(evt)) {
|
||||
.event => .{ .Event = evt },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
|
||||
const event = try parser.eventCreate();
|
||||
try parser.eventInit(event, eventType, opts orelse EventInit{});
|
||||
@@ -104,13 +121,6 @@ pub const Event = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Event,
|
||||
});
|
||||
const Generated = generate.Union.compile(Interfaces);
|
||||
pub const Union = Generated._union;
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
@@ -198,4 +208,13 @@ pub fn testExecFn(
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
};
|
||||
try checkCases(js_env, &legacy);
|
||||
|
||||
var remove = [_]Case{
|
||||
.{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" },
|
||||
.{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" },
|
||||
.{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" },
|
||||
.{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "0" },
|
||||
};
|
||||
try checkCases(js_env, &remove);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ const c = @cImport({
|
||||
@cInclude("dom/dom.h");
|
||||
@cInclude("dom/bindings/hubbub/parser.h");
|
||||
@cInclude("events/event_target.h");
|
||||
@cInclude("events/event.h");
|
||||
});
|
||||
|
||||
const Callback = @import("jsruntime").Callback;
|
||||
const EventToInterface = @import("events/event.zig").Event.toInterface;
|
||||
|
||||
// Vtable
|
||||
// ------
|
||||
@@ -360,6 +362,10 @@ pub const EventInit = struct {
|
||||
composed: bool = false,
|
||||
};
|
||||
|
||||
pub fn eventDestroy(evt: *Event) void {
|
||||
c._dom_event_destroy(evt);
|
||||
}
|
||||
|
||||
pub fn eventInit(evt: *Event, typ: []const u8, opts: EventInit) !void {
|
||||
const s = try strFromData(typ);
|
||||
const err = c._dom_event_init(evt, s, opts.bubbles, opts.cancelable);
|
||||
@@ -444,6 +450,23 @@ pub fn eventPreventDefault(evt: *Event) !void {
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventGetInternalType(evt: *Event) !EventType {
|
||||
var res: u32 = undefined;
|
||||
const err = c._dom_event_get_internal_type(evt, &res);
|
||||
try DOMErr(err);
|
||||
return @enumFromInt(res);
|
||||
}
|
||||
|
||||
pub fn eventSetInternalType(evt: *Event, internal_type: EventType) !void {
|
||||
const err = c._dom_event_set_internal_type(evt, @intFromEnum(internal_type));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub const EventType = enum(u8) {
|
||||
event = 0,
|
||||
progress_event = 1,
|
||||
};
|
||||
|
||||
// EventHandler
|
||||
fn event_handler_cbk(data: *anyopaque) *Callback {
|
||||
const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data);
|
||||
@@ -454,7 +477,14 @@ const event_handler = struct {
|
||||
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
|
||||
if (data) |d| {
|
||||
const func = event_handler_cbk(d);
|
||||
func.call(.{event}) catch unreachable;
|
||||
|
||||
if (event) |evt| {
|
||||
func.call(.{
|
||||
EventToInterface(evt) catch unreachable,
|
||||
}) catch unreachable;
|
||||
} else {
|
||||
func.call(.{event}) catch unreachable;
|
||||
}
|
||||
// NOTE: we can not call func.deinit here
|
||||
// b/c the handler can be called several times
|
||||
// either on this dispatch event or in anoter one
|
||||
@@ -671,7 +701,12 @@ pub const EventTargetTBase = extern struct {
|
||||
|
||||
pub fn dispatch_event(et: [*c]c.dom_event_target, evt: ?*c.struct_dom_event, res: [*c]bool) callconv(.C) c.dom_exception {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
return c._dom_event_target_dispatch(et, &self.eti, evt, c.DOM_BUBBLING_PHASE, res);
|
||||
// Set the event target to the target dispatched.
|
||||
const e = c._dom_event_set_target(evt, et);
|
||||
if (e != c.DOM_NO_ERR) {
|
||||
return e;
|
||||
}
|
||||
return c._dom_event_target_dispatch(et, &self.eti, evt, c.DOM_AT_TARGET, res);
|
||||
}
|
||||
|
||||
pub fn remove_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
|
||||
|
||||
@@ -7,6 +7,7 @@ const generate = @import("generate.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
const Window = @import("html/window.zig").Window;
|
||||
const xhr = @import("xhr/xhr.zig");
|
||||
|
||||
const documentTestExecFn = @import("dom/document.zig").testExecFn;
|
||||
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
|
||||
@@ -23,6 +24,8 @@ const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn;
|
||||
const AttrTestExecFn = @import("dom/attribute.zig").testExecFn;
|
||||
const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn;
|
||||
const EventTestExecFn = @import("events/event.zig").testExecFn;
|
||||
const XHRTestExecFn = xhr.testExecFn;
|
||||
const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn;
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
|
||||
@@ -78,6 +81,8 @@ fn testsAllExecFn(
|
||||
AttrTestExecFn,
|
||||
EventTargetTestExecFn,
|
||||
EventTestExecFn,
|
||||
XHRTestExecFn,
|
||||
ProgressEventTestExecFn,
|
||||
};
|
||||
|
||||
inline for (testFns) |testFn| {
|
||||
@@ -93,6 +98,11 @@ pub fn main() !void {
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
const TestAsync = @import("async/test.zig");
|
||||
std.testing.refAllDecls(TestAsync);
|
||||
}
|
||||
|
||||
test "jsruntime" {
|
||||
// generate tests
|
||||
try generate.tests();
|
||||
@@ -157,3 +167,20 @@ test "DocumentHTML is a libdom event target" {
|
||||
const et = parser.toEventTarget(parser.DocumentHTML, doc);
|
||||
_ = try parser.eventTargetDispatchEvent(et, event);
|
||||
}
|
||||
|
||||
test "XMLHttpRequest.validMethod" {
|
||||
// valid methods
|
||||
for ([_][]const u8{ "get", "GET", "head", "HEAD" }) |tc| {
|
||||
_ = try xhr.XMLHttpRequest.validMethod(tc);
|
||||
}
|
||||
|
||||
// forbidden
|
||||
for ([_][]const u8{ "connect", "CONNECT" }) |tc| {
|
||||
try std.testing.expectError(parser.DOMError.Security, xhr.XMLHttpRequest.validMethod(tc));
|
||||
}
|
||||
|
||||
// syntax
|
||||
for ([_][]const u8{ "foo", "BAR" }) |tc| {
|
||||
try std.testing.expectError(parser.DOMError.Syntax, xhr.XMLHttpRequest.validMethod(tc));
|
||||
}
|
||||
}
|
||||
|
||||
96
src/xhr/event_target.zig
Normal file
96
src/xhr/event_target.zig
Normal file
@@ -0,0 +1,96 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const log = std.log.scoped(.xhr);
|
||||
|
||||
pub const XMLHttpRequestEventTarget = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
|
||||
onloadstart_cbk: ?Callback = null,
|
||||
onprogress_cbk: ?Callback = null,
|
||||
onabort_cbk: ?Callback = null,
|
||||
onload_cbk: ?Callback = null,
|
||||
ontimeout_cbk: ?Callback = null,
|
||||
onloadend_cbk: ?Callback = null,
|
||||
|
||||
fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
|
||||
try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false);
|
||||
}
|
||||
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
|
||||
const et = @as(*parser.EventTarget, @ptrCast(self));
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id());
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove listener
|
||||
try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false);
|
||||
}
|
||||
|
||||
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback {
|
||||
return self.onloadstart_cbk;
|
||||
}
|
||||
pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Callback {
|
||||
return self.onprogress_cbk;
|
||||
}
|
||||
pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Callback {
|
||||
return self.onabort_cbk;
|
||||
}
|
||||
pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Callback {
|
||||
return self.onload_cbk;
|
||||
}
|
||||
pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Callback {
|
||||
return self.ontimeout_cbk;
|
||||
}
|
||||
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Callback {
|
||||
return self.onloadend_cbk;
|
||||
}
|
||||
|
||||
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk);
|
||||
try self.register(alloc, "loadstart", handler);
|
||||
self.onloadstart_cbk = handler;
|
||||
}
|
||||
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk);
|
||||
try self.register(alloc, "progress", handler);
|
||||
self.onprogress_cbk = handler;
|
||||
}
|
||||
pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk);
|
||||
try self.register(alloc, "abort", handler);
|
||||
self.onabort_cbk = handler;
|
||||
}
|
||||
pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk);
|
||||
try self.register(alloc, "load", handler);
|
||||
self.onload_cbk = handler;
|
||||
}
|
||||
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk);
|
||||
try self.register(alloc, "timeout", handler);
|
||||
self.ontimeout_cbk = handler;
|
||||
}
|
||||
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk);
|
||||
try self.register(alloc, "loadend", handler);
|
||||
self.onloadend_cbk = handler;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void {
|
||||
parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| {
|
||||
log.err("remove all listeners: {any}", .{e});
|
||||
};
|
||||
}
|
||||
};
|
||||
72
src/xhr/progress_event.zig
Normal file
72
src/xhr/progress_event.zig
Normal file
@@ -0,0 +1,72 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("../events/event.zig").Event;
|
||||
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
|
||||
pub const ProgressEvent = struct {
|
||||
pub const prototype = *Event;
|
||||
pub const Exception = DOMException;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const EventInit = struct {
|
||||
lengthComputable: bool = false,
|
||||
loaded: u64 = 0,
|
||||
total: u64 = 0,
|
||||
};
|
||||
|
||||
proto: parser.Event,
|
||||
lengthComputable: bool,
|
||||
loaded: u64 = 0,
|
||||
total: u64 = 0,
|
||||
|
||||
pub fn constructor(eventType: []const u8, opts: ?EventInit) !ProgressEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, eventType, .{});
|
||||
try parser.eventSetInternalType(event, .progress_event);
|
||||
|
||||
const o = opts orelse EventInit{};
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.lengthComputable = o.lengthComputable,
|
||||
.loaded = o.loaded,
|
||||
.total = o.total,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_lengthComputable(self: ProgressEvent) bool {
|
||||
return self.lengthComputable;
|
||||
}
|
||||
|
||||
pub fn get_loaded(self: ProgressEvent) u64 {
|
||||
return self.loaded;
|
||||
}
|
||||
|
||||
pub fn get_total(self: ProgressEvent) u64 {
|
||||
return self.total;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var progress_event = [_]Case{
|
||||
.{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" },
|
||||
.{ .src = "pevt.loaded", .ex = "0" },
|
||||
.{ .src = "pevt instanceof ProgressEvent", .ex = "true" },
|
||||
.{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" },
|
||||
.{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" },
|
||||
.{ .src = "document.dispatchEvent(pevt)", .ex = "true" },
|
||||
.{ .src = "eevt.type", .ex = "foo" },
|
||||
.{ .src = "eevt instanceof ProgressEvent", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &progress_event);
|
||||
}
|
||||
833
src/xhr/xhr.zig
Normal file
833
src/xhr/xhr.zig
Normal file
@@ -0,0 +1,833 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const DOMError = @import("../netsurf.zig").DOMError;
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
|
||||
const ProgressEvent = @import("progress_event.zig").ProgressEvent;
|
||||
const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget;
|
||||
|
||||
const Mime = @import("../browser/mime.zig");
|
||||
|
||||
const Loop = jsruntime.Loop;
|
||||
const YieldImpl = Loop.Yield(XMLHttpRequest);
|
||||
const Client = @import("../async/Client.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const log = std.log.scoped(.xhr);
|
||||
|
||||
// XHR interfaces
|
||||
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
XMLHttpRequestEventTarget,
|
||||
XMLHttpRequestUpload,
|
||||
XMLHttpRequest,
|
||||
});
|
||||
|
||||
pub const XMLHttpRequestUpload = struct {
|
||||
pub const prototype = *XMLHttpRequestEventTarget;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
|
||||
};
|
||||
|
||||
pub const XMLHttpRequestBodyInitTag = enum {
|
||||
Blob,
|
||||
BufferSource,
|
||||
FormData,
|
||||
URLSearchParams,
|
||||
String,
|
||||
};
|
||||
|
||||
pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
|
||||
Blob: []const u8,
|
||||
BufferSource: []const u8,
|
||||
FormData: []const u8,
|
||||
URLSearchParams: []const u8,
|
||||
String: []const u8,
|
||||
|
||||
fn contentType(self: XMLHttpRequestBodyInit) ![]const u8 {
|
||||
return switch (self) {
|
||||
.Blob => error.NotImplemented,
|
||||
.BufferSource => error.NotImplemented,
|
||||
.FormData => "multipart/form-data; boundary=TODO",
|
||||
.URLSearchParams => "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
.String => "text/plain; charset=UTF-8",
|
||||
};
|
||||
}
|
||||
|
||||
// Duplicate the body content.
|
||||
// The caller owns the allocated string.
|
||||
fn dupe(self: XMLHttpRequestBodyInit, alloc: std.mem.Allocator) ![]const u8 {
|
||||
return switch (self) {
|
||||
.Blob => error.NotImplemented,
|
||||
.BufferSource => error.NotImplemented,
|
||||
.FormData => error.NotImplemented,
|
||||
.URLSearchParams => error.NotImplemented,
|
||||
.String => |v| try alloc.dupe(u8, v),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const XMLHttpRequest = struct {
|
||||
pub const prototype = *XMLHttpRequestEventTarget;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const UNSENT: u16 = 0;
|
||||
pub const OPENED: u16 = 1;
|
||||
pub const HEADERS_RECEIVED: u16 = 2;
|
||||
pub const LOADING: u16 = 3;
|
||||
pub const DONE: u16 = 4;
|
||||
|
||||
// https://xhr.spec.whatwg.org/#response-type
|
||||
const ResponseType = enum {
|
||||
Empty,
|
||||
Text,
|
||||
ArrayBuffer,
|
||||
Blob,
|
||||
Document,
|
||||
JSON,
|
||||
};
|
||||
|
||||
// TODO use std.json.Value instead, but it causes comptime error.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/204
|
||||
// const JSONValue = std.json.Value;
|
||||
const JSONValue = u8;
|
||||
|
||||
const Response = union(ResponseType) {
|
||||
Empty: void,
|
||||
Text: []const u8,
|
||||
ArrayBuffer: void,
|
||||
Blob: void,
|
||||
Document: *parser.Document,
|
||||
JSON: JSONValue,
|
||||
};
|
||||
|
||||
const ResponseObjTag = enum {
|
||||
Document,
|
||||
Failure,
|
||||
JSON,
|
||||
};
|
||||
const ResponseObj = union(ResponseObjTag) {
|
||||
Document: *parser.Document,
|
||||
Failure: bool,
|
||||
JSON: std.json.Parsed(JSONValue),
|
||||
|
||||
fn deinit(self: ResponseObj) void {
|
||||
return switch (self) {
|
||||
.Document => |d| {
|
||||
const doc = @as(*parser.DocumentHTML, @ptrCast(d));
|
||||
parser.documentHTMLClose(doc) catch {};
|
||||
},
|
||||
.JSON => |p| p.deinit(),
|
||||
.Failure => {},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const PrivState = enum { new, open, send, write, finish, wait, done };
|
||||
|
||||
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
|
||||
alloc: std.mem.Allocator,
|
||||
cli: Client,
|
||||
impl: YieldImpl,
|
||||
|
||||
priv_state: PrivState = .new,
|
||||
req: ?Client.Request = null,
|
||||
|
||||
method: std.http.Method,
|
||||
state: u16,
|
||||
url: ?[]const u8,
|
||||
uri: std.Uri,
|
||||
headers: std.http.Headers,
|
||||
sync: bool = true,
|
||||
err: ?anyerror = null,
|
||||
|
||||
// TODO uncomment this field causes casting issue with
|
||||
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
|
||||
// not sure. see
|
||||
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
|
||||
// upload: ?XMLHttpRequestUpload = null,
|
||||
|
||||
timeout: u32 = 0,
|
||||
withCredentials: bool = false,
|
||||
// TODO: response readonly attribute any response;
|
||||
response_bytes: ?[]const u8 = null,
|
||||
response_type: ResponseType = .Empty,
|
||||
response_headers: std.http.Headers,
|
||||
response_status: u10 = 0,
|
||||
response_override_mime_type: ?[]const u8 = null,
|
||||
response_mime: Mime = undefined,
|
||||
response_obj: ?ResponseObj = null,
|
||||
send_flag: bool = false,
|
||||
|
||||
payload: ?[]const u8 = null,
|
||||
|
||||
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.headers = .{ .allocator = alloc, .owned = true },
|
||||
.response_headers = .{ .allocator = alloc, .owned = true },
|
||||
.impl = YieldImpl.init(loop),
|
||||
.method = undefined,
|
||||
.url = null,
|
||||
.uri = undefined,
|
||||
.state = UNSENT,
|
||||
// TODO retrieve the HTTP client globally to reuse existing connections.
|
||||
.cli = .{ .allocator = alloc, .loop = loop },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
|
||||
if (self.url) |v| alloc.free(v);
|
||||
self.url = null;
|
||||
|
||||
if (self.payload) |v| alloc.free(v);
|
||||
self.payload = null;
|
||||
|
||||
if (self.response_bytes) |v| alloc.free(v);
|
||||
if (self.response_obj) |v| v.deinit();
|
||||
|
||||
self.response_obj = null;
|
||||
self.response_mime = Mime.Empty;
|
||||
self.response_type = .Empty;
|
||||
|
||||
// TODO should we clearRetainingCapacity instead?
|
||||
self.headers.clearAndFree();
|
||||
self.response_headers.clearAndFree();
|
||||
self.response_status = 0;
|
||||
|
||||
self.send_flag = false;
|
||||
|
||||
self.priv_state = .new;
|
||||
|
||||
if (self.req) |*r| {
|
||||
r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
|
||||
self.reset();
|
||||
self.headers.deinit();
|
||||
self.response_headers.deinit();
|
||||
|
||||
self.proto.deinit(alloc);
|
||||
|
||||
// TODO the client must be shared between requests.
|
||||
self.cli.deinit();
|
||||
}
|
||||
|
||||
pub fn get_readyState(self: *XMLHttpRequest) u16 {
|
||||
return self.state;
|
||||
}
|
||||
|
||||
pub fn get_timeout(self: *XMLHttpRequest) u32 {
|
||||
return self.timeout;
|
||||
}
|
||||
|
||||
pub fn set_timeout(self: *XMLHttpRequest, timeout: u32) !void {
|
||||
// TODO If the current global object is a Window object and this’s
|
||||
// synchronous flag is set, then throw an "InvalidAccessError"
|
||||
// DOMException.
|
||||
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout
|
||||
self.timeout = timeout;
|
||||
}
|
||||
|
||||
pub fn get_withCredentials(self: *XMLHttpRequest) bool {
|
||||
return self.withCredentials;
|
||||
}
|
||||
|
||||
pub fn set_withCredentials(self: *XMLHttpRequest, withCredentials: bool) !void {
|
||||
if (self.state != OPENED and self.state != UNSENT) return DOMError.InvalidState;
|
||||
if (self.send_flag) return DOMError.InvalidState;
|
||||
|
||||
self.withCredentials = withCredentials;
|
||||
}
|
||||
|
||||
pub fn _open(
|
||||
self: *XMLHttpRequest,
|
||||
alloc: std.mem.Allocator,
|
||||
method: []const u8,
|
||||
url: []const u8,
|
||||
asyn: ?bool,
|
||||
username: ?[]const u8,
|
||||
password: ?[]const u8,
|
||||
) !void {
|
||||
_ = username;
|
||||
_ = password;
|
||||
|
||||
// TODO If this’s relevant global object is a Window object and its
|
||||
// associated Document is not fully active, then throw an
|
||||
// "InvalidStateError" DOMException.
|
||||
|
||||
self.method = try validMethod(method);
|
||||
|
||||
self.reset(alloc);
|
||||
|
||||
self.url = try alloc.dupe(u8, url);
|
||||
self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax;
|
||||
self.sync = if (asyn) |b| !b else false;
|
||||
|
||||
self.state = OPENED;
|
||||
self.dispatchEvt("readystatechange");
|
||||
}
|
||||
|
||||
// dispatch request event.
|
||||
// errors are logged only.
|
||||
fn dispatchEvt(self: *XMLHttpRequest, typ: []const u8) void {
|
||||
const evt = parser.eventCreate() catch |e| {
|
||||
return log.err("dispatch event create: {any}", .{e});
|
||||
};
|
||||
|
||||
// We can we defer event destroy once the event is dispatched.
|
||||
defer parser.eventDestroy(evt);
|
||||
|
||||
parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true }) catch |e| {
|
||||
return log.err("dispatch event init: {any}", .{e});
|
||||
};
|
||||
_ = parser.eventTargetDispatchEvent(@as(*parser.EventTarget, @ptrCast(self)), evt) catch |e| {
|
||||
return log.err("dispatch event: {any}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
fn dispatchProgressEvent(
|
||||
self: *XMLHttpRequest,
|
||||
typ: []const u8,
|
||||
opts: ProgressEvent.EventInit,
|
||||
) void {
|
||||
var evt = ProgressEvent.constructor(typ, .{
|
||||
// https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface
|
||||
.lengthComputable = opts.total > 0,
|
||||
.total = opts.total,
|
||||
.loaded = opts.loaded,
|
||||
}) catch |e| {
|
||||
return log.err("construct progress event: {any}", .{e});
|
||||
};
|
||||
|
||||
_ = parser.eventTargetDispatchEvent(
|
||||
@as(*parser.EventTarget, @ptrCast(self)),
|
||||
@as(*parser.Event, @ptrCast(&evt)),
|
||||
) catch |e| {
|
||||
return log.err("dispatch progress event: {any}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
const methods = [_]struct {
|
||||
tag: std.http.Method,
|
||||
name: []const u8,
|
||||
}{
|
||||
.{ .tag = .DELETE, .name = "DELETE" },
|
||||
.{ .tag = .GET, .name = "GET" },
|
||||
.{ .tag = .HEAD, .name = "HEAD" },
|
||||
.{ .tag = .OPTIONS, .name = "OPTIONS" },
|
||||
.{ .tag = .POST, .name = "POST" },
|
||||
.{ .tag = .PUT, .name = "PUT" },
|
||||
};
|
||||
const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" };
|
||||
|
||||
pub fn validMethod(m: []const u8) DOMError!std.http.Method {
|
||||
for (methods) |method| {
|
||||
if (std.ascii.eqlIgnoreCase(method.name, m)) {
|
||||
return method.tag;
|
||||
}
|
||||
}
|
||||
// If method is a forbidden method, then throw a "SecurityError" DOMException.
|
||||
for (methods_forbidden) |method| {
|
||||
if (std.ascii.eqlIgnoreCase(method, m)) {
|
||||
return DOMError.Security;
|
||||
}
|
||||
}
|
||||
|
||||
// If method is not a method, then throw a "SyntaxError" DOMException.
|
||||
return DOMError.Syntax;
|
||||
}
|
||||
|
||||
pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void {
|
||||
if (self.state != OPENED) return DOMError.InvalidState;
|
||||
if (self.send_flag) return DOMError.InvalidState;
|
||||
return try self.headers.append(name, value);
|
||||
}
|
||||
|
||||
// TODO body can be either a XMLHttpRequestBodyInit or a document
|
||||
pub fn _send(self: *XMLHttpRequest, alloc: std.mem.Allocator, body: ?[]const u8) !void {
|
||||
if (self.state != OPENED) return DOMError.InvalidState;
|
||||
if (self.send_flag) return DOMError.InvalidState;
|
||||
|
||||
// The body argument provides the request body, if any, and is ignored
|
||||
// if the request method is GET or HEAD.
|
||||
// https://xhr.spec.whatwg.org/#the-send()-method
|
||||
// var used_body: ?XMLHttpRequestBodyInit = null;
|
||||
if (body != null and self.method != .GET and self.method != .HEAD) {
|
||||
// TODO If body is a Document, then set this’s request body to body, serialized, converted, and UTF-8 encoded.
|
||||
|
||||
const body_init = XMLHttpRequestBodyInit{ .String = body.? };
|
||||
|
||||
// keep the user content type from request headers.
|
||||
if (self.headers.getFirstEntry("Content-Type") == null) {
|
||||
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
|
||||
try self.headers.append("Content-Type", try body_init.contentType());
|
||||
}
|
||||
|
||||
// copy the payload
|
||||
if (self.payload) |v| alloc.free(v);
|
||||
self.payload = try body_init.dupe(alloc);
|
||||
}
|
||||
|
||||
log.debug("{any} {any}", .{ self.method, self.uri });
|
||||
|
||||
self.send_flag = true;
|
||||
self.impl.yield(self);
|
||||
}
|
||||
|
||||
// onYield is a callback called between each request's steps.
|
||||
// Between each step, the code is blocking.
|
||||
// Yielding allows pseudo-async and gives a chance to other async process
|
||||
// to be called.
|
||||
pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void {
|
||||
if (err) |e| return self.onErr(e);
|
||||
|
||||
switch (self.priv_state) {
|
||||
.new => {
|
||||
self.priv_state = .open;
|
||||
self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onErr(e);
|
||||
},
|
||||
.open => {
|
||||
// prepare payload transfert.
|
||||
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
|
||||
|
||||
self.priv_state = .send;
|
||||
self.req.?.send(.{}) catch |e| return self.onErr(e);
|
||||
},
|
||||
.send => {
|
||||
if (self.payload) |payload| {
|
||||
self.priv_state = .write;
|
||||
self.req.?.writeAll(payload) catch |e| return self.onErr(e);
|
||||
} else {
|
||||
self.priv_state = .finish;
|
||||
self.req.?.finish() catch |e| return self.onErr(e);
|
||||
}
|
||||
},
|
||||
.write => {
|
||||
self.priv_state = .finish;
|
||||
self.req.?.finish() catch |e| return self.onErr(e);
|
||||
},
|
||||
.finish => {
|
||||
self.priv_state = .wait;
|
||||
self.req.?.wait() catch |e| return self.onErr(e);
|
||||
},
|
||||
.wait => {
|
||||
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
|
||||
|
||||
self.priv_state = .done;
|
||||
self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onErr(e);
|
||||
|
||||
// extract a mime type from headers.
|
||||
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
|
||||
self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e);
|
||||
|
||||
// TODO handle override mime type
|
||||
|
||||
self.state = HEADERS_RECEIVED;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
self.response_status = @intFromEnum(self.req.?.response.status);
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
|
||||
// TODO set correct length
|
||||
const total = 0;
|
||||
var loaded: u64 = 0;
|
||||
|
||||
// dispatch a progress event loadstart.
|
||||
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
|
||||
|
||||
const reader = self.req.?.reader();
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
ln = reader.read(&buffer) catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
loaded = loaded + ln;
|
||||
|
||||
// TODO dispatch only if 50ms have passed.
|
||||
|
||||
self.state = LOADING;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
// dispatch a progress event progress.
|
||||
self.dispatchProgressEvent("progress", .{
|
||||
.loaded = loaded,
|
||||
.total = total,
|
||||
});
|
||||
}
|
||||
self.response_bytes = buf.items;
|
||||
self.send_flag = false;
|
||||
|
||||
self.state = DONE;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
// dispatch a progress event load.
|
||||
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
|
||||
// dispatch a progress event loadend.
|
||||
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
|
||||
},
|
||||
.done => {
|
||||
if (self.req) |*r| {
|
||||
r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
// finalize fetch process.
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
self.impl.yield(self);
|
||||
}
|
||||
|
||||
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
|
||||
self.priv_state = .done;
|
||||
if (self.req) |*r| {
|
||||
r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
self.err = err;
|
||||
self.state = DONE;
|
||||
self.send_flag = false;
|
||||
self.dispatchEvt("readystatechange");
|
||||
self.dispatchProgressEvent("error", .{});
|
||||
self.dispatchProgressEvent("loadend", .{});
|
||||
|
||||
log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err });
|
||||
}
|
||||
|
||||
pub fn _abort(self: *XMLHttpRequest) void {
|
||||
self.onErr(DOMError.Abort);
|
||||
}
|
||||
|
||||
pub fn get_responseType(self: *XMLHttpRequest) []const u8 {
|
||||
return switch (self.response_type) {
|
||||
.Empty => "",
|
||||
.ArrayBuffer => "arraybuffer",
|
||||
.Blob => "blob",
|
||||
.Document => "document",
|
||||
.JSON => "json",
|
||||
.Text => "text",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_responseType(self: *XMLHttpRequest, rtype: []const u8) !void {
|
||||
if (self.state == LOADING or self.state == DONE) return DOMError.InvalidState;
|
||||
|
||||
if (std.mem.eql(u8, rtype, "")) {
|
||||
self.response_type = .Empty;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, rtype, "arraybuffer")) {
|
||||
self.response_type = .ArrayBuffer;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, rtype, "blob")) {
|
||||
self.response_type = .Blob;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, rtype, "document")) {
|
||||
self.response_type = .Document;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, rtype, "json")) {
|
||||
self.response_type = .JSON;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, rtype, "text")) {
|
||||
self.response_type = .Text;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO retrieve the redirected url
|
||||
pub fn get_responseURL(self: *XMLHttpRequest) ?[]const u8 {
|
||||
return self.url;
|
||||
}
|
||||
|
||||
pub fn get_responseXML(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
|
||||
if (self.response_type != .Empty and self.response_type != .Document) {
|
||||
return DOMError.InvalidState;
|
||||
}
|
||||
|
||||
if (self.state != DONE) return null;
|
||||
|
||||
// fastpath if response is previously parsed.
|
||||
if (self.response_obj) |obj| {
|
||||
return switch (obj) {
|
||||
.Failure => null,
|
||||
.Document => |v| .{ .Document = v },
|
||||
.JSON => null,
|
||||
};
|
||||
}
|
||||
|
||||
self.setResponseObjDocument(alloc);
|
||||
|
||||
if (self.response_obj) |obj| {
|
||||
return switch (obj) {
|
||||
.Failure => null,
|
||||
.Document => |v| .{ .Document = v },
|
||||
.JSON => null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://xhr.spec.whatwg.org/#the-response-attribute
|
||||
pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
|
||||
if (self.response_type == .Empty or self.response_type == .Text) {
|
||||
if (self.state == LOADING or self.state == DONE) return .{ .Text = "" };
|
||||
return .{ .Text = try self.get_responseText() };
|
||||
}
|
||||
|
||||
// fastpath if response is previously parsed.
|
||||
if (self.response_obj) |obj| {
|
||||
return switch (obj) {
|
||||
.Failure => null,
|
||||
.Document => |v| .{ .Document = v },
|
||||
.JSON => |v| .{ .JSON = v.value },
|
||||
};
|
||||
}
|
||||
|
||||
if (self.response_type == .ArrayBuffer) {
|
||||
// TODO If this’s response type is "arraybuffer", then set this’s
|
||||
// response object to a new ArrayBuffer object representing this’s
|
||||
// received bytes. If this throws an exception, then set this’s
|
||||
// response object to failure and return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (self.response_type == .Blob) {
|
||||
// TODO Otherwise, if this’s response type is "blob", set this’s
|
||||
// response object to a new Blob object representing this’s
|
||||
// received bytes with type set to the result of get a final MIME
|
||||
// type for this.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise, if this’s response type is "document", set a
|
||||
// document response for this.
|
||||
if (self.response_type == .Document) {
|
||||
self.setResponseObjDocument(alloc);
|
||||
}
|
||||
|
||||
if (self.response_type == .JSON) {
|
||||
if (self.response_bytes == null) return null;
|
||||
|
||||
// TODO Let jsonObject be the result of running parse JSON from bytes
|
||||
// on this’s received bytes. If that threw an exception, then return
|
||||
// null.
|
||||
self.setResponseObjJSON(alloc);
|
||||
}
|
||||
|
||||
if (self.response_obj) |obj| {
|
||||
return switch (obj) {
|
||||
.Failure => null,
|
||||
.Document => |v| .{ .Document = v },
|
||||
.JSON => |v| .{ .JSON = v.value },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// setResponseObjDocument parses the received bytes as HTML document and
|
||||
// stores the result into response_obj.
|
||||
// If the par sing fails, a Failure is stored in response_obj.
|
||||
// TODO parse XML.
|
||||
// https://xhr.spec.whatwg.org/#response-object
|
||||
fn setResponseObjDocument(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
|
||||
const isHTML = self.response_mime.eql(Mime.HTML);
|
||||
|
||||
// TODO If finalMIME is not an HTML MIME type or an XML MIME type, then
|
||||
// return.
|
||||
if (!isHTML) return;
|
||||
|
||||
const ccharset = alloc.dupeZ(u8, self.response_mime.charset orelse "utf-8") catch {
|
||||
self.response_obj = .{ .Failure = true };
|
||||
return;
|
||||
};
|
||||
defer alloc.free(ccharset);
|
||||
|
||||
var fbs = std.io.fixedBufferStream(self.response_bytes.?);
|
||||
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch {
|
||||
self.response_obj = .{ .Failure = true };
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO Set document’s URL to xhr’s response’s URL.
|
||||
// TODO Set document’s origin to xhr’s relevant settings object’s origin.
|
||||
|
||||
self.response_obj = .{
|
||||
.Document = parser.documentHTMLToDocument(doc),
|
||||
};
|
||||
}
|
||||
|
||||
// setResponseObjJSON parses the received bytes as a std.json.Value.
|
||||
fn setResponseObjJSON(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
|
||||
// TODO should we use parseFromSliceLeaky if we expect the allocator is
|
||||
// already an arena?
|
||||
const p = std.json.parseFromSlice(
|
||||
JSONValue,
|
||||
alloc,
|
||||
self.response_bytes.?,
|
||||
.{},
|
||||
) catch |e| {
|
||||
log.err("parse JSON: {}", .{e});
|
||||
self.response_obj = .{ .Failure = true };
|
||||
return;
|
||||
};
|
||||
|
||||
self.response_obj = .{ .JSON = p };
|
||||
}
|
||||
|
||||
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
|
||||
if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState;
|
||||
|
||||
return if (self.response_bytes) |v| v else "";
|
||||
}
|
||||
|
||||
pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 {
|
||||
return self.response_headers.getFirstValue(name);
|
||||
}
|
||||
|
||||
// The caller owns the string returned.
|
||||
// TODO change the return type to express the string ownership and let
|
||||
// jsruntime free the string once copied to v8.
|
||||
// see https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 {
|
||||
if (self.response_headers.list.items.len == 0) return "";
|
||||
self.response_headers.sort();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
errdefer buf.deinit(alloc);
|
||||
|
||||
const w = buf.writer(alloc);
|
||||
|
||||
for (self.response_headers.list.items) |entry| {
|
||||
if (entry.value.len == 0) continue;
|
||||
|
||||
try w.writeAll(entry.name);
|
||||
try w.writeAll(": ");
|
||||
try w.writeAll(entry.value);
|
||||
try w.writeAll("\r\n");
|
||||
}
|
||||
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_status(self: *XMLHttpRequest) u16 {
|
||||
return self.response_status;
|
||||
}
|
||||
|
||||
pub fn get_statusText(self: *XMLHttpRequest) []const u8 {
|
||||
if (self.response_status == 0) return "";
|
||||
|
||||
return std.http.Status.phrase(@enumFromInt(self.response_status)) orelse "";
|
||||
}
|
||||
};
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var send = [_]Case{
|
||||
.{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", .ex = "undefined" },
|
||||
.{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" },
|
||||
|
||||
.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
|
||||
// Getter returning a callback crashes.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/200
|
||||
// .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" },
|
||||
//.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
|
||||
|
||||
.{ .src = "req.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
|
||||
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
|
||||
|
||||
// ensure open resets values
|
||||
.{ .src = "req.status", .ex = "0" },
|
||||
.{ .src = "req.statusText", .ex = "" },
|
||||
.{ .src = "req.getAllResponseHeaders()", .ex = "" },
|
||||
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "null" },
|
||||
.{ .src = "req.responseText", .ex = "" },
|
||||
|
||||
.{ .src = "req.send(); nb", .ex = "0" },
|
||||
|
||||
// Each case executed waits for all loop callaback calls.
|
||||
// So the url has been retrieved.
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt.type", .ex = "load" },
|
||||
.{ .src = "evt.loaded > 0", .ex = "true" },
|
||||
.{ .src = "evt instanceof ProgressEvent", .ex = "true" },
|
||||
.{ .src = "req.status", .ex = "200" },
|
||||
.{ .src = "req.statusText", .ex = "OK" },
|
||||
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" },
|
||||
.{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" },
|
||||
.{ .src = "req.responseText.length > 64", .ex = "true" },
|
||||
.{ .src = "req.response", .ex = "" },
|
||||
.{ .src = "req.responseXML instanceof Document", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &send);
|
||||
|
||||
var document = [_]Case{
|
||||
.{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req2.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
|
||||
.{ .src = "req2.responseType = 'document'", .ex = "document" },
|
||||
|
||||
.{ .src = "req2.send()", .ex = "undefined" },
|
||||
|
||||
// Each case executed waits for all loop callaback calls.
|
||||
// So the url has been retrieved.
|
||||
.{ .src = "req2.status", .ex = "200" },
|
||||
.{ .src = "req2.statusText", .ex = "OK" },
|
||||
.{ .src = "req2.response instanceof Document", .ex = "true" },
|
||||
.{ .src = "req2.responseXML instanceof Document", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &document);
|
||||
|
||||
// var json = [_]Case{
|
||||
// .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
// .{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
|
||||
// .{ .src = "req3.responseType = 'json'", .ex = "json" },
|
||||
|
||||
// .{ .src = "req3.send()", .ex = "undefined" },
|
||||
|
||||
// // Each case executed waits for all loop callaback calls.
|
||||
// // So the url has been retrieved.
|
||||
// .{ .src = "req3.status", .ex = "200" },
|
||||
// .{ .src = "req3.statusText", .ex = "OK" },
|
||||
// .{ .src = "req3.response", .ex = "" },
|
||||
// };
|
||||
// try checkCases(js_env, &json);
|
||||
//
|
||||
var post = [_]Case{
|
||||
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req3.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
|
||||
.{ .src = "req3.send('foo')", .ex = "undefined" },
|
||||
|
||||
// Each case executed waits for all loop callaback calls.
|
||||
// So the url has been retrieved.
|
||||
.{ .src = "req3.status", .ex = "200" },
|
||||
.{ .src = "req3.statusText", .ex = "OK" },
|
||||
.{ .src = "req3.responseText.length > 64", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &post);
|
||||
}
|
||||
2
vendor/jsruntime-lib
vendored
2
vendor/jsruntime-lib
vendored
Submodule vendor/jsruntime-lib updated: 8fe165bc49...2d7b816f48
Reference in New Issue
Block a user