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
45 changed files with 400 additions and 2194 deletions

View File

@@ -17,7 +17,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.11'
default: 'v0.1.9'
v8:
description: 'v8 version to install'
required: false

View File

@@ -26,7 +26,7 @@ jobs:
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
# branch should not be protected
branch: 'main'
allowlist: krichprollsch,francisbouvier,katie-lpd
allowlist: krichprollsch,francisbouvier
remote-organization-name: lightpanda-io
remote-repository-name: cla

View File

@@ -56,14 +56,6 @@ jobs:
- name: zig build debug
run: zig build -Dengine=v8
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: lightpanda-build-dev
path: |
zig-out/bin/lightpanda
retention-days: 1
zig-build-release:
name: zig build release
@@ -139,30 +131,3 @@ jobs:
- name: format and send json result
run: /perf-fmt bench-browser ${{ github.sha }} bench.json
demo-puppeteer:
name: demo-puppeteer
needs: zig-build-dev
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: 'lightpanda-io/demo'
fetch-depth: 0
- run: npm install
- name: download artifact
uses: actions/download-artifact@v4
with:
name: lightpanda-build-dev
- run: chmod a+x ./lightpanda
- name: run puppeteer
run: |
python3 -m http.server 1234 -d ./public &
./lightpanda &
RUNS=2 npm run bench-puppeteer-cdp

4
.gitmodules vendored
View File

@@ -28,7 +28,3 @@
[submodule "vendor/zig-async-io"]
path = vendor/zig-async-io
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

@@ -1,10 +0,0 @@
# Contributing
Lightpanda accepts pull requests through GitHub.
You have to sign our [CLA](CLA.md) during your first pull request process
otherwise we're not able to accept your contributions.
The process signature uses the [CLA assistant
lite](https://github.com/marketplace/actions/cla-assistant-lite). You can see
an example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).

View File

@@ -74,6 +74,4 @@ COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
EXPOSE 9222/tcp
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "9222"]
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "3245"]

View File

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

112
README.md
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>
</p>
<h1 align="center">Lightpanda Browser</h1>
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
<h1 align="center">Lightpanda</h1>
<div align="center">
<br />
@@ -13,15 +11,15 @@
Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of Web APIs (partial, WIP)
- Support of the Web APIs (partial, WIP)
- Compatible with Playwright, Puppeteer through CDP (WIP)
Fast scraping and web automation with minimal memory footprint:
- Ultra-low memory footprint (9x less than Chrome)
- Exceptionally fast execution (11x faster than Chrome) & instant startup
- Ultra-low memory footprint (12x less than Chrome)
- 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).
@@ -29,14 +27,14 @@ See [benchmark details](https://github.com/lightpanda-io/demo).
### Javascript execution is mandatory for the modern web
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. Its not possible anymore, because Javascript is everywhere, like it or not:
Back in the good old times, grabbing a webpage was as easy as making an HTTP request, cURL-like. Its not possible anymore, because Javascript is everywhere, like it or not:
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
- JS web frameworks: React, Vue, Angular & others
### Chrome is not the right tool
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. 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
- Hard to package, deploy and maintain at scale
@@ -44,104 +42,38 @@ If we need Javascript, why not use a real web browser? Take a huge desktop appli
### Lightpanda is built for performance
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But thats we did:
If we want both Javascript and performance, for a real headless browser, we need to start from scratch. Not yet another iteration of Chromium, really from a blank page. Crazy right? But thats we did:
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated: without graphical rendering
- Opinionated, no rendering
## 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.
:warning: You should expect most websites to fail or crash.
Here are the key features we want to implement before releasing a Beta version:
Here are the key features we have implemented:
- [x] HTTP loader
- [x] HTML parser and DOM tree (based on Netsurf libs)
- [x] Javascript support (v8)
- [x] Loader
- [x] HTML parser and DOM tree
- [x] Javascript support
- [x] Basic DOM APIs
- [x] Ajax
- [x] XHR API
- [x] Fetch API
- [ ] Fetch API
- [x] DOM dump
- [x] Basic CDP/websockets server
- [ ] Basic CDP server
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
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.
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.
## Quick start
### Install from the nightly builds
You can download the last binary from the [nightly
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
Linux x86_64 and MacOS aarch64.
```console
# Download the binary
$ wget https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux
$ chmod a+x ./lightpanda-x86_64-linux
$ ./lightpanda-x86_64-linux -h
usage: ./lightpanda-x86_64-linux [options] [URL]
start Lightpanda browser
* if an url is provided the browser will fetch the page and exit
* otherwhise the browser starts a CDP server
-h, --help Print this help message and exit.
--host Host of the CDP server (default "127.0.0.1")
--port Port of the CDP server (default "9222")
--timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
--dump Dump document in stdout (fetch mode only)
```
### Dump an URL
```console
$ ./lightpanda-x86_64-linux --dump https://lightpanda.io
info(browser): GET https://lightpanda.io/ http.Status.ok
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
<!DOCTYPE html>
```
### Start a CDP server
```console
$ ./lightpanda-x86_64-linux --host 127.0.0.1 --port 9222
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
info(server): accepting new conn...
```
Once the CDP server started, you can run a Puppeteer script by configuring the
`browserWSEndpoint`.
```js
'use scrict'
import puppeteer from 'puppeteer-core';
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:9222",
});
// The rest of your script remains the same.
const context = await browser.createBrowserContext();
const page = await context.newPage();
await page.goto('https://wikipedia.com/');
await page.close();
await context.close();
```
## Build from sources
We do not provide yet binary versions of Lightpanda, you have to compile it from source.
### Prerequisites
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"),
});
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 {

View File

@@ -29,7 +29,6 @@ const Mime = @import("mime.zig");
const jsruntime = @import("jsruntime");
const Loop = jsruntime.Loop;
const Env = jsruntime.Env;
const Module = jsruntime.Module;
const apiweb = @import("../apiweb.zig");
@@ -43,19 +42,14 @@ const FetchResult = @import("../http/Client.zig").Client.FetchResult;
const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("asyncio").Client;
const polyfill = @import("../polyfill/polyfill.zig");
const log = std.log.scoped(.browser);
pub const user_agent = "Lightpanda/1.0";
// Browser is an instance of the browser.
// You can create multiple browser instances.
// A browser contains only one session.
// TODO allow multiple sessions per browser.
pub const Browser = struct {
session: Session = undefined,
agent: []const u8 = user_agent,
const uri = "about:blank";
@@ -115,7 +109,7 @@ pub const Session = struct {
.uri = uri,
.alloc = alloc,
.arena = std.heap.ArenaAllocator.init(alloc),
.window = Window.create(null, .{ .agent = user_agent }),
.window = Window.create(null),
.loader = Loader.init(alloc),
.storageShed = storage.Shed.init(alloc),
.httpClient = undefined,
@@ -126,21 +120,6 @@ pub const Session = struct {
try self.env.load(&self.jstypes);
}
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
_ = referrer;
const self: *Session = @ptrCast(@alignCast(ctx));
if (self.page == null) return error.NoPage;
log.debug("fetch module: specifier: {s}", .{specifier});
const alloc = self.arena.allocator();
const body = try self.page.?.fetchData(alloc, specifier);
defer alloc.free(body);
return self.env.compileModule(body, specifier);
}
fn deinit(self: *Session) void {
if (self.page) |*p| p.end();
@@ -214,31 +193,6 @@ pub const Page = struct {
};
}
// start js env.
// - auxData: extra data forwarded to the Inspector
// see Inspector.contextCreated
pub fn start(self: *Page, auxData: ?[]const u8) !void {
// start JS env
log.debug("start js env", .{});
try self.session.env.start();
// register the module loader
try self.session.env.setModuleLoadFn(self.session, Session.fetchModule);
// add global objects
log.debug("setup global env", .{});
try self.session.env.bindGlobal(&self.session.window);
// load polyfills
try polyfill.load(self.arena.allocator(), self.session.env);
// inspector
if (self.session.inspector) |inspector| {
log.debug("inspector context created", .{});
inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData);
}
}
// reset js env and mem arena.
pub fn end(self: *Page) void {
self.session.env.stop();
@@ -298,11 +252,6 @@ pub const Page = struct {
log.debug("starting GET {s}", .{uri});
// if the uri is about:blank, nothing to do.
if (std.mem.eql(u8, "about:blank", uri)) {
return;
}
// own the url
if (self.rawuri) |prev| alloc.free(prev);
self.rawuri = try alloc.dupe(u8, uri);
@@ -325,15 +274,18 @@ pub const Page = struct {
const req = resp.req;
log.info("GET {any} {d}", .{ self.uri, @intFromEnum(req.response.status) });
log.info("GET {any} {d}", .{ self.uri, req.response.status });
// TODO handle redirection
log.debug("{?} {d} {s}", .{
req.response.version,
@intFromEnum(req.response.status),
req.response.reason,
// TODO log headers
});
if (req.response.status != .ok) {
log.debug("{?} {d} {s}", .{
req.response.version,
req.response.status,
req.response.reason,
// TODO log headers
});
return error.BadStatusCode;
}
// TODO handle charset
// https://html.spec.whatwg.org/#content-type
@@ -398,6 +350,11 @@ pub const Page = struct {
// https://html.spec.whatwg.org/#read-html
// start JS env
// TODO load the js env concurrently with the HTML parsing.
log.debug("start js env", .{});
try self.session.env.start();
// inspector
if (self.session.inspector) |inspector| {
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
@@ -409,6 +366,10 @@ pub const Page = struct {
.httpClient = &self.session.httpClient,
});
// add global objects
log.debug("setup global env", .{});
try self.session.env.bindGlobal(&self.session.window);
// browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing.
// TODO fetch the script resources concurrently but execute them in the
@@ -417,7 +378,7 @@ pub const Page = struct {
// sasync stores scripts which can be run asynchronously.
// for now they are just run after the non-async one in order to
// dispatch DOMContentLoaded the sooner as possible.
var sasync = std.ArrayList(Script).init(alloc);
var sasync = std.ArrayList(*parser.Element).init(alloc);
defer sasync.deinit();
const root = parser.documentToNode(doc);
@@ -432,10 +393,21 @@ pub const Page = struct {
}
const e = parser.nodeToElement(next.?);
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
// ignore non-script tags
if (tag != .script) continue;
// ignore non-js script.
const script = try Script.init(e) orelse continue;
if (script.kind == .unknown) continue;
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
const stype = try parser.elementGetAttribute(e, "type");
if (!isJS(stype)) {
continue;
}
// Ignore the defer attribute b/c we analyze all script
// after the document has been parsed.
@@ -449,8 +421,8 @@ pub const Page = struct {
// > then the classic script will be fetched in parallel to
// > parsing and evaluated as soon as it is available.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
if (script.isasync) {
try sasync.append(script);
if (try parser.elementGetAttribute(e, "async") != null) {
try sasync.append(e);
continue;
}
@@ -473,7 +445,7 @@ pub const Page = struct {
// > page.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err});
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
@@ -490,9 +462,9 @@ pub const Page = struct {
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
// eval async scripts.
for (sasync.items) |s| {
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
self.evalScript(s) catch |err| log.warn("evaljs: {any}", .{err});
for (sasync.items) |e| {
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
@@ -514,15 +486,15 @@ pub const Page = struct {
// evalScript evaluates the src in priority.
// if no src is present, we evaluate the text source.
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
fn evalScript(self: *Page, s: Script) !void {
fn evalScript(self: *Page, e: *parser.Element) !void {
const alloc = self.arena.allocator();
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
const opt_src = try parser.elementGetAttribute(s.element, "src");
const opt_src = try parser.elementGetAttribute(e, "src");
if (opt_src) |src| {
log.debug("starting GET {s}", .{src});
self.fetchScript(s) catch |err| {
self.fetchScript(src) catch |err| {
switch (err) {
FetchError.BadStatusCode => return err,
@@ -541,10 +513,26 @@ pub const Page = struct {
return;
}
// TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
if (opt_text) |text| {
try s.eval(alloc, self.session.env, text);
// TODO handle charset attribute
const res = self.session.env.exec(text, "") catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval inline {s}: {s}", .{ text, msg });
}
return;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval inline {s}", .{msg});
}
return;
}
@@ -559,9 +547,12 @@ pub const Page = struct {
JsErr,
};
// the caller owns the returned string
fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 {
log.debug("starting fetch {s}", .{src});
// fetchScript senf a GET request to the src and execute the script
// received.
fn fetchScript(self: *Page, src: []const u8) !void {
const alloc = self.arena.allocator();
log.debug("starting fetch script {s}", .{src});
var buffer: [1024]u8 = undefined;
var b: []u8 = buffer[0..];
@@ -572,91 +563,46 @@ pub const Page = struct {
const resp = fetchres.req.response;
log.info("fetch {any}: {d}", .{ u, resp.status });
log.info("fetch script {any}: {d}", .{ u, resp.status });
if (resp.status != .ok) return FetchError.BadStatusCode;
// TODO check content-type
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
defer alloc.free(body);
// check no body
if (body.len == 0) return FetchError.NoBody;
return body;
}
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
// fetchScript senf a GET request to the src and execute the script
// received.
fn fetchScript(self: *Page, s: Script) !void {
const alloc = self.arena.allocator();
const body = try self.fetchData(alloc, s.src);
defer alloc.free(body);
try s.eval(alloc, self.session.env, body);
}
const Script = struct {
element: *parser.Element,
kind: Kind,
isasync: bool,
src: []const u8,
const Kind = enum {
unknown,
javascript,
module,
const res = self.session.env.exec(body, src) catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval remote {s}: {s}", .{ src, msg });
}
return FetchError.JsErr;
};
fn init(e: *parser.Element) !?Script {
// ignore non-script tags
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
if (tag != .script) return null;
return .{
.element = e,
.kind = kind(try parser.elementGetAttribute(e, "type")),
.isasync = try parser.elementGetAttribute(e, "async") != null,
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval remote {s}: {s}", .{ src, msg });
}
}
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
fn kind(stype: ?[]const u8) Kind {
if (stype == null or stype.?.len == 0) return .javascript;
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
if (std.mem.eql(u8, stype.?, "module")) return .module;
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
fn isJS(stype: ?[]const u8) bool {
if (stype == null or stype.?.len == 0) return true;
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
if (!std.mem.eql(u8, stype.?, "module")) return true;
return .unknown;
}
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env);
defer try_catch.deinit();
const res = switch (self.kind) {
.unknown => return error.UnknownScript,
.javascript => env.exec(body, self.src),
.module => env.module(body, self.src),
} catch {
if (try try_catch.err(alloc, env)) |msg| {
defer alloc.free(msg);
log.info("eval script {s}: {s}", .{ self.src, msg });
}
return FetchError.JsErr;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, env);
defer alloc.free(msg);
log.debug("eval script {s}: {s}", .{ self.src, msg });
}
}
};
return false;
}
};

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const Client = @import("../http/Client.zig");
const user_agent = @import("browser.zig").user_agent;
const user_agent = "Lightpanda.io/1.0";
pub const Loader = struct {
client: Client,

View File

@@ -112,7 +112,7 @@ fn getWindowForTarget(
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(?Params).get(alloc, msg);
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getWindowForTarget" });

View File

@@ -31,11 +31,6 @@ const emulation = @import("emulation.zig").emulation;
const fetch = @import("fetch.zig").fetch;
const performance = @import("performance.zig").performance;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const inspector = @import("inspector.zig").inspector;
const dom = @import("dom.zig").dom;
const css = @import("css.zig").css;
const security = @import("security.zig").security;
const log_cdp = std.log.scoped(.cdp);
@@ -64,13 +59,9 @@ const Domains = enum {
Log,
Runtime,
Network,
DOM,
CSS,
Inspector,
Emulation,
Fetch,
Performance,
Security,
};
// The caller is responsible for calling `free` on the returned slice.
@@ -78,20 +69,12 @@ pub fn do(
alloc: std.mem.Allocator,
s: []const u8,
ctx: *Ctx,
) anyerror![]const u8 {
) ![]const u8 {
// incoming message parser
var msg = IncomingMessage.init(alloc, s);
defer msg.deinit();
return dispatch(alloc, &msg, ctx);
}
pub fn dispatch(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) anyerror![]const u8 {
const method = try msg.getMethod();
// retrieve domain from method
@@ -102,26 +85,21 @@ pub fn dispatch(
// select corresponding domain
const action = iter.next() orelse return error.BadMethod;
return switch (domain) {
.Browser => browser(alloc, msg, action, ctx),
.Target => target(alloc, msg, action, ctx),
.Page => page(alloc, msg, action, ctx),
.Log => log(alloc, msg, action, ctx),
.Runtime => runtime(alloc, msg, action, ctx),
.Network => network(alloc, msg, action, ctx),
.DOM => dom(alloc, msg, action, ctx),
.CSS => css(alloc, msg, action, ctx),
.Inspector => inspector(alloc, msg, action, ctx),
.Emulation => emulation(alloc, msg, action, ctx),
.Fetch => fetch(alloc, msg, action, ctx),
.Performance => performance(alloc, msg, action, ctx),
.Security => security(alloc, msg, action, ctx),
.Browser => browser(alloc, &msg, action, ctx),
.Target => target(alloc, &msg, action, ctx),
.Page => page(alloc, &msg, action, ctx),
.Log => log(alloc, &msg, action, ctx),
.Runtime => runtime(alloc, &msg, action, ctx),
.Network => network(alloc, &msg, action, ctx),
.Emulation => emulation(alloc, &msg, action, ctx),
.Fetch => fetch(alloc, &msg, action, ctx),
.Performance => performance(alloc, &msg, action, ctx),
};
}
pub const State = struct {
executionContextId: u32 = 0,
contextID: ?[]const u8 = null,
sessionID: ?[]const u8 = null,
frameID: []const u8 = FrameID,
url: []const u8 = URLBase,
securityOrigin: []const u8 = URLBase,
@@ -215,18 +193,18 @@ pub fn sendEvent(
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
const event_msg = try stringify(alloc, resp);
try ctx.send(event_msg);
try server.sendAsync(ctx, event_msg);
}
// Common
// ------
// TODO: hard coded IDs
pub const BrowserSessionID = "BROWSERSESSIONID597D9875C664CAC0";
pub const ContextSessionID = "CONTEXTSESSIONID0497A05C95417CF4";
pub const BrowserSessionID = "9559320D92474062597D9875C664CAC0";
pub const ContextSessionID = "4FDC2CB760A23A220497A05C95417CF4";
pub const URLBase = "chrome://newtab/";
pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C";
pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE";
pub const FrameID = "90D14BBD8AED408A0467AC93100BCDBE";
pub const LoaderID = "CFC8BED824DD2FD56CF1EF33C965C79C";
pub const TimestampEvent = struct {
timestamp: f64,

View File

@@ -1,59 +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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn css(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

View File

@@ -1,59 +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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn dom(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

View File

@@ -1,59 +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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn inspector(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

View File

@@ -130,8 +130,7 @@ pub const IncomingMessage = struct {
// asking for getParams, we don't know how to parse them.
fn scanParams(self: *IncomingMessage) !void {
const tt = try self.scanner.peekNextTokenType();
// accept object begin or null JSON value.
if (tt != .object_begin and tt != .null) return error.InvalidParams;
if (tt != .object_begin) return error.InvalidParams;
try self.scanner.skipValue();
self.params_skip = true;
}
@@ -147,19 +146,10 @@ pub const IncomingMessage = struct {
return error.SkippedParams;
}
self.scanUntil("params") catch |err| {
// handle nullable type
if (@typeInfo(T) == .Optional) {
if (err == error.InvalidToken or err == error.EndOfDocument) {
return null;
}
}
return err;
};
try self.scanUntil("params");
// parse "params"
const options = std.json.ParseOptions{
.ignore_unknown_fields = true,
.max_value_len = self.scanner.input.len,
.allocate = .alloc_always,
};
@@ -260,34 +250,3 @@ test "read incoming message with null session id" {
try std.testing.expectEqual(1, try msg.getId());
}
}
test "message with nullable params" {
const T = struct {
bar: []const u8,
};
// nullable type, params is present => value
const not_null =
\\{"id": 1,"method":"foo","params":{"bar":"baz"}}
;
var msg = IncomingMessage.init(std.testing.allocator, not_null);
defer msg.deinit();
const input = try Input(?T).get(std.testing.allocator, &msg);
defer input.deinit();
try std.testing.expectEqualStrings(input.params.?.bar, "baz");
// nullable type, params is not present => null
const is_null =
\\{"id": 1,"method":"foo","sessionId":"AAA"}
;
var msg_null = IncomingMessage.init(std.testing.allocator, is_null);
defer msg_null.deinit();
const input_null = try Input(?T).get(std.testing.allocator, &msg_null);
defer input_null.deinit();
try std.testing.expectEqual(null, input_null.params);
try std.testing.expectEqualStrings("AAA", input_null.sessionId.?);
// not nullable type, params is not present => error
const params_or_error = msg_null.getParams(std.testing.allocator, T);
try std.testing.expectError(error.EndOfDocument, params_or_error);
}

View File

@@ -323,7 +323,7 @@ fn navigate(
.loaderId = ctx.state.loaderID,
};
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?
@@ -331,9 +331,8 @@ fn navigate(
// TODO: noop event, we have no env context at this point, is it necesarry?
try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", void, {}, input.sessionId);
// Launch navigate, the page must have been created by a
// target.createTarget.
var p = ctx.browser.session.page orelse return error.NoPage;
// Launch navigate
const p = try ctx.browser.session.createPage();
ctx.state.executionContextId += 1;
const auxData = try std.fmt.allocPrint(
alloc,

View File

@@ -28,7 +28,6 @@ const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const stringify = cdp.stringify;
const target = @import("target.zig");
const log = std.log.scoped(.cdp);
@@ -117,25 +116,17 @@ fn sendInspector(
}
}
ctx.state.sessionID = msg.sessionId;
// remove awaitPromise true params
// TODO: delete when Promise are correctly handled by zig-js-runtime
if (method == .callFunctionOn or method == .evaluate) {
if (std.mem.indexOf(u8, msg.json, "\"awaitPromise\":true")) |_| {
const buf = try alloc.alloc(u8, msg.json.len + 1);
defer alloc.free(buf);
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
ctx.sendInspector(buf);
return "";
}
const buf = try alloc.alloc(u8, msg.json.len + 1);
defer alloc.free(buf);
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
ctx.sendInspector(buf);
} else {
ctx.sendInspector(msg.json);
}
ctx.sendInspector(msg.json);
if (msg.id == null) return "";
return result(alloc, msg.id.?, null, null, msg.sessionId);
return "";
}
pub const AuxData = struct {

View File

@@ -1,59 +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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn security(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "security.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

View File

@@ -38,8 +38,6 @@ const Methods = enum {
disposeBrowserContext,
createTarget,
closeTarget,
sendMessageToTarget,
detachFromTarget,
};
pub fn target(
@@ -60,15 +58,13 @@ pub fn target(
.disposeBrowserContext => disposeBrowserContext(alloc, msg, ctx),
.createTarget => createTarget(alloc, msg, ctx),
.closeTarget => closeTarget(alloc, msg, ctx),
.sendMessageToTarget => sendMessageToTarget(alloc, msg, ctx),
.detachFromTarget => detachFromTarget(alloc, msg, ctx),
};
}
// TODO: hard coded IDs
pub const PageTargetID = "PAGETARGETIDB638E9DC0F52DDC";
pub const BrowserTargetID = "browser9-targ-et6f-id0e-83f3ab73a30c";
pub const BrowserContextID = "BROWSERCONTEXTIDA95049E9DFE95EA9";
const PageTargetID = "CFCD6EC01573CF29BB638E9DC0F52DDC";
const BrowserTargetID = "2d2bdef9-1c95-416f-8c0e-83f3ab73a30c";
const BrowserContextID = "65618675CB7D3585A95049E9DFE95EA9";
// TODO: noop method
fn setDiscoverTargets(
@@ -99,19 +95,6 @@ const AttachToTarget = struct {
waitingForDebugger: bool = false,
};
const TargetCreated = struct {
sessionId: []const u8,
targetInfo: struct {
targetId: []const u8,
type: []const u8 = "page",
title: []const u8,
url: []const u8,
attached: bool = true,
canAccessOpener: bool = false,
browserContextId: []const u8,
},
};
const TargetFilter = struct {
type: ?[]const u8 = null,
exclude: ?bool = null,
@@ -140,7 +123,7 @@ fn setAutoAttach(
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = PageTargetID,
.title = "about:blank",
.title = "New Incognito tab",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
@@ -173,8 +156,8 @@ fn attachToTarget(
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = input.params.targetId,
.title = "about:blank",
.targetId = PageTargetID,
.title = "New Incognito tab",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
@@ -187,7 +170,7 @@ fn attachToTarget(
sessionId: []const u8,
};
const output = SessionId{
.sessionId = input.sessionId orelse cdp.BrowserSessionID,
.sessionId = input.sessionId orelse BrowserContextID,
};
return result(alloc, input.id, SessionId, output, null);
}
@@ -201,7 +184,7 @@ fn getTargetInfo(
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(?Params).get(alloc, msg);
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" });
@@ -254,7 +237,7 @@ fn getBrowserContexts(
return result(alloc, input.id, Resp, resp, null);
}
const ContextID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
const ContextID = "22648B09EDCCDD11109E2D4FEFBE4F89";
// TODO: noop method
fn createBrowserContext(
@@ -309,14 +292,14 @@ fn disposeBrowserContext(
// output
const res = try result(alloc, input.id, null, .{}, null);
try ctx.send(res);
try server.sendAsync(ctx, res);
return error.DisposeBrowserContext;
}
// TODO: hard coded IDs
const TargetID = "TARGETID460A8F29706A2ADF14316298";
const LoaderID = "LOADERID42AA389647D702B4D805F49A";
const TargetID = "57356548460A8F29706A2ADF14316298";
const LoaderID = "DD4A76F842AA389647D702B4D805F49A";
fn createTarget(
alloc: std.mem.Allocator,
@@ -344,46 +327,15 @@ fn createTarget(
ctx.state.securityOrigin = "://";
ctx.state.secureContextType = "InsecureScheme";
ctx.state.loaderID = LoaderID;
ctx.state.sessionID = msg.sessionId;
// TODO stop the previous page instead?
if (ctx.browser.session.page != null) return error.pageAlreadyExists;
// create the page
const p = try ctx.browser.session.createPage();
ctx.state.executionContextId += 1;
// start the js env
const auxData = try std.fmt.allocPrint(
alloc,
// NOTE: we assume this is the default web page
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{ctx.state.frameID},
);
defer alloc.free(auxData);
try p.start(auxData);
// send targetCreated event
const created = TargetCreated{
.sessionId = cdp.ContextSessionID,
.targetInfo = .{
.targetId = ctx.state.frameID,
.title = "about:blank",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
.attached = true,
},
};
try cdp.sendEvent(alloc, ctx, "Target.targetCreated", TargetCreated, created, input.sessionId);
// send attachToTarget event
const attached = AttachToTarget{
.sessionId = cdp.ContextSessionID,
.targetInfo = .{
.targetId = ctx.state.frameID,
.title = "about:blank",
.title = "",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
.attached = true,
},
.waitingForDebugger = true,
};
@@ -426,7 +378,7 @@ fn closeTarget(
success: bool = true,
};
const res = try result(alloc, input.id, Resp, Resp{}, null);
try ctx.send(res);
try server.sendAsync(ctx, res);
// Inspector.detached event
const InspectorDetached = struct {
@@ -458,66 +410,5 @@ fn closeTarget(
null,
);
if (ctx.browser.session.page != null) ctx.browser.session.page.?.end();
return "";
}
fn sendMessageToTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
message: []const u8,
sessionId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} ({s})", .{ input.id, "target.sendMessageToTarget", input.params.message });
// get the wrapped message.
var wmsg = IncomingMessage.init(alloc, input.params.message);
defer wmsg.deinit();
const res = cdp.dispatch(alloc, &wmsg, ctx) catch |e| {
log.err("send message {d} ({s}): {any}", .{ input.id, input.params.message, e });
// TODO dispatch error correctly.
return e;
};
// receivedMessageFromTarget event
const ReceivedMessageFromTarget = struct {
message: []const u8,
sessionId: []const u8,
};
try cdp.sendEvent(
alloc,
ctx,
"Target.receivedMessageFromTarget",
ReceivedMessageFromTarget,
.{
.message = res,
.sessionId = input.params.sessionId,
},
null,
);
return "";
}
// noop
fn detachFromTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.detachFromTarget" });
// output
return result(alloc, input.id, bool, true, input.sessionId);
}

View File

@@ -29,7 +29,6 @@ const EventHandler = @import("../events/event.zig").EventHandler;
const DOMException = @import("exceptions.zig").DOMException;
const Nod = @import("node.zig");
const Window = @import("../html/window.zig").Window;
// EventTarget interfaces
pub const Union = Nod.Union;
@@ -41,10 +40,9 @@ pub const EventTarget = struct {
pub const mem_guarantied = true;
pub fn toInterface(et: *parser.EventTarget) !Union {
return switch (try parser.eventTargetGetType(et)) {
.window => .{ .Window = @as(*Window, @ptrCast(et)) },
.node => Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))),
};
// NOTE: for now we state that all EventTarget are Nodes
// TODO: handle other types (eg. Window)
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
}
// JS funcs

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, 0, 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: [4]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 URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node;
// HTMLElement interfaces
pub const Interfaces = .{
@@ -118,25 +117,6 @@ pub const HTMLElement = struct {
pub fn get_style(_: *parser.ElementHTML) CSSProperties {
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)
@@ -1088,12 +1068,4 @@ pub fn testExecFn(
.{ .src = "script.async", .ex = "false" },
};
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

@@ -1,128 +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 Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
pub const History = struct {
pub const mem_guarantied = true;
const ScrollRestorationMode = enum {
auto,
manual,
};
scrollRestoration: ScrollRestorationMode = .auto,
state: std.json.Value = .null,
// count tracks the history length until we implement correctly pushstate.
count: u32 = 0,
pub fn get_length(self: *History) u32 {
// TODO return the real history length value.
return self.count;
}
pub fn get_scrollRestoration(self: *History) []const u8 {
return switch (self.scrollRestoration) {
.auto => "auto",
.manual => "manual",
};
}
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
}
pub fn get_state(self: *History) std.json.Value {
return self.state;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
self.count += 1;
_ = url;
_ = data;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
_ = self;
_ = url;
_ = data;
}
// TODO implement the function
pub fn _go(self: *History, delta: ?i32) void {
_ = self;
_ = delta;
}
// TODO implement the function
pub fn _back(self: *History) void {
_ = self;
}
// TODO implement the function
pub fn _forward(self: *History) void {
_ = self;
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var history = [_]Case{
.{ .src = "history.scrollRestoration", .ex = "auto" },
.{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
.{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
.{ .src = "history.scrollRestoration", .ex = "manual" },
.{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
.{ .src = "history.scrollRestoration", .ex = "auto" },
.{ .src = "history.state", .ex = "null" },
.{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
.{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
.{ .src = "history.go()", .ex = "undefined" },
.{ .src = "history.go(1)", .ex = "undefined" },
.{ .src = "history.go(-1)", .ex = "undefined" },
.{ .src = "history.forward()", .ex = "undefined" },
.{ .src = "history.back()", .ex = "undefined" },
};
try checkCases(js_env, &history);
}

View File

@@ -21,8 +21,6 @@ const generate = @import("../generate.zig");
const HTMLDocument = @import("document.zig").HTMLDocument;
const HTMLElem = @import("elements.zig");
const Window = @import("window.zig").Window;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
pub const Interfaces = generate.Tuple(.{
HTMLDocument,
@@ -30,6 +28,4 @@ pub const Interfaces = generate.Tuple(.{
HTMLElem.HTMLMediaElement,
HTMLElem.Interfaces,
Window,
Navigator,
History,
});

View File

@@ -1,102 +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 Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
pub const Navigator = struct {
pub const mem_guarantied = true;
agent: []const u8 = "Lightpanda/1.0",
version: []const u8 = "1.0",
vendor: []const u8 = "",
platform: []const u8 = std.fmt.comptimePrint("{any} {any}", .{ builtin.os.tag, builtin.cpu.arch }),
language: []const u8 = "en-US",
pub fn get_userAgent(self: *Navigator) []const u8 {
return self.agent;
}
pub fn get_appCodeName(_: *Navigator) []const u8 {
return "Mozilla";
}
pub fn get_appName(_: *Navigator) []const u8 {
return "Netscape";
}
pub fn get_appVersion(self: *Navigator) []const u8 {
return self.version;
}
pub fn get_platform(self: *Navigator) []const u8 {
return self.platform;
}
pub fn get_product(_: *Navigator) []const u8 {
return "Gecko";
}
pub fn get_productSub(_: *Navigator) []const u8 {
return "20030107";
}
pub fn get_vendor(self: *Navigator) []const u8 {
return self.vendor;
}
pub fn get_vendorSub(_: *Navigator) []const u8 {
return "";
}
pub fn get_language(self: *Navigator) []const u8 {
return self.language;
}
// TODO wait for arrays.
//pub fn get_languages(self: *Navigator) [][]const u8 {
// return .{self.language};
//}
pub fn get_online(_: *Navigator) bool {
return true;
}
pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
_ = scheme;
_ = url;
}
pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
_ = scheme;
_ = url;
}
pub fn get_cookieEnabled(_: *Navigator) bool {
return true;
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var navigator = [_]Case{
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
.{ .src = "navigator.appVersion", .ex = "1.0" },
.{ .src = "navigator.language", .ex = "en-US" },
};
try checkCases(js_env, &navigator);
}

View File

@@ -25,11 +25,11 @@ const CallbackArg = jsruntime.CallbackArg;
const Loop = jsruntime.Loop;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
const storage = @import("../storage/storage.zig");
const log = std.log.scoped(.window);
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct {
@@ -42,7 +42,6 @@ pub const Window = struct {
document: ?*parser.DocumentHTML = null,
target: []const u8,
history: History = .{},
storageShelf: ?*storage.Shelf = null,
@@ -51,13 +50,9 @@ pub const Window = struct {
timeoutid: u32 = 0,
timeoutids: [512]u64 = undefined,
navigator: Navigator,
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
pub fn create(target: ?[]const u8) Window {
return Window{
.target = target orelse "",
.navigator = navigator orelse .{},
.base = .{ .et_type = @intFromEnum(parser.EventTargetType.window) },
};
}
@@ -73,8 +68,8 @@ pub const Window = struct {
return self;
}
pub fn get_navigator(self: *Window) *Navigator {
return &self.navigator;
pub fn _debug(_: *Window, str: []const u8) void {
log.debug("{s}", .{str});
}
pub fn get_self(self: *Window) *Window {
@@ -89,10 +84,6 @@ pub const Window = struct {
return self.document;
}
pub fn get_history(self: *Window) *History {
return &self.history;
}
pub fn get_name(self: *Window) []const u8 {
return self.target;
}

View File

@@ -17,15 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const posix = std.posix;
const builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const websocket = @import("websocket");
const Browser = @import("browser/browser.zig").Browser;
const server = @import("server.zig");
const handler = @import("handler.zig");
const MaxSize = @import("msg.zig").MaxSize;
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
@@ -34,18 +32,101 @@ pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
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);
pub const std_options = .{
// Set the log level to info
.log_level = .debug,
// 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,
// Define logFn to override the std implementation
.logFn = logFn,
/// `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 =
@@ -57,9 +138,8 @@ const usage =
\\ * otherwhise the browser starts a CDP server
\\
\\ -h, --help Print this help message and exit.
\\ --verbose Display all logs. By default only info, warn and err levels are displayed.
\\ --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")
\\ --dump Dump document in stdout (fetch mode only)
\\
@@ -90,11 +170,10 @@ const CliMode = union(CliModeTag) {
host: []const u8 = Host,
port: u16 = Port,
timeout: u8 = Timeout,
tcp: bool = false, // undocumented TCP mode
// default options
const Host = "127.0.0.1";
const Port = 9222;
const Port = 3245;
const Timeout = 3; // in seconds
};
@@ -119,10 +198,6 @@ const CliMode = union(CliModeTag) {
if (std.mem.eql(u8, "-h", opt) or std.mem.eql(u8, "--help", opt)) {
return printUsageExit(execname, 0);
}
if (std.mem.eql(u8, "--verbose", opt)) {
verbose = true;
continue;
}
if (std.mem.eql(u8, "--dump", opt)) {
_fetch.dump = true;
continue;
@@ -160,10 +235,6 @@ const CliMode = union(CliModeTag) {
return printUsageExit(execname, 1);
}
}
if (std.mem.eql(u8, "--tcp", opt)) {
_server.tcp = true;
continue;
}
// unknown option
if (std.mem.startsWith(u8, opt, "--")) {
@@ -246,70 +317,33 @@ pub fn main() !void {
defer cli_mode.deinit();
switch (cli_mode) {
.server => |opts| {
.server => |mode| {
// Stream server
const addr = blk: {
if (opts.tcp) {
break :blk opts.addr;
} else {
const unix_path = "/tmp/lightpanda";
std.fs.deleteFileAbsolute(unix_path) catch {}; // file could not exists
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});
// server
var srv = StreamServer.init(.{
.reuse_address = true,
.reuse_port = true,
.nonblocking = true,
});
defer srv.deinit();
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
var loop = try jsruntime.Loop.init(alloc);
defer loop.deinit();
// TCP server mode
if (opts.tcp) {
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();
// listen
try server.listen(alloc, &loop, srv.sockfd.?, std.time.ns_per_s * @as(u64, mode.timeout));
},
.fetch => |opts| {
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
.fetch => |mode| {
log.debug("Fetch mode: url {s}, dump {any}", .{ mode.url, mode.dump });
// vm
const vm = jsruntime.VM.init();
@@ -326,41 +360,24 @@ pub fn main() !void {
// page
const page = try browser.session.createPage();
try page.start(null);
defer page.end();
_ = page.navigate(opts.url, null) catch |err| switch (err) {
_ = page.navigate(mode.url, null) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => {
log.err("'{s}' is not a valid URL ({any})\n", .{ opts.url, err });
return printUsageExit(opts.execname, 1);
log.err("'{s}' is not a valid URL ({any})\n", .{ mode.url, err });
return printUsageExit(mode.execname, 1);
},
else => {
log.err("'{s}' fetching error ({any})s\n", .{ opts.url, err });
return printUsageExit(opts.execname, 1);
log.err("'{s}' fetching error ({any})s\n", .{ mode.url, err });
return printUsageExit(mode.execname, 1);
},
};
try page.wait();
// dump
if (opts.dump) {
if (mode.dump) {
try page.dump(std.io.getStdOut());
}
},
}
}
var verbose: bool = builtin.mode == .Debug; // In debug mode, force verbose.
fn logFn(
comptime level: std.log.Level,
comptime scope: @Type(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
if (!verbose) {
// hide all messages with level greater of equal to debug level.
if (@intFromEnum(level) >= @intFromEnum(std.log.Level.debug)) return;
}
// default std log function.
std.log.defaultLog(level, scope, format, args);
}

View File

@@ -54,7 +54,7 @@ fn execJS(
defer storageShelf.deinit();
// alias global as self and window
var window = Window.create(null, null);
var window = Window.create(null);
window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window);

View File

@@ -18,50 +18,44 @@
const std = @import("std");
pub const HeaderSize = 4;
pub const MsgSize = 256 * 1204; // 256KB
// NOTE: Theorically we could go up to 4GB with a 4 bytes binary encoding
// but we prefer to put a lower hard limit for obvious memory size reasons.
pub const MaxSize = HeaderSize + MsgSize;
pub const Msg = struct {
pub fn getSize(data: []const u8) usize {
return std.mem.readInt(u32, data[0..HeaderSize], .little);
}
pub fn setSize(len: usize, header: *[4]u8) void {
std.mem.writeInt(u32, 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)
/// MsgBuffer returns messages from a raw text read stream,
/// according to the following format `<msg_size>:<msg>`.
/// It handles both:
/// - combined messages in one read
/// - 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.
pub const Buffer = struct {
buf: []u8,
pub const MsgBuffer = struct {
size: usize = 0,
buf: []u8,
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;
}
fn isEmpty(self: *const Buffer) bool {
fn isEmpty(self: MsgBuffer) bool {
return self.size == 0 and self.pos == 0;
}
fn reset(self: *Buffer) void {
fn reset(self: *MsgBuffer) void {
self.size = 0;
self.pos = 0;
}
// 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,
left: []const u8,
} {
@@ -70,9 +64,11 @@ pub const Buffer = struct {
// msg size
var msg_size: usize = undefined;
if (self.isEmpty()) {
// decode msg size header
msg_size = Msg.getSize(_input);
_input = _input[HeaderSize..];
// parse msg size metadata
const size_pos = std.mem.indexOfScalar(u8, _input, ':') orelse return error.InputWithoutSize;
const size_str = _input[0..size_pos];
msg_size = try std.fmt.parseInt(u32, size_str, 10);
_input = _input[size_pos + 1 ..];
} else {
msg_size = self.size;
}
@@ -81,7 +77,7 @@ pub const Buffer = struct {
const is_multipart = !self.isEmpty() or _input.len < msg_size;
if (is_multipart) {
// set msg size on empty Buffer
// set msg size on empty MsgBuffer
if (self.isEmpty()) {
self.size = msg_size;
}
@@ -94,11 +90,19 @@ pub const Buffer = struct {
return error.MsgTooBig;
}
// copy the current input into Buffer
// NOTE: we could use @memcpy but it's not Thread-safe (alias problem)
// see https://www.openmymind.net/Zigs-memcpy-copyForwards-and-copyBackwards/
// Intead we just use std.mem.copyForwards
std.mem.copyForwards(u8, self.buf[self.pos..new_pos], _input[0..]);
// check if the current input can fit in MsgBuffer
if (new_pos > self.buf.len) {
// we want to realloc at least:
// - a size big enough to fit the entire input (ie. new_pos)
// - 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
self.pos = new_pos;
@@ -116,45 +120,47 @@ pub const Buffer = struct {
}
};
test "Buffer" {
fn doTest(nb: *u8) void {
nb.* += 1;
}
test "MsgBuffer" {
const Case = struct {
input: []const u8,
nb: u8,
};
const alloc = std.testing.allocator;
const cases = [_]Case{
// simple
.{ .input = .{ 2, 0, 0, 0 } ++ "ok", .nb = 1 },
.{ .input = "2:ok", .nb = 1 },
// combined
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 3, 0, 0, 0 } ++ "foo", .nb = 2 },
.{ .input = "2:ok3:foo7:bar2:ok", .nb = 3 }, // "bar2:ok" is a message, no need to escape "2:" here
// multipart
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "9:multi", .nb = 0 },
.{ .input = "part", .nb = 1 },
// multipart & combined
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "part" ++ .{ 2, 0, 0, 0 } ++ "ok", .nb = 2 },
.{ .input = "9:multi", .nb = 0 },
.{ .input = "part2:ok", .nb = 2 },
// multipart & combined with other multipart
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "part" ++ .{ 8, 0, 0, 0 } ++ "co", .nb = 1 },
.{ .input = "9:multi", .nb = 0 },
.{ .input = "part8:co", .nb = 1 },
.{ .input = "mbined", .nb = 1 },
// several multipart
.{ .input = .{ 23, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "23:multi", .nb = 0 },
.{ .input = "several", .nb = 0 },
.{ .input = "complex", .nb = 0 },
.{ .input = "part", .nb = 1 },
// combined & multipart
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 9, 0, 0, 0 } ++ "multi", .nb = 1 },
.{ .input = "2:ok9:multi", .nb = 1 },
.{ .input = "part", .nb = 1 },
};
var b: [MaxSize]u8 = undefined;
var buf = Buffer{ .buf = &b };
var msg_buf = try MsgBuffer.init(alloc, 10);
defer msg_buf.deinit(alloc);
for (cases) |case| {
var nb: u8 = 0;
var input = case.input;
var input: []const u8 = case.input;
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
return err;
};

View File

@@ -560,11 +560,6 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
}
// EventTarget
pub const EventTargetType = enum(u4) {
node = c.DOM_EVENT_TARGET_NODE,
window = 2,
};
pub const EventTarget = c.dom_event_target;
pub fn eventTargetToNode(et: *EventTarget) *Node {
@@ -806,14 +801,6 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
return res;
}
pub fn eventTargetGetType(et: *EventTarget) !EventTargetType {
var res: c.dom_event_target_type = undefined;
const err = eventTargetVtable(et).dom_event_target_get_type.?(et, &res);
try DOMErr(err);
return @enumFromInt(res);
}
pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {
std.debug.assert(@inComptime());
switch (@typeInfo(T)) {
@@ -837,10 +824,8 @@ pub const EventTargetTBase = extern struct {
.remove_event_listener = remove_event_listener,
.add_event_listener = add_event_listener,
.iter_event_listener = iter_event_listener,
.dom_event_target_get_type = dom_event_target_get_type,
},
eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
et_type: c.dom_event_target_type = @intFromEnum(EventTargetType.node),
pub fn add_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 {
const self = @as(*Self, @ptrCast(et));
@@ -873,17 +858,6 @@ pub const EventTargetTBase = extern struct {
const self = @as(*Self, @ptrCast(et));
return c._dom_event_target_iter_event_listener(self.eti, t, capture, cur, next, l);
}
pub fn dom_event_target_get_type(
et: [*c]c.dom_event_target,
res: [*c]c.dom_event_target_type,
) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
res.* = self.et_type;
return c.DOM_NO_ERR;
}
};
// NodeType

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

@@ -96,7 +96,7 @@ fn testExecFn(
});
// alias global as self and window
var window = Window.create(null, null);
var window = Window.create(null);
window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
@@ -136,9 +136,6 @@ fn testsAllExecFn(
URLTestExecFn,
HTMLElementTestExecFn,
MutationObserverTestExecFn,
@import("polyfill/fetch.zig").testExecFn,
@import("html/navigator.zig").testExecFn,
@import("html/history.zig").testExecFn,
};
inline for (testFns) |testFn| {
@@ -361,7 +358,7 @@ test "bug document html parsing #4" {
}
test "Window is a libdom event target" {
var window = Window.create(null, null);
var window = Window.create(null);
const event = try parser.eventCreate();
try parser.eventInit(event, "foo", .{});

View File

@@ -19,8 +19,6 @@
const std = @import("std");
const builtin = @import("builtin");
const Stream = @import("handler.zig").Stream;
const jsruntime = @import("jsruntime");
const Completion = jsruntime.IO.Completion;
const AcceptError = jsruntime.IO.AcceptError;
@@ -30,8 +28,7 @@ const CloseError = jsruntime.IO.CloseError;
const CancelError = jsruntime.IO.CancelError;
const TimeoutError = jsruntime.IO.TimeoutError;
const MsgBuffer = @import("msg.zig").Buffer;
const MaxSize = @import("msg.zig").MaxSize;
const MsgBuffer = @import("msg.zig").MsgBuffer;
const Browser = @import("browser/browser.zig").Browser;
const cdp = @import("cdp/cdp.zig");
@@ -52,7 +49,6 @@ const MaxStdOutSize = 512; // ensure debug msg are not too long
pub const Ctx = struct {
loop: *jsruntime.Loop,
stream: ?*Stream,
// internal fields
accept_socket: std.posix.socket_t,
@@ -121,8 +117,8 @@ pub const Ctx = struct {
std.debug.assert(completion == self.conn_completion);
const size = result catch |err| {
if (self.isClosed() and err == error.FileDescriptorInvalid) {
log.debug("read has been canceled", .{});
if (err == error.Canceled) {
log.debug("read canceled", .{});
return;
}
log.err("read error: {any}", .{err});
@@ -162,7 +158,7 @@ pub const Ctx = struct {
// read and execute input
var input: []const u8 = self.read_buf[0..size];
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) {
return;
} else {
@@ -175,7 +171,6 @@ pub const Ctx = struct {
self.do(parts.msg) catch |err| {
if (err != error.Closed) {
log.err("do error: {any}", .{err});
log.debug("last msg: {s}", .{parts.msg});
}
};
}
@@ -204,7 +199,7 @@ pub const Ctx = struct {
if (now.since(self.last_active.?) > self.timeout) {
// close current connection
log.debug("conn timeout, closing...", .{});
self.close();
self.cancelAndClose();
return;
}
@@ -218,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
// ---------
@@ -254,7 +262,7 @@ pub const Ctx = struct {
if (std.mem.eql(u8, cmd, "close")) {
// close connection
log.info("close cmd, closing conn...", .{});
self.close();
self.cancelAndClose();
return error.Closed;
}
@@ -275,27 +283,30 @@ pub const Ctx = struct {
// send result
if (!std.mem.eql(u8, res, "")) {
return self.send(res);
return sendAsync(self, res);
}
}
pub fn send(self: *Ctx, msg: []const u8) !void {
if (self.stream) |stream| {
// if we have a stream connection, just write on it
defer self.alloc().free(msg);
try stream.send(msg);
fn cancelAndClose(self: *Ctx) void {
if (isLinux) { // cancel is only available on Linux
self.loop.io.cancel(
*Ctx,
self,
Ctx.cancelCbk,
self.accept_completion,
self.conn_completion,
);
} else {
// otherwise write asynchronously on the socket connection
return sendAsync(self, msg);
self.close();
}
}
fn close(self: *Ctx) void {
std.posix.close(self.conn_socket);
// conn is closed
self.last_active = null;
std.posix.close(self.conn_socket);
log.debug("connection closed", .{});
self.last_active = null;
// restart a new browser session in case of re-connect
if (!self.sessionNew) {
@@ -348,10 +359,10 @@ pub const Ctx = struct {
const s = try std.fmt.allocPrint(
allocator,
tpl,
.{ msg_open, ctx.state.sessionID orelse 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 {
@@ -411,17 +422,16 @@ const Send = struct {
pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void {
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,
loop: *jsruntime.Loop,
server_socket: std.posix.socket_t,
stream: ?*Stream,
timeout: u64,
) anyerror!void {
@@ -436,8 +446,8 @@ pub fn handle(
// create buffers
var read_buf: [BufReadSize]u8 = undefined;
var buf: [MaxSize]u8 = undefined;
var msg_buf = MsgBuffer{ .buf = &buf };
var msg_buf = try MsgBuffer.init(loop.alloc, BufReadSize * 256); // 256KB
defer msg_buf.deinit(loop.alloc);
// create I/O completions
var accept_completion: Completion = undefined;
@@ -448,7 +458,6 @@ pub fn handle(
// for accepting connections and receving messages
var ctx = Ctx{
.loop = loop,
.stream = stream,
.browser = &browser,
.sessionNew = true,
.read_buf = &read_buf,
@@ -488,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

@@ -173,7 +173,7 @@ pub const Bottle = struct {
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't implement the feature.
// So for now, we won't impement the feature.
}
pub fn _removeItem(self: *Bottle, k: []const u8) !void {

View File

@@ -33,8 +33,6 @@ const Client = @import("asyncio").Client;
const Types = @import("../main_wpt.zig").Types;
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
// tags containing javascript sources.
// 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();
defer js_env.stop();
// load polyfills
try polyfill.load(alloc, js_env);
// display console logs
defer {
const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
@@ -90,7 +85,7 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
}
// setup global env vars.
var window = Window.create(null, null);
var window = Window.create(null);
window.replaceDocument(html_doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(&window);

View File

@@ -756,10 +756,8 @@ pub const XMLHttpRequest = struct {
// 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 = try self.get_responseText() };
}
return .{ .Text = "" };
if (self.state == LOADING or self.state == DONE) return .{ .Text = "" };
return .{ .Text = try self.get_responseText() };
}
// fastpath if response is previously parsed.
@@ -776,7 +774,6 @@ pub const XMLHttpRequest = struct {
// response object to a new ArrayBuffer object representing thiss
// received bytes. If this throws an exception, then set thiss
// response object to failure and return null.
log.err("response type ArrayBuffer not implemented", .{});
return null;
}
@@ -785,7 +782,6 @@ pub const XMLHttpRequest = struct {
// response object to a new Blob object representing thiss
// received bytes with type set to the result of get a final MIME
// type for this.
log.err("response type Blob not implemented", .{});
return null;
}
@@ -948,7 +944,7 @@ pub fn testExecFn(
.{ .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.length == req.responseText.length", .ex = "true" },
.{ .src = "req.response", .ex = "" },
.{ .src = "req.responseXML instanceof Document", .ex = "true" },
};
try checkCases(js_env, &send);

Submodule vendor/websocket.zig deleted from 1b49626c78