1 Commits

Author SHA1 Message Date
Pierre Tachoire
58ca323918 window: implement a debug() function
In order to ease debug log in js world
2024-11-28 16:31:30 +01:00
22 changed files with 249 additions and 1150 deletions

4
.gitmodules vendored
View File

@@ -28,7 +28,3 @@
[submodule "vendor/zig-async-io"] [submodule "vendor/zig-async-io"]
path = vendor/zig-async-io path = vendor/zig-async-io
url = git@github.com:lightpanda-io/zig-async-io.git url = git@github.com:lightpanda-io/zig-async-io.git
[submodule "vendor/websocket.zig"]
path = vendor/websocket.zig
url = git@github.com:lightpanda-io/websocket.zig.git
branch = lightpanda

View File

@@ -11,7 +11,6 @@ The following files are licensed under MIT:
``` ```
src/http/Client.zig src/http/Client.zig
src/polyfill/fetch.js
``` ```
The following directories and their subdirectories are licensed under their The following directories and their subdirectories are licensed under their

View File

@@ -2,9 +2,7 @@
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a> <a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
</p> </p>
<h1 align="center">Lightpanda Browser</h1> <h1 align="center">Lightpanda</h1>
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
<div align="center"> <div align="center">
<br /> <br />
@@ -18,10 +16,10 @@ Lightpanda is the open-source browser made for headless usage:
Fast scraping and web automation with minimal memory footprint: Fast scraping and web automation with minimal memory footprint:
- Ultra-low memory footprint (9x less than Chrome) - Ultra-low memory footprint (12x less than Chrome)
- Blazingly fast execution (11x faster than Chrome) & instant startup - Blazingly fast & instant startup (64x faster than Chrome)
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png"> <img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark2.png">
See [benchmark details](https://github.com/lightpanda-io/demo). See [benchmark details](https://github.com/lightpanda-io/demo).
@@ -36,7 +34,7 @@ Back in the good old times, grabbing a webpage was as easy as making an HTTP req
### Chrome is not the right tool ### Chrome is not the right tool
So if we need Javascript, why not use a real web browser. Lets take a huge desktop application, hack it, and run it on the server, right? Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure its such a good idea? So if we need Javascript, why not use a real web browser. Lets take a huge desktop application, hack it, and run it on the server, right? Hundreds of instance of Chrome if you use it at scale. Are you sure its such a good idea?
- Heavy on RAM and CPU, expensive to run - Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale - Hard to package, deploy and maintain at scale
@@ -48,34 +46,34 @@ If we want both Javascript and performance, for a real headless browser, we need
- Not based on Chromium, Blink or WebKit - Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind - Low-level system programming language (Zig) with optimisations in mind
- Opinionated, without graphical rendering - Opinionated, no rendering
## Status ## Status
Lightpanda is still a work in progress and is currently at a Beta stage. Lightpanda is still a work in progress and is currently at the Alpha stage.
Here are the key features we have implemented: Here are the key features we want to implement before releasing a Beta version:
- [x] HTTP loader - [x] Loader
- [x] HTML parser and DOM tree - [x] HTML parser and DOM tree
- [x] Javascript support (v8) - [x] Javascript support
- [x] Basic DOM APIs - [x] Basic DOM APIs
- [x] Ajax - [x] Ajax
- [x] XHR API - [x] XHR API
- [x] Fetch API - [ ] Fetch API
- [x] DOM dump - [x] DOM dump
- [x] Basic CDP/websockets server - [ ] Basic CDP server
We will not provide binary versions until we reach at least the Beta stage.
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation. NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project. You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
## Install
We do provide [nighly builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for Linux x86_64 and MacOS aarch64.
## Build from sources ## Build from sources
We do not provide yet binary versions of Lightpanda, you have to compile it from source.
### Prerequisites ### Prerequisites
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to

View File

@@ -168,11 +168,6 @@ fn common(
.root_source_file = b.path("vendor/tls.zig/src/main.zig"), .root_source_file = b.path("vendor/tls.zig/src/main.zig"),
}); });
step.root_module.addImport("tls", tlsmod); step.root_module.addImport("tls", tlsmod);
const wsmod = b.addModule("websocket", .{
.root_source_file = b.path("vendor/websocket.zig/src/websocket.zig"),
});
step.root_module.addImport("websocket", wsmod);
} }
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module { fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {

View File

@@ -42,8 +42,6 @@ const FetchResult = @import("../http/Client.zig").Client.FetchResult;
const UserContext = @import("../user_context.zig").UserContext; const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("asyncio").Client; const HttpClient = @import("asyncio").Client;
const polyfill = @import("../polyfill/polyfill.zig");
const log = std.log.scoped(.browser); const log = std.log.scoped(.browser);
// Browser is an instance of the browser. // Browser is an instance of the browser.
@@ -357,9 +355,6 @@ pub const Page = struct {
log.debug("start js env", .{}); log.debug("start js env", .{});
try self.session.env.start(); try self.session.env.start();
// load polyfills
try polyfill.load(alloc, self.session.env);
// inspector // inspector
if (self.session.inspector) |inspector| { if (self.session.inspector) |inspector| {
inspector.contextCreated(self.session.env, "", self.origin.?, auxData); inspector.contextCreated(self.session.env, "", self.origin.?, auxData);

View File

@@ -193,7 +193,7 @@ pub fn sendEvent(
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID }; const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
const event_msg = try stringify(alloc, resp); const event_msg = try stringify(alloc, resp);
try ctx.send(event_msg); try server.sendAsync(ctx, event_msg);
} }
// Common // Common

View File

@@ -323,7 +323,7 @@ fn navigate(
.loaderId = ctx.state.loaderID, .loaderId = ctx.state.loaderID,
}; };
const res = try result(alloc, input.id, Resp, resp, input.sessionId); const res = try result(alloc, input.id, Resp, resp, input.sessionId);
try ctx.send(res); try server.sendAsync(ctx, res);
// TODO: at this point do we need async the following actions to be async? // TODO: at this point do we need async the following actions to be async?

View File

@@ -292,7 +292,7 @@ fn disposeBrowserContext(
// output // output
const res = try result(alloc, input.id, null, .{}, null); const res = try result(alloc, input.id, null, .{}, null);
try ctx.send(res); try server.sendAsync(ctx, res);
return error.DisposeBrowserContext; return error.DisposeBrowserContext;
} }
@@ -378,7 +378,7 @@ fn closeTarget(
success: bool = true, success: bool = true,
}; };
const res = try result(alloc, input.id, Resp, Resp{}, null); const res = try result(alloc, input.id, Resp, Resp{}, null);
try ctx.send(res); try server.sendAsync(ctx, res);
// Inspector.detached event // Inspector.detached event
const InspectorDetached = struct { const InspectorDetached = struct {

View File

@@ -1,95 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 ws = @import("websocket");
const Msg = @import("msg.zig").Msg;
const log = std.log.scoped(.handler);
pub const Stream = struct {
addr: std.net.Address,
socket: std.posix.socket_t = undefined,
ws_host: []const u8,
ws_port: u16,
ws_conn: *ws.Conn = undefined,
fn connectCDP(self: *Stream) !void {
const flags: u32 = std.posix.SOCK.STREAM;
const proto = blk: {
if (self.addr.any.family == std.posix.AF.UNIX) break :blk @as(u32, 0);
break :blk std.posix.IPPROTO.TCP;
};
const socket = try std.posix.socket(self.addr.any.family, flags, proto);
try std.posix.connect(
socket,
&self.addr.any,
self.addr.getOsSockLen(),
);
log.debug("connected to Stream server", .{});
self.socket = socket;
}
fn closeCDP(self: *const Stream) void {
const close_msg: []const u8 = .{ 5, 0 } ++ "close";
self.recv(close_msg) catch |err| {
log.err("stream close error: {any}", .{err});
};
std.posix.close(self.socket);
}
fn start(self: *Stream, ws_conn: *ws.Conn) !void {
try self.connectCDP();
self.ws_conn = ws_conn;
}
pub fn recv(self: *const Stream, data: []const u8) !void {
var pos: usize = 0;
while (pos < data.len) {
const len = try std.posix.write(self.socket, data[pos..]);
pos += len;
}
}
pub fn send(self: *const Stream, data: []const u8) !void {
return self.ws_conn.write(data);
}
};
pub const Handler = struct {
stream: *Stream,
pub fn init(_: ws.Handshake, ws_conn: *ws.Conn, stream: *Stream) !Handler {
try stream.start(ws_conn);
return .{ .stream = stream };
}
pub fn close(self: *Handler) void {
self.stream.closeCDP();
}
pub fn clientMessage(self: *Handler, data: []const u8) !void {
var header: [2]u8 = undefined;
Msg.setSize(data.len, &header);
try self.stream.recv(&header);
try self.stream.recv(data);
}
};

View File

@@ -26,7 +26,6 @@ const checkCases = jsruntime.test_utils.checkCases;
const Element = @import("../dom/element.zig").Element; const Element = @import("../dom/element.zig").Element;
const URL = @import("../url/url.zig").URL; const URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node;
// HTMLElement interfaces // HTMLElement interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -118,25 +117,6 @@ pub const HTMLElement = struct {
pub fn get_style(_: *parser.ElementHTML) CSSProperties { pub fn get_style(_: *parser.ElementHTML) CSSProperties {
return .{}; return .{};
} }
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
const n = @as(*parser.Node, @ptrCast(e));
return try parser.nodeTextContent(n) orelse "";
}
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
const n = @as(*parser.Node, @ptrCast(e));
// create text node.
const doc = try parser.nodeOwnerDocument(n) orelse return error.NoDocument;
const t = try parser.documentCreateTextNode(doc, s);
// remove existing children.
try Node.removeChildren(n);
// attach the text node.
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
}
}; };
// Deprecated HTMLElements in Chrome (2023/03/15) // Deprecated HTMLElements in Chrome (2023/03/15)
@@ -1088,12 +1068,4 @@ pub fn testExecFn(
.{ .src = "script.async", .ex = "false" }, .{ .src = "script.async", .ex = "false" },
}; };
try checkCases(js_env, &script); try checkCases(js_env, &script);
var innertext = [_]Case{
.{ .src = "const backup = document.getElementById('content')", .ex = "undefined" },
.{ .src = "document.getElementById('content').innerText = 'foo';", .ex = "foo" },
.{ .src = "document.getElementById('content').innerText", .ex = "foo" },
.{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" },
};
try checkCases(js_env, &innertext);
} }

View File

@@ -28,6 +28,8 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget;
const storage = @import("../storage/storage.zig"); const storage = @import("../storage/storage.zig");
const log = std.log.scoped(.window);
// https://dom.spec.whatwg.org/#interface-window-extensions // https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct { pub const Window = struct {
@@ -66,6 +68,10 @@ pub const Window = struct {
return self; return self;
} }
pub fn _debug(_: *Window, str: []const u8) void {
log.debug("{s}", .{str});
}
pub fn get_self(self: *Window) *Window { pub fn get_self(self: *Window) *Window {
return self; return self;
} }

View File

@@ -17,15 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const posix = std.posix;
const builtin = @import("builtin"); const builtin = @import("builtin");
const jsruntime = @import("jsruntime"); const jsruntime = @import("jsruntime");
const websocket = @import("websocket");
const Browser = @import("browser/browser.zig").Browser; const Browser = @import("browser/browser.zig").Browser;
const server = @import("server.zig"); const server = @import("server.zig");
const handler = @import("handler.zig");
const MaxSize = @import("msg.zig").MaxSize;
const parser = @import("netsurf"); const parser = @import("netsurf");
const apiweb = @import("apiweb.zig"); const apiweb = @import("apiweb.zig");
@@ -34,12 +32,103 @@ pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext; pub const UserContext = apiweb.UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop); pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
// Simple blocking websocket connection model
// ie. 1 thread per ws connection without thread pool and epoll/kqueue
pub const websocket_blocking = true;
const log = std.log.scoped(.cli); const log = std.log.scoped(.cli);
// Inspired by std.net.StreamServer in Zig < 0.12
pub const StreamServer = struct {
/// Copied from `Options` on `init`.
kernel_backlog: u31,
reuse_address: bool,
reuse_port: bool,
nonblocking: bool,
/// `undefined` until `listen` returns successfully.
listen_address: std.net.Address,
sockfd: ?posix.socket_t,
pub const Options = struct {
/// How many connections the kernel will accept on the application's behalf.
/// If more than this many connections pool in the kernel, clients will start
/// seeing "Connection refused".
kernel_backlog: u31 = 128,
/// Enable SO.REUSEADDR on the socket.
reuse_address: bool = false,
/// Enable SO.REUSEPORT on the socket.
reuse_port: bool = false,
/// Non-blocking mode.
nonblocking: bool = false,
};
/// After this call succeeds, resources have been acquired and must
/// be released with `deinit`.
pub fn init(options: Options) StreamServer {
return StreamServer{
.sockfd = null,
.kernel_backlog = options.kernel_backlog,
.reuse_address = options.reuse_address,
.reuse_port = options.reuse_port,
.nonblocking = options.nonblocking,
.listen_address = undefined,
};
}
/// Release all resources. The `StreamServer` memory becomes `undefined`.
pub fn deinit(self: *StreamServer) void {
self.close();
self.* = undefined;
}
fn setSockOpt(fd: posix.socket_t, level: i32, option: u32, value: c_int) !void {
try posix.setsockopt(fd, level, option, &std.mem.toBytes(value));
}
pub fn listen(self: *StreamServer, address: std.net.Address) !void {
const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
var use_sock_flags: u32 = sock_flags;
if (self.nonblocking) use_sock_flags |= posix.SOCK.NONBLOCK;
const proto = if (address.any.family == posix.AF.UNIX) @as(u32, 0) else posix.IPPROTO.TCP;
const sockfd = try posix.socket(address.any.family, use_sock_flags, proto);
self.sockfd = sockfd;
errdefer {
posix.close(sockfd);
self.sockfd = null;
}
// socket options
if (self.reuse_address) {
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
}
if (@hasDecl(posix.SO, "REUSEPORT") and self.reuse_port) {
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1);
}
if (builtin.target.os.tag == .linux) { // posix.TCP not available on MacOS
// WARNING: disable Nagle's alogrithm to avoid latency issues
try setSockOpt(sockfd, posix.IPPROTO.TCP, posix.TCP.NODELAY, 1);
}
var socklen = address.getOsSockLen();
try posix.bind(sockfd, &address.any, socklen);
try posix.listen(sockfd, self.kernel_backlog);
try posix.getsockname(sockfd, &self.listen_address.any, &socklen);
}
/// Stop listening. It is still necessary to call `deinit` after stopping listening.
/// Calling `deinit` will automatically call `close`. It is safe to call `close` when
/// not listening.
pub fn close(self: *StreamServer) void {
if (self.sockfd) |fd| {
posix.close(fd);
self.sockfd = null;
self.listen_address = undefined;
}
}
};
const usage = const usage =
\\usage: {s} [options] [URL] \\usage: {s} [options] [URL]
\\ \\
@@ -50,7 +139,7 @@ const usage =
\\ \\
\\ -h, --help Print this help message and exit. \\ -h, --help Print this help message and exit.
\\ --host Host of the CDP server (default "127.0.0.1") \\ --host Host of the CDP server (default "127.0.0.1")
\\ --port Port of the CDP server (default "9222") \\ --port Port of the CDP server (default "3245")
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3") \\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
\\ --dump Dump document in stdout (fetch mode only) \\ --dump Dump document in stdout (fetch mode only)
\\ \\
@@ -81,11 +170,10 @@ const CliMode = union(CliModeTag) {
host: []const u8 = Host, host: []const u8 = Host,
port: u16 = Port, port: u16 = Port,
timeout: u8 = Timeout, timeout: u8 = Timeout,
tcp: bool = false, // undocumented TCP mode
// default options // default options
const Host = "127.0.0.1"; const Host = "127.0.0.1";
const Port = 9222; const Port = 3245;
const Timeout = 3; // in seconds const Timeout = 3; // in seconds
}; };
@@ -147,10 +235,6 @@ const CliMode = union(CliModeTag) {
return printUsageExit(execname, 1); return printUsageExit(execname, 1);
} }
} }
if (std.mem.eql(u8, "--tcp", opt)) {
_server.tcp = true;
continue;
}
// unknown option // unknown option
if (std.mem.startsWith(u8, opt, "--")) { if (std.mem.startsWith(u8, opt, "--")) {
@@ -233,70 +317,33 @@ pub fn main() !void {
defer cli_mode.deinit(); defer cli_mode.deinit();
switch (cli_mode) { switch (cli_mode) {
.server => |opts| { .server => |mode| {
// Stream server // server
const addr = blk: { var srv = StreamServer.init(.{
if (opts.tcp) { .reuse_address = true,
break :blk opts.addr; .reuse_port = true,
} else { .nonblocking = true,
const unix_path = "/tmp/lightpanda"; });
std.fs.deleteFileAbsolute(unix_path) catch {}; // file could not exists defer srv.deinit();
break :blk try std.net.Address.initUnix(unix_path);
}
};
const socket = server.listen(addr) catch |err| {
log.err("Server listen error: {any}\n", .{err});
return printUsageExit(opts.execname, 1);
};
defer std.posix.close(socket);
log.debug("Server opts: listening internally on {any}...", .{addr});
const timeout = std.time.ns_per_s * @as(u64, opts.timeout); srv.listen(mode.addr) catch |err| {
log.err("address (host:port) {any}\n", .{err});
return printUsageExit(mode.execname, 1);
};
defer srv.close();
log.info("Server mode: listening on {s}:{d}...", .{ mode.host, mode.port });
// loop // loop
var loop = try jsruntime.Loop.init(alloc); var loop = try jsruntime.Loop.init(alloc);
defer loop.deinit(); defer loop.deinit();
// TCP server mode // listen
if (opts.tcp) { try server.listen(alloc, &loop, srv.sockfd.?, std.time.ns_per_s * @as(u64, mode.timeout));
return server.handle(alloc, &loop, socket, null, timeout);
}
// start stream server in separate thread
var stream = handler.Stream{
.ws_host = opts.host,
.ws_port = opts.port,
.addr = addr,
};
const cdp_thread = try std.Thread.spawn(
.{ .allocator = alloc },
server.handle,
.{ alloc, &loop, socket, &stream, timeout },
);
// Websocket server
var ws = try websocket.Server(handler.Handler).init(alloc, .{
.port = opts.port,
.address = opts.host,
.max_message_size = MaxSize + 14, // overhead websocket
.max_conn = 1,
.handshake = .{
.timeout = 3,
.max_size = 1024,
// since we aren't using hanshake.headers
// we can set this to 0 to save a few bytes.
.max_headers = 0,
},
});
defer ws.deinit();
try ws.listen(&stream);
cdp_thread.join();
}, },
.fetch => |opts| { .fetch => |mode| {
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump }); log.debug("Fetch mode: url {s}, dump {any}", .{ mode.url, mode.dump });
// vm // vm
const vm = jsruntime.VM.init(); const vm = jsruntime.VM.init();
@@ -314,21 +361,21 @@ pub fn main() !void {
// page // page
const page = try browser.session.createPage(); const page = try browser.session.createPage();
_ = page.navigate(opts.url, null) catch |err| switch (err) { _ = page.navigate(mode.url, null) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => { error.UnsupportedUriScheme, error.UriMissingHost => {
log.err("'{s}' is not a valid URL ({any})\n", .{ opts.url, err }); log.err("'{s}' is not a valid URL ({any})\n", .{ mode.url, err });
return printUsageExit(opts.execname, 1); return printUsageExit(mode.execname, 1);
}, },
else => { else => {
log.err("'{s}' fetching error ({any})s\n", .{ opts.url, err }); log.err("'{s}' fetching error ({any})s\n", .{ mode.url, err });
return printUsageExit(opts.execname, 1); return printUsageExit(mode.execname, 1);
}, },
}; };
try page.wait(); try page.wait();
// dump // dump
if (opts.dump) { if (mode.dump) {
try page.dump(std.io.getStdOut()); try page.dump(std.io.getStdOut());
} }
}, },

View File

@@ -18,47 +18,44 @@
const std = @import("std"); const std = @import("std");
pub const MsgSize = 16 * 1204; // 16KB /// MsgBuffer returns messages from a raw text read stream,
pub const HeaderSize = 2; /// according to the following format `<msg_size>:<msg>`.
pub const MaxSize = HeaderSize + MsgSize;
pub const Msg = struct {
pub fn getSize(data: []const u8) usize {
return std.mem.readInt(u16, data[0..HeaderSize], .little);
}
pub fn setSize(len: usize, header: *[2]u8) void {
std.mem.writeInt(u16, header, @intCast(len), .little);
}
};
/// Buffer returns messages from a raw text read stream,
/// with the message size being encoded on the 2 first bytes (little endian)
/// It handles both: /// It handles both:
/// - combined messages in one read /// - combined messages in one read
/// - single message in several reads (multipart) /// - single message in several reads (multipart)
/// It's safe (and a good practice) to reuse the same Buffer /// It's safe (and a good practice) to reuse the same MsgBuffer
/// on several reads of the same stream. /// on several reads of the same stream.
pub const Buffer = struct { pub const MsgBuffer = struct {
buf: []u8,
size: usize = 0, size: usize = 0,
buf: []u8,
pos: usize = 0, pos: usize = 0,
fn isFinished(self: *const Buffer) bool { const MaxSize = 1024 * 1024; // 1MB
pub fn init(alloc: std.mem.Allocator, size: usize) std.mem.Allocator.Error!MsgBuffer {
const buf = try alloc.alloc(u8, size);
return .{ .buf = buf };
}
pub fn deinit(self: MsgBuffer, alloc: std.mem.Allocator) void {
alloc.free(self.buf);
}
fn isFinished(self: *MsgBuffer) bool {
return self.pos >= self.size; return self.pos >= self.size;
} }
fn isEmpty(self: *const Buffer) bool { fn isEmpty(self: MsgBuffer) bool {
return self.size == 0 and self.pos == 0; return self.size == 0 and self.pos == 0;
} }
fn reset(self: *Buffer) void { fn reset(self: *MsgBuffer) void {
self.size = 0; self.size = 0;
self.pos = 0; self.pos = 0;
} }
// read input // read input
pub fn read(self: *Buffer, input: []const u8) !struct { pub fn read(self: *MsgBuffer, alloc: std.mem.Allocator, input: []const u8) !struct {
msg: []const u8, msg: []const u8,
left: []const u8, left: []const u8,
} { } {
@@ -67,9 +64,11 @@ pub const Buffer = struct {
// msg size // msg size
var msg_size: usize = undefined; var msg_size: usize = undefined;
if (self.isEmpty()) { if (self.isEmpty()) {
// decode msg size header // parse msg size metadata
msg_size = Msg.getSize(_input); const size_pos = std.mem.indexOfScalar(u8, _input, ':') orelse return error.InputWithoutSize;
_input = _input[HeaderSize..]; const size_str = _input[0..size_pos];
msg_size = try std.fmt.parseInt(u32, size_str, 10);
_input = _input[size_pos + 1 ..];
} else { } else {
msg_size = self.size; msg_size = self.size;
} }
@@ -78,7 +77,7 @@ pub const Buffer = struct {
const is_multipart = !self.isEmpty() or _input.len < msg_size; const is_multipart = !self.isEmpty() or _input.len < msg_size;
if (is_multipart) { if (is_multipart) {
// set msg size on empty Buffer // set msg size on empty MsgBuffer
if (self.isEmpty()) { if (self.isEmpty()) {
self.size = msg_size; self.size = msg_size;
} }
@@ -91,11 +90,19 @@ pub const Buffer = struct {
return error.MsgTooBig; return error.MsgTooBig;
} }
// copy the current input into Buffer // check if the current input can fit in MsgBuffer
// NOTE: we could use @memcpy but it's not Thread-safe (alias problem) if (new_pos > self.buf.len) {
// see https://www.openmymind.net/Zigs-memcpy-copyForwards-and-copyBackwards/ // we want to realloc at least:
// Intead we just use std.mem.copyForwards // - a size big enough to fit the entire input (ie. new_pos)
std.mem.copyForwards(u8, self.buf[self.pos..new_pos], _input[0..]); // - a size big enough (ie. current msg size + starting buffer size)
// to avoid multiple reallocation
const new_size = @max(self.buf.len + self.size, new_pos);
// resize the MsgBuffer to fit
self.buf = try alloc.realloc(self.buf, new_size);
}
// copy the current input into MsgBuffer
@memcpy(self.buf[self.pos..new_pos], _input[0..]);
// set the new cursor position // set the new cursor position
self.pos = new_pos; self.pos = new_pos;
@@ -113,45 +120,47 @@ pub const Buffer = struct {
} }
}; };
test "Buffer" { fn doTest(nb: *u8) void {
nb.* += 1;
}
test "MsgBuffer" {
const Case = struct { const Case = struct {
input: []const u8, input: []const u8,
nb: u8, nb: u8,
}; };
const alloc = std.testing.allocator;
const cases = [_]Case{ const cases = [_]Case{
// simple // simple
.{ .input = .{ 2, 0 } ++ "ok", .nb = 1 }, .{ .input = "2:ok", .nb = 1 },
// combined // combined
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 3, 0 } ++ "foo", .nb = 2 }, .{ .input = "2:ok3:foo7:bar2:ok", .nb = 3 }, // "bar2:ok" is a message, no need to escape "2:" here
// multipart // multipart
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 }, .{ .input = "9:multi", .nb = 0 },
.{ .input = "part", .nb = 1 }, .{ .input = "part", .nb = 1 },
// multipart & combined // multipart & combined
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 }, .{ .input = "9:multi", .nb = 0 },
.{ .input = "part" ++ .{ 2, 0 } ++ "ok", .nb = 2 }, .{ .input = "part2:ok", .nb = 2 },
// multipart & combined with other multipart // multipart & combined with other multipart
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 }, .{ .input = "9:multi", .nb = 0 },
.{ .input = "part" ++ .{ 8, 0 } ++ "co", .nb = 1 }, .{ .input = "part8:co", .nb = 1 },
.{ .input = "mbined", .nb = 1 }, .{ .input = "mbined", .nb = 1 },
// several multipart // several multipart
.{ .input = .{ 23, 0 } ++ "multi", .nb = 0 }, .{ .input = "23:multi", .nb = 0 },
.{ .input = "several", .nb = 0 }, .{ .input = "several", .nb = 0 },
.{ .input = "complex", .nb = 0 }, .{ .input = "complex", .nb = 0 },
.{ .input = "part", .nb = 1 }, .{ .input = "part", .nb = 1 },
// combined & multipart // combined & multipart
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 9, 0 } ++ "multi", .nb = 1 }, .{ .input = "2:ok9:multi", .nb = 1 },
.{ .input = "part", .nb = 1 }, .{ .input = "part", .nb = 1 },
}; };
var msg_buf = try MsgBuffer.init(alloc, 10);
var b: [MaxSize]u8 = undefined; defer msg_buf.deinit(alloc);
var buf = Buffer{ .buf = &b };
for (cases) |case| { for (cases) |case| {
var nb: u8 = 0; var nb: u8 = 0;
var input = case.input; var input: []const u8 = case.input;
while (input.len > 0) { while (input.len > 0) {
const parts = buf.read(input) catch |err| { const parts = msg_buf.read(alloc, input) catch |err| {
if (err == error.MsgMultipart) break; // go to the next case input if (err == error.MsgMultipart) break; // go to the next case input
return err; return err;
}; };

View File

@@ -1,671 +0,0 @@
// fetch.js code comes from
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
//
// The original code source is available in MIT license.
//
// The script comes from the built version from npm.
// You can get the package with the command:
//
// wget $(npm view whatwg-fetch dist.tarball)
//
// The source is the content of `package/dist/fetch.umd.js` file.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.WHATWGFetch = {})));
}(this, (function (exports) { 'use strict';
/* eslint-disable no-prototype-builtins */
var g =
(typeof globalThis !== 'undefined' && globalThis) ||
(typeof self !== 'undefined' && self) ||
// eslint-disable-next-line no-undef
(typeof global !== 'undefined' && global) ||
{};
var support = {
searchParams: 'URLSearchParams' in g,
iterable: 'Symbol' in g && 'iterator' in Symbol,
blob:
'FileReader' in g &&
'Blob' in g &&
(function() {
try {
new Blob();
return true
} catch (e) {
return false
}
})(),
formData: 'FormData' in g,
// Arraybuffer is available but xhr doesn't implement it for now.
// arrayBuffer: 'ArrayBuffer' in g
arrayBuffer: false
};
function isDataView(obj) {
return obj && DataView.prototype.isPrototypeOf(obj)
}
if (support.arrayBuffer) {
var viewClasses = [
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]'
];
var isArrayBufferView =
ArrayBuffer.isView ||
function(obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
};
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = String(name);
}
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
throw new TypeError('Invalid character in header field name: "' + name + '"')
}
return name.toLowerCase()
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = String(value);
}
return value
}
// Build a destructive iterator for the value list
function iteratorFor(items) {
var iterator = {
next: function() {
var value = items.shift();
return {done: value === undefined, value: value}
}
};
if (support.iterable) {
iterator[Symbol.iterator] = function() {
return iterator
};
}
return iterator
}
function Headers(headers) {
this.map = {};
if (headers instanceof Headers) {
headers.forEach(function(value, name) {
this.append(name, value);
}, this);
} else if (Array.isArray(headers)) {
headers.forEach(function(header) {
if (header.length != 2) {
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
}
this.append(header[0], header[1]);
}, this);
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
this.append(name, headers[name]);
}, this);
}
}
Headers.prototype.append = function(name, value) {
name = normalizeName(name);
value = normalizeValue(value);
var oldValue = this.map[name];
this.map[name] = oldValue ? oldValue + ', ' + value : value;
};
Headers.prototype['delete'] = function(name) {
delete this.map[normalizeName(name)];
};
Headers.prototype.get = function(name) {
name = normalizeName(name);
return this.has(name) ? this.map[name] : null
};
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
};
Headers.prototype.set = function(name, value) {
this.map[normalizeName(name)] = normalizeValue(value);
};
Headers.prototype.forEach = function(callback, thisArg) {
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this);
}
}
};
Headers.prototype.keys = function() {
var items = [];
this.forEach(function(value, name) {
items.push(name);
});
return iteratorFor(items)
};
Headers.prototype.values = function() {
var items = [];
this.forEach(function(value) {
items.push(value);
});
return iteratorFor(items)
};
Headers.prototype.entries = function() {
var items = [];
this.forEach(function(value, name) {
items.push([name, value]);
});
return iteratorFor(items)
};
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
}
function consumed(body) {
if (body._noBody) return
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true;
}
function fileReaderReady(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result);
};
reader.onerror = function() {
reject(reader.error);
};
})
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
reader.readAsArrayBuffer(blob);
return promise
}
function readBlobAsText(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
var encoding = match ? match[1] : 'utf-8';
reader.readAsText(blob, encoding);
return promise
}
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf);
var chars = new Array(view.length);
for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i]);
}
return chars.join('')
}
function bufferClone(buf) {
if (buf.slice) {
return buf.slice(0)
} else {
var view = new Uint8Array(buf.byteLength);
view.set(new Uint8Array(buf));
return view.buffer
}
}
function Body() {
this.bodyUsed = false;
this._initBody = function(body) {
/*
fetch-mock wraps the Response object in an ES6 Proxy to
provide useful test harness features such as flush. However, on
ES5 browsers without fetch or Proxy support pollyfills must be used;
the proxy-pollyfill is unable to proxy an attribute unless it exists
on the object before the Proxy is created. This change ensures
Response.bodyUsed exists on the instance, while maintaining the
semantic of setting Request.bodyUsed in the constructor before
_initBody is called.
*/
// eslint-disable-next-line no-self-assign
this.bodyUsed = this.bodyUsed;
this._bodyInit = body;
if (!body) {
this._noBody = true;
this._bodyText = '';
} else if (typeof body === 'string') {
this._bodyText = body;
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body;
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body;
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString();
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer);
// IE 10-11 can't handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer]);
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body);
} else {
this._bodyText = body = Object.prototype.toString.call(body);
}
if (!this.headers.get('content-type')) {
if (typeof body === 'string') {
this.headers.set('content-type', 'text/plain;charset=UTF-8');
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set('content-type', this._bodyBlob.type);
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
}
}
};
if (support.blob) {
this.blob = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
};
}
this.arrayBuffer = function() {
if (this._bodyArrayBuffer) {
var isConsumed = consumed(this);
if (isConsumed) {
return isConsumed
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
return Promise.resolve(
this._bodyArrayBuffer.buffer.slice(
this._bodyArrayBuffer.byteOffset,
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
)
)
} else {
return Promise.resolve(this._bodyArrayBuffer)
}
} else if (support.blob) {
return this.blob().then(readBlobAsArrayBuffer)
} else {
throw new Error('could not read as ArrayBuffer')
}
};
this.text = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
} else {
return Promise.resolve(this._bodyText)
}
};
if (support.formData) {
this.formData = function() {
return this.text().then(decode)
};
}
this.json = function() {
return this.text().then(JSON.parse)
};
return this
}
// HTTP methods whose capitalization should be normalized
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
function normalizeMethod(method) {
var upcased = method.toUpperCase();
return methods.indexOf(upcased) > -1 ? upcased : method
}
function Request(input, options) {
if (!(this instanceof Request)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
options = options || {};
var body = options.body;
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError('Already read')
}
this.url = input.url;
this.credentials = input.credentials;
if (!options.headers) {
this.headers = new Headers(input.headers);
}
this.method = input.method;
this.mode = input.mode;
this.signal = input.signal;
if (!body && input._bodyInit != null) {
body = input._bodyInit;
input.bodyUsed = true;
}
} else {
this.url = String(input);
}
this.credentials = options.credentials || this.credentials || 'same-origin';
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers);
}
this.method = normalizeMethod(options.method || this.method || 'GET');
this.mode = options.mode || this.mode || null;
this.signal = options.signal || this.signal || (function () {
if ('AbortController' in g) {
var ctrl = new AbortController();
return ctrl.signal;
}
}());
this.referrer = null;
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
}
this._initBody(body);
if (this.method === 'GET' || this.method === 'HEAD') {
if (options.cache === 'no-store' || options.cache === 'no-cache') {
// Search for a '_' parameter in the query string
var reParamSearch = /([?&])_=[^&]*/;
if (reParamSearch.test(this.url)) {
// If it already exists then set the value with the current time
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
} else {
// Otherwise add a new '_' parameter to the end with the current time
var reQueryString = /\?/;
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
}
}
}
}
Request.prototype.clone = function() {
return new Request(this, {body: this._bodyInit})
};
function decode(body) {
var form = new FormData();
body
.trim()
.split('&')
.forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=');
var name = split.shift().replace(/\+/g, ' ');
var value = split.join('=').replace(/\+/g, ' ');
form.append(decodeURIComponent(name), decodeURIComponent(value));
}
});
return form
}
function parseHeaders(rawHeaders) {
var headers = new Headers();
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
// https://github.com/github/fetch/issues/748
// https://github.com/zloirock/core-js/issues/751
preProcessedHeaders
.split('\r')
.map(function(header) {
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
})
.forEach(function(line) {
var parts = line.split(':');
var key = parts.shift().trim();
if (key) {
var value = parts.join(':').trim();
try {
headers.append(key, value);
} catch (error) {
console.warn('Response ' + error.message);
}
}
});
return headers
}
Body.call(Request.prototype);
function Response(bodyInit, options) {
if (!(this instanceof Response)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
if (!options) {
options = {};
}
this.type = 'default';
this.status = options.status === undefined ? 200 : options.status;
if (this.status < 200 || this.status > 599) {
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
}
this.ok = this.status >= 200 && this.status < 300;
this.statusText = options.statusText === undefined ? '' : '' + options.statusText;
this.headers = new Headers(options.headers);
this.url = options.url || '';
this._initBody(bodyInit);
}
Body.call(Response.prototype);
Response.prototype.clone = function() {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
})
};
Response.error = function() {
var response = new Response(null, {status: 200, statusText: ''});
response.ok = false;
response.status = 0;
response.type = 'error';
return response
};
var redirectStatuses = [301, 302, 303, 307, 308];
Response.redirect = function(url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError('Invalid status code')
}
return new Response(null, {status: status, headers: {location: url}})
};
exports.DOMException = g.DOMException;
try {
new exports.DOMException();
} catch (err) {
exports.DOMException = function(message, name) {
this.message = message;
this.name = name;
var error = Error(message);
this.stack = error.stack;
};
exports.DOMException.prototype = Object.create(Error.prototype);
exports.DOMException.prototype.constructor = exports.DOMException;
}
function fetch(input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init);
if (request.signal && request.signal.aborted) {
return reject(new exports.DOMException('Aborted', 'AbortError'))
}
var xhr = new XMLHttpRequest();
function abortXhr() {
xhr.abort();
}
xhr.onload = function() {
var options = {
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
};
// This check if specifically for when a user fetches a file locally from the file system
// Only if the status is out of a normal range
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
options.status = 200;
} else {
options.status = xhr.status;
}
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
var body = 'response' in xhr ? xhr.response : xhr.responseText;
setTimeout(function() {
resolve(new Response(body, options));
}, 0);
};
xhr.onerror = function() {
setTimeout(function() {
reject(new TypeError('Network request failed'));
}, 0);
};
xhr.ontimeout = function() {
setTimeout(function() {
reject(new TypeError('Network request timed out'));
}, 0);
};
xhr.onabort = function() {
setTimeout(function() {
reject(new exports.DOMException('Aborted', 'AbortError'));
}, 0);
};
function fixUrl(url) {
try {
return url === '' && g.location.href ? g.location.href : url
} catch (e) {
return url
}
}
xhr.open(request.method, fixUrl(request.url), true);
if (request.credentials === 'include') {
xhr.withCredentials = true;
} else if (request.credentials === 'omit') {
xhr.withCredentials = false;
}
if ('responseType' in xhr) {
if (support.blob) {
xhr.responseType = 'blob';
} else if (
support.arrayBuffer
) {
xhr.responseType = 'arraybuffer';
}
}
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
names.push(normalizeName(name));
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
});
request.headers.forEach(function(value, name) {
if (names.indexOf(name) === -1) {
xhr.setRequestHeader(name, value);
}
});
} else {
request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value);
});
}
if (request.signal) {
request.signal.addEventListener('abort', abortXhr);
xhr.onreadystatechange = function() {
// DONE (success or failure)
if (xhr.readyState === 4) {
request.signal.removeEventListener('abort', abortXhr);
}
};
}
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
})
}
fetch.polyfill = true;
if (!g.fetch) {
g.fetch = fetch;
g.Headers = Headers;
g.Request = Request;
g.Response = Response;
}
exports.Headers = Headers;
exports.Request = Request;
exports.Response = Response;
exports.fetch = fetch;
Object.defineProperty(exports, '__esModule', { value: true });
})));

View File

@@ -1,55 +0,0 @@
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
// fetch.js code comes from
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
//
// The original code source is available in MIT license.
//
// The script comes from the built version from npm.
// You can get the package with the command:
//
// wget $(npm view whatwg-fetch dist.tarball)
//
// The source is the content of `package/dist/fetch.umd.js` file.
pub const source = @embedFile("fetch.js");
pub fn testExecFn(
alloc: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
try @import("polyfill.zig").load(alloc, js_env.*);
var fetch = [_]Case{
.{
.src =
\\var ok = false;
\\const request = new Request("https://httpbin.io/json");
\\fetch(request)
\\ .then((response) => { ok = response.ok; });
\\false;
,
.ex = "false",
},
// all events have been resolved.
.{ .src = "ok", .ex = "true" },
};
try checkCases(js_env, &fetch);
var fetch2 = [_]Case{
.{
.src =
\\var ok2 = false;
\\const request2 = new Request("https://httpbin.io/json");
\\(async function () { resp = await fetch(request2); ok2 = resp.ok; }());
\\false;
,
.ex = "false",
},
// all events have been resolved.
.{ .src = "ok2", .ex = "true" },
};
try checkCases(js_env, &fetch2);
}

View File

@@ -1,56 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const Env = jsruntime.Env;
const fetch = @import("fetch.zig").fetch_polyfill;
const log = std.log.scoped(.polyfill);
const modules = [_]struct {
name: []const u8,
source: []const u8,
}{
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
};
pub fn load(alloc: std.mem.Allocator, env: Env) !void {
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env);
defer try_catch.deinit();
for (modules) |m| {
const res = env.exec(m.source, m.name) catch {
if (try try_catch.err(alloc, env)) |msg| {
defer alloc.free(msg);
log.err("load {s}: {s}", .{ m.name, msg });
}
return;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, env);
defer alloc.free(msg);
log.debug("load {s}: {s}", .{ m.name, msg });
}
}
}

View File

@@ -136,7 +136,6 @@ fn testsAllExecFn(
URLTestExecFn, URLTestExecFn,
HTMLElementTestExecFn, HTMLElementTestExecFn,
MutationObserverTestExecFn, MutationObserverTestExecFn,
@import("polyfill/fetch.zig").testExecFn,
}; };
inline for (testFns) |testFn| { inline for (testFns) |testFn| {

View File

@@ -19,8 +19,6 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Stream = @import("handler.zig").Stream;
const jsruntime = @import("jsruntime"); const jsruntime = @import("jsruntime");
const Completion = jsruntime.IO.Completion; const Completion = jsruntime.IO.Completion;
const AcceptError = jsruntime.IO.AcceptError; const AcceptError = jsruntime.IO.AcceptError;
@@ -30,8 +28,7 @@ const CloseError = jsruntime.IO.CloseError;
const CancelError = jsruntime.IO.CancelError; const CancelError = jsruntime.IO.CancelError;
const TimeoutError = jsruntime.IO.TimeoutError; const TimeoutError = jsruntime.IO.TimeoutError;
const MsgBuffer = @import("msg.zig").Buffer; const MsgBuffer = @import("msg.zig").MsgBuffer;
const MaxSize = @import("msg.zig").MaxSize;
const Browser = @import("browser/browser.zig").Browser; const Browser = @import("browser/browser.zig").Browser;
const cdp = @import("cdp/cdp.zig"); const cdp = @import("cdp/cdp.zig");
@@ -52,7 +49,6 @@ const MaxStdOutSize = 512; // ensure debug msg are not too long
pub const Ctx = struct { pub const Ctx = struct {
loop: *jsruntime.Loop, loop: *jsruntime.Loop,
stream: ?*Stream,
// internal fields // internal fields
accept_socket: std.posix.socket_t, accept_socket: std.posix.socket_t,
@@ -121,8 +117,8 @@ pub const Ctx = struct {
std.debug.assert(completion == self.conn_completion); std.debug.assert(completion == self.conn_completion);
const size = result catch |err| { const size = result catch |err| {
if (self.isClosed() and err == error.FileDescriptorInvalid) { if (err == error.Canceled) {
log.debug("read has been canceled", .{}); log.debug("read canceled", .{});
return; return;
} }
log.err("read error: {any}", .{err}); log.err("read error: {any}", .{err});
@@ -162,7 +158,7 @@ pub const Ctx = struct {
// read and execute input // read and execute input
var input: []const u8 = self.read_buf[0..size]; var input: []const u8 = self.read_buf[0..size];
while (input.len > 0) { while (input.len > 0) {
const parts = self.msg_buf.read(input) catch |err| { const parts = self.msg_buf.read(self.alloc(), input) catch |err| {
if (err == error.MsgMultipart) { if (err == error.MsgMultipart) {
return; return;
} else { } else {
@@ -203,7 +199,7 @@ pub const Ctx = struct {
if (now.since(self.last_active.?) > self.timeout) { if (now.since(self.last_active.?) > self.timeout) {
// close current connection // close current connection
log.debug("conn timeout, closing...", .{}); log.debug("conn timeout, closing...", .{});
self.close(); self.cancelAndClose();
return; return;
} }
@@ -217,6 +213,19 @@ pub const Ctx = struct {
); );
} }
fn cancelCbk(self: *Ctx, completion: *Completion, result: CancelError!void) void {
std.debug.assert(completion == self.accept_completion);
_ = result catch |err| {
log.err("cancel error: {any}", .{err});
self.err = err;
return;
};
log.debug("cancel done", .{});
self.close();
}
// shortcuts // shortcuts
// --------- // ---------
@@ -253,7 +262,7 @@ pub const Ctx = struct {
if (std.mem.eql(u8, cmd, "close")) { if (std.mem.eql(u8, cmd, "close")) {
// close connection // close connection
log.info("close cmd, closing conn...", .{}); log.info("close cmd, closing conn...", .{});
self.close(); self.cancelAndClose();
return error.Closed; return error.Closed;
} }
@@ -274,27 +283,30 @@ pub const Ctx = struct {
// send result // send result
if (!std.mem.eql(u8, res, "")) { if (!std.mem.eql(u8, res, "")) {
return self.send(res); return sendAsync(self, res);
} }
} }
pub fn send(self: *Ctx, msg: []const u8) !void { fn cancelAndClose(self: *Ctx) void {
if (self.stream) |stream| { if (isLinux) { // cancel is only available on Linux
// if we have a stream connection, just write on it self.loop.io.cancel(
defer self.alloc().free(msg); *Ctx,
try stream.send(msg); self,
Ctx.cancelCbk,
self.accept_completion,
self.conn_completion,
);
} else { } else {
// otherwise write asynchronously on the socket connection self.close();
return sendAsync(self, msg);
} }
} }
fn close(self: *Ctx) void { fn close(self: *Ctx) void {
std.posix.close(self.conn_socket);
// conn is closed // conn is closed
self.last_active = null;
std.posix.close(self.conn_socket);
log.debug("connection closed", .{}); log.debug("connection closed", .{});
self.last_active = null;
// restart a new browser session in case of re-connect // restart a new browser session in case of re-connect
if (!self.sessionNew) { if (!self.sessionNew) {
@@ -350,7 +362,7 @@ pub const Ctx = struct {
.{ msg_open, cdp.ContextSessionID }, .{ msg_open, cdp.ContextSessionID },
); );
try ctx.send(s); try sendAsync(ctx, s);
} }
pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void { pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void {
@@ -410,17 +422,16 @@ const Send = struct {
pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void { pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void {
const sd = try Send.init(ctx, msg); const sd = try Send.init(ctx, msg);
ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, sd.msg); ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, msg);
} }
// Listener and handler // Listen
// -------------------- // ------
pub fn handle( pub fn listen(
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
loop: *jsruntime.Loop, loop: *jsruntime.Loop,
server_socket: std.posix.socket_t, server_socket: std.posix.socket_t,
stream: ?*Stream,
timeout: u64, timeout: u64,
) anyerror!void { ) anyerror!void {
@@ -435,8 +446,8 @@ pub fn handle(
// create buffers // create buffers
var read_buf: [BufReadSize]u8 = undefined; var read_buf: [BufReadSize]u8 = undefined;
var buf: [MaxSize]u8 = undefined; var msg_buf = try MsgBuffer.init(loop.alloc, BufReadSize * 256); // 256KB
var msg_buf = MsgBuffer{ .buf = &buf }; defer msg_buf.deinit(loop.alloc);
// create I/O completions // create I/O completions
var accept_completion: Completion = undefined; var accept_completion: Completion = undefined;
@@ -447,7 +458,6 @@ pub fn handle(
// for accepting connections and receving messages // for accepting connections and receving messages
var ctx = Ctx{ var ctx = Ctx{
.loop = loop, .loop = loop,
.stream = stream,
.browser = &browser, .browser = &browser,
.sessionNew = true, .sessionNew = true,
.read_buf = &read_buf, .read_buf = &read_buf,
@@ -487,43 +497,3 @@ pub fn handle(
} }
} }
} }
fn setSockOpt(fd: std.posix.socket_t, level: i32, option: u32, value: c_int) !void {
try std.posix.setsockopt(fd, level, option, &std.mem.toBytes(value));
}
fn isUnixSocket(addr: std.net.Address) bool {
return addr.any.family == std.posix.AF.UNIX;
}
pub fn listen(address: std.net.Address) !std.posix.socket_t {
// create socket
const flags = std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC | std.posix.SOCK.NONBLOCK;
const proto = if (isUnixSocket(address)) @as(u32, 0) else std.posix.IPPROTO.TCP;
const sockfd = try std.posix.socket(address.any.family, flags, proto);
errdefer std.posix.close(sockfd);
// socket options
if (@hasDecl(std.posix.SO, "REUSEPORT")) {
try setSockOpt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEPORT, 1);
} else {
try setSockOpt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, 1);
}
if (!isUnixSocket(address)) {
if (builtin.target.os.tag == .linux) { // posix.TCP not available on MacOS
// WARNING: disable Nagle's alogrithm to avoid latency issues
try setSockOpt(sockfd, std.posix.IPPROTO.TCP, std.posix.TCP.NODELAY, 1);
}
}
// bind & listen
var socklen = address.getOsSockLen();
try std.posix.bind(sockfd, &address.any, socklen);
const kernel_backlog = 1; // default value is 128. Here we just want 1 connection
try std.posix.listen(sockfd, kernel_backlog);
var listen_address: std.net.Address = undefined;
try std.posix.getsockname(sockfd, &listen_address.any, &socklen);
return sockfd;
}

View File

@@ -33,8 +33,6 @@ const Client = @import("asyncio").Client;
const Types = @import("../main_wpt.zig").Types; const Types = @import("../main_wpt.zig").Types;
const UserContext = @import("../main_wpt.zig").UserContext; const UserContext = @import("../main_wpt.zig").UserContext;
const polyfill = @import("../polyfill/polyfill.zig");
// runWPT parses the given HTML file, starts a js env and run the first script // runWPT parses the given HTML file, starts a js env and run the first script
// tags containing javascript sources. // tags containing javascript sources.
// It loads first the js libs files. // It loads first the js libs files.
@@ -76,9 +74,6 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
try js_env.start(); try js_env.start();
defer js_env.stop(); defer js_env.stop();
// load polyfills
try polyfill.load(alloc, js_env);
// display console logs // display console logs
defer { defer {
const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable; const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;

View File

@@ -756,10 +756,8 @@ pub const XMLHttpRequest = struct {
// https://xhr.spec.whatwg.org/#the-response-attribute // https://xhr.spec.whatwg.org/#the-response-attribute
pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response { pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
if (self.response_type == .Empty or self.response_type == .Text) { if (self.response_type == .Empty or self.response_type == .Text) {
if (self.state == LOADING or self.state == DONE) { if (self.state == LOADING or self.state == DONE) return .{ .Text = "" };
return .{ .Text = try self.get_responseText() }; return .{ .Text = try self.get_responseText() };
}
return .{ .Text = "" };
} }
// fastpath if response is previously parsed. // fastpath if response is previously parsed.
@@ -776,7 +774,6 @@ pub const XMLHttpRequest = struct {
// response object to a new ArrayBuffer object representing thiss // response object to a new ArrayBuffer object representing thiss
// received bytes. If this throws an exception, then set thiss // received bytes. If this throws an exception, then set thiss
// response object to failure and return null. // response object to failure and return null.
log.err("response type ArrayBuffer not implemented", .{});
return null; return null;
} }
@@ -785,7 +782,6 @@ pub const XMLHttpRequest = struct {
// response object to a new Blob object representing thiss // response object to a new Blob object representing thiss
// received bytes with type set to the result of get a final MIME // received bytes with type set to the result of get a final MIME
// type for this. // type for this.
log.err("response type Blob not implemented", .{});
return null; return null;
} }
@@ -948,7 +944,7 @@ pub fn testExecFn(
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" }, .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" },
.{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" }, .{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" },
.{ .src = "req.responseText.length > 64", .ex = "true" }, .{ .src = "req.responseText.length > 64", .ex = "true" },
.{ .src = "req.response.length == req.responseText.length", .ex = "true" }, .{ .src = "req.response", .ex = "" },
.{ .src = "req.responseXML instanceof Document", .ex = "true" }, .{ .src = "req.responseXML instanceof Document", .ex = "true" },
}; };
try checkCases(js_env, &send); try checkCases(js_env, &send);

Submodule vendor/websocket.zig deleted from 1b49626c78