nx_cookie.nx source
↩ module page · 191 lines · 6049 B
1// cookie.nx -- parse HTTP Cookie header (RFC 6265 ยง4.2).
2//
3// Parses the Cookie request header value:
4// "name1=value1; name2=value2; name3=value3"
5//
6// This is the client-sent form that every HTTP server sees on
7// every authenticated request. Used for: session IDs, CSRF
8// tokens, language prefs, A/B flags. Nishi-pages + sovereign
9// HTTP handlers need this to validate session cookies.
10//
11// Differences from query.nx:
12// - Separator is "; " (semicolon + space) per RFC, though we
13// also accept bare ";" for robustness.
14// - No percent-encoding on names; values may be quoted with
15// surrounding double-quotes (we strip them on parse).
16// - Empty values legal ("foo=; bar=baz") means cookie deletion.
17//
18// Not handled here: Set-Cookie (the server-sent response header)
19// which has many more attributes (Path, Domain, Secure, HttpOnly,
20// SameSite, Max-Age, Expires). That's a separate set_cookie.nx
21// shipper for the writer side.
22//
23// Invariants:
24// C1 Zero-alloc -- offsets into caller's buffer.
25// C2 Leading/trailing whitespace on names + values trimmed.
26// C3 Quoted values ("foo") have quotes stripped; embedded
27// escaped characters NOT unescaped today (RFC doesn't
28// specify quoted-string escape semantics for Cookie).
29
30// nx_safety_envelope:
31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
32// sil_target: SIL1
33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
34// verdict: NOT_YET_EVALUATED
35
36import "nx_syscalls.nx"
37
38const COOKIE_ERR_OVERFLOW: i64 = -1
39
40struct CookieEntry {
41 name_off: i64, name_len: i64,
42 val_off: i64, val_len: i64,
43}
44
45// Is byte ASCII whitespace (space or tab)?
46func cookie_is_ws(b: i64) -> i64 {
47 if b == 0x20 { return 1 }
48 if b == 0x09 { return 1 }
49 return 0
50}
51
52// Skip leading whitespace.
53func cookie_ltrim(buf: *u8, off: i64, end: i64) -> i64 {
54 var i: i64 = off
55 while i < end {
56 if cookie_is_ws(buf[i]) == 0 { return i }
57 i = i + 1
58 }
59 return end
60}
61
62// Trim trailing whitespace from [start, end).
63func cookie_rtrim(buf: *u8, start: i64, end: i64) -> i64 {
64 var e: i64 = end
65 while e > start {
66 if cookie_is_ws(buf[e - 1]) == 0 { return e }
67 e = e - 1
68 }
69 return e
70}
71
72// Parse a Cookie header value into caller's entry table. Returns
73// number of cookies parsed or COOKIE_ERR_OVERFLOW.
74func cookie_parse(buf: *u8, n: i64,
75 entries: *CookieEntry, cap: i64) -> i64 {
76 var count: i64 = 0
77 var i: i64 = 0
78
79 while i < n {
80 // Skip leading separator whitespace.
81 i = cookie_ltrim(buf, i, n)
82 if i >= n { break }
83
84 // Name runs to '='.
85 let name_start: i64 = i
86 while i < n {
87 if buf[i] == 0x3D { break }
88 if buf[i] == 0x3B { break }
89 i = i + 1
90 }
91 let name_end: i64 = cookie_rtrim(buf, name_start, i)
92
93 if count >= cap { return COOKIE_ERR_OVERFLOW }
94 let e: *CookieEntry = entries + count * 32
95 e.name_off = name_start
96 e.name_len = name_end - name_start
97 e.val_off = i
98 e.val_len = 0
99
100 // If we stopped on '=' there's a value; else empty.
101 if i < n {
102 if buf[i] == 0x3D {
103 i = i + 1
104 let v_start_trim: i64 = cookie_ltrim(buf, i, n)
105 var v_start: i64 = v_start_trim
106 i = v_start
107 while i < n {
108 if buf[i] == 0x3B { break }
109 i = i + 1
110 }
111 let v_end_trim: i64 = cookie_rtrim(buf, v_start, i)
112 // Strip surrounding quotes if present.
113 var v_end: i64 = v_end_trim
114 if v_end > v_start {
115 if buf[v_start] == 0x22 {
116 if buf[v_end - 1] == 0x22 {
117 v_start = v_start + 1
118 v_end = v_end - 1
119 }
120 }
121 }
122 e.val_off = v_start
123 e.val_len = v_end - v_start
124 }
125 }
126
127 count = count + 1
128
129 // Consume ';' if present.
130 if i < n {
131 if buf[i] == 0x3B { i = i + 1 }
132 }
133 }
134 return count
135}
136
137// Find cookie by name. Returns index or -1.
138func cookie_find(buf: *u8, entries: *CookieEntry, count: i64,
139 name: *u8, name_len: i64) -> i64 {
140 var i: i64 = 0
141 while i < count {
142 let e: *CookieEntry = entries + i * 32
143 if e.name_len == name_len {
144 var j: i64 = 0
145 var ok: i64 = 1
146 while j < name_len {
147 if buf[e.name_off + j] != name[j] {
148 ok = 0
149 break
150 }
151 j = j + 1
152 }
153 if ok == 1 { return i }
154 }
155 i = i + 1
156 }
157 return -1
158}
159
160// Compile-only smoke: "sid=abc123; theme=dark; empty=".
161func main() -> i64 {
162 let entries_raw: *u8 = sys_mmap(512)
163 let entries: *CookieEntry = entries_raw as *CookieEntry
164
165 let input: *u8 = "sid=abc123; theme=dark; empty="
166 let n: i64 = cookie_parse(input, 30, entries, 16)
167 if n != 3 { return 1 }
168
169 let e0: *CookieEntry = entries
170 if e0.name_len != 3 { return 2 }
171 if e0.val_len != 6 { return 3 }
172 if input[e0.name_off] != 0x73 { return 4 } // 's'
173 if input[e0.val_off] != 0x61 { return 5 } // 'a'
174
175 let e2: *CookieEntry = entries + 2 * 32
176 if e2.name_len != 5 { return 6 }
177 if e2.val_len != 0 { return 7 }
178
179 // Quoted value.
180 let q: *u8 = "user=\"Alice Smith\""
181 let q_entries: *CookieEntry = (sys_mmap(512)) as *CookieEntry
182 let qn: i64 = cookie_parse(q, 18, q_entries, 16)
183 if qn != 1 { return 8 }
184 let qe: *CookieEntry = q_entries
185 if qe.val_len != 11 { return 9 } // "Alice Smith" (quotes stripped)
186
187 // Lookup
188 let idx: i64 = cookie_find(input, entries, n, "theme", 5)
189 if idx != 1 { return 10 }
190 return 0
191}