nx_html_extract_links.nx source
↩ module page · 108 lines · 5164 B
1// nx_html_extract_links.nx -- enumerate <a href="..."> links in a
2// parsed HTML document, resolving each href against the page base URL.
3//
4// Arc B2 sibling of nx_html_extract_imgs. Same shape, same guarantees:
5// - script/style/textarea/title raw-text bodies are SKIPPED so they
6// can't pollute the extracted list with literal "<a" tokens-inside-
7// strings
8// - URL resolution is RFC 3986 ยง5.2 via nx_url_resolve
9// - caller-supplied flat URL buffer + parallel (offset, length)
10// arrays; substrate-honest truncation
11//
12// This is the primitive the bits-up browser uses to discover
13// navigation targets in a fetched page.
14//
15// nx_safety_envelope:
16// intended_use: "Anchor-link enumeration for the bits-up browser."
17// sil_target: SIL1
18// evidence: [composes_b1_raw_text_mode,
19// composes_rfc3986_url_resolver]
20// hazard_register: [bug-tape-javascript-url-bypass-via-href,
21// bug-tape-data-url-bypass-via-href]
22// residual_risk: "Caller is responsible for scheme-allowlisting
23// (e.g. reject javascript:, data:, file:);
24// substrate emits absolute URLs verbatim."
25// verdict: NOT_YET_EVALUATED
26
27import "nx_syscalls.nx"
28import "nx_html_tokenizer.nx"
29import "nx_dom_query.nx"
30import "nx_url_resolve.nx"
31import "nx_url_canon.nx" // an href/src is HTML TEXT: decode character references BEFORE resolving
32
33// Case-insensitive check that a tag name is exactly "a".
34func _link_name_eq_a(src: *u8, name_off: i64, name_len: i64) -> i64 {
35 if name_len != 1 { return 0 }
36 let c: i64 = src[name_off]
37 if c == 0x61 { return 1 } // 'a'
38 if c == 0x41 { return 1 } // 'A'
39 return 0
40}
41
42func nx_html_extract_links(html: *u8, html_len: i64,
43 base_url: *u8, base_url_len: i64,
44 url_buf: *u8, url_buf_cap: i64,
45 offsets: *i64, lengths: *i64,
46 max_links: i64) -> i64 {
47 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor
48 nx_html_cursor_init(c, html, html_len)
49 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken
50 let href_attr: *u8 = "href" as *u8
51 let val_off_p: *i64 = sys_mmap(8) as *i64
52 let val_len_p: *i64 = sys_mmap(8) as *i64
53 let out_len_p: *i64 = sys_mmap(8) as *i64
54 // ONE scratch for the whole call, not one per element.
55 let canon_buf: *u8 = sys_mmap(UC_SCRATCH)
56
57 var buf_pos: i64 = 0
58 var count: i64 = 0
59
60 while count < max_links {
61 nx_html_next_token(c, tok)
62 if tok.kind == NX_HTML_TOK_EOF { return count }
63
64 if tok.kind == NX_HTML_TOK_START_TAG {
65 // Raw-text-mode tags: skip their bodies.
66 if nx_html_is_raw_text_tag(html, tok.name_off, tok.name_len) == 1 {
67 let tag_name_off: i64 = tok.name_off
68 let tag_name_len: i64 = tok.name_len
69 nx_html_consume_raw_text(c, html + tag_name_off, tag_name_len, tok)
70 } else {
71 // Regular <a> tag candidate.
72 if _link_name_eq_a(html, tok.name_off, tok.name_len) == 1 {
73 let has: i64 = nx_dom_find_attr(html, tok.src_off, tok.src_len,
74 href_attr, val_off_p, val_len_p)
75 if has == 1 {
76 let rel_off: i64 = val_off_p[0]
77 let rel_len: i64 = val_len_p[0]
78 if rel_len > 0 {
79 let remaining: i64 = url_buf_cap - buf_pos
80 // `&` in an href is CORRECT html for a single `&`. Copying the
81 // attribute verbatim produced a param literally named `amp;...`, so
82 // every multi-param url we harvested was malformed. Canon REFUSES
83 // (returns 0) rather than truncate, and we then fall back to the raw
84 // bytes so a url too long for the scratch is still resolved exactly as
85 // before -- a canonicaliser must never LOSE a link it cannot improve.
86 var canon_rel: *u8 = html + rel_off
87 var canon_len: i64 = rel_len
88 let cl: i64 = nx_url_canon(html + rel_off, rel_len, canon_buf, UC_SCRATCH - 1)
89 if cl > 0 { canon_rel = canon_buf; canon_len = cl }
90 let rc: i64 = nx_url_resolve(
91 base_url, base_url_len,
92 canon_rel, canon_len,
93 url_buf + buf_pos, remaining,
94 out_len_p)
95 if rc == NX_URL_RESOLVE_OK {
96 offsets[count] = buf_pos
97 lengths[count] = out_len_p[0]
98 buf_pos = buf_pos + out_len_p[0]
99 count = count + 1
100 }
101 }
102 }
103 }
104 }
105 }
106 }
107 return count
108}