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}