Skip to content

Commit c743835

Browse files
committed
Merge upstream/main into gos-282-no-content
Keep both tests added after the cache-reload test: no-content and hard reload.
2 parents 340962b + d7d2def commit c743835

9 files changed

Lines changed: 436 additions & 187 deletions

File tree

‎README.md‎

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,31 @@ the [contribution guide](CONTRIBUTING.md).
1212
Gosub is a modular, embeddable browser engine. The primary entry point is `GosubEngine` in the
1313
[`gosub_engine`](crates/gosub_engine) crate. You provide a render backend and a compositor; the
1414
engine owns a multi-zone/tab model, an async networking stack, cookie and storage isolation per
15-
zone, and an event bus. Your user-agent (UA) drives everything via `TabCommand` and reacts to
15+
zone, sandboxed child processes for parsing and rendering, and an event bus. Your user-agent (UA) drives everything via `TabCommand` and reacts to
1616
`EngineEvent`.
1717

1818
**Core components:**
1919

2020
| Crate | Role |
2121
|---|---|
22-
| `gosub_engine` | `GosubEngine` — the unified entry point |
22+
| `gosub_engine` | `GosubEngine` - the unified entry point |
2323
| `gosub_interface` | Shared traits wiring the components together (the config system) |
2424
| `gosub_html5` | HTML5 tokenizer / parser |
2525
| `gosub_css3` | CSS3 tokenizer / parser |
26-
| [`gosub-sonar`](https://github.com/gosub-io/gosub-sonar) | Networking stack (async, streaming, priority-scheduled) — external crate |
26+
| [`gosub-sonar`](https://github.com/gosub-io/gosub-sonar) | Networking stack (async, streaming, priority-scheduled) - external crate |
2727
| `gosub_lattice` | CSS table layout |
28-
| `gosub_render_pipeline` | Render pipeline — layout (Taffy), stages, tiling, compositor |
28+
| `gosub_render_pipeline` | Render pipeline - layout (Taffy), stages, tiling, compositor |
2929
| `gosub_renderer_cairo` | Cairo render backend (CPU) |
3030
| `gosub_renderer_skia` | Skia render backend (CPU / GPU) |
3131
| `gosub_renderer_vello` | Vello / wgpu render backend (GPU) |
32-
| `gosub_fontmanager` | Font system — text shaping and measurement |
33-
| `gosub_jsapi` | Browser Web API implementations (console, fetch, DOM, …) |
32+
| `gosub_renderer_dynamic` | Picks one of the render backends at runtime |
33+
| `gosub_svg` | SVG rendering |
34+
| `gosub_fontmanager` | Font system - text shaping and measurement |
35+
| `gosub_jsapi` | Browser Web API implementations (console, fetch, DOM, ...) |
3436
| `gosub_v8` | V8 JavaScript engine bindings |
3537
| `gosub_config` | Configuration store |
38+
| `gosub_sandbox` | OS-level sandboxing: syscall filters, namespaces, child-process spawning |
39+
| `gosub_ipc` | Transport between the engine and its child processes |
3640

3741
For the full crate listing see [`docs/crates.md`](docs/crates.md).
3842

@@ -41,76 +45,92 @@ For the full crate listing see [`docs/crates.md`](docs/crates.md).
4145

4246
The engine is under active development. What works today:
4347

44-
- **Multi-zone / multi-tab model** — zones isolate cookies and storage; tabs are controlled via `TabCommand`
45-
- **Async networking** — streaming HTTP fetcher with priority queues, inflight coalescing, redirect handling, and per-zone cookie isolation
46-
- **Event-driven UA interface** — `TabCommand` flows in; `EngineEvent` (navigation, downloads, redraw, crashes) flows out on a control bus, with per-resource detail on a separate opt-in stream
47-
- **HTML5 and CSS3 parsing** — spec-compliant parsers for both, including shadow DOM (parsing, flat-tree traversal, CSS scoping)
48-
- **Resource pipeline** — parser-driven sub-resource discovery; stylesheets, images, web fonts and data URLs all fetched through one fetcher, with hierarchical cancellation
49-
- **Layout and paint** — block, inline, flex and grid via Taffy, floats, absolute and fixed positioning, and CSS tables via `gosub_lattice`
50-
- **Form controls** — text editing, and colour / date / time pickers opened by the embedder through `EngineEvent::PickerRequested`
51-
- **Pluggable render backends** — Null (headless), Cairo (GTK4), Skia (CPU / GPU), Vello (wgpu)
52-
- **Conformance measurement** — a gated web-platform-tests run in CI, plus a reftest runner; see [docs/wpt.md](docs/wpt.md) for where the numbers stand
53-
54-
**[`docs/status.md`](docs/status.md) is the detailed version of this list** — per component,
48+
- **Multi-zone / multi-tab model** - zones isolate cookies and storage; tabs are controlled via `TabCommand`
49+
- **Async networking** - streaming HTTP fetcher with priority queues, inflight coalescing, redirect handling, and per-zone cookie isolation; `gemini`, `gopher` and `finger` next to `http(s)`, `data` and `file`
50+
- **Process isolation** - on Linux, networking, cookies, image decoding, file-backed `localStorage` and rendering (with Parley or cosmic-text fonts) run in sandboxed child processes, on by default once the embedder calls `child_process::dispatch_with::<C>()` first in `main`; see [docs/process-isolation.md](docs/process-isolation.md)
51+
- **Web security policies** - CORS, mixed content, referrer policy, ORB, an SSRF policy over address spaces, and per-document security state
52+
- **Encrypted storage** - every SQLite store (cookies, localStorage, places, config) opens with an optional SQLCipher key
53+
- **Event-driven UA interface** - `TabCommand` flows in; `EngineEvent` (navigation, downloads, redraw, crashes) flows out on a control bus, with per-resource detail on a separate opt-in stream
54+
- **HTML5 and CSS3 parsing** - spec-compliant parsers for both, including shadow DOM (parsing, flat-tree traversal, CSS scoping)
55+
- **Resource pipeline** - parser-driven sub-resource discovery; stylesheets, images, web fonts and data URLs all fetched through one fetcher, with hierarchical cancellation
56+
- **Layout and paint** - block, inline, flex and grid via Taffy, floats, absolute and fixed positioning, and CSS tables via `gosub_lattice`
57+
- **Mobile** - `<meta name="viewport">` layout width and pinch zoom through a visual viewport; the engine runs on Android, in-process
58+
- **Form controls** - text editing, and colour / date / time pickers opened by the embedder through `EngineEvent::PickerRequested`
59+
- **Pluggable render backends** - Null (headless), Cairo (GTK4), Skia (CPU / GPU), Vello (wgpu)
60+
- **Conformance measurement** - a gated web-platform-tests run in CI, plus a reftest runner; see [docs/wpt.md](docs/wpt.md) for where the numbers stand
61+
62+
**[`docs/status.md`](docs/status.md) is the detailed version of this list** - per component,
5563
what works and what does not, with the numbers.
5664

5765
Not there yet: scripting is not wired into the engine. The V8 and web-API crates exist and
58-
build, but no page runs JavaScript — see [docs/javascript.md](docs/javascript.md). WPT drives
66+
build, but no page runs JavaScript - see [docs/javascript.md](docs/javascript.md). WPT drives
5967
the DOM through a separate test-only QuickJS binding (`gosub_domjs`).
6068

6169

70+
## Gosub Beacon
71+
72+
[Gosub Beacon](https://github.com/gosub-io/gosub-beacon) is the browser built on this engine,
73+
for Linux, macOS, Windows and Android. It builds against this repository's `beacon` branch, not
74+
`main`. Builds are on its [releases page](https://github.com/gosub-io/gosub-beacon/releases).
75+
76+
6277
## Documentation
6378

6479
**[`docs/README.md`](docs/README.md) indexes every page under `/docs`.** The pages below are the
6580
ones worth reading first.
6681

6782
**Getting started**
6883

69-
- [Tutorial](docs/tutorial.md) — start the engine, open a tab, navigate, handle events
70-
- [Configuration](docs/configuration.md) — choosing a render backend and font system
71-
- [Running the examples](docs/examples.md) — headless, GUI (winit / GTK4 / egui), and component tools
72-
- [Headless usage](docs/headless.md) — rendering real pages without a window
73-
- [WebAssembly](docs/webassembly.md) — compile and run the engine in the browser
74-
- [Development](docs/development.md) — tests and benchmarks
84+
- [Tutorial](docs/tutorial.md) - start the engine, open a tab, navigate, handle events
85+
- [Configuration](docs/configuration.md) - choosing a render backend and font system
86+
- [Running the examples](docs/examples.md) - headless, GUI (winit / GTK4 / egui), and component tools
87+
- [Headless usage](docs/headless.md) - rendering real pages without a window
88+
- [WebAssembly](docs/webassembly.md) - compile and run the engine in the browser
89+
- [Development](docs/development.md) - tests and benchmarks
7590

7691
**Reference**
7792

78-
- [What the engine can do today](docs/status.md) — per-component capability and gaps
79-
- [Crates](docs/crates.md) — the workspace crate layout, and where to find anything
80-
- [Component tools](docs/binaries.md) — the standalone `cargo run --bin …` tools
93+
- [Embedder API](docs/embedder-api.md) - every `TabCommand` and `EngineEvent`, and what to do
94+
with each
95+
- [What the engine can do today](docs/status.md) - per-component capability and gaps
96+
- [Crates](docs/crates.md) - the workspace crate layout, and where to find anything
97+
- [Component tools](docs/binaries.md) - the standalone `cargo run --bin ...` tools
8198

8299
**Architecture**
83100

84-
- [The two worlds](docs/two-worlds.md) — the two parallel document/style models and the seam
101+
- [The two worlds](docs/two-worlds.md) - the two parallel document/style models and the seam
85102
that joins them. Read this before diving into either.
86-
- [Interface trait families](docs/interface.md) — `ModuleConfiguration` and the `Has*` traits
103+
- [Interface trait families](docs/interface.md) - `ModuleConfiguration` and the `Has*` traits
87104
that wire the workspace together
88-
- [Module configuration](docs/moduleconfig.md) — the same picture from an embedder's side:
105+
- [Module configuration](docs/moduleconfig.md) - the same picture from an embedder's side:
89106
what `DefaultRenderConfig` wires for you and how to go fully custom
90-
- [Zones and tabs](docs/zones-and-tabs.md) — zones as isolated profiles, tabs as independent
107+
- [Zones and tabs](docs/zones-and-tabs.md) - zones as isolated profiles, tabs as independent
91108
worker tasks, and the command/event flow between them
92-
- [Resource pipelines](docs/resource-pipeline.md) — how fetched bytes become typed assets
93-
- [CSS internals](docs/css.md) — `gosub_css3` from text to computed value
94-
- [HTML5 parsing](docs/html5.md) — tokenizer, tree builder, arena DOM
109+
- [Process isolation](docs/process-isolation.md) - the broker, its sandboxed children, and
110+
the embedder contract that turns them on; [security assessment](docs/security-assessment.md)
111+
reads the same design as an attacker would
112+
- [Resource pipelines](docs/resource-pipeline.md) - how fetched bytes become typed assets
113+
- [CSS internals](docs/css.md) - `gosub_css3` from text to computed value
114+
- [HTML5 parsing](docs/html5.md) - tokenizer, tree builder, arena DOM
95115
- [Render pipeline](docs/render-pipeline/README.md)
96-
- [Lattice table layout](docs/lattice.md) · [Fonts](docs/fonts.md) · [JavaScript stack](docs/javascript.md)
97-
- [Networking — architecture](docs/network/net-architecture.md) and [design notes](docs/network/net-design.md)
116+
- [Lattice table layout](docs/lattice.md) - [Fonts](docs/fonts.md) - [JavaScript stack](docs/javascript.md)
117+
- [Networking - architecture](docs/network/net-architecture.md) and [design notes](docs/network/net-design.md)
98118
- [Cookies](docs/cookies.md)
99119
- [Storage (local / session)](docs/datastores.md)
100-
- [Pump](docs/network/pump.md) — moving HTTP stream data to targets
120+
- [Pump](docs/network/pump.md) - moving HTTP stream data to targets
101121

102122
**Conformance**
103123

104-
- [**WPT quickstart**](docs/wpt-quickstart.md) — clone to a fixed test in about ten minutes.
124+
- [**WPT quickstart**](docs/wpt-quickstart.md) - clone to a fixed test in about ten minutes.
105125
The fastest way to make a first contribution.
106-
- [Web-platform-tests](docs/wpt.md) — the harness in full: what is bound, the expectations
126+
- [Web-platform-tests](docs/wpt.md) - the harness in full: what is bound, the expectations
107127
format, the reftest runner, and what CI does
108128

109129

110130
## Contributing
111131

112132
We welcome contributions. Because the engine is still taking shape, a lot of work is exploratory
113-
— building proofs-of-concept, reading specs, and making architectural decisions — rather than
133+
- building proofs-of-concept, reading specs, and making architectural decisions - rather than
114134
pure coding.
115135

116136
Join us on [Zulip](https://chat.developer.gosub.io) or [Discord](https://chat.gosub.io) before

‎crates/gosub_engine/src/engine/engine.rs‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,37 @@ mod tests {
13731373
engine.shutdown().await.expect("shutdown");
13741374
}
13751375

1376+
/// A detached tab gives its `max_tabs` slot back at once, before its worker has
1377+
/// stopped, so a zone that keeps opening and closing tabs never runs out.
1378+
#[tokio::test]
1379+
async fn detached_tab_frees_its_slot() {
1380+
let mut engine = engine_with_max_zones(1);
1381+
let _join = tokio::spawn(engine.start().expect("start"));
1382+
let mut zone = engine.zone_builder().services(services()).create().expect("zone");
1383+
let max = ZoneConfig::default().max_tabs;
1384+
1385+
let mut tabs = Vec::new();
1386+
for _ in 0..max {
1387+
tabs.push(zone.tab_builder().create().await.expect("tab"));
1388+
}
1389+
assert!(matches!(
1390+
zone.tab_builder().create().await,
1391+
Err(EngineError::TabLimitExceeded)
1392+
));
1393+
1394+
for _ in 0..max * 2 {
1395+
let closing = zone.detach_tab(tabs.remove(0).tab_id).expect("known tab");
1396+
assert!(zone.detach_tab(crate::tab::TabId::new()).is_none(), "unknown id");
1397+
tabs.push(zone.tab_builder().create().await.expect("the slot is free"));
1398+
tokio::time::timeout(Duration::from_secs(10), closing)
1399+
.await
1400+
.expect("the worker stops");
1401+
}
1402+
1403+
engine.close_zone(zone).await;
1404+
engine.shutdown().await.expect("shutdown");
1405+
}
1406+
13761407
/// A page served as an attachment is offered as a download; `RenderDownload` overrides
13771408
/// that and loads the already-spooled body as the page. No second request is made.
13781409
#[tokio::test(flavor = "current_thread")]
@@ -3971,6 +4002,93 @@ mod tests {
39714002
assert!(tab.can_go_back(), "the cursor went back to /page's entry");
39724003
}
39734004

4005+
/// A reload ignoring the cache asks for a fresh copy all the way to the server
4006+
/// (`Cache-Control` and `Pragma: no-cache`, no validators); a plain reload does not.
4007+
#[tokio::test]
4008+
async fn reload_ignoring_cache_sends_no_cache() {
4009+
use crate::events::{NavigationEvent, TabCommand};
4010+
use cow_utils::CowUtils;
4011+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
4012+
4013+
let (heads_tx, mut heads) = mpsc::unbounded_channel::<String>();
4014+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
4015+
let port = listener.local_addr().unwrap().port();
4016+
tokio::spawn(async move {
4017+
while let Ok((mut stream, _)) = listener.accept().await {
4018+
let heads_tx = heads_tx.clone();
4019+
tokio::spawn(async move {
4020+
// The whole head: the test reads its headers, not only the request line.
4021+
let mut buf = Vec::new();
4022+
let mut chunk = [0u8; 4096];
4023+
while !buf.windows(4).any(|w| w == b"\r\n\r\n") {
4024+
match stream.read(&mut chunk).await {
4025+
Ok(0) | Err(_) => break,
4026+
Ok(n) => buf.extend_from_slice(&chunk[..n]),
4027+
}
4028+
}
4029+
let head = String::from_utf8_lossy(&buf).cow_to_ascii_lowercase().into_owned();
4030+
if head.starts_with("get /page ") {
4031+
let _ = heads_tx.send(head);
4032+
}
4033+
let page = "<html><body>page</body></html>";
4034+
let response = format!(
4035+
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nCache-Control: max-age=3600\r\nETag: \"v1\"\r\n\
4036+
Content-Length: {}\r\nConnection: close\r\n\r\n{page}",
4037+
page.len()
4038+
);
4039+
let _ = stream.write_all(response.as_bytes()).await;
4040+
});
4041+
}
4042+
});
4043+
4044+
let mut engine = engine_with_max_zones(1);
4045+
let mut events = engine.subscribe_events();
4046+
let _join = tokio::spawn(engine.start().expect("start"));
4047+
let mut zone = engine.zone_builder().services(services()).create().expect("zone");
4048+
let tab = zone.tab_builder().create().await.expect("tab");
4049+
4050+
async fn finished(events: &mut broadcast::Receiver<EngineEvent>) {
4051+
tokio::time::timeout(Duration::from_secs(10), async {
4052+
loop {
4053+
match events.recv().await {
4054+
Ok(EngineEvent::Navigation {
4055+
event: NavigationEvent::Finished { .. },
4056+
..
4057+
}) => return,
4058+
Ok(_) => continue,
4059+
Err(e) => panic!("event stream closed: {e}"),
4060+
}
4061+
}
4062+
})
4063+
.await
4064+
.expect("timed out waiting for the load to finish");
4065+
}
4066+
let no_cache = |head: &str| head.contains("cache-control: no-cache") && head.contains("pragma: no-cache");
4067+
4068+
tab.navigate(format!("http://127.0.0.1:{port}/page"))
4069+
.await
4070+
.expect("navigate");
4071+
// The server records a request before it answers, so by `Finished` it is queued.
4072+
finished(&mut events).await;
4073+
assert!(!no_cache(&heads.try_recv().unwrap()), "a first load is a plain one");
4074+
4075+
tab.send(TabCommand::Reload { ignore_cache: true })
4076+
.await
4077+
.expect("reload");
4078+
finished(&mut events).await;
4079+
let head = heads.try_recv().expect("the hard reload was answered from the cache");
4080+
assert!(no_cache(&head), "hard reload without no-cache:\n{head}");
4081+
assert!(!head.contains("if-none-match"), "hard reload sent a validator:\n{head}");
4082+
4083+
tab.send(TabCommand::Reload { ignore_cache: false })
4084+
.await
4085+
.expect("reload");
4086+
finished(&mut events).await;
4087+
if let Ok(head) = heads.try_recv() {
4088+
assert!(!no_cache(&head), "a plain reload asked for no-cache:\n{head}");
4089+
}
4090+
}
4091+
39744092
/// Session history end to end: two navigations push two entries, GoBack moves the cursor
39754093
/// (announced immediately via HistoryChanged) and refetches the first page, GoForward
39764094
/// returns to the second. Verifies the tree from the embedder's point of view only.

0 commit comments

Comments
 (0)