Search code examples
rustprotocol-buffersprost

Message containing only an enum field has 0 encoded len


I'm trying to serialize data, which is specified by the following protobuf file:

syntax = "proto3";

package efficient_servers.protobuf;

message Request {
  oneof msg {
    Walk walk = 1;
    OneToOne oneToOne = 2;
    OneToAll oneToAll = 3;
    Reset reset = 4;
  }
}

message Walk {
  repeated Location locations = 1; // repeated n times, determines edge directions
  repeated uint32 lengths = 2; // [mm], repeated n-1 times
}

message OneToOne {
  Location origin = 1;
  Location destination = 2;
}

message OneToAll {
  Location origin = 1;
}

message Reset {}

message Location {
  int32 x = 1; // [mm]
  int32 y = 2; // [mm]
}

message Response {
  enum Status {OK = 0; ERROR = 1;};

  Status status = 1; // Always present
  string errMsg = 2;
  uint64 shortest_path_length = 3; // [mm]
  uint64 total_length = 4; // [mm]
}

I need to create a Response, that has only the status field filled out with the OK variant, the encoded_len() function returns 0. To me, this seems, that the default values of the message are ignored by the encoder, which is understandable, it wants to minimize the message size. But what if I want to send just a message with a single default field? In thate case, the message size amounts to zero and no message is encoded.

Is there a way around this nonsense? Note that I cannot edit the .proto specification file.

I've tried using just the ERROR variant of the status enum, and that returned a valid, non-zero encoded_len(), but I also need to serialize to OK variant.

I'm using the following code:

pub fn generate_response(data: Result<RequestResponse, String>) -> BytesMut {
    let mut response = Response::default();
    match data {
        Ok(request_response) => {
            match request_response.request {
                Msg::Reset(_) => {
                    response.set_status(Status::Ok);
                },
                Msg::OneToAll(_) => {
                    response.set_status(Status::Ok);
                    response.shortest_path_length = request_response.length;
                }
                Msg::OneToOne(_) => {
                    response.set_status(Status::Ok);
                    response.total_length = request_response.length;
                }
                Msg::Walk(_) => {
                    response.set_status(Status::Ok);
                },
            }
        }
        Err(msg) => {
            response.set_status(Status::Error);
            response.err_msg = msg.parse().unwrap();
        }
    }
    let encoded_len = response.encoded_len();
    let mut buf = BytesMut::with_capacity(encoded_len);
    buf.resize(encoded_len, 0);
    response.encode(&mut buf).unwrap();
    return buf;
}

To summarize, the encoded_len() function returns 0 when ResponseRequest.request is either Msg::Reset or Msg:Walk, which means, that no data i serialized, and therefore I have no data to send.


Solution

  • But what if I want to send just a message with a single default field? In that case, the message size amounts to zero and no message is encoded.

    So you send a response with length 0, and the receiving side decodes that as a Response, and you end up with a Response message instance which just contains the default values.

    That shouldn't cause any problems, assuming whatever transport you're using is capable of saying "the response has length X", and therefore is capable of handling an empty response.

    Note that if you have a Response field within some other message type, there's still a difference between "this field is populated with the default message" and "this field isn't populated" - you'd end up with the tag for the field and then the 0-byte length-prefixed message.