Skip to content

OCaml Protoc Plugin incorretly parses Int64.min_int in sint64 field #45

@mjschwenne

Description

@mjschwenne

Hello!

While working with a protobuf struct with a sint64 field, I encoded the minimum possible 64-bit signed integer into a protobuf struct. I did set int64_as_int=false while generating the OCaml code to get true 64-bit integers rather than the OCaml default 63-bit integer. That worked fine, but when I attempted to decode the struct, the number that was returned was -1, not -9223372036854775808.

I've verified that the encoding was correct by writing the encoded message to a file both manually inspecting it with xxd and using protoscope.

Below is a minimal working example of this issue. Start with a very simple protobuf file

syntax = "proto3";

message s64 {
  sint64 field = 1;
}

and the OCaml code generation from protoc:

protoc --ocaml_out=mwe --ocaml_opt="annot=[@@deriving show];int64_as_int=false" s64.proto

which produces this OCaml file

Generated OCaml file
(********************************************************)
(*           AUTOGENERATED FILE - DO NOT EDIT!          *)
(********************************************************)
(* Generated by: ocaml-protoc-plugin                    *)
(* https://github.com/andersfugmann/ocaml-protoc-plugin *)
(********************************************************)
(*
  Source: proto/s64.proto
  Syntax: proto3
  Parameters:
    debug=false
    annot='[@@deriving show]'
    opens=[]
    int64_as_int=false
    int32_as_int=true
    fixed_as_int=false
    singleton_record=false
    prefix_output_with_package=false
*)
[@@@ocaml.alert "-protobuf"] (* Disable deprecation warnings for protobuf*)
(**/**)
module Runtime' = Ocaml_protoc_plugin [@@warning "-33"]
module Imported'modules = struct
end
(**/**)
module rec S64 : sig
  type t = (int64) [@@deriving show]
  val make: ?field:int64 -> unit -> t
  (** Helper function to generate a message using default values *)

  val to_proto: t -> Runtime'.Writer.t
  (** Serialize the message to binary format *)

  val from_proto: Runtime'.Reader.t -> (t, [> Runtime'.Result.error]) result
  (** Deserialize from binary format *)

  val to_json: Runtime'.Json_options.t -> t -> Runtime'.Json.t
  (** Serialize to Json (compatible with Yojson.Basic.t) *)

  val from_json: Runtime'.Json.t -> (t, [> Runtime'.Result.error]) result
  (** Deserialize from Json (compatible with Yojson.Basic.t) *)

  val name: unit -> string
  (** Fully qualified protobuf name of this message *)

  (**/**)
  type make_t = ?field:int64 -> unit -> t
  val merge: t -> t -> t
  val to_proto': Runtime'.Writer.t -> t -> unit
  val from_proto_exn: Runtime'.Reader.t -> t
  val from_json_exn: Runtime'.Json.t -> t
  (**/**)
end = struct
  module This'_ = S64
  let name () = ".s64"
  type t = (int64) [@@deriving show]
  type make_t = ?field:int64 -> unit -> t
  let make ?(field = 0L) () = (field)
  let merge =
  let merge_field = Runtime'.Merge.merge Runtime'.Spec.( basic ((1, "field", "field"), sint64, (0L)) ) in
  fun (t1_field) (t2_field) -> merge_field t1_field t2_field
  let spec () = Runtime'.Spec.( basic ((1, "field", "field"), sint64, (0L)) ^:: nil )
  let to_proto' =
    let serialize = Runtime'.apply_lazy (fun () -> Runtime'.Serialize.serialize (spec ())) in
    fun writer (field) -> serialize writer field

  let to_proto t = let writer = Runtime'.Writer.init () in to_proto' writer t; writer
  let from_proto_exn =
    let constructor field = (field) in
    Runtime'.apply_lazy (fun () -> Runtime'.Deserialize.deserialize (spec ()) constructor)
  let from_proto writer = Runtime'.Result.catch (fun () -> from_proto_exn writer)
  let to_json options =
    let serialize = Runtime'.Serialize_json.serialize ~message_name:(name ()) (spec ()) options in
    fun (field) -> serialize field
  let from_json_exn =
    let constructor field = (field) in
    Runtime'.apply_lazy (fun () -> Runtime'.Deserialize_json.deserialize ~message_name:(name ()) (spec ()) constructor)
  let from_json json = Runtime'.Result.catch (fun () -> from_json_exn json)
end

Then serialize and parse Int64.min_int from the main program, saving the encoded file for examination later:

open S64
open Stdio
open Ocaml_protoc_plugin

let () =
  let s64 = S64.make ~field:Int64.min_int () in
  let enc = Writer.contents (S64.to_proto s64) in
  let out = open_out_bin "msg.bin" in
  Printf.fprintf out "%s" enc;
  close_out out;
  match S64.from_proto (Reader.create enc) with
  | Error e ->
      printf "Error parsing struct: %s\n" (Runtime'.Result.show_error e)
  | Ok z ->
      printf "Parsed: %s should be %s\n" (Int64.to_string z)
        (Int64.to_string Int64.min_int)

Running this program outputs Parsed: -1 should be -9223372036854775808

The number is correctly encoded as 64 ones, and can be verified with protoscope or by hand with xxd:

protoscope -descriptor-set s64.descriptor -message-type s64 msg.bin
1: -9223372036854775808z
xxd -b msg.bin 
00000000: 00001000 11111111 11111111 11111111 11111111 11111111  ......
00000006: 11111111 11111111 11111111 11111111 00000001           .....

Which is why I suspect that the issue occurs while decoding the integer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions