123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408openCoreopenAsyncmoduleFormat=structmoduleT=structtypet=|Bytes|Json|Hex|Sexp[@@derivingenumerate,sexp_of]endincludeTletarg_type=Command.Arg_type.enumerated_sexpable~case_sensitive:false(moduleT)endletrecjsonaf_of_msgpackmsgpack=letopenJsonaf.Exportinmatch(msgpack:Msgpack.t)with|Nil->jsonaf_of_unit()|Boolbool->jsonaf_of_boolbool|Stringstring->jsonaf_of_stringstring|Binarybytes->jsonaf_of_bytesbytes|Intint->jsonaf_of_intint|Int64int64->`Number(Printf.sprintf"%Ld"int64)|Uint64uint64->`Number(Printf.sprintf"%Lu"uint64)|Floatfloat->jsonaf_of_floatfloat|Arrayts->`Array(List.mapts~f:jsonaf_of_msgpack)|Mapalist->`Object(List.mapalist~f:(fun(key,value)->matchkeywith|Stringkey->key,jsonaf_of_msgpackvalue|_->raise_s[%message"Cannot convert Msgpack map key to JSON map key - not a string"(key:Msgpack.t)]))|Ext{type_id;data}->`Object["type",jsonaf_of_inttype_id;"data",jsonaf_of_bytesdata];;letrecmsgpack_of_jsonaf:Jsonaf.t->Msgpack.t=function|`Null->Nil|`False->Boolfalse|`True->Booltrue|`Stringstring->Stringstring|`Numberstring->(matchInt.of_stringstringwith|int->Intint|exception_->(matchScanf.sscanfstring"%Ld%!"Fn.idwith|int64->Int64int64|exception_->(matchScanf.sscanfstring"%Lu%!"Fn.idwith|uint64->Uint64uint64|exception_->(matchFloat.of_stringstringwith|float->Floatfloat|exception_->raise_s[%message"Failed to parse number"~_:string]))))|`Object[("type",`Numbertype_id);("data",`Stringdata)]->Ext{type_id=Int.of_stringtype_id;data=Bytes.of_stringdata}|`Objectalist->alist|>List.map~f:(fun(key,value)->Msgpack.Stringkey,msgpack_of_jsonafvalue)|>Map|`Arrayts->Array(List.mapts~f:msgpack_of_jsonaf);;(* Check if the two msgpack values are effectively equivalent (accounts for lossiness due
to JSON translation). *)letreceffectively_equivalent_msgpackm1m2=match(m1:Msgpack.t),(m2:Msgpack.t)with|Nil,Nil->true|Nil,_->false|Boolb1,Boolb2->Bool.equalb1b2|Bool_,_->false|Strings1,Strings2->String.equals1s2|Strings1,Binarys2->String.equals1(Bytes.to_strings2)|String_,_->false|Binarys1,Strings2->String.equal(Bytes.to_strings1)s2|Binarys1,Binarys2->Bytes.equals1s2|Binary_,_->false|Inti1,Inti2->Int.equali1i2|Inti1,Int64i2->Int64.equal(Int64.of_inti1)i2|Inti1,Uint64i2->i1>=0&&Int64.equal(Int64.of_inti1)i2|Inti1,Floati2->Float.is_integeri2&&i1=Float.to_inti2|Int_,_->false|Int64i1,Inti2->Int64.equali1(Int64.of_inti2)|Int64i1,Int64i2->Int64.equali1i2|Int64i1,Uint64i2->Int64.(i1>zero)&&Int64.equali1i2|Int64i1,Floati2->Float.is_integeri2&&Int64.equali1(Float.to_int64i2)|Int64_,_->false|Uint64i1,Inti2->i2>=0&&Int64.equali1(Int64.of_inti2)|Uint64i1,Int64i2->Int64.(i2>zero)&&Int64.equali1i2|Uint64i1,Uint64i2->Int64.equali1i2|Uint64i1,Floati2->Float.is_integeri2&&Float.is_positivei2&&Int64.equali1(Float.to_int64i2)|Uint64_,_->false|Floati1,Inti2->Float.is_integeri1&&Float.to_inti1=i2|Floati1,Int64i2->Float.is_integeri1&&Int64.equal(Float.to_int64i1)i2|Floati1,Uint64i2->Float.is_integeri1&&Float.is_positivei1&&Int64.equal(Float.to_int64i1)i2|Floatf1,Floatf2->Float.equalf1f2|Float_,_->false|Arraya1,Arraya2->List.equaleffectively_equivalent_msgpacka1a2|Array_,_->false|Mapa1,Mapa2->leteffectively_equivalent_msgpack_maps=Tuple2.equal~eq1:Msgpack.equal~eq2:effectively_equivalent_msgpackinList.equaleffectively_equivalent_msgpack_mapsa1a2|Map_,_->false|Exte1,Exte2->e1.type_id=e2.type_id&&Bytes.equale1.datae2.data|Ext_,_->false;;lethex_of_bytesbytes=String.concat_mapbytes~f:(funbyte->sprintf"%02x"(Char.to_intbyte));;letconv~from~to_~reader~writer=let%bindreader=match(from:Format.t)with|Bytes|Json|Sexp->returnreader|Hex->letpipe_r=Reader.pipereader|>Pipe.map'~max_queue_length:1~f:(funqueue->return(Queue.concat_mapqueue~f:String.to_list))inPipe.create_reader~close_on_exception:false(funwriter->letwrite2c1c2=lethex_digits=String.of_char_list[c1;c2]inScanf.sscanfhex_digits"%02x"(funcode->code|>Char.of_int_exn|>String.of_char|>Pipe.writewriter)inDeferred.repeat_until_finishedNone(funleftover->match%bindPipe.read_exactlypipe_r~num_values:2with|`Eof->(matchleftoverwith|None->Pipe.closewriter;return(`Finished())|Somech->failwithf"Invalid hex input: trailing char '%c'"ch())|`Fewerqueue->letfirst=Queue.getqueue0in(matchleftoverwith|None->return(`Repeat(Somefirst))|Somech->let%map()=write2chfirstin`RepeatNone)|`Exactlyqueue->letfirst=Queue.getqueue0inletsecond=Queue.getqueue1in(matchleftoverwith|None->let%map()=write2firstsecondin`RepeatNone|Somech->let%map()=write2chfirstin`Repeat(Somesecond))))|>Reader.of_pipe(Info.of_string"stdin")inletwrite_msgpack=letmsgpack_to_output_format=match(to_:Format.t)with|Bytes->Msgpack.string_of_t_exn?bufsize:None|Hex->Fn.composehex_of_bytesMsgpack.string_of_t_exn|Json->(* Inserting a trailing newline here delineates the different JSON messages (e.g.,
allowing "12" and "1" "2" to be distinguished) and also makes the CLI output
more readable. *)Fn.compose(sprintf!"%{Jsonaf#hum}\n")jsonaf_of_msgpack|Sexp->funmsgpack->msgpack|>[%sexp_of:Msgpack.t]|>Sexp.to_string_hum(* Adding whitespace after "Nil" is needed to avoid conflation with the next
sexp, which may also be Nil, in which case the two strings will be read as a
single atom. All other Mspgack constructors have arguments, so they have sexp
list representations. *)|>(function|"Nil"->"Nil "|sexp->sexp)infunmsgpack->Writer.writewriter(msgpack_to_output_formatmsgpack);Writer.flushedwriterinlet%bindread_result=matchfromwith|Bytes|Hex->Angstrom_async.parse_manyMsgpack.Internal.Parser.msgwrite_msgpackreader|Json->(match%bindAngstrom_async.parse_manyJsonaf_kernel.Parser.t_without_trailing_whitespace(Fn.composewrite_msgpackmsgpack_of_jsonaf)readerwith|Error_aserror->returnerror|Ok()->Angstrom_async.parseAngstrom.(skip_whileChar.is_whitespace*>peek_char>>=function|None->return()|Somech->fail(sprintf"'%c'"ch)<?>"Trailing character")reader)|Sexp->Monitor.try_with(fun()->Reader.read_sexpsreader|>Pipe.iter~f:(Fn.composewrite_msgpack[%of_sexp:Msgpack.t]))>>|Result.map_error~f:Exn.to_stringinlet%map()=Writer.closewriterand()=Reader.closereaderinmatchread_resultwith|Errormessage->failwithmessage|Ok()->();;letcommand=Command.async~behave_nicely_in_pipeline:true~summary:"Convert Msgpack messages"(let%map_open.Command()=return()andfrom=flag"from"(requiredFormat.arg_type)~doc:"FORMAT Input format"andto_=flag"to"(requiredFormat.arg_type)~doc:"FORMAT Output format"infun()->conv~from~to_~reader:(forceReader.stdin)~writer:(forceWriter.stdout));;let%expect_test"Msgpack ~= Msgpack->JSON->Msgpack"=Quickcheck.test(Msgpack.quickcheck_generator~only_string_keys:true~only_finite_floats:true)~sexp_of:[%sexp_of:Msgpack.t]~f:(funexpected->letjson=jsonaf_of_msgpackexpectedinletactual=msgpack_of_jsonafjsoninmatcheffectively_equivalent_msgpackexpectedactualwith|true->()|false->raise_s[%message"Not effectively equivalent"(expected:Msgpack.t)(actual:Msgpack.t)(json:Jsonaf.t)]);[%expect{| |}];return();;let%expect_test"Msgpack->JSON->Msgpack == Msgpack->JSON->Msgpack->JSON->Msgpack"=Quickcheck.test(Msgpack.quickcheck_generator~only_string_keys:true~only_finite_floats:true)~sexp_of:[%sexp_of:Msgpack.t]~f:(funoriginal->letexpected=original|>jsonaf_of_msgpack|>msgpack_of_jsonafinletjson=jsonaf_of_msgpackexpectedinletactual=msgpack_of_jsonafjsoninmatchMsgpack.equalexpectedactualwith|true->()|false->raise_s[%message"Not exactly equivalent"(original:Msgpack.t)(expected:Msgpack.t)(actual:Msgpack.t)(json:Jsonaf.t)]);[%expect{| |}];return();;let%expect_test"Msgpack ~= Msgpack->Sexp->Msgpack"=Quickcheck.test(* We don't want to test NAN with roundtripping through sexp because [of_sexp: float]
converts -NAN to NAN. *)(Msgpack.quickcheck_generator~only_string_keys:false~only_finite_floats:true)~sexp_of:[%sexp_of:Msgpack.t]~f:(funexpected->letsexp=[%sexp_of:Msgpack.t]expectedinletactual=[%of_sexp:Msgpack.t]sexpinmatcheffectively_equivalent_msgpackexpectedactualwith|true->()|false->raise_s[%message"Not effectively equivalent"~expected:(Msgpack.string_of_t_exnexpected)~actual:(Msgpack.string_of_t_exnactual)(sexp:Sexp.t)(actual:Msgpack.t)]);[%expect{| |}];return();;let%expect_test"Msgpack->Sexp->Msgpack == Msgpack->Sexp->Msgpack->Sexp->Msgpack"=Quickcheck.test(Msgpack.quickcheck_generator~only_string_keys:false~only_finite_floats:true)~sexp_of:[%sexp_of:Msgpack.t]~f:(funoriginal->letexpected=original|>[%sexp_of:Msgpack.t]|>[%of_sexp:Msgpack.t]inletsexp=[%sexp_of:Msgpack.t]expectedinletactual=[%of_sexp:Msgpack.t]sexpinmatchMsgpack.equalexpectedactualwith|true->()|false->raise_s[%message"Not exactly equivalent"~original:(Msgpack.string_of_t_exnoriginal)~expected:(Msgpack.string_of_t_exnexpected)~actual:(Msgpack.string_of_t_exnactual)(sexp:Sexp.t)]);[%expect{| |}];return();;letquickcheck_conv_roundtripquickcheck_generator~format=letpipe()=let%map`Readerreader_fd,`Writerwriter_fd=Unix.pipe(Info.of_string"")inWriter.createwriter_fd,Reader.createreader_fdinlet%bindwriter1,reader1=pipe()inlet%bindwriter2,reader2=pipe()inlet%bindwriter3,reader3=pipe()inletpass1=conv~from:Bytes~to_:format~reader:reader1~writer:writer2inletpass2=conv~from:format~to_:Bytes~reader:reader2~writer:writer3inlet%bind()=Async_quickcheck.async_testquickcheck_generator~sexp_of:[%sexp_of:Msgpack.t]~f:(funmsgpack->letexpected_bytes=Msgpack.string_of_t_exnmsgpackinWriter.writewriter1expected_bytes;let%bind()=Writer.flushedwriter1inletactual_bytes=Bytes.create(String.lengthexpected_bytes)inmatch%mapReader.readreader3actual_byteswith|`Eof->raise_s[%message"Expected bytes but got EOF"(expected_bytes:string)(msgpack:Msgpack.t)]|`Okn_bytes->letactual_bytes=(* We use [String.prefix] here in case
[n_bytes < String.length expected_bytes] so we exclude trailing garbage. *)String.prefix(Bytes.to_stringactual_bytes)n_bytesin(matchn_bytes=String.lengthexpected_bytes&&String.equalactual_bytesexpected_byteswith|true->()|false->raise_s[%message"Mismatch between actual and expected bytes"(expected_bytes:string)(actual_bytes:string)(msgpack:Msgpack.t)]))inlet%bind()=Writer.closewriter1inlet%bind()=pass1inlet%bind()=pass2inlet%bind()=match%mapReader.contentsreader3with|""->()|contents->raise_s[%message"Unconsumed bytes"~_:(contents:string)]inreturn();;let%expect_test"Hex/Bytes Roundtrip"=let%map()=quickcheck_conv_roundtrip(Msgpack.quickcheck_generator~only_string_keys:false~only_finite_floats:false)~format:Hexin[%expect{| |}];;let%expect_test"Sexp/Bytes Roundtrip"=let%map()=quickcheck_conv_roundtrip(Msgpack.quickcheck_generator~only_string_keys:false~only_finite_floats:true)~format:Sexpin[%expect{| |}];;let%expect_test"Json/Bytes Roundtrip"=let%map()=quickcheck_conv_roundtrip(let%map.Quickcheck.Generatormsg=Msgpack.quickcheck_generator~only_string_keys:true~only_finite_floats:truein(* Since we already checked the correctness of the Msgpack->JSON roundtripping in
the earlier tests, we just test the [conv] roundtrip on the lossy version. *)msg|>jsonaf_of_msgpack|>msgpack_of_jsonaf)~format:Jsonin[%expect{| |}];;