code wiki / (root) / nx_html_extract_imgs.nx

nx_html_extract_imgs.nx source

↩ module page · 143 lines · 6732 B

1// nx_html_extract_imgs.nx -- walk a parsed HTML document, find every 2// <img src="..."> element, resolve the src URL against a base URL, 3// and emit absolute URLs into caller-supplied buffers. 4// 5// Arc B2 of NISHI_BROWSER_GOOGLE_IMAGE_SEARCH_ROADMAP. Built on top 6// of the just-shipped Arc B1 raw-text mode so <script> bodies 7// containing "<img" tokens-inside-strings don't pollute results. 8// 9// API: 10// nx_html_extract_imgs(html, html_len, 11// base_url, base_url_len, 12// url_buf, url_buf_cap, 13// offsets, lengths, max_imgs) 14// -> count of imgs emitted (or negative error) 15// 16// `offsets` and `lengths` are caller-supplied parallel i64 arrays 17// of length >= max_imgs. Each (offsets[k], lengths[k]) describes 18// where the k-th absolute URL lives within url_buf. 19// 20// If url_buf fills up before max_imgs is reached, we stop emitting 21// further URLs and return the count we did emit (truncation is 22// substrate-honest: caller sees how many imgs they got). 23// 24// nx_safety_envelope: 25// intended_use: "Image-source enumeration for the bits-up 26// browser; feeds the image fetch + decode arc." 27// sil_target: SIL1 28// evidence: [composes_b1_raw_text_mode, 29// composes_rfc3986_url_resolver] 30// hazard_register: [bug-tape-href-injection-bypassing-quoting, 31// bug-tape-base-href-attack-via-page-content] 32// residual_risk: "<base href> in document body NOT honored 33// in this revision -- caller passes base URL 34// explicitly. Adding <base href> support is 35// Arc B3 once we audit its attack surface." 36// verdict: NOT_YET_EVALUATED 37 38import "nx_syscalls.nx" 39import "nx_html_tokenizer.nx" 40import "nx_dom_query.nx" 41import "nx_url_resolve.nx" 42import "nx_url_canon.nx" // an href/src is HTML TEXT: decode character references BEFORE resolving 43 44const NX_IMG_EXTRACT_OK_BASE: i64 = 0 45const NX_IMG_EXTRACT_BAD_BASE: i64 = -1 46 47// case-insensitive equality on a 3-byte tag name "img" / "IMG" / etc. 48func _img_name_eq_img(src: *u8, name_off: i64, name_len: i64) -> i64 { 49 if name_len != 3 { return 0 } 50 let c0: i64 = src[name_off] 51 let c1: i64 = src[name_off + 1] 52 let c2: i64 = src[name_off + 2] 53 var lc0: i64 = c0 54 var lc1: i64 = c1 55 var lc2: i64 = c2 56 if lc0 >= 65 { if lc0 <= 90 { lc0 = lc0 + 32 } } 57 if lc1 >= 65 { if lc1 <= 90 { lc1 = lc1 + 32 } } 58 if lc2 >= 65 { if lc2 <= 90 { lc2 = lc2 + 32 } } 59 if lc0 == 0x69 { if lc1 == 0x6D { if lc2 == 0x67 { return 1 } } } 60 return 0 61} 62 63func nx_html_extract_imgs(html: *u8, html_len: i64, 64 base_url: *u8, base_url_len: i64, 65 url_buf: *u8, url_buf_cap: i64, 66 offsets: *i64, lengths: *i64, 67 max_imgs: i64) -> i64 { 68 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor 69 nx_html_cursor_init(c, html, html_len) 70 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken 71 let src_attr: *u8 = "src" as *u8 72 let val_off_p: *i64 = sys_mmap(8) as *i64 73 let val_len_p: *i64 = sys_mmap(8) as *i64 74 let out_len_p: *i64 = sys_mmap(8) as *i64 75 // ONE scratch for the whole call, not one per element. 76 let canon_buf: *u8 = sys_mmap(UC_SCRATCH) 77 78 var buf_pos: i64 = 0 79 var count: i64 = 0 80 81 while count < max_imgs { 82 nx_html_next_token(c, tok) 83 if tok.kind == NX_HTML_TOK_EOF { return count } 84 85 // Raw-text-mode tags: skip their bodies so script/style/textarea/title 86 // bodies containing "<img" never get tokenized as tags. 87 if tok.kind == NX_HTML_TOK_START_TAG { 88 if nx_html_is_raw_text_tag(html, tok.name_off, tok.name_len) == 1 { 89 let tag_name_off: i64 = tok.name_off 90 let tag_name_len: i64 = tok.name_len 91 nx_html_consume_raw_text(c, html + tag_name_off, tag_name_len, tok) 92 // tok is now the TEXT body; the next iteration's 93 // nx_html_next_token will see the close tag and yield END_TAG. 94 } 95 } 96 97 // <img> is in the HTML5 void-element set, so it surfaces as 98 // SELF_CLOSING (the tokenizer auto-promotes per Task #81). 99 // We accept both kinds to be lenient with future tokenizer 100 // changes. 101 var is_img_tag: i64 = 0 102 if tok.kind == NX_HTML_TOK_SELF_CLOSING { is_img_tag = 1 } 103 if tok.kind == NX_HTML_TOK_START_TAG { is_img_tag = 1 } 104 105 if is_img_tag == 1 { 106 if _img_name_eq_img(html, tok.name_off, tok.name_len) == 1 { 107 let has: i64 = nx_dom_find_attr(html, tok.src_off, tok.src_len, 108 src_attr, val_off_p, val_len_p) 109 if has == 1 { 110 let rel_off: i64 = val_off_p[0] 111 let rel_len: i64 = val_len_p[0] 112 if rel_len > 0 { 113 let remaining: i64 = url_buf_cap - buf_pos 114 // `&amp;` in an href is CORRECT html for a single `&`. Copying the 115 // attribute verbatim produced a param literally named `amp;...`, so 116 // every multi-param url we harvested was malformed. Canon REFUSES 117 // (returns 0) rather than truncate, and we then fall back to the raw 118 // bytes so a url too long for the scratch is still resolved exactly as 119 // before -- a canonicaliser must never LOSE a link it cannot improve. 120 var canon_rel: *u8 = html + rel_off 121 var canon_len: i64 = rel_len 122 let cl: i64 = nx_url_canon(html + rel_off, rel_len, canon_buf, UC_SCRATCH - 1) 123 if cl > 0 { canon_rel = canon_buf; canon_len = cl } 124 let rc: i64 = nx_url_resolve( 125 base_url, base_url_len, 126 canon_rel, canon_len, 127 url_buf + buf_pos, remaining, 128 out_len_p) 129 if rc == NX_URL_RESOLVE_OK { 130 offsets[count] = buf_pos 131 lengths[count] = out_len_p[0] 132 buf_pos = buf_pos + out_len_p[0] 133 count = count + 1 134 } 135 // On TRUNC or BAD_BASE: skip this img (substrate-honest: 136 // returning early would lie about the number found). 137 } 138 } 139 } 140 } 141 } 142 return count 143}