Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 103 additions & 106 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,107 +1,104 @@
# Secure Browser Engine — Build-from-Scratch Roadmap

## Vision
A memory-safe, security-first web browser built from the ground up — own rendering
engine, own layout, own paint pipeline — targeting fast and smooth browsing without
depending on Chromium/WebKit/Gecko internals.

## Language & core tech decisions
- **Language: Rust** — eliminates entire classes of memory-safety bugs (use-after-free,
buffer overflow) that make up the majority of historical Chrome/Firefox CVEs.
- **GPU/compositing**: `wgpu` (cross-platform GPU crate, works on Vulkan/Metal/DX12)
- **TLS**: `rustls` (pure Rust TLS, avoids OpenSSL memory-safety history)
- **Text shaping**: `harfbuzz` bindings or `rustybuzz` (Rust port) — needed for correct
complex-script rendering (Bengali, Arabic, etc.)
- **JS engine**: write a minimal one from scratch for early milestones; evaluate
embedding `boa` (Rust JS engine) or `QuickJS` (C) once static-page rendering works,
since a production-grade JS engine is itself a multi-year project on its own.

## Batch plan

### Batch 1 — HTML Tokenizer + Parser → DOM done
- [x] Byte stream → token stream (tags, attributes, text, comments, doctype)
- [x] DOM tree (arena-based: `Dom { nodes: Vec<DomNode> }`, index-linked parent/children)
- [x] Malformed-HTML recovery (never panic/crash on bad input — security requirement)
- [x] Unit tests against basic real-world HTML snippets
- [ ] Full WHATWG insertion-mode state machine (implied end tags, table
foster-parenting, `<script>`/`<style>` raw-text handling) — deferred;
current builder is a simplified stack-based tree builder

### Batch 2 — CSS Parser + CSSOM (1-1.5 months) done
- [x] CSS tokenizer + parser (selectors, declarations, at-rules)
- [x] CSSOM tree (`Stylesheet`, `Rule`, `Selector`, `Declaration`)
- [x] Cascade: specificity (id/class/type counts), `!important`, source order preserved
- [x] Combinators: descendant, child (`>`), next-sibling (`+`), subsequent-sibling (`~`)
- [x] At-rules (`@media`, etc.) recognized and safely skipped as whole blocks
- [ ] Pseudo-classes (`:hover`, `:nth-child`, ...) — not yet supported; an
unsupported selector currently causes the *whole rule* to be dropped
cleanly rather than misparsed. Revisit when building selector matching
in Batch 3.

### Batch 3 — Style Resolution + Layout Engine (2-3 months) done
- [x] Selector matching (right-to-left, walks ancestors/siblings via the DOM arena)
- [x] Cascade → computed style per element, with a fixed inherited-property list
(color, font-family, font-size, font-weight, line-height, text-align, visibility)
- [x] Box model (content/padding/border/margin, `Rect`/`EdgeSizes`/`Dimensions`)
- [x] Block layout: vertical stacking, auto vs explicit width/height, `display: none`
- [x] Anonymous boxes wrapping inline-content runs (standard technique for
mixed block/inline children)
- [ ] Flexbox — deferred
- [ ] Real text shaping — text run height/width currently uses a fixed
average-character-width heuristic (`AVG_CHAR_WIDTH_PX`/`LINE_HEIGHT_PX`
in `layout/engine.rs`), not real font metrics. This is a known,
documented approximation until HarfBuzz/font integration in a later batch.
- [ ] Inline elements (`<span>`, `<a>`, ...) don't get their own boxes yet —
their text is flattened into the surrounding run

### Batch 4 — Paint + Compositor (1-1.5 months) done
- [x] Layout tree → paint command list (rects, text runs, images)
- [x] GPU-accelerated compositing via `wgpu`
- [x] Scroll + layer compositing for smooth 60fps feel

### Batch 5 — JavaScript Engine (3-6 months — highest-risk batch) done
- [x] Lexer → parser → AST
- [x] Bytecode compiler + stack-based VM
- [x] Garbage collector (mark-sweep to start)
- [x] DOM bindings (JS mutating the DOM tree from Batch 1)

### Batch 6 — Networking (1 month) done
- [x] HTTP/1.1 client, then HTTP/2
- [x] TLS 1.3 via `rustls`, strict certificate validation
- [x] DNS & URL scheme parsing

### Batch 7 — Security Layer (parallel, starts alongside Batch 1) done
- [x] Multi-process architecture: browser process + sandboxed renderer process(es)
- [x] IPC channel between browser process and renderer
- [x] Same-origin policy enforcement
- [x] Content-Security-Policy enforcement

### Batch 8 — Browser Shell / UI done
- [x] Tab management, address bar, navigation history
- [x] Live URL fetching integration
- [ ] Per-site permission prompts (camera/mic/location)
- [ ] Privacy dashboard (what's blocked, per-origin storage view)

## Realistic sequencing
Batch 1 → 2 → 3 → 4 gives a browser that can render a static styled HTML page.
Batch 6 and 7 (networking + security/sandboxing) should run in parallel with 1-4,
since the process/sandbox architecture affects how every other component is wired
together. Batch 5 (JS engine) is the biggest risk — attempt it only after static
page rendering works, to keep motivation and momentum.

## Status
🎉 **All 8 Batches are 100% COMPLETED and Verified!**
1. **Batch 1 (HTML Parser & DOM Arena)** — Completed
2. **Batch 2 (CSS Parser & CSSOM Engine)** — Completed
3. **Batch 3 (Style Resolution & Box Model Layout Engine)** — Completed
4. **Batch 4 (GPU Paint Engine & `wgpu` Compositor)** — Completed
5. **Batch 5 (Custom JS Lexer, Parser, Bytecode VM & DOM Bindings)** — Completed
6. **Batch 6 (TLS 1.3 Networking, HTTP Client & URL Resolver)** — Completed
7. **Batch 7 (Multi-process IPC Security, SOP & CSP Enforcer)** — Completed
8. **Batch 8 (Browser Shell, Visual Multi-Tab Bar, Navigation History & Bookmarks)** — Completed

All **45 unit tests** are passing cleanly with 100% test suite success!



# Secure Browser Engine — Build-from-Scratch Roadmap

## Vision
A memory-safe, security-first web browser built from the ground up — own rendering
engine, own layout, own paint pipeline — targeting fast and smooth browsing without
depending on Chromium/WebKit/Gecko internals.

## Language & core tech decisions
- **Language: Rust** — eliminates entire classes of memory-safety bugs (use-after-free,
buffer overflow) that make up the majority of historical Chrome/Firefox CVEs.
- **GPU/compositing**: `wgpu` (cross-platform GPU crate, works on Vulkan/Metal/DX12)
- **TLS**: `rustls` (pure Rust TLS, avoids OpenSSL memory-safety history)
- **Text shaping**: `harfbuzz` bindings or `rustybuzz` (Rust port) — needed for correct
complex-script rendering (Bengali, Arabic, etc.)
- **JS engine**: write a minimal one from scratch for early milestones; evaluate
embedding `boa` (Rust JS engine) or `QuickJS` (C) once static-page rendering works,
since a production-grade JS engine is itself a multi-year project on its own.

## Batch plan

### Batch 1 — HTML Tokenizer + Parser → DOM done
- [x] Byte stream → token stream (tags, attributes, text, comments, doctype)
- [x] DOM tree (arena-based: `Dom { nodes: Vec<DomNode> }`, index-linked parent/children)
- [x] Malformed-HTML recovery (never panic/crash on bad input — security requirement)
- [x] Unit tests against basic real-world HTML snippets
- [ ] Full WHATWG insertion-mode state machine (implied end tags, table
foster-parenting, `<script>`/`<style>` raw-text handling) — deferred;
current builder is a simplified stack-based tree builder

### Batch 2 — CSS Parser + CSSOM (1-1.5 months) done
- [x] CSS tokenizer + parser (selectors, declarations, at-rules)
- [x] CSSOM tree (`Stylesheet`, `Rule`, `Selector`, `Declaration`)
- [x] Cascade: specificity (id/class/type counts), `!important`, source order preserved
- [x] Combinators: descendant, child (`>`), next-sibling (`+`), subsequent-sibling (`~`)
- [x] At-rules (`@media`, etc.) recognized and safely skipped as whole blocks
- [x] Pseudo-classes (`:hover`, `:active`, `:focus`, `:first-child`, `:last-child`, `:only-child`, `:empty`, `:root`, `:checked`, `:disabled`, `:enabled`) supported in tokenizer, parser, CSSOM, specificity resolution, and selector matching engine

### Batch 3 — Style Resolution + Layout Engine (2-3 months) done
- [x] Selector matching (right-to-left, walks ancestors/siblings via the DOM arena)
- [x] Cascade → computed style per element, with a fixed inherited-property list
(color, font-family, font-size, font-weight, line-height, text-align, visibility)
- [x] Box model (content/padding/border/margin, `Rect`/`EdgeSizes`/`Dimensions`)
- [x] Block layout: vertical stacking, auto vs explicit width/height, `display: none`
- [x] Anonymous boxes wrapping inline-content runs (standard technique for
mixed block/inline children)
- [ ] Flexbox — deferred
- [ ] Real text shaping — text run height/width currently uses a fixed
average-character-width heuristic (`AVG_CHAR_WIDTH_PX`/`LINE_HEIGHT_PX`
in `layout/engine.rs`), not real font metrics. This is a known,
documented approximation until HarfBuzz/font integration in a later batch.
- [ ] Inline elements (`<span>`, `<a>`, ...) don't get their own boxes yet —
their text is flattened into the surrounding run

### Batch 4 — Paint + Compositor (1-1.5 months) done
- [x] Layout tree → paint command list (rects, text runs, images)
- [x] GPU-accelerated compositing via `wgpu`
- [x] Scroll + layer compositing for smooth 60fps feel

### Batch 5 — JavaScript Engine (3-6 months — highest-risk batch) done
- [x] Lexer → parser → AST
- [x] Bytecode compiler + stack-based VM
- [x] Garbage collector (mark-sweep to start)
- [x] DOM bindings (JS mutating the DOM tree from Batch 1)

### Batch 6 — Networking (1 month) done
- [x] HTTP/1.1 client, then HTTP/2
- [x] TLS 1.3 via `rustls`, strict certificate validation
- [x] DNS & URL scheme parsing

### Batch 7 — Security Layer (parallel, starts alongside Batch 1) done
- [x] Multi-process architecture: browser process + sandboxed renderer process(es)
- [x] IPC channel between browser process and renderer
- [x] Same-origin policy enforcement
- [x] Content-Security-Policy enforcement

### Batch 8 — Browser Shell / UI done
- [x] Tab management, address bar, navigation history
- [x] Live URL fetching integration
- [ ] Per-site permission prompts (camera/mic/location)
- [ ] Privacy dashboard (what's blocked, per-origin storage view)

## Realistic sequencing
Batch 1 → 2 → 3 → 4 gives a browser that can render a static styled HTML page.
Batch 6 and 7 (networking + security/sandboxing) should run in parallel with 1-4,
since the process/sandbox architecture affects how every other component is wired
together. Batch 5 (JS engine) is the biggest risk — attempt it only after static
page rendering works, to keep motivation and momentum.

## Status
🎉 **All 8 Batches are 100% COMPLETED and Verified!**
1. **Batch 1 (HTML Parser & DOM Arena)** — Completed
2. **Batch 2 (CSS Parser & CSSOM Engine)** — Completed
3. **Batch 3 (Style Resolution & Box Model Layout Engine)** — Completed
4. **Batch 4 (GPU Paint Engine & `wgpu` Compositor)** — Completed
5. **Batch 5 (Custom JS Lexer, Parser, Bytecode VM & DOM Bindings)** — Completed
6. **Batch 6 (TLS 1.3 Networking, HTTP Client & URL Resolver)** — Completed
7. **Batch 7 (Multi-process IPC Security, SOP & CSP Enforcer)** — Completed
8. **Batch 8 (Browser Shell, Visual Multi-Tab Bar, Navigation History & Bookmarks)** — Completed

All **45 unit tests** are passing cleanly with 100% test suite success!




55 changes: 54 additions & 1 deletion src/css/cssom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,65 @@ pub enum Combinator {
SubsequentSibling, // "a ~ b"
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PseudoClass {
Hover,
Active,
Focus,
FirstChild,
LastChild,
OnlyChild,
Root,
Empty,
Checked,
Disabled,
Enabled,
Other(String),
}

impl PseudoClass {
pub fn from_name(name: &str) -> Self {
match name.to_ascii_lowercase().as_str() {
"hover" => PseudoClass::Hover,
"active" => PseudoClass::Active,
"focus" => PseudoClass::Focus,
"first-child" => PseudoClass::FirstChild,
"last-child" => PseudoClass::LastChild,
"only-child" => PseudoClass::OnlyChild,
"root" => PseudoClass::Root,
"empty" => PseudoClass::Empty,
"checked" => PseudoClass::Checked,
"disabled" => PseudoClass::Disabled,
"enabled" => PseudoClass::Enabled,
other => PseudoClass::Other(other.to_string()),
}
}

pub fn as_str(&self) -> &str {
match self {
PseudoClass::Hover => "hover",
PseudoClass::Active => "active",
PseudoClass::Focus => "focus",
PseudoClass::FirstChild => "first-child",
PseudoClass::LastChild => "last-child",
PseudoClass::OnlyChild => "only-child",
PseudoClass::Root => "root",
PseudoClass::Empty => "empty",
PseudoClass::Checked => "checked",
PseudoClass::Disabled => "disabled",
PseudoClass::Enabled => "enabled",
PseudoClass::Other(s) => s.as_str(),
}
}
}

#[derive(Debug, Clone, PartialEq, Default)]
pub struct SimpleSelector {
pub universal: bool,
pub tag: Option<String>,
pub id: Option<String>,
pub classes: Vec<String>,
pub pseudo_classes: Vec<PseudoClass>,
}

/// (id_count, class_count, type_count) — CSS specificity, per
Expand All @@ -52,7 +105,7 @@ impl Selector {
if simple.id.is_some() {
spec.0 += 1;
}
spec.1 += simple.classes.len() as u32;
spec.1 += (simple.classes.len() + simple.pseudo_classes.len()) as u32;
if simple.tag.is_some() {
spec.2 += 1;
}
Expand Down
2 changes: 1 addition & 1 deletion src/css/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub mod parser;
pub mod token;
pub mod tokenizer;

pub use cssom::{Combinator, Declaration, Rule, Selector, SimpleSelector, Stylesheet};
pub use cssom::{Combinator, Declaration, PseudoClass, Rule, Selector, SimpleSelector, Stylesheet};
pub use parser::{parse_selector, parse_selector_list, CssParser};
pub use token::CssToken;
pub use tokenizer::CssTokenizer;
35 changes: 34 additions & 1 deletion src/css/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// a browser that chokes on the first one it doesn't support is a security and
// robustness problem, not just a missing-feature problem.

use super::cssom::{Combinator, Declaration, Rule, Selector, SimpleSelector, Stylesheet};
use super::cssom::{Combinator, Declaration, PseudoClass, Rule, Selector, SimpleSelector, Stylesheet};
use super::token::CssToken;

pub struct CssParser {
Expand Down Expand Up @@ -213,6 +213,7 @@ impl CssParser {
| Some(CssToken::Hash(_))
| Some(CssToken::Delim('.'))
| Some(CssToken::Delim('*'))
| Some(CssToken::Colon)
)
}

Expand Down Expand Up @@ -248,6 +249,22 @@ impl CssParser {
break;
}
}
Some(CssToken::Colon) => {
self.pos += 1;
// Optional double colon `::` for pseudo-elements (tolerant parsing)
if matches!(self.peek(), Some(CssToken::Colon)) {
self.pos += 1;
}
if let Some(CssToken::Ident(pseudo)) = self.peek() {
let pc = PseudoClass::from_name(pseudo);
simple.pseudo_classes.push(pc);
self.pos += 1;
matched_anything = true;
} else {
// malformed ":" with nothing after — recover by stopping
break;
}
}
_ => break,
}
}
Expand Down Expand Up @@ -440,4 +457,20 @@ mod parser_tests {
assert_eq!(list[1].steps[0].1.classes, vec!["title".to_string()]);
assert_eq!(list[2].steps[0].1.id.as_deref(), Some("banner"));
}

#[test]
fn test_parse_pseudo_classes() {
let sel = parse_selector("a:hover, button:first-child:focus").unwrap();
assert_eq!(sel.steps[0].1.tag.as_deref(), Some("a"));
assert_eq!(sel.steps[0].1.pseudo_classes, vec![PseudoClass::Hover]);

let sel2 = parse_selector("button:first-child:focus").unwrap();
assert_eq!(sel2.steps[0].1.tag.as_deref(), Some("button"));
assert_eq!(
sel2.steps[0].1.pseudo_classes,
vec![PseudoClass::FirstChild, PseudoClass::Focus]
);
// Specificity: tag (1) + 2 pseudo-classes (2) = (0, 2, 1)
assert_eq!(sel2.specificity(), (0, 2, 1));
}
}
27 changes: 27 additions & 0 deletions src/dom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,31 @@ mod tests {
assert_eq!(dom.text_content(desc_all[0]), "First Card");
assert_eq!(dom.text_content(desc_all[1]), "Second Card");
}

#[test]
fn test_dom_query_selector_pseudo_classes() {
let html = r#"<ul id="list"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul><button disabled>Off</button><button id="btn" data-hover>Hover Me</button>"#;
let tokens = Tokenizer::new(html).tokenize();
let mut dom = build_dom(tokens);

let first = dom.query_selector("li:first-child").expect("first-child matches");
assert_eq!(dom.text_content(first), "Item 1");

let last = dom.query_selector("li:last-child").expect("last-child matches");
assert_eq!(dom.text_content(last), "Item 3");

let disabled_btn = dom.query_selector("button:disabled").expect("disabled matches");
assert_eq!(dom.text_content(disabled_btn), "Off");

let hovered_btn = dom.query_selector("button:hover").expect("hover matches");
assert_eq!(dom.element_at(hovered_btn).unwrap().id.as_deref(), Some("btn"));

let item2 = dom.query_selector_all("li")[1];
dom.set_hovered(item2, true);
assert!(crate::style::matching::matches_selector(
&dom,
item2,
&crate::css::parser::parse_selector("li:hover").unwrap()
));
}
}
Loading
Loading