Protocol Buffers codec for hand-written schemas
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

claude: complete Err -> Error module rename across call sites

Follow up to the module rename: update the remaining callers that
still referenced [Err] (library [claude.ml{,i}], [client.ml], the test
driver [test.ml]), and fix one stray [^ e] string concatenation in
hermest's CLI that needed [Json.Error.to_string e] now that
[Json.of_string] yields a structured error.

+72 -45
+1 -1
fuzz/fuzz_protobuf.ml
··· 189 189 let s = Protobuf.encode_string kitchen_codec k in 190 190 match Protobuf.decode_string kitchen_codec s with 191 191 | Error msg -> 192 - Alcobar.pp Format.err_formatter "%s\n" msg; 192 + Alcobar.pp Format.err_formatter "%s\n" (Protobuf.Error.to_string msg); 193 193 guard false 194 194 | Ok k' -> check_eq ~eq:kitchen_equal ~pp:pp_kitchen k k' 195 195
+1 -1
lib/dune
··· 1 1 (library 2 2 (name protobuf) 3 3 (public_name protobuf) 4 - (libraries bytesrw fmt leb128)) 4 + (libraries bytesrw fmt leb128 loc))
+19 -9
lib/protobuf.ml
··· 7 7 combinator call sites. *) 8 8 9 9 module Wire = Wire 10 + module Error = Error 10 11 11 12 (* -- Nested-message depth tracking. 12 13 ··· 789 790 | other -> write_value buf other v)); 790 791 Buffer.contents buf 791 792 792 - let decode_string : type a. a t -> string -> (a, string) result = 793 + let decode_string : type a. a t -> string -> (a, Error.t) result = 793 794 fun codec s -> 794 795 depth := 0; 795 796 try ··· 800 801 let v, off = decode_bytes codec s 0 in 801 802 if off <> String.length s then 802 803 Error 803 - (Fmt.str "trailing %d bytes after scalar" (String.length s - off)) 804 + (Error.of_wire_error 805 + (Fmt.str "trailing %d bytes after scalar" 806 + (String.length s - off))) 804 807 else Ok v 805 808 | Rec c -> ( 806 809 match Lazy.force c with ··· 810 813 let v, off = decode_bytes other s 0 in 811 814 if off <> String.length s then 812 815 Error 813 - (Fmt.str "trailing %d bytes after scalar" 814 - (String.length s - off)) 816 + (Error.of_wire_error 817 + (Fmt.str "trailing %d bytes after scalar" 818 + (String.length s - off))) 815 819 else Ok v) 816 - with Wire.Decode_error msg -> Error msg 820 + with Wire.Decode_error msg -> Error (Error.of_wire_error msg) 817 821 818 822 let encode codec w v = 819 823 let s = encode_string codec v in ··· 842 846 [Error]. *) 843 847 844 848 let decode_with_unknowns_string : type a. 845 - a t -> string -> (a * string, string) result = 849 + a t -> string -> (a * string, Error.t) result = 846 850 fun codec s -> 847 851 depth := 0; 848 852 try ··· 851 855 | Rec c -> ( 852 856 match Lazy.force c with 853 857 | Message m -> Ok (m.decode_body_with_unknowns s 0 (String.length s)) 854 - | _ -> Error "decode_with_unknowns_string: codec is not a message") 855 - | _ -> Error "decode_with_unknowns_string: codec is not a message" 856 - with Wire.Decode_error msg -> Error msg 858 + | _ -> 859 + Error 860 + (Error.of_wire_error 861 + "decode_with_unknowns_string: codec is not a message")) 862 + | _ -> 863 + Error 864 + (Error.of_wire_error 865 + "decode_with_unknowns_string: codec is not a message") 866 + with Wire.Decode_error msg -> Error (Error.of_wire_error msg) 857 867 858 868 let encode_with_unknowns_string : type a. a t -> unknowns:string -> a -> string 859 869 =
+9 -4
lib/protobuf.mli
··· 185 185 (** [encode_string c v] encodes [v] as a protobuf message body (no outer length 186 186 prefix). *) 187 187 188 - val decode_string : 'a t -> string -> ('a, string) result 188 + module Error = Error 189 + (** Structured decode errors extending {!Loc.Error}. *) 190 + 191 + val decode_string : 'a t -> string -> ('a, Error.t) result 189 192 (** [decode_string c s] decodes the entire input as a message body. Returns 190 - [Error msg] on malformed input; trailing garbage is an error. *) 193 + [Error e] on malformed input; trailing garbage is an error. Use 194 + {!Error.to_string} to render a human-readable message. *) 191 195 192 196 val encode : 'a t -> Bytesrw.Bytes.Writer.t -> 'a -> unit 193 197 (** [encode c w v] encodes [v] and writes it to [w] as a single slice. Useful 194 198 for composition with other bytesrw pipelines. *) 195 199 196 - val decode : 'a t -> Bytesrw.Bytes.Reader.t -> ('a, string) result 200 + val decode : 'a t -> Bytesrw.Bytes.Reader.t -> ('a, Error.t) result 197 201 (** [decode c r] drains [r] to end-of-data and decodes the full content. *) 198 202 199 203 (** {1 Unknown field preservation} ··· 202 206 preserve them so a decoded-then-re-encoded message round-trips even when 203 207 intermediate tooling runs an older schema. *) 204 208 205 - val decode_with_unknowns_string : 'a t -> string -> ('a * string, string) result 209 + val decode_with_unknowns_string : 210 + 'a t -> string -> ('a * string, Error.t) result 206 211 (** [decode_with_unknowns_string c s] returns [Ok (value, unknown_wire)] where 207 212 [unknown_wire] is a byte string holding the wire bytes of every tag that was 208 213 not in the schema, re-serialized in canonical form and sorted by tag. Pair
+4 -2
test/interop/protoc/test.ml
··· 57 57 r.wire_hex (hex_encode our_wire); 58 58 (* decode direction: our codec reads protoc's bytes into the same value *) 59 59 match Protobuf.decode_string test1_codec expected_wire with 60 - | Error msg -> Alcotest.failf "%s: decode failed: %s" r.name msg 60 + | Error msg -> 61 + Alcotest.failf "%s: decode failed: %a" r.name Protobuf.Error.pp msg 61 62 | Ok decoded -> 62 63 Alcotest.(check int32) (Fmt.str "decode %s" r.name) r.a decoded.a) 63 64 rows ··· 164 165 (fun (r : everything_row) -> 165 166 let expected_wire = hex_decode r.wire_hex in 166 167 match Protobuf.decode_string everything_codec expected_wire with 167 - | Error msg -> Alcotest.failf "%s: decode failed: %s" r.name msg 168 + | Error msg -> 169 + Alcotest.failf "%s: decode failed: %a" r.name Protobuf.Error.pp msg 168 170 | Ok decoded -> 169 171 let reencoded = Protobuf.encode_string everything_codec decoded in 170 172 Alcotest.(check string)
+38 -28
test/test_protobuf.ml
··· 22 22 let wire = Protobuf.encode_string test1_codec { a = 150l } in 23 23 Alcotest.(check string) "Test1 a=150" "089601" (hex wire); 24 24 match Protobuf.decode_string test1_codec wire with 25 - | Error msg -> Alcotest.fail msg 25 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 26 26 | Ok r -> Alcotest.(check int32) "decoded a" 150l r.a 27 27 28 28 (* --- Test 2: message Test2 { string b = 2; } with b = "testing". --- *) ··· 39 39 let wire = Protobuf.encode_string test2_codec { b = "testing" } in 40 40 Alcotest.(check string) "Test2 b=testing" "120774657374696e67" (hex wire); 41 41 match Protobuf.decode_string test2_codec wire with 42 - | Error msg -> Alcotest.fail msg 42 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 43 43 | Ok r -> Alcotest.(check string) "decoded b" "testing" r.b 44 44 45 45 (* --- Test 3: a record with every scalar type. --- *) ··· 121 121 in 122 122 let wire = Protobuf.encode_string all_scalars_codec v in 123 123 match Protobuf.decode_string all_scalars_codec wire with 124 - | Error msg -> Alcotest.fail msg 124 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 125 125 | Ok r -> 126 126 Alcotest.(check int32) "i32" v.i32 r.i32; 127 127 Alcotest.(check int64) "i64" v.i64 r.i64; ··· 154 154 let v = { name = Some "Ada"; age = Some 36l } in 155 155 let wire = Protobuf.encode_string opt_codec v in 156 156 match Protobuf.decode_string opt_codec wire with 157 - | Error msg -> Alcotest.fail msg 157 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 158 158 | Ok r -> 159 159 Alcotest.(check (option string)) "name" v.name r.name; 160 160 Alcotest.(check (option int32)) "age" v.age r.age ··· 164 164 let wire = Protobuf.encode_string opt_codec v in 165 165 Alcotest.(check int) "empty wire" 0 (String.length wire); 166 166 match Protobuf.decode_string opt_codec wire with 167 - | Error msg -> Alcotest.fail msg 167 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 168 168 | Ok r -> 169 169 Alcotest.(check (option string)) "name" None r.name; 170 170 Alcotest.(check (option int32)) "age" None r.age ··· 173 173 let v = { name = Some "solo"; age = None } in 174 174 let wire = Protobuf.encode_string opt_codec v in 175 175 match Protobuf.decode_string opt_codec wire with 176 - | Error msg -> Alcotest.fail msg 176 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 177 177 | Ok r -> 178 178 Alcotest.(check (option string)) "name" (Some "solo") r.name; 179 179 Alcotest.(check (option int32)) "age" None r.age ··· 192 192 let v = { tags = [ "a"; "bb"; "ccc" ] } in 193 193 let wire = Protobuf.encode_string rep_str_codec v in 194 194 match Protobuf.decode_string rep_str_codec wire with 195 - | Error msg -> Alcotest.fail msg 195 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 196 196 | Ok r -> Alcotest.(check (list string)) "tags" v.tags r.tags 197 197 198 198 (* --- Test 6: packed repeated varint. --- *) ··· 211 211 (* Tag 1 wire type 2 (length-delim), length 5, body = 01 02 03 96 01 *) 212 212 Alcotest.(check string) "packed wire" "0a050102039601" (hex wire); 213 213 match Protobuf.decode_string packed_codec wire with 214 - | Error msg -> Alcotest.fail msg 214 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 215 215 | Ok r -> Alcotest.(check (list int32)) "nums" v.nums r.nums 216 216 217 217 let test_packed_accepts_non_packed () = ··· 227 227 Protobuf.encode_string unpacked_codec { nums = [ 1l; 2l; 3l; 150l ] } 228 228 in 229 229 match Protobuf.decode_string packed_codec unpacked_wire with 230 - | Error msg -> Alcotest.fail msg 230 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 231 231 | Ok r -> Alcotest.(check (list int32)) "nums" [ 1l; 2l; 3l; 150l ] r.nums 232 232 233 233 (* --- Test 7: nested messages. --- *) ··· 253 253 let v = { inner = { x = 42l }; label = "hi" } in 254 254 let wire = Protobuf.encode_string outer_codec v in 255 255 match Protobuf.decode_string outer_codec wire with 256 - | Error msg -> Alcotest.fail msg 256 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 257 257 | Ok r -> 258 258 Alcotest.(check int32) "inner.x" 42l r.inner.x; 259 259 Alcotest.(check string) "label" "hi" r.label ··· 270 270 Protobuf.Wire.write_string buf "extra-junk"; 271 271 let wire = Buffer.contents buf in 272 272 match Protobuf.decode_string test1_codec wire with 273 - | Error msg -> Alcotest.fail msg 273 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 274 274 | Ok r -> Alcotest.(check int32) "a decoded despite stray tag 99" 150l r.a 275 275 276 276 (* --- Test 9: field out-of-order on the wire. --- *) ··· 285 285 Protobuf.Wire.write_int32 buf 7l; 286 286 let wire = Buffer.contents buf in 287 287 match Protobuf.decode_string all_scalars_codec wire with 288 - | Error msg -> Alcotest.fail msg 288 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 289 289 | Ok r -> 290 290 Alcotest.(check int32) "i32 decoded" 7l r.i32; 291 291 Alcotest.(check string) "str decoded" "backwards" r.str; ··· 316 316 let v = { entries = [ ("alice", 30l); ("bob", 25l); ("", 0l) ] } in 317 317 let wire = Protobuf.encode_string dict_codec v in 318 318 match Protobuf.decode_string dict_codec wire with 319 - | Error msg -> Alcotest.fail msg 319 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 320 320 | Ok r -> 321 321 Alcotest.(check int) "entry count" 3 (List.length r.entries); 322 322 Alcotest.(check (list (pair string int32))) "entries" v.entries r.entries ··· 349 349 { a = 42l; b = "hello"; c = [ 1l; 2l; 3l ] } 350 350 in 351 351 match Protobuf.decode_with_unknowns_string schema_v1 original with 352 - | Error msg -> Alcotest.fail msg 352 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 353 353 | Ok (v1, unknowns) -> ( 354 354 Alcotest.(check int32) "a decoded" 42l v1.a; 355 355 Alcotest.(check bool) ··· 361 361 Protobuf.encode_with_unknowns_string schema_v1 ~unknowns { a = v1.a } 362 362 in 363 363 match Protobuf.decode_string schema_v2 reemitted with 364 - | Error msg -> Alcotest.failf "v2 re-decode failed: %s" msg 364 + | Error msg -> 365 + Alcotest.failf "v2 re-decode failed: %s" 366 + (Protobuf.Error.to_string msg) 365 367 | Ok v2' -> 366 368 Alcotest.(check int32) "a survived" 42l v2'.a; 367 369 Alcotest.(check string) "b survived" "hello" v2'.b; ··· 370 372 let test_unknowns_empty_when_schema_matches () = 371 373 let wire = Protobuf.encode_string schema_v1 { a = 42l } in 372 374 match Protobuf.decode_with_unknowns_string schema_v1 wire with 373 - | Error msg -> Alcotest.fail msg 375 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 374 376 | Ok (_, unknowns) -> 375 377 Alcotest.(check int) "no unknowns" 0 (String.length unknowns) 376 378 377 379 (* --- Test 13: oneof --- *) 378 380 379 381 type payload = [ `None | `Text of string | `Num of int32 | `Blob of string ] 380 - 381 382 type msg_with_payload = { payload : payload } 382 383 383 384 let msg_with_payload_codec : msg_with_payload Protobuf.t = ··· 404 405 let v = { payload = `Text "hello" } in 405 406 let wire = Protobuf.encode_string msg_with_payload_codec v in 406 407 match Protobuf.decode_string msg_with_payload_codec wire with 407 - | Error msg -> Alcotest.fail msg 408 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 408 409 | Ok r -> Alcotest.(check bool) "roundtrip" true (r.payload = `Text "hello") 409 410 410 411 let test_oneof_num () = 411 412 let v = { payload = `Num 42l } in 412 413 let wire = Protobuf.encode_string msg_with_payload_codec v in 413 414 match Protobuf.decode_string msg_with_payload_codec wire with 414 - | Error msg -> Alcotest.fail msg 415 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 415 416 | Ok r -> Alcotest.(check bool) "roundtrip" true (r.payload = `Num 42l) 416 417 417 418 let test_oneof_none () = ··· 419 420 let wire = Protobuf.encode_string msg_with_payload_codec v in 420 421 Alcotest.(check int) "empty wire" 0 (String.length wire); 421 422 match Protobuf.decode_string msg_with_payload_codec wire with 422 - | Error msg -> Alcotest.fail msg 423 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 423 424 | Ok r -> Alcotest.(check bool) "payload is None" true (r.payload = `None) 424 425 425 426 let test_oneof_last_wins () = ··· 435 436 Protobuf.Wire.write_string buf "winner"; 436 437 let wire = Buffer.contents buf in 437 438 match Protobuf.decode_string msg_with_payload_codec wire with 438 - | Error msg -> Alcotest.fail msg 439 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 439 440 | Ok r -> Alcotest.(check bool) "last wins" true (r.payload = `Blob "winner") 440 441 441 442 let test_map_empty () = ··· 443 444 let wire = Protobuf.encode_string dict_codec v in 444 445 Alcotest.(check int) "empty wire" 0 (String.length wire); 445 446 match Protobuf.decode_string dict_codec wire with 446 - | Error msg -> Alcotest.fail msg 447 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 447 448 | Ok r -> Alcotest.(check (list (pair string int32))) "entries" [] r.entries 448 449 449 450 (* A schema that declares no fields: every input is unknown. *) ··· 501 502 let wire = Buffer.contents buf in 502 503 match Protobuf.decode_string empty_codec wire with 503 504 | Ok () -> () 504 - | Error msg -> Alcotest.failf "10k unknown fields rejected: %s" msg 505 + | Error msg -> 506 + Alcotest.failf "10k unknown fields rejected: %s" 507 + (Protobuf.Error.to_string msg) 505 508 506 509 (* ================================================================= 507 510 CVE-2022-1941 (protobuf-c++, 2022): null-pointer dereference when ··· 516 519 let wire = Protobuf.encode_string test1_codec { a = 42l } in 517 520 match Protobuf.decode_string empty_codec wire with 518 521 | Ok () -> () 519 - | Error msg -> Alcotest.failf "empty schema, all unknowns: %s" msg 522 + | Error msg -> 523 + Alcotest.failf "empty schema, all unknowns: %s" 524 + (Protobuf.Error.to_string msg) 520 525 521 526 (* ================================================================= 522 527 CVE-2022-3171 (protobuf-java, 2022): repeated group wire type ··· 648 653 649 654 let test_empty_input () = 650 655 match Protobuf.decode_string test1_codec "" with 651 - | Error msg -> Alcotest.failf "empty input should use defaults: %s" msg 656 + | Error msg -> 657 + Alcotest.failf "empty input should use defaults: %s" 658 + (Protobuf.Error.to_string msg) 652 659 | Ok r -> Alcotest.(check int32) "a defaults to 0" 0l r.a 653 660 654 661 (* ================================================================= ··· 685 692 (* Not valid UTF-8 *) 686 693 let wire = Protobuf.encode_string with_str_codec { s = raw } in 687 694 match Protobuf.decode_string with_str_codec wire with 688 - | Error msg -> Alcotest.failf "non-UTF-8 string must decode: %s" msg 695 + | Error msg -> 696 + Alcotest.failf "non-UTF-8 string must decode: %s" 697 + (Protobuf.Error.to_string msg) 689 698 | Ok r -> Alcotest.(check string) "roundtrip" raw r.s 690 699 691 700 (* ================================================================= ··· 701 710 let v = { entries = [ ("k", 1l); ("k", 2l); ("x", 99l); ("k", 3l) ] } in 702 711 let wire = Protobuf.encode_string dict_codec v in 703 712 match Protobuf.decode_string dict_codec wire with 704 - | Error msg -> Alcotest.fail msg 713 + | Error e -> Alcotest.fail (Protobuf.Error.to_string e) 705 714 | Ok r -> 706 715 Alcotest.(check int) "entry count preserved" 4 (List.length r.entries) 707 716 ··· 728 737 done; 729 738 let wire = Buffer.contents buf in 730 739 match Protobuf.decode_string rep_codec wire with 731 - | Error msg -> Alcotest.failf "many-repeated rejected: %s" msg 740 + | Error msg -> 741 + Alcotest.failf "many-repeated rejected: %s" (Protobuf.Error.to_string msg) 732 742 | Ok r -> 733 743 Alcotest.(check int) "count" n (List.length r.tags); 734 744 Alcotest.(check string) "first" "x" (List.hd r.tags)