Skip to content

Commit

Permalink
Add config option to print 64-bit integers in JSON as unquoted ints i…
Browse files Browse the repository at this point in the history
…f they can be losslessly converted into a 64-bit float.

PiperOrigin-RevId: 516625978
  • Loading branch information
protobuf-github-bot authored and copybara-github committed Mar 14, 2023
1 parent 626c7e7 commit 330e10d
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 27 deletions.
43 changes: 41 additions & 2 deletions src/google/protobuf/json/internal/unparser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
#include "google/protobuf/json/internal/unparser.h"

#include <cfloat>
#include <cmath>
#include <complex>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>

#include "google/protobuf/descriptor.h"
Expand Down Expand Up @@ -104,6 +106,33 @@ void WriteEnum(JsonWriter& writer, Field<Traits> field, int32_t value,
}
}

// Returns true if x round-trips through being cast to a double, i.e., if
// x is represenable exactly as a double. This is a slightly weaker condition
// than x < 2^52.
template <typename Int>
bool RoundTripsThroughDouble(Int x) {
auto d = static_cast<double>(x);
// d has guaranteed to be finite with no fractional part, because it came from
// an integer, so we only need to check that it is not outside of the
// representable range of `int`. The way to do this is somewhat not obvious:
// UINT64_MAX isn't representable, and what it gets rounded to when we go
// int->double is unspecified!
//
// Thus, we have to go through ldexp.
double min = 0;
double max_plus_one = std::ldexp(1.0, sizeof(Int) * 8);
if (std::is_signed<Int>::value) {
max_plus_one /= 2;
min = -max_plus_one;
}

if (d < min || d >= max_plus_one) {
return false;
}

return static_cast<Int>(d) == x;
}

// Mutually recursive with functions that follow.
template <typename Traits>
absl::Status WriteMessage(JsonWriter& writer, const Msg<Traits>& msg,
Expand Down Expand Up @@ -143,14 +172,24 @@ absl::Status WriteSingular(JsonWriter& writer, Field<Traits> field,
case FieldDescriptor::TYPE_INT64: {
auto x = Traits::GetInt64(field, std::forward<Args>(args)...);
RETURN_IF_ERROR(x.status());
writer.Write(MakeQuoted(*x));
if (writer.options().unquote_int64_if_possible &&
RoundTripsThroughDouble(*x)) {
writer.Write(*x);
} else {
writer.Write(MakeQuoted(*x));
}
break;
}
case FieldDescriptor::TYPE_FIXED64:
case FieldDescriptor::TYPE_UINT64: {
auto x = Traits::GetUInt64(field, std::forward<Args>(args)...);
RETURN_IF_ERROR(x.status());
writer.Write(MakeQuoted(*x));
if (writer.options().unquote_int64_if_possible &&
RoundTripsThroughDouble(*x)) {
writer.Write(*x);
} else {
writer.Write(MakeQuoted(*x));
}
break;
}
case FieldDescriptor::TYPE_SFIXED32:
Expand Down
32 changes: 16 additions & 16 deletions src/google/protobuf/json/internal/writer.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ struct WriterOptions {
bool always_print_enums_as_ints = false;
// Whether to preserve proto field names
bool preserve_proto_field_names = false;
// If set, int64 values that can be represented exactly as a double are
// printed without quotes.
bool unquote_int64_if_possible = false;
// The original parser used by json_util2 accepted a number of non-standard
// options. Setting this flag enables them.
//
Expand Down Expand Up @@ -153,8 +156,19 @@ class JsonWriter {
Write(view);
}

void Write(int64_t) = delete;
void Write(uint64_t) = delete;
void Write(int64_t val) {
char buf[22];
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
absl::string_view view(buf, static_cast<size_t>(len));
Write(view);
}

void Write(uint64_t val) {
char buf[22];
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
absl::string_view view(buf, static_cast<size_t>(len));
Write(view);
}

template <typename... Ts>
void Write(Quoted<Ts...> val) {
Expand Down Expand Up @@ -206,20 +220,6 @@ class JsonWriter {

void WriteQuoted(absl::string_view val) { WriteEscapedUtf8(val); }

void WriteQuoted(int64_t val) {
char buf[22];
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
absl::string_view view(buf, static_cast<size_t>(len));
Write(view);
}

void WriteQuoted(uint64_t val) {
char buf[22];
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
absl::string_view view(buf, static_cast<size_t>(len));
Write(view);
}

// Tries to write a non-finite double if necessary; returns false if
// nothing was written.
bool MaybeWriteSpecialFp(double val);
Expand Down
2 changes: 2 additions & 0 deletions src/google/protobuf/json/json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ absl::Status BinaryToJsonStream(google::protobuf::util::TypeResolver* resolver,
opts.preserve_proto_field_names = options.preserve_proto_field_names;
opts.always_print_enums_as_ints = options.always_print_enums_as_ints;
opts.always_print_primitive_fields = options.always_print_primitive_fields;
opts.unquote_int64_if_possible = options.unquote_int64_if_possible;

// TODO(b/234868512): Drop this setting.
opts.allow_legacy_syntax = true;
Expand Down Expand Up @@ -110,6 +111,7 @@ absl::Status MessageToJsonString(const Message& message, std::string* output,
opts.preserve_proto_field_names = options.preserve_proto_field_names;
opts.always_print_enums_as_ints = options.always_print_enums_as_ints;
opts.always_print_primitive_fields = options.always_print_primitive_fields;
opts.unquote_int64_if_possible = options.unquote_int64_if_possible;

// TODO(b/234868512): Drop this setting.
opts.allow_legacy_syntax = true;
Expand Down
12 changes: 3 additions & 9 deletions src/google/protobuf/json/json.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ struct ParseOptions {
// this option. If your enum needs to support different casing, consider using
// allow_alias instead.
bool case_insensitive_enum_parsing = false;

ParseOptions()
: ignore_unknown_fields(false), case_insensitive_enum_parsing(false) {}
};

struct PrintOptions {
Expand All @@ -75,12 +72,9 @@ struct PrintOptions {
bool always_print_enums_as_ints = false;
// Whether to preserve proto field names
bool preserve_proto_field_names = false;

PrintOptions()
: add_whitespace(false),
always_print_primitive_fields(false),
always_print_enums_as_ints(false),
preserve_proto_field_names(false) {}
// If set, int64 values that can be represented exactly as a double are
// printed without quotes.
bool unquote_int64_if_possible = false;
};

// Converts from protobuf message to JSON and appends it to |output|. This is a
Expand Down
22 changes: 22 additions & 0 deletions src/google/protobuf/json/json_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,28 @@ TEST_P(JsonTest, EvilString) {
EXPECT_EQ(m->string_value(), "\n\r\b\f\1\2\3");
}

TEST_P(JsonTest, Unquoted64) {
TestMessage m;
m.add_repeated_int64_value(0);
m.add_repeated_int64_value(42);
m.add_repeated_int64_value(-((int64_t{1} << 60) + 1));
m.add_repeated_int64_value(INT64_MAX);
// This is a power of two and is therefore representable.
m.add_repeated_int64_value(INT64_MIN);
m.add_repeated_uint64_value(0);
m.add_repeated_uint64_value(42);
m.add_repeated_uint64_value((uint64_t{1} << 60) + 1);
// This will be UB without the min/max check in RoundTripsThroughDouble().
m.add_repeated_uint64_value(UINT64_MAX);

PrintOptions opts;
opts.unquote_int64_if_possible = true;
EXPECT_THAT(
ToJson(m, opts),
R"({"repeatedInt64Value":[0,42,"-1152921504606846977","9223372036854775807",-9223372036854775808],)"
R"("repeatedUint64Value":[0,42,"1152921504606846977","18446744073709551615"]})");
}

TEST_P(JsonTest, TestAlwaysPrintEnumsAsInts) {
TestMessage orig;
orig.set_enum_value(proto3::BAR);
Expand Down

0 comments on commit 330e10d

Please sign in to comment.