An OCaml webserver, but the allocating version (vs httpz which doesnt)
at main 291 lines 9.5 kB view raw
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;;