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 // `&` 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}