|
| 1 | +[@@@ocaml.warning "-37"] |
| 2 | +(* values are constructed from C *) |
| 3 | + |
| 4 | +open Printf |
| 5 | +open ExtLib |
| 6 | +let ( let* ) = Result.bind |
| 7 | + |
| 8 | +(* sync urlparse_stubs.c *) |
| 9 | +type scheme = |
| 10 | + | Http |
| 11 | + | Https |
| 12 | +[@@deriving ord, eq, show] |
| 13 | + |
| 14 | +type t = { |
| 15 | + scheme : scheme; |
| 16 | + port : int; |
| 17 | + path : string list; |
| 18 | + query : (string * string list) list; |
| 19 | + fragment : string; |
| 20 | + host : string; |
| 21 | +} |
| 22 | +[@@deriving ord, eq, show] |
| 23 | + |
| 24 | +type path = string list |
| 25 | + |
| 26 | +type query = (string * string list) list |
| 27 | + |
| 28 | +let debug = show |
| 29 | + |
| 30 | +exception Malformed of string |
| 31 | + |
| 32 | +let scheme u = u.scheme |
| 33 | +let host u = u.host |
| 34 | +let port u = u.port |
| 35 | +let path u = u.path |
| 36 | +let query u = u.query |
| 37 | +let fragment u = if u.fragment <> "" then Some u.fragment else None |
| 38 | + |
| 39 | +let rec args_of_query query = |
| 40 | + match query with |
| 41 | + | [] -> Ok [] |
| 42 | + | (k, [ v ]) :: query -> |
| 43 | + let* args = args_of_query query in |
| 44 | + Ok ((k, v) :: args) |
| 45 | + | (k, _ :: _ :: _) :: _ -> Error (sprintf "key %S has multiple values" k) |
| 46 | + | (k, []) :: _ -> Error (sprintf "key %S has no value" k) |
| 47 | + |
| 48 | +let args url = url |> query |> args_of_query |
| 49 | + |
| 50 | +let query_of_args args = List.map (fun (k, v) -> k, [ v ]) args |
| 51 | + |
| 52 | +let default_port = function |
| 53 | + | Http -> 80 |
| 54 | + | Https -> 443 |
| 55 | + |
| 56 | +let default_scheme = function |
| 57 | + | 443 -> Https |
| 58 | + | _ -> Http |
| 59 | + |
| 60 | +let make ?scheme ~host ?port ?(path = []) ?(query = []) ?(fragment = "") () = |
| 61 | + let scheme, port = |
| 62 | + match scheme, port with |
| 63 | + | None, None -> Http, 80 |
| 64 | + | None, Some port -> default_scheme port, port |
| 65 | + | Some scheme, None -> scheme, default_port scheme |
| 66 | + | Some scheme, Some port -> scheme, port |
| 67 | + in |
| 68 | + if port <= 0 || port > 65535 then invalid_arg @@ sprintf "Url.make: %s port %d" host port; |
| 69 | + { scheme; host; port; path; query; fragment } |
| 70 | + |
| 71 | +let make_args ?scheme ~host ?port ?path ?args ?fragment () = |
| 72 | + make ?scheme ?port ?path ?query:(Option.map query_of_args args) ?fragment ~host () |
| 73 | + |
| 74 | +let without_path u = {u with path=[]} |
| 75 | +let without_query u = {u with query=[]} |
| 76 | +let without_fragment u = { u with fragment = "" } |
| 77 | +let without_parameters u = { u with query = []; fragment = "" } |
| 78 | + |
| 79 | +let string_of_scheme = function |
| 80 | + | Http -> "http" |
| 81 | + | Https -> "https" |
| 82 | + |
| 83 | +let decode = Web.rawurldecode |
| 84 | + |
| 85 | +let decode_plus = Web.urldecode |
| 86 | + |
| 87 | +module Re = struct |
| 88 | + |
| 89 | + open Tyre |
| 90 | + |
| 91 | + let list1 r = conv (fun (elt, li) -> elt :: li) (fun li -> List.hd li, List.tl li) (r <&> list r) |
| 92 | + |
| 93 | + module Charsets = struct |
| 94 | + open Charset |
| 95 | + |
| 96 | + let _gen_delims = set ":/?#[]@" |
| 97 | + |
| 98 | + let sub_delims = set "!$&'()*+,;=" |
| 99 | + |
| 100 | + let _reserved = _gen_delims || sub_delims |
| 101 | + |
| 102 | + let unreserved = ascii && (alpha || digit || set "-._~") |
| 103 | + |
| 104 | + let host = unreserved || sub_delims || char ':' |
| 105 | + |
| 106 | + let path = unreserved || sub_delims || char ':' || char '@' |
| 107 | + end |
| 108 | + |
| 109 | + let percent_encoded = matched_string (str "%" <* (xdigit <&> xdigit)) |
| 110 | + |
| 111 | + let charset_or_pct_enc ~allow_empty ~decode chrs = |
| 112 | + non_greedy @@ |
| 113 | + map decode @@ |
| 114 | + matched_string |
| 115 | + ( (if allow_empty then list else list1) |
| 116 | + (matched_string (charset chrs) <|> percent_encoded) |
| 117 | + ) |
| 118 | + |
| 119 | + let scheme = const Http (str "http") <|> const Https (str "https") |
| 120 | + |
| 121 | + let host = charset_or_pct_enc ~allow_empty:false ~decode Charsets.host |
| 122 | + |
| 123 | + let port = pos_int |
| 124 | + |
| 125 | + let path = non_greedy @@ list (str "/" *> charset_or_pct_enc ~allow_empty:true ~decode Charsets.path) <* opt (str "/") |
| 126 | + |
| 127 | + let query = |
| 128 | + let open Tyre in |
| 129 | + non_greedy |
| 130 | + begin |
| 131 | + separated_list ~sep:(str "&") |
| 132 | + begin |
| 133 | + let+ k = charset_or_pct_enc ~allow_empty:true ~decode Charsets.path |
| 134 | + and+ _ = str "=" |
| 135 | + and+ vs = separated_list ~sep:(str ",") (charset_or_pct_enc ~allow_empty:true ~decode:decode_plus Charsets.path) in |
| 136 | + k, vs |
| 137 | + end |
| 138 | + end |
| 139 | + |
| 140 | + let fragment = (charset_or_pct_enc ~allow_empty:true ~decode:decode_plus Charsets.path) |
| 141 | + |
| 142 | + let url = |
| 143 | + let+ scheme = opt (scheme <* str "://") |
| 144 | + and+ host |
| 145 | + and+ port = opt (str ":" *> port) |
| 146 | + and+ path |
| 147 | + and+ query = opt (str "?" *> query) |
| 148 | + and+ fragment = opt (str "#" *> fragment) |
| 149 | + in |
| 150 | + make ?scheme ~host ?port ~path ?query ?fragment () |
| 151 | +end |
| 152 | + |
| 153 | +let map_tyre_error r = |
| 154 | + r |
| 155 | + |> Result.map_error (function |
| 156 | + | `NoMatch _ -> "Invalid query string" |
| 157 | + | `ConverterFailure exn -> raise exn) |
| 158 | + |
| 159 | +let parse_re re = |
| 160 | + let re = Tyre.compile Tyre.(start *> re <* stop) in |
| 161 | + fun str -> |
| 162 | + if String.length str > 128_000 |
| 163 | + then (Error "String is longer than 128 000") |
| 164 | + else Tyre.exec re str |> map_tyre_error |
| 165 | + |
| 166 | +let to_exn parse txt = |
| 167 | + match parse txt with |
| 168 | + | Ok v -> v |
| 169 | + | Error msg -> raise (Malformed msg) |
| 170 | + |
| 171 | +let parse_query = parse_re Re.query |
| 172 | +let parse = parse_re Re.url |
| 173 | + |
| 174 | +let parse_query_exn = to_exn parse_query |
| 175 | +let parse_exn = to_exn parse |
| 176 | + |
| 177 | +let rec push_concat ~push ~sep f li = |
| 178 | + match li with |
| 179 | + | [ elt ] -> f elt |
| 180 | + | [] -> () |
| 181 | + | elt :: (_ :: _ as li) -> |
| 182 | + f elt; |
| 183 | + push sep; |
| 184 | + push_concat ~push ~sep f li |
| 185 | + |
| 186 | +let encode_plus = Web.urlencode |
| 187 | + |
| 188 | +let encode = Web.rawurlencode |
| 189 | + |
| 190 | +let push_path ~push path = |
| 191 | + push "/"; |
| 192 | + path |
| 193 | + |> List.iter begin fun segment -> |
| 194 | + push (encode segment); |
| 195 | + push "/" |
| 196 | + end |
| 197 | + |
| 198 | +let push_query ~push query = |
| 199 | + query |
| 200 | + |> push_concat ~push ~sep:"&" begin fun (k, vs) -> |
| 201 | + push (encode k); |
| 202 | + push "="; |
| 203 | + vs |> push_concat ~push ~sep:"," (fun v -> push (encode_plus v)) |
| 204 | + end |
| 205 | +let push_full_path ~push u = |
| 206 | + push_path ~push u.path; |
| 207 | + if u.query <> [] then begin |
| 208 | + push "?"; |
| 209 | + push_query ~push u.query |
| 210 | + end; |
| 211 | + if u.fragment <> "" then begin |
| 212 | + push "#"; |
| 213 | + push (encode u.fragment) |
| 214 | + end |
| 215 | + |
| 216 | +let push_url ~push u = |
| 217 | + let host = u.host in |
| 218 | + push @@ string_of_scheme u.scheme; |
| 219 | + push "://"; |
| 220 | + push host; |
| 221 | + if default_port u.scheme <> u.port then ( |
| 222 | + push ":"; |
| 223 | + push (string_of_int u.port)); |
| 224 | + push_full_path ~push u |
| 225 | + |
| 226 | +let string_of_push f a = |
| 227 | + let b = Buffer.create 256 in |
| 228 | + let push = Buffer.add_string b in |
| 229 | + f ~push a; |
| 230 | + Buffer.contents b |
| 231 | + |
| 232 | +let to_string = string_of_push push_url |
| 233 | + |
| 234 | +let full_path = string_of_push push_full_path |
| 235 | + |
| 236 | +let query_to_string = string_of_push push_query |
| 237 | + |
| 238 | +let is_root u = u.path = [] && u.query = [] && u.fragment = "" |
| 239 | + |
| 240 | +let root url = { url with path = []; query = []; fragment = "" } |
| 241 | + |
| 242 | +let hash u = Hashtbl.hash @@ to_string u |
| 243 | + |
| 244 | +let with_host url host = { url with host } |
| 245 | + |
| 246 | +let with_scheme url scheme = |
| 247 | + match scheme, url.scheme, url.port with |
| 248 | + | Http, Http, 80 | Https, Https, 443 -> url |
| 249 | + | Http, _, _ -> { url with scheme = Http; port = 80 } |
| 250 | + | Https, _, _ -> { url with scheme = Https; port = 443 } |
| 251 | + |
| 252 | +let with_query url query = { url with query } |
| 253 | + |
| 254 | +let with_path url path = { url with path } |
| 255 | + |
| 256 | +let with_fragment url fragment = { url with fragment } |
| 257 | + |
| 258 | +module Op = struct |
| 259 | + let ( / ) u segment = { u with path = u.path @ [ segment ] } |
| 260 | + |
| 261 | + let ( /? ) u args = |
| 262 | + if u.query <> [] then failwith "Query should be empty when using (/?)" else { u with query = query_of_args args } |
| 263 | +end |
| 264 | + |
| 265 | +include Op |
0 commit comments