code wiki / (root) / nx_cookie.nx

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}