123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248(*
* 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"]typecomponent=[|`Scheme|`Authority|`Userinfo(* subcomponent of authority in some schemes *)|`Host(* subcomponent of authority in some schemes *)|`Path|`Query|`Query_key|`Query_value|`Fragment|`Generic|`Customof(component*string*string)(* (component * safe chars * unsafe chars) *)]typepct_encoder={scheme:component;userinfo:component;host:component;path:component;query_key:component;query_value:component;fragment:component;}letreciter_concatfnsepbuf=function|last::[]->fnbuflast|el::rest->fnbufel;Buffer.add_stringbufsep;iter_concatfnsepbufrest|[]->()letrev_interjectelst=letrecauxacc=function|[]->acc|x::xs->aux(x::e::acc)xsinmatchlstwith|[]->[]|h::t->aux[h]tletcompare_optctt'=matcht,t'with|None,None->0|Some_,None->1|None,Some_->-1|Somea,Someb->cabletreccompare_listftt'=matcht,t'with|[],[]->0|_::_,[]->1|[],_::_->-1|x::xs,y::ys->matchfxywith0->compare_listfxsys|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.
*)typesafe_chars=boolarraymoduletypeScheme=sigvalsafe_chars_for_component:component->safe_charsvalnormalize_host:string->stringvalcanonicalize_port:intoption->intoptionvalcanonicalize_path:stringlist->stringlistendmoduleGeneric:Scheme=structletsub_delimsa=letsubd="!$&'()*+,;="infori=0toString.lengthsubd-1doletc=Char.codesubd.[i]ina.(c)<-truedone;aletsafe_chars:safe_chars=leta=Array.make256falseinletalways_safe="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~"infori=0toString.lengthalways_safe-1doletc=Char.codealways_safe.[i]ina.(c)<-truedone;aletpchar:safe_chars=leta=sub_delims(Array.copysafe_chars)ina.(Char.code':')<-true;a.(Char.code'@')<-true;aletsafe_chars_for_scheme:safe_chars=leta=Array.copysafe_charsina.(Char.code'+')<-true;a(** Safe characters for the path component of a URI *)letsafe_chars_for_path:safe_chars=leta=sub_delims(Array.copypchar)in(* delimiter: non-segment delimiting uses should be pct encoded *)a.(Char.code'/')<-false;aletsafe_chars_for_query:safe_chars=(* TODO: What about {"!","$",","}? See <https://github.com/avsm/ocaml-uri/commit/1ef3f1dfb41bdb4f33f223ffe16e62a33975661a#diff-740f2de53c9eb36e9670ddfbdb9ba914R171> *)leta=Array.copypcharina.(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;aletsafe_chars_for_query_key:safe_chars=leta=Array.copysafe_chars_for_queryina.(Char.code'=')<-false;aletsafe_chars_for_query_value:safe_chars=leta=Array.copysafe_chars_for_queryina.(Char.code',')<-false;aletsafe_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 *)letsafe_chars_for_userinfo:safe_chars=leta=Array.copysafe_charsin(* delimiter: non-segment delimiting uses should be pct encoded *)a.(Char.code':')<-false;aletrecsafe_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)->letsafe_chars=Array.copy(safe_chars_for_componentcomponent)infori=0toString.lengthsafe-1doletc=Char.codesafe.[i]insafe_chars.(c)<-truedone;fori=0toString.lengthunsafe-1doletc=Char.codeunsafe.[i]insafe_chars.(c)<-falsedone;safe_chars|`Generic|_->safe_charsletnormalize_hosthso=hsoletcanonicalize_portport=portletcanonicalize_pathpath=pathendmoduleHttp:Scheme=structincludeGenericletnormalize_hosths=String.lowercase_asciihsletcanonicalize_port=function|None->None|Some80->None|Somex->Somexletcanonicalize_path=function|[]->["/"]|x->xendmoduleHttps:Scheme=structincludeHttpletcanonicalize_port=function|None->None|Some443->None|Somex->SomexendmoduleFile:Scheme=structincludeGenericletnormalize_hosths=leths=String.lowercase_asciihsinifhs="localhost"then""elsehsendmoduleUrn:Scheme=structincludeGenericendletmodule_of_scheme=function|Somes->beginmatchString.lowercase_asciiswith|"http"->(moduleHttp:Scheme)|"https"->(moduleHttps:Scheme)|"file"->(moduleFile:Scheme)|"urn"->(moduleUrn:Scheme)|_->(moduleGeneric:Scheme)end|None->(moduleGeneric: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
*)modulePct:sigtypeencodedtypedecodedvalencode:?scheme:string->?component:component->decoded->encodedvaldecode:encoded->decoded(* The empty decoded string *)valempty_decoded:decoded(* Identity functions so we need to explicitly cast when using them below *)valcast_encoded:string->encodedvalcast_decoded:string->decodedvaluncast_encoded:encoded->stringvaluncast_decoded:decoded->string(* Lift HOFs for maps over encodings, decodings, and strings *)vallift_encoded:(encoded->encoded)->string->stringvallift_decoded:(decoded->decoded)->string->stringvalunlift_encoded:(string->string)->encoded->encodedvalunlift_decoded:(string->string)->decoded->decodedvalunlift_decoded2:(string->string->'a)->decoded->decoded->'aend=structtypeencoded=stringtypedecoded=stringletcast_encodedx=xletcast_decodedx=xletempty_decoded=""letuncast_decodedx=xletuncast_encodedx=xletlift_encodedf=fletlift_decodedf=fletunlift_encodedf=fletunlift_decodedf=fletunlift_decoded2f=f(** Scan for reserved characters and replace them with
percent-encoded equivalents.
@return a percent-encoded string *)letencode?scheme?(component=`Path)b=letmoduleScheme=(val(module_of_schemescheme):Scheme)inletsafe_chars=Scheme.safe_chars_for_componentcomponentinletlen=String.lengthbinletbuf=Buffer.createleninletrecscanstartcur=ifcur>=lenthenbeginBuffer.add_substringbufbstart(cur-start);endelsebeginletc=Char.codeb.[cur]inifsafe_chars.(c)thenscanstart(cur+1)elsebeginifcur>startthenBuffer.add_substringbufbstart(cur-start);Buffer.add_stringbuf(Printf.sprintf"%%%02X"c);scan(cur+1)(cur+1)endendinscan00;Buffer.contentsbufletint_of_hex_charc=letc=int_of_char(Char.uppercase_asciic)-48inifc>9thenifc>16&&c<23thenc-7elsefailwith"int_of_hex_char"elseifc>=0thencelsefailwith"int_of_hex_char"(** Scan for percent-encoding and convert them into ASCII.
@return a percent-decoded string *)letdecodeb=(* TODO: Should both strict and non-strict versions be exposed? *)letlen=String.lengthbinletbuf=Buffer.createleninletrecscanstartcur=ifcur>=lenthenBuffer.add_substringbufbstart(cur-start)elseifb.[cur]='%'thenbeginBuffer.add_substringbufbstart(cur-start);letcur=cur+1inifcur>=lenthenBuffer.add_charbuf'%'elsematchint_of_hex_charb.[cur]with|exception_->Buffer.add_charbuf'%';scancurcur|highbits->beginletcur=cur+1inifcur>=lenthenbeginBuffer.add_charbuf'%';Buffer.add_charbufb.[cur-1]endelsebeginletstart_at=matchint_of_hex_charb.[cur]with|lowbits->Buffer.add_charbuf(Char.chr(highbitslsl4+lowbits));cur+1|exception_->Buffer.add_charbuf'%';Buffer.add_charbufb.[cur-1];curinscanstart_atstart_atendendendelsescanstart(cur+1)inscan00;Buffer.contentsbufend(* Percent encode a string *)letpct_encode?scheme?(component=`Path)s=Pct.(uncast_encoded(encode?scheme~component(cast_decodeds)))letpct_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 *)letpct_decodes=Pct.(uncast_decoded(decode(cast_encodeds)))(* Userinfo string handling, to and from an id * credential pair *)moduleUserinfo=structtypet=string*stringoptionletcompare(u,p)(u',p')=matchString.compareuu'with|0->compare_optString.comparepp'|c->cletuserinfo_of_encodedus=matchStringext.split~max:2~on:':'uswith|[]->("",None)|[u]->(pct_decodeu,None)|u::p::_->(pct_decodeu,Some(pct_decodep))letencoded_of_userinfo?scheme~component(u,po)=letlen=String.(1+(lengthu)+(matchpowithNone->0|Somep->lengthp))inletbuf=Buffer.createleninBuffer.add_stringbuf(pct_encode?scheme~componentu);beginmatchpowithNone->();|Somep->Buffer.add_charbuf':';Buffer.add_stringbuf(pct_encode?scheme~componentp)end;Pct.cast_encoded(Buffer.contentsbuf)endletuserinfo_of_encoded=Userinfo.userinfo_of_encodedletencoded_of_userinfo?scheme~component=Userinfo.encoded_of_userinfo?scheme~component(* Path string handling, to and from a list of path tokens *)modulePath=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). *)typet=stringlistletcompare=compare_listString.compare(* Make a path token list from a percent-encoded string *)letpath_of_encodedps=lettokl=Stringext.full_splitps~on:'/'inList.mappct_decodetokl(* Subroutine for resolve <http://tools.ietf.org/html/rfc3986#section-5.2.4> *)letremove_dot_segmentsp=letrevp=List.revpinletrecloopascensionoutp=function|"/"::".."::r|".."::r->loop(ascension+1)outpr|"/"::"."::r|"."::r->loopascensionoutpr|"/"::[]|[]whenList.(lengthp>0&&hdp="/")->"/"::outp|[]whenascension>0->List.rev_append("/"::(rev_interject"/"Array.(to_list(makeascension".."))))outp|[]->List.(iflengthoutp>0&&hdoutp="/"thentloutpelseoutp)|"/"::"/"::rwhenascension>0->loop(ascension-1)outp("/"::r)|"/"::_::rwhenascension>0->loop(ascension-1)outpr|s::r->loop0(s::outp)rinloop0[]revpletencoded_of_path?scheme~componentp=letlen=List.fold_left(functok->String.lengthtok+c)0pinletbuf=Buffer.createleniniter_concat(funbuf->function|"/"->Buffer.add_charbuf'/'|seg->Buffer.add_stringbuf(pct_encode?scheme~componentseg))""bufp;Pct.cast_encoded(Buffer.contentsbuf)(* Subroutine for resolve <http://tools.ietf.org/html/rfc3986#section-5.2.3> *)letmergebhostbpathrelpath=matchbhost,List.revbpathwith|Some_,[]->"/"::relpath|_,("/"::rbpath|_::"/"::rbpath)->List.rev_append("/"::rbpath)relpath|_,_->relpathendletpath_of_encoded=Path.path_of_encodedletencoded_of_path?scheme~component=Path.encoded_of_path?scheme~component(* Query string handling, to and from an assoc list of key/values *)moduleQuery=structtypekv=(string*stringlist)listtypet=|KVofkv|Rawofstringoption*kvLazy.tletcomparexy=matchx,ywith|KVkvl,KVkvl'|Raw(_,lazykvl),KVkvl'|KVkvl,Raw(_,lazykvl')->compare_list(fun(k,vl)(k',vl')->matchString.comparekk'with|0->compare_listString.comparevlvl'|c->c)kvlkvl'|Raw(raw,_),Raw(raw',_)->compare_optString.comparerawraw'letfindqk=trySome(List.assockq)withNot_found->Noneletsplit_queryqs=letels=Stringext.split~on:'&'qsin(* Replace a + in a query string with a space in-place *)letplus_to_spaces=lets=Bytes.unsafe_of_stringsinfori=0toBytes.lengths-1doifBytes.getsi='+'thenBytes.setsi' 'done;Bytes.unsafe_to_stringsinletrecloopacc=function|(k::v::_)::tl->letn=plus_to_spacek,(matchStringext.split~on:','(plus_to_spacev)with|[]->[""]|l->l)inloop(n::acc)tl|[k]::tl->letn=plus_to_spacek,[]inloop(n::acc)tl|[]::tl->loop(("",[])::acc)tl|[]->accinmatchelswith|[]->["",[]]|els->loop[](List.rev_map(funel->Stringext.split~on:'='el~max:2)els)(* Make a query tuple list from a percent-encoded string *)letquery_of_encodedqs=List.map(fun(k,v)->(pct_decodek,List.mappct_decodev))(split_queryqs)(* Assemble a query string suitable for putting into a URI.
* Tuple inputs are percent decoded and will be encoded by
* this function.
*)letencoded_of_query?scheme?(pct_encoder=pct_encoder())l=letlen=List.fold_left(funa(k,v)->a+(String.lengthk)+(List.fold_left(funas->a+(String.lengths)+1)0v)+2)(-1)linletbuf=Buffer.createleniniter_concat(funbuf(k,v)->Buffer.add_stringbuf(pct_encode?scheme~component:pct_encoder.query_keyk);ifv<>[]then(Buffer.add_charbuf'=';iter_concat(funbufs->Buffer.add_stringbuf(pct_encode?scheme~component:pct_encoder.query_values))","bufv))"&"bufl;Buffer.contentsbufletof_rawqs=letlazy_query=Lazy.from_fun(fun()->query_of_encodedqs)inRaw(Someqs,lazy_query)letkv=functionRaw(_,lazykv)|KVkv->kvendletquery_of_encoded=Query.query_of_encodedletencoded_of_query?scheme=Query.encoded_of_query?scheme(* Type of the URI, with most bits being optional *)typet={scheme:Pct.decodedoption;userinfo:Userinfo.toption;host:[`Ipv4_literalofstring|`Ipv6_literalofstring|`HostofPct.decoded]option;port:intoption;path:Path.t;query:Query.t;fragment:Pct.decodedoption;}letempty={scheme=None;userinfo=None;host=None;port=None;path=[];query=Query.Raw(None,Lazy.from_val[]);fragment=None;}letcompare_decoded=Pct.unlift_decoded2String.compareletcompare_decoded_opt=compare_optcompare_decodedletcompare_hosth1h2=matchh1,h2with|`Ipv4_literalip1,`Ipv4_literalip2->String.compareip1ip2|`Ipv6_literalip1,`Ipv6_literalip2->String.compareip1ip2|`Hosth1,`Hosth2->compare_decodedh1h2|_->-1letcompare_host_opt=compare_optcompare_hostletcomparett'=(matchcompare_host_optt.hostt'.hostwith|0->(matchcompare_decoded_optt.schemet'.schemewith|0->(matchcompare_opt(funpp'->ifp<p'then-1elseifp>p'then1else0)t.portt'.portwith|0->(matchcompare_optUserinfo.comparet.userinfot'.userinfowith|0->(matchPath.comparet.patht'.pathwith|0->(matchQuery.comparet.queryt'.querywith|0->compare_decoded_optt.fragmentt'.fragment|c->c)|c->c)|c->c)|c->c)|c->c)|c->c)letequaltt'=comparett'=0letuncast_opt=function|Someh->Some(Pct.uncast_decodedh)|None->Noneletnormalizeschemuri=letmoduleScheme=(val(module_of_scheme(uncast_optschem)):Scheme)inletdobf=function|Somex->Some(Pct.unlift_decodedfx)|None->Nonein{uriwithscheme=dobString.lowercase_asciiuri.scheme;host=matchuri.hostwith|Some(`Ipv4_literalhost)->Some(`Ipv4_literal(Scheme.normalize_hosthost))|Some(`Ipv6_literalhost)->Some(`Ipv6_literal(Scheme.normalize_hosthost))|Some(`Hosthost)->Some(`Host(Pct.cast_decoded(Scheme.normalize_host(Pct.uncast_decodedhost))))|None->None}(** Convert a URI structure into a percent-encoded string
<http://tools.ietf.org/html/rfc3986#section-5.3>
*)letto_string?(pct_encoder=pct_encoder())uri=letscheme=matchuri.schemewith|Somes->Some(Pct.uncast_decodeds)|None->Noneinletbuf=Buffer.create128in(* Percent encode a decoded string and add it to the buffer *)letadd_pct_string?(component=`Path)x=Buffer.add_stringbuf(Pct.uncast_encoded(Pct.encode?scheme~componentx))in(matchuri.schemewith|None->()|Somex->add_pct_string~component:pct_encoder.schemex;Buffer.add_charbuf':');(* URI has a host if any host-related component is set. Defaults to "". *)if(matchuri.userinfo,uri.host,uri.portwith|Some_,_,_|_,Some_,_|_,_,Some_->true|_->false)thenBuffer.add_stringbuf"//";(matchuri.userinfowith|None->()|Someuserinfo->Buffer.add_stringbuf(Pct.uncast_encoded(encoded_of_userinfo?scheme~component:pct_encoder.userinfouserinfo));Buffer.add_charbuf'@');(matchuri.hostwith|None->()|Some(`Hosthost)->add_pct_string~component:pct_encoder.hosthost;|Some(`Ipv4_literalhost)->Buffer.add_stringbufhost|Some(`Ipv6_literalhost)->Buffer.add_charbuf'[';Buffer.add_stringbufhost;Buffer.add_charbuf']');(matchuri.portwith|None->()|Someport->Buffer.add_charbuf':';Buffer.add_stringbuf(string_of_intport));(matchuri.pathwith(* Handle relative paths correctly *)|[]->()|"/"::_->Buffer.add_stringbuf(Pct.uncast_encoded(encoded_of_path?scheme~component:pct_encoder.pathuri.path))|first_segment::_->(matchuri.hostwith|Some_->Buffer.add_charbuf'/'|None->(* ensure roundtrip by forcing relative path interpretation not scheme *)matchStringext.find_fromfirst_segment~pattern:":"with|None->()|Some_->matchschemewith|Some_->()|None->Buffer.add_stringbuf"./");Buffer.add_stringbuf(Pct.uncast_encoded(encoded_of_path?scheme~component:pct_encoder.pathuri.path)));Query.(matchuri.querywith|Raw(None,_)|KV[]->()|Raw(_,lazyq)|KVq->(* normalize e.g. percent capitalization *)Buffer.add_charbuf'?';Buffer.add_stringbuf(encoded_of_query?scheme~pct_encoderq));(matchuri.fragmentwith|None->()|Somef->Buffer.add_charbuf'#';add_pct_string~component:pct_encoder.fragmentf);Buffer.contentsbuf(* Various accessor functions, as the external uri type is abstract *)letget_decoded_opt=functionNone->None|Somex->Some(Pct.uncast_decodedx)letschemeuri=get_decoded_opturi.schemeletwith_schemeuri=function|Somescheme->{uriwithscheme=Some(Pct.cast_decodedscheme)}|None->{uriwithscheme=None}lethosturi=matchuri.hostwith|None->None|Some(`Ipv4_literalh|`Ipv6_literalh)->Someh|Some(`Hosth)->Some(Pct.uncast_decodedh)lethost_with_default?(default="localhost")uri=matchhosturiwith|None->default|Someh->hletuserinfo?(pct_encoder=pct_encoder())uri=matchuri.userinfowith|None->None|Someuserinfo->Some(Pct.uncast_encoded(matchuri.schemewith|None->encoded_of_userinfo~component:pct_encoder.userinfouserinfo|Somes->encoded_of_userinfo~scheme:(Pct.uncast_decodeds)~component:pct_encoder.userinfouserinfo))letwith_userinfouriuserinfo=letuserinfo=matchuserinfowith|Someu->Some(userinfo_of_encodedu)|None->Noneinmatchhosturiwith|None->{uriwithhost=Some(`Host(Pct.cast_decoded""));userinfo=userinfo}|Some_->{uriwithuserinfo=userinfo}letuseruri=matchuri.userinfowith|None->None|Some(user,_)->Someuserletpassworduri=matchuri.userinfowith|None|Some(_,None)->None|Some(_,Somepass)->Somepassletwith_passworduripassword=letresultuserinfo=matchhosturiwith|None->{uriwithhost=Some(`Host(Pct.cast_decoded""));userinfo=userinfo}|Some_->{uriwithuserinfo=userinfo}inmatchuri.userinfo,passwordwith|None,None->uri|None,Some_->result(Some("",password))|Some(user,_),_->result(Some(user,password))letporturi=uri.portletwith_porturiport=matchhosturiwith|Some_->{uriwithport=port}|None->beginmatchportwith|None->{uriwithhost=None;port=None}|Some_->{uriwithhost=Some(`Host(Pct.cast_decoded""));port=port}end(* Return the path component *)letpath?(pct_encoder=pct_encoder())uri=Pct.uncast_encoded(matchuri.schemewith|None->encoded_of_path~component:pct_encoder.pathuri.path|Somes->encoded_of_path~scheme:(Pct.uncast_decodeds)~component:pct_encoder.pathuri.path)letwith_pathuripath=letpath=path_of_encodedpathinmatchhosturi,pathwith|None,_|Some_,"/"::_|Some_,[]->{uriwithpath=path}|Some_,_->{uriwithpath="/"::path}letfragmenturi=get_decoded_opturi.fragmentletwith_fragmenturi=function|None->{uriwithfragment=None}|Somefrag->{uriwithfragment=Some(Pct.cast_decodedfrag)}letqueryuri=Query.kvuri.queryletverbatim_query?(pct_encoder=pct_encoder())uri=Query.(matchuri.querywith|Raw(qs,_)->qs|KV[]->None|KVkv->Some(encoded_of_query?scheme:(schemeuri)~pct_encoderkv))letget_query_param'urik=Query.(find(kvuri.query)k)letget_query_paramurik=matchget_query_param'urikwith|None->None|Somev->Some(String.concat","v)letwith_queryuriquery={uriwithquery=Query.KVquery}letq_sq=List.map(fun(k,v)->k,[v])qletwith_query'uriquery=with_queryuri(q_squery)letadd_query_paramurip=Query.({uriwithquery=KV(p::(kvuri.query))})letadd_query_param'uri(k,v)=Query.({uriwithquery=KV((k,[v])::(kvuri.query))})letadd_query_paramsurips=Query.({uriwithquery=KV(ps@(kvuri.query))})letadd_query_params'urips=Query.({uriwithquery=KV((q_sps)@(kvuri.query))})letremove_query_paramurik=Query.({uriwithquery=KV(List.filter(fun(k',_)->k<>k')(kvuri.query))})(* Construct encoded path and query components *)letpath_and_queryuri=match(pathuri),(queryuri)with|"",[]->"/"(* TODO: What about same document? (/) *)|"",q->(* TODO: What about same document? (/) *)letscheme=uncast_opturi.schemeinPrintf.sprintf"/?%s"(encoded_of_query?schemeq)|p,[]->p|p,q->letscheme=uncast_opturi.schemeinPrintf.sprintf"%s?%s"p(encoded_of_query?schemeq)(* 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> *)letresolveschembaseuri=letschem=Some(Pct.cast_decoded(matchschemebasewith|None->schem|Somescheme->scheme))innormalizeschemPath.(matchschemeuri,userinfouri,hosturiwith|Some_,_,_->{uriwithpath=remove_dot_segmentsuri.path}|None,Some_,_|None,_,Some_->{uriwithscheme=base.scheme;path=remove_dot_segmentsuri.path}|None,None,None->leturi={uriwithscheme=base.scheme;userinfo=base.userinfo;host=base.host;port=base.port}inletpath_str=pathuriinifpath_str=""then{uriwithpath=base.path;query=matchuri.querywith|Query.Raw(None,_)|Query.KV[]->base.query|_->uri.query}elseifpath_str.[0]='/'then{uriwithpath=remove_dot_segmentsuri.path}else{uriwithpath=remove_dot_segments(mergebase.hostbase.pathuri.path);})letcanonicalizeuri=leturi=resolve""emptyuriinletmoduleScheme=(val(module_of_scheme(uncast_opturi.scheme)):Scheme)in{uriwithport=Scheme.canonicalize_porturi.port;path=Scheme.canonicalize_pathuri.path;}letppppfuri=Format.pp_print_stringppf(to_stringuri)letpp_humppfuri=Format.pp_print_stringppf(to_stringuri)moduleParser=structopenAngstromletstring_of_char=String.make1letstring_of_char_listchars=String.concat""(List.mapstring_of_charchars)letscheme=lift(funs->Some(Pct.decode(Pct.cast_encodeds)))(take_while(func->c<>':'&&c<>'/'&&c<>'?'&&c<>'#')<*char':')<|>returnNoneletis_digit=function'0'..'9'->true|_->falselethex_digit=satisfy(function|'0'..'9'|'A'..'F'|'a'..'f'->true|_->false)lethexadecimal=liftstring_of_char_list(manyhex_digit)letc_dot=char'.'letc_at=char'@'letc_colon=char':'letdec_octet=take_while1(function'0'..'9'->true|_->false)>>=funnum->ifint_of_stringnum<256thenreturnnumelsefail"invalid octect"letipv4_address=lift2(funthreeone->String.concat"."three^"."^one)(count3(dec_octet<*c_dot))dec_octet(* -- after double colon, IPv4 dotted notation could appear anywhere *)letafter_double_colon=fix(funf->list[ipv4_address]<|>lift2(funxy->x::y)hexadecimal(c_colon*>f<|>return[]))letdouble_coloncount=after_double_colon>>=(funrest->letfiller_length=8-count-List.lengthrestiniffiller_length<=0thenfail"too many parts in IPv6 address"elsereturn(""::rest))<|>return[""]letrecpart=function|7->(* max 8 parts in an IPv6 address *)lift(funx->[x])hexadecimal|6->(* after 6 parts it could end in IPv4 dotted notation *)list[ipv4_address]<|>hex_part6|n->hex_partnandhex_partn=lift2(funxy->x::y)hexadecimal(c_colon*>(c_colon*>double_colon(n+1)<|>part(n+1)))letrecsplit_withfxs=matchxswith|[]->[],[]|y::ys->iffythenletzs,ts=split_withfysiny::zs,tselse[],xsletipv6=letformat_addrsegments=letbefore_double_colon,after_double_colon=split_with(funsegment->segment<>"")segmentsinletbefore=String.concat":"before_double_coloninletres=matchafter_double_colonwith|""::xs->before^"::"^String.concat":"xs|_->beforeinresinliftformat_addr(c_colon*>c_colon*>double_colon0<|>part0)letipv6_address=(char'[')*>ipv6<*(char']')letpct_encoded=lift2(funpctdigits->string_of_char_list(pct::digits))(char'%')(count2hex_digit)letsub_delims=satisfy(function|'!'|'$'|'&'|'\''|'('|')'|'*'|'+'|','|';'|'='->true|_->false)letunreserved=(* "[A-Za-z0-9-._~]" *)satisfy(function|'A'..'Z'|'a'..'z'|'0'..'9'|'-'|'.'|'_'|'~'->true|_->false)letreg_name=lift(String.concat"")(many(choice[string_of_char<$>unreserved;pct_encoded;string_of_char<$>sub_delims]))lethost=choice[ipv4_address>>|(funh->`Ipv4_literalh);ipv6_address>>|(funh->`Ipv6_literalh);reg_name>>|(funs->`Host(Pct.decode(Pct.cast_encodeds)))(* 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 / ":" )
*)]letuserinfo=lift(funx->lets=String.concat""xinSome(Userinfo.userinfo_of_encodeds))(many(choice[string_of_char<$>unreserved;pct_encoded;string_of_char<$>sub_delims;string_of_char<$>c_colon])<*c_at)<|>returnNoneletport=peek_char>>=function|Some':'->c_colon*>take_whileis_digit>>|funport->letdecoded=Pct.decode(Pct.cast_encodedport)in(trySome(int_of_string(Pct.uncast_decodeddecoded))with_->None)|Some_|None->returnNoneletauthority=string"//"*>lift3(funuserinfohostport->userinfo,Somehost,port)userinfohostport<|>return(None,None,None)letpath=liftPath.path_of_encoded(take_while(function'?'|'#'->false|_->true))letquery=liftQuery.of_raw(char'?'*>take_till(function'#'->true|_->false))<|>return(Query.Raw(None,Lazy.from_val[]))letfragment=lift(funs->Some(Pct.decode(Pct.cast_encodeds)))(char'#'*>take_while(fun_->true))<|>returnNonelet_uri_reference=lift4(funscheme(userinfo,host,port)pathqueryfragment->normalizescheme{scheme;userinfo;host;port;path;query;fragment})schemeauthoritypathquery<*>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 *)leturi_reference=take_while(function|'\n'->false|_->true)>>|funs->matchAngstrom.parse_string~consume:All_uri_referenceswith|Okt->t|Error_->(* Shouldn't really happen if the parser is forgiving. *)emptyendletdecode_hosthost=matchAngstrom.parse_string~consume:AllParser.hosthostwith|Okparsed->parsed|Error_->matchAngstrom.parse_string~consume:AllParser.ipv6hostwith|Okparsed->(`Ipv6_literalparsed)|Error_->(`Host(Pct.cast_decodedhost))(* 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.
*)letmake?scheme?userinfo?host?port?path?query?fragment()=letdecode=function|Somex->Some(Pct.cast_decodedx)|None->Noneinlethost=matchuserinfo,host,portwith|_,Some_,_|None,None,None->host|Some_,None,_|_,None,Some_->Some""inletuserinfo=matchuserinfowith|None->None|Someu->Some(userinfo_of_encodedu)inletpath=matchpathwith|None->[]|Somep->letpath=path_of_encodedpinmatchhost,pathwith|None,_|Some_,"/"::_|Some_,[]->path|Some_,_->"/"::pathinletquery=matchquerywith|None->Query.KV[]|Somep->Query.KVpinletscheme=decodeschemeinnormalizescheme{scheme;userinfo;host=(matchhostwith|Somehost->Some(decode_hosthost)|None->None);port;path;query;fragment=decodefragment}letwith_hosturihost={uriwithhost=(matchhostwith|Somehost->Some(decode_hosthost)|None->None)}letwith_uri?scheme?userinfo?host?port?path?query?fragmenturi=letwith_path_optuo=matchowith|None->with_pathu""|Somep->with_pathupinletwith_query_optuo=matchowith|None->with_queryu[]|Someq->with_queryuqinletwith_fou=matchowith|None->u|Somex->fuxinwith_with_schemeschemeuri|>with_with_userinfouserinfo|>with_with_hosthost|>with_with_portport|>with_with_path_optpath|>with_with_query_optquery|>with_with_fragmentfragmentletof_strings=(* To preserve the old regex parser's behavior, we only parse a prefix, and
* stop whenever we can't parse more. *)matchAngstrom.parse_string~consume:PrefixParser.uri_referenceswith|Okt->t|Error_->(* Shouldn't really happen if the parser is forgiving. *)emptymoduleAbsolute_http=structtypeuri=ttypet={scheme:[`Http|`Https];userinfo:Userinfo.toption;host:[`Ipv4_literalofstring|`Ipv6_literalofstring|`HostofPct.decoded];port:intoption;path:Path.t;query:Query.t;fragment:Pct.decodedoption}let(let*)=Result.bindletto_uri{scheme;userinfo;host;port;path;query;fragment}=letscheme=matchschemewith|`Http->Pct.cast_decoded"http"|`Https->Pct.cast_decoded"https"in({scheme=Somescheme;userinfo;host=Somehost;port;path;query;fragment}:uri);;letof_uri({scheme;userinfo;host;port;path;query;fragment}:uri)=let*scheme=matchschemewith|None->Error(`Msg"No scheme present in URI")|Somescheme->(matchPct.uncast_decodedschemewith|"http"->Ok`Http|"https"->Ok`Https|unsupported_scheme->Error(`Msg(Printf.sprintf"Only http and https URIs are supported. %s is invalid."unsupported_scheme)))inlet*host=Option.to_result~none:(`Msg"host is required for HTTP(S) uris")hostinOk{scheme;userinfo;host;port;path;query;fragment};;letof_strings=matchof_strings|>of_uriwith|Okt->t|Error(`Msgerror)->failwitherrorletto_string?pct_encodert=to_urit|>to_string?pct_encoderletnormalizet={twithhost=matcht.hostwith|(`Ipv4_literalhost)->(`Ipv4_literal(String.lowercase_asciihost))|(`Ipv6_literalhost)->(`Ipv6_literal(String.lowercase_asciihost))|(`Hosthost)->(`Host(Pct.cast_decoded(String.lowercase_ascii(Pct.uncast_decodedhost))))}letmake~scheme~host?userinfo?port?path?query?fragment()=letdecode=function|Somex->Some(Pct.cast_decodedx)|None->Noneinletuserinfo=matchuserinfowith|None->None|Someu->Some(userinfo_of_encodedu)inletpath=matchpathwith|None->[]|Somep->letpath=path_of_encodedpinmatchpathwith|"/"::_|[]->path|_->"/"::pathinletquery=matchquerywith|None->Query.KV[]|Somep->Query.KVpinnormalize{scheme;userinfo;host=decode_hosthost;port;path;query;fragment=decodefragment}lethostt=matcht.hostwith|(`Ipv4_literalh|`Ipv6_literalh)->h|(`Hosth)->(Pct.uncast_decodedh)letschemet=t.schemeend