code wiki / (root) / nx_accept_lang.nx

nx_accept_lang.nx source

↩ module page · 223 lines · 6720 B

1// accept_lang.nx -- parse HTTP Accept-Language header. 2// 3// RFC 7231 ยง5.3.5. Clients send a comma-separated list of 4// language tags with optional quality values: 5// Accept-Language: en-US,en;q=0.9,fr;q=0.8,*;q=0.1 6// 7// We parse this into a sorted array of (tag, quality_thousandths) 8// and expose a lookup helper that picks the best match from a 9// server-supported-languages list. 10// 11// Quality values: 12// - Default q = 1.0 if omitted 13// - Range 0.0..1.0; anything else clamped 14// - Stored as integer thousandths (0..1000) to avoid FP math 15// 16// Sorting: higher quality first; ties preserve input order 17// (stable). Caller walks sorted list first-match against their 18// supported set. 19// 20// Invariants: 21// AL1 Zero-alloc: offsets into caller's buffer. 22// AL2 \"*\" wildcard accepted as a language tag; it matches 23// any language that hasn't been explicitly excluded. 24// AL3 Language tag byte ranges preserved as-is (case-sensitive 25// in the buffer); caller does ASCII-lowercase compare if 26// they need RFC-correct case-insensitive matching. 27 28// nx_safety_envelope: 29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 30// sil_target: SIL1 31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 32// verdict: NOT_YET_EVALUATED 33 34import "nx_syscalls.nx" 35 36const AL_ERR_OVERFLOW: i64 = -1 37 38struct AcceptLang { 39 tag_off: i64, tag_len: i64, 40 quality_thou: i64, // 0..1000 41} 42 43// Skip whitespace. 44func al_is_ws(b: i64) -> i64 { 45 if b == 0x20 { return 1 } 46 if b == 0x09 { return 1 } 47 return 0 48} 49 50func al_ltrim(buf: *u8, off: i64, end: i64) -> i64 { 51 var i: i64 = off 52 while i < end { 53 if al_is_ws(buf[i]) == 0 { return i } 54 i = i + 1 55 } 56 return end 57} 58 59func al_rtrim(buf: *u8, start: i64, end: i64) -> i64 { 60 var e: i64 = end 61 while e > start { 62 if al_is_ws(buf[e - 1]) == 0 { return e } 63 e = e - 1 64 } 65 return e 66} 67 68// Parse a q=V.VVV value starting at *pos. Returns thousandths. 69// Handles q=1, q=1.0, q=0.5, q=0.123, q=0. 70func al_parse_q(buf: *u8, pos: *i64, end: i64) -> i64 { 71 // Skip optional whitespace + 'q'. 72 while *pos < end { 73 if al_is_ws(buf[*pos]) == 0 { break } 74 *pos = *pos + 1 75 } 76 if *pos >= end { return 1000 } 77 if buf[*pos] != 0x71 { return 1000 } // 'q' 78 *pos = *pos + 1 79 if *pos >= end { return 1000 } 80 if buf[*pos] != 0x3D { return 1000 } // '=' 81 *pos = *pos + 1 82 83 // Parse integer part. 84 var int_part: i64 = 0 85 while *pos < end { 86 let b: i64 = buf[*pos] 87 if b < 0x30 { break } 88 if b > 0x39 { break } 89 int_part = int_part * 10 + (b - 0x30) 90 *pos = *pos + 1 91 } 92 var thou: i64 = int_part * 1000 93 if *pos < end { 94 if buf[*pos] == 0x2E { 95 // Fractional part. 96 *pos = *pos + 1 97 var scale: i64 = 100 98 while *pos < end { 99 let b: i64 = buf[*pos] 100 if b < 0x30 { break } 101 if b > 0x39 { break } 102 if scale > 0 { 103 thou = thou + (b - 0x30) * scale 104 } 105 scale = scale / 10 106 *pos = *pos + 1 107 } 108 } 109 } 110 if thou > 1000 { thou = 1000 } 111 if thou < 0 { thou = 0 } 112 return thou 113} 114 115// Parse an Accept-Language header. Fills entries[] with the 116// parsed tags. Returns count or AL_ERR_OVERFLOW. 117func accept_lang_parse(buf: *u8, n: i64, 118 entries: *AcceptLang, cap: i64) -> i64 { 119 let pos_raw: *u8 = sys_mmap(16) 120 let pos: *i64 = pos_raw as *i64 121 *pos = 0 122 123 var count: i64 = 0 124 while *pos < n { 125 *pos = al_ltrim(buf, *pos, n) 126 if *pos >= n { break } 127 128 if count >= cap { return AL_ERR_OVERFLOW } 129 let e: *AcceptLang = entries + count * 24 130 131 // Tag runs to ',', ';', or end. 132 e.tag_off = *pos 133 while *pos < n { 134 if buf[*pos] == 0x2C { break } 135 if buf[*pos] == 0x3B { break } 136 *pos = *pos + 1 137 } 138 e.tag_len = al_rtrim(buf, e.tag_off, *pos) - e.tag_off 139 140 // Optional ';q=V'. 141 e.quality_thou = 1000 142 if *pos < n { 143 if buf[*pos] == 0x3B { 144 *pos = *pos + 1 145 e.quality_thou = al_parse_q(buf, pos, n) 146 // Skip to next ',' or end. 147 while *pos < n { 148 if buf[*pos] == 0x2C { break } 149 *pos = *pos + 1 150 } 151 } 152 } 153 count = count + 1 154 155 // Skip ','. 156 if *pos < n { 157 if buf[*pos] == 0x2C { *pos = *pos + 1 } 158 } 159 } 160 return count 161} 162 163// Stable sort by quality descending. Simple insertion sort; 164// Accept-Language is typically < 10 entries. 165func accept_lang_sort(entries: *AcceptLang, count: i64) -> i64 { 166 var i: i64 = 1 167 while i < count { 168 let cur: *AcceptLang = entries + i * 24 169 let cur_q: i64 = cur.quality_thou 170 let cur_to: i64 = cur.tag_off 171 let cur_tl: i64 = cur.tag_len 172 var j: i64 = i - 1 173 while j >= 0 { 174 let p: *AcceptLang = entries + j * 24 175 if p.quality_thou >= cur_q { break } 176 let dst: *AcceptLang = entries + (j + 1) * 24 177 dst.quality_thou = p.quality_thou 178 dst.tag_off = p.tag_off 179 dst.tag_len = p.tag_len 180 j = j - 1 181 } 182 let slot: *AcceptLang = entries + (j + 1) * 24 183 slot.quality_thou = cur_q 184 slot.tag_off = cur_to 185 slot.tag_len = cur_tl 186 i = i + 1 187 } 188 return 0 189} 190 191// Compile-only smoke. 192func main() -> i64 { 193 let entries_raw: *u8 = sys_mmap(512) 194 let entries: *AcceptLang = entries_raw as *AcceptLang 195 196 let input: *u8 = "en-US,en;q=0.9,fr;q=0.8,*;q=0.1" 197 let n: i64 = accept_lang_parse(input, 31, entries, 16) 198 if n != 4 { return 1 } 199 200 // entries[0] = \"en-US\" q=1.0 201 let e0: *AcceptLang = entries 202 if e0.tag_len != 5 { return 2 } 203 if e0.quality_thou != 1000 { return 3 } 204 205 // entries[1] = \"en\" q=0.9 -> 900 206 let e1: *AcceptLang = entries + 24 207 if e1.tag_len != 2 { return 4 } 208 if e1.quality_thou != 900 { return 5 } 209 210 // entries[2] = \"fr\" q=0.8 -> 800 211 let e2: *AcceptLang = entries + 48 212 if e2.quality_thou != 800 { return 6 } 213 214 // entries[3] = \"*\" q=0.1 -> 100 215 let e3: *AcceptLang = entries + 72 216 if e3.quality_thou != 100 { return 7 } 217 218 // Sort (already sorted for this input; sanity check stability). 219 accept_lang_sort(entries, n) 220 if entries[0].quality_thou != 1000 { return 8 } 221 if entries[3].quality_thou != 100 { return 9 } 222 return 0 223}