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

1000 lines
32 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"]
module Uri_re = struct
open Re
module Raw = struct
let (+) a b = seq [a;b]
let (/) a b = alt [a;b]
let gen_delims = Posix.re "[:/?#\\[\\]@]"
let sub_delims = Posix.re "[!$&'()*+,;=]"
let c_at = char '@'
let c_colon = char ':'
let c_slash = char '/'
let c_slash2 = Posix.re "//"
let c_dot = char '.'
let c_question = char '?'
let c_hash = char '#'
let reserved = gen_delims / sub_delims
let unreserved = Posix.re "[A-Za-z0-9-._~]"
let hexdig = Posix.re "[0-9A-Fa-f]"
let pct_encoded = (char '%') + hexdig + hexdig
let dec_octet = Posix.re "25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?"
let ipv4_address = (repn (dec_octet + c_dot) 3 (Some 3)) + dec_octet
(* following RFC2234, RFC3986, RFC6874 and
http://people.spodhuis.org/phil.pennock/software/emit_ipv6_regexp-0.304
*)
let zone_id = unreserved / pct_encoded
let ipv6_address =
let (=|) n a = repn a n (Some n) in
let (<|) n a = repn a 0 (Some n) in
let h16 = repn hexdig 1 (Some 4) in
let h16c = h16 + c_colon in
let cc = c_colon + c_colon in
let ls32 = (h16c + h16) / ipv4_address in
( char '['
+ (((6=|h16c) + ls32)
/ ( cc + (5=|h16c) + ls32)
/ ((1<| h16) + cc + (4=|h16c) + ls32)
/ ((1<|((1<|h16c) + h16)) + cc + (3=|h16c) + ls32)
/ ((1<|((2<|h16c) + h16)) + cc + (2=|h16c) + ls32)
/ ((1<|((3<|h16c) + h16)) + cc + h16c + ls32)
/ ((1<|((4<|h16c) + h16)) + cc + ls32)
/ ((1<|((5<|h16c) + h16)) + cc + h16)
/ ((1<|((6<|h16c) + h16)) + cc )
)
+ (opt (Posix.re "%25" + rep1 zone_id))
+ char ']'
)
let reg_name = rep ( unreserved / pct_encoded / sub_delims )
let host = ipv6_address / ipv4_address / reg_name (* | ipv4_literal TODO *)
let userinfo = rep (unreserved / pct_encoded / sub_delims / c_colon)
let port = Posix.re "[0-9]*"
let authority = (opt ((group userinfo) + c_at)) + (group host) + (opt (c_colon + (group port)))
let null_authority = (group empty) + (group empty) + (group empty)
let pchar = unreserved / pct_encoded / sub_delims / c_colon / c_at
let segment = rep pchar
let segment_nz = rep1 pchar
let segment_nz_nc = repn (unreserved / pct_encoded / sub_delims / c_at) 1 None
let path_abempty = rep (c_slash + segment)
let path_absolute = c_slash + (opt (segment_nz + (rep (c_slash + segment))))
let path_noscheme = segment_nz_nc + (rep (c_slash + segment ))
let path_rootless = segment_nz + (rep (c_slash + segment ))
let path_empty = empty
let path = path_abempty (* begins with "/" or is empty *)
/ path_absolute (* begins with "/" but not "//" *)
/ path_noscheme (* begins with a non-colon segment *)
/ path_rootless (* begins with a segment *)
/ path_empty (* zero characters *)
let hier_part = (c_slash2 + authority + path_abempty)
/ (path_absolute / path_rootless / path_empty)
let scheme = Posix.re "[A-Za-z][A-Za-z0-9+\\\\-\\.]*"
let query = group (rep ( pchar / c_slash / c_question))
let fragment = group (rep (pchar / c_slash / c_question))
let absolute_uri = scheme + c_colon + hier_part + (opt (c_question + query))
let uri = scheme + c_colon + hier_part + (opt (c_question + query)) + (opt (c_hash + fragment))
let relative_part = (c_slash2 + authority + path_abempty) / (path_absolute / path_noscheme / path_empty)
let relative_ref = relative_part + (opt (c_question + query)) + (opt (c_hash + fragment))
let uri_reference = Posix.re "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"
end
let ipv4_address = Posix.compile Raw.ipv4_address
let ipv6_address = Posix.compile Raw.ipv6_address
let uri_reference = Posix.compile Raw.uri_reference
let authority = Posix.compile Raw.authority
let host = Posix.compile Raw.host
end
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
]
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 option -> string option
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 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
| _ -> 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 = function
| Some hs -> Some (String.lowercase_ascii hs)
| None -> None
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 = function
| Some hs ->
let hs = String.lowercase_ascii hs in
if hs="localhost" then Some "" else Some hs
| None -> None
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)))
(* 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 (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:`Userinfo u);
begin match po with None -> ();
| Some p ->
Buffer.add_char buf ':';
Buffer.add_string buf (pct_encode ?scheme ~component:`Userinfo p)
end;
Pct.cast_encoded (Buffer.contents buf)
end
let userinfo_of_encoded = Userinfo.userinfo_of_encoded
let encoded_of_userinfo ?scheme = Userinfo.encoded_of_userinfo ?scheme
(* 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 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:`Path 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 = Path.encoded_of_path ?scheme
(* 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 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:`Query_key k);
if v <> [] then (
Buffer.add_char buf '=';
iter_concat (fun buf s ->
Buffer.add_string buf
(pct_encode ?scheme ~component:`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: 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 t t' =
(match compare_decoded_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 cast_opt = function
| Some h -> Some (Pct.cast_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=cast_opt (Scheme.normalize_host (uncast_opt uri.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=decode host; port; path; query; fragment=decode fragment }
(** Parse a URI string into a structure *)
let of_string s =
(* Given a series of Re substrings, cast each component
* into a Pct.encoded and return an optional type (None if
* the component is not present in the Uri *)
let get_opt_encoded s n =
try Some (Pct.cast_encoded (Re.Group.get s n))
with Not_found -> None
in
let get_opt s n =
try
let pct = Pct.cast_encoded (Re.Group.get s n) in
Some (Pct.decode pct)
with Not_found -> None
in
let subs = Re.exec Uri_re.uri_reference s in
let scheme = get_opt subs 2 in
let userinfo, host, port =
match get_opt_encoded subs 4 with
|None -> None, None, None
|Some a ->
let subs' = Re.exec Uri_re.authority (Pct.uncast_encoded a) in
let userinfo = match get_opt_encoded subs' 1 with
| Some x -> Some (Userinfo.userinfo_of_encoded (Pct.uncast_encoded x))
| None -> None
in
let host = get_opt subs' 2 in
let port =
match get_opt subs' 3 with
|None -> None
|Some x ->
(try
Some (int_of_string (Pct.uncast_decoded x))
with _ -> None)
in
userinfo, host, port
in
let path =
match get_opt_encoded subs 5 with
| Some x -> Path.path_of_encoded (Pct.uncast_encoded x)
| None -> []
in
let query =
match get_opt_encoded subs 7 with
| Some x -> Query.of_raw (Pct.uncast_encoded x)
| None -> Query.Raw (None, Lazy.from_val [])
in
let fragment = get_opt subs 9 in
normalize scheme { scheme; userinfo; host; port; path; query; fragment }
(** Convert a URI structure into a percent-encoded string
<http://tools.ietf.org/html/rfc3986#section-5.3>
*)
let to_string 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:`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 userinfo));
Buffer.add_char buf '@'
);
(match uri.host with
|None -> ()
|Some host ->
add_pct_string ~component:`Host host;
);
(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 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 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 q)
);
(match uri.fragment with
|None -> ()
|Some f -> Buffer.add_char buf '#'; add_pct_string ~component:`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 = get_decoded_opt uri.host
let with_host uri =
function
|Some host -> { uri with host=Some (Pct.cast_decoded host) }
|None -> { uri with host=None }
let host_with_default ?(default="localhost") uri =
match host uri with
|None -> default
|Some h -> h
let userinfo uri = match uri.userinfo with
| None -> None
| Some userinfo -> Some (Pct.uncast_encoded (match uri.scheme with
| None -> encoded_of_userinfo userinfo
| Some s -> encoded_of_userinfo ~scheme:(Pct.uncast_decoded s) 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 (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 (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 (Pct.cast_decoded ""); port=port }
end
(* Return the path component *)
let path uri = Pct.uncast_encoded (match uri.scheme with
| None -> encoded_of_path uri.path
| Some s -> encoded_of_path ~scheme:(Pct.uncast_decoded s) 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 uri = Query.(match uri.query with
| Raw (qs,_) -> qs
| KV [] -> None
| KV kv -> Some (encoded_of_query ?scheme:(scheme uri) 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)) }
)
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
(* 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 Re = Uri_re