An OCaml webserver, but the allocating version (vs httpz which doesnt)
1(* range.ml - Range request parsing and Content-Range response writing per RFC 7233 *)
2
3open Base
4
5(* Byte range specification *)
6type byte_range =
7 { kind : int (* 0=Range, 1=Suffix, 2=Open *)
8 ; start : int64
9 ; end_ : int64
10 }
11
12(* Kind constants - internal *)
13let kind_range = 0
14let kind_suffix = 1
15let kind_open = 2
16
17(* Maximum ranges to parse *)
18let max_ranges = 16
19
20let empty = { kind = 0; start = 0L; end_ = 0L }
21
22(* Query functions *)
23let is_range r = r.kind = kind_range
24let is_suffix r = r.kind = kind_suffix
25let is_open r = r.kind = kind_open
26
27type parse_status =
28 | Valid
29 | Invalid
30
31(* Resolved byte range *)
32type resolved =
33 { start : int64
34 ; end_ : int64
35 ; length : int64
36 }
37
38let empty_resolved = { start = 0L; end_ = 0L; length = 0L }
39
40type eval_result =
41 | Full_content
42 | Single_range
43 | Multiple_ranges
44 | Not_satisfiable
45
46(* Skip whitespace *)
47let skip_ws buf ~pos ~len =
48 let p = ref pos in
49 while !p < len && (
50 let c = Base_bigstring.unsafe_get buf !p in
51 Char.equal c ' ' || Char.equal c '\t'
52 ) do
53 Int.incr p
54 done;
55 !p
56;;
57
58(* Parse a non-negative int64, returns (value, end_pos, valid) *)
59let parse_int64 buf ~pos ~len =
60 let start = pos in
61 let p = ref pos in
62 let acc = ref 0L in
63 let valid = ref true in
64 while !valid && !p < len do
65 let c = Base_bigstring.unsafe_get buf !p in
66 if Char.is_digit c then (
67 let digit = Int64.of_int (Char.to_int c - 48) in
68 acc := Int64.(!acc * 10L + digit);
69 Int.incr p
70 ) else
71 valid := false
72 done;
73 if !p > start then (!acc, !p, true) else (0L, pos, false)
74;;
75
76(* Parse a single range-spec *)
77let parse_range_spec buf ~pos ~len =
78 let pos = skip_ws buf ~pos ~len in
79 if pos >= len then (false, empty, pos)
80 else
81 let c = Base_bigstring.unsafe_get buf pos in
82 if Char.equal c '-' then
83 (* Suffix range: -500 *)
84 let (suffix, end_pos, valid) = parse_int64 buf ~pos:(pos + 1) ~len in
85 if (not valid) || Int64.(suffix = 0L) then
86 (false, empty, end_pos)
87 else
88 (true, { kind = kind_suffix; start = suffix; end_ = 0L }, end_pos)
89 else
90 (* Start-end or start- *)
91 let (start, after_start, valid) = parse_int64 buf ~pos ~len in
92 if not valid then (false, empty, after_start)
93 else if after_start >= len then (false, empty, after_start)
94 else if not (Char.equal (Base_bigstring.unsafe_get buf after_start) '-') then
95 (false, empty, after_start)
96 else
97 let after_dash = after_start + 1 in
98 if after_dash >= len || (
99 let c = Base_bigstring.unsafe_get buf after_dash in
100 Char.equal c ',' || Char.equal c ' ' || Char.equal c '\t'
101 ) then
102 (* Open range: start- *)
103 (true, { kind = kind_open; start; end_ = 0L }, after_dash)
104 else
105 (* Closed range: start-end *)
106 let (end_val, end_pos, end_valid) = parse_int64 buf ~pos:after_dash ~len in
107 if (not end_valid) || Int64.(end_val < start) then
108 (false, empty, end_pos)
109 else
110 (true, { kind = kind_range; start; end_ = end_val }, end_pos)
111;;
112
113(* Parse Range header into array - internal implementation working on buffer region *)
114let parse_region buf ~off ~len (ranges : byte_range array) =
115 let end_pos = off + len in
116 (* Look for "=" to split unit and ranges *)
117 let eq_pos = ref off in
118 while !eq_pos < end_pos && not (Char.equal (Base_bigstring.unsafe_get buf !eq_pos) '=') do
119 Int.incr eq_pos
120 done;
121 if !eq_pos >= end_pos then (Invalid, 0)
122 else
123 (* Check for "bytes" unit *)
124 let unit_len = !eq_pos - off in
125 if unit_len <> 5 then (Invalid, 0)
126 else
127 let is_bytes =
128 let c0 = Base_bigstring.unsafe_get buf off in
129 let c1 = Base_bigstring.unsafe_get buf (off + 1) in
130 let c2 = Base_bigstring.unsafe_get buf (off + 2) in
131 let c3 = Base_bigstring.unsafe_get buf (off + 3) in
132 let c4 = Base_bigstring.unsafe_get buf (off + 4) in
133 (Char.equal c0 'b' || Char.equal c0 'B') &&
134 (Char.equal c1 'y' || Char.equal c1 'Y') &&
135 (Char.equal c2 't' || Char.equal c2 'T') &&
136 (Char.equal c3 'e' || Char.equal c3 'E') &&
137 (Char.equal c4 's' || Char.equal c4 'S')
138 in
139 if not is_bytes then (Invalid, 0)
140 else
141 (* Parse comma-separated range specs *)
142 let pos = ref (!eq_pos + 1) in
143 let count = ref 0 in
144 let valid = ref true in
145 while !valid && !pos < end_pos && !count < max_ranges do
146 pos := skip_ws buf ~pos:!pos ~len:end_pos;
147 if !pos >= end_pos then
148 valid := false
149 else
150 let (ok, range, after_range) = parse_range_spec buf ~pos:!pos ~len:end_pos in
151 if ok then (
152 Array.unsafe_set ranges !count range;
153 Int.incr count
154 );
155 pos := skip_ws buf ~pos:after_range ~len:end_pos;
156 if !pos < end_pos then
157 if Char.equal (Base_bigstring.unsafe_get buf !pos) ',' then
158 Int.incr pos
159 else
160 valid := false
161 done;
162 if !count > 0 then (Valid, !count) else (Invalid, 0)
163;;
164
165(* Parse Range header into array - from buffer and span *)
166let parse buf (sp : Span.t) (ranges : byte_range array) =
167 parse_region buf ~off:(Span.off sp) ~len:(Span.len sp) ranges
168;;
169
170(* Parse Range header from string - creates local buffer *)
171let parse_string (s : string) (ranges : byte_range array) =
172 let len = String.length s in
173 let buf = Base_bigstring.create len in
174 for i = 0 to len - 1 do
175 Base_bigstring.unsafe_set buf i (String.unsafe_get s i)
176 done;
177 (* Bind result to prevent tail call - local buf must stay in scope *)
178 let (status, count) = parse_region buf ~off:0 ~len ranges in
179 (status, count)
180;;
181
182(* Resolve a single range *)
183let resolve_range range ~resource_length =
184 let res_len = resource_length in
185 if Int64.(res_len <= 0L) then (false, empty_resolved)
186 else
187 let kind = range.kind in
188 let start_val = range.start in
189 let end_val = range.end_ in
190 if kind = kind_range then
191 (* Range: start-end *)
192 if Int64.(start_val >= res_len) then (false, empty_resolved)
193 else
194 let end_clamped = Int64.min end_val Int64.(res_len - 1L) in
195 let length = Int64.(end_clamped - start_val + 1L) in
196 (true, { start = start_val; end_ = end_clamped; length })
197 else if kind = kind_suffix then
198 (* Suffix: -N (last N bytes) *)
199 let suffix = start_val in (* stored in start field *)
200 if Int64.(suffix <= 0L) then (false, empty_resolved)
201 else
202 let start = Int64.max 0L Int64.(res_len - suffix) in
203 let end_ = Int64.(res_len - 1L) in
204 let length = Int64.(end_ - start + 1L) in
205 (true, { start; end_; length })
206 else
207 (* Open: start- *)
208 if Int64.(start_val >= res_len) then (false, empty_resolved)
209 else
210 let end_ = Int64.(res_len - 1L) in
211 let length = Int64.(end_ - start_val + 1L) in
212 (true, { start = start_val; end_; length })
213;;
214
215(* Evaluate ranges *)
216let evaluate (ranges : byte_range array) ~count ~resource_length (out : resolved array) =
217 if count = 0 then (Full_content, 0)
218 else
219 let resolved_count = ref 0 in
220 for i = 0 to count - 1 do
221 let (valid, r) = resolve_range (Array.unsafe_get ranges i) ~resource_length in
222 if valid then (
223 Array.unsafe_set out !resolved_count r;
224 Int.incr resolved_count
225 )
226 done;
227 if !resolved_count = 0 then (Not_satisfiable, 0)
228 else if !resolved_count = 1 then (Single_range, 1)
229 else (Multiple_ranges, !resolved_count)
230;;
231
232(* Response writing helpers *)
233
234let write_accept_ranges dst ~off =
235 let off = Buf_write.string dst ~off "Accept-Ranges: bytes" in
236 Buf_write.crlf dst ~off
237;;
238
239let write_accept_ranges_none dst ~off =
240 let off = Buf_write.string dst ~off "Accept-Ranges: none" in
241 Buf_write.crlf dst ~off
242;;
243
244(* Content-Range: bytes start-end/total *)
245let write_content_range dst ~off ~start ~end_ ~total =
246 let off = Buf_write.string dst ~off "Content-Range: bytes " in
247 let off = Buf_write.int64 dst ~off start in
248 let off = Buf_write.char dst ~off '-' in
249 let off = Buf_write.int64 dst ~off end_ in
250 let off = Buf_write.char dst ~off '/' in
251 let off = Buf_write.int64 dst ~off total in
252 Buf_write.crlf dst ~off
253;;
254
255let write_content_range_resolved dst ~off r ~total =
256 write_content_range dst ~off ~start:r.start ~end_:r.end_ ~total
257;;
258
259(* Content-Range: bytes * /total (for 416 responses) *)
260let write_content_range_unsatisfiable dst ~off ~total =
261 let off = Buf_write.string dst ~off "Content-Range: bytes */" in
262 let off = Buf_write.int64 dst ~off total in
263 Buf_write.crlf dst ~off
264;;
265
266(* Multipart helpers *)
267
268let write_multipart_boundary dst ~off ~boundary =
269 let off = Buf_write.string dst ~off "--" in
270 let off = Buf_write.string dst ~off boundary in
271 Buf_write.crlf dst ~off
272;;
273
274let write_multipart_final dst ~off ~boundary =
275 let off = Buf_write.string dst ~off "--" in
276 let off = Buf_write.string dst ~off boundary in
277 let off = Buf_write.string dst ~off "--" in
278 Buf_write.crlf dst ~off
279;;
280
281(* Generate a random boundary string *)
282let generate_boundary () =
283 let chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" in
284 let len = 24 in
285 let buf = Bytes.create len in
286 for i = 0 to len - 1 do
287 let idx = Random.int (String.length chars) in
288 Bytes.set buf i (String.get chars idx)
289 done;
290 Bytes.to_string buf
291;;