123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336openCoreopenAsyncmoduleFormat=structmoduleT=structtypet=|Bytes|Json|Hex[@@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()|Booleanbool->jsonaf_of_boolbool|Stringstring->jsonaf_of_stringstring|Binarybytes->jsonaf_of_bytesbytes|Integerint->jsonaf_of_intint|Int64int64->`Number(Printf.sprintf"%Ld"int64)|UInt64uint64->`Number(Printf.sprintf"%Lu"uint64)|Floatingfloat->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)]))|Extension{type_id;data}->`Object["type",jsonaf_of_inttype_id;"data",jsonaf_of_bytesdata];;letrecmsgpack_of_jsonaf:Jsonaf.t->Msgpack.t=function|`Null->Nil|`False->Booleanfalse|`True->Booleantrue|`Stringstring->Stringstring|`Numberstring->(matchInt.of_stringstringwith|int->Integerint|exception_->(matchScanf.sscanfstring"%Ld%!"Fn.idwith|int64->Int64int64|exception_->(matchScanf.sscanfstring"%Lu%!"Fn.idwith|uint64->UInt64uint64|exception_->(matchFloat.of_stringstringwith|float->Floatingfloat|exception_->raise_s[%message"Failed to parse number"~_:string]))))|`Object[("type",`Numbertype_id);("data",`Stringdata)]->Extension{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|Booleanb1,Booleanb2->Bool.equalb1b2|Boolean_,_->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|Integeri1,Integeri2->Int.equali1i2|Integeri1,Int64i2->Int64.equal(Int64.of_inti1)i2|Integeri1,UInt64i2->i1>=0&&Int64.equal(Int64.of_inti1)i2|Integeri1,Floatingi2->Float.is_integeri2&&i1=Float.to_inti2|Integer_,_->false|Int64i1,Integeri2->Int64.equali1(Int64.of_inti2)|Int64i1,Int64i2->Int64.equali1i2|Int64i1,UInt64i2->Int64.(i1>zero)&&Int64.equali1i2|Int64i1,Floatingi2->Float.is_integeri2&&Int64.equali1(Float.to_int64i2)|Int64_,_->false|UInt64i1,Integeri2->i2>=0&&Int64.equali1(Int64.of_inti2)|UInt64i1,Int64i2->Int64.(i2>zero)&&Int64.equali1i2|UInt64i1,UInt64i2->Int64.equali1i2|UInt64i1,Floatingi2->Float.is_integeri2&&Float.is_positivei2&&Int64.equali1(Float.to_int64i2)|UInt64_,_->false|Floatingi1,Integeri2->Float.is_integeri1&&Float.to_inti1=i2|Floatingi1,Int64i2->Float.is_integeri1&&Int64.equal(Float.to_int64i1)i2|Floatingi1,UInt64i2->Float.is_integeri1&&Float.is_positivei1&&Int64.equal(Float.to_int64i1)i2|Floatingf1,Floatingf2->Float.equalf1f2|Floating_,_->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|Extensione1,Extensione2->e1.type_id=e2.type_id&&Bytes.equale1.datae2.data|Extension_,_->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->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_msgpackinfunmsgpack->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)inlet%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();;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:(funmsg->letexpected_bytes=Msgpack.string_of_t_exnmsginWriter.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)(msg: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)(msg: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"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{||}];;