Source file conversions.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
open Core
open Async
module Format = struct
module T = struct
type t =
| Bytes
| Json
| Hex
[@@deriving enumerate, sexp_of]
end
include T
let arg_type = Command.Arg_type.enumerated_sexpable ~case_sensitive:false (module T)
end
let rec jsonaf_of_msgpack msgpack =
let open Jsonaf.Export in
match (msgpack : Msgpack.t) with
| Nil -> jsonaf_of_unit ()
| Boolean bool -> jsonaf_of_bool bool
| String string -> jsonaf_of_string string
| Binary bytes -> jsonaf_of_bytes bytes
| Integer int -> jsonaf_of_int int
| Int64 int64 -> `Number (Printf.sprintf "%Ld" int64)
| UInt64 uint64 -> `Number (Printf.sprintf "%Lu" uint64)
| Floating float -> jsonaf_of_float float
| Array ts -> `Array (List.map ts ~f:jsonaf_of_msgpack)
| Map alist ->
`Object
(List.map alist ~f:(fun (key, value) ->
match key with
| String key -> key, jsonaf_of_msgpack value
| _ ->
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_int type_id; "data", jsonaf_of_bytes data ]
;;
let rec msgpack_of_jsonaf : Jsonaf.t -> Msgpack.t = function
| `Null -> Nil
| `False -> Boolean false
| `True -> Boolean true
| `String string -> String string
| `Number string ->
(match Int.of_string string with
| int -> Integer int
| exception _ ->
(match Scanf.sscanf string "%Ld%!" Fn.id with
| int64 -> Int64 int64
| exception _ ->
(match Scanf.sscanf string "%Lu%!" Fn.id with
| uint64 -> UInt64 uint64
| exception _ ->
(match Float.of_string string with
| float -> Floating float
| exception _ -> raise_s [%message "Failed to parse number" ~_:string]))))
| `Object [ ("type", `Number type_id); ("data", `String data) ] ->
Extension { type_id = Int.of_string type_id; data = Bytes.of_string data }
| `Object alist ->
alist
|> List.map ~f:(fun (key, value) -> Msgpack.String key, msgpack_of_jsonaf value)
|> Map
| `Array ts -> Array (List.map ts ~f:msgpack_of_jsonaf)
;;
let rec effectively_equivalent_msgpack m1 m2 =
match (m1 : Msgpack.t), (m2 : Msgpack.t) with
| Nil, Nil -> true
| Nil, _ -> false
| Boolean b1, Boolean b2 -> Bool.equal b1 b2
| Boolean _, _ -> false
| String s1, String s2 -> String.equal s1 s2
| String s1, Binary s2 -> String.equal s1 (Bytes.to_string s2)
| String _, _ -> false
| Binary s1, String s2 -> String.equal (Bytes.to_string s1) s2
| Binary s1, Binary s2 -> Bytes.equal s1 s2
| Binary _, _ -> false
| Integer i1, Integer i2 -> Int.equal i1 i2
| Integer i1, Int64 i2 -> Int64.equal (Int64.of_int i1) i2
| Integer i1, UInt64 i2 -> i1 >= 0 && Int64.equal (Int64.of_int i1) i2
| Integer i1, Floating i2 -> Float.is_integer i2 && i1 = Float.to_int i2
| Integer _, _ -> false
| Int64 i1, Integer i2 -> Int64.equal i1 (Int64.of_int i2)
| Int64 i1, Int64 i2 -> Int64.equal i1 i2
| Int64 i1, UInt64 i2 -> Int64.(i1 > zero) && Int64.equal i1 i2
| Int64 i1, Floating i2 -> Float.is_integer i2 && Int64.equal i1 (Float.to_int64 i2)
| Int64 _, _ -> false
| UInt64 i1, Integer i2 -> i2 >= 0 && Int64.equal i1 (Int64.of_int i2)
| UInt64 i1, Int64 i2 -> Int64.(i2 > zero) && Int64.equal i1 i2
| UInt64 i1, UInt64 i2 -> Int64.equal i1 i2
| UInt64 i1, Floating i2 ->
Float.is_integer i2 && Float.is_positive i2 && Int64.equal i1 (Float.to_int64 i2)
| UInt64 _, _ -> false
| Floating i1, Integer i2 -> Float.is_integer i1 && Float.to_int i1 = i2
| Floating i1, Int64 i2 -> Float.is_integer i1 && Int64.equal (Float.to_int64 i1) i2
| Floating i1, UInt64 i2 ->
Float.is_integer i1 && Float.is_positive i1 && Int64.equal (Float.to_int64 i1) i2
| Floating f1, Floating f2 -> Float.equal f1 f2
| Floating _, _ -> false
| Array a1, Array a2 -> List.equal effectively_equivalent_msgpack a1 a2
| Array _, _ -> false
| Map a1, Map a2 ->
let effectively_equivalent_msgpack_maps =
Tuple2.equal ~eq1:Msgpack.equal ~eq2:effectively_equivalent_msgpack
in
List.equal effectively_equivalent_msgpack_maps a1 a2
| Map _, _ -> false
| Extension e1, Extension e2 -> e1.type_id = e2.type_id && Bytes.equal e1.data e2.data
| Extension _, _ -> false
;;
let hex_of_bytes bytes =
String.concat_map bytes ~f:(fun byte -> sprintf "%02x" (Char.to_int byte))
;;
let conv ~from ~to_ ~reader ~writer =
let%bind reader =
match (from : Format.t) with
| Bytes | Json -> return reader
| Hex ->
let pipe_r =
Reader.pipe reader
|> Pipe.map' ~max_queue_length:1 ~f:(fun queue ->
return (Queue.concat_map queue ~f:String.to_list))
in
Pipe.create_reader ~close_on_exception:false (fun writer ->
let write2 c1 c2 =
let hex_digits = String.of_char_list [ c1; c2 ] in
Scanf.sscanf hex_digits "%02x" (fun code ->
code |> Char.of_int_exn |> String.of_char |> Pipe.write writer)
in
Deferred.repeat_until_finished None (fun leftover ->
match%bind Pipe.read_exactly pipe_r ~num_values:2 with
| `Eof ->
(match leftover with
| None ->
Pipe.close writer;
return (`Finished ())
| Some ch -> failwithf "Invalid hex input: trailing char '%c'" ch ())
| `Fewer queue ->
let first = Queue.get queue 0 in
(match leftover with
| None -> return (`Repeat (Some first))
| Some ch ->
let%map () = write2 ch first in
`Repeat None)
| `Exactly queue ->
let first = Queue.get queue 0 in
let second = Queue.get queue 1 in
(match leftover with
| None ->
let%map () = write2 first second in
`Repeat None
| Some ch ->
let%map () = write2 ch first in
`Repeat (Some second))))
|> Reader.of_pipe (Info.of_string "stdin")
in
let write_msgpack =
let msgpack_to_output_format =
match (to_ : Format.t) with
| Bytes -> Msgpack.string_of_t_exn ?bufsize:None
| Hex -> Fn.compose hex_of_bytes Msgpack.string_of_t_exn
| Json ->
Fn.compose (sprintf !"%{Jsonaf#hum}\n") jsonaf_of_msgpack
in
fun msgpack ->
Writer.write writer (msgpack_to_output_format msgpack);
Writer.flushed writer
in
let%bind read_result =
match from with
| Bytes | Hex ->
Angstrom_async.parse_many Msgpack.Internal.Parser.msg write_msgpack reader
| Json ->
(match%bind
Angstrom_async.parse_many
Jsonaf_kernel.Parser.t_without_trailing_whitespace
(Fn.compose write_msgpack msgpack_of_jsonaf)
reader
with
| Error _ as error -> return error
| Ok () ->
Angstrom_async.parse
Angstrom.(
skip_while Char.is_whitespace *> peek_char
>>= function
| None -> return ()
| Some ch -> fail (sprintf "'%c'" ch) <?> "Trailing character")
reader)
in
let%map () = Writer.close writer
and () = Reader.close reader in
match read_result with
| Error message -> failwith message
| Ok () -> ()
;;
let command =
Command.async
~behave_nicely_in_pipeline:true
~summary:"Convert Msgpack messages"
(let%map_open.Command () = return ()
and from = flag "from" (required Format.arg_type) ~doc:"FORMAT Input format"
and to_ = flag "to" (required Format.arg_type) ~doc:"FORMAT Output format" in
fun () -> conv ~from ~to_ ~reader:(force Reader.stdin) ~writer:(force Writer.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:(fun expected ->
let json = jsonaf_of_msgpack expected in
let actual = msgpack_of_jsonaf json in
match effectively_equivalent_msgpack expected actual with
| 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:(fun original ->
let expected = original |> jsonaf_of_msgpack |> msgpack_of_jsonaf in
let json = jsonaf_of_msgpack expected in
let actual = msgpack_of_jsonaf json in
match Msgpack.equal expected actual with
| true -> ()
| false ->
raise_s
[%message
"Not exactly equivalent"
(original : Msgpack.t)
(expected : Msgpack.t)
(actual : Msgpack.t)
(json : Jsonaf.t)]);
[%expect {||}];
return ()
;;
let quickcheck_conv_roundtrip quickcheck_generator ~format =
let pipe () =
let%map `Reader reader_fd, `Writer writer_fd = Unix.pipe (Info.of_string "") in
Writer.create writer_fd, Reader.create reader_fd
in
let%bind writer1, reader1 = pipe () in
let%bind writer2, reader2 = pipe () in
let%bind writer3, reader3 = pipe () in
let pass1 = conv ~from:Bytes ~to_:format ~reader:reader1 ~writer:writer2 in
let pass2 = conv ~from:format ~to_:Bytes ~reader:reader2 ~writer:writer3 in
let%bind () =
Async_quickcheck.async_test
quickcheck_generator
~sexp_of:[%sexp_of: Msgpack.t]
~f:(fun msg ->
let expected_bytes = Msgpack.string_of_t_exn msg in
Writer.write writer1 expected_bytes;
let%bind () = Writer.flushed writer1 in
let actual_bytes = Bytes.create (String.length expected_bytes) in
match%map Reader.read reader3 actual_bytes with
| `Eof ->
raise_s
[%message
"Expected bytes but got EOF" (expected_bytes : string) (msg : Msgpack.t)]
| `Ok n_bytes ->
let actual_bytes =
String.prefix (Bytes.to_string actual_bytes) n_bytes
in
(match
n_bytes = String.length expected_bytes
&& String.equal actual_bytes expected_bytes
with
| true -> ()
| false ->
raise_s
[%message
"Mismatch between actual and expected bytes"
(expected_bytes : string)
(actual_bytes : string)
(msg : Msgpack.t)]))
in
let%bind () = Writer.close writer1 in
let%bind () = pass1 in
let%bind () = pass2 in
let%bind () =
match%map Reader.contents reader3 with
| "" -> ()
| contents -> raise_s [%message "Unconsumed bytes" ~_:(contents : string)]
in
return ()
;;
let%expect_test "Hex/Bytes Roundtrip" =
let%map () =
quickcheck_conv_roundtrip
(Msgpack.quickcheck_generator ~only_string_keys:false ~only_finite_floats:false)
~format:Hex
in
[%expect {||}]
;;
let%expect_test "Json/Bytes Roundtrip" =
let%map () =
quickcheck_conv_roundtrip
(let%map.Quickcheck.Generator msg =
Msgpack.quickcheck_generator ~only_string_keys:true ~only_finite_floats:true
in
msg |> jsonaf_of_msgpack |> msgpack_of_jsonaf)
~format:Json
in
[%expect {||}]
;;