nx_html_extract_links.nx source
↩ module page · 95 lines · 4104 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"
31
32// Case-insensitive check that a tag name is exactly "a".
33func _link_name_eq_a(src: *u8, name_off: i64, name_len: i64) -> i64 {
34 if name_len != 1 { return 0 }
35 let c: i64 = src[name_off]
36 if c == 0x61 { return 1 } // 'a'
37 if c == 0x41 { return 1 } // 'A'
38 return 0
39}
40
41func nx_html_extract_links(html: *u8, html_len: i64,
42 base_url: *u8, base_url_len: i64,
43 url_buf: *u8, url_buf_cap: i64,
44 offsets: *i64, lengths: *i64,
45 max_links: i64) -> i64 {
46 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor
47 nx_html_cursor_init(c, html, html_len)
48 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken
49 let href_attr: *u8 = "href" as *u8
50 let val_off_p: *i64 = sys_mmap(8) as *i64
51 let val_len_p: *i64 = sys_mmap(8) as *i64
52 let out_len_p: *i64 = sys_mmap(8) as *i64
53
54 var buf_pos: i64 = 0
55 var count: i64 = 0
56
57 while count < max_links {
58 nx_html_next_token(c, tok)
59 if tok.kind == NX_HTML_TOK_EOF { return count }
60
61 if tok.kind == NX_HTML_TOK_START_TAG {
62 // Raw-text-mode tags: skip their bodies.
63 if nx_html_is_raw_text_tag(html, tok.name_off, tok.name_len) == 1 {
64 let tag_name_off: i64 = tok.name_off
65 let tag_name_len: i64 = tok.name_len
66 nx_html_consume_raw_text(c, html + tag_name_off, tag_name_len, tok)
67 } else {
68 // Regular <a> tag candidate.
69 if _link_name_eq_a(html, tok.name_off, tok.name_len) == 1 {
70 let has: i64 = nx_dom_find_attr(html, tok.src_off, tok.src_len,
71 href_attr, val_off_p, val_len_p)
72 if has == 1 {
73 let rel_off: i64 = val_off_p[0]
74 let rel_len: i64 = val_len_p[0]
75 if rel_len > 0 {
76 let remaining: i64 = url_buf_cap - buf_pos
77 let rc: i64 = nx_url_resolve(
78 base_url, base_url_len,
79 html + rel_off, rel_len,
80 url_buf + buf_pos, remaining,
81 out_len_p)
82 if rc == NX_URL_RESOLVE_OK {
83 offsets[count] = buf_pos
84 lengths[count] = out_len_p[0]
85 buf_pos = buf_pos + out_len_p[0]
86 count = count + 1
87 }
88 }
89 }
90 }
91 }
92 }
93 }
94 return count
95}