Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for BSON element 11: uint64 #4380

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
5 changes: 2 additions & 3 deletions docs/mkdocs/docs/features/binary_formats/bson.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ The library uses the following mapping from JSON values types to BSON types:
| number_integer | -2147483648..2147483647 | int32 | 0x10 |
| number_integer | 2147483648..9223372036854775807 | int64 | 0x12 |
| number_unsigned | 0..2147483647 | int32 | 0x10 |
| number_unsigned | 2147483648..9223372036854775807 | int64 | 0x12 |
| number_unsigned | 9223372036854775808..18446744073709551615 | -- | -- |
| number_unsigned | 2147483648..18446744073709551615 | int64 | 0x11 |
| number_float | *any value* | double | 0x01 |
| string | *any value* | string | 0x02 |
| array | *any value* | document | 0x04 |
Expand Down Expand Up @@ -73,7 +72,7 @@ The library maps BSON record types to JSON value types as follows:
| Symbol | 0x0E | *unsupported* |
| JavaScript Code | 0x0F | *unsupported* |
| int32 | 0x10 | number_integer |
| Timestamp | 0x11 | *unsupported* |
| Timestamp | 0x11 | number_unsigned |
| 128-bit decimal float | 0x13 | *unsupported* |
| Max Key | 0x7F | *unsupported* |
| Min Key | 0xFF | *unsupported* |
Expand Down
2 changes: 1 addition & 1 deletion docs/mkdocs/docs/home/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@ A parsed number could not be stored as without changing it to NaN or INF.

### json.exception.out_of_range.407

UBJSON and BSON only support integer numbers up to 9223372036854775807.
UBJSON only supports integer numbers up to 9223372036854775807.

!!! failure "Example message"

Expand Down
6 changes: 6 additions & 0 deletions include/nlohmann/detail/input/binary_reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,12 @@ class binary_reader
return get_number<std::int32_t, true>(input_format_t::bson, value) && sax->number_integer(value);
}

case 0x11: // uint64
{
std::uint64_t value{};
return get_number<std::uint64_t, true>(input_format_t::bson, value) && sax->number_integer(value);
}

case 0x12: // int64
{
std::int64_t value{};
Expand Down
10 changes: 5 additions & 5 deletions include/nlohmann/detail/output/binary_writer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,7 @@ class binary_writer
{
return (value <= static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)()))
? sizeof(std::int32_t)
: sizeof(std::int64_t);
: sizeof(std::uint64_t);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems reasonable.

}

/*!
Expand All @@ -1080,14 +1080,14 @@ class binary_writer
write_bson_entry_header(name, 0x10 /* int32 */);
write_number<std::int32_t>(static_cast<std::int32_t>(j.m_data.m_value.number_unsigned), true);
}
else if (j.m_data.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
else if (j.m_data.m_value.number_unsigned <= std::numeric_limits<std::uint64_t>::max())
slowriot marked this conversation as resolved.
Show resolved Hide resolved
{
write_bson_entry_header(name, 0x12 /* int64 */);
write_number<std::int64_t>(static_cast<std::int64_t>(j.m_data.m_value.number_unsigned), true);
write_bson_entry_header(name, 0x11 /* uint64 */);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems reasonable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is logical, but I have some concerns about backward compatibility. It may break the following scenario:

void write()
{
    const uint64_t = 9223372036854775807L;
    json const j = {
            {"entry", l}
    };
    const std::vector<uint8_t> bson = json::to_bson(j);
    saveToTable1(bson);
    saveToTable2(bson);
}

write(); // was called before the code changes in this PR
void read()
{
    const std::vector<uint8_t> bson1 = loadFromTable1();
    json const j = json::from_bson(bson);
    const std::vector<uint8_t> bson1_roundtrip = json::to_bson(j);

    const std::vector<uint8_t> bson2 = loadFromTable2();

    if (equals(bson1_roundtrip, bson2)) { 
        ...
    }
}

read(); 

With the changes in this PR, the comparison between bson1_roundtrip and bson2 will fail. It is hard to tell if clients rely on this behavior, but I would like to highlight this potential issue.

write_number<std::uint64_t>(static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned), true);
}
else
{
JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BSON as it does not fit int64"), &j));
JSON_THROW(out_of_range::create(407, concat("unsigned integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BSON as it does not fit into uint64"), &j));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read the coverage information correctly, then this line is not covered in a test. Please check and add a test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can only happen if number_unsigned has a larger size than 64 bits. Does the library support that? If not, then this can't be hit.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, I failed to compile the library with 128-bit integers. For CBOR, we assume all unsigned integers to fit into 64 bits, so I think it's fair to do the same here. (Any code like the one above could not be tested anyway.)

}
}

Expand Down
16 changes: 11 additions & 5 deletions single_include/nlohmann/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9465,6 +9465,12 @@ class binary_reader
return get_number<std::int32_t, true>(input_format_t::bson, value) && sax->number_integer(value);
}

case 0x11: // uint64
{
std::uint64_t value{};
return get_number<std::uint64_t, true>(input_format_t::bson, value) && sax->number_integer(value);
}

case 0x12: // int64
{
std::int64_t value{};
Expand Down Expand Up @@ -16106,7 +16112,7 @@ class binary_writer
{
return (value <= static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)()))
? sizeof(std::int32_t)
: sizeof(std::int64_t);
: sizeof(std::uint64_t);
}

/*!
Expand All @@ -16120,14 +16126,14 @@ class binary_writer
write_bson_entry_header(name, 0x10 /* int32 */);
write_number<std::int32_t>(static_cast<std::int32_t>(j.m_data.m_value.number_unsigned), true);
}
else if (j.m_data.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
else if (j.m_data.m_value.number_unsigned <= static_cast<std::uint64_t>((std::numeric_limits<std::uint64_t>::max)()))
{
write_bson_entry_header(name, 0x12 /* int64 */);
write_number<std::int64_t>(static_cast<std::int64_t>(j.m_data.m_value.number_unsigned), true);
write_bson_entry_header(name, 0x11 /* uint64 */);
write_number<std::uint64_t>(static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned), true);
}
else
{
JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BSON as it does not fit int64"), &j));
JSON_THROW(out_of_range::create(407, concat("unsigned integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BSON as it does not fit into uint64"), &j));
}
}

Expand Down
22 changes: 12 additions & 10 deletions tests/src/unit-bson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,6 @@ TEST_CASE("BSON")

SECTION("non-empty object with unsigned integer (64-bit) member")
{
// directly encoding uint64 is not supported in bson (only for timestamp values)
json const j =
{
{ "entry", std::uint64_t{0x1234567804030201} }
Expand All @@ -340,7 +339,7 @@ TEST_CASE("BSON")
std::vector<std::uint8_t> const expected =
{
0x14, 0x00, 0x00, 0x00, // size (little endian)
0x12, /// entry: int64
0x11, /// entry: uint64
'e', 'n', 't', 'r', 'y', '\x00',
0x01, 0x02, 0x03, 0x04, 0x78, 0x56, 0x34, 0x12,
0x00 // end marker
Expand Down Expand Up @@ -1134,7 +1133,7 @@ TEST_CASE("BSON numerical data")
std::vector<std::uint8_t> const expected_bson =
{
0x14u, 0x00u, 0x00u, 0x00u, // size (little endian)
0x12u, /// entry: int64
0x11u, /// entry: uint64
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a consequence of the change in write_bson_unsigned above.

'e', 'n', 't', 'r', 'y', '\x00',
static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu),
static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu),
Expand Down Expand Up @@ -1184,7 +1183,7 @@ TEST_CASE("BSON numerical data")
std::vector<std::uint8_t> const expected_bson =
{
0x14u, 0x00u, 0x00u, 0x00u, // size (little endian)
0x12u, /// entry: int64
0x11u, /// entry: uint64
'e', 'n', 't', 'r', 'y', '\x00',
static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu),
static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu),
Expand All @@ -1197,12 +1196,15 @@ TEST_CASE("BSON numerical data")
0x00u // end marker
};

CHECK_THROWS_AS(json::to_bson(j), json::out_of_range&);
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_STD_STR(json::to_bson(j), "[json.exception.out_of_range.407] (/entry) integer number " + std::to_string(i) + " cannot be represented by BSON as it does not fit int64");
#else
CHECK_THROWS_WITH_STD_STR(json::to_bson(j), "[json.exception.out_of_range.407] integer number " + std::to_string(i) + " cannot be represented by BSON as it does not fit int64");
#endif
const auto bson = json::to_bson(j);
CHECK(bson == expected_bson);

auto j_roundtrip = json::from_bson(bson);

CHECK(j.at("entry").is_number_unsigned());
CHECK(j_roundtrip.at("entry").is_number_integer());
CHECK(j_roundtrip == j);
CHECK(json::from_bson(bson, true, false) == j);
}
}

Expand Down
Loading