Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy Markdown
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())
Comment thread
slowriot marked this conversation as resolved.
Outdated
{
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
Copy Markdown
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
Copy Markdown
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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

For compatibility, we could serialize unsigned integers until int64_max with 0x12 and all numbers larger with 0x11.

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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