mte/unikernel/duniverse/ocaml-uri/lib/uri.ml
2025-11-11 02:07:51 +01:00

1248 lines
38 KiB
OCaml

(*
* Copyright (c) 2012-2014 Anil Madhavapeddy <anil@recoil.org>
* Copyright (c) 2012-2014 David Sheets <sheets@alum.mit.edu>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*)
[@@@ocaml.warning "-32"]
type component = [
| `Scheme
| `Authority
| `Userinfo (* subcomponent of authority in some schemes *)
| `Host (* subcomponent of authority in some schemes *)
| `Path
| `Query
| `Query_key
| `Query_value
| `Fragment
| `Generic
| `Custom of (component * string * string) (* (component * safe chars * unsafe chars) *)
]
type pct_encoder = {
scheme: component;
userinfo: component;
host: component;
path: component;
query_key: component;
query_value: component;
fragment: component;
}
let rec iter_concat fn sep buf = function
| last::[] -> fn buf last
| el::rest ->
fn buf el;
Buffer.add_string buf sep;
iter_concat fn sep buf rest
| [] -> ()
let rev_interject e lst =
let rec aux acc = function
| [] -> acc
| x::xs -> aux (x::e::acc) xs
in match lst with
| [] -> []
| h::t -> aux [h] t
let compare_opt c t t' = match t, t' with
| None, None -> 0
| Some _, None -> 1
| None, Some _ -> -1
| Some a, Some b -> c a b
let rec compare_list f t t' = match t, t' with
| [], [] -> 0
| _::_, [] -> 1
| [], _::_ -> -1
| x::xs, y::ys ->
match f x y with 0 -> compare_list f xs ys | c -> c
(** Safe characters that are always allowed in a URI
* Unfortunately, this varies depending on which bit of the URI
* is being parsed, so there are multiple variants (and this
* set is probably not exhaustive. TODO: check.
*)
type safe_chars = bool array
module type Scheme = sig
val safe_chars_for_component : component -> safe_chars
val normalize_host : string -> string
val canonicalize_port : int option -> int option
val canonicalize_path : string list -> string list
end
module Generic : Scheme = struct
let sub_delims a =
let subd = "!$&'()*+,;=" in
for i = 0 to String.length subd - 1 do
let c = Char.code subd.[i] in
a.(c) <- true
done;
a
let safe_chars : safe_chars =
let a = Array.make 256 false in
let always_safe =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~" in
for i = 0 to String.length always_safe - 1 do
let c = Char.code always_safe.[i] in
a.(c) <- true
done;
a
let pchar : safe_chars =
let a = sub_delims (Array.copy safe_chars) in
a.(Char.code ':') <- true;
a.(Char.code '@') <- true;
a
let safe_chars_for_scheme : safe_chars =
let a = Array.copy safe_chars in
a.(Char.code '+') <- true;
a
(** Safe characters for the path component of a URI *)
let safe_chars_for_path : safe_chars =
let a = sub_delims (Array.copy pchar) in
(* delimiter: non-segment delimiting uses should be pct encoded *)
a.(Char.code '/') <- false;
a
let safe_chars_for_query : safe_chars =
(* TODO: What about {"!","$",","}? See <https://github.com/avsm/ocaml-uri/commit/1ef3f1dfb41bdb4f33f223ffe16e62a33975661a#diff-740f2de53c9eb36e9670ddfbdb9ba914R171> *)
let a = Array.copy pchar in
a.(Char.code '/') <- true;
a.(Char.code '?') <- true;
(* '&' is safe but we should encode literals to avoid ambiguity
with the already parsed qs params *)
a.(Char.code '&') <- false;
(* ';' is safe but some systems treat it like '&'. *)
a.(Char.code ';') <- false;
a.(Char.code '+') <- false;
a
let safe_chars_for_query_key : safe_chars =
let a = Array.copy safe_chars_for_query in
a.(Char.code '=') <- false;
a
let safe_chars_for_query_value : safe_chars =
let a = Array.copy safe_chars_for_query in
a.(Char.code ',') <- false;
a
let safe_chars_for_fragment : safe_chars = safe_chars_for_query
(** Safe characters for the userinfo subcomponent of a URI.
TODO: this needs more reserved characters added *)
let safe_chars_for_userinfo : safe_chars =
let a = Array.copy safe_chars in
(* delimiter: non-segment delimiting uses should be pct encoded *)
a.(Char.code ':') <- false;
a
let rec safe_chars_for_component = function
| `Path -> safe_chars_for_path
| `Userinfo -> safe_chars_for_userinfo
| `Query -> safe_chars_for_query
| `Query_key -> safe_chars_for_query_key
| `Query_value -> safe_chars_for_query_value
| `Fragment -> safe_chars_for_fragment
| `Scheme -> safe_chars_for_scheme
| `Custom ((component : component), safe, unsafe) ->
let safe_chars = Array.copy (safe_chars_for_component component) in
for i = 0 to String.length safe - 1 do
let c = Char.code safe.[i] in
safe_chars.(c) <- true
done;
for i = 0 to String.length unsafe - 1 do
let c = Char.code unsafe.[i] in
safe_chars.(c) <- false
done;
safe_chars
| `Generic
| _ -> safe_chars
let normalize_host hso = hso
let canonicalize_port port = port
let canonicalize_path path = path
end
module Http : Scheme = struct
include Generic
let normalize_host hs = String.lowercase_ascii hs
let canonicalize_port = function
| None -> None
| Some 80 -> None
| Some x -> Some x
let canonicalize_path = function
| [] -> ["/"]
| x -> x
end
module Https : Scheme = struct
include Http
let canonicalize_port = function
| None -> None
| Some 443 -> None
| Some x -> Some x
end
module File : Scheme = struct
include Generic
let normalize_host hs =
let hs = String.lowercase_ascii hs in
if hs="localhost" then "" else hs
end
module Urn : Scheme = struct
include Generic
end
let module_of_scheme = function
| Some s -> begin match String.lowercase_ascii s with
| "http" -> (module Http : Scheme)
| "https" -> (module Https : Scheme)
| "file" -> (module File : Scheme)
| "urn" -> (module Urn : Scheme)
| _ -> (module Generic : Scheme)
end
| None -> (module Generic : Scheme)
(** Portions of the URL must be converted to-and-from percent-encoding
* and this really, really shouldn't be mixed up. So this Pct module
* defines abstract Pct.encoded and Pct.decoded types which sets the
* state of the underlying string. There are functions to "cast" to
* and from these and normal strings, and this promotes a bit of
* internal safety. These types are not exposed to the external
* interface, as casting to-and-from is quite a bit of hassle and
* probably not a lot of use to the average consumer of this library
*)
module Pct : sig
type encoded
type decoded
val encode : ?scheme:string -> ?component:component -> decoded -> encoded
val decode : encoded -> decoded
(* The empty decoded string *)
val empty_decoded : decoded
(* Identity functions so we need to explicitly cast when using them below *)
val cast_encoded : string -> encoded
val cast_decoded : string -> decoded
val uncast_encoded : encoded -> string
val uncast_decoded : decoded -> string
(* Lift HOFs for maps over encodings, decodings, and strings *)
val lift_encoded : (encoded -> encoded) -> string -> string
val lift_decoded : (decoded -> decoded) -> string -> string
val unlift_encoded : (string -> string) -> encoded -> encoded
val unlift_decoded : (string -> string) -> decoded -> decoded
val unlift_decoded2 : (string -> string -> 'a) -> decoded -> decoded -> 'a
end = struct
type encoded = string
type decoded = string
let cast_encoded x = x
let cast_decoded x = x
let empty_decoded = ""
let uncast_decoded x = x
let uncast_encoded x = x
let lift_encoded f = f
let lift_decoded f = f
let unlift_encoded f = f
let unlift_decoded f = f
let unlift_decoded2 f = f
(** Scan for reserved characters and replace them with
percent-encoded equivalents.
@return a percent-encoded string *)
let encode ?scheme ?(component=`Path) b =
let module Scheme = (val (module_of_scheme scheme) : Scheme) in
let safe_chars = Scheme.safe_chars_for_component component in
let len = String.length b in
let buf = Buffer.create len in
let rec scan start cur =
if cur >= len then begin
Buffer.add_substring buf b start (cur-start);
end else begin
let c = Char.code b.[cur] in
if safe_chars.(c) then
scan start (cur+1)
else begin
if cur > start then Buffer.add_substring buf b start (cur-start);
Buffer.add_string buf (Printf.sprintf "%%%02X" c);
scan (cur+1) (cur+1)
end
end
in
scan 0 0;
Buffer.contents buf
let int_of_hex_char c =
let c = int_of_char (Char.uppercase_ascii c) - 48 in
if c > 9
then if c > 16 && c < 23
then c - 7
else failwith "int_of_hex_char"
else if c >= 0
then c
else failwith "int_of_hex_char"
(** Scan for percent-encoding and convert them into ASCII.
@return a percent-decoded string *)
let decode b =
(* TODO: Should both strict and non-strict versions be exposed? *)
let len = String.length b in
let buf = Buffer.create len in
let rec scan start cur =
if cur >= len then Buffer.add_substring buf b start (cur-start)
else if b.[cur] = '%' then begin
Buffer.add_substring buf b start (cur-start);
let cur = cur + 1 in
if cur >= len then Buffer.add_char buf '%'
else match int_of_hex_char b.[cur] with
| exception _ ->
Buffer.add_char buf '%';
scan cur cur
| highbits -> begin
let cur = cur + 1 in
if cur >= len then begin
Buffer.add_char buf '%';
Buffer.add_char buf b.[cur-1]
end else begin
let start_at =
match int_of_hex_char b.[cur] with
| lowbits ->
Buffer.add_char buf (Char.chr (highbits lsl 4 + lowbits));
cur+1
| exception _ ->
Buffer.add_char buf '%';
Buffer.add_char buf b.[cur-1];
cur
in scan start_at start_at
end
end
end else scan start (cur+1)
in
scan 0 0;
Buffer.contents buf
end
(* Percent encode a string *)
let pct_encode ?scheme ?(component=`Path) s =
Pct.(uncast_encoded (encode ?scheme ~component (cast_decoded s)))
let pct_encoder
?(scheme=`Scheme)
?(userinfo=`Userinfo)
?(host=`Host)
?(path=`Path)
?(query_key=`Query_key)
?(query_value=`Query_value)
?(fragment=`Fragment)
() =
{ scheme; userinfo; host; path; query_key; query_value; fragment }
(* Percent decode a string *)
let pct_decode s = Pct.(uncast_decoded (decode (cast_encoded s)))
(* Userinfo string handling, to and from an id * credential pair *)
module Userinfo = struct
type t = string * string option
let compare (u,p) (u',p') =
match String.compare u u' with
| 0 -> compare_opt String.compare p p'
| c -> c
let userinfo_of_encoded us =
match Stringext.split ~max:2 ~on:':' us with
| [] -> ("",None)
| [u] -> (pct_decode u,None)
| u::p::_ -> (pct_decode u,Some (pct_decode p))
let encoded_of_userinfo ?scheme ~component (u,po) =
let len = String.(
1 + (length u) + (match po with None -> 0 | Some p -> length p))
in
let buf = Buffer.create len in
Buffer.add_string buf (pct_encode ?scheme ~component u);
begin match po with None -> ();
| Some p ->
Buffer.add_char buf ':';
Buffer.add_string buf (pct_encode ?scheme ~component p)
end;
Pct.cast_encoded (Buffer.contents buf)
end
let userinfo_of_encoded = Userinfo.userinfo_of_encoded
let encoded_of_userinfo ?scheme ~component = Userinfo.encoded_of_userinfo ?scheme ~component
(* Path string handling, to and from a list of path tokens *)
module Path = struct
(* Invariant: every element is non-zero, slashes (/) only occur alone. *)
(* Yes, it's better this way. This means you can retain separator
context in recursion (e.g. remove_dot_segments for relative resolution). *)
type t = string list
let compare = compare_list String.compare
(* Make a path token list from a percent-encoded string *)
let path_of_encoded ps =
let tokl = Stringext.full_split ps ~on:'/' in
List.map pct_decode tokl
(* Subroutine for resolve <http://tools.ietf.org/html/rfc3986#section-5.2.4> *)
let remove_dot_segments p =
let revp = List.rev p in
let rec loop ascension outp = function
| "/"::".."::r | ".."::r -> loop (ascension + 1) outp r
| "/"::"."::r | "."::r -> loop ascension outp r
| "/"::[] | [] when List.(length p > 0 && hd p = "/") -> "/"::outp
| [] when ascension > 0 -> List.rev_append
("/"::(rev_interject "/" Array.(to_list (make ascension "..")))) outp
| [] -> List.(if length outp > 0 && hd outp = "/" then tl outp else outp)
| "/"::"/"::r when ascension > 0 -> loop (ascension - 1) outp ("/"::r)
| "/"::_::r when ascension > 0 -> loop (ascension - 1) outp r
| s::r -> loop 0 (s::outp) r
in loop 0 [] revp
let encoded_of_path ?scheme ~component p =
let len = List.fold_left (fun c tok -> String.length tok + c) 0 p in
let buf = Buffer.create len in
iter_concat (fun buf -> function
| "/" -> Buffer.add_char buf '/'
| seg -> Buffer.add_string buf (pct_encode ?scheme ~component seg)
) "" buf p;
Pct.cast_encoded (Buffer.contents buf)
(* Subroutine for resolve <http://tools.ietf.org/html/rfc3986#section-5.2.3> *)
let merge bhost bpath relpath =
match bhost, List.rev bpath with
| Some _, [] -> "/"::relpath
| _, ("/"::rbpath | _::"/"::rbpath) -> List.rev_append ("/"::rbpath) relpath
| _, _ -> relpath
end
let path_of_encoded = Path.path_of_encoded
let encoded_of_path ?scheme ~component = Path.encoded_of_path ?scheme ~component
(* Query string handling, to and from an assoc list of key/values *)
module Query = struct
type kv = (string * string list) list
type t =
| KV of kv
| Raw of string option * kv Lazy.t
let compare x y = match x, y with
| KV kvl, KV kvl'
| Raw (_, lazy kvl), KV kvl'
| KV kvl, Raw (_, lazy kvl') ->
compare_list (fun (k,vl) (k',vl') ->
match String.compare k k' with
| 0 -> compare_list String.compare vl vl'
| c -> c
) kvl kvl'
| Raw (raw,_), Raw (raw',_) -> compare_opt String.compare raw raw'
let find q k = try Some (List.assoc k q) with Not_found -> None
let split_query qs =
let els = Stringext.split ~on:'&' qs in
(* Replace a + in a query string with a space in-place *)
let plus_to_space s =
let s = Bytes.unsafe_of_string s in
for i = 0 to Bytes.length s - 1 do
if Bytes.get s i = '+' then Bytes.set s i ' '
done;
Bytes.unsafe_to_string s
in
let rec loop acc = function
| (k::v::_)::tl ->
let n = plus_to_space k,
(match Stringext.split ~on:',' (plus_to_space v) with
| [] -> [""] | l -> l) in
loop (n::acc) tl
| [k]::tl ->
let n = plus_to_space k, [] in
loop (n::acc) tl
| []::tl -> loop (("", [])::acc) tl
| [] -> acc
in
match els with
| [] -> ["",[]]
| els -> loop []
(List.rev_map (fun el -> Stringext.split ~on:'=' el ~max:2) els)
(* Make a query tuple list from a percent-encoded string *)
let query_of_encoded qs =
List.map
(fun (k, v) -> (pct_decode k, List.map pct_decode v))
(split_query qs)
(* Assemble a query string suitable for putting into a URI.
* Tuple inputs are percent decoded and will be encoded by
* this function.
*)
let encoded_of_query ?scheme ?(pct_encoder=pct_encoder ()) l =
let len = List.fold_left (fun a (k,v) ->
a + (String.length k)
+ (List.fold_left (fun a s -> a+(String.length s)+1) 0 v) + 2) (-1) l in
let buf = Buffer.create len in
iter_concat (fun buf (k,v) ->
Buffer.add_string buf (pct_encode ?scheme ~component:pct_encoder.query_key k);
if v <> [] then (
Buffer.add_char buf '=';
iter_concat (fun buf s ->
Buffer.add_string buf
(pct_encode ?scheme ~component:pct_encoder.query_value s)
) "," buf v)
) "&" buf l;
Buffer.contents buf
let of_raw qs =
let lazy_query = Lazy.from_fun (fun () -> query_of_encoded qs) in
Raw (Some qs, lazy_query)
let kv = function Raw (_, lazy kv) | KV kv -> kv
end
let query_of_encoded = Query.query_of_encoded
let encoded_of_query ?scheme = Query.encoded_of_query ?scheme
(* Type of the URI, with most bits being optional *)
type t = {
scheme: Pct.decoded option;
userinfo: Userinfo.t option;
host: [ `Ipv4_literal of string
| `Ipv6_literal of string
| `Host of Pct.decoded] option ;
port: int option;
path: Path.t;
query: Query.t;
fragment: Pct.decoded option;
}
let empty = {
scheme = None;
userinfo = None;
host = None;
port = None;
path = [];
query = Query.Raw (None, Lazy.from_val []);
fragment = None;
}
let compare_decoded = Pct.unlift_decoded2 String.compare
let compare_decoded_opt = compare_opt compare_decoded
let compare_host h1 h2 =
match h1, h2 with
| `Ipv4_literal ip1, `Ipv4_literal ip2 -> String.compare ip1 ip2
| `Ipv6_literal ip1, `Ipv6_literal ip2 -> String.compare ip1 ip2
| `Host h1, `Host h2 -> compare_decoded h1 h2
| _ -> -1
let compare_host_opt = compare_opt compare_host
let compare t t' =
(match compare_host_opt t.host t'.host with
| 0 -> (match compare_decoded_opt t.scheme t'.scheme with
| 0 -> (match compare_opt (fun p p' ->
if p < p' then -1 else if p > p' then 1 else 0
) t.port t'.port with
| 0 -> (match compare_opt Userinfo.compare t.userinfo t'.userinfo with
| 0 -> (match Path.compare t.path t'.path with
| 0 -> (match Query.compare t.query t'.query with
| 0 -> compare_decoded_opt t.fragment t'.fragment
| c -> c)
| c -> c)
| c -> c)
| c -> c)
| c -> c)
| c -> c)
let equal t t' = compare t t' = 0
let uncast_opt = function
| Some h -> Some (Pct.uncast_decoded h)
| None -> None
let normalize schem uri =
let module Scheme =
(val (module_of_scheme (uncast_opt schem)) : Scheme) in
let dob f = function
| Some x -> Some (Pct.unlift_decoded f x)
| None -> None
in {uri with
scheme=dob String.lowercase_ascii uri.scheme;
host= match uri.host with
| Some (`Ipv4_literal host) ->
Some (`Ipv4_literal (Scheme.normalize_host host))
| Some (`Ipv6_literal host) ->
Some (`Ipv6_literal (Scheme.normalize_host host))
| Some (`Host host) ->
Some (`Host (Pct.cast_decoded (Scheme.normalize_host (Pct.uncast_decoded host))))
| None -> None
}
(** Convert a URI structure into a percent-encoded string
<http://tools.ietf.org/html/rfc3986#section-5.3>
*)
let to_string ?(pct_encoder=pct_encoder ()) uri =
let scheme = match uri.scheme with
| Some s -> Some (Pct.uncast_decoded s)
| None -> None in
let buf = Buffer.create 128 in
(* Percent encode a decoded string and add it to the buffer *)
let add_pct_string ?(component=`Path) x =
Buffer.add_string buf (Pct.uncast_encoded (Pct.encode ?scheme ~component x))
in
(match uri.scheme with
|None -> ()
|Some x ->
add_pct_string ~component:pct_encoder.scheme x;
Buffer.add_char buf ':'
);
(* URI has a host if any host-related component is set. Defaults to "". *)
if (match uri.userinfo, uri.host, uri.port with
| Some _, _, _ | _, Some _, _ | _, _, Some _ -> true | _ -> false)
then Buffer.add_string buf "//";
(match uri.userinfo with
|None -> ()
|Some userinfo ->
Buffer.add_string buf
(Pct.uncast_encoded (encoded_of_userinfo ?scheme ~component:pct_encoder.userinfo userinfo));
Buffer.add_char buf '@'
);
(match uri.host with
|None -> ()
|Some (`Host host) ->
add_pct_string ~component:pct_encoder.host host;
|Some (`Ipv4_literal host) -> Buffer.add_string buf host
|Some (`Ipv6_literal host) ->
Buffer.add_char buf '[';
Buffer.add_string buf host;
Buffer.add_char buf ']'
);
(match uri.port with
|None -> ()
|Some port ->
Buffer.add_char buf ':';
Buffer.add_string buf (string_of_int port)
);
(match uri.path with (* Handle relative paths correctly *)
| [] -> ()
| "/"::_ ->
Buffer.add_string buf (Pct.uncast_encoded
(encoded_of_path ?scheme ~component:pct_encoder.path uri.path))
| first_segment::_ ->
(match uri.host with
| Some _ -> Buffer.add_char buf '/'
| None ->
(* ensure roundtrip by forcing relative path interpretation not scheme *)
match Stringext.find_from first_segment ~pattern:":" with
| None -> ()
| Some _ -> match scheme with
| Some _ -> ()
| None -> Buffer.add_string buf "./"
);
Buffer.add_string buf
(Pct.uncast_encoded (encoded_of_path ?scheme ~component:pct_encoder.path uri.path))
);
Query.(match uri.query with
| Raw (None,_) | KV [] -> ()
| Raw (_,lazy q) | KV q -> (* normalize e.g. percent capitalization *)
Buffer.add_char buf '?';
Buffer.add_string buf (encoded_of_query ?scheme ~pct_encoder q)
);
(match uri.fragment with
|None -> ()
|Some f -> Buffer.add_char buf '#'; add_pct_string ~component:pct_encoder.fragment f
);
Buffer.contents buf
(* Various accessor functions, as the external uri type is abstract *)
let get_decoded_opt = function None -> None |Some x -> Some (Pct.uncast_decoded x)
let scheme uri = get_decoded_opt uri.scheme
let with_scheme uri =
function
|Some scheme -> { uri with scheme=Some (Pct.cast_decoded scheme) }
|None -> { uri with scheme=None }
let host uri =
match uri.host with
| None -> None
| Some (`Ipv4_literal h | `Ipv6_literal h) -> Some h
| Some (`Host h) -> Some (Pct.uncast_decoded h)
let host_with_default ?(default="localhost") uri =
match host uri with
|None -> default
|Some h -> h
let userinfo ?(pct_encoder=pct_encoder ()) uri = match uri.userinfo with
| None -> None
| Some userinfo -> Some (Pct.uncast_encoded (match uri.scheme with
| None -> encoded_of_userinfo ~component:pct_encoder.userinfo userinfo
| Some s -> encoded_of_userinfo ~scheme:(Pct.uncast_decoded s) ~component:pct_encoder.userinfo userinfo))
let with_userinfo uri userinfo =
let userinfo = match userinfo with
| Some u -> Some (userinfo_of_encoded u)
| None -> None
in
match host uri with
| None -> { uri with host=Some (`Host (Pct.cast_decoded "")); userinfo=userinfo }
| Some _ -> { uri with userinfo=userinfo }
let user uri = match uri.userinfo with
| None -> None
| Some (user, _) -> Some user
let password uri = match uri.userinfo with
| None | Some (_, None) -> None
| Some (_, Some pass) -> Some pass
let with_password uri password =
let result userinfo = match host uri with
| None -> { uri with host=Some (`Host (Pct.cast_decoded "")); userinfo=userinfo }
| Some _ -> { uri with userinfo=userinfo }
in
match uri.userinfo, password with
| None, None -> uri
| None, Some _ -> result (Some ("",password))
| Some (user,_), _ -> result (Some (user, password))
let port uri = uri.port
let with_port uri port =
match host uri with
| Some _ -> { uri with port=port }
| None -> begin
match port with
| None -> { uri with host=None; port=None }
| Some _ -> { uri with host=Some (`Host (Pct.cast_decoded "")); port=port }
end
(* Return the path component *)
let path ?(pct_encoder=pct_encoder ()) uri = Pct.uncast_encoded (match uri.scheme with
| None -> encoded_of_path ~component:pct_encoder.path uri.path
| Some s -> encoded_of_path ~scheme:(Pct.uncast_decoded s) ~component:pct_encoder.path uri.path)
let with_path uri path =
let path = path_of_encoded path in
match host uri, path with
| None, _ | Some _, "/"::_ | Some _, [] -> { uri with path=path }
| Some _, _ -> { uri with path="/"::path }
let fragment uri = get_decoded_opt uri.fragment
let with_fragment uri =
function
|None -> { uri with fragment=None }
|Some frag -> { uri with fragment=Some (Pct.cast_decoded frag) }
let query uri = Query.kv uri.query
let verbatim_query ?(pct_encoder=pct_encoder ()) uri = Query.(match uri.query with
| Raw (qs,_) -> qs
| KV [] -> None
| KV kv -> Some (encoded_of_query ?scheme:(scheme uri) ~pct_encoder kv)
)
let get_query_param' uri k = Query.(find (kv uri.query) k)
let get_query_param uri k =
match get_query_param' uri k with
|None -> None
|Some v -> Some (String.concat "," v)
let with_query uri query = { uri with query=Query.KV query }
let q_s q = List.map (fun (k,v) -> k,[v]) q
let with_query' uri query = with_query uri (q_s query)
let add_query_param uri p = Query.({ uri with query=KV (p::(kv uri.query)) })
let add_query_param' uri (k,v) =
Query.({ uri with query=KV ((k,[v])::(kv uri.query)) })
let add_query_params uri ps = Query.({ uri with query=KV (ps@(kv uri.query)) })
let add_query_params' uri ps =
Query.({ uri with query=KV ((q_s ps)@(kv uri.query)) })
let remove_query_param uri k = Query.(
{ uri with query=KV (List.filter (fun (k',_) -> k<>k') (kv uri.query)) }
)
(* Construct encoded path and query components *)
let path_and_query uri =
match (path uri), (query uri) with
|"", [] -> "/" (* TODO: What about same document? (/) *)
|"", q -> (* TODO: What about same document? (/) *)
let scheme = uncast_opt uri.scheme in
Printf.sprintf "/?%s" (encoded_of_query ?scheme q)
|p, [] -> p
|p, q ->
let scheme = uncast_opt uri.scheme in
Printf.sprintf "%s?%s" p (encoded_of_query ?scheme q)
(* TODO: functions to add and remove from a URI *)
(* Resolve a URI wrt a base URI <http://tools.ietf.org/html/rfc3986#section-5.2> *)
let resolve schem base uri =
let schem = Some (Pct.cast_decoded (match scheme base with
| None -> schem
| Some scheme -> scheme
)) in
normalize schem
Path.(match scheme uri, userinfo uri, host uri with
| Some _, _, _ ->
{uri with path=remove_dot_segments uri.path}
| None, Some _, _
| None, _, Some _ ->
{uri with scheme=base.scheme; path=remove_dot_segments uri.path}
| None, None, None ->
let uri = {uri with scheme=base.scheme; userinfo=base.userinfo;
host=base.host; port=base.port} in
let path_str = path uri in
if path_str=""
then { uri with
path=base.path;
query=match uri.query with
| Query.Raw (None,_) | Query.KV [] -> base.query
| _ -> uri.query
}
else if path_str.[0]='/'
then {uri with path=remove_dot_segments uri.path}
else {uri with
path=remove_dot_segments (merge base.host base.path uri.path);
}
)
let canonicalize uri =
let uri = resolve "" empty uri in
let module Scheme =
(val (module_of_scheme (uncast_opt uri.scheme)) : Scheme) in
{ uri with
port=Scheme.canonicalize_port uri.port;
path=Scheme.canonicalize_path uri.path;
}
let pp ppf uri = Format.pp_print_string ppf (to_string uri)
let pp_hum ppf uri = Format.pp_print_string ppf (to_string uri)
module Parser = struct
open Angstrom
let string_of_char = String.make 1
let string_of_char_list chars =
String.concat "" (List.map string_of_char chars)
let scheme =
lift
(fun s -> Some (Pct.decode (Pct.cast_encoded s)))
(take_while (fun c -> c <> ':' && c <> '/' && c <> '?' && c <> '#')
<* char ':')
<|> return None
let is_digit = function '0' .. '9' -> true | _ -> false
let hex_digit =
satisfy (function
| '0' .. '9' | 'A' .. 'F' | 'a' .. 'f' ->
true
| _ ->
false)
let hexadecimal = lift string_of_char_list (many hex_digit)
let c_dot = char '.'
let c_at = char '@'
let c_colon = char ':'
let dec_octet =
take_while1 (function '0' .. '9' -> true | _ -> false) >>= fun num ->
if int_of_string num < 256 then
return num
else
fail "invalid octect"
let ipv4_address =
lift2
(fun three one -> String.concat "." three ^ "." ^ one)
(count 3 (dec_octet <* c_dot))
dec_octet
(* -- after double colon, IPv4 dotted notation could appear anywhere *)
let after_double_colon =
fix (fun f ->
list [ ipv4_address ]
<|> lift2 (fun x y -> x :: y) hexadecimal (c_colon *> f <|> return []))
let double_colon count =
after_double_colon >>= (fun rest ->
let filler_length = 8 - count - List.length rest in
if filler_length <= 0 then
fail "too many parts in IPv6 address"
else
return ("" :: rest))
<|> return [""]
let rec part = function
| 7 ->
(* max 8 parts in an IPv6 address *)
lift (fun x -> [ x ]) hexadecimal
| 6 ->
(* after 6 parts it could end in IPv4 dotted notation *)
list [ ipv4_address ] <|> hex_part 6
| n ->
hex_part n
and hex_part n =
lift2
(fun x y -> x :: y)
hexadecimal
(c_colon *> (c_colon *> double_colon (n + 1) <|> part (n + 1)))
let rec split_with f xs =
match xs with
| [] ->
[], []
| y :: ys ->
if f y then
let zs, ts = split_with f ys in
y :: zs, ts
else
[], xs
let ipv6 =
let format_addr segments =
let before_double_colon, after_double_colon =
split_with (fun segment -> segment <> "") segments
in
let before = String.concat ":" before_double_colon in
let res =
match after_double_colon with
| "" :: xs ->
before ^ "::" ^ String.concat ":" xs
| _ ->
before
in
res
in
lift format_addr (c_colon *> c_colon *> double_colon 0 <|> part 0)
let ipv6_address =
(char '[') *> ipv6 <* (char ']')
let pct_encoded =
lift2
(fun pct digits -> string_of_char_list (pct :: digits))
(char '%')
(count 2 hex_digit)
let sub_delims =
satisfy (function
| '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' ->
true
| _ ->
false)
let unreserved =
(* "[A-Za-z0-9-._~]" *)
satisfy (function
| 'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' | '-' | '.' | '_' | '~' ->
true
| _ ->
false)
let reg_name =
lift
(String.concat "")
(many
(choice
[ string_of_char <$> unreserved
; pct_encoded
; string_of_char <$> sub_delims
]))
let host =
choice
[ ipv4_address >>| (fun h -> `Ipv4_literal h)
; ipv6_address >>| (fun h -> `Ipv6_literal h)
; reg_name >>| (fun s -> `Host (Pct.decode (Pct.cast_encoded s)))
(* TODO(dinosaure): According to RFC3986:
host = IP-literal / IPv4address / reg-name
IP-literal = "[" ( IPv6address / IPvFuture ) "]"
IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
IPvFuture is not implemented. We should handle it:
IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
*)
]
let userinfo =
lift
(fun x ->
let s = String.concat "" x in
Some (Userinfo.userinfo_of_encoded s))
(many
(choice
[ string_of_char <$> unreserved
; pct_encoded
; string_of_char <$> sub_delims
; string_of_char <$> c_colon
])
<* c_at)
<|> return None
let port =
peek_char >>= function
| Some ':' ->
c_colon *> take_while is_digit >>| fun port ->
let decoded = Pct.decode (Pct.cast_encoded port) in
(try Some (int_of_string (Pct.uncast_decoded decoded)) with _ -> None)
| Some _ | None ->
return None
let authority =
string "//"
*> lift3
(fun userinfo host port ->
userinfo, Some host, port)
userinfo
host
port
<|> return (None, None, None)
let path =
lift
Path.path_of_encoded
(take_while (function '?' | '#' -> false | _ -> true))
let query =
lift
Query.of_raw
(char '?' *> take_till (function '#' -> true | _ -> false))
<|> return (Query.Raw (None, Lazy.from_val []))
let fragment =
lift
(fun s -> Some (Pct.decode (Pct.cast_encoded s)))
(char '#' *> take_while (fun _ -> true))
<|> return None
let _uri_reference =
lift4
(fun scheme (userinfo, host, port) path query fragment ->
normalize scheme { scheme; userinfo; host; port; path; query; fragment })
scheme
authority
path
query
<*> fragment
(* XXX(anmonteiro): For compatibility reasons with the old regex parser, we
* only parse until the first newline character and drop everything else
* after that *)
let uri_reference =
take_while (function | '\n' -> false | _ -> true) >>| fun s ->
match Angstrom.parse_string ~consume:All _uri_reference s with
| Ok t -> t
| Error _ ->
(* Shouldn't really happen if the parser is forgiving. *)
empty
end
let decode_host host =
match Angstrom.parse_string ~consume:All Parser.host host with
| Ok parsed -> parsed
| Error _ ->
match Angstrom.parse_string ~consume:All Parser.ipv6 host with
| Ok parsed -> (`Ipv6_literal parsed)
| Error _ -> (`Host (Pct.cast_decoded host))
(* Make a URI record. This is a bit more inefficient than it needs to be due to the
* casting/uncasting (which isn't fully identity due to the option box), but it is
* no big deal for now.
*)
let make ?scheme ?userinfo ?host ?port ?path ?query ?fragment () =
let decode = function
|Some x -> Some (Pct.cast_decoded x) |None -> None in
let host = match userinfo, host, port with
| _, Some _, _ | None, None, None -> host
| Some _, None, _ | _, None, Some _ -> Some ""
in
let userinfo = match userinfo with
| None -> None | Some u -> Some (userinfo_of_encoded u) in
let path = match path with
|None -> [] | Some p ->
let path = path_of_encoded p in
match host, path with
| None, _ | Some _, "/"::_ | Some _, [] -> path
| Some _, _ -> "/"::path
in
let query = match query with
| None -> Query.KV []
| Some p -> Query.KV p
in
let scheme = decode scheme in
normalize scheme
{ scheme; userinfo;
host =
(match host with
| Some host -> Some (decode_host host)
| None -> None);
port; path; query; fragment=decode fragment }
let with_host uri host =
{ uri with
host = (match host with
| Some host -> Some (decode_host host)
| None -> None)
}
let with_uri ?scheme ?userinfo ?host ?port ?path ?query ?fragment uri =
let with_path_opt u o =
match o with
| None -> with_path u ""
| Some p -> with_path u p
in
let with_query_opt u o =
match o with
| None -> with_query u []
| Some q -> with_query u q
in
let with_ f o u =
match o with
| None -> u
| Some x -> f u x
in
with_ with_scheme scheme uri
|> with_ with_userinfo userinfo
|> with_ with_host host
|> with_ with_port port
|> with_ with_path_opt path
|> with_ with_query_opt query
|> with_ with_fragment fragment
let of_string s =
(* To preserve the old regex parser's behavior, we only parse a prefix, and
* stop whenever we can't parse more. *)
match Angstrom.parse_string ~consume:Prefix Parser.uri_reference s with
| Ok t -> t
| Error _ ->
(* Shouldn't really happen if the parser is forgiving. *)
empty
module Absolute_http = struct
type uri = t
type t =
{ scheme : [ `Http | `Https ];
userinfo: Userinfo.t option;
host: [ `Ipv4_literal of string
| `Ipv6_literal of string
| `Host of Pct.decoded];
port : int option;
path : Path.t;
query : Query.t;
fragment : Pct.decoded option
}
let ( let* ) = Result.bind
let to_uri { scheme; userinfo; host; port; path; query; fragment } =
let scheme =
match scheme with
| `Http -> Pct.cast_decoded "http"
| `Https -> Pct.cast_decoded "https"
in
({ scheme = Some scheme;
userinfo;
host = Some host;
port;
path;
query;
fragment } : uri)
;;
let of_uri ({ scheme; userinfo; host; port; path; query; fragment }: uri) =
let* scheme =
match scheme with
| None -> Error (`Msg "No scheme present in URI")
| Some scheme ->
(match Pct.uncast_decoded scheme with
| "http" -> Ok `Http
| "https" -> Ok `Https
| unsupported_scheme ->
Error
(`Msg
(Printf.sprintf
"Only http and https URIs are supported. %s is invalid."
unsupported_scheme)))
in
let* host = Option.to_result ~none:(`Msg "host is required for HTTP(S) uris") host in
Ok { scheme; userinfo; host; port; path; query; fragment }
;;
let of_string s = match of_string s |> of_uri with
| Ok t -> t
| Error (`Msg error) -> failwith error
let to_string ?pct_encoder t = to_uri t |> to_string ?pct_encoder
let normalize t =
{ t with
host = match t.host with
| (`Ipv4_literal host) ->
(`Ipv4_literal (String.lowercase_ascii host))
| (`Ipv6_literal host) ->
(`Ipv6_literal (String.lowercase_ascii host))
| (`Host host) ->
(`Host (Pct.cast_decoded (String.lowercase_ascii (Pct.uncast_decoded host))))
}
let make ~scheme ~host ?userinfo ?port ?path ?query ?fragment () =
let decode = function
|Some x -> Some (Pct.cast_decoded x) |None -> None in
let userinfo = match userinfo with
| None -> None | Some u -> Some (userinfo_of_encoded u) in
let path = match path with
|None -> [] | Some p ->
let path = path_of_encoded p in
match path with
| "/"::_ | [] -> path
| _ -> "/"::path
in
let query = match query with
| None -> Query.KV []
| Some p -> Query.KV p
in
normalize
{ scheme;
userinfo;
host= decode_host host; port; path; query; fragment=decode fragment }
let host t =
match t.host with
| (`Ipv4_literal h | `Ipv6_literal h) -> h
| (`Host h) -> (Pct.uncast_decoded h)
let scheme t = t.scheme
end