Skip to content

Commit 5a3472c

Browse files
authored
Throw a better error for out of range JSON integers (#1578)
See: sourcemeta/jsonschema#235 Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 0af2764 commit 5a3472c

File tree

5 files changed

+79
-10
lines changed

5 files changed

+79
-10
lines changed

src/core/json/include/sourcemeta/core/json_error.h

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@ class SOURCEMETA_CORE_JSON_EXPORT JSONParseError : public std::exception {
2626
JSONParseError(const std::uint64_t line, const std::uint64_t column)
2727
: line_{line}, column_{column} {}
2828

29+
/// Create a parsing error with a custom error
30+
JSONParseError(const std::uint64_t line, const std::uint64_t column,
31+
std::string message)
32+
: line_{line}, column_{column}, message_{std::move(message)} {}
33+
2934
[[nodiscard]] auto what() const noexcept -> const char * override {
30-
return "Failed to parse the JSON document";
35+
return this->message_.c_str();
3136
}
3237

3338
/// Get the line number of the error
@@ -41,6 +46,21 @@ class SOURCEMETA_CORE_JSON_EXPORT JSONParseError : public std::exception {
4146
private:
4247
std::uint64_t line_;
4348
std::uint64_t column_;
49+
std::string message_{"Failed to parse the JSON document"};
50+
};
51+
52+
/// @ingroup json
53+
/// This class represents a numeric integer limit parsing error
54+
class SOURCEMETA_CORE_JSON_EXPORT JSONParseIntegerLimitError
55+
: public JSONParseError {
56+
public:
57+
/// Create a parsing error
58+
JSONParseIntegerLimitError(const std::uint64_t line,
59+
const std::uint64_t column)
60+
: JSONParseError{
61+
line, column,
62+
"The JSON value is not representable by the IETF RFC 8259 "
63+
"interoperable signed integer range"} {}
4464
};
4565

4666
/// @ingroup json
@@ -49,15 +69,17 @@ class SOURCEMETA_CORE_JSON_EXPORT JSONFileParseError : public JSONParseError {
4969
public:
5070
/// Create a file parsing error
5171
JSONFileParseError(const std::filesystem::path &path,
52-
const std::uint64_t line, const std::uint64_t column)
53-
: JSONParseError{line, column}, path_{path} {}
72+
const std::uint64_t line, const std::uint64_t column,
73+
std::string message)
74+
: JSONParseError{line, column, std::move(message)}, path_{path} {}
5475

5576
/// Create a file parsing error from a parse error
5677
JSONFileParseError(const std::filesystem::path &path,
5778
const JSONParseError &parent)
58-
: JSONParseError{parent.line(), parent.column()}, path_{path} {}
79+
: JSONParseError{parent.line(), parent.column(), parent.what()},
80+
path_{path} {}
5981

60-
/// Get the fiel path of the error
82+
/// Get the file path of the error
6183
[[nodiscard]] auto path() const noexcept -> const std::filesystem::path {
6284
return path_;
6385
}

src/core/json/json.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ auto read_json(const std::filesystem::path &path,
5555
auto stream{read_file(path)};
5656
try {
5757
return parse_json(stream, callback);
58+
} catch (const JSONParseIntegerLimitError &error) {
59+
// For producing better error messages
60+
throw JSONFileParseError(path, error);
5861
} catch (const JSONParseError &error) {
5962
// For producing better error messages
6063
throw JSONFileParseError(path, error);

src/core/json/parser.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ auto parse_number_integer(const std::uint64_t line, const std::uint64_t column,
293293
try {
294294
return std::stoll(string);
295295
} catch (const std::out_of_range &) {
296-
throw JSONParseError(line, column);
296+
throw JSONParseIntegerLimitError(line, column);
297297
}
298298
}
299299

test/json/json_parse_error_test.cc

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
1-
#include <exception>
21
#include <gtest/gtest.h>
2+
3+
#include <exception>
34
#include <sstream>
45

56
#include <sourcemeta/core/json.h>
67

7-
#define EXPECT_PARSE_ERROR(input, expected_line, expected_column) \
8+
#define __EXPECT_PARSE_ERROR(input, expected_line, expected_column, \
9+
expected_error, expected_message) \
810
try { \
911
sourcemeta::core::parse_json((input)); \
1012
FAIL() << "The parse function was expected to throw"; \
11-
} catch (const sourcemeta::core::JSONParseError &error) { \
13+
} catch (const sourcemeta::core::expected_error &error) { \
1214
EXPECT_EQ(error.line(), expected_line); \
1315
EXPECT_EQ(error.column(), expected_column); \
16+
EXPECT_STREQ(error.what(), expected_message); \
1417
SUCCEED(); \
1518
} catch (const std::exception &) { \
16-
FAIL() << "The parse function was expected to throw a parse error"; \
19+
FAIL() \
20+
<< "The parse function was expected to throw the desired parse error"; \
1721
}
1822

23+
#define EXPECT_PARSE_ERROR(input, expected_line, expected_column) \
24+
__EXPECT_PARSE_ERROR(input, expected_line, expected_column, JSONParseError, \
25+
"Failed to parse the JSON document");
26+
27+
#define EXPECT_PARSE_ERROR_INTEGER_LIMIT(input, expected_line, \
28+
expected_column) \
29+
__EXPECT_PARSE_ERROR(input, expected_line, expected_column, \
30+
JSONParseIntegerLimitError, \
31+
"The JSON value is not representable by the IETF RFC " \
32+
"8259 interoperable signed integer range");
33+
1934
TEST(JSON_parse_error, boolean_true_invalid) {
2035
std::istringstream input{"trrue"};
2136
EXPECT_PARSE_ERROR(input, 1, 3);
@@ -513,6 +528,16 @@ TEST(JSON_parse_error, integer_negative_with_leading_zero) {
513528
EXPECT_PARSE_ERROR(input, 1, 3);
514529
}
515530

531+
TEST(JSON_parse_error, integer_slightly_over_64_bit_positive_limit) {
532+
std::istringstream input{"9223372036854776000"};
533+
EXPECT_PARSE_ERROR_INTEGER_LIMIT(input, 1, 1);
534+
}
535+
536+
TEST(JSON_parse_error, integer_slightly_over_64_bit_positive_limit_in_object) {
537+
std::istringstream input{"{\"foo\":9223372036854776000}"};
538+
EXPECT_PARSE_ERROR_INTEGER_LIMIT(input, 1, 8);
539+
}
540+
516541
TEST(JSON_parse_error, integer_negative_with_leading_zero_and_space) {
517542
std::istringstream input{"[-0 5]"};
518543
EXPECT_PARSE_ERROR(input, 1, 5);
@@ -638,6 +663,24 @@ TEST(JSON_parse_error, read_json_invalid) {
638663
std::filesystem::path{TEST_DIRECTORY} / "stub_invalid.json");
639664
EXPECT_EQ(error.line(), 3);
640665
EXPECT_EQ(error.column(), 9);
666+
EXPECT_STREQ(error.what(), "Failed to parse the JSON document");
667+
} catch (...) {
668+
FAIL() << "The parse function was expected to throw a file parse error";
669+
}
670+
}
671+
672+
TEST(JSON_parse_error, read_json_invalid_bigint) {
673+
try {
674+
sourcemeta::core::read_json(std::filesystem::path{TEST_DIRECTORY} /
675+
"stub_bigint.json");
676+
} catch (const sourcemeta::core::JSONFileParseError &error) {
677+
EXPECT_EQ(error.path(),
678+
std::filesystem::path{TEST_DIRECTORY} / "stub_bigint.json");
679+
EXPECT_EQ(error.line(), 1);
680+
EXPECT_EQ(error.column(), 1);
681+
EXPECT_STREQ(error.what(),
682+
"The JSON value is not representable by the IETF RFC 8259 "
683+
"interoperable signed integer range");
641684
} catch (...) {
642685
FAIL() << "The parse function was expected to throw a file parse error";
643686
}

test/json/stub_bigint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9223372036854776000

0 commit comments

Comments
 (0)