code wiki / (root) / nx_url.nx

nx_url.nx source

↩ module page · 237 lines · 8334 B

1// nx_url.nx -- minimal URL parser (RFC 3986 subset). 2// 3// Parses "scheme://host[:port]/path?query#fragment" into its 4// component spans within the original buffer. No allocation; 5// each component is an offset+length pair pointing into the 6// original input string. 7// 8// Used by: 9// - HTTP client (host/port for connect, path for GET line) 10// - Sovereign Gemini browser (the next-app-after-chat use case) 11// - Service discovery URL canonicalization 12// 13// Out of scope: 14// - Percent-decoding 15// - IPv6 address brackets ([fe80::1]) 16// - userinfo (user:pass@host) 17// - International domain names (Punycode) 18 19// nx_safety_envelope: 20// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 21// sil_target: SIL1 22// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 23// verdict: NOT_YET_EVALUATED 24 25import "syscalls.nx" 26import "nx_runtime.nx" // REPOINTED from the twin runtime.nx: both landed in nx_cc_ingest's 152-module 27// closure (four siblings -- nx_bitstream/nx_deflate/nx_gzip_wrap/nx_huffman -- import nx_runtime.nx, 28// this file alone imported runtime.nx) so arena_new was defined TWICE and the dup-def guard failed the 29// build. SAFE BY CONSTRUCTION: the two files are SEMANTICALLY IDENTICAL -- diff is ZERO lines after 30// stripping CR (runtime.nx is a CRLF twin, 179 CR chars) and normalising syscalls.nx -> nx_syscalls.nx; 31// same funcs, same decls, 125 code lines each. Canonical is nx_runtime.nx (267 importers vs 14). 32const NX_MAGIC_65535: i64 = 65535 33const NX_MAGIC_8080: i64 = 8080 34 35struct NxUrl { 36 scheme_off: i64, scheme_len: i64, 37 host_off: i64, host_len: i64, 38 port: i64, // 0 = unspecified, else parsed port 39 path_off: i64, path_len: i64, 40 query_off: i64, query_len: i64, 41 frag_off: i64, frag_len: i64, 42} 43 44const NX_URL_BYTES: i64 = 88 45 46// Errors. 47const NX_URL_ERR_NO_SCHEME: i64 = -1 48const NX_URL_ERR_NO_HOST: i64 = -2 49const NX_URL_ERR_BAD_PORT: i64 = -3 50 51// strlen canonical in runtime.nx. 52 53// Allocate a fresh NxUrl with all fields zeroed. 54func nx_url_new() -> *NxUrl { 55 let raw: *u8 = sys_mmap(NX_URL_BYTES) 56 let u: *NxUrl = raw as *NxUrl 57 u.scheme_off = 0; u.scheme_len = 0 58 u.host_off = 0; u.host_len = 0 59 u.port = 0 60 u.path_off = 0; u.path_len = 0 61 u.query_off = 0; u.query_len = 0 62 u.frag_off = 0; u.frag_len = 0 63 return u 64} 65 66// Parse `s` (NUL-terminated) into `out`. Returns 0 on success or 67// a negative NX_URL_ERR_* code on failure. 68func nx_url_parse(s: *u8, out: *NxUrl) -> i64 { 69 let n: i64 = strlen(s) 70 var i: i64 = 0 71 72 // Scheme = [a-zA-Z][a-zA-Z0-9+\-.]* up to ':'. 73 out.scheme_off = 0 74 while i < n { 75 if s[i] == 0x3A { i = n + 1 } // sentinel break 76 else { i = i + 1 } 77 } 78 if i <= n + 1 { 79 // Re-scan to find scheme ':'. 80 var j: i64 = 0 81 var sc: i64 = -1 82 while j < n { 83 if s[j] == 0x3A { sc = j; j = n } 84 else { j = j + 1 } 85 } 86 if sc < 0 { return NX_URL_ERR_NO_SCHEME } 87 out.scheme_len = sc 88 i = sc + 1 89 } 90 91 // Expect "//" 92 if i + 1 >= n { return NX_URL_ERR_NO_HOST } 93 if s[i] != 0x2F { return NX_URL_ERR_NO_HOST } 94 if s[i + 1] != 0x2F { return NX_URL_ERR_NO_HOST } 95 i = i + 2 96 97 // Host = up to ':' / '/' / '?' / '#' / EOI. Walk with a stop 98 // flag (NishiLang has no `break` keyword today). 99 out.host_off = i 100 var host_end: i64 = i 101 var stop_flag: i64 = 0 102 while stop_flag == 0 { 103 if host_end >= n { stop_flag = 1 } 104 else { 105 let c2: i64 = s[host_end] 106 if c2 == 0x3A { stop_flag = 1 } 107 else { if c2 == 0x2F { stop_flag = 1 } 108 else { if c2 == 0x3F { stop_flag = 1 } 109 else { if c2 == 0x23 { stop_flag = 1 } 110 else { host_end = host_end + 1 } 111 } 112 } 113 } 114 } 115 } 116 out.host_len = host_end - i 117 if out.host_len == 0 { return NX_URL_ERR_NO_HOST } 118 i = host_end 119 120 // Optional port = ':' digits... 121 if i < n { 122 if s[i] == 0x3A { 123 i = i + 1 124 var port_val: i64 = 0 125 var pcount: i64 = 0 126 // Stop AT the first non-digit (e.g. the '/' that starts the path) -- 127 // must NOT advance i to n, or the path parse below is skipped. This 128 // was the bug that dropped the path on every explicit-:port URL. 129 var pstop: i64 = 0 130 while pstop == 0 { 131 if i >= n { pstop = 1 } 132 else { 133 let pc: i64 = s[i] 134 if pc < 0x30 { pstop = 1 } 135 else { if pc > 0x39 { pstop = 1 } 136 else { 137 port_val = port_val * 10 + (pc - 0x30) 138 if port_val > NX_MAGIC_65535 { return NX_URL_ERR_BAD_PORT } 139 pcount = pcount + 1 140 i = i + 1 141 } 142 } 143 } 144 } 145 if pcount == 0 { return NX_URL_ERR_BAD_PORT } 146 out.port = port_val 147 } 148 } 149 150 // Path = '/' up to '?' or '#'. 151 if i < n { 152 if s[i] == 0x2F { 153 out.path_off = i 154 var p_stop: i64 = 0 155 while p_stop == 0 { 156 if i >= n { p_stop = 1 } 157 else { 158 let c3: i64 = s[i] 159 if c3 == 0x3F { p_stop = 1 } 160 else { if c3 == 0x23 { p_stop = 1 } 161 else { i = i + 1 } 162 } 163 } 164 } 165 out.path_len = i - out.path_off 166 } 167 } 168 169 // Query = '?' up to '#'. 170 if i < n { 171 if s[i] == 0x3F { 172 i = i + 1 173 out.query_off = i 174 var q_stop: i64 = 0 175 while q_stop == 0 { 176 if i >= n { q_stop = 1 } 177 else { 178 if s[i] == 0x23 { q_stop = 1 } 179 else { i = i + 1 } 180 } 181 } 182 out.query_len = i - out.query_off 183 } 184 } 185 186 // Fragment = '#' to EOI. 187 if i < n { 188 if s[i] == 0x23 { 189 i = i + 1 190 out.frag_off = i 191 out.frag_len = n - i 192 } 193 } 194 195 return 0 196} 197 198// ---- self-test --------------------------------------------------- 199 200func main() -> i64 { 201 let url: *NxUrl = nx_url_new() 202 let s: *u8 = sys_mmap(128) 203 204 // "http://example.com:8080/path/to?key=val#section" 205 s[0]=0x68; s[1]=0x74; s[2]=0x74; s[3]=0x70 // "http" 206 s[4]=0x3A; s[5]=0x2F; s[6]=0x2F // "://" 207 s[7]=0x65; s[8]=0x78; s[9]=0x61; s[10]=0x6D 208 s[11]=0x70; s[12]=0x6C; s[13]=0x65; s[14]=0x2E 209 s[15]=0x63; s[16]=0x6F; s[17]=0x6D // "example.com" 210 s[18]=0x3A; s[19]=0x38; s[20]=0x30; s[21]=0x38; s[22]=0x30 // ":8080" 211 s[23]=0x2F; s[24]=0x70; s[25]=0x61; s[26]=0x74; s[27]=0x68 // "/path" 212 s[28]=0x2F; s[29]=0x74; s[30]=0x6F // "/to" 213 s[31]=0x3F; s[32]=0x6B; s[33]=0x65; s[34]=0x79 // "?key" 214 s[35]=0x3D; s[36]=0x76; s[37]=0x61; s[38]=0x6C // "=val" 215 s[39]=0x23; s[40]=0x73; s[41]=0x65; s[42]=0x63 // "#sec" 216 s[43]=0x74; s[44]=0x69; s[45]=0x6F; s[46]=0x6E 217 s[47]=0 218 219 let r: i64 = nx_url_parse(s, url) 220 if r != 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 221 222 if url.scheme_len != 4 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 223 if url.host_off != 7 { return __syscall(93, 3, 0, 0, 0, 0, 0) } 224 if url.host_len != 11 { return __syscall(93, 4, 0, 0, 0, 0, 0) } 225 if url.port != NX_MAGIC_8080 { return __syscall(93, 5, 0, 0, 0, 0, 0) } 226 if url.path_len != 8 { return __syscall(93, 6, 0, 0, 0, 0, 0) } // "/path/to" 227 if url.query_len != 7 { return __syscall(93, 7, 0, 0, 0, 0, 0) } // "key=val" 228 if url.frag_len != 7 { return __syscall(93, 8, 0, 0, 0, 0, 0) } // "section" 229 230 // No scheme should fail. 231 let no_scheme: *u8 = sys_mmap(8) 232 no_scheme[0] = 0x66; no_scheme[1] = 0x6F; no_scheme[2] = 0x6F; no_scheme[3] = 0 233 let r2: i64 = nx_url_parse(no_scheme, url) 234 if r2 != NX_URL_ERR_NO_SCHEME { return __syscall(93, 9, 0, 0, 0, 0, 0) } 235 236 return 0 237}