123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268(* (c) 2017 Hannes Mehnert, all rights reserved *)openAstringtype'as=stringarrayletroot=Array.make0""let[@inlinealways]check_host_labels=String.gets0<>'-'&&(* leading may not be '-' *)String.for_all(function|'a'..'z'|'A'..'Z'|'0'..'9'|'-'->true|_->false)s(* only LDH (letters, digits, hyphen)! *)lethost_exnt=(* TLD should not be all-numeric! *)if(ifArray.lengtht>0thenString.existsChar.Ascii.is_letter(Array.gett0)elsetrue)&&Array.for_allcheck_host_labeltthentelseinvalid_arg"invalid host name"lethostt=tryOk(host_exnt)with|Invalid_argumente->Error(`Msge)letcheck_service_labels=matchString.cut~sep:"_"swith|None->false|Some(empty,srv)->ifString.lengthempty>0thenfalseelseletslen=String.lengthsrvinifslen>0&&slen<=15then(* service must be LDH,
hyphen _not_ at begin nor end, no hyphen following a hyphen
1-15 characters *)letv,_=String.fold_left(fun(valid,h)c->leth'=c='-'inletv=Char.Ascii.(is_letterc||is_digitc)||h'inlethh=not(h&&h')in(v&&valid&&hh,h'))(true,false)srvinv&&String.getsrv0<>'-'&&String.getsrv(predslen)<>'-'elsefalselet[@inlinealways]is_protos=s="_tcp"||s="_udp"||s="_sctp"let[@inlinealways]check_label_lengths=letl=String.lengthsinl<64&&l>0let[@inlinealways]check_total_lengtht=Array.fold_left(funaccs->acc+1+String.lengths)1t<=255letservice_exnt=letl=Array.lengthtinififl>2thenletname=Array.subt0(l-2)incheck_service_label(Array.gett(l-1))&&is_proto(Array.gett(l-2))&&Array.for_allcheck_label_lengthname&&check_total_lengtht&&matchhostnamewithOk_->true|Error_->falseelsefalsethentelseinvalid_arg"invalid service name"letservicet=tryOk(service_exnt)with|Invalid_argumente->Error(`Msge)letrawt=tlet[@inlinealways]checkt=Array.for_allcheck_label_lengtht&&check_total_lengthtletget_label_exn?(rev=false)xsidx=letidx'=ifrevthenidxelsepred(Array.lengthxs)-idxintryArray.getxsidx'with|Invalid_argument_->invalid_arg"bad index for domain name"letget_label?revxsidx=tryOk(get_label_exn?revxsidx)with|Invalid_argumente->Error(`Msge)letfind_label_exn?(rev=false)xsp=letl=pred(Array.lengthxs)inletcheckx=x>=0&&x<=linletrecgonextidx=ifcheckidxthenifp(Array.getxsidx)thenidxelsegonext(nextidx)elseinvalid_arg"label not found"inletnext,start=ifrevthen(succ,0)else(pred,l)inletr=gonextstartinl-rletfind_label?revxsp=trySome(find_label_exn?revxsp)with|Invalid_argument_->Noneletcount_labelsxs=Array.lengthxsletprepend_label_exnxslbl=letn=Array.make1lblinletn=Array.appendxsninifcheck_label_lengthlbl&&check_total_lengthnthennelseinvalid_arg"invalid domain name"letprepend_labelxslbl=tryOk(prepend_label_exnxslbl)with|Invalid_argumente->Error(`Msge)letdrop_label_exn?(rev=false)?(amount=1)t=letlen=Array.lengtht-amountandstart=ifrevthenamountelse0inArray.subtstartlenletdrop_label?rev?amountt=tryOk(drop_label_exn?rev?amountt)with|Invalid_argument_->Error(`Msg"couldn't drop labels")letappend_exnprepost=letr=Array.appendpostpreinifcheck_total_lengthrthenrelseinvalid_arg"invalid domain name"letappendprepost=tryOk(append_exnprepost)with|Invalid_argument_->Error(`Msg"couldn't concatenate domain names")letof_strings_exnxs=letlabels=(* we support both example.com. and example.com *)matchList.revxswith|""::rst->rst|rst->rstinlett=Array.of_listlabelsinifchecktthentelseinvalid_arg"invalid host name"letof_stringsxs=tryOk(of_strings_exnxs)with|Invalid_argumente->Error(`Msge)letof_strings=of_strings(String.cuts~sep:"."s)letof_string_exns=of_strings_exn(String.cuts~sep:"."s)letof_arraya=aletto_arraya=aletto_strings?(trailing=false)dn=letlabels=Array.to_listdninList.rev(iftrailingthen""::labelselselabels)letto_string?trailingdn=String.concat~sep:"."(to_strings?trailingdn)letcanonicalt=letstr=to_stringtinof_string_exn(String.Ascii.lowercasestr)(*BISECT-IGNORE-BEGIN*)letppppfxs=Fmt.stringppf(to_stringxs)(*BISECT-IGNORE-END*)letcompare_labelab=String.compare(String.Ascii.lowercasea)(String.Ascii.lowercaseb)letcompare_domaincmp_subab=letla=Array.lengthainmatchcomparela(Array.lengthb)with|0->letreccmpidx=ifidx=lathen0elsematchcmp_sub(Array.getaidx)(Array.getbidx)with|0->cmp(succidx)|x->xincmp0|x->xletcompare=compare_domaincompare_labelletequal_label?(case_sensitive=false)ab=letcmp=ifcase_sensitivethenString.compareelsecompare_labelincmpab=0letequal?(case_sensitive=false)ab=letcmp=ifcase_sensitivethenString.compareelsecompare_labelincompare_domaincmpab=0letis_subdomain~subdomain~domain=letsupl=Array.lengthdomaininletreccmpidx=ifidx=suplthentrueelsecompare_label(Array.getdomainidx)(Array.getsubdomainidx)=0&&cmp(succidx)inifArray.lengthsubdomain<suplthenfalseelsecmp0moduleOrdered=structtypet=[`raw]sletcompare=compare_domaincompare_labelendmoduleHost_ordered=structtypet=[`host]sletcompare=compare_domaincompare_labelendmoduleService_ordered=structtypet=[`service]sletcompare=compare_domaincompare_labelendtype'at='asmoduleHost_map=structincludeMap.Make(Host_ordered)letfindkm=trySome(findkm)withNot_found->NoneendmoduleHost_set=Set.Make(Host_ordered)moduleService_map=structincludeMap.Make(Service_ordered)letfindkm=trySome(findkm)withNot_found->NoneendmoduleService_set=Set.Make(Service_ordered)moduleMap=structincludeMap.Make(Ordered)letfindkm=trySome(findkm)withNot_found->NoneendmoduleSet=Set.Make(Ordered)