code wiki / (root) / accept_lang.nx

accept_lang.nx source

↩ module page · 217 lines · 6683 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 28import "syscalls.nx" 29 30const AL_ERR_OVERFLOW: i64 = -1 31 32struct AcceptLang { 33 tag_off: i64, tag_len: i64, 34 quality_thou: i64, // 0..1000 35} 36 37// Skip whitespace. 38func al_is_ws(b: i64) -> i64 { 39 if b == 0x20 { return 1 } 40 if b == 0x09 { return 1 } 41 return 0 42} 43 44func al_ltrim(buf: *u8, off: i64, end: i64) -> i64 { 45 var i: i64 = off 46 while i < end { 47 if al_is_ws(buf[i]) == 0 { return i } 48 i = i + 1 49 } 50 return end 51} 52 53func al_rtrim(buf: *u8, start: i64, end: i64) -> i64 { 54 var e: i64 = end 55 while e > start { 56 if al_is_ws(buf[e - 1]) == 0 { return e } 57 e = e - 1 58 } 59 return e 60} 61 62// Parse a q=V.VVV value starting at *pos. Returns thousandths. 63// Handles q=1, q=1.0, q=0.5, q=0.123, q=0. 64func al_parse_q(buf: *u8, pos: *i64, end: i64) -> i64 { 65 // Skip optional whitespace + 'q'. 66 while *pos < end { 67 if al_is_ws(buf[*pos]) == 0 { break } 68 *pos = *pos + 1 69 } 70 if *pos >= end { return 1000 } 71 if buf[*pos] != 0x71 { return 1000 } // 'q' 72 *pos = *pos + 1 73 if *pos >= end { return 1000 } 74 if buf[*pos] != 0x3D { return 1000 } // '=' 75 *pos = *pos + 1 76 77 // Parse integer part. 78 var int_part: i64 = 0 79 while *pos < end { 80 let b: i64 = buf[*pos] 81 if b < 0x30 { break } 82 if b > 0x39 { break } 83 int_part = int_part * 10 + (b - 0x30) 84 *pos = *pos + 1 85 } 86 var thou: i64 = int_part * 1000 87 if *pos < end { 88 if buf[*pos] == 0x2E { 89 // Fractional part. 90 *pos = *pos + 1 91 var scale: i64 = 100 92 while *pos < end { 93 let b: i64 = buf[*pos] 94 if b < 0x30 { break } 95 if b > 0x39 { break } 96 if scale > 0 { 97 thou = thou + (b - 0x30) * scale 98 } 99 scale = scale / 10 100 *pos = *pos + 1 101 } 102 } 103 } 104 if thou > 1000 { thou = 1000 } 105 if thou < 0 { thou = 0 } 106 return thou 107} 108 109// Parse an Accept-Language header. Fills entries[] with the 110// parsed tags. Returns count or AL_ERR_OVERFLOW. 111func accept_lang_parse(buf: *u8, n: i64, 112 entries: *AcceptLang, cap: i64) -> i64 { 113 let pos_raw: *u8 = sys_mmap(16) 114 let pos: *i64 = pos_raw as *i64 115 *pos = 0 116 117 var count: i64 = 0 118 while *pos < n { 119 *pos = al_ltrim(buf, *pos, n) 120 if *pos >= n { break } 121 122 if count >= cap { return AL_ERR_OVERFLOW } 123 let e: *AcceptLang = entries + count * 24 124 125 // Tag runs to ',', ';', or end. 126 e.tag_off = *pos 127 while *pos < n { 128 if buf[*pos] == 0x2C { break } 129 if buf[*pos] == 0x3B { break } 130 *pos = *pos + 1 131 } 132 e.tag_len = al_rtrim(buf, e.tag_off, *pos) - e.tag_off 133 134 // Optional ';q=V'. 135 e.quality_thou = 1000 136 if *pos < n { 137 if buf[*pos] == 0x3B { 138 *pos = *pos + 1 139 e.quality_thou = al_parse_q(buf, pos, n) 140 // Skip to next ',' or end. 141 while *pos < n { 142 if buf[*pos] == 0x2C { break } 143 *pos = *pos + 1 144 } 145 } 146 } 147 count = count + 1 148 149 // Skip ','. 150 if *pos < n { 151 if buf[*pos] == 0x2C { *pos = *pos + 1 } 152 } 153 } 154 return count 155} 156 157// Stable sort by quality descending. Simple insertion sort; 158// Accept-Language is typically < 10 entries. 159func accept_lang_sort(entries: *AcceptLang, count: i64) -> i64 { 160 var i: i64 = 1 161 while i < count { 162 let cur: *AcceptLang = entries + i * 24 163 let cur_q: i64 = cur.quality_thou 164 let cur_to: i64 = cur.tag_off 165 let cur_tl: i64 = cur.tag_len 166 var j: i64 = i - 1 167 while j >= 0 { 168 let p: *AcceptLang = entries + j * 24 169 if p.quality_thou >= cur_q { break } 170 let dst: *AcceptLang = entries + (j + 1) * 24 171 dst.quality_thou = p.quality_thou 172 dst.tag_off = p.tag_off 173 dst.tag_len = p.tag_len 174 j = j - 1 175 } 176 let slot: *AcceptLang = entries + (j + 1) * 24 177 slot.quality_thou = cur_q 178 slot.tag_off = cur_to 179 slot.tag_len = cur_tl 180 i = i + 1 181 } 182 return 0 183} 184 185// Compile-only smoke. 186func main() -> i64 { 187 let entries_raw: *u8 = sys_mmap(512) 188 let entries: *AcceptLang = entries_raw as *AcceptLang 189 190 let input: *u8 = "en-US,en;q=0.9,fr;q=0.8,*;q=0.1" 191 let n: i64 = accept_lang_parse(input, 31, entries, 16) 192 if n != 4 { return 1 } 193 194 // entries[0] = \"en-US\" q=1.0 195 let e0: *AcceptLang = entries 196 if e0.tag_len != 5 { return 2 } 197 if e0.quality_thou != 1000 { return 3 } 198 199 // entries[1] = \"en\" q=0.9 -> 900 200 let e1: *AcceptLang = entries + 24 201 if e1.tag_len != 2 { return 4 } 202 if e1.quality_thou != 900 { return 5 } 203 204 // entries[2] = \"fr\" q=0.8 -> 800 205 let e2: *AcceptLang = entries + 48 206 if e2.quality_thou != 800 { return 6 } 207 208 // entries[3] = \"*\" q=0.1 -> 100 209 let e3: *AcceptLang = entries + 72 210 if e3.quality_thou != 100 { return 7 } 211 212 // Sort (already sorted for this input; sanity check stability). 213 accept_lang_sort(entries, n) 214 if entries[0].quality_thou != 1000 { return 8 } 215 if entries[3].quality_thou != 100 { return 9 } 216 return 0 217}