range_header.nx source
↩ module page · 191 lines · 6497 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
28import "syscalls.nx"
29
30const RH_ERR_FORMAT: i64 = -1
31const RH_ERR_UNIT: i64 = -2
32const RH_ERR_OVERFLOW: i64 = -3
33
34struct RangeSpec {
35 start: i64,
36 end: i64, // inclusive (RFC 7233 §2.1)
37}
38
39// Is byte a decimal digit?
40func rh_is_digit(b: i64) -> i64 {
41 if b < 0x30 { return 0 }
42 if b > 0x39 { return 0 }
43 return 1
44}
45
46// Parse a decimal u64 from buf[off..end]. Writes new offset into
47// *off. Returns value or -1 if no digits.
48func rh_parse_uint(buf: *u8, off: *i64, end: i64) -> i64 {
49 let start: i64 = *off
50 var v: i64 = 0
51 while *off < end {
52 if rh_is_digit(buf[*off]) == 0 { break }
53 v = v * 10 + (buf[*off] - 0x30)
54 *off = *off + 1
55 }
56 if *off == start { return -1 }
57 return v
58}
59
60// Parse a Range header value. `total_size` is the resource's
61// size in bytes -- used to resolve suffix ranges like \"-500\" and
62// to clamp. Fills ranges[] + returns count parsed, or negative.
63func range_parse(buf: *u8, n: i64, total_size: i64,
64 ranges: *RangeSpec, cap: i64) -> i64 {
65 if n < 6 { return RH_ERR_FORMAT } // \"bytes=\" minimum
66 // Must start with \"bytes=\".
67 if buf[0] != 0x62 { return RH_ERR_UNIT } // 'b'
68 if buf[1] != 0x79 { return RH_ERR_UNIT } // 'y'
69 if buf[2] != 0x74 { return RH_ERR_UNIT }
70 if buf[3] != 0x65 { return RH_ERR_UNIT }
71 if buf[4] != 0x73 { return RH_ERR_UNIT }
72 if buf[5] != 0x3D { return RH_ERR_FORMAT } // '='
73
74 var count: i64 = 0
75 let pos_raw: *u8 = sys_mmap(16)
76 let pos: *i64 = pos_raw as *i64
77 *pos = 6
78
79 while *pos < n {
80 // Skip whitespace.
81 while *pos < n {
82 if buf[*pos] != 0x20 { break }
83 *pos = *pos + 1
84 }
85 if *pos >= n { break }
86
87 if count >= cap { return RH_ERR_OVERFLOW }
88 let r: *RangeSpec = ranges + count * 16
89
90 var start_val: i64 = -1
91 var end_val: i64 = -1
92
93 // If it starts with '-' it's a suffix range (-500 = last 500).
94 if buf[*pos] == 0x2D {
95 *pos = *pos + 1
96 let suffix: i64 = rh_parse_uint(buf, pos, n)
97 if suffix < 0 { return RH_ERR_FORMAT }
98 if suffix == 0 {
99 // Degenerate. Skip.
100 if *pos < n {
101 if buf[*pos] == 0x2C { *pos = *pos + 1 }
102 }
103 continue
104 }
105 var s: i64 = total_size - suffix
106 if s < 0 { s = 0 }
107 start_val = s
108 end_val = total_size - 1
109 } else {
110 // Explicit start.
111 let s: i64 = rh_parse_uint(buf, pos, n)
112 if s < 0 { return RH_ERR_FORMAT }
113 if *pos >= n { return RH_ERR_FORMAT }
114 if buf[*pos] != 0x2D { return RH_ERR_FORMAT }
115 *pos = *pos + 1
116 // Optional end.
117 if *pos >= n {
118 start_val = s
119 end_val = total_size - 1
120 } else {
121 if buf[*pos] == 0x2C {
122 start_val = s
123 end_val = total_size - 1
124 } else {
125 let e: i64 = rh_parse_uint(buf, pos, n)
126 if e < 0 { return RH_ERR_FORMAT }
127 start_val = s
128 end_val = e
129 }
130 }
131 }
132
133 // Clamp end to size - 1.
134 if end_val > total_size - 1 { end_val = total_size - 1 }
135 // If range is out of bounds entirely, skip.
136 if start_val > end_val {
137 if *pos < n {
138 if buf[*pos] == 0x2C { *pos = *pos + 1 }
139 }
140 continue
141 }
142
143 r.start = start_val
144 r.end = end_val
145 count = count + 1
146
147 // Consume ',' separator.
148 if *pos < n {
149 if buf[*pos] == 0x2C { *pos = *pos + 1 }
150 }
151 }
152 if count == 0 { return RH_ERR_FORMAT }
153 return count
154}
155
156// Compile-only smoke.
157func main() -> i64 {
158 let ranges_raw: *u8 = sys_mmap(256)
159 let ranges: *RangeSpec = ranges_raw as *RangeSpec
160
161 // \"bytes=0-499\" against 1000-byte resource.
162 let n1: i64 = range_parse("bytes=0-499", 11, 1000, ranges, 16)
163 if n1 != 1 { return 1 }
164 if ranges[0].start != 0 { return 2 }
165 if ranges[0].end != 499 { return 3 }
166
167 // \"bytes=500-\" against 1000-byte resource -> (500, 999).
168 let n2: i64 = range_parse("bytes=500-", 10, 1000, ranges, 16)
169 if n2 != 1 { return 4 }
170 if ranges[0].start != 500 { return 5 }
171 if ranges[0].end != 999 { return 6 }
172
173 // \"bytes=-500\" against 1000-byte resource -> (500, 999).
174 let n3: i64 = range_parse("bytes=-500", 10, 1000, ranges, 16)
175 if n3 != 1 { return 7 }
176 if ranges[0].start != 500 { return 8 }
177 if ranges[0].end != 999 { return 9 }
178
179 // Multiple ranges: \"bytes=0-9,20-29\".
180 let n4: i64 = range_parse("bytes=0-9,20-29", 15, 1000, ranges, 16)
181 if n4 != 2 { return 10 }
182 if ranges[0].end != 9 { return 11 }
183 if ranges[1].start != 20 { return 12 }
184 if ranges[1].end != 29 { return 13 }
185
186 // Wrong unit rejected.
187 if range_parse("chars=0-", 8, 1000, ranges, 16) != RH_ERR_UNIT {
188 return 14
189 }
190 return 0
191}