nx_range_header.nx source
↩ module page · 198 lines · 6601 B
1// range_header.nx -- parse HTTP Range request header.
2//
3// RFC 7233 §3.1. Range requests let a client ask for a byte
4// range of a resource instead of the whole thing. Essential for:
5// - Video / audio seeking (HTML5 <video> sends Range requests
6// when you click the scrubber)
7// - Resumable downloads (browsers retry interrupted downloads
8// with \"Range: bytes=<last-byte-received>-\")
9// - Large-file diff tools
10//
11// Format (most useful forms):
12// Range: bytes=0-499 <- first 500 bytes
13// Range: bytes=500- <- byte 500 to end
14// Range: bytes=-500 <- last 500 bytes
15// Range: bytes=0-499,1000-1499 <- multiple ranges (multipart)
16//
17// We only support bytes= unit; other units (chars=, seconds=
18// in some video specs) are non-standard and rare.
19//
20// Invariants:
21// RH1 Returns RangeSpec array with (start, end) resolved vs
22// the file size. "-500" -> (size-500, size-1).
23// "500-" -> (500, size-1).
24// RH2 Ranges that extend past the file are clamped.
25// RH3 Empty / unparseable input returns RH_ERR_FORMAT so
26// caller falls back to serving 200 with full body.
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"
35const RH_MAGIC_7233: i64 = 7233
36
37const RH_ERR_FORMAT: i64 = -1
38const RH_ERR_UNIT: i64 = -2
39const RH_ERR_OVERFLOW: i64 = -3
40
41struct RangeSpec {
42 start: i64,
43 end: i64, // inclusive (RFC RH_MAGIC_7233 §2.1)
44}
45
46// Is byte a decimal digit?
47func rh_is_digit(b: i64) -> i64 {
48 if b < 0x30 { return 0 }
49 if b > 0x39 { return 0 }
50 return 1
51}
52
53// Parse a decimal u64 from buf[off..end]. Writes new offset into
54// *off. Returns value or -1 if no digits.
55func rh_parse_uint(buf: *u8, off: *i64, end: i64) -> i64 {
56 let start: i64 = *off
57 var v: i64 = 0
58 while *off < end {
59 if rh_is_digit(buf[*off]) == 0 { break }
60 v = v * 10 + (buf[*off] - 0x30)
61 *off = *off + 1
62 }
63 if *off == start { return -1 }
64 return v
65}
66
67// Parse a Range header value. `total_size` is the resource's
68// size in bytes -- used to resolve suffix ranges like \"-500\" and
69// to clamp. Fills ranges[] + returns count parsed, or negative.
70func range_parse(buf: *u8, n: i64, total_size: i64,
71 ranges: *RangeSpec, cap: i64) -> i64 {
72 if n < 6 { return RH_ERR_FORMAT } // \"bytes=\" minimum
73 // Must start with \"bytes=\".
74 if buf[0] != 0x62 { return RH_ERR_UNIT } // 'b'
75 if buf[1] != 0x79 { return RH_ERR_UNIT } // 'y'
76 if buf[2] != 0x74 { return RH_ERR_UNIT }
77 if buf[3] != 0x65 { return RH_ERR_UNIT }
78 if buf[4] != 0x73 { return RH_ERR_UNIT }
79 if buf[5] != 0x3D { return RH_ERR_FORMAT } // '='
80
81 var count: i64 = 0
82 let pos_raw: *u8 = sys_mmap(16)
83 let pos: *i64 = pos_raw as *i64
84 *pos = 6
85
86 while *pos < n {
87 // Skip whitespace.
88 while *pos < n {
89 if buf[*pos] != 0x20 { break }
90 *pos = *pos + 1
91 }
92 if *pos >= n { break }
93
94 if count >= cap { return RH_ERR_OVERFLOW }
95 let r: *RangeSpec = ranges + count * 16
96
97 var start_val: i64 = -1
98 var end_val: i64 = -1
99
100 // If it starts with '-' it's a suffix range (-500 = last 500).
101 if buf[*pos] == 0x2D {
102 *pos = *pos + 1
103 let suffix: i64 = rh_parse_uint(buf, pos, n)
104 if suffix < 0 { return RH_ERR_FORMAT }
105 if suffix == 0 {
106 // Degenerate. Skip.
107 if *pos < n {
108 if buf[*pos] == 0x2C { *pos = *pos + 1 }
109 }
110 continue
111 }
112 var s: i64 = total_size - suffix
113 if s < 0 { s = 0 }
114 start_val = s
115 end_val = total_size - 1
116 } else {
117 // Explicit start.
118 let s: i64 = rh_parse_uint(buf, pos, n)
119 if s < 0 { return RH_ERR_FORMAT }
120 if *pos >= n { return RH_ERR_FORMAT }
121 if buf[*pos] != 0x2D { return RH_ERR_FORMAT }
122 *pos = *pos + 1
123 // Optional end.
124 if *pos >= n {
125 start_val = s
126 end_val = total_size - 1
127 } else {
128 if buf[*pos] == 0x2C {
129 start_val = s
130 end_val = total_size - 1
131 } else {
132 let e: i64 = rh_parse_uint(buf, pos, n)
133 if e < 0 { return RH_ERR_FORMAT }
134 start_val = s
135 end_val = e
136 }
137 }
138 }
139
140 // Clamp end to size - 1.
141 if end_val > total_size - 1 { end_val = total_size - 1 }
142 // If range is out of bounds entirely, skip.
143 if start_val > end_val {
144 if *pos < n {
145 if buf[*pos] == 0x2C { *pos = *pos + 1 }
146 }
147 continue
148 }
149
150 r.start = start_val
151 r.end = end_val
152 count = count + 1
153
154 // Consume ',' separator.
155 if *pos < n {
156 if buf[*pos] == 0x2C { *pos = *pos + 1 }
157 }
158 }
159 if count == 0 { return RH_ERR_FORMAT }
160 return count
161}
162
163// Compile-only smoke.
164func main() -> i64 {
165 let ranges_raw: *u8 = sys_mmap(256)
166 let ranges: *RangeSpec = ranges_raw as *RangeSpec
167
168 // \"bytes=0-499\" against 1000-byte resource.
169 let n1: i64 = range_parse("bytes=0-499", 11, 1000, ranges, 16)
170 if n1 != 1 { return 1 }
171 if ranges[0].start != 0 { return 2 }
172 if ranges[0].end != 499 { return 3 }
173
174 // \"bytes=500-\" against 1000-byte resource -> (500, 999).
175 let n2: i64 = range_parse("bytes=500-", 10, 1000, ranges, 16)
176 if n2 != 1 { return 4 }
177 if ranges[0].start != 500 { return 5 }
178 if ranges[0].end != 999 { return 6 }
179
180 // \"bytes=-500\" against 1000-byte resource -> (500, 999).
181 let n3: i64 = range_parse("bytes=-500", 10, 1000, ranges, 16)
182 if n3 != 1 { return 7 }
183 if ranges[0].start != 500 { return 8 }
184 if ranges[0].end != 999 { return 9 }
185
186 // Multiple ranges: \"bytes=0-9,20-29\".
187 let n4: i64 = range_parse("bytes=0-9,20-29", 15, 1000, ranges, 16)
188 if n4 != 2 { return 10 }
189 if ranges[0].end != 9 { return 11 }
190 if ranges[1].start != 20 { return 12 }
191 if ranges[1].end != 29 { return 13 }
192
193 // Wrong unit rejected.
194 if range_parse("chars=0-", 8, 1000, ranges, 16) != RH_ERR_UNIT {
195 return 14
196 }
197 return 0
198}