nx_html_extract_imgs.nx source
↩ module page · 130 lines · 5672 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"
42
43const NX_IMG_EXTRACT_OK_BASE: i64 = 0
44const NX_IMG_EXTRACT_BAD_BASE: i64 = -1
45
46// case-insensitive equality on a 3-byte tag name "img" / "IMG" / etc.
47func _img_name_eq_img(src: *u8, name_off: i64, name_len: i64) -> i64 {
48 if name_len != 3 { return 0 }
49 let c0: i64 = src[name_off]
50 let c1: i64 = src[name_off + 1]
51 let c2: i64 = src[name_off + 2]
52 var lc0: i64 = c0
53 var lc1: i64 = c1
54 var lc2: i64 = c2
55 if lc0 >= 65 { if lc0 <= 90 { lc0 = lc0 + 32 } }
56 if lc1 >= 65 { if lc1 <= 90 { lc1 = lc1 + 32 } }
57 if lc2 >= 65 { if lc2 <= 90 { lc2 = lc2 + 32 } }
58 if lc0 == 0x69 { if lc1 == 0x6D { if lc2 == 0x67 { return 1 } } }
59 return 0
60}
61
62func nx_html_extract_imgs(html: *u8, html_len: i64,
63 base_url: *u8, base_url_len: i64,
64 url_buf: *u8, url_buf_cap: i64,
65 offsets: *i64, lengths: *i64,
66 max_imgs: i64) -> i64 {
67 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor
68 nx_html_cursor_init(c, html, html_len)
69 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken
70 let src_attr: *u8 = "src" as *u8
71 let val_off_p: *i64 = sys_mmap(8) as *i64
72 let val_len_p: *i64 = sys_mmap(8) as *i64
73 let out_len_p: *i64 = sys_mmap(8) as *i64
74
75 var buf_pos: i64 = 0
76 var count: i64 = 0
77
78 while count < max_imgs {
79 nx_html_next_token(c, tok)
80 if tok.kind == NX_HTML_TOK_EOF { return count }
81
82 // Raw-text-mode tags: skip their bodies so script/style/textarea/title
83 // bodies containing "<img" never get tokenized as tags.
84 if tok.kind == NX_HTML_TOK_START_TAG {
85 if nx_html_is_raw_text_tag(html, tok.name_off, tok.name_len) == 1 {
86 let tag_name_off: i64 = tok.name_off
87 let tag_name_len: i64 = tok.name_len
88 nx_html_consume_raw_text(c, html + tag_name_off, tag_name_len, tok)
89 // tok is now the TEXT body; the next iteration's
90 // nx_html_next_token will see the close tag and yield END_TAG.
91 }
92 }
93
94 // <img> is in the HTML5 void-element set, so it surfaces as
95 // SELF_CLOSING (the tokenizer auto-promotes per Task #81).
96 // We accept both kinds to be lenient with future tokenizer
97 // changes.
98 var is_img_tag: i64 = 0
99 if tok.kind == NX_HTML_TOK_SELF_CLOSING { is_img_tag = 1 }
100 if tok.kind == NX_HTML_TOK_START_TAG { is_img_tag = 1 }
101
102 if is_img_tag == 1 {
103 if _img_name_eq_img(html, tok.name_off, tok.name_len) == 1 {
104 let has: i64 = nx_dom_find_attr(html, tok.src_off, tok.src_len,
105 src_attr, val_off_p, val_len_p)
106 if has == 1 {
107 let rel_off: i64 = val_off_p[0]
108 let rel_len: i64 = val_len_p[0]
109 if rel_len > 0 {
110 let remaining: i64 = url_buf_cap - buf_pos
111 let rc: i64 = nx_url_resolve(
112 base_url, base_url_len,
113 html + rel_off, rel_len,
114 url_buf + buf_pos, remaining,
115 out_len_p)
116 if rc == NX_URL_RESOLVE_OK {
117 offsets[count] = buf_pos
118 lengths[count] = out_len_p[0]
119 buf_pos = buf_pos + out_len_p[0]
120 count = count + 1
121 }
122 // On TRUNC or BAD_BASE: skip this img (substrate-honest:
123 // returning early would lie about the number found).
124 }
125 }
126 }
127 }
128 }
129 return count
130}