code wiki / (root) / nx_html_extract_links.nx

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}