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}