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}