123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238openCoreincludeTimezone_intfincludeCore_private.Time_zonemoduletypeExtend_zone=Timezone_intf.Extend_zonemoduleZone_cache=structtypez={mutablefull:bool;basedir:string;table:tString.Table.t}letthe_one_and_only={full=false;basedir=Option.value(Sys.getenv"TZDIR")~default:"/usr/share/zoneinfo/";table=String.Table.create()};;letfindzone=Hashtbl.findthe_one_and_only.tablezoneletfind_or_loadzonename=matchfindzonenamewith|Somez->Somez|None->ifthe_one_and_only.fullthenNoneelse(tryletfilename=the_one_and_only.basedir^"/"^zonenameinletzone=input_tz_file~zonename~filenameinHashtbl.setthe_one_and_only.table~key:zonename~data:zone;Somezonewith|_->None);;lettraversebasedir~f=letskip_prefixes=["Etc/GMT";"right/";"posix/"]inletmaxdepth=10inletbasedir_len=String.lengthbasedir+1inletrecdfsdirdepth=ifdepth<1then()elseArray.iter(Caml.Sys.readdirdir)~f:(funfn->letfn=dir^"/"^fninletrelative_fn=String.drop_prefixfnbasedir_leninmatchCaml.Sys.is_directoryfnwith|true->ifnot(List.existsskip_prefixes~f:(funprefix->String.is_prefix~prefixrelative_fn))thendfsfn(depth-1)|false->frelative_fn)indfsbasedirmaxdepth;;letinit()=ifnotthe_one_and_only.fullthen(traversethe_one_and_only.basedir~f:(funzone_name->ignore(find_or_loadzone_name:toption));the_one_and_only.full<-true);;letto_alist()=Hashtbl.to_alistthe_one_and_only.tableletinitialized_zones()=List.sort~compare:(funab->String.ascending(fsta)(fstb))(to_alist());;letfind_or_load_matchingt1=letfile_sizefilename=letc=Stdio.In_channel.createfilenameinletl=Stdio.In_channel.lengthcinStdio.In_channel.closec;linlett1_file_size=Option.map(original_filenamet1)~f:file_sizeinwith_return(funr->letreturn_if_matcheszone_name=letfilename=String.concat~sep:"/"[the_one_and_only.basedir;zone_name]inletmatches=try[%compare.equal:int64option]t1_file_size(Some(file_sizefilename))&&[%compare.equal:Md5.toption](digestt1)Option.(join(map(find_or_loadzone_name)~f:digest))with|_->falseinifmatchesthenr.return(find_or_loadzone_name)else()inList.iter!likely_machine_zones~f:return_if_matches;traversethe_one_and_only.basedir~f:return_if_matches;None);;endletinit=Zone_cache.initletinitialized_zones=Zone_cache.initialized_zonesletfindzone=letzone=(* Some aliases for convenience *)matchzonewith(* case insensitivity *)|"utc"->"UTC"|"gmt"->"GMT"(* some aliases for common zones *)|"chi"->"America/Chicago"|"nyc"->"America/New_York"|"hkg"->"Asia/Hong_Kong"|"lon"|"ldn"->"Europe/London"|"tyo"->"Asia/Tokyo"(* catchall *)|_->zoneinZone_cache.find_or_loadzone;;letfind_exnzone=matchfindzonewith|None->Error.raise_s[%message"unknown zone"(zone:string)]|Somez->z;;letlocal=(* Load [TZ] immediately so that subsequent modifications to the environment cannot
alter the result of [force local]. *)letlocal_zone_name=Sys.getenv"TZ"inletload()=matchlocal_zone_namewith|Somezone_name->find_exnzone_name|None->letlocaltime_t=input_tz_file~zonename:"/etc/localtime"~filename:"/etc/localtime"in(* Load the matching zone file from the real zone cache so that we can serialize it
properly. The file loaded from /etc/localtime won't have a name we can use on the
other side to find the right zone. *)(matchZone_cache.find_or_load_matchinglocaltime_twith|Somet->t|None->localtime_t)inLazy.from_funload;;moduleStable=structincludeCore_private.Time_zone.StablemoduleV1=structtypenonrect=tlett_of_sexpsexp=matchsexpwith|Sexp.Atom"Local"->Lazy.forcelocal|Sexp.Atomname->(tryifString.equalname"UTC"||String.equalname"GMT"thenof_utc_offset_explicit_name~name~hours:0elseif(* This special handling is needed because the offset directionality of the
zone files in /usr/share/zoneinfo for GMT<offset> files is the reverse of
what is generally expected. That is, GMT+5 is what most people would call
GMT-5. *)String.is_prefixname~prefix:"GMT-"||String.is_prefixname~prefix:"GMT+"||String.is_prefixname~prefix:"UTC-"||String.is_prefixname~prefix:"UTC+"then(letoffset=letbase=Int.of_string(String.subname~pos:4~len:(String.lengthname-4))inmatchname.[3]with|'-'->-1*base|'+'->base|_->assertfalseinof_utc_offset_explicit_name~name~hours:offset)elsefind_exnnamewith|exc->of_sexp_error(sprintf"Timezone.t_of_sexp: %s"(Exn.to_stringexc))sexp)|_->of_sexp_error"Timezone.t_of_sexp: expected atom"sexp;;letsexp_of_tt=letname=nametinifString.equalname"/etc/localtime"thenfailwith"the local time zone cannot be serialized";Sexp.Atomname;;includeSexpable.Stable.To_stringable.V1(structtypenonrect=t[@@derivingsexp]end)(* The correctness of these relies on not exposing raw loading/creation functions to
the outside world that would allow the construction of two Zone's with the same
name and different transitions. *)letcomparet1t2=String.compare(to_stringt1)(to_stringt2)lethash_fold_tstatet=String.hash_fold_tstate(to_stringt)lethash=Ppx_hash_lib.Std.Hash.of_foldhash_fold_tinclude(Binable.Stable.Of_binable.V1[@alert"-legacy"](String)(structtypenonrect=tletto_binablet=letname=nametinifString.equalname"/etc/localtime"thenfailwith"the local time zone cannot be serialized";name;;letof_binables=t_of_sexp(Sexp.Atoms)end):Binable.Swithtypet:=t)endendincludeIdentifiable.Make(structletmodule_name="Timezone"includeStable.V1letof_string=of_stringletto_string=to_stringend)modulePrivate=structmoduleZone_cache=Zone_cacheend