426 Commits

Author SHA1 Message Date
Pierre Tachoire
ea19f7e348 server: implement /json/list
Mimic Chrome's endpoints to enable Selenium CDP connection.
2025-02-13 17:16:32 +01:00
Pierre Tachoire
d8fae5bc41 Merge pull request #408 from karlseguin/websocket_server
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Make TCP server websocket-aware
2025-02-13 09:04:23 +01:00
Karl Seguin
fa9b6f58e5 trying to fix submodule version 2025-02-13 09:42:26 +08:00
Karl Seguin
89ff1411e9 Fix memory leak on invalid websocket continuation frames 2025-02-13 09:34:25 +08:00
Karl Seguin
701e8277d6 support continuation frames 2025-02-13 08:51:21 +08:00
Karl Seguin
4a11f80c45 Make websocket client reader stateful
Move more logic into the reader. Avoid copying partial messages in
cases where we know that the buffer is large enough.

This is mostly groundwork for trying to add support for continuation
frames.
2025-02-13 08:51:21 +08:00
Karl Seguin
f1b275d5d0 Increase fuzz count. Add test for [too] large HTTP requests 2025-02-13 08:51:21 +08:00
Karl Seguin
68e0ffc95c "fix" test compilation 2025-02-13 08:51:21 +08:00
Karl Seguin
0753eb7691 zig fmt 2025-02-13 08:51:21 +08:00
Karl Seguin
92afcd174d remove websocket.zig dependency from build 2025-02-13 08:51:21 +08:00
Karl Seguin
94be7a0e79 Make TCP server websocket-aware
Adding HTTP & websocket awareness to the TCP server.

HTTP server handles `GET /json/version` and websocket upgrade requests.

Conceptually, websocket handling is the same code as before, but receiving
data will parse the websocket frames and writing data will wrap it in
a websocket frame.

The previous `Ctx` was split into a `Server` and a `Client`. This was
largely done to make it easy to write unit tests, since the `Client` is
a generic, all its dependencies (i.e. the server) can be mocked out. This
also makes it a bit nicer to know if there is or isn't a client (via the
server's client optional).

Added a MemoryPool for the Send object (I thought that was a nice touch!)

Removed MacOS hack on accept/conn completion usage.

Known issues:
- When framing an outgoing message, the entire message has to be duped. This
is no worse than how it was before, but it should be possible to eliminate
this in the future. Probably not part of this PR.

- Websocket parsing will reject continuation frames. I don't know of a single
client that will send a fragmented message (websocket has its own
message fragmentation), but we should probably still support this just in
case.

- I don't think the receive, timeout and close completions can safely be
re-used like we're doing. I believe they need to be associated with a specific
client socket.

- A new connection creates a new browser session. I think this is right (??),
but for the very first, we're throwing out a perfectly usable session. I'm
thinking this might be a change to how Browser/Sessions work.

- zig build test won't compile. This branch reproduces the issue with none
of these changes:
https://github.com/karlseguin/browser/tree/broken_test_build

(or, as a diff to main):
https://github.com/lightpanda-io/browser/compare/main...karlseguin:broken_test_build
2025-02-13 08:51:19 +08:00
Pierre Tachoire
0814daf99d Merge pull request #421 from lightpanda-io/upgrade-tigerbeetle
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
upgrade tigerbeetle
2025-02-12 14:46:35 +01:00
Pierre Tachoire
b2e3419bff upgrade tigerbeetle 2025-02-12 14:37:39 +01:00
Pierre Tachoire
6ba3e57f5f Merge pull request #404 from lightpanda-io/cdp-documentUpdated
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
cdp: dispatch a DOM.documentUpdated event
2025-02-11 14:38:39 +01:00
Pierre Tachoire
055530c8c6 cdp: send dom node children 2025-02-10 12:19:35 +01:00
Pierre Tachoire
fb3b38aec7 cdp: implement getSearchResults and discardSearchResults 2025-02-10 09:31:10 +01:00
Pierre Tachoire
4e4a8f1bab cdp: implement DOM.performSearch 2025-02-10 09:31:09 +01:00
Pierre Tachoire
39b3786776 cdp: ctx state has init and deinit now 2025-02-10 09:31:09 +01:00
Pierre Tachoire
8b22313ca1 netsurf: return empty string on null for node name 2025-02-10 09:31:09 +01:00
Pierre Tachoire
402f72cfa8 cdp: adjust page deinit 2025-02-10 09:31:08 +01:00
Pierre Tachoire
e7dcb8a605 cdp: introduce current page
avoid page struct copy
2025-02-10 09:31:08 +01:00
Pierre Tachoire
8f8a1fda85 cdp: implement DOM.getDocument 2025-02-10 09:31:08 +01:00
Pierre Tachoire
26be25c3d5 cdp: dispatch a DOM.documentUpdated event 2025-02-10 09:31:04 +01:00
Pierre Tachoire
50b53b00e0 Merge pull request #414 from karlseguin/url_query
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Improve memory and performance of url.Query
2025-02-10 09:30:37 +01:00
Karl Seguin
94531cb3d0 Improve memory and performance of url.Query
1 - Use getOrPut to avoid making 2 map lookups where possible.

2 - Use an arena allocator for Values, which makes memory management simpler.

3 - Because of #2, we no longer need to allocate key or values which don't need
    to be unescaped. The downside is that the input string has to outlive the
    query.Values (but I think this is currently always the case)

4 - Optimize unescape logic & allocations

5 - Improve test coverage
2025-02-10 16:11:44 +08:00
Pierre Tachoire
842760255b Merge pull request #413 from karlseguin/mime
Improve performance & compliance of MIME parsing
2025-02-10 08:59:00 +01:00
Pierre Tachoire
c78b582d71 Merge pull request #409 from karlseguin/unittest_build
Add a new unittest build step
2025-02-10 08:45:24 +01:00
Karl Seguin
4ab02fab1c Fix build.
zig build test can pass, but zig build run won't even compile. // TODO: fix.
2025-02-10 11:18:16 +08:00
Karl Seguin
6863f3227f Improve performance & compliance of MIME parsing
Common cases, text/html, text/xml and text/plain parse about 2x faster. Other
cases are about 30% faster.

Support quoted attributes, i.e. charset="utf-8" & valid escape sequences. This
potentially requires allocation, thus Mime.parse now takes an allocator.

Stricter validation around type/subtype based on RFC.

More tests.

Replace Mime.eql with isHTML(). Equality is complicated and was previously
incorrect (it was case sensitive, it should not be). Since we currently only
use isHTML-like behavior, built a (faster) method specifically for that.
2025-02-10 11:07:55 +08:00
Karl Seguin
d01d43eccb Remove setup/teardown functionality. YAGNI 2025-02-09 13:16:27 +08:00
Karl Seguin
2aa5f4fc82 small iterator tweak 2025-02-09 11:04:21 +08:00
Karl Seguin
3af0531111 zig fmt + add U32Iterator tests 2025-02-09 11:04:21 +08:00
Karl Seguin
6e58b98b3d Startup local HTTP server for testing
Change loader tests to use local HTTP server.

Add missing test scripts (i.e. storage) to unittest runs.
2025-02-09 11:04:21 +08:00
Karl Seguin
62805cdf1d add license to file 2025-02-09 11:04:21 +08:00
Karl Seguin
4229b1d2a4 Report memory leaks when using std.testing.allocator
Fix leaks in storage bottle
2025-02-09 11:04:21 +08:00
Karl Seguin
2c4661a250 Add a new unittest build step
Preserves all existing behavior (i.e. make test and zig build test are not
changed in any way).

The new 'unittest' only runs unit tests and is fast to build. It takes ~1.7 to
build unittest, vs ~11.09 to build test. This is really the main goal, and
hopefully any unit test which are (a) fast and (b) don't impact build times
will be run here.

The test runner is based on:
https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b

It allow filtering, i.e. `make unittest F="parse query dup"`.

'unittest' does memory leak detection when tests use std.testing.allocator.

Fixed a memory leak in url/query which was detected/reported with by the new
'unittest'.

In order to avoid having 3 src/test_xyx.zig files, I merged the existing
test_runner.zig and run_tests.zig into a single main_tests.zig. (this change
is superfluous, but I thought it was cleaner this way. Happy to revert this).
2025-02-09 11:04:21 +08:00
Pierre Tachoire
0c1a486ed9 Merge pull request #411 from karlseguin/reader_tweak
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Minor Reader tweaks
2025-02-08 11:33:49 +01:00
Karl Seguin
688cb55c2b Minor Reader tweaks
1- Remove `parser.trim`, it was only being used in 1 place. All other places
   are using `std.mem.trim(u8, X, &std.ascii.whitespace)`, so i updated MIME to
   use this as well

2- Use slightly more meaningful field name, i => pos, s = data

3- Leverage std.mem.indexOfScalarPos which can be more efficient for longer
   inputs (since it leverages SIMD)
2025-02-08 15:57:32 +08:00
Francis Bouvier
1594f148f8 Merge pull request #399 from karlseguin/generate
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Tweak generate.Tuple and generate.Union
2025-02-06 08:53:52 +01:00
Karl Seguin
fafd8c4af1 Improve format and re-add docstrings
Implements RP suggestions.
2025-02-06 09:57:04 +08:00
Karl Seguin
3d66758507 zig fmt 2025-02-01 15:38:08 +08:00
Karl Seguin
fc0ec860b0 Tweak generate.Tuple and generate.Union
Leverage comptime fields to give generated Tuple a default value, allowing
TupleT and Tuple to be merged.

Only call generate.Tuple on the final output. This eliminates redundant
deduplication, and results in a simpler API (nested types just need to expose
a natural Zig tuple).

generate.Union leverages the new Tuple and removes unused features.
2025-02-01 14:53:00 +08:00
Pierre Tachoire
00d332cd16 Merge pull request #396 from karlseguin/xmlserializer
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
Add HTML encoding to text node and HTML attribute values
2025-01-31 12:22:52 +01:00
Pierre Tachoire
4c8c0f8738 Merge pull request #394 from lightpanda-io/xmlserializer
implement XMLSerializer
2025-01-31 09:09:47 +01:00
Karl Seguin
54978132bb Add HTML encoding to text node and HTML attribute values 2025-01-31 16:01:32 +08:00
Pierre Tachoire
018abe0188 dom: implement outerHTML 2025-01-30 16:09:47 +01:00
Pierre Tachoire
b186497fb0 implement XMLSerializer 2025-01-30 16:09:47 +01:00
Pierre Tachoire
27f9963ccb Merge pull request #391 from lightpanda-io/cdp-ctx-sessionid
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
cdp: use an enum for SessionID
2025-01-30 14:02:47 +01:00
Pierre Tachoire
a4e3f03bf5 Merge pull request #393 from karlseguin/submodules_over_https
Use https:// instead of git@ for submodules
2025-01-30 14:01:43 +01:00
Pierre Tachoire
27a6be4ce0 Merge pull request #392 from karlseguin/readme_typo
Fix small install typo in readme
2025-01-30 08:59:39 +01:00
Karl Seguin
76a2520e56 Use https:// instead of git@ for submodules
Allows easily building the project, following the steps in readme, without
a github account or having some special git configuration.
2025-01-30 11:56:37 +08:00
Karl Seguin
0a472681af Fix small install typo in readme 2025-01-30 11:50:12 +08:00
Pierre Tachoire
6d530691f3 cdp: use an enum for SessionID 2025-01-29 18:38:05 +01:00
Pierre Tachoire
a74c9e8481 Merge pull request #389 from lightpanda-io/cdp-empty-params
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
chromedp: msg missing params or result
2025-01-28 17:11:03 +01:00
Pierre Tachoire
8aac26a331 cdp: check parameter's type on sendEvent
Disallow void type.
2025-01-28 16:01:47 +01:00
Pierre Tachoire
fc59a0f6ab cdp: send empty param instead of void
Sending void parameters generated unmarshal errors with chromedp client.
Empty struct is required.
2025-01-28 15:46:13 +01:00
Pierre Tachoire
3fb16774b7 Merge pull request #356 from lightpanda-io/location
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
dom: first draft for location
2025-01-27 13:16:05 +01:00
Pierre Tachoire
7b35bb4c0f dom: improve location impl 2025-01-27 12:33:06 +01:00
Pierre Tachoire
318e2bd1c6 dom: expose document.location 2025-01-27 12:33:05 +01:00
Pierre Tachoire
09ba4bcf43 upgrade libdom 2025-01-27 12:33:04 +01:00
Pierre Tachoire
0c89fa7b1e Merge pull request #383 from lightpanda-io/katie-lpd-patch-1
Update README.md
2025-01-27 10:26:42 +01:00
katie-lpd
7eedb3320d Update README.md 2025-01-27 09:59:30 +01:00
Pierre Tachoire
cfac75ea49 Merge pull request #380 from eltociear/patch-1
docs: update README.md
2025-01-26 20:59:27 +01:00
Ikko Eltociear Ashimine
f00a6c396f docs: update README.md
dependancies -> dependencies
dependancy -> dependency
2025-01-27 03:20:00 +09:00
Pierre Tachoire
e74a9711ca Merge pull request #378 from spidy0x0/patch-1
fix typo
2025-01-25 10:34:41 +01:00
Pierre Tachoire
636d3cdf90 Merge pull request #377 from arilotter/patch-1
fix typo in readme
2025-01-25 10:34:16 +01:00
spidy0x0
71966affa1 fix typo 2025-01-24 19:47:12 +00:00
Ari Lotter
bf4dc195ec fix typo in readme 2025-01-24 12:53:19 -05:00
Pierre Tachoire
dccca17e09 Merge pull request #376 from lightpanda-io/katie-lpd-patch-1
Update README.md
2025-01-24 12:03:09 +01:00
Pierre Tachoire
5381a4354c add badges 2025-01-24 12:02:42 +01:00
katie-lpd
c70425fbf7 Update README.md 2025-01-24 11:53:05 +01:00
Pierre Tachoire
341f5725a4 netsurf: implement location setter/getter 2025-01-23 15:22:03 +01:00
Pierre Tachoire
d7069df80d dom: first draft for location 2025-01-23 11:51:05 +01:00
Pierre Tachoire
579714a60b Merge pull request #374 from lightpanda-io/reuseport-unixsocket
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
server: REUSEPORT is not allowed on unix socket
2025-01-22 16:23:58 +01:00
Pierre Tachoire
bbdf63635a server: REUSEPORT is not allowed on unix socket
see 5b0af621c3
2025-01-22 16:08:22 +01:00
Pierre Tachoire
482ed8d958 Merge pull request #370 from lightpanda-io/kernel-version
upgrade vendor/zig-js-runtime
2025-01-22 13:58:22 +01:00
Pierre Tachoire
673e16878d ci: run tests on vendor changes 2025-01-22 13:48:52 +01:00
Pierre Tachoire
e11ceab029 upgrade vendor/zig-js-runtime 2025-01-22 13:46:54 +01:00
Pierre Tachoire
7fe719f43c Merge pull request #361 from lightpanda-io/docker-updage-zig-v8
docker: update zig v8
2025-01-17 12:36:53 +01:00
Pierre Tachoire
3fd3ac1de1 docker: update zig v8 2025-01-17 12:30:28 +01:00
Pierre Tachoire
0e90a675af Merge pull request #357 from katie-lpd/patch-2
Update README.md
2025-01-16 14:09:53 +01:00
katie-lpd
ee861c1f91 Update README.md 2025-01-16 14:06:00 +01:00
Pierre Tachoire
40c9355088 Merge pull request #355 from lightpanda-io/history
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
dom: history placeholder
2025-01-15 11:55:50 +01:00
Pierre Tachoire
8f1557254a typo fix 2025-01-15 11:45:30 +01:00
Pierre Tachoire
11d28b0bc3 dom: add placeholder for history interface 2025-01-15 11:45:30 +01:00
Pierre Tachoire
974cf780c0 dom: clean history file 2025-01-14 15:04:40 +01:00
Pierre Tachoire
73bb14e4a9 Merge pull request #285 from lightpanda-io/cdp-cdpcli
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
cdp: cdpcli compatibility
2025-01-13 18:16:40 +01:00
Pierre Tachoire
daf4236023 runtime: fix sessionid 2025-01-13 18:08:09 +01:00
Pierre Tachoire
4c9a24c64e start inspector when the js env starts 2025-01-13 10:53:38 +01:00
Pierre Tachoire
c149f65158 cdp: remove event dispateched by inspector 2025-01-13 10:53:36 +01:00
Pierre Tachoire
c5688c1bd3 cdp: display last message on cdp error 2025-01-13 10:53:35 +01:00
Pierre Tachoire
b276a15786 cdp: add target.detachFromTarget noop 2025-01-13 10:53:33 +01:00
Pierre Tachoire
2fed239ece browser: split page start from page navigate 2025-01-13 10:53:29 +01:00
Pierre Tachoire
8e2cb36597 cdp: fix some id inconsitency accross runtime messages 2025-01-13 10:49:48 +01:00
Pierre Tachoire
bcaace1c91 cdp: use identifiable hard coded ids 2025-01-13 10:47:51 +01:00
Pierre Tachoire
d664d07141 cdp: dispatch executionContextCreated on Runtime.enable 2025-01-13 10:47:42 +01:00
Pierre Tachoire
cb8b80c856 Merge pull request #345 from lightpanda-io/modules
browser: support for modules
2025-01-13 10:45:31 +01:00
Pierre Tachoire
d777d77b06 ci: update sig-v8 version 2025-01-13 10:36:52 +01:00
Pierre Tachoire
43678f8dc0 upgrade zig-js-runtime 2025-01-13 10:36:34 +01:00
Pierre Tachoire
5811577824 Merge pull request #354 from lightpanda-io/navigator-fix
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
navigator: remove useless import
2025-01-13 09:37:14 +01:00
Pierre Tachoire
1587122efa navigator: remove useless import 2025-01-10 17:42:20 +01:00
Pierre Tachoire
48e7c8ad0f browser: implement fetch module 2025-01-10 16:48:45 +01:00
Pierre Tachoire
766f9798f6 browser: load module 2025-01-09 11:48:39 +01:00
Pierre Tachoire
680d634725 update zig-js-runtime 2025-01-09 11:48:39 +01:00
Pierre Tachoire
7ac945bf88 browser: refacto script 2025-01-09 11:48:34 +01:00
Pierre Tachoire
188d7a8558 Merge pull request #352 from utay/fix-shell-main
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Fix shell main
2025-01-08 17:15:18 +01:00
Pierre Tachoire
ee561b0d1e Merge pull request #353 from lightpanda-io/cdp-enable-security
Cdp enable security
2025-01-08 16:55:31 +01:00
Pierre Tachoire
82bbe78e95 cdp: return correct result on Runtime 2025-01-08 16:46:05 +01:00
Pierre Tachoire
c761cd059b cdp: log errors on sendMessageToTarget 2025-01-08 16:17:29 +01:00
Pierre Tachoire
03e87155ca cdp: add security.enable 2025-01-08 16:17:20 +01:00
Yannick Utard
ea39cc52b1 Fix shell main 2025-01-08 15:28:19 +01:00
Pierre Tachoire
90fb90b186 Merge pull request #351 from lightpanda-io/ignore-blank
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
browser: ignore about:blank navigation
2025-01-08 13:54:00 +01:00
Pierre Tachoire
5f8327eaf7 browser: ignore about:blank navigation 2025-01-08 13:44:41 +01:00
Pierre Tachoire
07869f3c48 Merge pull request #350 from lightpanda-io/cdp-enable
Cdp enable
2025-01-08 13:42:42 +01:00
Pierre Tachoire
8377eb02a5 cdp: add CSS.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
3738e8eb44 cdp: add DOM.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
4b000e44b3 cdp: add Inspector.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
d6021d1702 Merge pull request #348 from lightpanda-io/dom-navigator
Dom navigator
2025-01-08 11:47:44 +01:00
Pierre Tachoire
b391eafc38 wpt: update tests 2025-01-08 11:38:11 +01:00
Pierre Tachoire
2af4545e10 dom: implement more navigator API 2025-01-08 11:03:26 +01:00
Pierre Tachoire
b96644d893 dom: implement navigatorLanguage 2025-01-08 11:03:26 +01:00
Pierre Tachoire
3cb77c0a32 dom: implement navigatorID 2025-01-08 11:03:25 +01:00
Pierre Tachoire
b3f7fb7be3 dom: implement navigator.userAgent 2025-01-08 11:03:11 +01:00
Pierre Tachoire
9fb51a1f29 Merge pull request #346 from lightpanda-io/target-created
cdp: add TargetCreated event on createTarget message
2025-01-08 10:10:18 +01:00
Pierre Tachoire
d78e8a725d cdp: remove useless parameter 2025-01-08 09:57:28 +01:00
Pierre Tachoire
7829bdbc95 Merge pull request #347 from lightpanda-io/send-message-to-target
cdp: add Target.sendMessageToTarget support
2025-01-07 16:21:41 +01:00
Pierre Tachoire
90ba6deba2 cdp: add Target.sendMessageToTarget support
see https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-sendMessageToTarget
2025-01-07 16:00:44 +01:00
Pierre Tachoire
5fc763a738 cdp: add TargetCreated event on createTarget message 2025-01-07 10:34:47 +01:00
Pierre Tachoire
84614e903c Merge pull request #344 from lightpanda-io/test-e2e
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Add end to end tests
2025-01-06 10:13:51 +01:00
Pierre Tachoire
c95d739347 ci: add test end to end
using puppeteer test from https://github.com/lightpanda-io/demo
2024-12-31 12:01:27 +01:00
Pierre Tachoire
c55849cef5 Merge pull request #343 from lightpanda-io/upgrade-zig-js
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
upgrade zig-js-runtime deps
2024-12-31 10:37:22 +01:00
Pierre Tachoire
88adb09417 upgrade zig-js-runtime deps 2024-12-31 10:09:48 +01:00
Pierre Tachoire
cb356ffca9 Merge pull request #341 from lightpanda-io/docker
fix docker port
2024-12-26 10:02:54 +01:00
Pierre Tachoire
2ba3f252ee fix docker port 2024-12-26 10:00:59 +01:00
Pierre Tachoire
ebe2c8e3dd Merge pull request #332 from lightpanda-io/verbose-option
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add --verbose option
2024-12-17 09:41:58 +01:00
Francis Bouvier
ca2d63edcf Merge pull request #330 from lightpanda-io/contributing
add a CONTRIBUTING file
2024-12-16 15:54:17 +01:00
Francis Bouvier
489a1a1c37 Merge pull request #338 from lightpanda-io/README
README: be more precise on current status
2024-12-16 13:43:45 +01:00
Francis Bouvier
5cad13eea3 README: be more precise on current status
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-16 13:35:24 +01:00
Pierre Tachoire
a3e09e015c Merge pull request #329 from lightpanda-io/readme-getting-started
readme: add a quick start section
2024-12-16 09:34:10 +01:00
Pierre Tachoire
22a6accac1 readme: add a quick start section 2024-12-13 17:45:30 +01:00
Pierre Tachoire
277c97a959 Merge pull request #331 from lightpanda-io/continue-on-error
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
browser: don't stop processing page on error status code
2024-12-13 17:41:34 +01:00
Pierre Tachoire
c08bc3239a Merge pull request #334 from lightpanda-io/patch-1
Update README.md
2024-12-13 15:05:34 +01:00
katie-lpd
f798c7e0fb Update README.md 2024-12-13 14:45:02 +01:00
Pierre Tachoire
193780a88f cla: add katie-lpd to the allow list
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
2024-12-13 14:33:57 +01:00
Pierre Tachoire
ab4973ab6c add --verbose option 2024-12-13 12:29:05 +01:00
Pierre Tachoire
f9da815e8f browser: don't stop processing page on error status code 2024-12-13 11:56:18 +01:00
Pierre Tachoire
59e187d59a add a CONTRIBUTING file 2024-12-13 11:28:14 +01:00
Francis Bouvier
3b018e2a6d Merge pull request #325 from lightpanda-io/await_promise
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
cdp: runtime, replace `"awaitPromise":true` only if present
2024-12-09 11:19:45 +01:00
Francis Bouvier
d4939c8260 Merge pull request #322 from lightpanda-io/cdp_msg_nullable_params
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
cdp: handle nullable Type for params
2024-12-08 19:08:27 +01:00
Francis Bouvier
485352594c Merge pull request #324 from lightpanda-io/json_parse_unknown_field
cdp: do not throw error on json parse for unknown fields
2024-12-08 19:05:11 +01:00
Francis Bouvier
bcdcfee467 Merge pull request #323 from lightpanda-io/cdp_msg_size
Cdp msg size
2024-12-08 18:58:14 +01:00
Francis Bouvier
0217e3fcae cdp: runtime, replace "awaitPromise":true only if present
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-08 15:33:32 +01:00
Francis Bouvier
6e7d6421d5 cdp: do not throw error on json parse for unknown fields
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-08 15:27:12 +01:00
Francis Bouvier
913d3af938 cdp: increase msg size 16KB -> 256KB
And move header size encoding from 2 bytes -> 2 bytes

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 23:06:55 +01:00
Francis Bouvier
53dd0a5e4c cdp: handle nullable Type for params
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 22:39:54 +01:00
Francis Bouvier
4b8c3cb188 Merge pull request #321 from lightpanda-io/README
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
README: more updates
2024-12-04 17:16:01 +01:00
Francis Bouvier
c9ca170d57 README: more updates
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 16:18:15 +01:00
Francis Bouvier
c6090dbc16 Update README
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 16:03:01 +01:00
Francis Bouvier
c787b6a1a5 Merge pull request #312 from lightpanda-io/fetch-polyfill
Fetch polyfill
2024-12-04 15:57:45 +01:00
Francis Bouvier
766aa0f60a Merge pull request #320 from lightpanda-io/http_json_version
websockets: add addr server info in Stream
2024-12-04 15:56:06 +01:00
Pierre Tachoire
fbe8835626 polyfill: use @embedfile to embed polyfill 2024-12-04 14:26:51 +01:00
Pierre Tachoire
4e2b35b585 tests: remove useless test file 2024-12-04 14:26:21 +01:00
Francis Bouvier
8ef79e348c websockets: add addr server info in Stream
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 12:07:52 +01:00
Pierre Tachoire
47eef392d1 add missing license header 2024-12-03 14:34:04 +01:00
Francis Bouvier
b846541ff6 Merge pull request #311 from lightpanda-io/msg_size_encode
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
Msg size encode
2024-12-03 14:33:33 +01:00
Pierre Tachoire
adfffd2b08 polyfill: fetch: disable Arraybuffer usage 2024-12-02 18:00:32 +01:00
Pierre Tachoire
4138c6fe95 polyfill: first draft to polyfill fetch api 2024-12-02 18:00:32 +01:00
Francis Bouvier
3088c7a632 Merge pull request #318 from lightpanda-io/README
Update README
2024-11-29 18:41:10 +01:00
Francis Bouvier
21e7638ae1 Update README
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 18:40:26 +01:00
Francis Bouvier
ca0d90dbcf Merge pull request #310 from lightpanda-io/websockets
websocket: first implementation
2024-11-29 18:22:05 +01:00
Pierre Tachoire
8870045b0f Merge pull request #317 from lightpanda-io/upgrade-zig-async-io
upgrade zig-async-io
2024-11-29 15:22:13 +01:00
Pierre Tachoire
cbc8b2edf9 upgrade zig-async-io 2024-11-29 15:12:13 +01:00
Francis Bouvier
8f297b83c1 msg: rename MsgBuffer -> Buffer
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 15:07:52 +01:00
Francis Bouvier
b800d0eeb8 msg: fix len for msg.Buffer and encode msg size as binary header
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 15:07:52 +01:00
Francis Bouvier
d95462073a websockets: fix port default in help
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 15:06:22 +01:00
Francis Bouvier
95ac92b343 server: fix cancel
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 14:59:54 +01:00
Pierre Tachoire
4cfb317af6 Merge pull request #316 from lightpanda-io/innerText
dom: implement innerText
2024-11-28 17:55:14 +01:00
Pierre Tachoire
487aaffa94 dom: implement innerText 2024-11-28 17:47:41 +01:00
Pierre Tachoire
24fd7c7286 Merge pull request #314 from lightpanda-io/fix-xhr
xhr: fix invalid response with empty type
2024-11-28 16:34:30 +01:00
Pierre Tachoire
50e62d44ff xhr: fix invalid response with empty type 2024-11-28 16:29:09 +01:00
Francis Bouvier
760c082757 cli: wording mode -> opts
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
Francis Bouvier
8449d5ab22 websocket: use Unix socket for internal server
And add an option for TCP only server

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
Francis Bouvier
27b50c46c3 Update websokets dep
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
Francis Bouvier
325ecedf0b websocket: first implementation
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
Francis Bouvier
8f7a8c0ee1 Merge pull request #309 from lightpanda-io/update_deps
update zig-js-runtime
2024-11-26 12:59:19 +01:00
Francis Bouvier
7dbb55da06 update zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-26 12:58:45 +01:00
Pierre Tachoire
4b90b70534 Merge pull request #308 from lightpanda-io/licensing
add licensing file to be more explicit w/ licenses
2024-11-26 12:52:16 +01:00
Pierre Tachoire
d6f1843ef3 add licensing file to be more explicit w/ licenses 2024-11-26 12:30:50 +01:00
Pierre Tachoire
5cf176927a Merge pull request #303 from lcoffe-botify/fix-makefile-download-zig
fix Makefile download-zig: url changed
2024-11-26 11:03:31 +01:00
Pierre Tachoire
eae21a034e Merge pull request #306 from lightpanda-io/cla
contrib: add CLA signature process
2024-11-26 10:31:17 +01:00
Pierre Tachoire
06855cdfa3 Merge pull request #307 from lightpanda-io/clean-ci
ci: remove useless token
2024-11-26 09:52:05 +01:00
Pierre Tachoire
45c7af9769 ci: remove useless token
The repos are public now, we don't need the token anymore to fetch.
2024-11-26 09:37:40 +01:00
Pierre Tachoire
f9ddd5c368 contrib: add CLA signature process 2024-11-26 09:35:14 +01:00
Pierre Tachoire
6bf4dc887e Merge pull request #305 from lightpanda-io/docker-ca-cert
docker: keep ca certs
2024-11-25 11:51:10 +01:00
Pierre Tachoire
4cc31bb41f docker: keep ca certs 2024-11-25 09:03:52 +01:00
Lucien Coffe
88e32505a3 fix Makefile download-zig: url changed 2024-11-22 18:50:11 +01:00
Francis Bouvier
5b5d28f7c1 Merge pull request #302 from lightpanda-io/zig-async-io
Use zig-async-io for xhr requests
2024-11-21 16:52:36 +01:00
Francis Bouvier
1a2fd9a584 Update dependencies
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-21 16:46:09 +01:00
Francis Bouvier
de286dd78e async: use zig-async-io
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-21 16:42:48 +01:00
Pierre Tachoire
70752027f1 async: remove @This from SigleThreaded 2024-11-19 15:55:26 +01:00
Pierre Tachoire
395eb3e8ad async: add missing tests execution 2024-11-19 15:54:04 +01:00
Pierre Tachoire
e1137274fb async: remove dead code 2024-11-19 15:51:36 +01:00
Francis Bouvier
bc716ef0ad Merge pull request #299 from lightpanda-io/cli_refacto
cli: code refacto
2024-11-19 15:31:19 +01:00
Pierre Tachoire
d2d2e851b0 async: fix assync call pop error 2024-11-18 17:39:38 +01:00
Pierre Tachoire
9149b60136 async: remove dead code 2024-11-18 17:39:38 +01:00
Pierre Tachoire
18ab0c8199 cdp: replace tick by run_for_ns 2024-11-18 17:39:38 +01:00
Pierre Tachoire
7fed1f3015 async: remove pseudo-async http client 2024-11-18 17:39:37 +01:00
Pierre Tachoire
6809bb5393 async: adapt async cli 2024-11-18 17:39:37 +01:00
Pierre Tachoire
fadf3f609a http: add full async client 2024-11-18 17:39:37 +01:00
Pierre Tachoire
8d2d089803 Merge pull request #301 from lightpanda-io/better-log
Better log
2024-11-18 14:39:40 +01:00
Pierre Tachoire
f6ad95c647 improve event's log result 2024-11-18 13:13:52 +01:00
Pierre Tachoire
0788e22dd5 typo fix 2024-11-18 13:13:23 +01:00
Francis Bouvier
de260693dc Merge pull request #296 from lightpanda-io/memory_leak
cdp: fix memory leak in msg parsing of the JSON
2024-11-15 02:17:01 +01:00
Francis Bouvier
f60fcbec04 Merge pull request #286 from lightpanda-io/cdp-refacto-input
cdp: refacto message JSON read
2024-11-15 01:03:53 +01:00
Francis Bouvier
4f99407462 Merge pull request #288 from lightpanda-io/cdp-create-target
cdp: browserContextId is optional in Target.createTarget
2024-11-15 00:53:22 +01:00
Pierre Tachoire
38813a20a7 Merge pull request #298 from lightpanda-io/docker-warning
docker: use absolute path with WORKDIR
2024-11-14 08:56:34 +01:00
Pierre Tachoire
2cd1e927f7 docker: use absolute path with WORKDIR
remove following the warning
```
 1 warning found (use docker --debug to expand):
 - WorkdirRelativePath: Relative workdir "browser" can have unexpected results if the base image changes (line 48)
 ```
2024-11-14 08:46:01 +01:00
Pierre Tachoire
5094942560 cdp: add msg tests into zig build test 2024-11-12 12:56:30 +01:00
Pierre Tachoire
82c37fc71b cdp: refacto message JSON read 2024-11-12 12:56:29 +01:00
Pierre Tachoire
8ba911c8dd cdp: return provided browser context id if any 2024-11-12 10:56:06 +01:00
Francis Bouvier
ac77453139 cli: code refacto
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-10 13:53:07 +01:00
Francis Bouvier
8a25545cac memory: use a GPA in Debug mode and a page allocator in Release
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-09 13:34:15 +01:00
Francis Bouvier
ed3a464843 cdp: fix memory leak in msg parsing of the JSON
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-09 03:25:42 +01:00
Francis Bouvier
1854074f64 Merge pull request #293 from lightpanda-io/cdp-contextid
cdp: use a u32 for context id
2024-11-07 15:49:40 +01:00
Francis Bouvier
ec5de2fce0 Merge pull request #287 from lightpanda-io/cdp-attach-to-target
cdp: add Target.attachToTarget noop
2024-11-07 15:49:15 +01:00
Francis Bouvier
3af34d11ca Merge pull request #291 from lightpanda-io/multi_build
Multi build
2024-11-06 18:17:24 +01:00
Francis Bouvier
eed7b7186d Merge pull request #284 from lightpanda-io/server-sync-deinit
server: ensure Send is always deinit in callback
2024-11-06 18:17:10 +01:00
Francis Bouvier
d5e7ebdc63 Merge pull request #295 from lightpanda-io/fix_cdp_full_async
Fix cdp full async
2024-11-06 18:14:43 +01:00
Francis Bouvier
3ecfa6aca8 Dockerfile: add install-libiconv
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-06 18:10:08 +01:00
Francis Bouvier
625c1741c6 Update zig-js-runtime (tigerbeetle)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-06 18:07:43 +01:00
Francis Bouvier
f6f5ec5eb3 server: add cancel current recv before accepting new connection
Only on Linux. On MacOS cancel is not supported for now and
we do not have any problem with the current recv operation
on a closed socket.

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-06 18:07:43 +01:00
Francis Bouvier
c74feb9c3a server: add log on I/O errors
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-05 17:16:39 +01:00
Pierre Tachoire
0d76f80223 cdp: use a u32 for context id 2024-11-04 10:08:36 +01:00
Pierre Tachoire
1e64513c16 Merge pull request #292 from lightpanda-io/tcp_nodelay
server: set TCP.NODELAY on linux to avoid latency issues
2024-11-04 10:04:25 +01:00
Francis Bouvier
64779acf32 Merge pull request #278 from lightpanda-io/cdp_full_async
Cdp full async
2024-11-01 18:14:21 +01:00
Francis Bouvier
c3a3ac19f4 server: set TCP.NODELAY on linux to avoid latency issues
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-01 17:54:49 +01:00
Francis Bouvier
b9bae3f66d build: update gitignore
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-30 13:41:48 +01:00
Francis Bouvier
2a2486cbe0 build: fix clean-libiconv
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-30 13:41:34 +01:00
Pierre Tachoire
0813d99b44 Merge pull request #290 from lightpanda-io/dockerfile
dockerfile: adjust binary name after merge
2024-10-30 10:42:15 +01:00
Pierre Tachoire
491e89d102 dockerfile: adjust binary name after merge 2024-10-30 10:40:59 +01:00
Francis Bouvier
f01558251c Merge pull request #277 from lightpanda-io/merge_bin
Merge get and server binaires
2024-10-29 22:27:09 +01:00
Francis Bouvier
8665d0420b Merge pull request #282 from lightpanda-io/docker-build
add a Dockerfile to build the project
2024-10-29 22:20:49 +01:00
Francis Bouvier
cf0636ca63 Update src/main.zig usage
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2024-10-29 22:19:44 +01:00
Francis Bouvier
46d0aa6f9e Remove all references to the name 'browsercore'
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-29 22:16:56 +01:00
Francis Bouvier
b9e2be2052 build: support multi os/arch conf for netsurf
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-29 20:06:29 +01:00
Pierre Tachoire
b3054d68bf cdp: browserContextId is optional in Target.createTarget
https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-createTarget
2024-10-29 10:37:23 +01:00
Pierre Tachoire
60adf0a9c3 cdp: add Target.attachToTarget noop 2024-10-29 10:34:36 +01:00
Francis Bouvier
be5d7022cc build: support multi os/arch conf for libiconv
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-28 21:08:46 +01:00
Francis Bouvier
d1951b286c build: support multi os/arch conf for mimalloc
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-28 16:06:41 +01:00
Pierre Tachoire
dcdef2f640 server: ensure Send is always deinit in callback 2024-10-25 09:51:37 +02:00
Pierre Tachoire
7afe74310f add a Dockerfile to build the project 2024-10-23 15:22:12 +02:00
Pierre Tachoire
826f82610e Merge pull request #280 from lightpanda-io/cdpdump-close-dir
cdp: close dir in dumpFile
2024-10-23 10:15:31 +02:00
Pierre Tachoire
5d7796b95d cdp: close dir in dumpFile
and avoid error.ProcessFdQuotaExceeded error
2024-10-23 10:02:34 +02:00
Pierre Tachoire
b3ac313cc7 Merge pull request #279 from lightpanda-io/ci-ubuntu
ci: use ubuntu 22.04 for nightly build
2024-10-22 15:43:39 +02:00
Pierre Tachoire
b281ba7754 ci: use zig-v8 0.1.9 2024-10-22 15:03:01 +02:00
Pierre Tachoire
10994b202b ci: use ubuntu latest for all expect nightly build 2024-10-22 14:27:47 +02:00
Pierre Tachoire
2aeac1bdeb ci: force ubuntu 22.04 for nightly build
To ensure a better compatibility.
2024-10-22 14:27:10 +02:00
Francis Bouvier
8508c21080 cdp: remove send sync
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-21 18:29:10 +02:00
Francis Bouvier
20dd140c31 cdp: send I/O next read before executing current cmd
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-21 18:21:43 +02:00
Francis Bouvier
486c19079a Merge get and server binaires
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-18 16:06:23 +02:00
Pierre Tachoire
f30501ca3c Merge pull request #276 from lightpanda-io/compare-position
node: implement node.compareDocumentPosition
2024-10-17 15:06:47 +02:00
Pierre Tachoire
e67e6e267b Merge pull request #275 from lightpanda-io/fake-css-properties
html: implement empty style property
2024-10-17 15:06:40 +02:00
Pierre Tachoire
8dc757ddf3 node: implement getRootNode 2024-10-17 14:44:34 +02:00
Pierre Tachoire
b64f7d013d node: implement node.compareDocumentPosition 2024-10-17 14:44:33 +02:00
Francis Bouvier
62ec936f1e Merge pull request #215 from lightpanda-io/cdp_basic
CDP basic
2024-10-17 10:52:09 +02:00
Francis Bouvier
8d83dfad45 ci: force ubuntu version (24.04)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-17 10:24:00 +02:00
Pierre Tachoire
e450072f45 ci: add zig v8 version into the cache key 2024-10-16 21:00:44 +02:00
Francis Bouvier
7f08d08a78 Update zig-v8 again
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 17:54:41 +02:00
Francis Bouvier
b0634cd871 Adapt wpt and shell to zig-js-runtime changes
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 15:21:03 +02:00
Francis Bouvier
462485bfcb Update zig-v8 and zig-js-runtime deps
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 14:56:04 +02:00
Francis Bouvier
2311765289 Remove some dead code
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 14:53:50 +02:00
Francis Bouvier
7bc7da5499 browser: back on createPage returning a Page (pointer)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 14:53:11 +02:00
Pierre Tachoire
b712a4771e html: implement empty style property 2024-10-16 10:22:23 +02:00
Francis Bouvier
8e05f09fc8 server, cdp: improve logging
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 22:57:56 +02:00
Francis Bouvier
84c49fbe34 cdp: ensure there is an ID on each request
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 17:28:18 +02:00
Francis Bouvier
7750956c7b msg: Add a more complex test case with 2 multipart messages combined
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 16:07:46 +02:00
Francis Bouvier
ea9af210f9 Remove heap allocation for Session
And adapt to similar changes on zig-js-runtime for Env

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 15:52:48 +02:00
Francis Bouvier
efca71510a browser: put back VM is an arg for browser init
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-12 10:41:59 +02:00
Francis Bouvier
cbf6348055 server: panic if sendInspector without an inspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-12 10:38:53 +02:00
Francis Bouvier
ec680593b0 msg: set a hard limit max size
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-11 18:13:20 +02:00
Francis Bouvier
fd6c25daaa msg: improve comments on reallocation
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-11 18:05:04 +02:00
Francis Bouvier
4b495f213f cdp: add comment on hard coded ID for page.createIsolatedWorld
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:21:09 +02:00
Francis Bouvier
7ad03fb548 cdp: fix a comment on page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:18:55 +02:00
Francis Bouvier
17c641845e msg: return error if input does not have "size:"
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:13:06 +02:00
Francis Bouvier
e53b9d984b browser: add comment for auxData param in page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:10:30 +02:00
Francis Bouvier
28593d93ff browser: panic if callInspector without Inspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:47:14 +02:00
Francis Bouvier
fa4920bd94 browser: rename setInspector -> initInspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:45:28 +02:00
Francis Bouvier
eaf5c6f86f cdp: ensure method action is present
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:42:20 +02:00
Francis Bouvier
0d89b98bad cdp: ensure token is a string when needed in parser
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:35:56 +02:00
Francis Bouvier
bf56345e48 msg: comments typos
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:19:35 +02:00
Francis Bouvier
2bc58bebce server: rename public -> jsruntime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:11:43 +02:00
Francis Bouvier
c564702eac server: formatting
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:10:54 +02:00
Francis Bouvier
9400dd799e Add cli options for server (host, port, timeout)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:06:39 +02:00
Francis Bouvier
ff0bbc3f96 server: simplify Send I/O
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 01:21:24 +02:00
Francis Bouvier
15414f5ee4 server: remove unused sendLater
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 01:00:12 +02:00
Francis Bouvier
f9b097794f Simplify browser session.setInspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:58:13 +02:00
Francis Bouvier
a2f65eb540 server: simplify onInspector methods
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:56:15 +02:00
Francis Bouvier
cea38a10e9 server: rename buf in read_buf
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:56:11 +02:00
Francis Bouvier
c8a91d4cf6 server: merge Cmd and Accept in Ctx
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:55:29 +02:00
Francis Bouvier
b0ff325125 server: move to TCP conn
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-08 23:44:47 +02:00
Francis Bouvier
c35c09db60 server: timeout mechanism
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-08 23:40:50 +02:00
Francis Bouvier
49adb61146 server: handle close and re-open connection
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-08 16:22:24 +02:00
Francis Bouvier
76a9034668 server: newSession on disposeBrowserContext
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-07 21:14:55 +02:00
Francis Bouvier
4c225e515d server: let the caller of sendSync free the string
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-07 16:04:29 +02:00
Francis Bouvier
9c913b2e6c Move loop outside Browser
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-07 15:57:16 +02:00
Francis Bouvier
5ab1d2a8a5 Add License in new cdp files
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 18:02:21 +02:00
Francis Bouvier
2f3a581859 Add TODOs and comments
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:48:54 +02:00
Francis Bouvier
8bdd2a14e8 Add Target.disposeBrowserContext
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:13:47 +02:00
Francis Bouvier
1675f69582 Add Target.closeTarget
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:13:29 +02:00
Francis Bouvier
94d2d28806 Redirect Runtime domain to JS engine Inspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:12:08 +02:00
Pierre Tachoire
82a5e50056 Merge pull request #274 from lightpanda-io/upgrade-libdom
upgrade libdom
2024-09-25 11:53:28 +02:00
Pierre Tachoire
46062e185a upgrade libdom 2024-09-25 11:47:34 +02:00
Pierre Tachoire
6929141210 Merge pull request #273 from lightpanda-io/nodelist-iterator
nodelist: remove debug log
2024-09-25 09:51:00 +02:00
Pierre Tachoire
cce36c5fbd nodelist: remove debug log 2024-09-25 09:50:31 +02:00
Pierre Tachoire
2518287326 Merge pull request #272 from lightpanda-io/nodelist-iterator
nodelist: implement iterators
2024-09-25 09:42:59 +02:00
Pierre Tachoire
aefab86501 nodelist: implement iterators 2024-09-25 09:37:14 +02:00
Pierre Tachoire
30679d18ee Merge pull request #271 from lightpanda-io/currentscript
implement DOM document.currentscript
2024-09-24 10:14:21 +02:00
Pierre Tachoire
95c0ff6f39 dom: implement currentScript 2024-09-24 10:01:13 +02:00
Pierre Tachoire
4d6f59ecb8 upgrade libdom 2024-09-24 10:01:12 +02:00
Pierre Tachoire
4b5668f4fd Merge pull request #270 from lightpanda-io/nodelist-foreach
DOM: implement nodelist.foreach
2024-09-20 18:37:22 +02:00
Pierre Tachoire
44a5fa011a dom: implement nodelist.foreach 2024-09-20 18:32:23 +02:00
Pierre Tachoire
c3fd0dbf7a Merge pull request #223 from lightpanda-io/settimout
dom: first draft for window setTimeout
2024-09-19 16:49:55 +02:00
Pierre Tachoire
aeaa745600 clearTimeout: ignore invalid timeout ids 2024-09-19 16:37:42 +02:00
Pierre Tachoire
be27359109 dom: implement clearTimeout 2024-09-19 16:37:42 +02:00
Pierre Tachoire
e8a2ce3614 upgrade zig-js-runtime lib 2024-09-19 16:37:40 +02:00
Pierre Tachoire
0559fb9365 dom: first draft for window setTimeout 2024-09-19 16:36:34 +02:00
Pierre Tachoire
89f898cfa9 Merge pull request #268 from lightpanda-io/HttpHeadersOversize
use 64KB for header buffer
2024-09-19 15:36:25 +02:00
Pierre Tachoire
183abc4610 use 64KB for header buffer 2024-09-18 09:24:57 +02:00
Pierre Tachoire
5dfdedea0e Merge pull request #266 from lightpanda-io/ci-node20
ci: upgrade cache action to node20
2024-09-13 15:04:11 +02:00
Pierre Tachoire
8d89c6053e ci: upgrade cache action to node20 2024-09-13 14:52:06 +02:00
Pierre Tachoire
58d184dba1 Merge pull request #265 from lightpanda-io/ci-cpu
ci: target cpu x86_64 to improve CPU compat
2024-09-13 14:47:58 +02:00
Pierre Tachoire
fd0813fead ci: target cpu x86_64 to improve CPU compat
The `-Dcpu` option force compiling using less CPU's capabilities.
This will improve the binary compatibility with older CPUs.
2024-09-13 14:36:44 +02:00
Pierre Tachoire
c9fae67649 Merge pull request #258 from lightpanda-io/tls.zig
use tls.zig with async client
2024-07-22 10:44:38 +02:00
Pierre Tachoire
3e7f9aaa82 Merge pull request #262 from lightpanda-io/browser-deinit
browser: fix invalid deinit order
2024-07-19 17:20:28 +02:00
Pierre Tachoire
22b03bf7df browser: fix invalid deinit order
http client depends on io loop and must be deinit before.
2024-07-19 17:15:53 +02:00
Pierre Tachoire
6a4d64ed00 use tls.zig with async client
see https://github.com/ziglang/zig/compare/master...ianic:zig:tls23 for
http.std.Client integration
2024-07-19 14:39:50 +02:00
Pierre Tachoire
df27ce09ca Merge pull request #260 from lightpanda-io/zig0.13
upgrade to zig 0.13
2024-07-19 14:36:13 +02:00
Pierre Tachoire
6da2954e0b upgrade to zig 0.13 2024-07-19 14:32:47 +02:00
Pierre Tachoire
95245229b0 upgrade zig-js-runtime 2024-07-19 14:32:47 +02:00
Pierre Tachoire
65c4b471af browser: handle wait with unstarted env 2024-07-19 11:14:50 +02:00
Pierre Tachoire
d6e0559efd upgrade to zig 0.13 2024-07-18 16:57:16 +02:00
Pierre Tachoire
5f5e01a2cf Merge pull request #253 from lightpanda-io/refacto_exec
Adapt to js_exec changes in zig-js-runtime
2024-07-18 16:56:41 +02:00
Pierre Tachoire
8c3939b842 wpt: remove useless Suite.stack 2024-07-18 16:52:32 +02:00
Pierre Tachoire
b537e52a6d browser: use log instead of std.log 2024-07-18 16:46:12 +02:00
Pierre Tachoire
4434e11bdd wpt: restore the test results 2024-07-18 16:40:44 +02:00
Francis Bouvier
b8ec53f708 Adapt to js_exec changes in zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-07-18 12:10:06 +02:00
Pierre Tachoire
f8395fec5c Merge pull request #257 from lightpanda-io/build-x86
ci: target cpu x86_64_v3+aes for compatibility
2024-07-17 17:04:20 +02:00
Pierre Tachoire
28155cb8d3 ci: target cpu x86_64_v3+aes for compatibility 2024-07-17 17:00:09 +02:00
Pierre Tachoire
f793278dfe Merge pull request #255 from lightpanda-io/ci
ci: remove container usage and download v8 from release
2024-07-17 15:16:41 +02:00
Pierre Tachoire
cdbbc71b0a ci: add nightly build for macos aarch64 2024-07-17 10:28:24 +02:00
Pierre Tachoire
bfa6f55551 ci: remove container usage and download v8 from release 2024-07-17 09:56:34 +02:00
Pierre Tachoire
5fb25cf015 Merge pull request #254 from lightpanda-io/nightly
ci: add linux amd64 nightly build
2024-07-16 14:29:36 +02:00
Pierre Tachoire
5cad450e5a ci: add linux amd64 nightly build 2024-07-16 12:47:26 +02:00
Francis Bouvier
14a3a662fd Fix response of runtime.Evaluate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-07-09 16:10:25 +02:00
Francis Bouvier
41409031fd Adapt to refacto in js_exec from zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-07-08 22:51:41 +02:00
Francis Bouvier
ea410c8ced Fix changes in Zig 0.12 std lib
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-20 00:32:06 +02:00
Francis Bouvier
aca64eedca Uniformize calling name conventions
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-19 15:56:44 +02:00
Francis Bouvier
0f8b47b598 Move MsgBuffer in it's own file for unit test purpose
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-19 15:48:20 +02:00
Francis Bouvier
5eae15889d Add some optional fields in Runtime.evaluate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-19 15:23:09 +02:00
Francis Bouvier
9319e4a7f1 Handle Runtime.callFunctionOn
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-17 16:35:22 +02:00
Francis Bouvier
4d756b5bfc Add a dumpFile utility function
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-17 16:34:47 +02:00
Francis Bouvier
409969621d Add Runtime.addBinding
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-12 17:56:54 +02:00
Francis Bouvier
7abb7277c9 Fix call to Runtime.executionContextCreated in Page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-12 17:56:07 +02:00
Francis Bouvier
9120b9c1de Add emulation.setTouchEmulationEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:19:08 +02:00
Francis Bouvier
08c11ac41f Add performance.enable
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:16:15 +02:00
Francis Bouvier
cecc03e1ed Add fetch.disable
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:12:31 +02:00
Francis Bouvier
7d67d131c2 Add network.setCacheDisabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:08:49 +02:00
Francis Bouvier
1929eed8ac Add contextID in state
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:03:12 +02:00
Francis Bouvier
ad8c9fac2b Add target.setDiscoverTargets
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:02:37 +02:00
Francis Bouvier
fa82160265 Add target.getBrowserContexts
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:02:02 +02:00
Francis Bouvier
dc1456f4e8 Handle CDP messages with different order
The 'method' still needs to be the first or the second key
(in this case after the 'id').

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 15:59:57 +02:00
Francis Bouvier
3ad19dffa1 Handle CDP msg with order <id, method> and <method, id>
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-30 17:43:01 +02:00
Francis Bouvier
bfb9db235e Basic Runtime.evaluate run
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-30 16:21:18 +02:00
Francis Bouvier
c57e50c5b9 Handle Runtime.evaluate (no-op)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-27 16:02:14 +02:00
Francis Bouvier
bafdca3ffa MsgBuffer to handle both combined and multipart read
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-22 16:24:39 +02:00
Francis Bouvier
ba12945e5b Move read input from Cmd callback to allow unit tests
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-24 11:17:55 +02:00
Francis Bouvier
96906df64b Implement own protocol to handle msg size
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 12:48:35 +02:00
Francis Bouvier
3396c70b67 Send Runtime.executionContextCreated events in Page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 10:44:33 +02:00
Francis Bouvier
28d5c682cd Use sendEvent in Runtime.executionContextCreated and expose it
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 10:44:03 +02:00
Francis Bouvier
7a03562a33 Typo fix Page.LifecycleEvent
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 10:43:11 +02:00
Francis Bouvier
4a31dd8aa3 Let Page.navigate do actually navigation
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 17:13:32 +02:00
Francis Bouvier
1b1b7cdfb0 Add page_life_cycle_events in CDP state
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 17:12:37 +02:00
Francis Bouvier
9e13ffb8ff Add sendEvent utility function
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 17:11:31 +02:00
Francis Bouvier
ed38705efd Basic version using Browser
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 11:57:44 +02:00
Francis Bouvier
1a1cd0353c Add dummy Page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 11:36:02 +02:00
Francis Bouvier
4f0b071c59 Fix getContent algo
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 11:35:47 +02:00
Francis Bouvier
9ce574a1f0 Add Page.createIsolatedWorld
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:57:31 +02:00
Francis Bouvier
c54b50eb0c Add Browser.setWindowBounds
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:52:06 +02:00
Francis Bouvier
aec7455151 Add Emulation.setDeviceMetricsOverride
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:46:43 +02:00
Francis Bouvier
c7ba567d7f Handle non-empty void params in getContent
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:45:46 +02:00
Francis Bouvier
fc1b3d5397 Contextual frameTree
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 20:54:30 +02:00
Francis Bouvier
508741c367 Add Browser.getWindowForTarget
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 20:53:18 +02:00
Francis Bouvier
f02de77295 Add getContent
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 20:38:27 +02:00
Francis Bouvier
9974b56607 Add Target.createTarget
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 16:43:19 +02:00
Francis Bouvier
0506a7bb53 Add Browser.createBrowserContext
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 16:14:19 +02:00
Francis Bouvier
06f161c423 Add Target.getTargetInfo
+ do not send attachedToTarget if sessionId

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 13:20:23 +02:00
Francis Bouvier
69f5bb9ed3 Add sessionId in Runime.runIfWaitingForDebugger response
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 13:18:51 +02:00
Francis Bouvier
490eb40028 Add method cdp function
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 13:18:16 +02:00
Francis Bouvier
43a558f5ae Make getParams return nullable
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 12:10:20 +02:00
Francis Bouvier
e4ae2df1a4 Add some optional params in methods
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 11:57:39 +02:00
Francis Bouvier
1620138421 Return sessionId in Emulation.setFocusEmulationEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 11:32:11 +02:00
Francis Bouvier
e59fc903f2 Return a result in Page.getFrameTree
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 10:20:47 +02:00
Francis Bouvier
4d8cdc6dc8 Handle sessionId in result
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-17 14:18:18 +02:00
Francis Bouvier
21afa1f4b3 Do not emit optional null value in JSON output
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-17 14:04:34 +02:00
Francis Bouvier
05c5d06df5 Change Page.addScriptToEvaluateOnNewDocument
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 17:28:28 +02:00
Francis Bouvier
9e8b765f7a Allow method with sessionId and use it when appropriate (*.enable)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 17:19:50 +02:00
Francis Bouvier
36dbc28bde Add Runtime.runIfWaitingForDebugger
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 16:40:50 +02:00
Francis Bouvier
26eda90f7e Add setFocusEmulationEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 16:38:47 +02:00
Francis Bouvier
211fa3d947 Handle several JSON msg in 1 read
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 16:38:15 +02:00
Francis Bouvier
67bbd9957d Add Network domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 01:03:04 +02:00
Francis Bouvier
aff2250504 Add Emulation domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 01:02:44 +02:00
Francis Bouvier
86b1c851c0 Add Page.addScriptToEvaluateOnNewDocument
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:57:25 +02:00
Francis Bouvier
0a03dcb465 Add Page.setLifecycleEventsEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:54:40 +02:00
Francis Bouvier
e073e3388d Add Runtime domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:50:17 +02:00
Francis Bouvier
626fae0da0 Add Log domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:48:40 +02:00
Francis Bouvier
a708a7f387 Add Page.getFrameTree
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:48:17 +02:00
Francis Bouvier
b1242207a9 Add Page domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:41:26 +02:00
Francis Bouvier
980571073d Big refacto
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:38:06 +02:00
Francis Bouvier
5e1fe656e8 send Target.attachedToTarget after Target.setAutoAttach
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 21:34:00 +02:00
Francis Bouvier
ffbfd36502 Add stringify function in cdp
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 21:33:32 +02:00
Francis Bouvier
e908cb0ec4 Use send as normal behavior in cmdCallback
+ add nanoseconds param in sendLater

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 21:32:14 +02:00
Francis Bouvier
95a64b7696 Handle concurrent calls to sendLater
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 17:57:33 +02:00
Francis Bouvier
cfd6fc9532 Working sendLater (I/O timeout)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 17:36:22 +02:00
Francis Bouvier
defab0c774 Free msg at the right place
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 15:52:13 +02:00
Francis Bouvier
babac692d5 Remove alloc from CmdContext struct
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 15:47:19 +02:00
Francis Bouvier
c57bb9ef72 WIP: CDP
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 12:14:33 +02:00
83 changed files with 9266 additions and 2089 deletions

View File

@@ -1,23 +1,73 @@
name: "Browsercore install"
description: "Install deps for the project browsercore"
inputs:
zig:
description: 'Zig version to install'
required: false
default: '0.13.0'
arch:
description: 'CPU arch used to select the v8 lib'
required: false
default: 'x86_64'
os:
description: 'OS used to select the v8 lib'
required: false
default: 'linux'
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.11'
v8:
description: 'v8 version to install'
required: false
default: '11.1.134'
cache-dir:
description: 'cache dir to use'
required: false
default: '~/.cache'
runs:
using: "composite"
steps:
- name: Install apt deps
if: ${{ inputs.os == 'linux' }}
shell: bash
run: sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
- uses: mlugg/setup-zig@v1
with:
version: ${{ inputs.zig }}
- name: Cache v8
id: cache-v8
uses: actions/cache@v4
env:
cache-name: cache-v8
with:
path: ${{ inputs.cache-dir }}/v8
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
shell: bash
run: |
mkdir -p ${{ inputs.cache-dir }}/v8
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
- name: install v8
shell: bash
run: |
mkdir -p vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/debug
ln -s /usr/local/lib/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/debug/libc_v8.a
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a
mkdir -p vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/release
ln -s /usr/local/lib/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/release/libc_v8.a
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a
- name: libiconv
shell: bash
run: |
ln -s /usr/local/lib/libiconv vendor/libiconv
run: make install-libiconv
- name: build mimalloc
shell: bash

73
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: nightly build
on:
schedule:
- cron: "2 2 * * *"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
permissions:
contents: write
jobs:
build-linux-x86_64:
env:
ARCH: x86_64
OS: linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly
build-macos-aarch64:
env:
ARCH: aarch64
OS: macos
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
with:
os: ${{env.OS}}
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly

32
.github/workflows/cla.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
permissions:
actions: write
contents: read
pull-requests: write
statuses: write
jobs:
CLAAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
uses: contributor-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_GH_PAT }}
with:
path-to-signatures: 'signatures/browser/version1/cla.json'
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
remote-organization-name: lightpanda-io
remote-repository-name: cla

View File

@@ -1,7 +1,6 @@
name: wpt
env:
ARCH: x86_64-linux
AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
@@ -46,22 +45,11 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# docker blocks io_uring syscalls by default now.
# see https://github.com/tigerbeetle/tigerbeetle/pull/1995
# see https://github.com/moby/moby/pull/46762
options: "--security-opt seccomp=unconfined"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive

View File

@@ -1,5 +1,8 @@
name: zig-fmt
env:
ZIG_VERSION: 0.13.0
on:
pull_request:
@@ -26,15 +29,12 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig:0.12.1
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
outputs:
zig_fmt_errs: ${{ steps.fmt.outputs.zig_fmt_errs }}
steps:
- uses: mlugg/setup-zig@v1
with:
version: ${{ env.ZIG_VERSION }}
- uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -1,7 +1,6 @@
name: zig-test
env:
ARCH: x86_64-linux
AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
@@ -17,6 +16,7 @@ on:
- "src/*.zig"
- "vendor/zig-js-runtime"
- ".github/**"
- "vendor/**"
pull_request:
# By default GH trigger on types opened, synchronize and reopened.
@@ -33,6 +33,7 @@ on:
- "src/*.zig"
- "vendor/**"
- ".github/**"
- "vendor/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -44,17 +45,11 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -63,6 +58,14 @@ 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
@@ -70,17 +73,11 @@ jobs:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -96,22 +93,11 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# docker blocks io_uring syscalls by default now.
# see https://github.com/tigerbeetle/tigerbeetle/pull/1995
# see https://github.com/moby/moby/pull/46762
options: "--security-opt seccomp=unconfined"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -155,3 +141,30 @@ 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

5
.gitignore vendored
View File

@@ -1,6 +1,5 @@
zig-cache
/.zig-cache/
zig-out
/vendor/netsurf/build/
/vendor/netsurf/lib/
/vendor/netsurf/include/
/vendor/netsurf/out
/vendor/libiconv/

18
.gitmodules vendored
View File

@@ -1,24 +1,30 @@
[submodule "vendor/zig-js-runtime"]
path = vendor/zig-js-runtime
url = git@github.com:lightpanda-io/zig-js-runtime.git
url = https://github.com/lightpanda-io/zig-js-runtime.git/
[submodule "vendor/netsurf/libwapcaplet"]
path = vendor/netsurf/libwapcaplet
url = git@github.com:lightpanda-io/libwapcaplet.git
url = https://github.com/lightpanda-io/libwapcaplet.git/
[submodule "vendor/netsurf/libparserutils"]
path = vendor/netsurf/libparserutils
url = git@github.com:lightpanda-io/libparserutils.git
url = https://github.com/lightpanda-io/libparserutils.git/
[submodule "vendor/netsurf/libdom"]
path = vendor/netsurf/libdom
url = git@github.com:lightpanda-io/libdom.git
url = https://github.com/lightpanda-io/libdom.git/
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
path = vendor/netsurf/share/netsurf-buildsystem
url = https://source.netsurf-browser.org/buildsystem.git
[submodule "vendor/netsurf/libhubbub"]
path = vendor/netsurf/libhubbub
url = git@github.com:lightpanda-io/libhubbub.git
url = https://github.com/lightpanda-io/libhubbub.git/
[submodule "tests/wpt"]
path = tests/wpt
url = https://github.com/lightpanda-io/wpt
[submodule "vendor/mimalloc"]
path = vendor/mimalloc
url = git@github.com:microsoft/mimalloc.git
url = https://github.com/microsoft/mimalloc.git/
[submodule "vendor/tls.zig"]
path = vendor/tls.zig
url = https://github.com/ianic/tls.zig.git/
[submodule "vendor/zig-async-io"]
path = vendor/zig-async-io
url = https://github.com/lightpanda-io/zig-async-io.git/

93
CLA.md Normal file
View File

@@ -0,0 +1,93 @@
# Lightpanda (Selecy SAS) Grant and Contributor License Agreement (“Agreement”)
This agreement is based on the Apache Software Foundation Contributor License
Agreement. (v r190612)
Thank you for your interest in software projects stewarded by Lightpanda
(Selecy SAS) (“Lightpanda”). In order to clarify the intellectual property
license granted with Contributions from any person or entity, Lightpanda must
have a Contributor License Agreement (CLA) on file that has been agreed to by
each Contributor, indicating agreement to the license terms below. This license
is for your protection as a Contributor as well as the protection of Lightpanda
and its users; it does not change your rights to use your own Contributions for
any other purpose. This Agreement allows an individual to contribute to
Lightpanda on that individuals own behalf, or an entity (the “Corporation”) to
submit Contributions to Lightpanda, to authorize Contributions submitted by its
designated employees to Lightpanda, and to grant copyright and patent licenses
thereto.
You accept and agree to the following terms and conditions for Your present and
future Contributions submitted to Lightpanda. Except for the license granted
herein to Lightpanda and recipients of software distributed by Lightpanda, You
reserve all right, title, and interest in and to Your Contributions.
1. Definitions. “You” (or “Your”) shall mean the copyright owner or legal
entity authorized by the copyright owner that is making this Agreement with
Lightpanda. For legal entities, the entity making a Contribution and all
other entities that control, are controlled by, or are under common control
with that entity are considered to be a single Contributor. For the purposes
of this definition, “control” means (i) the power, direct or indirect, to
cause the direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
“Contribution” shall mean any work, as well as any modifications or
additions to an existing work, that is intentionally submitted by You to
Lightpanda for inclusion in, or documentation of, any of the products owned
or managed by Lightpanda (the “Work”). For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication
sent to Lightpanda or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems (such
as GitHub), and issue tracking systems that are managed by, or on behalf of,
Lightpanda for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise designated
in writing by You as “Not a Contribution.”
2. Grant of Copyright License. Subject to the terms and conditions of this
Agreement, You hereby grant to Lightpanda and to recipients of software
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable copyright license to reproduce, prepare derivative
works of, publicly display, publicly perform, sublicense, and distribute
Your Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this
Agreement, You hereby grant to Lightpanda and to recipients of software
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable (except as stated in this section) patent license
to make, have made, use, offer to sell, sell, import, and otherwise transfer
the Work, where such license applies only to those patent claims licensable
by You that are necessarily infringed by Your Contribution(s) alone or by
combination of Your Contribution(s) with the Work to which such
Contribution(s) were submitted. If any entity institutes patent litigation
against You or any other entity (including a cross-claim or counterclaim in
a lawsuit) alleging that your Contribution, or the Work to which you have
contributed, constitutes direct or contributory patent infringement, then
any patent licenses granted to that entity under this Agreement for that
Contribution or Work shall terminate as of the date such litigation is
filed.
4. You represent that You are legally entitled to grant the above license. If
You are an individual, and if Your employer(s) has rights to intellectual
property that you create that includes Your Contributions, you represent
that You have received permission to make Contributions on behalf of that
employer, or that Your employer has waived such rights for your
Contributions to Lightpanda. If You are a Corporation, any individual who
makes a contribution from an account associated with You will be considered
authorized to Contribute on Your behalf.
5. You represent that each of Your Contributions is Your original creation (see
section 7 for submissions on behalf of others).
6. You are not expected to provide support for Your Contributions,except to the
extent You desire to provide support. You may provide support for free, for
a fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may
submit it to Lightpanda separately from any Contribution, identifying the
complete details of its source and of any license or other restriction
(including, but not limited to, related patents, trademarks, and license
agreements) of which you are personally aware, and conspicuously marking the
work as “Submitted on behalf of a third-party: [named here]”.

10
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,10 @@
# 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).

79
Dockerfile Normal file
View File

@@ -0,0 +1,79 @@
FROM ubuntu:22.04
ARG ZIG=0.13.0
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG OS=linux
ARG ARCH=x86_64
ARG V8=11.1.134
ARG ZIG_V8=v0.1.11
RUN apt-get update -yq && \
apt-get install -yq xz-utils \
python3 ca-certificates git \
pkg-config libglib2.0-dev \
gperf libexpat1-dev \
cmake clang \
curl git
# install minisig
RUN curl -L -O https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz && \
tar xvzf minisign-0.11-linux.tar.gz
# install zig
RUN curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz && \
curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz.minisig
RUN minisign-linux/x86_64/minisign -Vm zig-linux-x86_64-${ZIG}.tar.xz -P ${ZIG_MINISIG}
# clean minisg
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
# install zig
RUN tar xvf zig-linux-x86_64-${ZIG}.tar.xz && \
mv zig-linux-x86_64-${ZIG} /usr/local/lib && \
ln -s /usr/local/lib/zig-linux-x86_64-${ZIG}/zig /usr/local/bin/zig
# clean up zig install
RUN rm -fr zig-linux-x86_64-${ZIG}.tar.xz zig-linux-x86_64-${ZIG}.tar.xz.minisig
# force use of http instead of ssh with github
RUN cat <<EOF > /root/.gitconfig
[url "https://github.com/"]
insteadOf="git@github.com:"
EOF
# clone lightpanda
RUN git clone git@github.com:lightpanda-io/browser.git
WORKDIR /browser
# install deps
RUN git submodule init && \
git submodule update --recursive
RUN cd vendor/zig-js-runtime && \
git submodule init && \
git submodule update --recursive
RUN make install-libiconv && \
make install-netsurf && \
make install-mimalloc
# download and install v8
RUN curl -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_${OS}_${ARCH}.a && \
mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release && \
mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release/libc_v8.a
# build release
RUN make build
FROM ubuntu:22.04
# copy ca certificates
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
EXPOSE 9222/tcp
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "9222"]

23
LICENSING.md Normal file
View File

@@ -0,0 +1,23 @@
# Licensing
License names used in this document are as per [SPDX License
List](https://spdx.org/licenses/).
The default license for this project is [AGPL-3.0-only](LICENSE).
## MIT
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
original upstream licenses:
```
vendor/
tests/wpt/
```

108
Makefile
View File

@@ -3,6 +3,27 @@
ZIG := zig
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# option test filter make unittest F="server"
F=
# OS and ARCH
kernel = $(shell uname -ms)
ifeq ($(kernel), Darwin arm64)
OS := macos
ARCH := aarch64
else ifeq ($(kernel), Linux aarch64)
OS := linux
ARCH := aarch64
else ifeq ($(kernel), Linux arm64)
OS := linux
ARCH := aarch64
else ifeq ($(kernel), Linux x86_64)
OS := linux
ARCH := x86_64
else
$(error "Unhandled kernel: $(kernel)")
endif
# Infos
# -----
@@ -23,33 +44,14 @@ help:
# $(ZIG) commands
# ------------
.PHONY: build build-dev run run-release shell test bench download-zig wpt
.PHONY: build build-dev run run-release shell test bench download-zig wpt unittest
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
kernel = $(shell uname -ms)
## Download the zig recommended version
download-zig:
ifeq ($(kernel), Darwin x86_64)
$(eval target="macos")
$(eval arch="x86_64")
else ifeq ($(kernel), Darwin arm64)
$(eval target="macos")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux aarch64)
$(eval target="linux")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux arm64)
$(eval target="linux")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux x86_64)
$(eval target="linux")
$(eval arch="x86_64")
else
$(error "Unhandled kernel: $(kernel)")
endif
$(eval url = "https://ziglang.org/builds/zig-$(target)-$(arch)-$(zig_version).tar.xz")
$(eval dest = "/tmp/zig-$(target)-$(arch)-$(zig_version).tar.xz")
$(eval url = "https://ziglang.org/download/$(zig_version)/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
$(eval dest = "/tmp/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
@printf "\e[36mDownload zig version $(zig_version)...\e[0m\n"
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mDownloaded $(dest)\e[0m\n"
@@ -69,7 +71,7 @@ build-dev:
## Run the server in debug mode
run: build
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/browsercore || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run a JS shell in debug mode
shell:
@@ -91,6 +93,9 @@ test:
@$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
@printf "\e[33mTest OK\e[0m\n"
unittest:
@TEST_FILTER='${F}' $(ZIG) build unittest -freference-trace --summary all
# Install and build required dependencies commands
# ------------
.PHONY: install-submodule
@@ -100,10 +105,10 @@ test:
.PHONY: install-dev install
## Install and build dependencies for release
install: install-submodule install-zig-js-runtime install-netsurf install-mimalloc
install: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc
## Install and build dependencies for dev
install-dev: install-submodule install-zig-js-runtime-dev install-netsurf-dev install-mimalloc-dev
install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev
install-netsurf-dev: _install-netsurf
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
@@ -111,14 +116,16 @@ install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
install-netsurf: _install-netsurf
install-netsurf: OPTCFLAGS := -DNDEBUG
BC_NS := $(BC)vendor/netsurf
ICONV := $(BC)vendor/libiconv
BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH)
ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
# TODO: add Linux iconv path (I guess it depends on the distro)
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
# and stick to a specif version. Maybe build from source. Anyway not now.
_install-netsurf: install-libiconv
_install-netsurf: clean-netsurf
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
ls $(ICONV) 1> /dev/null || (printf "\e[33mERROR: you need to install libiconv in your system (on MacOS on with Homebrew)\e[0m\n"; exit 1;) && \
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
mkdir -p $(BC_NS) && \
cp -R vendor/netsurf/share $(BC_NS) && \
export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
@@ -156,10 +163,7 @@ _install-netsurf: install-libiconv
clean-netsurf:
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
cd vendor/netsurf && \
rm -R build && \
rm -R lib && \
rm -R include
rm -Rf $(BC_NS)
test-netsurf:
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
@@ -169,14 +173,22 @@ test-netsurf:
cd vendor/netsurf/libdom && \
BUILDDIR=$(BC_NS)/build/libdom make test
install-libiconv:
ifeq ("$(wildcard vendor/libiconv/lib/libiconv.a)","")
download-libiconv:
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
@mkdir -p vendor/libiconv
@cd vendor/libiconv && \
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
endif
install-libiconv: download-libiconv clean-libiconv
@cd vendor/libiconv/libiconv-1.17 && \
./configure --prefix=$(BC)vendor/libiconv --enable-static && \
./configure --prefix=$(ICONV) --enable-static && \
make && make install
clean-libiconv:
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
@cd vendor/libiconv/libiconv-1.17 && \
make clean
endif
install-zig-js-runtime-dev:
@@ -188,24 +200,28 @@ install-zig-js-runtime:
make install
.PHONY: _build_mimalloc
_build_mimalloc:
@cd vendor/mimalloc && \
mkdir -p out/include && \
cp include/mimalloc.h out/include/ && \
cd out && \
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) .. && \
make
MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH)
_build_mimalloc: clean-mimalloc
@mkdir -p $(MIMALLOC)/build && \
cd $(MIMALLOC)/build && \
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \
make && \
mkdir -p $(MIMALLOC)/lib
install-mimalloc-dev: _build_mimalloc
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
install-mimalloc-dev:
@cd vendor/mimalloc/out && \
mv libmimalloc-debug.a libmimalloc.a
@cd $(MIMALLOC) && \
mv build/libmimalloc-debug.a lib/libmimalloc.a
install-mimalloc: _build_mimalloc
install-mimalloc:
@cd $(MIMALLOC) && \
mv build/libmimalloc.a lib/libmimalloc.a
clean-mimalloc:
@rm -fr vendor/mimalloc/lib/*
@rm -Rf $(MIMALLOC)/build
## Init and update git submodule
install-submodule:

172
README.md
View File

@@ -2,81 +2,113 @@
<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</h1>
<h1 align="center">Lightpanda Browser</h1>
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
<div align="center">
<br />
[![Commit Activity](https://img.shields.io/github/commit-activity/m/lightpanda-io/browser)](https://github.com/lightpanda-io/browser/commits/main)
[![License](https://img.shields.io/github/license/lightpanda-io/browser)](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
[![Twitter Follow](https://img.shields.io/twitter/follow/lightpanda_io)](https://twitter.com/lightpanda_io)
[![GitHub stars](https://img.shields.io/github/stars/lightpanda-io/browser)](https://github.com/lightpanda-io/browser)
</div>
<div align="center">
<a href="https://trendshift.io/repositories/12815" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12815" alt="lightpanda-io%2Fbrowser | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of the Web APIs (partial, WIP)
- Support of Web APIs (partial, WIP)
- Compatible with Playwright, Puppeteer through CDP (WIP)
Fast scraping and web automation with minimal memory footprint:
Fast web automation for AI agents, LLM training, scraping and testing with minimal memory footprint:
- Ultra-low memory footprint (12x less than Chrome)
- Blazingly fast & instant startup (64x faster than Chrome)
- Ultra-low memory footprint (9x less than Chrome)
- Exceptionally fast execution (11x faster than Chrome) & instant startup
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark2.png">
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png">
See [benchmark details](https://github.com/lightpanda-io/demo).
## Why?
## Quick start
### Javascript execution is mandatory for the modern web
### Install from the nightly builds
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:
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.
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
- JS web frameworks: React, Vue, Angular & others
```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]
### Chrome is not the right tool
start Lightpanda browser
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?
* if an url is provided the browser will fetch the page and exit
* otherwhise the browser starts a CDP server
- Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale
- Bloated, lots of features are not useful in headless usage
-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)
```
### Lightpanda is built for performance
### Dump an URL
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:
```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>
```
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated, no rendering
### Start a CDP server
## Status
```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...
```
Lightpanda is still a work in progress and is currently at the Alpha stage.
Once the CDP server started, you can run a Puppeteer script by configuring the
`browserWSEndpoint`.
Here are the key features we want to implement before releasing a Beta version:
```js
'use strict'
- [x] Loader
- [x] HTML parser and DOM tree
- [x] Javascript support
- [x] Basic DOM APIs
- [x] Ajax
- [x] XHR API
- [ ] Fetch API
- [x] DOM dump
- [ ] Basic CDP server
import puppeteer from 'puppeteer-core';
We will not provide binary versions until we reach at least the Beta stage.
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:9222",
});
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.
// The rest of your script remains the same.
const context = await browser.createBrowserContext();
const page = await context.newPage();
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.
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.12.1`. You have to
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to
install it with the right version in order to build the project.
Lightpanda also depends on
@@ -108,9 +140,9 @@ brew install cmake
You can run `make install` to install deps all in one (or `make install-dev` if you need the development versions).
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependancies, including the v8 Javascript engine.
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependencies, including the v8 Javascript engine.
#### Step by step build dependancy
#### Step by step build dependency
The project uses git submodules for dependencies.
@@ -154,7 +186,7 @@ This build task is very long and cpu consuming, as you will build v8 from source
make install-zig-js-runtime
```
For dev env, use `make iinstall-zig-js-runtime-dev`.
For dev env, use `make install-zig-js-runtime-dev`.
## Test
@@ -196,3 +228,57 @@ To add a new test, copy the file you want from the [WPT
repo](https://github.com/web-platform-tests/wpt) into the `tests/wpt` directory.
:warning: Please keep the original directory tree structure of `tests/wpt`.
## Contributing
Lightpanda accepts pull requests through GitHub.
You have to sign our [CLA](CLA.md) during the pull request process otherwise
we're not able to accept your contributions.
## Why?
### 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:
- 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?
- Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale
- Bloated, lots of features are not useful in headless usage
### 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 what we did:
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated: without graphical rendering
## Status
Lightpanda is still a work in progress and is currently at a Beta stage.
:warning: You should expect most websites to fail or crash.
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] Basic DOM APIs
- [x] Ajax
- [x] XHR API
- [x] Fetch API
- [x] DOM dump
- [x] Basic CDP/websockets 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.
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.

123
build.zig
View File

@@ -45,14 +45,14 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const mode = b.standardOptimizeOption(.{});
const options = try jsruntime.buildOptions(b);
const options = jsruntime.buildOptions(b);
// browser
// -------
// compile and install
const exe = b.addExecutable(.{
.name = "browsercore",
.name = "lightpanda",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = mode,
@@ -75,7 +75,7 @@ pub fn build(b: *std.Build) !void {
// compile and install
const shell = b.addExecutable(.{
.name = "browsercore-shell",
.name = "lightpanda-shell",
.root_source_file = b.path("src/main_shell.zig"),
.target = target,
.optimize = mode,
@@ -98,8 +98,8 @@ pub fn build(b: *std.Build) !void {
// compile
const tests = b.addTest(.{
.root_source_file = b.path("src/run_tests.zig"),
.test_runner = b.path("src/test_runner.zig"),
.root_source_file = b.path("src/main_tests.zig"),
.test_runner = b.path("src/main_tests.zig"),
.target = target,
.optimize = mode,
});
@@ -119,12 +119,33 @@ pub fn build(b: *std.Build) !void {
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
// unittest
// ----
// compile
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/unit_tests.zig"),
.test_runner = b.path("src/unit_tests.zig"),
.target = target,
.optimize = mode,
});
try common(b, unit_tests, options);
const run_unit_tests = b.addRunArtifact(unit_tests);
if (b.args) |args| {
run_unit_tests.addArgs(args);
}
// step
const unit_test_step = b.step("unittest", "Run unit tests");
unit_test_step.dependOn(&run_unit_tests.step);
// wpt
// -----
// compile and install
const wpt = b.addExecutable(.{
.name = "browsercore-wpt",
.name = "lightpanda-wpt",
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = mode,
@@ -139,28 +160,6 @@ pub fn build(b: *std.Build) !void {
// step
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
// get
// -----
// compile and install
const get = b.addExecutable(.{
.name = "browsercore-get",
.root_source_file = b.path("src/main_get.zig"),
.target = target,
.optimize = mode,
});
try common(b, get, options);
b.installArtifact(get);
// run
const get_cmd = b.addRunArtifact(get);
if (b.args) |args| {
get_cmd.addArgs(args);
}
// step
const get_step = b.step("get", "request URL");
get_step.dependOn(&get_cmd.step);
}
fn common(
@@ -168,33 +167,64 @@ fn common(
step: *std.Build.Step.Compile,
options: jsruntime.Options,
) !void {
const target = step.root_module.resolved_target.?;
const jsruntimemod = try jsruntime_pkgs.module(
b,
options,
step.root_module.optimize.?,
step.root_module.resolved_target.?,
target,
);
step.root_module.addImport("jsruntime", jsruntimemod);
const netsurf = moduleNetSurf(b);
const netsurf = try moduleNetSurf(b, target);
netsurf.addImport("jsruntime", jsruntimemod);
step.root_module.addImport("netsurf", netsurf);
const asyncio = b.addModule("asyncio", .{
.root_source_file = b.path("vendor/zig-async-io/src/lib.zig"),
});
step.root_module.addImport("asyncio", asyncio);
const tlsmod = b.addModule("tls", .{
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
});
step.root_module.addImport("tls", tlsmod);
}
fn moduleNetSurf(b: *std.Build) *std.Build.Module {
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
const mod = b.addModule("netsurf", .{
.root_source_file = b.path("src/netsurf/netsurf.zig"),
.target = target,
});
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
// iconv
mod.addObjectFile(b.path("vendor/libiconv/lib/libiconv.a"));
mod.addIncludePath(b.path("vendor/libiconv/include"));
const libiconv_lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
const libiconv_include_path = try std.fmt.allocPrint(
mod.owner.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(libiconv_lib_path));
mod.addIncludePath(b.path(libiconv_include_path));
// mimalloc
mod.addImport("mimalloc", moduleMimalloc(b));
mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
// netsurf libs
const ns = "vendor/netsurf";
mod.addIncludePath(b.path(ns ++ "/include"));
const ns_include_path = try std.fmt.allocPrint(
mod.owner.allocator,
ns ++ "/out/{s}-{s}/include",
.{ @tagName(os), @tagName(arch) },
);
mod.addIncludePath(b.path(ns_include_path));
const libs: [4][]const u8 = .{
"libdom",
@@ -203,20 +233,35 @@ fn moduleNetSurf(b: *std.Build) *std.Build.Module {
"libwapcaplet",
};
inline for (libs) |lib| {
mod.addObjectFile(b.path(ns ++ "/lib/" ++ lib ++ ".a"));
const ns_lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(ns_lib_path));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
return mod;
}
fn moduleMimalloc(b: *std.Build) *std.Build.Module {
fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
const mod = b.addModule("mimalloc", .{
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
.target = target,
});
mod.addObjectFile(b.path("vendor/mimalloc/out/libmimalloc.a"));
mod.addIncludePath(b.path("vendor/mimalloc/out/include"));
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
const mimalloc = "vendor/mimalloc";
const lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(lib_path));
mod.addIncludePath(b.path(mimalloc ++ "/include"));
return mod;
}

View File

@@ -26,6 +26,8 @@ const Events = @import("events/event.zig");
const XHR = @import("xhr/xhr.zig");
const Storage = @import("storage/storage.zig");
const URL = @import("url/url.zig");
const Iterators = @import("iterator/iterator.zig");
const XMLSerializer = @import("xmlserializer/xmlserializer.zig");
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
@@ -38,6 +40,8 @@ pub const Interfaces = generate.Tuple(.{
XHR.Interfaces,
Storage.Interfaces,
URL.Interfaces,
});
Iterators.Interfaces,
XMLSerializer.Interfaces,
}){};
pub const UserContext = @import("user_context.zig").UserContext;

View File

@@ -1,133 +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 posix = std.posix;
const io = std.io;
const assert = std.debug.assert;
const tcp = @import("tcp.zig");
pub const Stream = struct {
alloc: std.mem.Allocator,
conn: *tcp.Conn,
handle: posix.socket_t,
pub fn close(self: Stream) void {
posix.close(self.handle);
self.alloc.destroy(self.conn);
}
pub const ReadError = posix.ReadError;
pub const WriteError = posix.WriteError;
pub const Reader = io.Reader(Stream, ReadError, read);
pub const Writer = io.Writer(Stream, WriteError, write);
pub fn reader(self: Stream) Reader {
return .{ .context = self };
}
pub fn writer(self: Stream) Writer {
return .{ .context = self };
}
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
return self.conn.receive(self.handle, buffer) catch |err| switch (err) {
else => return error.Unexpected,
};
}
pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize {
return posix.readv(s.handle, iovecs);
}
/// Returns the number of bytes read. If the number read is smaller than
/// `buffer.len`, it means the stream reached the end. Reaching the end of
/// a stream is not an error condition.
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
return readAtLeast(s, buffer, buffer.len);
}
/// Returns the number of bytes read, calling the underlying read function
/// the minimal number of times until the buffer has at least `len` bytes
/// filled. If the number read is less than `len` it means the stream
/// reached the end. Reaching the end of the stream is not an error
/// condition.
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try s.read(buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
/// file system thread instead of non-blocking. It needs to be reworked to properly
/// use non-blocking I/O.
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
return self.conn.send(self.handle, buffer) catch |err| switch (err) {
error.AccessDenied => error.AccessDenied,
error.WouldBlock => error.WouldBlock,
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
error.MessageTooBig => error.FileTooBig,
error.BrokenPipe => error.BrokenPipe,
else => return error.Unexpected,
};
}
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.write(bytes[index..]);
}
}
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writev`.
pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize {
if (iovecs.len == 0) return 0;
const first_buffer = iovecs[0].iov_base[0..iovecs[0].iov_len];
return try self.write(first_buffer);
}
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writevAll`.
pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
};

View File

@@ -1,112 +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 net = std.net;
const Stream = @import("stream.zig").Stream;
const Loop = @import("jsruntime").Loop;
const NetworkImpl = Loop.Network(Conn.Command);
// Conn is a TCP connection using jsruntime Loop async I/O.
// connect, send and receive are blocking, but use async I/O in the background.
// Client doesn't own the socket used for the connection, the caller is
// responsible for closing it.
pub const Conn = struct {
const Command = struct {
impl: NetworkImpl,
done: bool = false,
err: ?anyerror = null,
ln: usize = 0,
fn ok(self: *Command, err: ?anyerror, ln: usize) void {
self.err = err;
self.ln = ln;
self.done = true;
}
fn wait(self: *Command) !usize {
while (!self.done) try self.impl.tick();
if (self.err) |err| return err;
return self.ln;
}
pub fn onConnect(self: *Command, err: ?anyerror) void {
self.ok(err, 0);
}
pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
};
loop: *Loop,
pub fn connect(self: *Conn, socket: std.posix.socket_t, address: std.net.Address) !void {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.connect(&cmd, socket, address);
_ = try cmd.wait();
}
pub fn send(self: *Conn, socket: std.posix.socket_t, buffer: []const u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.send(&cmd, socket, buffer);
return try cmd.wait();
}
pub fn receive(self: *Conn, socket: std.posix.socket_t, buffer: []u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.receive(&cmd, socket, buffer);
return try cmd.wait();
}
};
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
// TODO async resolve
const list = try net.getAddressList(alloc, name, port);
defer list.deinit();
if (list.addrs.len == 0) return error.UnknownHostName;
for (list.addrs) |addr| {
return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) {
error.ConnectionRefused => {
continue;
},
else => return err,
};
}
return std.posix.ConnectError.ConnectionRefused;
}
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
const sockfd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP);
errdefer std.posix.close(sockfd);
var conn = try alloc.create(Conn);
conn.* = Conn{ .loop = loop };
try conn.connect(sockfd, addr);
return Stream{
.alloc = alloc,
.conn = conn,
.handle = sockfd,
};
}

View File

@@ -1,189 +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 http = std.http;
const Client = @import("Client.zig");
const Request = @import("Client.zig").Request;
pub const Loop = @import("jsruntime").Loop;
const url = "https://w3.org";
test "blocking mode fetch API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
const res = try client.fetch(.{
.location = .{ .uri = try std.Uri.parse(url) },
});
try std.testing.expect(res.status == .ok);
}
test "blocking mode open/send/wait API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var buf: [2014]u8 = undefined;
var req = try client.open(.GET, try std.Uri.parse(url), .{
.server_header_buffer = &buf,
});
defer req.deinit();
try req.send();
try req.finish();
try req.wait();
try std.testing.expect(req.response.status == .ok);
}
// Example how to write an async http client using the modified standard client.
const AsyncClient = struct {
cli: Client,
const YieldImpl = Loop.Yield(AsyncRequest);
const AsyncRequest = struct {
const State = enum { new, open, send, finish, wait, done };
cli: *Client,
uri: std.Uri,
req: ?Request = undefined,
state: State = .new,
impl: YieldImpl,
err: ?anyerror = null,
buf: [2014]u8 = undefined,
pub fn deinit(self: *AsyncRequest) void {
if (self.req) |*r| r.deinit();
}
pub fn fetch(self: *AsyncRequest) void {
self.state = .new;
return self.impl.yield(self);
}
fn onerr(self: *AsyncRequest, err: anyerror) void {
self.state = .done;
self.err = err;
}
pub fn onYield(self: *AsyncRequest, err: ?anyerror) void {
if (err) |e| return self.onerr(e);
switch (self.state) {
.new => {
self.state = .open;
self.req = self.cli.open(.GET, self.uri, .{
.server_header_buffer = &self.buf,
}) catch |e| return self.onerr(e);
},
.open => {
self.state = .send;
self.req.?.send() catch |e| return self.onerr(e);
},
.send => {
self.state = .finish;
self.req.?.finish() catch |e| return self.onerr(e);
},
.finish => {
self.state = .wait;
self.req.?.wait() catch |e| return self.onerr(e);
},
.wait => {
self.state = .done;
return;
},
.done => return,
}
return self.impl.yield(self);
}
pub fn wait(self: *AsyncRequest) !void {
while (self.state != .done) try self.impl.tick();
if (self.err) |err| return err;
}
};
pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient {
return .{
.cli = .{
.allocator = alloc,
.loop = loop,
},
};
}
pub fn deinit(self: *AsyncClient) void {
self.cli.deinit();
}
pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest {
return .{
.impl = YieldImpl.init(self.cli.loop),
.cli = &self.cli,
.uri = uri,
};
}
};
test "non blocking client" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client = AsyncClient.init(alloc, &loop);
defer client.deinit();
var reqs: [3]AsyncClient.AsyncRequest = undefined;
for (0..reqs.len) |i| {
reqs[i] = try client.createRequest(try std.Uri.parse(url));
reqs[i].fetch();
}
for (0..reqs.len) |i| {
try reqs[i].wait();
reqs[i].deinit();
}
}

View File

@@ -24,49 +24,69 @@ const Types = @import("root").Types;
const parser = @import("netsurf");
const Loader = @import("loader.zig").Loader;
const Dump = @import("dump.zig");
const Mime = @import("mime.zig");
const Mime = @import("mime.zig").Mime;
const jsruntime = @import("jsruntime");
const Loop = jsruntime.Loop;
const Env = jsruntime.Env;
const Module = jsruntime.Module;
const apiweb = @import("../apiweb.zig");
const Window = @import("../html/window.zig").Window;
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
const URL = @import("../url/url.zig").URL;
const Location = @import("../html/location.zig").Location;
const storage = @import("../storage/storage.zig");
const FetchResult = std.http.Client.FetchResult;
const FetchResult = @import("../http/Client.zig").Client.FetchResult;
const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("../async/Client.zig");
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,
session: Session = undefined,
agent: []const u8 = user_agent,
pub fn init(alloc: std.mem.Allocator, vm: jsruntime.VM) !Browser {
const uri = "about:blank";
pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void {
// We want to ensure the caller initialised a VM, but the browser
// doesn't use it directly...
_ = vm;
return Browser{
.session = try Session.init(alloc, "about:blank"),
};
try Session.init(&self.session, alloc, loop, uri);
}
pub fn deinit(self: *Browser) void {
self.session.deinit();
}
pub fn currentSession(self: *Browser) *Session {
return self.session;
pub fn newSession(
self: *Browser,
alloc: std.mem.Allocator,
loop: *jsruntime.Loop,
) !void {
self.session.deinit();
try Session.init(&self.session, alloc, loop, uri);
}
pub fn currentPage(self: *Browser) ?*Page {
if (self.session.page == null) return null;
return &self.session.page.?;
}
};
@@ -90,50 +110,90 @@ pub const Session = struct {
// TODO handle proxy
loader: Loader,
env: Env = undefined,
loop: Loop,
inspector: ?jsruntime.Inspector = null,
window: Window,
// TODO move the shed to the browser?
storageShed: storage.Shed,
page: ?*Page = null,
page: ?Page = null,
httpClient: HttpClient,
jstypes: [Types.len]usize = undefined,
fn init(alloc: std.mem.Allocator, uri: []const u8) !*Session {
var self = try alloc.create(Session);
fn init(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void {
self.* = Session{
.uri = uri,
.alloc = alloc,
.arena = std.heap.ArenaAllocator.init(alloc),
.window = Window.create(null),
.window = Window.create(null, .{ .agent = user_agent }),
.loader = Loader.init(alloc),
.loop = try Loop.init(alloc),
.storageShed = storage.Shed.init(alloc),
.httpClient = undefined,
};
self.env = try Env.init(self.arena.allocator(), &self.loop, null);
self.httpClient = .{ .allocator = alloc, .loop = &self.loop };
Env.init(&self.env, self.arena.allocator(), loop, null);
self.httpClient = .{ .allocator = alloc };
try self.env.load(&self.jstypes);
}
return self;
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) |page| page.end();
if (self.page) |*p| p.deinit();
if (self.inspector) |inspector| {
inspector.deinit(self.alloc);
}
self.env.deinit();
self.arena.deinit();
self.loader.deinit();
self.loop.deinit();
self.storageShed.deinit();
self.httpClient.deinit();
self.alloc.destroy(self);
self.loader.deinit();
self.storageShed.deinit();
}
pub fn createPage(self: *Session) !Page {
return Page.init(self.alloc, self);
pub fn initInspector(
self: *Session,
ctx: anytype,
onResp: jsruntime.InspectorOnResponseFn,
onEvent: jsruntime.InspectorOnEventFn,
) !void {
const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx_opaque, onResp, onEvent);
self.env.setInspector(self.inspector.?);
}
pub fn callInspector(self: *Session, msg: []const u8) void {
if (self.inspector) |inspector| {
inspector.send(msg, self.env);
} else {
@panic("No Inspector");
}
}
// NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page {
if (self.page != null) return error.SessionPageExists;
const p: Page = undefined;
self.page = p;
Page.init(&self.page.?, self.alloc, self);
return &self.page.?;
}
};
@@ -152,19 +212,46 @@ pub const Page = struct {
uri: std.Uri = undefined,
origin: ?[]const u8 = null,
// html url and location
url: ?URL = null,
location: Location = .{},
raw_data: ?[]const u8 = null,
fn init(
self: *Page,
alloc: std.mem.Allocator,
session: *Session,
) !Page {
if (session.page != null) return error.SessionPageExists;
var page = Page{
) void {
self.* = .{
.arena = std.heap.ArenaAllocator.init(alloc),
.session = session,
};
session.page = &page;
return page;
}
// 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.
@@ -172,6 +259,14 @@ pub const Page = struct {
self.session.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
if (self.url) |*u| u.deinit(self.arena.allocator());
self.url = null;
self.location.url = null;
self.session.window.replaceLocation(&self.location) catch |e| {
log.err("reset window location: {any}", .{e});
};
self.doc = null;
// clear netsurf memory arena.
parser.deinit();
@@ -179,6 +274,7 @@ pub const Page = struct {
}
pub fn deinit(self: *Page) void {
self.end();
self.arena.deinit();
self.session.page = null;
}
@@ -198,34 +294,49 @@ pub const Page = struct {
}
pub fn wait(self: *Page) !void {
const alloc = self.arena.allocator();
var res = try self.session.env.waitTryCatch(alloc);
defer res.deinit(alloc);
if (res.success) {
log.debug("wait: {s}", .{res.result});
} else {
if (builtin.mode == .Debug and res.stack != null) {
log.info("wait: {s}", .{res.stack.?});
} else {
log.info("wait: {s}", .{res.result});
// try catch
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
self.session.env.wait() catch |err| {
// the js env could not be started if the document wasn't an HTML.
if (err == error.EnvNotStarted) return;
const alloc = self.arena.allocator();
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("wait error: {s}", .{msg});
return;
}
}
return;
};
log.debug("wait: OK", .{});
}
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
pub fn navigate(self: *Page, uri: []const u8) !void {
// - auxData: extra data forwarded to the Inspector
// see Inspector.contextCreated
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void {
const alloc = self.arena.allocator();
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);
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?);
if (self.url) |*prev| prev.deinit(alloc);
self.url = try URL.constructor(alloc, self.rawuri.?, null);
self.location.url = &self.url.?;
try self.session.window.replaceLocation(&self.location);
// prepare origin value.
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
@@ -243,18 +354,15 @@ pub const Page = struct {
const req = resp.req;
log.info("GET {any} {d}", .{ self.uri, req.response.status });
log.info("GET {any} {d}", .{ self.uri, @intFromEnum(req.response.status) });
// TODO handle redirection
if (req.response.status != .ok) {
log.debug("{?} {d} {s}", .{
req.response.version,
req.response.status,
req.response.reason,
// TODO log headers
});
return error.BadStatusCode;
}
log.debug("{?} {d} {s}", .{
req.response.version,
@intFromEnum(req.response.status),
req.response.reason,
// TODO log headers
});
// TODO handle charset
// https://html.spec.whatwg.org/#content-type
@@ -275,9 +383,11 @@ pub const Page = struct {
defer alloc.free(ct.?);
log.debug("header content-type: {s}", .{ct.?});
const mime = try Mime.parse(ct.?);
if (mime.eql(Mime.HTML)) {
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8");
var mime = try Mime.parse(alloc, ct.?);
defer mime.deinit();
if (mime.isHTML()) {
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", auxData);
} else {
log.info("non-HTML document: {s}", .{ct.?});
@@ -287,7 +397,7 @@ pub const Page = struct {
}
// https://html.spec.whatwg.org/#read-html
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void {
const alloc = self.arena.allocator();
// start netsurf memory arena.
@@ -312,17 +422,17 @@ pub const Page = struct {
// TODO set the referrer to the document.
self.session.window.replaceDocument(html_doc);
try self.session.window.replaceDocument(html_doc);
self.session.window.setStorageShelf(
try self.session.storageShed.getOrPut(self.origin orelse "null"),
);
// 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(alloc);
// inspector
if (self.session.inspector) |inspector| {
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
}
// replace the user context document with the new one.
try self.session.env.setUserContext(.{
@@ -330,10 +440,6 @@ 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
@@ -342,7 +448,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(*parser.Element).init(alloc);
var sasync = std.ArrayList(Script).init(alloc);
defer sasync.deinit();
const root = parser.documentToNode(doc);
@@ -357,21 +463,10 @@ 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.
// > 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;
}
const script = try Script.init(e) orelse continue;
if (script.kind == .unknown) continue;
// Ignore the defer attribute b/c we analyze all script
// after the document has been parsed.
@@ -385,8 +480,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 (try parser.elementGetAttribute(e, "async") != null) {
try sasync.append(e);
if (script.isasync) {
try sasync.append(script);
continue;
}
@@ -408,7 +503,9 @@ pub const Page = struct {
// > immediately before the browser continues to parse the
// > page.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
// TODO wait for deferred scripts
@@ -424,8 +521,10 @@ pub const Page = struct {
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
// eval async scripts.
for (sasync.items) |e| {
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
for (sasync.items) |s| {
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
self.evalScript(s) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
// TODO wait for async scripts
@@ -446,15 +545,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, e: *parser.Element) !void {
fn evalScript(self: *Page, s: Script) !void {
const alloc = self.arena.allocator();
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
const opt_src = try parser.elementGetAttribute(e, "src");
const opt_src = try parser.elementGetAttribute(s.element, "src");
if (opt_src) |src| {
log.debug("starting GET {s}", .{src});
self.fetchScript(src) catch |err| {
self.fetchScript(s) catch |err| {
switch (err) {
FetchError.BadStatusCode => return err,
@@ -473,22 +572,10 @@ pub const Page = struct {
return;
}
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
// TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
if (opt_text) |text| {
// TODO handle charset attribute
var res = try self.session.env.execTryCatch(alloc, text, "");
defer res.deinit(alloc);
if (res.success) {
log.debug("eval inline: {s}", .{res.result});
} else {
if (builtin.mode == .Debug and res.stack != null) {
log.info("eval inline: {s}", .{res.stack.?});
} else {
log.info("eval inline: {s}", .{res.result});
}
}
try s.eval(alloc, self.session.env, text);
return;
}
@@ -503,12 +590,9 @@ pub const Page = struct {
JsErr,
};
// 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});
// 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});
var buffer: [1024]u8 = undefined;
var b: []u8 = buffer[0..];
@@ -519,42 +603,91 @@ pub const Page = struct {
const resp = fetchres.req.response;
log.info("fech script {any}: {d}", .{ u, resp.status });
log.info("fetch {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;
var res = try self.session.env.execTryCatch(alloc, body, src);
defer res.deinit(alloc);
return body;
}
if (res.success) {
log.debug("eval remote {s}: {s}", .{ src, res.result });
} else {
if (builtin.mode == .Debug and res.stack != null) {
log.info("eval remote {s}: {s}", .{ src, res.stack.? });
} else {
log.info("eval remote {s}: {s}", .{ src, res.result });
}
return FetchError.JsErr;
// 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,
};
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",
};
}
}
// > 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;
// > 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;
return false;
}
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 });
}
}
};
};

View File

@@ -25,82 +25,87 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
// writer must be a std.io.Writer
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
try writer.writeAll("<!DOCTYPE html>\n");
try writeNode(parser.documentToNode(doc), writer);
try writeChildren(parser.documentToNode(doc), writer);
try writer.writeAll("\n");
}
pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
switch (try parser.nodeType(node)) {
.element => {
// open the tag
const tag = try parser.nodeLocalName(node);
try writer.writeAll("<");
try writer.writeAll(tag);
// write the attributes
const map = try parser.nodeGetAttributes(node);
const ln = try parser.namedNodeMapGetLength(map);
var i: u32 = 0;
while (i < ln) {
const attr = try parser.namedNodeMapItem(map, i) orelse break;
try writer.writeAll(" ");
try writer.writeAll(try parser.attributeGetName(attr));
try writer.writeAll("=\"");
const attribute_value = try parser.attributeGetValue(attr) orelse "";
try writeEscapedAttributeValue(writer, attribute_value);
try writer.writeAll("\"");
i += 1;
}
try writer.writeAll(">");
// void elements can't have any content.
if (try isVoid(parser.nodeToElement(node))) return;
// write the children
// TODO avoid recursion
try writeChildren(node, writer);
// close the tag
try writer.writeAll("</");
try writer.writeAll(tag);
try writer.writeAll(">");
},
.text => {
const v = try parser.nodeValue(node) orelse return;
try writeEscapedTextNode(writer, v);
},
.cdata_section => {
const v = try parser.nodeValue(node) orelse return;
try writer.writeAll("<![CDATA[");
try writer.writeAll(v);
try writer.writeAll("]]>");
},
.comment => {
const v = try parser.nodeValue(node) orelse return;
try writer.writeAll("<!--");
try writer.writeAll(v);
try writer.writeAll("-->");
},
// TODO handle processing instruction dump
.processing_instruction => return,
// document fragment is outside of the main document DOM, so we
// don't output it.
.document_fragment => return,
// document will never be called, but required for completeness.
.document => return,
// done globally instead, but required for completeness.
.document_type => return,
// deprecated
.attribute => return,
.entity_reference => return,
.entity => return,
.notation => return,
}
}
// writer must be a std.io.Writer
pub fn writeNode(root: *parser.Node, writer: anytype) !void {
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse break;
switch (try parser.nodeType(next.?)) {
.element => {
// open the tag
const tag = try parser.nodeLocalName(next.?);
try writer.writeAll("<");
try writer.writeAll(tag);
// write the attributes
const map = try parser.nodeGetAttributes(next.?);
const ln = try parser.namedNodeMapGetLength(map);
var i: u32 = 0;
while (i < ln) {
const attr = try parser.namedNodeMapItem(map, i) orelse break;
try writer.writeAll(" ");
try writer.writeAll(try parser.attributeGetName(attr));
try writer.writeAll("=\"");
try writer.writeAll(try parser.attributeGetValue(attr) orelse "");
try writer.writeAll("\"");
i += 1;
}
try writer.writeAll(">");
// void elements can't have any content.
if (try isVoid(parser.nodeToElement(next.?))) continue;
// write the children
// TODO avoid recursion
try writeNode(next.?, writer);
// close the tag
try writer.writeAll("</");
try writer.writeAll(tag);
try writer.writeAll(">");
},
.text => {
const v = try parser.nodeValue(next.?) orelse continue;
try writer.writeAll(v);
},
.cdata_section => {
const v = try parser.nodeValue(next.?) orelse continue;
try writer.writeAll("<![CDATA[");
try writer.writeAll(v);
try writer.writeAll("]]>");
},
.comment => {
const v = try parser.nodeValue(next.?) orelse continue;
try writer.writeAll("<!--");
try writer.writeAll(v);
try writer.writeAll("-->");
},
// TODO handle processing instruction dump
.processing_instruction => continue,
// document fragment is outside of the main document DOM, so we
// don't output it.
.document_fragment => continue,
// document will never be called, but required for completeness.
.document => continue,
// done globally instead, but required for completeness.
.document_type => continue,
// deprecated
.attribute => continue,
.entity_reference => continue,
.entity => continue,
.notation => continue,
}
try writeNode(next.?, writer);
}
}
@@ -115,18 +120,87 @@ fn isVoid(elem: *parser.Element) !bool {
};
}
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
var v = value;
while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
return writer.writeAll(v);
};
try writer.writeAll(v[0..index]);
switch (v[index]) {
'&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"),
else => unreachable,
}
v = v[index + 1 ..];
}
}
fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
var v = value;
while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
return writer.writeAll(v);
};
try writer.writeAll(v[0..index]);
switch (v[index]) {
'&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"),
'"' => try writer.writeAll("&quot;"),
else => unreachable,
}
v = v[index + 1 ..];
}
}
const testing = std.testing;
test "dump.writeHTML" {
const out = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
defer out.close();
try testWriteHTML(
"<div id=\"content\">Over 9000!</div>",
"<div id=\"content\">Over 9000!</div>",
);
const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close();
try testWriteHTML(
"<root><!-- a comment --></root>",
"<root><!-- a comment --></root>",
);
const doc_html = try parser.documentHTMLParse(file.reader(), "UTF-8");
// ignore close error
try testWriteHTML(
"<p>&lt; &gt; &amp;</p>",
"<p>&lt; &gt; &amp;</p>",
);
try testWriteHTML(
"<p id=\"&quot;&gt;&lt;&amp;&quot;''\">wat?</p>",
"<p id='\">&lt;&amp;&quot;&#39;&apos;'>wat?</p>",
);
try testWriteFullHTML(
\\<!DOCTYPE html>
\\<html><head><title>It's over what?</title><meta name="a" value="b">
\\</head><body>9000</body></html>
\\
, "<html><title>It's over what?</title><meta name=a value=\"b\">\n<body>9000");
}
fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
const expected =
"<!DOCTYPE html>\n<html><head></head><body>" ++
expected_body ++
"</body></html>\n";
return testWriteFullHTML(expected, src);
}
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
var buf = std.ArrayListUnmanaged(u8){};
defer buf.deinit(testing.allocator);
const doc_html = try parser.documentHTMLParseFromStr(src);
defer parser.documentHTMLClose(doc_html) catch {};
const doc = parser.documentHTMLToDocument(doc_html);
try writeHTML(doc, out);
try writeHTML(doc, buf.writer(testing.allocator));
try testing.expectEqualStrings(expected, buf.items);
}

View File

@@ -17,17 +17,18 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Client = @import("../http/Client.zig");
const user_agent = "Lightpanda.io/1.0";
const user_agent = @import("browser.zig").user_agent;
pub const Loader = struct {
client: std.http.Client,
// use 16KB for headers buffer size.
server_header_buffer: [1024 * 16]u8 = undefined,
client: Client,
// use 64KB for headers buffer size.
server_header_buffer: [1024 * 64]u8 = undefined,
pub const Response = struct {
alloc: std.mem.Allocator,
req: *std.http.Client.Request,
req: *Client.Request,
pub fn deinit(self: *Response) void {
self.req.deinit();
@@ -37,7 +38,7 @@ pub const Loader = struct {
pub fn init(alloc: std.mem.Allocator) Loader {
return Loader{
.client = std.http.Client{
.client = Client{
.allocator = alloc,
},
};
@@ -54,7 +55,7 @@ pub const Loader = struct {
pub fn get(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !Response {
var resp = Response{
.alloc = alloc,
.req = try alloc.create(std.http.Client.Request),
.req = try alloc.create(Client.Request),
};
errdefer alloc.destroy(resp.req);
@@ -78,13 +79,19 @@ pub const Loader = struct {
}
};
test "basic url get" {
test "loader: get" {
const alloc = std.testing.allocator;
var loader = Loader.init(alloc);
defer loader.deinit();
var result = try loader.get(alloc, "https://en.wikipedia.org/wiki/Main_Page");
const uri = try std.Uri.parse("http://localhost:9582/loader");
var result = try loader.get(alloc, uri);
defer result.deinit();
try std.testing.expect(result.req.response.status == std.http.Status.ok);
try std.testing.expectEqual(.ok, result.req.response.status);
var res: [128]u8 = undefined;
const size = try result.req.readAll(&res);
try std.testing.expectEqual(6, size);
try std.testing.expectEqualStrings("Hello!", res[0..6]);
}

View File

@@ -17,141 +17,375 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;
const strparser = @import("../str/parser.zig");
const Reader = strparser.Reader;
const trim = strparser.trim;
pub const Mime = struct {
content_type: ContentType,
params: []const u8 = "",
charset: ?[]const u8 = null,
arena: std.heap.ArenaAllocator,
const Self = @This();
pub const ContentTypeEnum = enum {
text_xml,
text_html,
text_plain,
other,
};
const MimeError = error{
Empty,
TooBig,
Invalid,
InvalidChar,
pub const ContentType = union(ContentTypeEnum) {
text_xml: void,
text_html: void,
text_plain: void,
other: struct { type: []const u8, sub_type: []const u8 },
};
pub fn parse(allocator: Allocator, input: []const u8) !Mime {
if (input.len > 255) {
return error.TooBig;
}
var arena = std.heap.ArenaAllocator.init(allocator);
errdefer arena.deinit();
var trimmed = trim(input);
const content_type, const type_len = try parseContentType(trimmed);
if (type_len >= trimmed.len) {
return .{ .arena = arena, .content_type = content_type };
}
const params = trimLeft(trimmed[type_len..]);
var charset: ?[]const u8 = null;
var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| {
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse return error.Invalid;
const name = trimLeft(attr[0..i]);
const value = trimRight(attr[i + 1 ..]);
if (value.len == 0) {
return error.Invalid;
}
switch (name.len) {
7 => if (isCaseEqual("charset", name)) {
charset = try parseValue(arena.allocator(), value);
},
else => {},
}
}
return .{
.arena = arena,
.params = params,
.charset = charset,
.content_type = content_type,
};
}
pub fn deinit(self: *Mime) void {
self.arena.deinit();
}
pub fn isHTML(self: *const Mime) bool {
return self.content_type == .text_html;
}
fn parseContentType(value: []const u8) !struct { ContentType, usize } {
const separator = std.mem.indexOfScalarPos(u8, value, 0, '/') orelse {
return error.Invalid;
};
const end = std.mem.indexOfScalarPos(u8, value, separator, ';') orelse blk: {
break :blk value.len;
};
const main_type = value[0..separator];
const sub_type = trimRight(value[separator + 1 .. end]);
if (parseCommonContentType(main_type, sub_type)) |content_type| {
return .{ content_type, end + 1 };
}
if (main_type.len == 0) {
return error.Invalid;
}
if (validType(main_type) == false) {
return error.Invalid;
}
if (sub_type.len == 0) {
return error.Invalid;
}
if (validType(sub_type) == false) {
return error.Invalid;
}
const content_type = ContentType{ .other = .{
.type = main_type,
.sub_type = sub_type,
} };
return .{ content_type, end + 1 };
}
fn parseCommonContentType(main_type: []const u8, sub_type: []const u8) ?ContentType {
switch (main_type.len) {
4 => if (isCaseEqual("text", main_type)) {
switch (sub_type.len) {
3 => if (isCaseEqual("xml", sub_type)) {
return .{ .text_xml = {} };
},
4 => if (isCaseEqual("html", sub_type)) {
return .{ .text_html = {} };
},
5 => if (isCaseEqual("plain", sub_type)) {
return .{ .text_plain = {} };
},
else => {},
}
},
else => {},
}
return null;
}
const T_SPECIAL = blk: {
var v = [_]bool{false} ** 256;
for ("()<>@,;:\\\"/[]?=") |b| {
v[b] = true;
}
break :blk v;
};
fn parseValue(allocator: Allocator, value: []const u8) ![]const u8 {
if (value[0] != '"') {
return value;
}
// 1 to skip the opening quote
var value_pos: usize = 1;
var unescaped_len: usize = 0;
const last = value.len - 1;
while (value_pos < value.len) {
switch (value[value_pos]) {
'"' => break,
'\\' => {
if (value_pos == last) {
return error.Invalid;
}
const next = value[value_pos + 1];
if (T_SPECIAL[next] == false) {
return error.Invalid;
}
value_pos += 2;
},
else => value_pos += 1,
}
unescaped_len += 1;
}
if (unescaped_len == 0) {
return error.Invalid;
}
value_pos = 1;
const owned = try allocator.alloc(u8, unescaped_len);
for (0..unescaped_len) |i| {
switch (value[value_pos]) {
'"' => break,
'\\' => {
owned[i] = value[value_pos + 1];
value_pos += 2;
},
else => |c| {
owned[i] = c;
value_pos += 1;
},
}
}
return owned;
}
const VALID_CODEPOINTS = blk: {
var v: [256]bool = undefined;
for (0..256) |i| {
v[i] = std.ascii.isAlphanumeric(i);
}
for ("!#$%&\\*+-.^'_`|~") |b| {
v[b] = true;
}
break :blk v;
};
fn validType(value: []const u8) bool {
for (value) |b| {
if (VALID_CODEPOINTS[b] == false) {
return false;
}
}
return true;
}
fn trim(s: []const u8) []const u8 {
return std.mem.trim(u8, s, &std.ascii.whitespace);
}
fn trimLeft(s: []const u8) []const u8 {
return std.mem.trimLeft(u8, s, &std.ascii.whitespace);
}
fn trimRight(s: []const u8) []const u8 {
return std.mem.trimRight(u8, s, &std.ascii.whitespace);
}
fn isCaseEqual(comptime target: anytype, value: []const u8) bool {
// - 8 beause we don't care about the sentinel
const bit_len = @bitSizeOf(@TypeOf(target.*)) - 8;
const byte_len = bit_len / 8;
const T = @Type(.{ .Int = .{
.bits = bit_len,
.signedness = .unsigned,
} });
const bit_target: T = @bitCast(@as(*const [byte_len]u8, target).*);
if (@as(T, @bitCast(value[0..byte_len].*)) == bit_target) {
return true;
}
return std.ascii.eqlIgnoreCase(value, target);
}
};
mtype: []const u8,
msubtype: []const u8,
params: []const u8 = "",
charset: ?[]const u8 = null,
boundary: ?[]const u8 = null,
pub const Empty = Self{ .mtype = "", .msubtype = "" };
pub const HTML = Self{ .mtype = "text", .msubtype = "html" };
pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" };
// https://mimesniff.spec.whatwg.org/#http-token-code-point
fn isHTTPCodePoint(c: u8) bool {
return switch (c) {
'!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^' => return true,
'_', '`', '|', '~' => return true,
else => std.ascii.isAlphanumeric(c),
};
}
fn valid(s: []const u8) bool {
const ln = s.len;
var i: usize = 0;
while (i < ln) {
if (!isHTTPCodePoint(s[i])) return false;
i += 1;
}
return true;
}
// https://mimesniff.spec.whatwg.org/#parsing-a-mime-type
pub fn parse(s: []const u8) Self.MimeError!Self {
const ln = s.len;
if (ln == 0) return MimeError.Empty;
// limit input size
if (ln > 255) return MimeError.TooBig;
var res = Self{ .mtype = "", .msubtype = "" };
var r = Reader{ .s = s };
res.mtype = trim(r.until('/'));
if (res.mtype.len == 0) return MimeError.Invalid;
if (!valid(res.mtype)) return MimeError.InvalidChar;
if (!r.skip()) return MimeError.Invalid;
res.msubtype = trim(r.until(';'));
if (res.msubtype.len == 0) return MimeError.Invalid;
if (!valid(res.msubtype)) return MimeError.InvalidChar;
if (!r.skip()) return res;
res.params = trim(r.tail());
if (res.params.len == 0) return MimeError.Invalid;
// parse well known parameters.
// don't check invalid parameter format.
var rp = Reader{ .s = res.params };
while (true) {
const name = trim(rp.until('='));
if (!rp.skip()) return res;
const value = trim(rp.until(';'));
if (std.ascii.eqlIgnoreCase(name, "charset")) {
res.charset = value;
}
if (std.ascii.eqlIgnoreCase(name, "boundary")) {
res.boundary = value;
}
if (!rp.skip()) return res;
}
return res;
}
test "parse valid" {
for ([_][]const u8{
"text/html",
" \ttext/html",
"text \t/html",
"text/ \thtml",
"text/html \t",
}) |tc| {
const m = try Self.parse(tc);
try testing.expectEqualStrings("text", m.mtype);
try testing.expectEqualStrings("html", m.msubtype);
}
const m2 = try Self.parse("text/javascript1.5");
try testing.expectEqualStrings("text", m2.mtype);
try testing.expectEqualStrings("javascript1.5", m2.msubtype);
const m3 = try Self.parse("text/html; charset=utf-8");
try testing.expectEqualStrings("text", m3.mtype);
try testing.expectEqualStrings("html", m3.msubtype);
try testing.expectEqualStrings("charset=utf-8", m3.params);
try testing.expectEqualStrings("utf-8", m3.charset.?);
const m4 = try Self.parse("text/html; boundary=----");
try testing.expectEqualStrings("text", m4.mtype);
try testing.expectEqualStrings("html", m4.msubtype);
try testing.expectEqualStrings("boundary=----", m4.params);
try testing.expectEqualStrings("----", m4.boundary.?);
}
test "parse invalid" {
for ([_][]const u8{
const testing = std.testing;
test "Mime: invalid " {
const invalids = [_][]const u8{
"",
"te xt/html;",
"te@xt/html;",
"text/ht@ml;",
"text/html;",
"/text/html",
"/html",
}) |tc| {
_ = Self.parse(tc) catch continue;
try testing.expect(false);
"text",
"text /html",
"text/ html",
"text / html",
"text/html other",
"text/html; x",
"text/html; x=",
"text/html; x= ",
"text/html; = ",
"text/html;=",
"text/html; charset=\"\"",
"text/html; charset=\"",
"text/html; charset=\"\\",
"text/html; charset=\"\\a\"", // invalid to escape non special characters
};
for (invalids) |invalid| {
try testing.expectError(error.Invalid, Mime.parse(undefined, invalid));
}
}
// Compare type and subtype.
pub fn eql(self: Self, b: Self) bool {
if (!std.mem.eql(u8, self.mtype, b.mtype)) return false;
return std.mem.eql(u8, self.msubtype, b.msubtype);
test "Mime: parse common" {
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html");
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain");
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml;");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html;");
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain;");
try expect(.{ .content_type = .{ .text_xml = {} } }, " \ttext/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html ");
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain \t\t");
try expect(.{ .content_type = .{ .text_xml = {} } }, "TEXT/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/Html");
try expect(.{ .content_type = .{ .text_plain = {} } }, "TEXT/PLAIN");
try expect(.{ .content_type = .{ .text_xml = {} } }, " TeXT/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "teXt/HtML ;");
try expect(.{ .content_type = .{ .text_plain = {} } }, "tExT/PlAiN;");
}
test "Mime: parse uncommon" {
const text_javascript = Expectation{
.content_type = .{ .other = .{ .type = "text", .sub_type = "javascript" } },
};
try expect(text_javascript, "text/javascript");
try expect(text_javascript, "text/javascript;");
try expect(text_javascript, " text/javascript\t ");
try expect(text_javascript, " text/javascript\t ;");
try expect(
.{ .content_type = .{ .other = .{ .type = "Text", .sub_type = "Javascript" } } },
"Text/Javascript",
);
}
test "Mime: parse charset" {
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "utf-8",
.params = "charset=utf-8",
}, "text/xml; charset=utf-8");
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "utf-8",
.params = "charset=\"utf-8\"",
}, "text/xml;charset=\"utf-8\"");
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "\\ \" ",
.params = "charset=\"\\\\ \\\" \"",
}, "text/xml;charset=\"\\\\ \\\" \" ");
}
test "Mime: isHTML" {
const isHTML = struct {
fn isHTML(expected: bool, input: []const u8) !void {
var mime = try Mime.parse(testing.allocator, input);
defer mime.deinit();
try testing.expectEqual(expected, mime.isHTML());
}
}.isHTML;
try isHTML(true, "text/html");
try isHTML(true, "text/html;");
try isHTML(true, "text/html; charset=utf-8");
try isHTML(false, "text/htm"); // htm not html
try isHTML(false, "text/plain");
try isHTML(false, "over/9000");
}
const Expectation = struct {
content_type: Mime.ContentType,
params: []const u8 = "",
charset: ?[]const u8 = null,
};
fn expect(expected: Expectation, input: []const u8) !void {
var actual = try Mime.parse(testing.allocator, input);
defer actual.deinit();
try testing.expectEqual(
std.meta.activeTag(expected.content_type),
std.meta.activeTag(actual.content_type),
);
switch (expected.content_type) {
.other => |e| {
const a = actual.content_type.other;
try testing.expectEqualStrings(e.type, a.type);
try testing.expectEqualStrings(e.sub_type, a.sub_type);
},
else => {}, // already asserted above
}
try testing.expectEqualStrings(expected.params, actual.params);
if (expected.charset) |ec| {
try testing.expectEqualStrings(ec, actual.charset.?);
} else {
try testing.expectEqual(null, actual.charset);
}
}

148
src/cdp/browser.zig Normal file
View File

@@ -0,0 +1,148 @@
// 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 {
getVersion,
setDownloadBehavior,
getWindowForTarget,
setWindowBounds,
};
pub fn browser(
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) {
.getVersion => getVersion(alloc, msg, ctx),
.setDownloadBehavior => setDownloadBehavior(alloc, msg, ctx),
.getWindowForTarget => getWindowForTarget(alloc, msg, ctx),
.setWindowBounds => setWindowBounds(alloc, msg, ctx),
};
}
// TODO: hard coded data
const ProtocolVersion = "1.3";
const Product = "Chrome/124.0.6367.29";
const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
const JsVersion = "12.4.254.8";
fn getVersion(
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, "browser.getVersion" });
// ouput
const Res = struct {
protocolVersion: []const u8 = ProtocolVersion,
product: []const u8 = Product,
revision: []const u8 = Revision,
userAgent: []const u8 = UserAgent,
jsVersion: []const u8 = JsVersion,
};
return result(alloc, input.id, Res, .{}, null);
}
// TODO: noop method
fn setDownloadBehavior(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
behavior: []const u8,
browserContextId: ?[]const u8 = null,
downloadPath: ?[]const u8 = null,
eventsEnabled: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("REQ > id {d}, method {s}", .{ input.id, "browser.setDownloadBehavior" });
// output
return result(alloc, input.id, null, null, null);
}
// TODO: hard coded ID
const DevToolsWindowID = 1923710101;
fn getWindowForTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: ?[]const u8 = null,
};
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" });
// output
const Resp = struct {
windowId: u64 = DevToolsWindowID,
bounds: struct {
left: ?u64 = null,
top: ?u64 = null,
width: ?u64 = null,
height: ?u64 = null,
windowState: []const u8 = "normal",
} = .{},
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
}
// TODO: noop method
fn setWindowBounds(
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, "browser.setWindowBounds" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}

277
src/cdp/cdp.zig Normal file
View File

@@ -0,0 +1,277 @@
// 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 browser = @import("browser.zig").browser;
const target = @import("target.zig").target;
const page = @import("page.zig").page;
const log = @import("log.zig").log;
const runtime = @import("runtime.zig").runtime;
const network = @import("network.zig").network;
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 cdpdom = @import("dom.zig");
const css = @import("css.zig").css;
const security = @import("security.zig").security;
const log_cdp = std.log.scoped(.cdp);
pub const Error = error{
UnknonwDomain,
UnknownMethod,
NoResponse,
RequestWithoutID,
};
pub fn isCdpError(err: anyerror) ?Error {
// see https://github.com/ziglang/zig/issues/2473
const errors = @typeInfo(Error).ErrorSet.?;
inline for (errors) |e| {
if (std.mem.eql(u8, e.name, @errorName(err))) {
return @errorCast(err);
}
}
return null;
}
const Domains = enum {
Browser,
Target,
Page,
Log,
Runtime,
Network,
DOM,
CSS,
Inspector,
Emulation,
Fetch,
Performance,
Security,
};
// The caller is responsible for calling `free` on the returned slice.
pub fn do(
alloc: std.mem.Allocator,
s: []const u8,
ctx: *Ctx,
) anyerror![]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
var iter = std.mem.splitScalar(u8, method, '.');
const domain = std.meta.stringToEnum(Domains, iter.first()) orelse
return error.UnknonwDomain;
// 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),
};
}
pub const State = struct {
executionContextId: u32 = 0,
contextID: ?[]const u8 = null,
sessionID: SessionID = .CONTEXTSESSIONID0497A05C95417CF4,
frameID: []const u8 = FrameID,
url: []const u8 = URLBase,
securityOrigin: []const u8 = URLBase,
secureContextType: []const u8 = "Secure", // TODO: enum
loaderID: []const u8 = LoaderID,
page_life_cycle_events: bool = false, // TODO; Target based value
// DOM
nodelist: cdpdom.NodeList,
nodesearchlist: cdpdom.NodeSearchList,
pub fn init(alloc: std.mem.Allocator) State {
return .{
.nodelist = cdpdom.NodeList.init(alloc),
.nodesearchlist = cdpdom.NodeSearchList.init(alloc),
};
}
pub fn deinit(self: *State) void {
self.nodelist.deinit();
// deinit all node searches.
for (self.nodesearchlist.items) |*s| s.deinit();
self.nodesearchlist.deinit();
}
pub fn reset(self: *State) void {
self.nodelist.reset();
// deinit all node searches.
for (self.nodesearchlist.items) |*s| s.deinit();
self.nodesearchlist.clearAndFree();
}
};
// Utils
// -----
pub fn dumpFile(
alloc: std.mem.Allocator,
id: u16,
script: []const u8,
) !void {
const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id});
defer alloc.free(name);
var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
defer dir.close();
const f = try dir.createFile(name, .{});
defer f.close();
const nb = try f.write(script);
std.debug.assert(nb == script.len);
const p = try dir.realpathAlloc(alloc, name);
defer alloc.free(p);
}
// caller owns the slice returned
pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 {
var out = std.ArrayList(u8).init(alloc);
defer out.deinit();
// Do not emit optional null fields
const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false };
try std.json.stringify(res, options, out.writer());
const ret = try alloc.alloc(u8, out.items.len);
@memcpy(ret, out.items);
return ret;
}
const resultNull = "{{\"id\": {d}, \"result\": {{}}}}";
const resultNullSession = "{{\"id\": {d}, \"result\": {{}}, \"sessionId\": \"{s}\"}}";
// caller owns the slice returned
pub fn result(
alloc: std.mem.Allocator,
id: u16,
comptime T: ?type,
res: anytype,
sessionID: ?[]const u8,
) ![]const u8 {
log_cdp.debug(
"Res > id {d}, sessionID {?s}, result {any}",
.{ id, sessionID, res },
);
if (T == null) {
// No need to stringify a custom JSON msg, just use string templates
if (sessionID) |sID| {
return try std.fmt.allocPrint(alloc, resultNullSession, .{ id, sID });
}
return try std.fmt.allocPrint(alloc, resultNull, .{id});
}
const Resp = struct {
id: u16,
result: T.?,
sessionId: ?[]const u8,
};
const resp = Resp{ .id = id, .result = res, .sessionId = sessionID };
return stringify(alloc, resp);
}
pub fn sendEvent(
alloc: std.mem.Allocator,
ctx: *Ctx,
name: []const u8,
comptime T: type,
params: T,
sessionID: ?[]const u8,
) !void {
// some clients like chromedp expects empty parameters structs.
if (T == void) @compileError("sendEvent: use struct{} instead of void for empty parameters");
log_cdp.debug("Event > method {s}, sessionID {?s}", .{ name, sessionID });
const Resp = struct {
method: []const u8,
params: T,
sessionId: ?[]const u8,
};
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
const event_msg = try stringify(alloc, resp);
try ctx.send(event_msg);
}
// Common
// ------
// TODO: hard coded IDs
pub const SessionID = enum {
BROWSERSESSIONID597D9875C664CAC0,
CONTEXTSESSIONID0497A05C95417CF4,
pub fn parse(str: []const u8) !SessionID {
inline for (@typeInfo(SessionID).Enum.fields) |enumField| {
if (std.mem.eql(u8, str, enumField.name)) {
return @field(SessionID, enumField.name);
}
}
return error.InvalidSessionID;
}
};
pub const BrowserSessionID = @tagName(SessionID.BROWSERSESSIONID597D9875C664CAC0);
pub const ContextSessionID = @tagName(SessionID.CONTEXTSESSIONID0497A05C95417CF4);
pub const URLBase = "chrome://newtab/";
pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C";
pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE";
pub const TimestampEvent = struct {
timestamp: f64,
};

59
src/cdp/css.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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);
}

342
src/cdp/dom.zig Normal file
View File

@@ -0,0 +1,342 @@
// 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 css = @import("../dom/css.zig");
const parser = @import("netsurf");
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
getDocument,
performSearch,
getSearchResults,
discardSearchResults,
};
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),
.getDocument => getDocument(alloc, msg, ctx),
.performSearch => performSearch(alloc, msg, ctx),
.getSearchResults => getSearchResults(alloc, msg, ctx),
.discardSearchResults => discardSearchResults(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);
}
// NodeList references tree nodes with an array id.
pub const NodeList = struct {
coll: List,
const List = std.ArrayList(*parser.Node);
pub fn init(alloc: std.mem.Allocator) NodeList {
return .{
.coll = List.init(alloc),
};
}
pub fn deinit(self: *NodeList) void {
self.coll.deinit();
}
pub fn reset(self: *NodeList) void {
self.coll.clearAndFree();
}
pub fn set(self: *NodeList, node: *parser.Node) !NodeId {
for (self.coll.items, 0..) |n, i| {
if (n == node) return @intCast(i);
}
try self.coll.append(node);
return @intCast(self.coll.items.len);
}
};
const NodeId = u32;
const Node = struct {
nodeId: NodeId,
parentId: ?NodeId = null,
backendNodeId: NodeId,
nodeType: u32,
nodeName: []const u8 = "",
localName: []const u8 = "",
nodeValue: []const u8 = "",
childNodeCount: ?u32 = null,
children: ?[]const Node = null,
documentURL: ?[]const u8 = null,
baseURL: ?[]const u8 = null,
xmlVersion: []const u8 = "",
compatibilityMode: []const u8 = "NoQuirksMode",
isScrollable: bool = false,
fn init(n: *parser.Node, nlist: *NodeList) !Node {
const id = try nlist.set(n);
return .{
.nodeId = id,
.backendNodeId = id,
.nodeType = @intFromEnum(try parser.nodeType(n)),
.nodeName = try parser.nodeName(n),
.localName = try parser.nodeLocalName(n),
.nodeValue = try parser.nodeValue(n) orelse "",
};
}
fn initChildren(
self: *Node,
alloc: std.mem.Allocator,
n: *parser.Node,
nlist: *NodeList,
) !std.ArrayList(Node) {
const children = try parser.nodeGetChildNodes(n);
const ln = try parser.nodeListLength(children);
self.childNodeCount = ln;
var list = try std.ArrayList(Node).initCapacity(alloc, ln);
var i: u32 = 0;
while (i < ln) {
defer i += 1;
const child = try parser.nodeListItem(children, i) orelse continue;
try list.append(try Node.init(child, nlist));
}
self.children = list.items;
return list;
}
};
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
fn getDocument(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
depth: ?u32 = null,
pierce: ?bool = null,
};
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, "DOM.getDocument" });
// retrieve the root node
const page = ctx.browser.currentPage() orelse return error.NoPage;
if (page.doc == null) return error.NoDocument;
const node = parser.documentToNode(page.doc.?);
var n = try Node.init(node, &ctx.state.nodelist);
var list = try n.initChildren(alloc, node, &ctx.state.nodelist);
defer list.deinit();
// output
const Resp = struct {
root: Node,
};
const resp: Resp = .{
.root = n,
};
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
try ctx.send(res);
return "";
}
pub const NodeSearch = struct {
coll: List,
name: []u8,
alloc: std.mem.Allocator,
var count: u8 = 0;
const List = std.ArrayListUnmanaged(NodeId);
pub fn initCapacity(alloc: std.mem.Allocator, ln: usize) !NodeSearch {
count += 1;
return .{
.alloc = alloc,
.coll = try List.initCapacity(alloc, ln),
.name = try std.fmt.allocPrint(alloc, "{d}", .{count}),
};
}
pub fn deinit(self: *NodeSearch) void {
self.coll.deinit(self.alloc);
self.alloc.free(self.name);
}
pub fn append(self: *NodeSearch, id: NodeId) !void {
try self.coll.append(self.alloc, id);
}
};
pub const NodeSearchList = std.ArrayList(NodeSearch);
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
fn performSearch(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
query: []const u8,
includeUserAgentShadowDOM: ?bool = null,
};
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, "DOM.performSearch" });
// retrieve the root node
const page = ctx.browser.currentPage() orelse return error.NoPage;
if (page.doc == null) return error.NoDocument;
const list = try css.querySelectorAll(alloc, parser.documentToNode(page.doc.?), input.params.query);
const ln = list.nodes.items.len;
var ns = try NodeSearch.initCapacity(alloc, ln);
for (list.nodes.items) |n| {
const id = try ctx.state.nodelist.set(n);
try ns.append(id);
}
try ctx.state.nodesearchlist.append(ns);
// output
const Resp = struct {
searchId: []const u8,
resultCount: u32,
};
const resp: Resp = .{
.searchId = ns.name,
.resultCount = @intCast(ln),
};
return result(alloc, input.id, Resp, resp, input.sessionId);
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
fn discardSearchResults(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
searchId: []const u8,
};
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, "DOM.discardSearchResults" });
// retrieve the search from context
for (ctx.state.nodesearchlist.items, 0..) |*s, i| {
if (!std.mem.eql(u8, s.name, input.params.searchId)) continue;
s.deinit();
_ = ctx.state.nodesearchlist.swapRemove(i);
break;
}
return result(alloc, input.id, null, null, input.sessionId);
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
fn getSearchResults(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
searchId: []const u8,
fromIndex: u32,
toIndex: u32,
};
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, "DOM.getSearchResults" });
if (input.params.fromIndex >= input.params.toIndex) return error.BadIndices;
// retrieve the search from context
var ns: ?*const NodeSearch = undefined;
for (ctx.state.nodesearchlist.items) |s| {
if (!std.mem.eql(u8, s.name, input.params.searchId)) continue;
ns = &s;
break;
}
if (ns == null) return error.searchResultNotFound;
const items = ns.?.coll.items;
if (input.params.fromIndex >= items.len) return error.BadFromIndex;
if (input.params.toIndex > items.len) return error.BadToIndex;
// output
const Resp = struct {
nodeIds: []NodeId,
};
const resp: Resp = .{
.nodeIds = ns.?.coll.items[input.params.fromIndex..input.params.toIndex],
};
return result(alloc, input.id, Resp, resp, input.sessionId);
}

123
src/cdp/emulation.zig Normal file
View File

@@ -0,0 +1,123 @@
// 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 stringify = cdp.stringify;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
setEmulatedMedia,
setFocusEmulationEnabled,
setDeviceMetricsOverride,
setTouchEmulationEnabled,
};
pub fn emulation(
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) {
.setEmulatedMedia => setEmulatedMedia(alloc, msg, ctx),
.setFocusEmulationEnabled => setFocusEmulationEnabled(alloc, msg, ctx),
.setDeviceMetricsOverride => setDeviceMetricsOverride(alloc, msg, ctx),
.setTouchEmulationEnabled => setTouchEmulationEnabled(alloc, msg, ctx),
};
}
const MediaFeature = struct {
name: []const u8,
value: []const u8,
};
// TODO: noop method
fn setEmulatedMedia(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
media: ?[]const u8 = null,
features: ?[]MediaFeature = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setEmulatedMedia" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setFocusEmulationEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
enabled: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setFocusEmulationEnabled" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setDeviceMetricsOverride(
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, "emulation.setDeviceMetricsOverride" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setTouchEmulationEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setTouchEmulationEnabled" });
return result(alloc, input.id, null, null, input.sessionId);
}

59
src/cdp/fetch.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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 {
disable,
};
pub fn fetch(
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) {
.disable => disable(alloc, msg, ctx),
};
}
// TODO: noop method
fn disable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "fetch.disable" });
return result(alloc, input.id, null, null, input.sessionId);
}

59
src/cdp/inspector.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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);
}

59
src/cdp/log.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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 stringify = cdp.stringify;
const log_cdp = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn log(
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 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log_cdp.debug("Req > id {d}, method {s}", .{ input.id, "log.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

293
src/cdp/msg.zig Normal file
View File

@@ -0,0 +1,293 @@
// 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");
// Parse incoming protocol message in json format.
pub const IncomingMessage = struct {
scanner: std.json.Scanner,
json: []const u8,
obj_begin: bool = false,
obj_end: bool = false,
id: ?u16 = null,
scan_sessionId: bool = false,
sessionId: ?[]const u8 = null,
method: ?[]const u8 = null,
params_skip: bool = false,
pub fn init(alloc: std.mem.Allocator, json: []const u8) IncomingMessage {
return .{
.json = json,
.scanner = std.json.Scanner.initCompleteInput(alloc, json),
};
}
pub fn deinit(self: *IncomingMessage) void {
self.scanner.deinit();
}
fn scanUntil(self: *IncomingMessage, key: []const u8) !void {
while (true) {
switch (try self.scanner.next()) {
.end_of_document => return error.EndOfDocument,
.object_begin => {
if (self.obj_begin) return error.InvalidObjectBegin;
self.obj_begin = true;
},
.object_end => {
if (!self.obj_begin) return error.InvalidObjectEnd;
if (self.obj_end) return error.InvalidObjectEnd;
self.obj_end = true;
},
.string => |s| {
// is the key what we expects?
if (std.mem.eql(u8, s, key)) return;
// save other known keys
if (std.mem.eql(u8, s, "id")) try self.scanId();
if (std.mem.eql(u8, s, "sessionId")) try self.scanSessionId();
if (std.mem.eql(u8, s, "method")) try self.scanMethod();
if (std.mem.eql(u8, s, "params")) try self.scanParams();
// TODO should we skip unknown key?
},
else => return error.InvalidToken,
}
}
}
fn scanId(self: *IncomingMessage) !void {
const t = try self.scanner.next();
if (t != .number) return error.InvalidId;
self.id = try std.fmt.parseUnsigned(u16, t.number, 10);
}
fn getId(self: *IncomingMessage) !u16 {
if (self.id != null) return self.id.?;
try self.scanUntil("id");
try self.scanId();
return self.id.?;
}
fn scanSessionId(self: *IncomingMessage) !void {
switch (try self.scanner.next()) {
// session id can be null.
.null => return,
.string => |s| self.sessionId = s,
else => return error.InvalidSessionId,
}
self.scan_sessionId = true;
}
fn getSessionId(self: *IncomingMessage) !?[]const u8 {
if (self.scan_sessionId) return self.sessionId;
self.scanUntil("sessionId") catch |err| {
if (err != error.EndOfDocument) return err;
// if the document doesn't contains any session id key, we must
// return null value.
self.scan_sessionId = true;
return null;
};
try self.scanSessionId();
return self.sessionId;
}
fn scanMethod(self: *IncomingMessage) !void {
const t = try self.scanner.next();
if (t != .string) return error.InvalidMethod;
self.method = t.string;
}
pub fn getMethod(self: *IncomingMessage) ![]const u8 {
if (self.method != null) return self.method.?;
try self.scanUntil("method");
try self.scanMethod();
return self.method.?;
}
// scanParams skip found parameters b/c if we encounter params *before*
// 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;
try self.scanner.skipValue();
self.params_skip = true;
}
// getParams restart the JSON parsing
fn getParams(self: *IncomingMessage, alloc: ?std.mem.Allocator, T: type) !T {
if (T == void) return void{};
std.debug.assert(alloc != null); // if T is not void, alloc should not be null
if (self.params_skip) {
// TODO if the params have been skipped, we have to retart the
// parsing from start.
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;
};
// parse "params"
const options = std.json.ParseOptions{
.ignore_unknown_fields = true,
.max_value_len = self.scanner.input.len,
.allocate = .alloc_always,
};
return try std.json.innerParse(T, alloc.?, &self.scanner, options);
}
};
pub fn Input(T: type) type {
return struct {
arena: ?*std.heap.ArenaAllocator = null,
id: u16,
params: T,
sessionId: ?[]const u8,
const Self = @This();
pub fn get(alloc: std.mem.Allocator, msg: *IncomingMessage) !Self {
var arena: ?*std.heap.ArenaAllocator = null;
var allocator: ?std.mem.Allocator = null;
if (T != void) {
arena = try alloc.create(std.heap.ArenaAllocator);
arena.?.* = std.heap.ArenaAllocator.init(alloc);
allocator = arena.?.allocator();
}
errdefer {
if (arena) |_arena| {
_arena.deinit();
alloc.destroy(_arena);
}
}
return .{
.arena = arena,
.params = try msg.getParams(allocator, T),
.id = try msg.getId(),
.sessionId = try msg.getSessionId(),
};
}
pub fn deinit(self: Self) void {
if (self.arena) |arena| {
const allocator = arena.child_allocator;
arena.deinit();
allocator.destroy(arena);
}
}
};
}
test "read incoming message" {
const inputs = [_][]const u8{
\\{"id":1,"method":"foo","sessionId":"bar","params":{"bar":"baz"}}
,
\\{"params":{"bar":"baz"},"id":1,"method":"foo","sessionId":"bar"}
,
\\{"sessionId":"bar","params":{"bar":"baz"},"id":1,"method":"foo"}
,
\\{"method":"foo","sessionId":"bar","params":{"bar":"baz"},"id":1}
,
};
for (inputs) |input| {
var msg = IncomingMessage.init(std.testing.allocator, input);
defer msg.deinit();
try std.testing.expectEqual(1, try msg.getId());
try std.testing.expectEqualSlices(u8, "foo", try msg.getMethod());
try std.testing.expectEqualSlices(u8, "bar", (try msg.getSessionId()).?);
const T = struct { bar: []const u8 };
const in = Input(T).get(std.testing.allocator, &msg) catch |err| {
if (err != error.SkippedParams) return err;
// TODO remove this check when params in the beginning is handled.
continue;
};
defer in.deinit();
try std.testing.expectEqualSlices(u8, "baz", in.params.bar);
}
}
test "read incoming message with null session id" {
const inputs = [_][]const u8{
\\{"id":1}
,
\\{"params":{"bar":"baz"},"id":1,"method":"foo"}
,
\\{"sessionId":null,"params":{"bar":"baz"},"id":1,"method":"foo"}
,
};
for (inputs) |input| {
var msg = IncomingMessage.init(std.testing.allocator, input);
defer msg.deinit();
try std.testing.expect(try msg.getSessionId() == null);
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);
}

75
src/cdp/network.zig Normal file
View File

@@ -0,0 +1,75 @@
// 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,
setCacheDisabled,
};
pub fn network(
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),
.setCacheDisabled => setCacheDisabled(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, "network.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setCacheDisabled(
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, "network.setCacheDisabled" });
return result(alloc, input.id, null, null, input.sessionId);
}

464
src/cdp/page.zig Normal file
View File

@@ -0,0 +1,464 @@
// 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 stringify = cdp.stringify;
const sendEvent = cdp.sendEvent;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Runtime = @import("runtime.zig");
const Methods = enum {
enable,
getFrameTree,
setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument,
createIsolatedWorld,
navigate,
};
pub fn page(
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),
.getFrameTree => getFrameTree(alloc, msg, ctx),
.setLifecycleEventsEnabled => setLifecycleEventsEnabled(alloc, msg, ctx),
.addScriptToEvaluateOnNewDocument => addScriptToEvaluateOnNewDocument(alloc, msg, ctx),
.createIsolatedWorld => createIsolatedWorld(alloc, msg, ctx),
.navigate => navigate(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, "page.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}
const Frame = struct {
id: []const u8,
loaderId: []const u8,
url: []const u8,
domainAndRegistry: []const u8 = "",
securityOrigin: []const u8,
mimeType: []const u8 = "text/html",
adFrameStatus: struct {
adFrameType: []const u8 = "none",
} = .{},
secureContextType: []const u8,
crossOriginIsolatedContextType: []const u8 = "NotIsolated",
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
};
fn getFrameTree(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.getFrameTree" });
// output
const FrameTree = struct {
frameTree: struct {
frame: Frame,
},
childFrames: ?[]@This() = null,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.page.getFrameTree { ");
try writer.writeAll(".frameTree = { ");
try writer.writeAll(".frame = { ");
const frame = self.frameTree.frame;
try writer.writeAll(".id = ");
try std.fmt.formatText(frame.id, "s", options, writer);
try writer.writeAll(", .loaderId = ");
try std.fmt.formatText(frame.loaderId, "s", options, writer);
try writer.writeAll(", .url = ");
try std.fmt.formatText(frame.url, "s", options, writer);
try writer.writeAll(" } } }");
}
};
const frameTree = FrameTree{
.frameTree = .{
.frame = .{
.id = ctx.state.frameID,
.url = ctx.state.url,
.securityOrigin = ctx.state.securityOrigin,
.secureContextType = ctx.state.secureContextType,
.loaderId = ctx.state.loaderID,
},
},
};
return result(alloc, input.id, FrameTree, frameTree, input.sessionId);
}
fn setLifecycleEventsEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
enabled: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.setLifecycleEventsEnabled" });
ctx.state.page_life_cycle_events = true;
// output
return result(alloc, input.id, null, null, input.sessionId);
}
const LifecycleEvent = struct {
frameId: []const u8,
loaderId: ?[]const u8,
name: []const u8 = undefined,
timestamp: f32 = undefined,
};
// TODO: hard coded method
fn addScriptToEvaluateOnNewDocument(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
source: []const u8,
worldName: ?[]const u8 = null,
includeCommandLineAPI: bool = false,
runImmediately: bool = false,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.addScriptToEvaluateOnNewDocument" });
// output
const Res = struct {
identifier: []const u8 = "1",
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.page.addScriptToEvaluateOnNewDocument { ");
try writer.writeAll(".identifier = ");
try std.fmt.formatText(self.identifier, "s", options, writer);
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Res, Res{}, input.sessionId);
}
// TODO: hard coded method
fn createIsolatedWorld(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
frameId: []const u8,
worldName: []const u8,
grantUniveralAccess: bool,
};
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, "page.createIsolatedWorld" });
// noop executionContextCreated event
try Runtime.executionContextCreated(
alloc,
ctx,
0,
"",
input.params.worldName,
// TODO: hard coded ID
"7102379147004877974.3265385113993241162",
.{
.isDefault = false,
.type = "isolated",
.frameId = input.params.frameId,
},
input.sessionId,
);
// output
const Resp = struct {
executionContextId: u8 = 0,
};
return result(alloc, input.id, Resp, .{}, input.sessionId);
}
fn navigate(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
url: []const u8,
referrer: ?[]const u8 = null,
transitionType: ?[]const u8 = null, // TODO: enum
frameId: ?[]const u8 = null,
referrerPolicy: ?[]const u8 = null, // TODO: enum
};
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, "page.navigate" });
// change state
ctx.state.reset();
ctx.state.url = input.params.url;
// TODO: hard coded ID
ctx.state.loaderID = "AF8667A203C5392DBE9AC290044AA4C2";
var life_event = LifecycleEvent{
.frameId = ctx.state.frameID,
.loaderId = ctx.state.loaderID,
};
var ts_event: cdp.TimestampEvent = undefined;
// frameStartedLoading event
// TODO: event partially hard coded
const FrameStartedLoading = struct {
frameId: []const u8,
};
const frame_started_loading = FrameStartedLoading{ .frameId = ctx.state.frameID };
try sendEvent(
alloc,
ctx,
"Page.frameStartedLoading",
FrameStartedLoading,
frame_started_loading,
input.sessionId,
);
if (ctx.state.page_life_cycle_events) {
life_event.name = "init";
life_event.timestamp = 343721.796037;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// output
const Resp = struct {
frameId: []const u8,
loaderId: ?[]const u8,
errorText: ?[]const u8 = null,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.page.navigate.Resp { ");
try writer.writeAll(".frameId = ");
try std.fmt.formatText(self.frameId, "s", options, writer);
if (self.loaderId) |loaderId| {
try writer.writeAll(", .loaderId = '");
try std.fmt.formatText(loaderId, "s", options, writer);
}
try writer.writeAll(" }");
}
};
const resp = Resp{
.frameId = ctx.state.frameID,
.loaderId = ctx.state.loaderID,
};
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
try ctx.send(res);
// TODO: at this point do we need async the following actions to be async?
// Send Runtime.executionContextsCleared event
// TODO: noop event, we have no env context at this point, is it necesarry?
try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", struct {}, .{}, input.sessionId);
// Launch navigate, the page must have been created by a
// target.createTarget.
var p = ctx.browser.currentPage() orelse return error.NoPage;
ctx.state.executionContextId += 1;
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.navigate(input.params.url, auxData);
// Events
// lifecycle init event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
life_event.name = "init";
life_event.timestamp = 343721.796037;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// DOM.documentUpdated
try sendEvent(
alloc,
ctx,
"DOM.documentUpdated",
struct {},
.{},
input.sessionId,
);
// frameNavigated event
const FrameNavigated = struct {
frame: Frame,
type: []const u8 = "Navigation",
};
const frame_navigated = FrameNavigated{
.frame = .{
.id = ctx.state.frameID,
.url = ctx.state.url,
.securityOrigin = ctx.state.securityOrigin,
.secureContextType = ctx.state.secureContextType,
.loaderId = ctx.state.loaderID,
},
};
try sendEvent(
alloc,
ctx,
"Page.frameNavigated",
FrameNavigated,
frame_navigated,
input.sessionId,
);
// domContentEventFired event
// TODO: partially hard coded
ts_event = .{ .timestamp = 343721.803338 };
try sendEvent(
alloc,
ctx,
"Page.domContentEventFired",
cdp.TimestampEvent,
ts_event,
input.sessionId,
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
life_event.name = "DOMContentLoaded";
life_event.timestamp = 343721.803338;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// loadEventFired event
// TODO: partially hard coded
ts_event = .{ .timestamp = 343721.824655 };
try sendEvent(
alloc,
ctx,
"Page.loadEventFired",
cdp.TimestampEvent,
ts_event,
input.sessionId,
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
life_event.name = "load";
life_event.timestamp = 343721.824655;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// frameStoppedLoading
const FrameStoppedLoading = struct { frameId: []const u8 };
try sendEvent(
alloc,
ctx,
"Page.frameStoppedLoading",
FrameStoppedLoading,
.{ .frameId = ctx.state.frameID },
input.sessionId,
);
return "";
}

59
src/cdp/performance.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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 performance(
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, "performance.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

195
src/cdp/runtime.zig Normal file
View File

@@ -0,0 +1,195 @@
// 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 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 stringify = cdp.stringify;
const target = @import("target.zig");
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
runIfWaitingForDebugger,
evaluate,
addBinding,
callFunctionOn,
releaseObject,
};
pub fn runtime(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
// NOTE: we could send it anyway to the JS runtime but it's good to check it
return error.UnknownMethod;
return switch (method) {
.runIfWaitingForDebugger => runIfWaitingForDebugger(alloc, msg, ctx),
else => sendInspector(alloc, method, msg, ctx),
};
}
fn sendInspector(
alloc: std.mem.Allocator,
method: Methods,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// save script in file at debug mode
if (std.log.defaultLogEnabled(.debug)) {
// input
var id: u16 = undefined;
var script: ?[]const u8 = null;
if (method == .evaluate) {
const Params = struct {
expression: []const u8,
contextId: ?u8 = null,
returnByValue: ?bool = null,
awaitPromise: ?bool = null,
userGesture: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.evaluate" });
const params = input.params;
const func = try alloc.alloc(u8, params.expression.len);
@memcpy(func, params.expression);
script = func;
id = input.id;
} else if (method == .callFunctionOn) {
const Params = struct {
functionDeclaration: []const u8,
objectId: ?[]const u8 = null,
executionContextId: ?u8 = null,
arguments: ?[]struct {
value: ?[]const u8 = null,
objectId: ?[]const u8 = null,
} = null,
returnByValue: ?bool = null,
awaitPromise: ?bool = null,
userGesture: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.callFunctionOn" });
const params = input.params;
const func = try alloc.alloc(u8, params.functionDeclaration.len);
@memcpy(func, params.functionDeclaration);
script = func;
id = input.id;
}
if (script) |src| {
try cdp.dumpFile(alloc, id, src);
alloc.free(src);
}
}
if (msg.sessionId) |s| {
ctx.state.sessionID = cdp.SessionID.parse(s) catch |err| {
log.err("parse sessionID: {s} {any}", .{ s, err });
return err;
};
}
// 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);
try ctx.sendInspector(buf);
return "";
}
}
try ctx.sendInspector(msg.json);
if (msg.id == null) return "";
return result(alloc, msg.id.?, null, null, msg.sessionId);
}
pub const AuxData = struct {
isDefault: bool = true,
type: []const u8 = "default",
frameId: []const u8 = cdp.FrameID,
};
pub fn executionContextCreated(
alloc: std.mem.Allocator,
ctx: *Ctx,
id: u16,
origin: []const u8,
name: []const u8,
uniqueID: []const u8,
auxData: ?AuxData,
sessionID: ?[]const u8,
) !void {
const Params = struct {
context: struct {
id: u64,
origin: []const u8,
name: []const u8,
uniqueId: []const u8,
auxData: ?AuxData = null,
},
};
const params = Params{
.context = .{
.id = id,
.origin = origin,
.name = name,
.uniqueId = uniqueID,
.auxData = auxData,
},
};
try cdp.sendEvent(alloc, ctx, "Runtime.executionContextCreated", Params, params, sessionID);
}
// TODO: noop method
// should we be passing this also to the JS Inspector?
fn runIfWaitingForDebugger(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "runtime.runIfWaitingForDebugger" });
return result(alloc, input.id, null, null, input.sessionId);
}

59
src/cdp/security.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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);
}

529
src/cdp/target.zig Normal file
View File

@@ -0,0 +1,529 @@
// 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 stringify = cdp.stringify;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
setDiscoverTargets,
setAutoAttach,
attachToTarget,
getTargetInfo,
getBrowserContexts,
createBrowserContext,
disposeBrowserContext,
createTarget,
closeTarget,
sendMessageToTarget,
detachFromTarget,
};
pub fn target(
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) {
.setDiscoverTargets => setDiscoverTargets(alloc, msg, ctx),
.setAutoAttach => setAutoAttach(alloc, msg, ctx),
.attachToTarget => attachToTarget(alloc, msg, ctx),
.getTargetInfo => getTargetInfo(alloc, msg, ctx),
.getBrowserContexts => getBrowserContexts(alloc, msg, ctx),
.createBrowserContext => createBrowserContext(alloc, msg, ctx),
.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";
// TODO: noop method
fn setDiscoverTargets(
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.setDiscoverTargets" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
const AttachToTarget = 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,
},
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,
};
// TODO: noop method
fn setAutoAttach(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
autoAttach: bool,
waitForDebuggerOnStart: bool,
flatten: bool = true,
filter: ?[]TargetFilter = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.setAutoAttach" });
// attachedToTarget event
if (input.sessionId == null) {
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = PageTargetID,
.title = "about:blank",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
}
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn attachToTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: []const u8,
flatten: bool = true,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.attachToTarget" });
// attachedToTarget event
if (input.sessionId == null) {
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = input.params.targetId,
.title = "about:blank",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
}
// output
const SessionId = struct {
sessionId: []const u8,
};
const output = SessionId{
.sessionId = input.sessionId orelse cdp.BrowserSessionID,
};
return result(alloc, input.id, SessionId, output, null);
}
fn getTargetInfo(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(?Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" });
// output
const TargetInfo = struct {
targetId: []const u8,
type: []const u8,
title: []const u8 = "",
url: []const u8 = "",
attached: bool = true,
openerId: ?[]const u8 = null,
canAccessOpener: bool = false,
openerFrameId: ?[]const u8 = null,
browserContextId: ?[]const u8 = null,
subtype: ?[]const u8 = null,
};
const targetInfo = TargetInfo{
.targetId = BrowserTargetID,
.type = "browser",
};
return result(alloc, input.id, TargetInfo, targetInfo, null);
}
// Browser context are not handled and not in the roadmap for now
// The following methods are "fake"
// TODO: noop method
fn getBrowserContexts(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *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.getBrowserContexts" });
// ouptut
const Resp = struct {
browserContextIds: [][]const u8,
};
var resp: Resp = undefined;
if (ctx.state.contextID) |contextID| {
var contextIDs = [1][]const u8{contextID};
resp = .{ .browserContextIds = &contextIDs };
} else {
const contextIDs = [0][]const u8{};
resp = .{ .browserContextIds = &contextIDs };
}
return result(alloc, input.id, Resp, resp, null);
}
const ContextID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
// TODO: noop method
fn createBrowserContext(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
disposeOnDetach: bool = false,
proxyServer: ?[]const u8 = null,
proxyBypassList: ?[]const u8 = null,
originsWithUniversalNetworkAccess: ?[][]const u8 = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createBrowserContext" });
ctx.state.contextID = ContextID;
// output
const Resp = struct {
browserContextId: []const u8 = ContextID,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.target.createBrowserContext { ");
try writer.writeAll(".browserContextId = ");
try std.fmt.formatText(self.browserContextId, "s", options, writer);
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
}
fn disposeBrowserContext(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
browserContextId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.disposeBrowserContext" });
// output
const res = try result(alloc, input.id, null, .{}, null);
try ctx.send(res);
return error.DisposeBrowserContext;
}
// TODO: hard coded IDs
const TargetID = "TARGETID460A8F29706A2ADF14316298";
const LoaderID = "LOADERID42AA389647D702B4D805F49A";
fn createTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
url: []const u8,
width: ?u64 = null,
height: ?u64 = null,
browserContextId: ?[]const u8 = null,
enableBeginFrameControl: bool = false,
newWindow: bool = false,
background: bool = false,
forTab: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createTarget" });
// change CDP state
ctx.state.frameID = TargetID;
ctx.state.url = "about:blank";
ctx.state.securityOrigin = "://";
ctx.state.secureContextType = "InsecureScheme";
ctx.state.loaderID = LoaderID;
if (msg.sessionId) |s| {
ctx.state.sessionID = cdp.SessionID.parse(s) catch |err| {
log.err("parse sessionID: {s} {any}", .{ s, err });
return err;
};
}
// TODO stop the previous page instead?
if (ctx.browser.currentPage() != 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",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
.attached = true,
},
.waitingForDebugger = true,
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, input.sessionId);
// output
const Resp = struct {
targetId: []const u8 = TargetID,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.target.createTarget { ");
try writer.writeAll(".targetId = ");
try std.fmt.formatText(self.targetId, "s", options, writer);
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
}
fn closeTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.closeTarget" });
// output
const Resp = struct {
success: bool = true,
};
const res = try result(alloc, input.id, Resp, Resp{}, null);
try ctx.send(res);
// Inspector.detached event
const InspectorDetached = struct {
reason: []const u8 = "Render process gone.",
};
try cdp.sendEvent(
alloc,
ctx,
"Inspector.detached",
InspectorDetached,
.{},
input.sessionId orelse cdp.ContextSessionID,
);
// detachedFromTarget event
const TargetDetached = struct {
sessionId: []const u8,
targetId: []const u8,
};
try cdp.sendEvent(
alloc,
ctx,
"Target.detachedFromTarget",
TargetDetached,
.{
.sessionId = input.sessionId orelse cdp.ContextSessionID,
.targetId = input.params.targetId,
},
null,
);
if (ctx.browser.currentPage()) |page| 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

@@ -21,7 +21,6 @@ const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const parser = @import("netsurf");
@@ -32,12 +31,12 @@ const ProcessingInstruction = @import("processing_instruction.zig").ProcessingIn
const HTMLElem = @import("../html/elements.zig");
// CharacterData interfaces
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
Comment,
Text.Text,
Text.Interfaces,
ProcessingInstruction,
});
};
// CharacterData implementation
pub const CharacterData = struct {

View File

@@ -29,7 +29,6 @@ const Node = @import("node.zig").Node;
const NodeList = @import("nodelist.zig").NodeList;
const NodeUnion = @import("node.zig").Union;
const Walker = @import("walker.zig").WalkerDepthFirst;
const collection = @import("html_collection.zig");
const css = @import("css.zig");
@@ -435,7 +434,11 @@ pub fn testExecFn(
.{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" },
.{ .src = "document.querySelectorAll('p').length", .ex = "2" },
.{ .src = "document.querySelectorAll('.ok').item(0).id", .ex = "link" },
.{ .src =
\\Array.from(document.querySelectorAll('#content > p#para-empty'))
\\.map(row => row.querySelector('span').textContent)
\\.length;
, .ex = "1" },
};
try checkCases(js_env, &querySelector);

View File

@@ -16,25 +16,23 @@
// 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 generate = @import("../generate.zig");
const DOMException = @import("exceptions.zig").DOMException;
const EventTarget = @import("event_target.zig").EventTarget;
const DOMImplementation = @import("implementation.zig").DOMImplementation;
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
const DOMTokenList = @import("token_list.zig").DOMTokenList;
const NodeList = @import("nodelist.zig").NodeList;
const NodeList = @import("nodelist.zig");
const Nod = @import("node.zig");
const MutationObserver = @import("mutation_observer.zig");
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
DOMException,
EventTarget,
DOMImplementation,
NamedNodeMap,
DOMTokenList,
NodeList,
NodeList.Interfaces,
Nod.Node,
Nod.Interfaces,
MutationObserver.Interfaces,
});
};

View File

@@ -26,7 +26,7 @@ const checkCases = jsruntime.test_utils.checkCases;
const Variadic = jsruntime.Variadic;
const collection = @import("html_collection.zig");
const writeNode = @import("../browser/dump.zig").writeNode;
const dump = @import("../browser/dump.zig");
const css = @import("css.zig");
const Node = @import("node.zig").Node;
@@ -102,7 +102,17 @@ pub const Element = struct {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try writeNode(parser.elementToNode(self), buf.writer());
try dump.writeChildren(parser.elementToNode(self), buf.writer());
// TODO express the caller owned the slice.
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
return buf.toOwnedSlice();
}
pub fn get_outerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try dump.writeNode(parser.elementToNode(self), buf.writer());
// TODO express the caller owned the slice.
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
return buf.toOwnedSlice();
@@ -470,4 +480,9 @@ pub fn testExecFn(
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" },
};
try checkCases(js_env, &innerHTML);
var outerHTML = [_]Case{
.{ .src = "document.getElementById('para').outerHTML", .ex = "<p id=\"para\"> And</p>" },
};
try checkCases(js_env, &outerHTML);
}

View File

@@ -26,15 +26,13 @@ const CallbackResult = jsruntime.CallbackResult;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
MutationObserver,
MutationRecord,
MutationRecords,
});
};
const Walker = @import("../dom/walker.zig").WalkerChildren;

View File

@@ -40,13 +40,14 @@ const DocumentType = @import("document_type.zig").DocumentType;
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
const Walker = @import("walker.zig").WalkerDepthFirst;
// HTML
const HTML = @import("../html/html.zig");
const HTMLElem = @import("../html/elements.zig");
// Node interfaces
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
Attr,
CData.CharacterData,
CData.Interfaces,
@@ -56,12 +57,10 @@ pub const Interfaces = generate.Tuple(.{
DocumentFragment,
HTMLCollection,
HTMLCollectionIterator,
HTML.Interfaces,
});
const Generated = generate.Union.compile(Interfaces);
pub const Union = Generated._union;
pub const Tags = Generated._enum;
};
pub const Union = generate.Union(Interfaces);
// Node implementation
pub const Node = struct {
@@ -195,21 +194,68 @@ pub const Node = struct {
return try Node.toInterface(clone);
}
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) void {
// TODO
_ = other;
_ = self;
std.log.err("Not implemented {s}", .{"node.compareDocumentPosition()"});
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
if (self == other) return 0;
const docself = try parser.nodeOwnerDocument(self);
const docother = try parser.nodeOwnerDocument(other);
// Both are in different document.
if (docself == null or docother == null or docother.? != docself.?) {
return @intFromEnum(parser.DocumentPosition.disconnected);
}
// TODO Both are in a different trees in the same document.
const w = Walker{};
var next: ?*parser.Node = null;
// Is other a descendant of self?
while (true) {
next = try w.get_next(self, next) orelse break;
if (other == next) {
return @intFromEnum(parser.DocumentPosition.following) +
@intFromEnum(parser.DocumentPosition.contained_by);
}
}
// Is self a descendant of other?
next = null;
while (true) {
next = try w.get_next(other, next) orelse break;
if (self == next) {
return @intFromEnum(parser.DocumentPosition.contains) +
@intFromEnum(parser.DocumentPosition.preceding);
}
}
next = null;
while (true) {
next = try w.get_next(parser.documentToNode(docself.?), next) orelse break;
if (other == next) {
// other precedes self.
return @intFromEnum(parser.DocumentPosition.preceding);
}
if (self == next) {
// other follows self.
return @intFromEnum(parser.DocumentPosition.following);
}
}
return 0;
}
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
return try parser.nodeContains(self, other);
}
pub fn _getRootNode(self: *parser.Node) void {
// TODO
_ = self;
std.log.err("Not implemented {s}", .{"node.getRootNode()"});
pub fn _getRootNode(self: *parser.Node) !?HTMLElem.Union {
// TODO return thiss shadow-including root if options["composed"] is true
const res = try parser.nodeOwnerDocument(self);
if (res == null) {
return null;
}
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
}
pub fn _hasChildNodes(self: *parser.Node) !bool {
@@ -384,6 +430,21 @@ pub fn testExecFn(
;
try runScript(js_env, alloc, trim_and_replace, "proto_test");
var node_compare_document_position = [_]Case{
.{ .src = "document.body.compareDocumentPosition(document.firstChild); ", .ex = "10" },
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", .ex = "10" },
.{ .src = "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "20" },
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "0" },
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "2" },
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "4" },
};
try checkCases(js_env, &node_compare_document_position);
var get_root_node = [_]Case{
.{ .src = "document.getElementById('content').getRootNode().__proto__.constructor.name", .ex = "HTMLDocument" },
};
try checkCases(js_env, &get_root_node);
var first_child = [_]Case{
// for next test cases
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },

View File

@@ -21,14 +21,81 @@ const std = @import("std");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const CallbackResult = jsruntime.CallbackResult;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node;
const U32Iterator = @import("../iterator/iterator.zig").U32Iterator;
const log = std.log.scoped(.nodelist);
const DOMException = @import("exceptions.zig").DOMException;
pub const Interfaces = .{
NodeListIterator,
NodeList,
};
pub const NodeListIterator = struct {
pub const mem_guarantied = true;
coll: *NodeList,
index: u32 = 0,
pub const Return = struct {
value: ?NodeUnion,
done: bool,
};
pub fn _next(self: *NodeListIterator) !Return {
const e = try self.coll._item(self.index);
if (e == null) {
return Return{
.value = null,
.done = true,
};
}
self.index += 1;
return Return{
.value = e,
.done = false,
};
}
};
pub const NodeListEntriesIterator = struct {
pub const mem_guarantied = true;
coll: *NodeList,
index: u32 = 0,
pub const Return = struct {
value: ?NodeUnion,
done: bool,
};
pub fn _next(self: *NodeListEntriesIterator) !Return {
const e = try self.coll._item(self.index);
if (e == null) {
return Return{
.value = null,
.done = true,
};
}
self.index += 1;
return Return{
.value = e,
.done = false,
};
}
};
// Nodelist is implemented in pure Zig b/c libdom's NodeList doesn't allow to
// append nodes.
// WEB IDL https://dom.spec.whatwg.org/#nodelist
@@ -72,9 +139,50 @@ pub const NodeList = struct {
return try Node.toInterface(n);
}
// TODO _symbol_iterator
pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
var res = CallbackResult.init(alloc);
defer res.deinit();
// TODO implement postAttach
for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i);
cbk.trycall(.{ n, ii, self }, &res) catch |e| {
log.err("callback error: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
return e;
};
}
}
pub fn _keys(self: *NodeList) U32Iterator {
return .{
.length = self.get_length(),
};
}
pub fn _values(self: *NodeList) NodeListIterator {
return .{
.coll = self,
};
}
pub fn _symbol_iterator(self: *NodeList) NodeListIterator {
return self._values();
}
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
const ln = self.get_length();
var i: u32 = 0;
while (i < ln) {
defer i += 1;
const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
const node = try self._item(i) orelse unreachable;
try js_obj.set(k, node);
}
}
};
// Tests
@@ -87,6 +195,14 @@ pub fn testExecFn(
var childnodes = [_]Case{
.{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" },
.{ .src = "list.length", .ex = "9" },
.{ .src = "list[0].__proto__.constructor.name", .ex = "Text" },
.{ .src =
\\let i = 0;
\\list.forEach(function (n, idx) {
\\ i += idx;
\\});
\\i;
, .ex = "36" },
};
try checkCases(js_env, &childnodes);
}

View File

@@ -21,7 +21,6 @@ const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const parser = @import("netsurf");
@@ -31,9 +30,9 @@ const CDATASection = @import("cdata_section.zig").CDATASection;
const UserContext = @import("../user_context.zig").UserContext;
// Text interfaces
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
CDATASection,
});
};
pub const Text = struct {
pub const Self = parser.Text;

View File

@@ -37,12 +37,12 @@ const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const log = std.log.scoped(.events);
// Event interfaces
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
Event,
ProgressEvent,
});
const Generated = generate.Union.compile(Interfaces);
pub const Union = Generated._union;
};
pub const Union = generate.Union(Interfaces);
// https://dom.spec.whatwg.org/#event
pub const Event = struct {
@@ -251,12 +251,12 @@ pub const EventHandler = struct {
Event.toInterface(evt) catch unreachable,
}, &res) catch |e| log.err("event handler error: {any}", .{e});
} else {
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error: {any}", .{e});
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
}
// in case of function error, we log the result and the trace.
if (!res.success) {
log.info("event handler error: {s}", .{res.result orelse "unknown"});
log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
}
}

View File

@@ -17,430 +17,213 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
// Utils
// -----
fn itoa(comptime i: u8) ![]const u8 {
var len: usize = undefined;
if (i < 10) {
len = 1;
} else if (i < 100) {
len = 2;
} else if (i < 1000) {
len = 3;
} else {
return error.GenerateTooMuchMembers;
}
var buf: [len]u8 = undefined;
return try std.fmt.bufPrint(buf[0..], "{d}", .{i});
}
fn fmtName(comptime T: type) [:0]const u8 {
var it = std.mem.splitBackwards(u8, @typeName(T), ".");
return it.first() ++ "";
}
// ----
const Type = std.builtin.Type;
// Union
// -----
// Generate a flatten tagged Union from various structs and union of structs
// TODO: make this function more generic
// TODO: dedup
pub const Union = struct {
_enum: type,
_union: type,
// Generate a flatten tagged Union from a Tuple
pub fn Union(interfaces: anytype) type {
// @setEvalBranchQuota(10000);
const tuple = Tuple(interfaces){};
const fields = std.meta.fields(@TypeOf(tuple));
pub fn compile(comptime tuple: anytype) Union {
return private_compile(tuple) catch |err| @compileError(@errorName(err));
}
const tag_type = switch (fields.len) {
0 => unreachable,
1 => u0,
2 => u1,
3...4 => u2,
5...8 => u3,
9...16 => u4,
17...32 => u5,
33...64 => u6,
65...128 => u7,
129...256 => u8,
else => @compileError("Too many interfaces to generate union"),
};
fn private_compile(comptime tuple: anytype) !Union {
@setEvalBranchQuota(10000);
// check types provided
const tuple_T = @TypeOf(tuple);
const tuple_info = @typeInfo(tuple_T);
if (tuple_info != .Struct or !tuple_info.Struct.is_tuple) {
return error.GenerateArgNotTuple;
}
const tuple_members = tuple_info.Struct.fields;
// first iteration to get the total number of members
var members_nb = 0;
for (tuple_members) |member| {
const member_T = @field(tuple, member.name);
const member_info = @typeInfo(member_T);
if (member_info == .Union) {
const member_union = member_info.Union;
members_nb += member_union.fields.len;
} else if (member_info == .Struct) {
members_nb += 1;
} else {
return error.GenerateMemberNotUnionOrStruct;
}
}
// define the tag type regarding the members nb
var tag_type: type = undefined;
if (members_nb < 3) {
tag_type = u1;
} else if (members_nb < 4) {
tag_type = u2;
} else if (members_nb < 8) {
tag_type = u3;
} else if (members_nb < 16) {
tag_type = u4;
} else if (members_nb < 32) {
tag_type = u5;
} else if (members_nb < 64) {
tag_type = u6;
} else if (members_nb < 128) {
tag_type = u7;
} else if (members_nb < 256) {
tag_type = u8;
} else if (members_nb < 65536) {
tag_type = u16;
} else {
return error.GenerateTooMuchMembers;
}
// second iteration to generate tags
var enum_fields: [members_nb]std.builtin.Type.EnumField = undefined;
var done = 0;
for (tuple_members) |member| {
const member_T = @field(tuple, member.name);
const member_info = @typeInfo(member_T);
if (member_info == .Union) {
const member_union = member_info.Union;
for (member_union.fields) |field| {
enum_fields[done] = .{
.name = fmtName(field.type),
.value = done,
};
done += 1;
}
} else if (member_info == .Struct) {
enum_fields[done] = .{
.name = fmtName(member_T),
.value = done,
};
done += 1;
}
}
const decls: [0]std.builtin.Type.Declaration = undefined;
const enum_info = std.builtin.Type.Enum{
.tag_type = tag_type,
.fields = &enum_fields,
.decls = &decls,
.is_exhaustive = true,
};
const enum_T = @Type(std.builtin.Type{ .Enum = enum_info });
// third iteration to generate union type
var union_fields: [members_nb]std.builtin.Type.UnionField = undefined;
done = 0;
for (tuple_members, 0..) |member, i| {
const member_T = @field(tuple, member.name);
const member_info = @typeInfo(member_T);
if (member_info == .Union) {
const member_union = member_info.Union;
for (member_union.fields) |field| {
var T: type = undefined;
if (@hasDecl(field.type, "Self")) {
T = @field(field.type, "Self");
T = *T;
} else {
T = field.type;
}
union_fields[done] = .{
.name = fmtName(field.type),
.type = T,
.alignment = @alignOf(T),
};
done += 1;
}
} else if (member_info == .Struct) {
const member_name = try itoa(i);
var T = @field(tuple, member_name);
if (@hasDecl(T, "Self")) {
T = @field(T, "Self");
T = *T;
}
union_fields[done] = .{
// UnionField.name expect a null terminated string.
// concatenate the `[]const u8` string with an empty string
// literal (`name ++ ""`) to explicitly coerce it to `[:0]const
// u8`.
.name = fmtName(member_T) ++ "",
.type = T,
.alignment = @alignOf(T),
};
done += 1;
}
}
const union_info = std.builtin.Type.Union{
.layout = .auto,
.tag_type = enum_T,
.fields = &union_fields,
.decls = &decls,
};
const union_T = @Type(std.builtin.Type{ .Union = union_info });
return .{
._enum = enum_T,
._union = union_T,
// second iteration to generate tags
var enum_fields: [fields.len]Type.EnumField = undefined;
for (fields, 0..) |field, index| {
const member = @field(tuple, field.name);
const full_name = @typeName(member);
const separator = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse unreachable;
const name = full_name[separator + 1 ..];
enum_fields[index] = .{
.name = name ++ "",
.value = index,
};
}
};
const enum_info = Type.Enum{
.tag_type = tag_type,
.fields = &enum_fields,
.decls = &.{},
.is_exhaustive = true,
};
const enum_T = @Type(.{ .Enum = enum_info });
// third iteration to generate union type
var union_fields: [fields.len]Type.UnionField = undefined;
for (fields, enum_fields, 0..) |field, e, index| {
var FT = @field(tuple, field.name);
if (@hasDecl(FT, "Self")) {
FT = *(@field(FT, "Self"));
}
union_fields[index] = .{
.type = FT,
.name = e.name,
.alignment = @alignOf(FT),
};
}
return @Type(.{ .Union = .{
.layout = .auto,
.tag_type = enum_T,
.fields = &union_fields,
.decls = &.{},
} });
}
// Tuple
// -----
fn tupleNb(comptime tuple: anytype) usize {
var nb = 0;
for (@typeInfo(@TypeOf(tuple)).Struct.fields) |member| {
const member_T = @field(tuple, member.name);
if (@TypeOf(member_T) == type) {
nb += 1;
} else {
const member_info = @typeInfo(@TypeOf(member_T));
if (member_info != .Struct and !member_info.Struct.is_tuple) {
@compileError("GenerateMemberNotTypeOrTuple");
}
for (member_info.Struct.fields) |field| {
if (@TypeOf(@field(member_T, field.name)) != type) {
@compileError("GenerateMemberTupleChildNotType");
}
}
nb += member_info.Struct.fields.len;
}
}
return nb;
}
fn tupleTypes(comptime nb: usize, comptime tuple: anytype) [nb]type {
var types: [nb]type = undefined;
var done = 0;
for (@typeInfo(@TypeOf(tuple)).Struct.fields) |member| {
const T = @field(tuple, member.name);
if (@TypeOf(T) == type) {
types[done] = T;
done += 1;
} else {
const info = @typeInfo(@TypeOf(T));
for (info.Struct.fields) |field| {
types[done] = @field(T, field.name);
done += 1;
}
}
}
return types;
}
fn isDup(comptime nb: usize, comptime list: [nb]type, comptime T: type, comptime i: usize) bool {
for (list, 0..) |item, index| {
if (i >= index) {
// check sequentially
continue;
}
if (T == item) {
return true;
}
}
return false;
}
fn dedupIndexes(comptime nb: usize, comptime types: [nb]type) [nb]i32 {
var dedup_indexes: [nb]i32 = undefined;
for (types, 0..) |T, i| {
if (isDup(nb, types, T, i)) {
dedup_indexes[i] = -1;
} else {
dedup_indexes[i] = i;
}
}
return dedup_indexes;
}
fn dedupNb(comptime nb: usize, comptime dedup_indexes: [nb]i32) usize {
var dedup_nb = 0;
for (dedup_indexes) |index| {
if (index != -1) {
dedup_nb += 1;
}
}
return dedup_nb;
}
fn TupleT(comptime tuple: anytype) type {
// Flattens and depuplicates a list of nested tuples. For example
// input: {A, B, {C, B, D}, {A, E}}
// output {A, B, C, D, E}
pub fn Tuple(args: anytype) type {
@setEvalBranchQuota(100000);
// logic
const nb = tupleNb(tuple);
const types = tupleTypes(nb, tuple);
const dedup_indexes = dedupIndexes(nb, types);
const dedup_nb = dedupNb(nb, dedup_indexes);
const count = countInterfaces(args, 0);
var interfaces: [count]type = undefined;
_ = flattenInterfaces(args, &interfaces, 0);
// generate the tuple type
var fields: [dedup_nb]std.builtin.Type.StructField = undefined;
var done = 0;
for (dedup_indexes) |index| {
if (index == -1) {
const unfiltered_count, const filter_set = filterMap(count, interfaces);
var field_index: usize = 0;
var fields: [unfiltered_count]Type.StructField = undefined;
for (filter_set, 0..) |filter, i| {
if (filter) {
continue;
}
fields[done] = .{
// StructField.name expect a null terminated string.
// concatenate the `[]const u8` string with an empty string
// literal (`name ++ ""`) to explicitly coerce it to `[:0]const
// u8`.
.name = try itoa(done) ++ "",
fields[field_index] = .{
.name = std.fmt.comptimePrint("{d}", .{field_index}),
.type = type,
.default_value = null,
.is_comptime = false,
// has to be true in order to properly capture the default value
.is_comptime = true,
.alignment = @alignOf(type),
.default_value = @ptrCast(&interfaces[i]),
};
done += 1;
field_index += 1;
}
const decls: [0]std.builtin.Type.Declaration = undefined;
const info = std.builtin.Type.Struct{
return @Type(.{ .Struct = .{
.layout = .auto,
.fields = &fields,
.decls = &decls,
.decls = &.{},
.is_tuple = true,
};
return @Type(std.builtin.Type{ .Struct = info });
} });
}
// Create a flatten tuple from various structs and tuple of structs
// Duplicates will be removed.
// TODO: make this function more generic
pub fn Tuple(comptime tuple: anytype) TupleT(tuple) {
// check types provided
const tuple_T = @TypeOf(tuple);
const tuple_info = @typeInfo(tuple_T);
if (tuple_info != .Struct or !tuple_info.Struct.is_tuple) {
@compileError("GenerateArgNotTuple");
}
// generate the type
const T = TupleT(tuple);
// logic
const nb = tupleNb(tuple);
const types = tupleTypes(nb, tuple);
const dedup_indexes = dedupIndexes(nb, types);
// instantiate the tuple
var t: T = undefined;
var done = 0;
for (dedup_indexes) |index| {
if (index == -1) {
continue;
fn countInterfaces(args: anytype, count: usize) usize {
var new_count = count;
for (@typeInfo(@TypeOf(args)).Struct.fields) |f| {
const member = @field(args, f.name);
if (@TypeOf(member) == type) {
new_count += 1;
} else {
new_count = countInterfaces(member, new_count);
}
const name = try itoa(done);
@field(t, name) = types[index];
done += 1;
}
return t;
return new_count;
}
// Tests
// -----
const Error = error{
GenerateArgNotTuple,
GenerateMemberNotUnionOrStruct,
GenerateMemberNotTupleOrStruct,
GenerateMemberTupleNotStruct,
GenerateTooMuchMembers,
};
const Astruct = struct {
value: u8 = 0,
};
const Bstruct = struct {
value: u8 = 0,
};
const Cstruct = struct {
value: u8 = 0,
};
const Dstruct = struct {
value: u8 = 0,
};
pub fn tests() !void {
// Union from structs
const FromStructs = try Union.private_compile(.{ Astruct, Bstruct, Cstruct });
const from_structs_enum = @typeInfo(FromStructs._enum);
try std.testing.expect(from_structs_enum == .Enum);
try std.testing.expect(from_structs_enum.Enum.fields.len == 3);
try std.testing.expect(from_structs_enum.Enum.tag_type == u2);
try std.testing.expect(from_structs_enum.Enum.fields[0].value == 0);
try std.testing.expectEqualStrings(from_structs_enum.Enum.fields[0].name, "Astruct");
const from_structs_union = @typeInfo(FromStructs._union);
try std.testing.expect(from_structs_union == .Union);
try std.testing.expect(from_structs_union.Union.tag_type == FromStructs._enum);
try std.testing.expect(from_structs_union.Union.fields.len == 3);
try std.testing.expect(from_structs_union.Union.fields[0].type == Astruct);
try std.testing.expectEqualStrings(from_structs_union.Union.fields[0].name, "Astruct");
// Union from union and structs
const FromMix = try Union.private_compile(.{ FromStructs._union, Dstruct });
const from_mix_enum = @typeInfo(FromMix._enum);
try std.testing.expect(from_mix_enum == .Enum);
try std.testing.expect(from_mix_enum.Enum.fields.len == 4);
try std.testing.expect(from_mix_enum.Enum.tag_type == u3);
try std.testing.expect(from_mix_enum.Enum.fields[0].value == 0);
try std.testing.expectEqualStrings(from_mix_enum.Enum.fields[3].name, "Dstruct");
const from_mix_union = @typeInfo(FromMix._union);
try std.testing.expect(from_mix_union == .Union);
try std.testing.expect(from_mix_union.Union.tag_type == FromMix._enum);
try std.testing.expect(from_mix_union.Union.fields.len == 4);
try std.testing.expect(from_mix_union.Union.fields[3].type == Dstruct);
try std.testing.expectEqualStrings(from_mix_union.Union.fields[3].name, "Dstruct");
std.debug.print("Generate Union: OK\n", .{});
// Tuple from structs
const tuple_structs = .{ Astruct, Bstruct };
const tFromStructs = Tuple(tuple_structs);
const t_from_structs = @typeInfo(@TypeOf(tFromStructs));
try std.testing.expect(t_from_structs == .Struct);
try std.testing.expect(t_from_structs.Struct.is_tuple);
try std.testing.expect(t_from_structs.Struct.fields.len == 2);
try std.testing.expect(@field(tFromStructs, "0") == Astruct);
try std.testing.expect(@field(tFromStructs, "1") == Bstruct);
// Tuple from tuple and structs
const tuple_mix = .{ tFromStructs, Cstruct };
const tFromMix = Tuple(tuple_mix);
const t_from_mix = @typeInfo(@TypeOf(tFromMix));
try std.testing.expect(t_from_mix == .Struct);
try std.testing.expect(t_from_mix.Struct.is_tuple);
try std.testing.expect(t_from_mix.Struct.fields.len == 3);
try std.testing.expect(@field(tFromMix, "0") == Astruct);
try std.testing.expect(@field(tFromMix, "1") == Bstruct);
try std.testing.expect(@field(tFromMix, "2") == Cstruct);
// Tuple with dedup
const tuple_dedup = .{ Cstruct, Astruct, tFromStructs, Bstruct };
const tFromDedup = Tuple(tuple_dedup);
const t_from_dedup = @typeInfo(@TypeOf(tFromDedup));
try std.testing.expect(t_from_dedup == .Struct);
try std.testing.expect(t_from_dedup.Struct.is_tuple);
try std.testing.expect(t_from_dedup.Struct.fields.len == 3);
try std.testing.expect(@field(tFromDedup, "0") == Cstruct);
try std.testing.expect(@field(tFromDedup, "1") == Astruct);
try std.testing.expect(@field(tFromDedup, "2") == Bstruct);
std.debug.print("Generate Tuple: OK\n", .{});
fn flattenInterfaces(args: anytype, interfaces: []type, index: usize) usize {
var new_index = index;
for (@typeInfo(@TypeOf(args)).Struct.fields) |f| {
const member = @field(args, f.name);
if (@TypeOf(member) == type) {
interfaces[new_index] = member;
new_index += 1;
} else {
new_index = flattenInterfaces(member, interfaces, new_index);
}
}
return new_index;
}
fn filterMap(comptime count: usize, interfaces: [count]type) struct { usize, [count]bool } {
var map: [count]bool = undefined;
var unfiltered_count: usize = 0;
outer: for (interfaces, 0..) |iface, i| {
for (interfaces[i + 1 ..]) |check| {
if (iface == check) {
map[i] = true;
continue :outer;
}
}
map[i] = false;
unfiltered_count += 1;
}
return .{ unfiltered_count, map };
}
test "generate.Union" {
const Astruct = struct {
pub const Self = Other;
const Other = struct {};
};
const Bstruct = struct {
value: u8 = 0,
};
const Cstruct = struct {
value: u8 = 0,
};
const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
const ti = @typeInfo(value).Union;
try std.testing.expectEqual(3, ti.fields.len);
try std.testing.expectEqualStrings("*generate.test.generate.Union.Astruct.Other", @typeName(ti.fields[0].type));
try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
try std.testing.expectEqual(Bstruct, ti.fields[1].type);
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");
try std.testing.expectEqual(Cstruct, ti.fields[2].type);
try std.testing.expectEqualStrings(ti.fields[2].name, "Cstruct");
}
test "generate.Tuple" {
const Astruct = struct {};
const Bstruct = struct {
value: u8 = 0,
};
const Cstruct = struct {
value: u8 = 0,
};
{
const tuple = Tuple(.{ Astruct, Bstruct }){};
const ti = @typeInfo(@TypeOf(tuple)).Struct;
try std.testing.expectEqual(true, ti.is_tuple);
try std.testing.expectEqual(2, ti.fields.len);
try std.testing.expectEqual(Astruct, tuple.@"0");
try std.testing.expectEqual(Bstruct, tuple.@"1");
}
{
// dedupe
const tuple = Tuple(.{ Cstruct, Astruct, .{Astruct}, Bstruct, .{ Astruct, .{ Astruct, Bstruct } } }){};
const ti = @typeInfo(@TypeOf(tuple)).Struct;
try std.testing.expectEqual(true, ti.is_tuple);
try std.testing.expectEqual(3, ti.fields.len);
try std.testing.expectEqual(Cstruct, tuple.@"0");
try std.testing.expectEqual(Astruct, tuple.@"1");
try std.testing.expectEqual(Bstruct, tuple.@"2");
}
}

View File

@@ -28,6 +28,7 @@ const Node = @import("../dom/node.zig").Node;
const Document = @import("../dom/document.zig").Document;
const NodeList = @import("../dom/nodelist.zig").NodeList;
const HTMLElem = @import("elements.zig");
const Location = @import("location.zig").Location;
const collection = @import("../dom/html_collection.zig");
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
@@ -153,8 +154,12 @@ pub const HTMLDocument = struct {
return try collection.HTMLCollectionAll(parser.documentHTMLToNode(self), true);
}
pub fn get_currentScript(_: *parser.DocumentHTML) !?*parser.Element {
return null;
pub fn get_currentScript(self: *parser.DocumentHTML) !?*parser.Script {
return try parser.documentHTMLGetCurrentScript(self);
}
pub fn get_location(self: *parser.DocumentHTML) !?*Location {
return try parser.documentHTMLGetLocation(Location, self);
}
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

View File

@@ -26,6 +26,7 @@ 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 = .{
@@ -96,18 +97,45 @@ pub const Interfaces = .{
HTMLTrackElement,
HTMLUListElement,
HTMLVideoElement,
CSSProperties,
};
const Generated = generate.Union.compile(Interfaces);
pub const Union = Generated._union;
pub const Tags = Generated._enum;
pub const Union = generate.Union(Interfaces);
// Abstract class
// --------------
const CSSProperties = struct {
pub const mem_guarantied = true;
};
pub const HTMLElement = struct {
pub const Self = parser.ElementHTML;
pub const prototype = *Element;
pub const mem_guarantied = true;
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)
@@ -1059,4 +1087,12 @@ 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);
}

128
src/html/history.zig Normal file
View File

@@ -0,0 +1,128 @@
// 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,11 +21,17 @@ 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;
const Location = @import("location.zig").Location;
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
HTMLDocument,
HTMLElem.HTMLElement,
HTMLElem.HTMLMediaElement,
HTMLElem.Interfaces,
Window,
});
Navigator,
History,
Location,
};

129
src/html/location.zig Normal file
View File

@@ -0,0 +1,129 @@
// 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 URL = @import("../url/url.zig").URL;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
pub const Location = struct {
pub const mem_guarantied = true;
url: ?*URL = null,
pub fn deinit(_: *Location, _: std.mem.Allocator) void {}
pub fn get_href(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
if (self.url) |u| return u.get_href(alloc);
return "";
}
pub fn get_protocol(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
if (self.url) |u| return u.get_protocol(alloc);
return "";
}
pub fn get_host(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
if (self.url) |u| return u.get_host(alloc);
return "";
}
pub fn get_hostname(self: *Location) []const u8 {
if (self.url) |u| return u.get_hostname();
return "";
}
pub fn get_port(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
if (self.url) |u| return u.get_port(alloc);
return "";
}
pub fn get_pathname(self: *Location) []const u8 {
if (self.url) |u| return u.get_pathname();
return "";
}
pub fn get_search(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
if (self.url) |u| return u.get_search(alloc);
return "";
}
pub fn get_hash(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
if (self.url) |u| return u.get_hash(alloc);
return "";
}
pub fn get_origin(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
if (self.url) |u| return u.get_origin(alloc);
return "";
}
// TODO
pub fn _assign(_: *Location, url: []const u8) !void {
_ = url;
}
// TODO
pub fn _replace(_: *Location, url: []const u8) !void {
_ = url;
}
// TODO
pub fn _reload(_: *Location) !void {}
pub fn _toString(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
return try self.get_href(alloc);
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var location = [_]Case{
.{ .src = "location.href", .ex = "https://lightpanda.io/opensource-browser/" },
.{ .src = "document.location.href", .ex = "https://lightpanda.io/opensource-browser/" },
.{ .src = "location.host", .ex = "lightpanda.io" },
.{ .src = "location.hostname", .ex = "lightpanda.io" },
.{ .src = "location.origin", .ex = "https://lightpanda.io" },
.{ .src = "location.pathname", .ex = "/opensource-browser/" },
.{ .src = "location.hash", .ex = "" },
.{ .src = "location.port", .ex = "" },
.{ .src = "location.search", .ex = "" },
};
try checkCases(js_env, &location);
}

102
src/html/navigator.zig Normal file
View File

@@ -0,0 +1,102 @@
// 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

@@ -19,11 +19,20 @@
const std = @import("std");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
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 Location = @import("location.zig").Location;
const storage = @import("../storage/storage.zig");
var emptyLocation = Location{};
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct {
@@ -36,17 +45,36 @@ pub const Window = struct {
document: ?*parser.DocumentHTML = null,
target: []const u8,
history: History = .{},
location: *Location = &emptyLocation,
storageShelf: ?*storage.Shelf = null,
pub fn create(target: ?[]const u8) Window {
// store a map between internal timeouts ids and pointers to uint.
// the maximum number of possible timeouts is fixed.
timeoutid: u32 = 0,
timeoutids: [512]u64 = undefined,
navigator: Navigator,
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
return Window{
.target = target orelse "",
.navigator = navigator orelse .{},
};
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) void {
pub fn replaceLocation(self: *Window, loc: *Location) !void {
self.location = loc;
if (self.document != null) {
try parser.documentHTMLSetLocation(Location, self.document.?, self.location);
}
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.document = doc;
try parser.documentHTMLSetLocation(Location, doc, self.location);
}
pub fn setStorageShelf(self: *Window, shelf: *storage.Shelf) void {
@@ -57,6 +85,14 @@ pub const Window = struct {
return self;
}
pub fn get_navigator(self: *Window) *Navigator {
return &self.navigator;
}
pub fn get_location(self: *Window) *Location {
return self.location;
}
pub fn get_self(self: *Window) *Window {
return self;
}
@@ -69,6 +105,10 @@ 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;
}
@@ -82,4 +122,26 @@ pub const Window = struct {
if (self.storageShelf == null) return parser.DOMError.NotSupported;
return &self.storageShelf.?.bucket.session;
}
// TODO handle callback arguments.
pub fn _setTimeout(self: *Window, loop: *Loop, cbk: Callback, delay: ?u32) !u32 {
if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout;
const ddelay: u63 = delay orelse 0;
const id = loop.timeout(ddelay * std.time.ns_per_ms, cbk);
self.timeoutids[self.timeoutid] = id;
defer self.timeoutid += 1;
return self.timeoutid;
}
pub fn _clearTimeout(self: *Window, loop: *Loop, id: u32) void {
// I do would prefer return an error in this case, but it seems some JS
// uses invalid id, in particular id 0.
// So we silently ignore invalid id for now.
if (id >= self.timeoutid) return;
loop.cancel(self.timeoutids[id], null);
}
};

View File

@@ -1,21 +1,3 @@
// 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/>.
//! HTTP(S) Client implementation.
//!
//! Connections are opened in a thread-safe manner, but individual Requests are not.
@@ -24,7 +6,6 @@
const std = @import("std");
const builtin = @import("builtin");
const Stream = @import("stream.zig").Stream;
const testing = std.testing;
const http = std.http;
const mem = std.mem;
@@ -35,19 +16,15 @@ const assert = std.debug.assert;
const use_vectors = builtin.zig_backend != .stage2_x86_64;
const Client = @This();
const proto = http.protocol;
const proto = std.http.protocol;
const Loop = @import("jsruntime").Loop;
const tcp = @import("tcp.zig");
const tls23 = @import("tls");
pub const disable_tls = std.options.http_disable_tls;
/// Used for all client allocations. Must be thread-safe.
allocator: Allocator,
// std.net.Stream implementation using jsruntime Loop
loop: *Loop,
ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
ca_bundle_mutex: std.Thread.Mutex = .{},
@@ -215,9 +192,9 @@ pub const ConnectionPool = struct {
/// An interface to either a plain or TLS connection.
pub const Connection = struct {
stream: Stream,
stream: net.Stream,
/// undefined unless protocol is tls.
tls_client: if (!disable_tls) *std.crypto.tls.Client else void,
tls_client: if (!disable_tls) *tls23.Connection(net.Stream) else void,
/// The protocol that this connection is using.
protocol: Protocol,
@@ -246,12 +223,12 @@ pub const Connection = struct {
pub const Protocol = enum { plain, tls };
pub fn readvDirectTls(conn: *Connection, buffers: []std.posix.iovec) ReadError!usize {
return conn.tls_client.readv(conn.stream, buffers) catch |err| {
return conn.tls_client.readv(buffers) catch |err| {
// https://github.com/ziglang/zig/issues/2473
if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert;
switch (err) {
error.TlsConnectionTruncated, error.TlsRecordOverflow, error.TlsDecodeError, error.TlsBadRecordMac, error.TlsBadLength, error.TlsIllegalParameter, error.TlsUnexpectedMessage => return error.TlsFailure,
error.TlsRecordOverflow, error.TlsBadRecordMac, error.TlsUnexpectedMessage => return error.TlsFailure,
error.ConnectionTimedOut => return error.ConnectionTimedOut,
error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
else => return error.UnexpectedReadFailure,
@@ -278,7 +255,7 @@ pub const Connection = struct {
if (conn.read_end != conn.read_start) return;
var iovecs = [1]std.posix.iovec{
.{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len },
.{ .base = &conn.read_buf, .len = conn.read_buf.len },
};
const nread = try conn.readvDirect(&iovecs);
if (nread == 0) return error.EndOfStream;
@@ -314,8 +291,8 @@ pub const Connection = struct {
}
var iovecs = [2]std.posix.iovec{
.{ .iov_base = buffer.ptr, .iov_len = buffer.len },
.{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len },
.{ .base = buffer.ptr, .len = buffer.len },
.{ .base = &conn.read_buf, .len = conn.read_buf.len },
};
const nread = try conn.readvDirect(&iovecs);
@@ -344,7 +321,7 @@ pub const Connection = struct {
}
pub fn writeAllDirectTls(conn: *Connection, buffer: []const u8) WriteError!void {
return conn.tls_client.writeAll(conn.stream, buffer) catch |err| switch (err) {
return conn.tls_client.writeAll(buffer) catch |err| switch (err) {
error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
else => return error.UnexpectedWriteFailure,
};
@@ -412,7 +389,7 @@ pub const Connection = struct {
if (disable_tls) unreachable;
// try to cleanly close the TLS connection, for any server that cares.
_ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};
conn.tls_client.close() catch {};
allocator.destroy(conn.tls_client);
}
@@ -1350,7 +1327,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
errdefer client.allocator.destroy(conn);
conn.* = .{ .data = undefined };
const stream = tcp.tcpConnectToHost(client.allocator, client.loop, host, port) catch |err| switch (err) {
const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) {
error.ConnectionRefused => return error.ConnectionRefused,
error.NetworkUnreachable => return error.NetworkUnreachable,
error.ConnectionTimedOut => return error.ConnectionTimedOut,
@@ -1376,13 +1353,13 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
if (protocol == .tls) {
if (disable_tls) unreachable;
conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client);
conn.data.tls_client = try client.allocator.create(tls23.Connection(net.Stream));
errdefer client.allocator.destroy(conn.data.tls_client);
conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed;
// This is appropriate for HTTPS because the HTTP headers contain
// the content length which is used to detect truncation attacks.
conn.data.tls_client.allow_truncation_attacks = true;
conn.data.tls_client.* = tls23.client(stream, .{
.host = host,
.root_ca = client.ca_bundle,
}) catch return error.TlsInitializationFailed;
}
client.connection_pool.addUsed(conn);
@@ -1390,6 +1367,41 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
return &conn.data;
}
pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError;
/// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open.
///
/// This function is threadsafe.
pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection {
if (client.connection_pool.findConnection(.{
.host = path,
.port = 0,
.protocol = .plain,
})) |node|
return node;
const conn = try client.allocator.create(ConnectionPool.Node);
errdefer client.allocator.destroy(conn);
conn.* = .{ .data = undefined };
const stream = try std.net.connectUnixSocket(path);
errdefer stream.close();
conn.data = .{
.stream = stream,
.tls_client = undefined,
.protocol = .plain,
.host = try client.allocator.dupe(u8, path),
.port = 0,
};
errdefer client.allocator.free(conn.data.host);
client.connection_pool.addUsed(conn);
return &conn.data;
}
/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP
/// CONNECT. This will reuse a connection if one is already open.
///
@@ -1560,7 +1572,7 @@ pub const RequestOptions = struct {
};
fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } {
const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
const protocol_map = std.StaticStringMap(Connection.Protocol).initComptime(.{
.{ "http", .plain },
.{ "ws", .plain },
.{ "https", .tls },

53
src/iterator/iterator.zig Normal file
View File

@@ -0,0 +1,53 @@
const std = @import("std");
pub const Interfaces = .{
U32Iterator,
};
pub const U32Iterator = struct {
pub const mem_guarantied = true;
length: u32,
index: u32 = 0,
pub const Return = struct {
value: u32,
done: bool,
};
pub fn _next(self: *U32Iterator) Return {
const i = self.index;
if (i >= self.length) {
return .{
.value = 0,
.done = true,
};
}
self.index = i + 1;
return .{
.value = i,
.done = false,
};
}
};
const testing = std.testing;
test "U32Iterator" {
const Return = U32Iterator.Return;
{
var it = U32Iterator{ .length = 0 };
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
}
{
var it = U32Iterator{ .length = 3 };
try testing.expectEqual(Return{ .value = 0, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 1, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 2, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
}
}

View File

@@ -17,91 +17,291 @@
// 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 Browser = @import("browser/browser.zig").Browser;
const server = @import("server.zig");
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
const socket_path = "/tmp/browsercore-server.sock";
// Simple blocking websocket connection model
// ie. 1 thread per ws connection without thread pool and epoll/kqueue
pub const websocket_blocking = true;
var doc: *parser.DocumentHTML = undefined;
var server: std.net.Server = undefined;
const log = std.log.scoped(.cli);
fn execJS(
alloc: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
// start JS env
try js_env.start(alloc);
defer js_env.stop();
pub const std_options = .{
// Set the log level to info
.log_level = .debug,
// alias global as self and window
var window = Window.create(null);
window.replaceDocument(doc);
try js_env.bindGlobal(window);
// Define logFn to override the std implementation
.logFn = logFn,
};
while (true) {
const usage =
\\usage: {s} [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.
\\ --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")
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
\\ --dump Dump document in stdout (fetch mode only)
\\
;
// read cmd
const conn = try server.accept();
var buf: [100]u8 = undefined;
const read = try conn.stream.read(&buf);
const cmd = buf[0..read];
std.debug.print("<- {s}\n", .{cmd});
if (std.mem.eql(u8, cmd, "exit")) {
break;
}
const res = try js_env.execTryCatch(alloc, cmd, "cdp");
if (res.success) {
std.debug.print("-> {s}\n", .{res.result});
}
_ = try conn.stream.write(res.result);
}
fn printUsageExit(execname: []const u8, res: u8) anyerror {
std.io.getStdErr().writer().print(usage, .{execname}) catch |err| {
std.log.err("Print usage error: {any}", .{err});
return error.Cli;
};
if (res == 1) return error.Usage;
return error.NoError;
}
const CliModeTag = enum {
server,
fetch,
};
const CliMode = union(CliModeTag) {
server: Server,
fetch: Fetch,
const Server = struct {
execname: []const u8 = undefined,
args: *std.process.ArgIterator = undefined,
host: []const u8 = Host,
port: u16 = Port,
timeout: u8 = Timeout,
// default options
const Host = "127.0.0.1";
const Port = 9222;
const Timeout = 3; // in seconds
};
const Fetch = struct {
execname: []const u8 = undefined,
args: *std.process.ArgIterator = undefined,
url: []const u8 = "",
dump: bool = false,
};
fn init(alloc: std.mem.Allocator, args: *std.process.ArgIterator) !CliMode {
args.* = try std.process.argsWithAllocator(alloc);
errdefer args.deinit();
const execname = args.next().?;
var default_mode: CliModeTag = .server;
var _server = Server{};
var _fetch = Fetch{};
while (args.next()) |opt| {
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;
}
if (std.mem.eql(u8, "--host", opt)) {
if (args.next()) |arg| {
_server.host = arg;
continue;
} else {
std.log.err("--host not provided\n", .{});
return printUsageExit(execname, 1);
}
}
if (std.mem.eql(u8, "--port", opt)) {
if (args.next()) |arg| {
_server.port = std.fmt.parseInt(u16, arg, 10) catch |err| {
log.err("--port {any}\n", .{err});
return printUsageExit(execname, 1);
};
continue;
} else {
log.err("--port not provided\n", .{});
return printUsageExit(execname, 1);
}
}
if (std.mem.eql(u8, "--timeout", opt)) {
if (args.next()) |arg| {
_server.timeout = std.fmt.parseInt(u8, arg, 10) catch |err| {
log.err("--timeout {any}\n", .{err});
return printUsageExit(execname, 1);
};
continue;
} else {
log.err("--timeout not provided\n", .{});
return printUsageExit(execname, 1);
}
}
// unknown option
if (std.mem.startsWith(u8, opt, "--")) {
log.err("unknown option\n", .{});
return printUsageExit(execname, 1);
}
// other argument is considered to be an URL, ie. fetch mode
default_mode = .fetch;
// allow only one url
if (_fetch.url.len != 0) {
log.err("more than 1 url provided\n", .{});
return printUsageExit(execname, 1);
}
_fetch.url = opt;
}
if (default_mode == .server) {
// server mode
_server.execname = execname;
_server.args = args;
return CliMode{ .server = _server };
} else {
// fetch mode
_fetch.execname = execname;
_fetch.args = args;
return CliMode{ .fetch = _fetch };
}
}
fn deinit(self: CliMode) void {
switch (self) {
inline .server, .fetch => |*_mode| {
_mode.args.deinit();
},
}
}
};
pub fn main() !void {
// create v8 vm
const vm = jsruntime.VM.init();
defer vm.deinit();
// alloc
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
try parser.init();
defer parser.deinit();
// document
const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close();
doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
defer parser.documentHTMLClose(doc) catch |err| {
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
};
// remove socket file of internal server
// reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket
// see: https://gavv.net/articles/unix-socket-reuse/
// TODO: use a lock file instead
std.posix.unlink(socket_path) catch |err| {
if (err != error.FileNotFound) {
return err;
// allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the page allocator
var alloc: std.mem.Allocator = undefined;
var _gpa: ?std.heap.GeneralPurposeAllocator(.{}) = null;
if (builtin.mode == .Debug) {
_gpa = std.heap.GeneralPurposeAllocator(.{}){};
alloc = _gpa.?.allocator();
} else {
alloc = std.heap.page_allocator;
}
defer {
if (_gpa) |*gpa| {
switch (gpa.deinit()) {
.ok => std.debug.print("No memory leaks\n", .{}),
.leak => @panic("Memory leak"),
}
}
}
// args
var args: std.process.ArgIterator = undefined;
const cli_mode = CliMode.init(alloc, &args) catch |err| {
if (err == error.NoError) {
std.posix.exit(0);
} else {
std.posix.exit(1);
}
return;
};
defer cli_mode.deinit();
// server
const addr = try std.net.Address.initUnix(socket_path);
server = try addr.listen(.{});
defer server.deinit();
std.debug.print("Listening on: {s}...\n", .{socket_path});
switch (cli_mode) {
.server => |opts| {
const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| {
log.err("address (host:port) {any}\n", .{err});
return printUsageExit(opts.execname, 1);
};
try jsruntime.loadEnv(&arena, null, execJS);
var loop = try jsruntime.Loop.init(alloc);
defer loop.deinit();
const timeout = std.time.ns_per_s * @as(u64, opts.timeout);
server.run(alloc, address, timeout, &loop) catch |err| {
log.err("Server error", .{});
return err;
};
},
.fetch => |opts| {
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
// vm
const vm = jsruntime.VM.init();
defer vm.deinit();
// loop
var loop = try jsruntime.Loop.init(alloc);
defer loop.deinit();
// browser
var browser = Browser{};
try Browser.init(&browser, alloc, &loop, vm);
defer browser.deinit();
// page
const page = try browser.session.createPage();
try page.start(null);
defer page.end();
_ = page.navigate(opts.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);
},
else => {
log.err("'{s}' fetching error ({any})s\n", .{ opts.url, err });
return printUsageExit(opts.execname, 1);
},
};
try page.wait();
// dump
if (opts.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

@@ -1,97 +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 Browser = @import("browser/browser.zig").Browser;
const jsruntime = @import("jsruntime");
const apiweb = @import("apiweb.zig");
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
pub const std_options = std.Options{
.log_level = .debug,
};
const usage =
\\usage: {s} [options] <url>
\\ request the url with the browser
\\
\\ -h, --help Print this help message and exit.
\\ --dump Dump document in stdout
\\
;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const check = gpa.deinit();
if (check == .leak) {
std.log.warn("leaks detected\n", .{});
}
}
const allocator = gpa.allocator();
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
const execname = args.next().?;
var url: []const u8 = "";
var dump: bool = false;
while (args.next()) |arg| {
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.posix.exit(0);
}
if (std.mem.eql(u8, "--dump", arg)) {
dump = true;
continue;
}
// allow only one url
if (url.len != 0) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.posix.exit(1);
}
url = arg;
}
if (url.len == 0) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.posix.exit(1);
}
const vm = jsruntime.VM.init();
defer vm.deinit();
var browser = try Browser.init(allocator, vm);
defer browser.deinit();
var page = try browser.currentSession().createPage();
defer page.deinit();
try page.navigate(url);
defer page.end();
try page.wait();
if (dump) {
try page.dump(std.io.getStdOut());
}
}

View File

@@ -24,12 +24,13 @@ const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
const storage = @import("storage/storage.zig");
const Client = @import("asyncio").Client;
const html_test = @import("html_test.zig").html;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
const Client = @import("async/Client.zig");
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
var doc: *parser.DocumentHTML = undefined;
@@ -38,10 +39,10 @@ fn execJS(
js_env: *jsruntime.Env,
) anyerror!void {
// start JS env
try js_env.start(alloc);
try js_env.start();
defer js_env.stop();
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
var cli = Client{ .allocator = alloc };
defer cli.deinit();
try js_env.setUserContext(UserContext{
@@ -53,8 +54,8 @@ fn execJS(
defer storageShelf.deinit();
// alias global as self and window
var window = Window.create(null);
window.replaceDocument(doc);
var window = Window.create(null, null);
try window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window);
@@ -87,5 +88,5 @@ pub fn main() !void {
defer vm.deinit();
// launch shell
try jsruntime.shell(&arena, execJS, .{ .app_name = "browsercore" });
try jsruntime.shell(&arena, execJS, .{ .app_name = "lightpanda-shell" });
}

View File

@@ -29,8 +29,10 @@ const Window = @import("html/window.zig").Window;
const xhr = @import("xhr/xhr.zig");
const storage = @import("storage/storage.zig");
const url = @import("url/url.zig");
const URL = url.URL;
const urlquery = @import("url/query.zig");
const Client = @import("async/Client.zig");
const Client = @import("asyncio").Client;
const Location = @import("html/location.zig").Location;
const documentTestExecFn = @import("dom/document.zig").testExecFn;
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
@@ -59,6 +61,7 @@ const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExec
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = @import("user_context.zig").UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
var doc: *parser.DocumentHTML = undefined;
@@ -71,7 +74,7 @@ fn testExecFn(
defer parser.deinit();
// start JS env
try js_env.start(alloc);
try js_env.start();
defer js_env.stop();
var storageShelf = storage.Shelf.init(alloc);
@@ -86,7 +89,7 @@ fn testExecFn(
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
};
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
var cli = Client{ .allocator = alloc };
defer cli.deinit();
try js_env.setUserContext(.{
@@ -95,9 +98,14 @@ fn testExecFn(
});
// alias global as self and window
var window = Window.create(null);
var window = Window.create(null, null);
window.replaceDocument(doc);
var u = try URL.constructor(alloc, "https://lightpanda.io/opensource-browser/", null);
defer u.deinit(alloc);
var location = Location{ .url = &u };
try window.replaceLocation(&location);
try window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window);
@@ -135,6 +143,11 @@ fn testsAllExecFn(
URLTestExecFn,
HTMLElementTestExecFn,
MutationObserverTestExecFn,
@import("polyfill/fetch.zig").testExecFn,
@import("html/navigator.zig").testExecFn,
@import("html/history.zig").testExecFn,
@import("html/location.zig").testExecFn,
@import("xmlserializer/xmlserializer.zig").testExecFn,
};
inline for (testFns) |testFn| {
@@ -210,8 +223,14 @@ pub fn main() !void {
try parser.init();
defer parser.deinit();
std.testing.allocator_instance = .{};
try test_fn.func();
std.debug.print("{s}\tOK\n", .{test_fn.name});
if (std.testing.allocator_instance.deinit() == .leak) {
std.debug.print("======Memory Leak: {s}======\n", .{test_fn.name});
} else {
std.debug.print("{s}\tOK\n", .{test_fn.name});
}
}
}
}
@@ -283,7 +302,7 @@ fn run_js(out: Out) !void {
const row_shape = .{ []const u8, pretty.Measure, u64, u64, pretty.Measure };
const table = try pretty.GenerateTable(4, row_shape, pretty.TableConf{ .margin_left = " " });
const header = .{ "FUNCTION", "DURATION", "ALLOCATIONS (nb)", "RE-ALLOCATIONS (nb)", "HEAP SIZE" };
var t = table.init("Benchmark browsercore 🚀", header);
var t = table.init("Benchmark lightpanda 🚀", header);
try t.addRow(.{ "browser", dur, stats.alloc_nb, stats.realloc_nb, size });
try t.addRow(.{ "libdom", dur, 0, 0, zerosize }); // TODO get libdom bench info.
try t.addRow(.{ "v8", dur, 0, 0, zerosize }); // TODO get v8 bench info.
@@ -295,9 +314,6 @@ const kb = 1024;
const ms = std.time.ns_per_ms;
test {
const asyncTest = @import("async/test.zig");
std.testing.refAllDecls(asyncTest);
const dumpTest = @import("browser/dump.zig");
std.testing.refAllDecls(dumpTest);
@@ -318,12 +334,18 @@ test {
const queryTest = @import("url/query.zig");
std.testing.refAllDecls(queryTest);
std.testing.refAllDecls(@import("generate.zig"));
std.testing.refAllDecls(@import("cdp/msg.zig"));
// Don't use refAllDecls, as this will pull in the entire project
// and break the test build.
// We should fix this. See this branch & the commit message for details:
// https://github.com/karlseguin/browser/commit/193ab5ceab3d3758ea06db04f7690460d79eb79e
_ = @import("server.zig");
}
fn testJSRuntime(alloc: std.mem.Allocator) !void {
// generate tests
try generate.tests();
// create JS vm
const vm = jsruntime.VM.init();
defer vm.deinit();
@@ -355,7 +377,7 @@ test "bug document html parsing #4" {
}
test "Window is a libdom event target" {
var window = Window.create(null);
var window = Window.create(null, null);
const event = try parser.eventCreate();
try parser.eventInit(event, "foo", .{});

View File

@@ -50,11 +50,12 @@ const Out = enum {
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const GlobalType = apiweb.GlobalType;
pub const UserContext = apiweb.UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
// TODO For now the WPT tests run is specific to WPT.
// It manually load js framwork libs, and run the first script w/ js content in
// the HTML page.
// Once browsercore will have the html loader, it would be useful to refacto
// Once lightpanda will have the html loader, it would be useful to refacto
// this test to use it.
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@@ -142,7 +143,7 @@ pub fn main() !void {
defer arena.deinit();
const res = wpt.run(&arena, wpt_dir, tc, &loader) catch |err| {
const suite = try Suite.init(alloc, tc, false, @errorName(err), null);
const suite = try Suite.init(alloc, tc, false, @errorName(err));
try results.append(suite);
if (out == .text) {
@@ -151,9 +152,9 @@ pub fn main() !void {
failures += 1;
continue;
};
// no need to call res.deinit() thanks to the arena allocator.
defer res.deinit(arena.allocator());
const suite = try Suite.init(alloc, tc, res.success, res.result, res.stack);
const suite = try Suite.init(alloc, tc, res.ok, res.msg orelse "");
try results.append(suite);
if (out == .json) {
@@ -196,7 +197,7 @@ pub fn main() !void {
try cases.append(Case{
.pass = suite.pass,
.name = suite.name,
.message = suite.stack orelse suite.message,
.message = suite.message,
});
}
@@ -288,7 +289,7 @@ fn runSafe(
argv.appendAssumeCapacity(tc);
defer _ = argv.pop();
const run = try std.ChildProcess.run(.{
const run = try std.process.Child.run(.{
.allocator = alloc,
.argv = argv.items,
.max_output_bytes = 1024 * 1024,

View File

@@ -1008,6 +1008,7 @@ pub fn nodeLocalName(node: *Node) ![]const u8 {
var s: ?*String = undefined;
const err = nodeVtable(node).dom_node_get_local_name.?(node, &s);
try DOMErr(err);
if (s == null) return "";
var s_lower: ?*String = undefined;
const errStr = c.dom_string_tolower(s, true, &s_lower);
try DOMErr(errStr);
@@ -1098,6 +1099,7 @@ pub fn nodeName(node: *Node) ![]const u8 {
var s: ?*String = undefined;
const err = nodeVtable(node).dom_node_get_node_name.?(node, &s);
try DOMErr(err);
if (s == null) return "";
return strToData(s.?);
}
@@ -1794,7 +1796,7 @@ pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
// Document Position
pub const DocumentPosition = enum(u2) {
pub const DocumentPosition = enum(u32) {
disconnected = c.DOM_DOCUMENT_POSITION_DISCONNECTED,
preceding = c.DOM_DOCUMENT_POSITION_PRECEDING,
following = c.DOM_DOCUMENT_POSITION_FOLLOWING,
@@ -2249,3 +2251,35 @@ pub inline fn documentHTMLSetTitle(doc: *DocumentHTML, v: []const u8) !void {
const err = documentHTMLVtable(doc).set_title.?(doc, try strFromData(v));
try DOMErr(err);
}
pub fn documentHTMLSetCurrentScript(doc: *DocumentHTML, script: ?*Script) !void {
var s: ?*ElementHTML = null;
if (script != null) s = @ptrCast(script.?);
const err = documentHTMLVtable(doc).set_current_script.?(doc, s);
try DOMErr(err);
}
pub fn documentHTMLGetCurrentScript(doc: *DocumentHTML) !?*Script {
var elem: ?*ElementHTML = undefined;
const err = documentHTMLVtable(doc).get_current_script.?(doc, &elem);
try DOMErr(err);
if (elem == null) return null;
return @ptrCast(elem.?);
}
pub fn documentHTMLSetLocation(T: type, doc: *DocumentHTML, location: *T) !void {
const l = @as(*anyopaque, @ptrCast(location));
const err = documentHTMLVtable(doc).set_location.?(doc, l);
try DOMErr(err);
}
pub fn documentHTMLGetLocation(T: type, doc: *DocumentHTML) !?*T {
var l: ?*anyopaque = undefined;
const err = documentHTMLVtable(doc).get_location.?(doc, &l);
try DOMErr(err);
if (l == null) return null;
const ptr: *align(@alignOf(*T)) anyopaque = @alignCast(l.?);
return @as(*T, @ptrCast(ptr));
}

671
src/polyfill/fetch.js Normal file
View File

@@ -0,0 +1,671 @@
// 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 });
})));

55
src/polyfill/fetch.zig Normal file
View File

@@ -0,0 +1,55 @@
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);
}

56
src/polyfill/polyfill.zig Normal file
View File

@@ -0,0 +1,56 @@
// 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 });
}
}
}

1774
src/server.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,15 +21,13 @@ const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const DOMError = @import("netsurf").DOMError;
const log = std.log.scoped(.storage);
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
Bottle,
});
};
// See https://storage.spec.whatwg.org/#model for storage hierarchy.
// A Shed contains map of Shelves. The key is the document's origin.
@@ -151,20 +149,22 @@ pub const Bottle = struct {
}
pub fn _setItem(self: *Bottle, k: []const u8, v: []const u8) !void {
const old = self.map.get(k);
if (old != null and std.mem.eql(u8, v, old.?)) return;
// owns k and v by copying them.
const kk = try self.alloc.dupe(u8, k);
errdefer self.alloc.free(kk);
const vv = try self.alloc.dupe(u8, v);
errdefer self.alloc.free(vv);
self.map.put(self.alloc, kk, vv) catch |e| {
const gop = self.map.getOrPut(self.alloc, k) catch |e| {
log.debug("set item: {any}", .{e});
return DOMError.QuotaExceeded;
};
if (gop.found_existing == false) {
gop.key_ptr.* = try self.alloc.dupe(u8, k);
gop.value_ptr.* = try self.alloc.dupe(u8, v);
return;
}
if (std.mem.eql(u8, v, gop.value_ptr.*) == false) {
self.alloc.free(gop.value_ptr.*);
gop.value_ptr.* = try self.alloc.dupe(u8, v);
}
// > Broadcast this with key, oldValue, and value.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
//
@@ -173,12 +173,14 @@ 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 impement the feature.
// So for now, we won't implement the feature.
}
pub fn _removeItem(self: *Bottle, k: []const u8) !void {
const old = self.map.fetchRemove(k);
if (old == null) return;
if (self.map.fetchRemove(k)) |kv| {
self.alloc.free(kv.key);
self.alloc.free(kv.value);
}
// > Broadcast this with key, oldValue, and null.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
@@ -237,14 +239,17 @@ test "storage bottle" {
var bottle = Bottle.init(std.testing.allocator);
defer bottle.deinit();
try std.testing.expect(0 == bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo"));
try std.testing.expectEqual(0, bottle.get_length());
try std.testing.expectEqual(null, bottle._getItem("foo"));
try bottle._setItem("foo", "bar");
try std.testing.expect(std.mem.eql(u8, "bar", bottle._getItem("foo").?));
try std.testing.expectEqualStrings("bar", bottle._getItem("foo").?);
try bottle._setItem("foo", "other");
try std.testing.expectEqualStrings("other", bottle._getItem("foo").?);
try bottle._removeItem("foo");
try std.testing.expect(0 == bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo"));
try std.testing.expectEqual(0, bottle.get_length());
try std.testing.expectEqual(null, bottle._getItem("foo"));
}

View File

@@ -18,89 +18,108 @@
// some utils to parser strings.
const std = @import("std");
const testing = std.testing;
pub const Reader = struct {
s: []const u8,
i: usize = 0,
pos: usize = 0,
data: []const u8,
pub fn until(self: *Reader, c: u8) []const u8 {
const ln = self.s.len;
const start = self.i;
while (self.i < ln) {
if (c == self.s[self.i]) return self.s[start..self.i];
self.i += 1;
}
const pos = self.pos;
const data = self.data;
return self.s[start..self.i];
const index = std.mem.indexOfScalarPos(u8, data, pos, c) orelse data.len;
self.pos = index;
return data[pos..index];
}
pub fn tail(self: *Reader) []const u8 {
if (self.i > self.s.len) return "";
defer self.i = self.s.len;
return self.s[self.i..];
const pos = self.pos;
const data = self.data;
if (pos > data.len) {
return "";
}
self.pos = data.len;
return data[pos..];
}
pub fn skip(self: *Reader) bool {
if (self.i >= self.s.len) return false;
self.i += 1;
const pos = self.pos;
if (pos >= self.data.len) {
return false;
}
self.pos = pos + 1;
return true;
}
};
test "Reader.skip" {
var r = Reader{ .s = "foo" };
try testing.expect(r.skip());
try testing.expect(r.skip());
try testing.expect(r.skip());
try testing.expect(!r.skip());
try testing.expect(!r.skip());
// converts a comptime-known string (i.e. null terminated) to an uint
pub fn asUint(comptime string: anytype) AsUintReturn(string) {
const byteLength = @bitSizeOf(@TypeOf(string.*)) / 8 - 1;
const expectedType = *const [byteLength:0]u8;
if (@TypeOf(string) != expectedType) {
@compileError("expected : " ++ @typeName(expectedType) ++
", got: " ++ @typeName(@TypeOf(string)));
}
return @bitCast(@as(*const [byteLength]u8, string).*);
}
test "Reader.tail" {
var r = Reader{ .s = "foo" };
fn AsUintReturn(comptime string: anytype) type {
return @Type(.{
.Int = .{
.bits = @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
.signedness = .unsigned,
},
});
}
const testing = std.testing;
test "parser.Reader: skip" {
var r = Reader{ .data = "foo" };
try testing.expectEqual(true, r.skip());
try testing.expectEqual(true, r.skip());
try testing.expectEqual(true, r.skip());
try testing.expectEqual(false, r.skip());
try testing.expectEqual(false, r.skip());
}
test "parser.Reader: tail" {
var r = Reader{ .data = "foo" };
try testing.expectEqualStrings("foo", r.tail());
try testing.expectEqualStrings("", r.tail());
try testing.expectEqualStrings("", r.tail());
}
test "Reader.until" {
var r = Reader{ .s = "foo.bar.baz" };
test "parser.Reader: until" {
var r = Reader{ .data = "foo.bar.baz" };
try testing.expectEqualStrings("foo", r.until('.'));
_ = r.skip();
try testing.expectEqualStrings("bar", r.until('.'));
_ = r.skip();
try testing.expectEqualStrings("baz", r.until('.'));
r = Reader{ .s = "foo" };
r = Reader{ .data = "foo" };
try testing.expectEqualStrings("foo", r.until('.'));
try testing.expectEqualStrings("", r.tail());
r = Reader{ .s = "" };
r = Reader{ .data = "" };
try testing.expectEqualStrings("", r.until('.'));
try testing.expectEqualStrings("", r.tail());
}
pub fn trim(s: []const u8) []const u8 {
const ln = s.len;
if (ln == 0) {
return "";
}
var start: usize = 0;
while (start < ln) {
if (!std.ascii.isWhitespace(s[start])) break;
start += 1;
}
test "parser: asUint" {
const ASCII_x = @as(u8, @bitCast([1]u8{'x'}));
const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' }));
const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' }));
const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' }));
var end: usize = ln;
while (end > 0) {
if (!std.ascii.isWhitespace(s[end - 1])) break;
end -= 1;
}
try testing.expectEqual(ASCII_x, asUint("x"));
try testing.expectEqual(ASCII_ab, asUint("ab"));
try testing.expectEqual(ASCII_xyz, asUint("xyz"));
try testing.expectEqual(ASCII_abcd, asUint("abcd"));
return s[start..end];
}
test "trim" {
try testing.expectEqualStrings("", trim(""));
try testing.expectEqualStrings("foo", trim("foo"));
try testing.expectEqualStrings("foo", trim(" \n\tfoo"));
try testing.expectEqualStrings("foo", trim("foo \n\t"));
try testing.expectEqual(u8, @TypeOf(asUint("x")));
try testing.expectEqual(u16, @TypeOf(asUint("ab")));
try testing.expectEqual(u24, @TypeOf(asUint("xyz")));
try testing.expectEqual(u32, @TypeOf(asUint("abcd")));
}

View File

@@ -1,28 +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 tests = @import("run_tests.zig");
pub const Types = tests.Types;
pub const UserContext = tests.UserContext;
pub fn main() !void {
try tests.main();
}

347
src/unit_tests.zig Normal file
View File

@@ -0,0 +1,347 @@
// 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 Allocator = std.mem.Allocator;
pub const std_options = std.Options{
.http_disable_tls = true,
};
const BORDER = "=" ** 80;
// use in custom panic handler
var current_test: ?[]const u8 = null;
pub fn main() !void {
var mem: [8192]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&mem);
const allocator = fba.allocator();
const env = Env.init(allocator);
defer env.deinit(allocator);
var slowest = SlowTracker.init(allocator, 5);
defer slowest.deinit();
var pass: usize = 0;
var fail: usize = 0;
var skip: usize = 0;
var leak: usize = 0;
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
var listener = try address.listen(.{ .reuse_address = true });
defer listener.deinit();
const http_thread = try std.Thread.spawn(.{}, serverHTTP, .{&listener});
defer http_thread.join();
const printer = Printer.init();
printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
for (builtin.test_functions) |t| {
if (std.mem.eql(u8, t.name, "unit_tests.test_0")) {
// don't display anything for this test
try t.func();
continue;
}
var status = Status.pass;
slowest.startTiming();
const is_unnamed_test = isUnnamed(t);
if (env.filter) |f| {
if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) {
continue;
}
}
const friendly_name = blk: {
const name = t.name;
var it = std.mem.splitScalar(u8, name, '.');
while (it.next()) |value| {
if (std.mem.eql(u8, value, "test")) {
const rest = it.rest();
break :blk if (rest.len > 0) rest else name;
}
}
break :blk name;
};
current_test = friendly_name;
std.testing.allocator_instance = .{};
const result = t.func();
current_test = null;
const ns_taken = slowest.endTiming(friendly_name);
if (std.testing.allocator_instance.deinit() == .leak) {
leak += 1;
printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER });
}
if (result) |_| {
pass += 1;
} else |err| switch (err) {
error.SkipZigTest => {
skip += 1;
status = .skip;
},
else => {
status = .fail;
fail += 1;
printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER });
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
if (env.fail_first) {
break;
}
},
}
if (env.verbose) {
const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
} else {
printer.status(status, ".", .{});
}
}
const total_tests = pass + fail;
const status = if (fail == 0) Status.pass else Status.fail;
printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
if (skip > 0) {
printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
}
if (leak > 0) {
printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
}
printer.fmt("\n", .{});
try slowest.display(printer);
printer.fmt("\n", .{});
std.posix.exit(if (fail == 0) 0 else 1);
}
const Printer = struct {
out: std.fs.File.Writer,
fn init() Printer {
return .{
.out = std.io.getStdErr().writer(),
};
}
fn fmt(self: Printer, comptime format: []const u8, args: anytype) void {
std.fmt.format(self.out, format, args) catch unreachable;
}
fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void {
const color = switch (s) {
.pass => "\x1b[32m",
.fail => "\x1b[31m",
.skip => "\x1b[33m",
else => "",
};
const out = self.out;
out.writeAll(color) catch @panic("writeAll failed?!");
std.fmt.format(out, format, args) catch @panic("std.fmt.format failed?!");
self.fmt("\x1b[0m", .{});
}
};
const Status = enum {
pass,
fail,
skip,
text,
};
const SlowTracker = struct {
const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);
max: usize,
slowest: SlowestQueue,
timer: std.time.Timer,
fn init(allocator: Allocator, count: u32) SlowTracker {
const timer = std.time.Timer.start() catch @panic("failed to start timer");
var slowest = SlowestQueue.init(allocator, {});
slowest.ensureTotalCapacity(count) catch @panic("OOM");
return .{
.max = count,
.timer = timer,
.slowest = slowest,
};
}
const TestInfo = struct {
ns: u64,
name: []const u8,
};
fn deinit(self: SlowTracker) void {
self.slowest.deinit();
}
fn startTiming(self: *SlowTracker) void {
self.timer.reset();
}
fn endTiming(self: *SlowTracker, test_name: []const u8) u64 {
var timer = self.timer;
const ns = timer.lap();
var slowest = &self.slowest;
if (slowest.count() < self.max) {
// Capacity is fixed to the # of slow tests we want to track
// If we've tracked fewer tests than this capacity, than always add
slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
return ns;
}
{
// Optimization to avoid shifting the dequeue for the common case
// where the test isn't one of our slowest.
const fastest_of_the_slow = slowest.peekMin() orelse unreachable;
if (fastest_of_the_slow.ns > ns) {
// the test was faster than our fastest slow test, don't add
return ns;
}
}
// the previous fastest of our slow tests, has been pushed off.
_ = slowest.removeMin();
slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
return ns;
}
fn display(self: *SlowTracker, printer: Printer) !void {
var slowest = self.slowest;
const count = slowest.count();
printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" });
while (slowest.removeMinOrNull()) |info| {
const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;
printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name });
}
}
fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order {
_ = context;
return std.math.order(a.ns, b.ns);
}
};
const Env = struct {
verbose: bool,
fail_first: bool,
filter: ?[]const u8,
fn init(allocator: Allocator) Env {
return .{
.verbose = readEnvBool(allocator, "TEST_VERBOSE", true),
.fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false),
.filter = readEnv(allocator, "TEST_FILTER"),
};
}
fn deinit(self: Env, allocator: Allocator) void {
if (self.filter) |f| {
allocator.free(f);
}
}
fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 {
const v = std.process.getEnvVarOwned(allocator, key) catch |err| {
if (err == error.EnvironmentVariableNotFound) {
return null;
}
std.log.warn("failed to get env var {s} due to err {}", .{ key, err });
return null;
};
return v;
}
fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool {
const value = readEnv(allocator, key) orelse return deflt;
defer allocator.free(value);
return std.ascii.eqlIgnoreCase(value, "true");
}
};
fn isUnnamed(t: std.builtin.TestFn) bool {
const marker = ".test_";
const test_name = t.name;
const index = std.mem.indexOf(u8, test_name, marker) orelse return false;
_ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;
return true;
}
fn serverHTTP(listener: *std.net.Server) !void {
var read_buffer: [1024]u8 = undefined;
ACCEPT: while (true) {
var conn = try listener.accept();
defer conn.stream.close();
var server = std.http.Server.init(conn, &read_buffer);
while (server.state == .ready) {
var request = server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => continue :ACCEPT,
else => {
std.debug.print("Test HTTP Server error: {}\n", .{err});
return err;
},
};
const path = request.head.target;
if (std.mem.eql(u8, path, "/loader")) {
try writeResponse(&request, .{
.body = "Hello!",
});
}
}
}
}
const Response = struct {
body: []const u8 = "",
status: std.http.Status = .ok,
};
fn writeResponse(req: *std.http.Server.Request, res: Response) !void {
try req.respond(res.body, .{ .status = res.status });
}
test {
std.testing.refAllDecls(@import("url/query.zig"));
std.testing.refAllDecls(@import("browser/dump.zig"));
std.testing.refAllDecls(@import("browser/loader.zig"));
std.testing.refAllDecls(@import("browser/mime.zig"));
std.testing.refAllDecls(@import("cdp/msg.zig"));
std.testing.refAllDecls(@import("css/css.zig"));
std.testing.refAllDecls(@import("css/libdom_test.zig"));
std.testing.refAllDecls(@import("css/match_test.zig"));
std.testing.refAllDecls(@import("css/parser.zig"));
std.testing.refAllDecls(@import("generate.zig"));
std.testing.refAllDecls(@import("http/Client.zig"));
std.testing.refAllDecls(@import("storage/storage.zig"));
std.testing.refAllDecls(@import("iterator/iterator.zig"));
std.testing.refAllDecls(@import("server.zig"));
}

View File

@@ -19,58 +19,58 @@
const std = @import("std");
const Reader = @import("../str/parser.zig").Reader;
const asUint = @import("../str/parser.zig").asUint;
// Values is a map with string key of string values.
pub const Values = struct {
alloc: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
map: std.StringArrayHashMapUnmanaged(List),
const List = std.ArrayListUnmanaged([]const u8);
pub fn init(alloc: std.mem.Allocator) Values {
pub fn init(allocator: std.mem.Allocator) Values {
return .{
.alloc = alloc,
.map = .{},
.arena = std.heap.ArenaAllocator.init(allocator),
};
}
pub fn deinit(self: *Values) void {
var it = self.map.iterator();
while (it.next()) |entry| {
for (entry.value_ptr.items) |v| self.alloc.free(v);
entry.value_ptr.deinit(self.alloc);
self.alloc.free(entry.key_ptr.*);
}
self.map.deinit(self.alloc);
self.arena.deinit();
}
// add the key value couple to the values.
// the key and the value are duplicated.
pub fn append(self: *Values, k: []const u8, v: []const u8) !void {
const vv = try self.alloc.dupe(u8, v);
const allocator = self.arena.allocator();
const owned_value = try allocator.dupe(u8, v);
if (self.map.getPtr(k)) |list| {
return try list.append(self.alloc, vv);
var gop = try self.map.getOrPut(allocator, k);
if (gop.found_existing) {
return gop.value_ptr.append(allocator, owned_value);
}
const kk = try self.alloc.dupe(u8, k);
gop.key_ptr.* = try allocator.dupe(u8, k);
var list = List{};
try list.append(self.alloc, vv);
try self.map.put(self.alloc, kk, list);
try list.append(allocator, owned_value);
gop.value_ptr.* = list;
}
// append by taking the ownership of the key and the value
fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void {
if (self.map.getPtr(k)) |list| {
return try list.append(self.alloc, v);
const allocator = self.arena.allocator();
var gop = try self.map.getOrPut(allocator, k);
if (gop.found_existing) {
return gop.value_ptr.append(allocator, v);
}
var list = List{};
try list.append(self.alloc, v);
try self.map.put(self.alloc, k, list);
try list.append(allocator, v);
gop.value_ptr.* = list;
}
pub fn get(self: *Values, k: []const u8) [][]const u8 {
pub fn get(self: *const Values, k: []const u8) []const []const u8 {
if (self.map.get(k)) |list| {
return list.items;
}
@@ -78,7 +78,7 @@ pub const Values = struct {
return &[_][]const u8{};
}
pub fn first(self: *Values, k: []const u8) []const u8 {
pub fn first(self: *const Values, k: []const u8) []const u8 {
if (self.map.getPtr(k)) |list| {
if (list.items.len == 0) return "";
return list.items[0];
@@ -88,10 +88,7 @@ pub const Values = struct {
}
pub fn delete(self: *Values, k: []const u8) void {
if (self.map.getPtr(k)) |list| {
list.deinit(self.alloc);
_ = self.map.fetchSwapRemove(k);
}
_ = self.map.fetchSwapRemove(k);
}
pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void {
@@ -105,80 +102,48 @@ pub const Values = struct {
}
}
pub fn count(self: *Values) usize {
pub fn count(self: *const Values) usize {
return self.map.count();
}
// the caller owned the returned string.
pub fn encode(self: *Values, writer: anytype) !void {
var i: usize = 0;
pub fn encode(self: *const Values, writer: anytype) !void {
var it = self.map.iterator();
const first_entry = it.next() orelse return;
try encodeKeyValues(first_entry, writer);
while (it.next()) |entry| {
defer i += 1;
if (i > 0) try writer.writeByte('&');
if (entry.value_ptr.items.len == 0) {
try escape(writer, entry.key_ptr.*);
continue;
}
const start = i;
for (entry.value_ptr.items) |v| {
defer i += 1;
if (start < i) try writer.writeByte('&');
try escape(writer, entry.key_ptr.*);
if (v.len > 0) try writer.writeByte('=');
try escape(writer, v);
}
try writer.writeByte('&');
try encodeKeyValues(entry, writer);
}
}
};
fn unhex(c: u8) u8 {
if ('0' <= c and c <= '9') return c - '0';
if ('a' <= c and c <= 'f') return c - 'a' + 10;
if ('A' <= c and c <= 'F') return c - 'A' + 10;
return 0;
}
fn encodeKeyValues(entry: anytype, writer: anytype) !void {
const key = entry.key_ptr.*;
// unescape decodes a percent encoded string.
// The caller owned the returned string.
pub fn unescape(alloc: std.mem.Allocator, s: []const u8) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(alloc);
var i: usize = 0;
while (i < s.len) {
defer i += 1;
switch (s[i]) {
'%' => {
if (i + 2 > s.len) return error.EscapeError;
if (!std.ascii.isHex(s[i + 1])) return error.EscapeError;
if (!std.ascii.isHex(s[i + 2])) return error.EscapeError;
try buf.append(alloc, unhex(s[i + 1]) << 4 | unhex(s[i + 2]));
i += 2;
},
'+' => try buf.append(alloc, ' '), // TODO should we decode or keep as it?
else => try buf.append(alloc, s[i]),
}
try escape(key, writer);
const values = entry.value_ptr.items;
if (values.len == 0) {
return;
}
return try buf.toOwnedSlice(alloc);
if (values[0].len > 0) {
try writer.writeByte('=');
try escape(values[0], writer);
}
for (values[1..]) |value| {
try writer.writeByte('&');
try escape(key, writer);
if (value.len > 0) {
try writer.writeByte('=');
try escape(value, writer);
}
}
}
test "unescape" {
var v: []const u8 = undefined;
const alloc = std.testing.allocator;
v = try unescape(alloc, "%7E");
try std.testing.expect(std.mem.eql(u8, "~", v));
alloc.free(v);
}
pub fn escape(writer: anytype, raw: []const u8) !void {
fn escape(raw: []const u8, writer: anytype) !void {
var start: usize = 0;
for (raw, 0..) |char, index| {
if ('a' <= char and char <= 'z' or 'A' <= char and char <= 'Z' or '0' <= char and char <= '9') {
@@ -196,15 +161,17 @@ pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values {
var values = Values.init(alloc);
errdefer values.deinit();
const arena = values.arena.allocator();
const ln = s.len;
if (ln == 0) return values;
var r = Reader{ .s = s };
var r = Reader{ .data = s };
while (true) {
const param = r.until('&');
if (param.len == 0) break;
var rr = Reader{ .s = param };
var rr = Reader{ .data = param };
const k = rr.until('=');
if (k.len == 0) continue;
@@ -212,8 +179,8 @@ pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values {
const v = rr.tail();
// decode k and v
const kk = try unescape(alloc, k);
const vv = try unescape(alloc, v);
const kk = try unescape(arena, k);
const vv = try unescape(arena, v);
try values.appendOwned(kk, vv);
@@ -223,61 +190,244 @@ pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values {
return values;
}
test "parse empty query" {
var values = try parseQuery(std.testing.allocator, "");
defer values.deinit();
// The return'd string may or may not be allocated. Callers should use arenas
fn unescape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
const HEX_CHAR = comptime blk: {
var all = std.mem.zeroes([256]bool);
for ('a'..('f' + 1)) |b| all[b] = true;
for ('A'..('F' + 1)) |b| all[b] = true;
for ('0'..('9' + 1)) |b| all[b] = true;
break :blk all;
};
try std.testing.expect(values.count() == 0);
const HEX_DECODE = comptime blk: {
var all = std.mem.zeroes([256]u8);
for ('a'..('z' + 1)) |b| all[b] = b - 'a' + 10;
for ('A'..('Z' + 1)) |b| all[b] = b - 'A' + 10;
for ('0'..('9' + 1)) |b| all[b] = b - '0';
break :blk all;
};
var has_plus = false;
var unescaped_len = input.len;
{
// Figure out if we have any spaces and what the final unescaped length
// will be (which will let us know if we have anything to unescape in
// the first place)
var i: usize = 0;
while (i < input.len) {
const c = input[i];
if (c == '%') {
if (i + 2 >= input.len or !HEX_CHAR[input[i + 1]] or !HEX_CHAR[input[i + 2]]) {
return error.EscapeError;
}
i += 3;
unescaped_len -= 2;
} else if (c == '+') {
has_plus = true;
i += 1;
} else {
i += 1;
}
}
}
// no encoding, and no plus. nothing to unescape
if (unescaped_len == input.len and has_plus == false) {
return input;
}
var unescaped = try allocator.alloc(u8, unescaped_len);
errdefer allocator.free(unescaped);
var input_pos: usize = 0;
for (0..unescaped_len) |unescaped_pos| {
switch (input[input_pos]) {
'+' => {
unescaped[unescaped_pos] = ' ';
input_pos += 1;
},
'%' => {
const encoded = input[input_pos + 1 .. input_pos + 3];
const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*));
unescaped[unescaped_pos] = switch (encoded_as_uint) {
asUint("20") => ' ',
asUint("21") => '!',
asUint("22") => '"',
asUint("23") => '#',
asUint("24") => '$',
asUint("25") => '%',
asUint("26") => '&',
asUint("27") => '\'',
asUint("28") => '(',
asUint("29") => ')',
asUint("2A") => '*',
asUint("2B") => '+',
asUint("2C") => ',',
asUint("2F") => '/',
asUint("3A") => ':',
asUint("3B") => ';',
asUint("3D") => '=',
asUint("3F") => '?',
asUint("40") => '@',
asUint("5B") => '[',
asUint("5D") => ']',
else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]],
};
input_pos += 3;
},
else => |c| {
unescaped[unescaped_pos] = c;
input_pos += 1;
},
}
}
return unescaped;
}
test "parse empty query &" {
var values = try parseQuery(std.testing.allocator, "&");
defer values.deinit();
const testing = std.testing;
test "url.Query: unescape" {
const allocator = testing.allocator;
const cases = [_]struct { expected: []const u8, input: []const u8, free: bool }{
.{ .expected = "", .input = "", .free = false },
.{ .expected = "over", .input = "over", .free = false },
.{ .expected = "Hello World", .input = "Hello World", .free = false },
.{ .expected = "~", .input = "%7E", .free = true },
.{ .expected = "~", .input = "%7e", .free = true },
.{ .expected = "Hello~World", .input = "Hello%7eWorld", .free = true },
.{ .expected = "Hello World", .input = "Hello++World", .free = true },
};
try std.testing.expect(values.count() == 0);
for (cases) |case| {
const value = try unescape(allocator, case.input);
defer if (case.free) {
allocator.free(value);
};
try testing.expectEqualStrings(case.expected, value);
}
try testing.expectError(error.EscapeError, unescape(undefined, "%"));
try testing.expectError(error.EscapeError, unescape(undefined, "%a"));
try testing.expectError(error.EscapeError, unescape(undefined, "%1"));
try testing.expectError(error.EscapeError, unescape(undefined, "123%45%6"));
try testing.expectError(error.EscapeError, unescape(undefined, "%zzzzz"));
try testing.expectError(error.EscapeError, unescape(undefined, "%0\xff"));
}
test "parse query" {
var values = try parseQuery(std.testing.allocator, "a=b&b=c");
defer values.deinit();
test "url.Query: parseQuery" {
try testParseQuery(.{}, "");
try std.testing.expect(values.count() == 2);
try std.testing.expect(values.get("a").len == 1);
try std.testing.expect(std.mem.eql(u8, values.get("a")[0], "b"));
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
try testParseQuery(.{}, "&");
try std.testing.expect(values.get("b").len == 1);
try std.testing.expect(std.mem.eql(u8, values.get("b")[0], "c"));
try std.testing.expect(std.mem.eql(u8, values.first("b"), "c"));
try testParseQuery(.{ .a = [_][]const u8{"b"} }, "a=b");
try testParseQuery(.{ .hello = [_][]const u8{"world"} }, "hello=world");
try testParseQuery(.{ .hello = [_][]const u8{ "world", "all" } }, "hello=world&hello=all");
try testParseQuery(.{
.a = [_][]const u8{"b"},
.b = [_][]const u8{"c"},
}, "a=b&b=c");
try testParseQuery(.{ .a = [_][]const u8{""} }, "a");
try testParseQuery(.{ .a = [_][]const u8{ "", "", "" } }, "a&a&a");
try testParseQuery(.{ .abc = [_][]const u8{""} }, "abc");
try testParseQuery(.{
.abc = [_][]const u8{""},
.dde = [_][]const u8{ "", "" },
}, "abc&dde&dde");
try testParseQuery(.{
.@"power is >" = [_][]const u8{"9,000?"},
}, "power%20is%20%3E=9%2C000%3F");
}
test "parse query no value" {
var values = try parseQuery(std.testing.allocator, "a");
test "url.Query.Values: get/first/count" {
var values = Values.init(testing.allocator);
defer values.deinit();
try std.testing.expect(values.count() == 1);
try std.testing.expect(std.mem.eql(u8, values.first("a"), ""));
{
// empty
try testing.expectEqual(0, values.count());
try testing.expectEqual(0, values.get("").len);
try testing.expectEqualStrings("", values.first(""));
try testing.expectEqual(0, values.get("key").len);
try testing.expectEqualStrings("", values.first("key"));
}
{
// add 1 value => key
try values.appendOwned("key", "value");
try testing.expectEqual(1, values.count());
try testing.expectEqual(1, values.get("key").len);
try testing.expectEqualSlices(
[]const u8,
&.{"value"},
values.get("key"),
);
try testing.expectEqualStrings("value", values.first("key"));
}
{
// add another value for the same key
try values.appendOwned("key", "another");
try testing.expectEqual(1, values.count());
try testing.expectEqual(2, values.get("key").len);
try testing.expectEqualSlices(
[]const u8,
&.{ "value", "another" },
values.get("key"),
);
try testing.expectEqualStrings("value", values.first("key"));
}
{
// add a new key (and value)
try values.appendOwned("over", "9000!");
try testing.expectEqual(2, values.count());
try testing.expectEqual(2, values.get("key").len);
try testing.expectEqual(1, values.get("over").len);
try testing.expectEqualSlices(
[]const u8,
&.{"9000!"},
values.get("over"),
);
try testing.expectEqualStrings("9000!", values.first("over"));
}
}
test "parse query dup" {
var values = try parseQuery(std.testing.allocator, "a=b&a=c");
test "url.Query.Values: encode" {
var values = try parseQuery(
testing.allocator,
"hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c",
);
defer values.deinit();
try std.testing.expect(values.count() == 1);
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
try std.testing.expect(values.get("a").len == 2);
}
test "encode query" {
var values = try parseQuery(std.testing.allocator, "a=b&b=c");
defer values.deinit();
try values.append("a", "~");
var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(std.testing.allocator);
try values.encode(buf.writer(std.testing.allocator));
try std.testing.expect(std.mem.eql(u8, buf.items, "a=b&a=%7E&b=c"));
defer buf.deinit(testing.allocator);
try values.encode(buf.writer(testing.allocator));
try testing.expectEqualStrings(
"hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c",
buf.items,
);
}
fn testParseQuery(expected: anytype, query: []const u8) !void {
var values = try parseQuery(testing.allocator, query);
defer values.deinit();
var count: usize = 0;
inline for (@typeInfo(@TypeOf(expected)).Struct.fields) |f| {
const actual = values.get(f.name);
const expect = @field(expected, f.name);
try testing.expectEqual(expect.len, actual.len);
for (expect, actual) |e, a| {
try testing.expectEqualStrings(e, a);
}
count += 1;
}
try testing.expectEqual(count, values.count());
}

View File

@@ -21,14 +21,13 @@ const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const query = @import("query.zig");
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
URL,
URLSearchParams,
});
};
// https://url.spec.whatwg.org/#url
//

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const parser = @import("netsurf");
const Client = @import("async/Client.zig");
const Client = @import("asyncio").Client;
pub const UserContext = struct {
document: *parser.DocumentHTML,

View File

@@ -28,15 +28,17 @@ const Loop = jsruntime.Loop;
const Env = jsruntime.Env;
const Window = @import("../html/window.zig").Window;
const storage = @import("../storage/storage.zig");
const Client = @import("asyncio").Client;
const Types = @import("../main_wpt.zig").Types;
const UserContext = @import("../main_wpt.zig").UserContext;
const Client = @import("../async/Client.zig");
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.
pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !jsruntime.JSResult {
pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !Res {
const alloc = arena.allocator();
try parser.init();
defer parser.deinit();
@@ -53,10 +55,11 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
var loop = try Loop.init(alloc);
defer loop.deinit();
var cli = Client{ .allocator = alloc, .loop = &loop };
var cli = Client{ .allocator = alloc };
defer cli.deinit();
var js_env = try Env.init(alloc, &loop, UserContext{
var js_env: Env = undefined;
Env.init(&js_env, alloc, &loop, UserContext{
.document = html_doc,
.httpClient = &cli,
});
@@ -70,27 +73,28 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
try js_env.load(&js_types);
// start JS env
try js_env.start(alloc);
try js_env.start();
defer js_env.stop();
// load polyfills
try polyfill.load(alloc, js_env);
// display console logs
defer {
var res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
defer res.deinit(alloc);
if (res.result.len > 0) {
std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{res.result});
if (res.msg != null and res.msg.?.len > 0) {
std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{res.msg.?});
}
}
// setup global env vars.
var window = Window.create(null);
window.replaceDocument(html_doc);
var window = Window.create(null, null);
try window.replaceDocument(html_doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(&window);
// thanks to the arena, we don't need to deinit res.
var res: jsruntime.JSResult = undefined;
const init =
\\console = [];
\\console.log = function () {
@@ -100,10 +104,8 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
\\ console.push("debug", ...arguments);
\\};
;
res = try evalJS(js_env, alloc, init, "init");
if (!res.success) {
return res;
}
var res = try evalJS(js_env, alloc, init, "init");
if (!res.ok) return res;
res.deinit(alloc);
// loop hover the scripts.
@@ -122,20 +124,14 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
}
res = try evalJS(js_env, alloc, try loader.get(path), src);
if (!res.success) {
return res;
}
if (!res.ok) return res;
res.deinit(alloc);
}
// If the script as a source text, execute it.
const src = try parser.nodeTextContent(s) orelse continue;
res = try evalJS(js_env, alloc, src, "");
// return the first failure.
if (!res.success) {
return res;
}
if (!res.ok) return res;
res.deinit(alloc);
}
@@ -150,25 +146,52 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
);
// wait for all async executions
res = try js_env.waitTryCatch(alloc);
if (!res.success) {
return res;
}
res.deinit(alloc);
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(js_env);
defer try_catch.deinit();
js_env.wait() catch {
return .{
.ok = false,
.msg = try try_catch.err(alloc, js_env),
};
};
// Check the final test status.
res = try evalJS(js_env, alloc, "report.status;", "teststatus");
if (!res.success) {
return res;
}
if (!res.ok) return res;
res.deinit(alloc);
// return the detailed result.
return try evalJS(js_env, alloc, "report.log", "teststatus");
}
fn evalJS(env: jsruntime.Env, alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8) !jsruntime.JSResult {
return try env.execTryCatch(alloc, script, name);
pub const Res = struct {
ok: bool,
msg: ?[]const u8,
pub fn deinit(res: Res, alloc: std.mem.Allocator) void {
if (res.msg) |msg| {
alloc.free(msg);
}
}
};
fn evalJS(env: jsruntime.Env, alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8) !Res {
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env);
defer try_catch.deinit();
const v = env.exec(script, name) catch {
return .{
.ok = false,
.msg = try try_catch.err(alloc, env),
};
};
return .{
.ok = true,
.msg = try v.toString(alloc, env),
};
}
// browse the path to find the tests list.

View File

@@ -67,28 +67,22 @@ pub const Suite = struct {
pass: bool,
name: []const u8,
message: ?[]const u8,
stack: ?[]const u8,
cases: ?[]Case,
// caller owns the wpt.Suite.
// owner must call deinit().
pub fn init(alloc: std.mem.Allocator, name: []const u8, pass: bool, res: []const u8, stack: ?[]const u8) !Suite {
pub fn init(alloc: std.mem.Allocator, name: []const u8, pass: bool, res: []const u8) !Suite {
var suite = Suite{
.alloc = alloc,
.pass = false,
.name = try alloc.dupe(u8, name),
.message = null,
.stack = null,
.cases = null,
};
// handle JS error.
if (!pass) {
suite.message = try alloc.dupe(u8, res);
if (stack) |st| {
suite.stack = try alloc.dupe(u8, st);
}
return suite;
}
@@ -155,10 +149,6 @@ pub const Suite = struct {
pub fn deinit(self: Suite) void {
self.alloc.free(self.name);
if (self.stack) |stack| {
self.alloc.free(stack);
}
if (self.message) |res| {
self.alloc.free(res);
}
@@ -175,9 +165,6 @@ pub const Suite = struct {
if (self.message) |v| {
return v;
}
if (self.stack) |v| {
return v;
}
return "";
}
};
@@ -199,7 +186,7 @@ test "success test case" {
,
};
const suite = Suite.init(alloc, "foo", res.pass, res.result, null) catch unreachable; // TODO
const suite = Suite.init(alloc, "foo", res.pass, res.result) catch unreachable; // TODO
defer suite.deinit();
try testing.expect(suite.pass == true);
@@ -226,7 +213,7 @@ test "failed test case" {
,
};
const suite = Suite.init(alloc, "foo", res.pass, res.result, null) catch unreachable; // TODO
const suite = Suite.init(alloc, "foo", res.pass, res.result) catch unreachable; // TODO
defer suite.deinit();
try testing.expect(suite.pass == false);
@@ -251,7 +238,7 @@ test "invalid result" {
,
};
const suite = Suite.init(alloc, "foo", res.pass, res.result, null) catch unreachable; // TODO
const suite = Suite.init(alloc, "foo", res.pass, res.result) catch unreachable; // TODO
defer suite.deinit();
try testing.expect(suite.pass == false);
@@ -266,7 +253,7 @@ test "invalid result" {
,
};
const suite2 = Suite.init(alloc, "foo", res2.pass, res2.result, null) catch unreachable; // TODO
const suite2 = Suite.init(alloc, "foo", res2.pass, res2.result) catch unreachable; // TODO
defer suite2.deinit();
try testing.expect(suite2.pass == false);

View File

@@ -21,7 +21,6 @@ const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const DOMError = @import("netsurf").DOMError;
const DOMException = @import("../dom/exceptions.zig").DOMException;
@@ -29,11 +28,10 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
const ProgressEvent = @import("progress_event.zig").ProgressEvent;
const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget;
const Mime = @import("../browser/mime.zig");
const Mime = @import("../browser/mime.zig").Mime;
const Loop = jsruntime.Loop;
const YieldImpl = Loop.Yield(XMLHttpRequest);
const Client = @import("../async/Client.zig");
const Client = @import("asyncio").Client;
const parser = @import("netsurf");
@@ -43,11 +41,11 @@ const log = std.log.scoped(.xhr);
// XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
pub const Interfaces = generate.Tuple(.{
pub const Interfaces = .{
XMLHttpRequestEventTarget,
XMLHttpRequestUpload,
XMLHttpRequest,
});
};
pub const XMLHttpRequestUpload = struct {
pub const prototype = *XMLHttpRequestEventTarget;
@@ -98,10 +96,11 @@ pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator,
cli: *Client,
impl: YieldImpl,
io: Client.IO,
priv_state: PrivState = .new,
req: ?Client.Request = null,
ctx: ?Client.Ctx = null,
method: std.http.Method,
state: u16,
@@ -135,8 +134,14 @@ pub const XMLHttpRequest = struct {
response_header_buffer: [1024 * 16]u8 = undefined,
response_status: u10 = 0,
response_override_mime_type: ?[]const u8 = null,
response_mime: Mime = undefined,
// TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
// not sure. see
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
// response_override_mime_type: ?[]const u8 = null,
response_mime: ?Mime = null,
response_obj: ?ResponseObj = null,
send_flag: bool = false,
@@ -288,7 +293,7 @@ pub const XMLHttpRequest = struct {
.alloc = alloc,
.headers = Headers.init(alloc),
.response_headers = Headers.init(alloc),
.impl = YieldImpl.init(loop),
.io = Client.IO.init(loop),
.method = undefined,
.url = null,
.uri = undefined,
@@ -308,8 +313,11 @@ pub const XMLHttpRequest = struct {
if (self.response_obj) |v| v.deinit();
self.response_obj = null;
self.response_mime = Mime.Empty;
self.response_type = .Empty;
if (self.response_mime) |*mime| {
mime.deinit();
self.response_mime = null;
}
// TODO should we clearRetainingCapacity instead?
self.headers.clearAndFree();
@@ -320,16 +328,20 @@ pub const XMLHttpRequest = struct {
self.priv_state = .new;
if (self.req) |*r| {
r.deinit();
self.req = null;
}
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
self.reset();
self.headers.deinit();
self.response_headers.deinit();
if (self.response_mime) |*mime| {
mime.deinit();
}
self.proto.deinit(alloc);
}
@@ -382,7 +394,11 @@ pub const XMLHttpRequest = struct {
self.reset(alloc);
self.url = try alloc.dupe(u8, url);
self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax;
self.uri = std.Uri.parse(self.url.?) catch |err| {
log.debug("parse url ({s}): {any}", .{ self.url.?, err });
return DOMError.Syntax;
};
log.debug("open url ({s})", .{self.url.?});
self.sync = if (asyn) |b| !b else false;
self.state = OPENED;
@@ -494,138 +510,160 @@ pub const XMLHttpRequest = struct {
log.debug("{any} {any}", .{ self.method, self.uri });
self.send_flag = true;
self.impl.yield(self);
}
// onYield is a callback called between each request's steps.
// Between each step, the code is blocking.
// Yielding allows pseudo-async and gives a chance to other async process
// to be called.
pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void {
if (err) |e| return self.onErr(e);
self.priv_state = .open;
switch (self.priv_state) {
.new => {
self.priv_state = .open;
self.req = self.cli.open(self.method, self.uri, .{
.server_header_buffer = &self.response_header_buffer,
.extra_headers = self.headers.all(),
}) catch |e| return self.onErr(e);
},
.open => {
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
self.req.?.send() catch |e| return self.onErr(e);
},
.send => {
if (self.payload) |payload| {
self.priv_state = .write;
self.req.?.writeAll(payload) catch |e| return self.onErr(e);
} else {
self.priv_state = .finish;
self.req.?.finish() catch |e| return self.onErr(e);
}
},
.write => {
self.priv_state = .finish;
self.req.?.finish() catch |e| return self.onErr(e);
},
.finish => {
self.priv_state = .wait;
self.req.?.wait() catch |e| return self.onErr(e);
},
.wait => {
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
self.priv_state = .done;
var it = self.req.?.response.iterateHeaders();
self.response_headers.load(&it) catch |e| return self.onErr(e);
// extract a mime type from headers.
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e);
// TODO handle override mime type
self.state = HEADERS_RECEIVED;
self.dispatchEvt("readystatechange");
self.response_status = @intFromEnum(self.req.?.response.status);
var buf: std.ArrayListUnmanaged(u8) = .{};
// TODO set correct length
const total = 0;
var loaded: u64 = 0;
// dispatch a progress event loadstart.
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
var prev_dispatch: ?std.time.Instant = null;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
loaded = loaded + ln;
// Dispatch only if 50ms have passed.
const now = std.time.Instant.now() catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = LOADING;
self.dispatchEvt("readystatechange");
// dispatch a progress event progress.
self.dispatchProgressEvent("progress", .{
.loaded = loaded,
.total = total,
});
}
self.response_bytes = buf.items;
self.send_flag = false;
self.state = DONE;
self.dispatchEvt("readystatechange");
// dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
// dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
},
.done => {
if (self.req) |*r| {
r.deinit();
self.req = null;
}
// finalize fetch process.
return;
},
self.req = try self.cli.create(self.method, self.uri, .{
.server_header_buffer = &self.response_header_buffer,
.extra_headers = self.headers.all(),
});
errdefer {
self.req.?.deinit();
self.req = null;
}
self.impl.yield(self);
self.ctx = try Client.Ctx.init(&self.io, &self.req.?);
errdefer {
self.ctx.?.deinit();
self.ctx = null;
}
self.ctx.?.userData = self;
try self.cli.async_open(
self.method,
self.uri,
.{ .server_header_buffer = &self.response_header_buffer },
&self.ctx.?,
onRequestConnect,
);
}
fn onRequestWait(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
self.priv_state = .done;
var it = self.req.?.response.iterateHeaders();
self.response_headers.load(&it) catch |e| return self.onErr(e);
// extract a mime type from headers.
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e);
// TODO handle override mime type
self.state = HEADERS_RECEIVED;
self.dispatchEvt("readystatechange");
self.response_status = @intFromEnum(self.req.?.response.status);
var buf: std.ArrayListUnmanaged(u8) = .{};
// TODO set correct length
const total = 0;
var loaded: u64 = 0;
// dispatch a progress event loadstart.
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
// TODO read async
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
var prev_dispatch: ?std.time.Instant = null;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
loaded = loaded + ln;
// Dispatch only if 50ms have passed.
const now = std.time.Instant.now() catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = LOADING;
self.dispatchEvt("readystatechange");
// dispatch a progress event progress.
self.dispatchProgressEvent("progress", .{
.loaded = loaded,
.total = total,
});
}
self.response_bytes = buf.items;
self.send_flag = false;
self.state = DONE;
self.dispatchEvt("readystatechange");
// dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
// dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
fn onRequestFinish(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .wait;
return ctx.req.async_wait(ctx, onRequestWait) catch |e| return self.onErr(e);
}
fn onRequestSend(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
if (self.payload) |payload| {
self.priv_state = .write;
return ctx.req.async_writeAll(payload, ctx, onRequestWrite) catch |e| return self.onErr(e);
}
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestWrite(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestConnect(ctx: *Client.Ctx, res: anyerror!void) anyerror!void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
return ctx.req.async_send(ctx, onRequestSend) catch |err| return self.onErr(err);
}
fn selfCtx(ctx: *Client.Ctx) *XMLHttpRequest {
return @ptrCast(@alignCast(ctx.userData));
}
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
self.priv_state = .done;
if (self.req) |*r| {
r.deinit();
self.req = null;
}
self.err = err;
self.state = DONE;
@@ -635,6 +673,12 @@ pub const XMLHttpRequest = struct {
self.dispatchProgressEvent("loadend", .{});
log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
pub fn _abort(self: *XMLHttpRequest) void {
@@ -717,8 +761,10 @@ 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 = "" };
return .{ .Text = try self.get_responseText() };
if (self.state == LOADING or self.state == DONE) {
return .{ .Text = try self.get_responseText() };
}
return .{ .Text = "" };
}
// fastpath if response is previously parsed.
@@ -735,6 +781,7 @@ 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;
}
@@ -743,6 +790,7 @@ 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;
}
@@ -778,13 +826,14 @@ pub const XMLHttpRequest = struct {
// TODO parse XML.
// https://xhr.spec.whatwg.org/#response-object
fn setResponseObjDocument(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
const isHTML = self.response_mime.eql(Mime.HTML);
const response_mime = &self.response_mime.?;
const isHTML = response_mime.isHTML();
// TODO If finalMIME is not an HTML MIME type or an XML MIME type, then
// return.
if (!isHTML) return;
const ccharset = alloc.dupeZ(u8, self.response_mime.charset orelse "utf-8") catch {
const ccharset = alloc.dupeZ(u8, response_mime.charset orelse "utf-8") catch {
self.response_obj = .{ .Failure = true };
return;
};
@@ -882,7 +931,7 @@ pub fn testExecFn(
// .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" },
//.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
.{ .src = "req.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
// ensure open resets values
@@ -905,14 +954,14 @@ 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", .ex = "" },
.{ .src = "req.response.length == req.responseText.length", .ex = "true" },
.{ .src = "req.responseXML instanceof Document", .ex = "true" },
};
try checkCases(js_env, &send);
var document = [_]Case{
.{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req2.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req2.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req2.responseType = 'document'", .ex = "document" },
.{ .src = "req2.send()", .ex = "undefined" },
@@ -928,7 +977,7 @@ pub fn testExecFn(
var json = [_]Case{
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req3.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req3.responseType = 'json'", .ex = "json" },
.{ .src = "req3.send()", .ex = "undefined" },
@@ -943,7 +992,7 @@ pub fn testExecFn(
var post = [_]Case{
.{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req4.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req4.open('POST', 'https://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req4.send('foo')", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
@@ -956,7 +1005,7 @@ pub fn testExecFn(
var cbk = [_]Case{
.{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req5.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req5.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
.{ .src = "var status = 0; req5.onload = function () { status = this.status };", .ex = "function () { status = this.status }" },
.{ .src = "req5.send()", .ex = "undefined" },

View File

@@ -0,0 +1,71 @@
// Copyright (C) 2023-2025 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 jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const DOMError = @import("netsurf").DOMError;
const parser = @import("netsurf");
const dump = @import("../browser/dump.zig");
pub const Interfaces = .{
XMLSerializer,
};
// https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor
pub const XMLSerializer = struct {
pub const mem_guarantied = true;
pub fn constructor() !XMLSerializer {
return .{};
}
pub fn deinit(_: *XMLSerializer, _: std.mem.Allocator) void {}
pub fn _serializeToString(_: XMLSerializer, alloc: std.mem.Allocator, root: *parser.Node) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
if (try parser.nodeType(root) == .document) {
try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer());
} else {
try dump.writeNode(root, buf.writer());
}
// TODO express the caller owned the slice.
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
return try buf.toOwnedSlice();
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var serializer = [_]Case{
.{ .src = "const s = new XMLSerializer()", .ex = "undefined" },
.{ .src = "s.serializeToString(document.getElementById('para'))", .ex = "<p id=\"para\"> And</p>" },
};
try checkCases(js_env, &serializer);
}

1
vendor/tls.zig vendored Submodule

Submodule vendor/tls.zig added at 0ea9e6d769

1
vendor/zig-async-io vendored Submodule

Submodule vendor/zig-async-io added at 570f436c72