123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151(* Windows path syntax. *)letis_drive_letter=function'A'..'Z'|'a'..'z'->true|_->falseletis_sepc=c='\\'||c='/'(* A recognizer matches at position [i] of [s] and returns the position
one past the match, or [None] if it doesn't match. *)typerecognizer=string->int->intoptionletcharpf:recognizer=funsi->ifi<String.lengths&&fs.[i]thenSome(i+1)elseNonelet(*>)(p:recognizer)(q:recognizer):recognizer=funsi->Option.bind(psi)(qs)let(<|>)(p:recognizer)(q:recognizer):recognizer=funsi->matchpsiwithSome_asr->r|None->qsiletoptp:recognizer=p<|>(fun_i->Somei)(* Zero or more matches of [p]. *)letrecmanyp:recognizer=funsi->matchpsiwithSomej->manypsj|None->Someiletchrc=charp(Char.equalc)letsep=charpis_sepletbslash=chr'\\'letqmark=chr'?'(* The (possibly empty) run of non-separator characters at [i]. *)letcomponent=many(charp(func->not(is_sepc)))(* A component whose text satisfies [f]. *)letcomponent_isf:recognizer=funsi->matchcomponentsiwith|Someewhenf(String.subsi(e-i))->Somee|_->None(* The component after this one. [opt sep] rather than [sep] so that a
malformed prefix is cut at whatever exists. *)letnext=optsep*>componentletdrive=charpis_drive_letter*>chr':'letq_or_dot=component_is(func->c="?"||c=".")letunc_kw=component_is(func->String.uppercase_asciic="UNC")(* The "C:", "\\server\share", "\\.\device", "\\?\..." or "\??\..."
volume prefix of a path. *)letvolume_prefix=drive(* C: *)<|>(sep*>sep*>q_or_dot*>optsep*>unc_kw*>next*>next)(* \\?\UNC\server\share *)<|>(sep*>sep*>component*>next)(* \\server\share, \\?\C: or \\.\device *)<|>(bslash*>qmark*>qmark*>component*>next)(* \??\C: - the NT object-manager form (backslash only) *)(* Win32 does no normalization in the verbatim and NT namespaces. *)letverbatim_prefix=bslash*>(bslash<|>qmark)*>qmarkletverbatims=Option.is_some(verbatim_prefixs0)(* [\??\], [\\?\] and [\\.\] all name the NT object-manager namespace. *)letnt_prefix=(verbatim_prefix<|>(bslash*>bslash*>chr'.'))*>bslash(* [is_relative p] is [true] unless [p] begins with a volume or a separator. *)letis_relatives=Option.is_none((volume_prefix<|>sep)s0)letvolume_ends=Option.value(volume_prefixs0)~default:0letdropns=String.subsn(String.lengths-n)(* A path's volume prefix, and the rest of the path. *)letsplit_volumep=letn=volume_endpinString.subp0n,dropnpletsplitp=letvend=volume_endpinletsep_char=ifverbatimpthenChar.equal'\\'elseis_sepinletsep_ati=sep_charp.[i]in(* Trailing separators are ignored; one is kept for a bare root. *)letrectrimi=ifi>vend+1&&sep_at(i-1)thentrim(i-1)elseiinletstop=trim(String.lengthp)inifstop<=vend||(stop=vend+1&&sep_atvend)thenNoneelseletrecrsepi=ifi<vendthenNoneelseifsep_atithenSomeielsersep(i-1)inmatchrsep(stop-1)with|None->Some(String.subp0vend,String.subpvend(stop-vend))|Someidx->letbasename=String.subp(idx+1)(stop-idx-1)inletdirname=(* keep the root separator itself *)String.subp0(ifidx=vendthenvend+1elsetrimidx)inSome(dirname,basename)letparent_and_leafp=matchsplitpwith|Some("",leaf)->".",leaf|Someparts->parts|None->".",(ifp=""then"."elsep)letdirnamep=fst(parent_and_leafp)letbasenamep=snd(parent_and_leafp)letconcatab=letl=String.lengthainifl=0thenbelseif(ifverbatimathena.[l-1]='\\'elseis_sepa.[l-1])thena^belseifdrivea0=Somelthena^b(* a bare drive is drive-relative: adding a separator would change its meaning *)elsea^"\\"^bletjoinp1p2=matchp1,p2with|p1,""->concatp1p2|_,p2whennot(is_relativep2)->p2|".",p2->p2|p1,p2->concatp1p2letnormaliserest=letrecgoacc=function|[]->List.revacc|(""|".")::xs->goaccxs|".."::xs->go(matchaccwith[]->[]|_::acc->acc)xs|x::xs->go(x::acc)xsin"\\"^String.concat"\\"(go[](String.split_on_char'\\'rest))(* [qualify p] is the absolute Win32 path [p] named in the NT namespace. *)letqualifyp=letafterr=Option.map(funi->dropip)(rp0)in"\\??\\"^matchafternt_prefix,after(bslash*>bslash)with|Somerest,_->rest(* \??\, \\?\ or \\.\ *)|None,Someshare->"UNC\\"^share(* \\server\share *)|None,None->p(* C:\... *)letto_nt~cwdp=ifverbatimpthenqualifypelse(letbackslashes=String.map(func->ifc='/'then'\\'elsec)inletvol,rest=split_volume(backslashesp)inletcwd_vol,cwd_rest=split_volume(backslashescwd)inletrooted=rest<>""&&rest.[0]='\\'inletvol,base=matchvolwith|""->cwd_vol,(ifrootedthen""elsecwd_rest)|vwhenrooted||v.[0]='\\'->v,""|vwhenString.uppercase_asciiv=String.uppercase_asciicwd_vol->cwd_vol,cwd_rest|v->v,""(* Win32 keeps a current directory per drive; but we sadly can't see it *)inqualify(vol^normalise(base^"\\"^rest)))