diff --git a/src/core/gzip/deflate.h b/src/core/gzip/deflate.h index 592341a45..d45ddada1 100644 --- a/src/core/gzip/deflate.h +++ b/src/core/gzip/deflate.h @@ -8,6 +8,7 @@ #include // std::min #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t, std::uint16_t #include // std::memcpy @@ -213,7 +214,10 @@ class DeflateDecoder { } all_lengths[index++] = 0; } - } else if (symbol == 18) { + } else { + // The code length tree is built over a 19 symbol alphabet, so its + // decoder can never hand back anything past symbol 18 + assert(symbol == 18); const auto repeats{this->reader_->read_bits(7) + 11}; for (std::size_t step = 0; step < repeats; ++step) { if (index >= all_lengths.size()) { @@ -221,8 +225,6 @@ class DeflateDecoder { } all_lengths[index++] = 0; } - } else { - throw GZIPError{"Invalid code length symbol"}; } } diff --git a/src/core/gzip/huffman.h b/src/core/gzip/huffman.h index 01c72b330..e3d243ef7 100644 --- a/src/core/gzip/huffman.h +++ b/src/core/gzip/huffman.h @@ -7,6 +7,7 @@ #include // std::ranges::fill #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t, std::uint16_t @@ -37,9 +38,10 @@ class HuffmanDecoder { std::ranges::fill(this->lut_, std::uint16_t{0}); for (std::size_t symbol = 0; symbol < length_count; ++symbol) { - if (lengths[symbol] > MAX_HUFFMAN_BITS) { - throw GZIPError{"Huffman code length out of range"}; - } + // The fixed trees use lengths five to nine, the code length tree + // reads three bit values, and the dynamic trees copy code length + // symbols below sixteen, so no caller can supply a longer length + assert(lengths[symbol] <= MAX_HUFFMAN_BITS); this->count_[lengths[symbol]]++; } diff --git a/src/core/http/helpers.h b/src/core/http/helpers.h index 9b6982e7f..146531e45 100644 --- a/src/core/http/helpers.h +++ b/src/core/http/helpers.h @@ -4,6 +4,7 @@ #include #include +#include // assert #include // std::size_t #include // std::uint8_t, std::uint16_t #include // std::string_view @@ -29,11 +30,12 @@ inline auto http_media_specificity(const std::string_view range, return 1; } const auto range_slash{range.find('/')}; - const auto candidate_slash{candidate.find('/')}; - if (range_slash == std::string_view::npos || - candidate_slash == std::string_view::npos) { + if (range_slash == std::string_view::npos) { return 0; } + // Every caller validates its candidate media types upfront + const auto candidate_slash{candidate.find('/')}; + assert(candidate_slash != std::string_view::npos); if (range.size() - range_slash != 2 || range[range_slash + 1] != '*') { return 0; } diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h index 114d6aac1..b2e6e1c49 100644 --- a/src/core/jsonpath/parser.h +++ b/src/core/jsonpath/parser.h @@ -460,10 +460,8 @@ class JSONPathParser { // member-name-shorthand = name-first *name-char auto parse_shorthand_name() -> JSONPath::SelectorName { JSON::String name; - if (this->at_end()) { - this->fail(); - } - + // Every caller rejects a query that ends right before a shorthand name + assert(!this->at_end()); const auto first{static_cast(this->peek())}; if (first == '_' || is_alpha(static_cast(first))) { name += static_cast(first); @@ -641,36 +639,33 @@ class JSONPathParser { } // comparison-op = "==" / "!=" / "<=" / ">=" / "<" / ">" + // Callers look ahead for a full comparison operator before parsing one, so + // the first character is one of the four operator openers, and an equals + // sign always follows an exclamation mark or another equals sign auto parse_comparison_operator() -> JSONPath::FilterComparisonOperator { const char character{this->peek()}; if (character == '=' || character == '!') { this->position_ += 1; - if (this->at_end() || this->peek() != '=') { - this->fail(); - } - + assert(!this->at_end() && this->peek() == '='); this->position_ += 1; return character == '=' ? JSONPath::FilterComparisonOperator::Equal : JSONPath::FilterComparisonOperator::NotEqual; } - if (character == '<' || character == '>') { + assert(character == '<' || character == '>'); + this->position_ += 1; + const bool inclusive{!this->at_end() && this->peek() == '='}; + if (inclusive) { this->position_ += 1; - const bool inclusive{!this->at_end() && this->peek() == '='}; - if (inclusive) { - this->position_ += 1; - } - - if (character == '<') { - return inclusive ? JSONPath::FilterComparisonOperator::LessEqual - : JSONPath::FilterComparisonOperator::Less; - } + } - return inclusive ? JSONPath::FilterComparisonOperator::GreaterEqual - : JSONPath::FilterComparisonOperator::Greater; + if (character == '<') { + return inclusive ? JSONPath::FilterComparisonOperator::LessEqual + : JSONPath::FilterComparisonOperator::Less; } - this->fail(); + return inclusive ? JSONPath::FilterComparisonOperator::GreaterEqual + : JSONPath::FilterComparisonOperator::Greater; } auto parse_comparison_or_test() -> JSONPath::FilterExpression { diff --git a/src/core/uri/setters.cc b/src/core/uri/setters.cc index 05fe7192f..a0240dbca 100644 --- a/src/core/uri/setters.cc +++ b/src/core/uri/setters.cc @@ -4,6 +4,7 @@ #include "escaping.h" #include "normalize.h" +#include // assert #include // std::size_t #include // std::optional #include // std::string @@ -15,10 +16,8 @@ namespace { auto apply_leading_slash_transform(std::optional parsed_path, const bool needs_leading_slash) -> std::optional { - if (!parsed_path.has_value()) { - return parsed_path; - } - + // Every caller wraps a concrete string into the optional + assert(parsed_path.has_value()); const auto &path_value = parsed_path.value(); if (needs_leading_slash) { diff --git a/test/crypto/crypto_secure_test.cc b/test/crypto/crypto_secure_test.cc index d7aea5843..71dc43fbb 100644 --- a/test/crypto/crypto_secure_test.cc +++ b/test/crypto/crypto_secure_test.cc @@ -141,6 +141,7 @@ TEST(secure_string_from_a_pointer_and_a_length) { const char content[]{"hunter2"}; const sourcemeta::core::SecureString secret{content, 7}; EXPECT_EQ(secret.size(), 7); + EXPECT_EQ(secret, "hunter2"); EXPECT_EQ(secret[0], 'h'); EXPECT_EQ(secret[6], '2'); } @@ -148,6 +149,7 @@ TEST(secure_string_from_a_pointer_and_a_length) { TEST(secure_string_of_a_repeated_byte) { const sourcemeta::core::SecureString secret{5, 'x'}; EXPECT_EQ(secret.size(), 5); + EXPECT_EQ(secret, "xxxxx"); EXPECT_EQ(secret[0], 'x'); EXPECT_EQ(secret[4], 'x'); } @@ -157,6 +159,7 @@ TEST(secure_string_writes_a_byte_by_index) { secret[0] = 'H'; EXPECT_EQ(secret[0], 'H'); EXPECT_EQ(secret.size(), 7); + EXPECT_EQ(secret, "Hunter2"); } TEST(secure_allocator_instances_are_interchangeable) { diff --git a/test/gzip/gzip_streambuf_test.cc b/test/gzip/gzip_streambuf_test.cc index 727a8bbb2..ea4b8d2a8 100644 --- a/test/gzip/gzip_streambuf_test.cc +++ b/test/gzip/gzip_streambuf_test.cc @@ -985,3 +985,217 @@ TEST(dynamic_block_incomplete_code_throws) { EXPECT_EQ(std::string{error.what()}, "Incomplete Huffman code"); } } + +TEST(fixed_huffman_block_round_trip) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x13\xcb\x48\xcd\xc9\xc9\x57\xc8\xc0" + "\x4e\x02\x00\xf6\xd2\x53\x38\x1d\x00\x00\x00", + 29}; + const auto result{decompress_via_stream(compressed)}; + EXPECT_EQ(result, "hello hello hello hello hello"); +} + +TEST(fixed_huffman_empty_block) { + const std::string compressed{"\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x13\x03" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 20}; + const auto result{decompress_via_stream(compressed)}; + EXPECT_EQ(result, std::string{}); +} + +TEST(fixed_backref_before_any_output_throws) { + const std::string compressed{"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x03" + "\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 22}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, + "Backref distance exceeds bytes available"); + } +} + +TEST(fixed_unassigned_distance_code_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x4b\x04\x3e\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00", + 23}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, "Invalid Huffman code"); + } +} + +TEST(fixed_invalid_literal_length_symbol_throws) { + const std::string compressed{"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x1b" + "\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 22}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, "Invalid literal/length code"); + } +} + +TEST(dynamic_repeat_without_previous_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x05\x00\x02\x24\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00", + 24}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, + "Repeat-previous code length with no previous"); + } +} + +TEST(dynamic_code_length_overshoot_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x05\x00\x80\xe4\xff\x1f\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00", + 26}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, "Code length count overflow"); + } +} + +TEST(dynamic_code_length_cap_overflow_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xed\x1f\x80\xe4\xff\xff\x1f\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 27}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, "Code length count overflow"); + } +} + +TEST(dynamic_oversubscribed_code_lengths_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x05\x20\x92\x24\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00", + 24}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, "Over-subscribed Huffman code"); + } +} + +TEST(dynamic_block_without_distance_codes) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x05\xc0\x81\x08\x00\x00\x00\x00" + "\x20\x7f\xeb\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 32}; + const auto result{decompress_via_stream(compressed)}; + EXPECT_EQ(result, std::string{}); +} + +TEST(window_wraparound_round_trip) { + const std::string input(200000, 'a'); + const auto compressed{sourcemeta::core::gzip( + reinterpret_cast(input.data()), input.size())}; + const auto result{decompress_via_stream(compressed)}; + EXPECT_EQ(result.size(), input.size()); + EXPECT_EQ(result, input); +} + +TEST(window_wrapping_backrefs_round_trip) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x4b\x4c\x1a\x85\xa3\x70\x14" + "\x8e\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38" + "\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x47\xe1\x28" + "\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70" + "\x14\x8e\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51" + "\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x47\xe1" + "\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3" + "\x70\x14\x8e\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2" + "\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x47" + "\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85" + "\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e" + "\xc2\x51\x38\x0a\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a" + "\x47\xe1\x28\x1c\x85\xa3\x70\x14\x8e\xc2\x51\x38\x0a\x87\x36\x1c\x0d" + "\xab\xd1\x10\x00\x00\xba\xf1\x51\x56\xc0\x81\x00\x00", + 234}; + // The member trailer carries the CRC32 of the full expected output, so a + // successful decode already proves every byte, and the probes pin the + // alternating pattern at the start, both window boundary copies, and the end + const auto result{decompress_via_stream(compressed)}; + EXPECT_EQ(result.size(), 33216); + EXPECT_EQ(result.at(0), 'a'); + EXPECT_EQ(result.at(1), 'b'); + EXPECT_EQ(result.at(32700), 'a'); + EXPECT_EQ(result.at(32701), 'b'); + EXPECT_EQ(result.at(32958), 'a'); + EXPECT_EQ(result.at(32959), 'b'); + EXPECT_EQ(result.at(33214), 'a'); + EXPECT_EQ(result.at(33215), 'b'); +} + +TEST(dynamic_repeat_previous_cap_overflow_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xed\x1f\x84\x28\x7f\xff\xff" + "\xff\xff\x79\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 30}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, "Code length count overflow"); + } +} + +TEST(dynamic_repeat_zero_cap_overflow_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xed\x1f\x20\xe5\xff\xff\xde" + "\x7b\xef\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + 29}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &error) { + EXPECT_EQ(std::string{error.what()}, "Code length count overflow"); + } +} + +TEST(multiple_members_concatenate) { + const std::string first{"hello "}; + const std::string second{"world"}; + const auto compressed_first{sourcemeta::core::gzip( + reinterpret_cast(first.data()), first.size())}; + const auto compressed_second{sourcemeta::core::gzip( + reinterpret_cast(second.data()), second.size())}; + const auto result{ + decompress_via_stream(compressed_first + compressed_second)}; + EXPECT_EQ(result, "hello world"); +} + +TEST(trailing_garbage_after_member_is_ignored) { + const std::string input{"hello world"}; + const auto compressed{sourcemeta::core::gzip( + reinterpret_cast(input.data()), input.size())}; + const auto result{decompress_via_stream(compressed + "trailing garbage")}; + EXPECT_EQ(result, input); +} + +TEST(truncated_trailer_throws) { + const std::string compressed{ + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x13\x03\x00\x00\x00\x00\x00", 16}; + try { + decompress_via_stream(compressed); + FAIL(); + } catch (const sourcemeta::core::GZIPError &) { + } +} diff --git a/test/http/http_match_accept_test.cc b/test/http/http_match_accept_test.cc index da3ea7eaf..6281debba 100644 --- a/test/http/http_match_accept_test.cc +++ b/test/http/http_match_accept_test.cc @@ -466,3 +466,103 @@ TEST(level_example_level3_matches_html_weight) { RFC9110_LEVEL_ACCEPT, {"text/html;level=3", "text/html"}), "text/html;level=3"); } + +TEST(range_without_slash_is_ignored) { + EXPECT_EQ(sourcemeta::core::http_match_accept("foo, application/json", + {"application/json"}), + "application/json"); +} + +TEST(range_without_slash_matches_nothing) { + EXPECT_EQ(sourcemeta::core::http_match_accept("foo", {"application/json"}), + ""); +} + +TEST(type_wildcard_with_different_slash_position_matches_nothing) { + EXPECT_EQ(sourcemeta::core::http_match_accept("text/*", {"ab/cd"}), ""); +} + +TEST(type_wildcard_with_different_type_of_same_length_matches_nothing) { + EXPECT_EQ(sourcemeta::core::http_match_accept("text/*", {"abcd/efg"}), ""); +} + +TEST(empty_q_value_excludes_the_candidate) { + EXPECT_EQ(sourcemeta::core::http_match_accept("text/html;q=", {"text/html"}), + ""); +} + +TEST(q_parameter_without_a_value_excludes_the_candidate) { + EXPECT_EQ(sourcemeta::core::http_match_accept("text/html;q", {"text/html"}), + ""); +} + +TEST(q_value_above_one_excludes_the_candidate) { + EXPECT_EQ( + sourcemeta::core::http_match_accept("text/html;q=1.5", {"text/html"}), + ""); +} + +TEST(q_value_with_leading_digit_above_one_excludes_the_candidate) { + EXPECT_EQ(sourcemeta::core::http_match_accept("text/html;q=2", {"text/html"}), + ""); +} + +TEST(q_value_without_a_dot_separator_excludes_the_candidate) { + EXPECT_EQ( + sourcemeta::core::http_match_accept("text/html;q=0x5", {"text/html"}), + ""); +} + +TEST(q_value_with_a_non_numeric_fraction_excludes_the_candidate) { + EXPECT_EQ( + sourcemeta::core::http_match_accept("text/html;q=0.a", {"text/html"}), + ""); +} + +TEST(q_value_with_too_many_digits_excludes_the_candidate) { + EXPECT_EQ( + sourcemeta::core::http_match_accept("text/html;q=0.1234", {"text/html"}), + ""); +} + +TEST(uppercase_q_value_is_recognised) { + EXPECT_EQ(sourcemeta::core::http_match_accept("text/html;Q=0", {"text/html"}), + ""); +} + +TEST(empty_parameter_before_the_q_value_is_skipped) { + EXPECT_EQ( + sourcemeta::core::http_match_accept("text/html;;q=0.5", {"text/html"}), + "text/html"); +} + +TEST(range_with_multiple_parameters_matches_a_candidate_with_them) { + EXPECT_EQ(sourcemeta::core::http_match_accept( + "text/html;a=1;b=2", {"text/html;a=1;b=2", "text/html"}), + "text/html;a=1;b=2"); +} + +TEST(range_with_a_quoted_escaped_parameter_value) { + const std::string_view header{R"(text/html;title="a\"b")"}; + const std::string_view candidate{R"(text/html;title="a\"b")"}; + EXPECT_EQ(sourcemeta::core::http_match_accept(header, {candidate}), + candidate); +} + +TEST(range_with_a_flag_parameter_matches_a_candidate_with_it) { + EXPECT_EQ(sourcemeta::core::http_match_accept( + "text/html;flag", {"text/html;flag", "text/plain"}), + "text/html;flag"); +} + +TEST(range_parameters_with_whitespace_after_semicolon) { + EXPECT_EQ(sourcemeta::core::http_match_accept( + "text/html;a=1; b=2", {"text/html;b=2;a=1", "text/plain"}), + "text/html;b=2;a=1"); +} + +TEST(range_with_an_empty_type_is_ignored) { + EXPECT_EQ(sourcemeta::core::http_match_accept(";q=0.5, application/json", + {"application/json"}), + "application/json"); +} diff --git a/test/io/io_canonical_test.cc b/test/io/io_canonical_test.cc index 4631b2c0f..a0ba70e68 100644 --- a/test/io/io_canonical_test.cc +++ b/test/io/io_canonical_test.cc @@ -40,3 +40,19 @@ TEST(unmapped_error_surfaces_as_filesystem_error) { std::filesystem::remove(loop_path); } #endif + +// On Windows, resolving through a file reports path not found, which the +// canonicalization helpers convert to a file not found error instead +#if !defined(_WIN32) +TEST(file_as_intermediate_directory_throws) { + const auto path{std::filesystem::path{STUBS_DIRECTORY} / "test.txt" / + "child"}; + try { + sourcemeta::core::canonical(path); + FAIL(); + } catch (const std::filesystem::filesystem_error &) { + } catch (...) { + FAIL(); + } +} +#endif diff --git a/test/io/io_flush_test.cc b/test/io/io_flush_test.cc index 997f71d6c..1de40bb1e 100644 --- a/test/io/io_flush_test.cc +++ b/test/io/io_flush_test.cc @@ -1,6 +1,10 @@ #include #include +#if !defined(_WIN32) +#include // geteuid +#endif + TEST(test_txt) { const auto path{std::filesystem::path{STUBS_DIRECTORY} / "test.txt"}; sourcemeta::core::flush(path); @@ -18,3 +22,50 @@ TEST(not_exists) { FAIL(); } } + +// On Windows, opening a directory without backup semantics is reported as +// access denied rather than a generic filesystem error +#if !defined(_WIN32) +TEST(flush_a_directory_throws) { + const auto path{std::filesystem::path{STUBS_DIRECTORY}}; + try { + sourcemeta::core::flush(path); + FAIL(); + } catch (const std::filesystem::filesystem_error &) { + } catch (...) { + FAIL(); + } +} +#endif + +// POSIX permission bits don't map cleanly to Windows ACLs +#if !defined(_WIN32) +TEST(flush_an_unreadable_file_throws_permission_error) { + // The root user opens files regardless of their permission bits + if (::geteuid() == 0) { + return; + } + + const auto path{std::filesystem::temp_directory_path() / + "sourcemeta_core_io_flush_locked.txt"}; + std::ofstream output{path}; + output << "content"; + output.close(); + std::filesystem::permissions(path, std::filesystem::perms::none, + std::filesystem::perm_options::replace); + try { + sourcemeta::core::flush(path); + std::filesystem::permissions(path, std::filesystem::perms::owner_all, + std::filesystem::perm_options::replace); + std::filesystem::remove(path); + FAIL(); + } catch (const sourcemeta::core::IOFilePermissionError &error) { + EXPECT_EQ(error.path(), path); + std::filesystem::permissions(path, std::filesystem::perms::owner_all, + std::filesystem::perm_options::replace); + std::filesystem::remove(path); + } catch (...) { + FAIL(); + } +} +#endif diff --git a/test/jose/jose_algorithm_test.cc b/test/jose/jose_algorithm_test.cc index 3ebfc439f..3be88a626 100644 --- a/test/jose/jose_algorithm_test.cc +++ b/test/jose/jose_algorithm_test.cc @@ -183,3 +183,153 @@ TEST(jws_algorithm_digest_bits_512_family) { sourcemeta::core::JWSAlgorithm::EdDSA), std::uint16_t{512}); } + +TEST(jwe_algorithm_round_trips) { + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("RSA-OAEP").value(), + sourcemeta::core::JWEAlgorithm::RSA_OAEP); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::RSA_OAEP), + "RSA-OAEP"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("RSA-OAEP-256").value(), + sourcemeta::core::JWEAlgorithm::RSA_OAEP_256); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::RSA_OAEP_256), + "RSA-OAEP-256"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("ECDH-ES").value(), + sourcemeta::core::JWEAlgorithm::ECDH_ES); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::ECDH_ES), + "ECDH-ES"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("ECDH-ES+A128KW").value(), + sourcemeta::core::JWEAlgorithm::ECDH_ES_A128KW); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::ECDH_ES_A128KW), + "ECDH-ES+A128KW"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("ECDH-ES+A192KW").value(), + sourcemeta::core::JWEAlgorithm::ECDH_ES_A192KW); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::ECDH_ES_A192KW), + "ECDH-ES+A192KW"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("ECDH-ES+A256KW").value(), + sourcemeta::core::JWEAlgorithm::ECDH_ES_A256KW); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::ECDH_ES_A256KW), + "ECDH-ES+A256KW"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("A128KW").value(), + sourcemeta::core::JWEAlgorithm::A128KW); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::A128KW), + "A128KW"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("A192KW").value(), + sourcemeta::core::JWEAlgorithm::A192KW); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::A192KW), + "A192KW"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("A256KW").value(), + sourcemeta::core::JWEAlgorithm::A256KW); + EXPECT_EQ(sourcemeta::core::jwe_algorithm_name( + sourcemeta::core::JWEAlgorithm::A256KW), + "A256KW"); + EXPECT_EQ(sourcemeta::core::to_jwe_algorithm("dir").value(), + sourcemeta::core::JWEAlgorithm::DIR); + EXPECT_EQ( + sourcemeta::core::jwe_algorithm_name(sourcemeta::core::JWEAlgorithm::DIR), + "dir"); +} + +TEST(jwe_algorithm_rejects_unknown) { + EXPECT_FALSE(sourcemeta::core::to_jwe_algorithm("A512KW").has_value()); + EXPECT_FALSE(sourcemeta::core::to_jwe_algorithm("").has_value()); +} + +TEST(jwe_encryption_round_trips) { + EXPECT_EQ(sourcemeta::core::to_jwe_encryption("A128GCM").value(), + sourcemeta::core::JWEEncryption::A128GCM); + EXPECT_EQ(sourcemeta::core::jwe_encryption_name( + sourcemeta::core::JWEEncryption::A128GCM), + "A128GCM"); + EXPECT_EQ(sourcemeta::core::to_jwe_encryption("A192GCM").value(), + sourcemeta::core::JWEEncryption::A192GCM); + EXPECT_EQ(sourcemeta::core::jwe_encryption_name( + sourcemeta::core::JWEEncryption::A192GCM), + "A192GCM"); + EXPECT_EQ(sourcemeta::core::to_jwe_encryption("A256GCM").value(), + sourcemeta::core::JWEEncryption::A256GCM); + EXPECT_EQ(sourcemeta::core::jwe_encryption_name( + sourcemeta::core::JWEEncryption::A256GCM), + "A256GCM"); + EXPECT_EQ(sourcemeta::core::to_jwe_encryption("A128CBC-HS256").value(), + sourcemeta::core::JWEEncryption::A128CBC_HS256); + EXPECT_EQ(sourcemeta::core::jwe_encryption_name( + sourcemeta::core::JWEEncryption::A128CBC_HS256), + "A128CBC-HS256"); + EXPECT_EQ(sourcemeta::core::to_jwe_encryption("A192CBC-HS384").value(), + sourcemeta::core::JWEEncryption::A192CBC_HS384); + EXPECT_EQ(sourcemeta::core::jwe_encryption_name( + sourcemeta::core::JWEEncryption::A192CBC_HS384), + "A192CBC-HS384"); + EXPECT_EQ(sourcemeta::core::to_jwe_encryption("A256CBC-HS512").value(), + sourcemeta::core::JWEEncryption::A256CBC_HS512); + EXPECT_EQ(sourcemeta::core::jwe_encryption_name( + sourcemeta::core::JWEEncryption::A256CBC_HS512), + "A256CBC-HS512"); +} + +TEST(jwe_encryption_rejects_unknown) { + EXPECT_FALSE(sourcemeta::core::to_jwe_encryption("A512GCM").has_value()); + EXPECT_FALSE(sourcemeta::core::to_jwe_encryption("").has_value()); +} + +TEST(jwe_algorithm_asymmetry) { + EXPECT_TRUE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::RSA_OAEP)); + EXPECT_TRUE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::RSA_OAEP_256)); + EXPECT_TRUE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::ECDH_ES)); + EXPECT_TRUE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::ECDH_ES_A128KW)); + EXPECT_TRUE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::ECDH_ES_A192KW)); + EXPECT_TRUE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::ECDH_ES_A256KW)); + EXPECT_FALSE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::A128KW)); + EXPECT_FALSE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::A192KW)); + EXPECT_FALSE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::A256KW)); + EXPECT_FALSE(sourcemeta::core::jwe_algorithm_is_asymmetric( + sourcemeta::core::JWEAlgorithm::DIR)); +} + +TEST(jwe_encryption_key_sizes) { + EXPECT_EQ(sourcemeta::core::jwe_encryption_key_bytes( + sourcemeta::core::JWEEncryption::A128GCM), + 16); + EXPECT_EQ(sourcemeta::core::jwe_encryption_key_bytes( + sourcemeta::core::JWEEncryption::A192GCM), + 24); + EXPECT_EQ(sourcemeta::core::jwe_encryption_key_bytes( + sourcemeta::core::JWEEncryption::A256GCM), + 32); + EXPECT_EQ(sourcemeta::core::jwe_encryption_key_bytes( + sourcemeta::core::JWEEncryption::A128CBC_HS256), + 32); + EXPECT_EQ(sourcemeta::core::jwe_encryption_key_bytes( + sourcemeta::core::JWEEncryption::A192CBC_HS384), + 48); + EXPECT_EQ(sourcemeta::core::jwe_encryption_key_bytes( + sourcemeta::core::JWEEncryption::A256CBC_HS512), + 64); +} + +TEST(to_jws_algorithm_rs512) { + EXPECT_EQ(sourcemeta::core::to_jws_algorithm("RS512").value(), + sourcemeta::core::JWSAlgorithm::RS512); +} + +TEST(to_jws_algorithm_ps512) { + EXPECT_EQ(sourcemeta::core::to_jws_algorithm("PS512").value(), + sourcemeta::core::JWSAlgorithm::PS512); +} diff --git a/test/json/json_parse_error_test.cc b/test/json/json_parse_error_test.cc index 315ca0994..4020d5a8c 100644 --- a/test/json/json_parse_error_test.cc +++ b/test/json/json_parse_error_test.cc @@ -766,3 +766,21 @@ TEST(trailing_content_after_newline_reports_position) { EXPECT_EQ(error.column(), 3); } } + +TEST(read_json_in_place_with_callback_invalid) { + sourcemeta::core::JSON output{nullptr}; + try { + sourcemeta::core::read_json(std::filesystem::path{TEST_DIRECTORY} / + "stub_invalid_1.json", + output, nullptr); + FAIL(); + } catch (const sourcemeta::core::JSONFileParseError &error) { + EXPECT_EQ(error.path(), + std::filesystem::path{TEST_DIRECTORY} / "stub_invalid_1.json"); + EXPECT_EQ(error.line(), 3); + EXPECT_EQ(error.column(), 9); + EXPECT_STREQ(error.what(), "Failed to parse the JSON document"); + } catch (...) { + FAIL(); + } +} diff --git a/test/json/json_parse_test.cc b/test/json/json_parse_test.cc index 564194af2..506532a6a 100644 --- a/test/json/json_parse_test.cc +++ b/test/json/json_parse_test.cc @@ -1758,3 +1758,108 @@ TEST(parse_default_string_view_with_line_column_does_not_invoke_ub) { EXPECT_EQ(error.column(), 1); } } + +TEST(parse_in_place_stream_with_positions_and_callback) { + std::istringstream stream{"{ \"foo\": 1 }"}; + std::uint64_t line{1}; + std::uint64_t column{0}; + sourcemeta::core::JSON output{nullptr}; + std::size_t events{0}; + sourcemeta::core::parse_json( + stream, line, column, output, + [&events](const sourcemeta::core::JSON::ParsePhase, + const sourcemeta::core::JSON::Type, const std::uint64_t, + const std::uint64_t, const sourcemeta::core::JSON::ParseContext, + const std::size_t, + const sourcemeta::core::JSON::String &) { events += 1; }); + EXPECT_TRUE(output.is_object()); + EXPECT_EQ(output.size(), 1); + EXPECT_EQ(output.at("foo").to_integer(), 1); + EXPECT_EQ(line, 1); + EXPECT_EQ(column, 12); + EXPECT_EQ(events, 4); +} + +TEST(parse_in_place_string_with_positions_and_callback) { + const std::string input{"[ 1, 2 ]"}; + std::uint64_t line{1}; + std::uint64_t column{0}; + sourcemeta::core::JSON output{nullptr}; + std::size_t events{0}; + sourcemeta::core::parse_json( + input, line, column, output, + [&events](const sourcemeta::core::JSON::ParsePhase, + const sourcemeta::core::JSON::Type, const std::uint64_t, + const std::uint64_t, const sourcemeta::core::JSON::ParseContext, + const std::size_t, + const sourcemeta::core::JSON::String &) { events += 1; }); + EXPECT_TRUE(output.is_array()); + EXPECT_EQ(output.size(), 2); + EXPECT_EQ(output.at(0).to_integer(), 1); + EXPECT_EQ(output.at(1).to_integer(), 2); + EXPECT_EQ(line, 1); + EXPECT_EQ(column, 8); + EXPECT_EQ(events, 6); +} + +TEST(parse_in_place_stream_with_callback) { + std::istringstream stream{"\"hello\""}; + sourcemeta::core::JSON output{nullptr}; + std::size_t events{0}; + sourcemeta::core::parse_json( + stream, output, + [&events](const sourcemeta::core::JSON::ParsePhase, + const sourcemeta::core::JSON::Type, const std::uint64_t, + const std::uint64_t, const sourcemeta::core::JSON::ParseContext, + const std::size_t, + const sourcemeta::core::JSON::String &) { events += 1; }); + EXPECT_TRUE(output.is_string()); + EXPECT_EQ(output.to_string(), "hello"); + EXPECT_EQ(events, 2); +} + +TEST(parse_root_decimal_with_callback) { + const std::string input{"3.14159265358979323846"}; + sourcemeta::core::JSON output{nullptr}; + std::size_t events{0}; + sourcemeta::core::parse_json( + input, output, + [&events](const sourcemeta::core::JSON::ParsePhase, + const sourcemeta::core::JSON::Type, const std::uint64_t, + const std::uint64_t, const sourcemeta::core::JSON::ParseContext, + const std::size_t, + const sourcemeta::core::JSON::String &) { events += 1; }); + EXPECT_TRUE(output.is_decimal()); + EXPECT_EQ(output.to_decimal(), + sourcemeta::core::Decimal{"3.14159265358979323846"}); + EXPECT_EQ(events, 2); +} + +TEST(parse_root_real_with_callback) { + const std::string input{"1.5"}; + sourcemeta::core::JSON output{nullptr}; + std::size_t events{0}; + sourcemeta::core::parse_json( + input, output, + [&events](const sourcemeta::core::JSON::ParsePhase, + const sourcemeta::core::JSON::Type, const std::uint64_t, + const std::uint64_t, const sourcemeta::core::JSON::ParseContext, + const std::size_t, + const sourcemeta::core::JSON::String &) { events += 1; }); + EXPECT_TRUE(output.is_real()); + EXPECT_EQ(output.to_real(), 1.5); + EXPECT_EQ(events, 2); +} + +TEST(parse_high_precision_decimal_root) { + const auto result{sourcemeta::core::parse_json("3.14159265358979323846")}; + EXPECT_TRUE(result.is_decimal()); + EXPECT_EQ(result.to_decimal(), + sourcemeta::core::Decimal{"3.14159265358979323846"}); +} + +TEST(parse_exponent_number_root) { + const auto result{sourcemeta::core::parse_json("1e309")}; + EXPECT_TRUE(result.is_decimal()); + EXPECT_EQ(result.to_decimal(), sourcemeta::core::Decimal{"1e309"}); +} diff --git a/test/json/json_value_test.cc b/test/json/json_value_test.cc index 1c3251a3a..99ed5df7a 100644 --- a/test/json/json_value_test.cc +++ b/test/json/json_value_test.cc @@ -783,3 +783,61 @@ TEST(direct_list_inits_deeply_nested_array_without_stack_overflow) { sourcemeta::core::JSON copy{source}; EXPECT_TRUE(copy.is_array()); } + +TEST(boolean_ordering) { + EXPECT_LT(sourcemeta::core::JSON{false}, sourcemeta::core::JSON{true}); + EXPECT_FALSE(sourcemeta::core::JSON{true} < sourcemeta::core::JSON{false}); +} + +TEST(object_ordering) { + const auto left{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const auto right{sourcemeta::core::parse_json(R"JSON({ "b": 2 })JSON")}; + EXPECT_LT(left, right); + EXPECT_FALSE(right < left); +} + +TEST(array_is_not_positive) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 1 ])JSON")}; + EXPECT_FALSE(document.is_positive()); +} + +TEST(copy_assign_integer_over_object) { + auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const sourcemeta::core::JSON other{42}; + document = other; + EXPECT_TRUE(document.is_integer()); + EXPECT_EQ(document.to_integer(), 42); +} + +TEST(copy_assign_real_over_object) { + auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const sourcemeta::core::JSON other{1.5}; + document = other; + EXPECT_TRUE(document.is_real()); + EXPECT_EQ(document.to_real(), 1.5); +} + +TEST(copy_assign_string_over_object) { + auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const sourcemeta::core::JSON other{"hello"}; + document = other; + EXPECT_TRUE(document.is_string()); + EXPECT_EQ(document.to_string(), "hello"); +} + +TEST(copy_assign_decimal_over_object) { + auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const auto other{sourcemeta::core::parse_json("3.14159265358979323846")}; + EXPECT_TRUE(other.is_decimal()); + document = other; + EXPECT_TRUE(document.is_decimal()); + EXPECT_EQ(document, other); +} + +TEST(deep_copy_of_a_nested_object) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": { "b": [ 1, 2 ] } }, 3 ])JSON")}; + const sourcemeta::core::JSON copy{document}; + EXPECT_EQ(copy, document); + EXPECT_EQ(copy.at(0).at("a").at("b").at(1).to_integer(), 2); +} diff --git a/test/jsonld/jsonld_expand_error_test.cc b/test/jsonld/jsonld_expand_error_test.cc index 0b85f576e..fb5783c65 100644 --- a/test/jsonld/jsonld_expand_error_test.cc +++ b/test/jsonld/jsonld_expand_error_test.cc @@ -618,3 +618,34 @@ TEST(error_code_value_is_owned) { EXPECT_STREQ(error.what(), "A custom error code longer than small string optimization"); } + +TEST(type_redefinition_with_non_boolean_protected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "@type": { "@container": "@set", "@protected": 1 } } + })"); + + EXPECT_JSONLD_EXPAND_ERROR(sourcemeta::core::jsonld_expand(input), + "Invalid @protected value", + "/@context/@type/@protected"); +} + +TEST(type_redefinition_with_invalid_container) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "@type": { "@container": "@list" } } + })"); + + EXPECT_JSONLD_EXPAND_ERROR(sourcemeta::core::jsonld_expand(input), + "Keyword redefinition", "/@context/@type"); +} + +TEST(null_term_with_non_boolean_protected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { + "term": { "@id": null, "@protected": "yes" } + } + })"); + + EXPECT_JSONLD_EXPAND_ERROR(sourcemeta::core::jsonld_expand(input), + "Invalid @protected value", + "/@context/term/@protected"); +} diff --git a/test/jsonld/jsonld_expand_test.cc b/test/jsonld/jsonld_expand_test.cc index f25bfe040..fb80d0245 100644 --- a/test/jsonld/jsonld_expand_test.cc +++ b/test/jsonld/jsonld_expand_test.cc @@ -462,3 +462,58 @@ TEST(reverse_term_ignores_prefix_entry) { EXPECT_EQ(sourcemeta::core::jsonld_expand(input), expected); } + +TEST(type_redefinition_with_set_container) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "@type": { "@container": "@set" } }, + "@type": "http://example.com/Type" + })"); + + const auto result{sourcemeta::core::jsonld_expand(input)}; + EXPECT_TRUE(result.is_array()); + EXPECT_EQ(result.size(), 1); + EXPECT_TRUE(result.at(0).defines("@type")); + EXPECT_TRUE(result.at(0).at("@type").is_array()); + EXPECT_EQ(result.at(0).at("@type").size(), 1); + EXPECT_EQ(result.at(0).at("@type").at(0).to_string(), + "http://example.com/Type"); +} + +TEST(term_with_slash_expands_against_vocabulary) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { + "@vocab": "http://vocab.example/", + "a/b": { "@type": "@id" } + }, + "a/b": "http://example.com/x" + })"); + + const auto result{sourcemeta::core::jsonld_expand(input)}; + const auto expected{sourcemeta::core::parse_json(R"JSON([ + { + "http://vocab.example/a/b": [ + { "@id": "http://example.com/x" } + ] + } + ])JSON")}; + EXPECT_EQ(result, expected); +} + +TEST(compact_iri_term_with_unresolvable_prefix) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { + "ex:suffix": { "@type": "@id" } + }, + "ex:suffix": "http://example.com/x" + })"); + + const auto result{sourcemeta::core::jsonld_expand(input)}; + const auto expected{sourcemeta::core::parse_json(R"JSON([ + { + "ex:suffix": [ + { "@id": "http://example.com/x" } + ] + } + ])JSON")}; + EXPECT_EQ(result, expected); +} diff --git a/test/jsonpath/jsonpath_evaluate_test.cc b/test/jsonpath/jsonpath_evaluate_test.cc index 5776c6bd0..8357de7b7 100644 --- a/test/jsonpath/jsonpath_evaluate_test.cc +++ b/test/jsonpath/jsonpath_evaluate_test.cc @@ -1,7 +1,10 @@ #include #include +#include #include +#include // std::string + namespace { struct ResultNode { @@ -22,6 +25,20 @@ auto evaluate_nodes(const sourcemeta::core::JSONPath &path, return result; } +// Wraps the given document in three hundred nested single-element arrays so +// evaluation exceeds the recursion limit and continues on the iterative walk +auto deeply_nested_array(sourcemeta::core::JSON &&bottom) + -> sourcemeta::core::JSON { + auto current{std::move(bottom)}; + for (std::size_t depth{0}; depth < 300; depth += 1) { + auto wrapper{sourcemeta::core::JSON::make_array()}; + wrapper.push_back(std::move(current)); + current = std::move(wrapper); + } + + return current; +} + } // namespace TEST(jsonpath_evaluate_root) { @@ -277,3 +294,145 @@ TEST(jsonpath_evaluate_move_construction) { const auto nodes{evaluate_nodes(moved, document)}; EXPECT_EQ(nodes.size(), 1); } + +TEST(jsonpath_evaluate_deep_descendant_name) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "a": { "b": 7 } })JSON"))}; + const sourcemeta::core::JSONPath path{"$..b"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 7); + EXPECT_EQ(nodes.at(0).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_single_name) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "a": { "b": 7 } })JSON"))}; + const sourcemeta::core::JSONPath path{"$..a.b"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 7); + EXPECT_EQ(nodes.at(0).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_single_index) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[1]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 2); + EXPECT_EQ(nodes.at(0).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_negative_index) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[-1]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 3); + EXPECT_EQ(nodes.at(0).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_wildcard_array) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[*]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 3); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); + EXPECT_EQ(nodes.at(1).value->to_integer(), 2); + EXPECT_EQ(nodes.at(2).value->to_integer(), 3); + EXPECT_EQ(nodes.at(0).location.size(), 302); + EXPECT_EQ(nodes.at(1).location.size(), 302); + EXPECT_EQ(nodes.at(2).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_wildcard_object) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "a": { "b": 7 } })JSON"))}; + const sourcemeta::core::JSONPath path{"$..a[*]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 7); + EXPECT_EQ(nodes.at(0).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_slice) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[0:2]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); + EXPECT_EQ(nodes.at(1).value->to_integer(), 2); + EXPECT_EQ(nodes.at(0).location.size(), 302); + EXPECT_EQ(nodes.at(1).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_negative_step_slice) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[::-1]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 3); + EXPECT_EQ(nodes.at(0).value->to_integer(), 3); + EXPECT_EQ(nodes.at(1).value->to_integer(), 2); + EXPECT_EQ(nodes.at(2).value->to_integer(), 1); + EXPECT_EQ(nodes.at(0).location.size(), 302); + EXPECT_EQ(nodes.at(1).location.size(), 302); + EXPECT_EQ(nodes.at(2).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_filter) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[?@ > 1]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_integer(), 2); + EXPECT_EQ(nodes.at(1).value->to_integer(), 3); + EXPECT_EQ(nodes.at(0).location.size(), 302); + EXPECT_EQ(nodes.at(1).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_index_pair) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[0, 2]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); + EXPECT_EQ(nodes.at(1).value->to_integer(), 3); + EXPECT_EQ(nodes.at(0).location.size(), 302); + EXPECT_EQ(nodes.at(1).location.size(), 302); +} + +TEST(jsonpath_evaluate_deep_descendant_then_zero_step_slice) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "values": [ 1, 2, 3 ] })JSON"))}; + const sourcemeta::core::JSONPath path{"$..values[0:2:0]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_evaluate_deep_descendant_then_filter_on_object) { + const auto document{deeply_nested_array( + sourcemeta::core::parse_json(R"JSON({ "a": { "b": 7 } })JSON"))}; + const sourcemeta::core::JSONPath path{"$..a[?@ == 7]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 7); + EXPECT_EQ(nodes.at(0).location.size(), 302); +} + +TEST(jsonpath_evaluate_multibyte_shorthand) { + const auto document{sourcemeta::core::parse_json("{ \"a\xc3\xa9\": 1 }")}; + const sourcemeta::core::JSONPath path{"$.a\xc3\xa9"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_integer()); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/a\xc3\xa9"); +} diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc index 06ce8a876..8ab9f7c0a 100644 --- a/test/jsonpath/jsonpath_filter_test.cc +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -1,7 +1,10 @@ #include #include +#include #include +#include // std::string + namespace { struct ResultNode { @@ -22,6 +25,23 @@ auto evaluate_nodes(const sourcemeta::core::JSONPath &path, return result; } +// A single-element array whose item nests three hundred arrays deep with a +// small object at the bottom, so filter sub-queries exceed the recursion +// limit and continue on the iterative walk +auto deep_candidate_document() -> sourcemeta::core::JSON { + auto current{sourcemeta::core::parse_json( + R"JSON({ "b": [ 1, 2 ], "c": { "d": 7 } })JSON")}; + for (std::size_t depth{0}; depth < 300; depth += 1) { + auto wrapper{sourcemeta::core::JSON::make_array()}; + wrapper.push_back(std::move(current)); + current = std::move(wrapper); + } + + auto document{sourcemeta::core::JSON::make_array()}; + document.push_back(std::move(current)); + return document; +} + } // namespace TEST(jsonpath_filter_existence) { @@ -383,3 +403,180 @@ TEST(jsonpath_filter_current_node_comparison) { const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } + +TEST(jsonpath_filter_single_index_tail_existence) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ [ 5 ], [], { "x": 1 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@[0]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at(0).to_integer(), 5); +} + +TEST(jsonpath_filter_empty_slice_existence) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ [ 1, 2, 3 ] ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@[1:1]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_filter_negative_step_slice_existence) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ [ 1, 2 ], [] ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@[::-1]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->size(), 2); + EXPECT_EQ(nodes.at(0).value->at(0).to_integer(), 1); + EXPECT_EQ(nodes.at(0).value->at(1).to_integer(), 2); +} + +TEST(jsonpath_filter_nested_filter_on_object) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "x": 1 }, { "x": 2 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@[?@ == 1]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at("x").to_integer(), 1); +} + +TEST(jsonpath_filter_deep_descendant_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..b[1]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_single_name_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..c.d]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_wildcard_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..c[*]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_slice_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..b[0:2]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_negative_step_slice_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..b[::-1]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_nested_filter_array_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..b[?@ > 1]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_nested_filter_object_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..c[?@ == 7]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_index_pair_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..b[0, 1]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_value_comparison) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?value(@..d) == 7]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_wildcard_array_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..b[*]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(sourcemeta::core::to_string(nodes.at(0).location), "/0"); +} + +TEST(jsonpath_filter_deep_descendant_zero_step_slice_existence) { + const auto document{deep_candidate_document()}; + const sourcemeta::core::JSONPath path{"$[?@..b[0:2:0]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_filter_zero_step_slice_existence) { + const auto document{sourcemeta::core::parse_json(R"JSON([ [ 1, 2 ] ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@[0:2:0]]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_filter_less_than_reals) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1.5 }, { "a": 3.5 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a < 2.5]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at("a").to_real(), 1.5); +} + +TEST(jsonpath_filter_match_considers_the_whole_input) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "abc" }, { "a": "xabc" } ])JSON")}; + const sourcemeta::core::JSONPath path{R"($[?match(@.a, "ab.*")])"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at("a").to_string(), "abc"); +} + +TEST(jsonpath_filter_search_considers_any_substring) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "abc" }, { "a": "xyz" } ])JSON")}; + const sourcemeta::core::JSONPath path{R"($[?search(@.a, "b")])"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at("a").to_string(), "abc"); +} + +TEST(jsonpath_filter_negated_search) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "abc" }, { "a": "xyz" } ])JSON")}; + const sourcemeta::core::JSONPath path{R"($[?!search(@.a, "b")])"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at("a").to_string(), "xyz"); +} diff --git a/test/jsonpath/jsonpath_parse_test.cc b/test/jsonpath/jsonpath_parse_test.cc index d37041cec..6dbe36ade 100644 --- a/test/jsonpath/jsonpath_parse_test.cc +++ b/test/jsonpath/jsonpath_parse_test.cc @@ -15,10 +15,15 @@ FAIL(); \ } +// A valid query must parse without throwing and survive a serialization +// round trip unchanged #define EXPECT_JSONPATH_VALID(input) \ { \ const sourcemeta::core::JSONPath path{input}; \ - EXPECT_TRUE(true); \ + const auto path_json{path.to_json()}; \ + const auto reparsed{sourcemeta::core::JSONPath::from_json(path_json)}; \ + EXPECT_TRUE(reparsed.has_value()); \ + EXPECT_EQ(reparsed.value().to_json(), path_json); \ } TEST(jsonpath_parse_root_only) { EXPECT_JSONPATH_VALID("$"); } @@ -336,3 +341,102 @@ TEST(jsonpath_parse_deep_function_nesting_rejected) { TEST(jsonpath_parse_invalid_selector_character) { EXPECT_JSONPATH_PARSE_ERROR("$[!]", 3); } + +TEST(jsonpath_parse_error_truncated_after_bracket){ + EXPECT_JSONPATH_PARSE_ERROR("$[", 3)} + +TEST(jsonpath_parse_error_truncated_escape){ + EXPECT_JSONPATH_PARSE_ERROR("$['a\\", 6)} + +TEST(jsonpath_parse_error_surrogate_followed_by_other_escape){ + EXPECT_JSONPATH_PARSE_ERROR("$[\"\\uD834\\t\"]", 11)} + +TEST(jsonpath_parse_error_truncated_hex_quad){ + EXPECT_JSONPATH_PARSE_ERROR("$[\"\\u12", 8)} + +TEST(jsonpath_parse_error_truncated_after_unicode_escape){ + EXPECT_JSONPATH_PARSE_ERROR("$[\"\\u", 6)} + +TEST(jsonpath_parse_error_truncated_shorthand){ + EXPECT_JSONPATH_PARSE_ERROR("$.", 3)} + +TEST(jsonpath_parse_error_truncated_filter){ + EXPECT_JSONPATH_PARSE_ERROR("$[?", 4)} + +TEST(jsonpath_parse_error_truncated_negation){ + EXPECT_JSONPATH_PARSE_ERROR("$[?!", 5)} + +TEST(jsonpath_parse_error_truncated_filter_query){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@", 5)} + +TEST(jsonpath_parse_error_value_function_as_test){ + EXPECT_JSONPATH_PARSE_ERROR("$[?length(@.a)]", 15)} + +TEST(jsonpath_parse_error_non_singular_comparable){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@..a == 1]", 9)} + +TEST(jsonpath_parse_error_logical_function_as_comparable){ + EXPECT_JSONPATH_PARSE_ERROR("$[?match(@.a, \"x\") == 1]", 20)} + +TEST(jsonpath_parse_error_integer_literal_beyond_ijson_range){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == 9007199254740992]", 27)} + +TEST(jsonpath_parse_error_real_literal_overflow){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == 1e999]", 16)} + +TEST(jsonpath_parse_error_function_without_parenthesis){ + EXPECT_JSONPATH_PARSE_ERROR("$[?count]", 9)} + +TEST(jsonpath_parse_error_truncated_function_arguments){ + EXPECT_JSONPATH_PARSE_ERROR("$[?count(", 10)} + +TEST(jsonpath_parse_error_unterminated_function_arguments){ + EXPECT_JSONPATH_PARSE_ERROR("$[?count(@.a", 13)} + +TEST(jsonpath_parse_error_truncated_after_argument_comma){ + EXPECT_JSONPATH_PARSE_ERROR("$[?count(@.a,", 14)} + +TEST(jsonpath_parse_error_parenthesized_function_argument){ + EXPECT_JSONPATH_PARSE_ERROR("$[?count((@.a))]", 10)} + +TEST(jsonpath_parse_error_single_equals_comparison){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a =1]", 8)} + +TEST(jsonpath_parse_error_negation_without_equals){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a !< 1]", 8)} + +TEST(jsonpath_parse_multibyte_shorthand){EXPECT_JSONPATH_VALID("$.a\xc3\xa9")} + +TEST(jsonpath_parse_error_truncated_after_test_query){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a", 7)} + +TEST(jsonpath_parse_error_truncated_after_comparison_operator){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a ==", 10)} + +TEST(jsonpath_parse_error_non_singular_right_comparable){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == @..b]", 15)} + +TEST(jsonpath_parse_error_logical_function_as_right_comparable){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == match(@.b, \"x\")]", 26)} + +TEST(jsonpath_parse_error_function_name_without_call_as_comparable){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == length]", 17)} + +TEST(jsonpath_parse_error_invalid_hex_digit){ + EXPECT_JSONPATH_PARSE_ERROR("$[\"\\u12G4\"]", 8)} + +TEST(jsonpath_parse_error_truncated_after_conjunction){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a &&", 10)} + +TEST(jsonpath_parse_error_truncated_after_negation_in_conjunction){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a && !", 12)} + +TEST(jsonpath_parse_error_value_function_call_as_test){ + EXPECT_JSONPATH_PARSE_ERROR("$[?value(@.a)]", 14)} + +TEST(jsonpath_parse_error_negated_value_function_as_test){ + EXPECT_JSONPATH_PARSE_ERROR("$[?!length(@.a)]", 16)} + +TEST(jsonpath_parse_error_negated_literal) { + EXPECT_JSONPATH_PARSE_ERROR("$[?!1]", 5) +} diff --git a/test/jsonpointer/jsonpointer_get_test.cc b/test/jsonpointer/jsonpointer_get_test.cc index 8f99d260d..62fdeb29d 100644 --- a/test/jsonpointer/jsonpointer_get_test.cc +++ b/test/jsonpointer/jsonpointer_get_test.cc @@ -554,3 +554,109 @@ TEST(weak_token_hyphen) { EXPECT_TRUE(result.is_integer()); EXPECT_EQ(result.to_integer(), 2); } + +TEST(index_token_on_object) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "0": 1 + })JSON"); + + const auto pointer{sourcemeta::core::to_pointer("/0")}; + const sourcemeta::core::JSON &result{ + sourcemeta::core::get(document, pointer)}; + EXPECT_TRUE(result.is_integer()); + EXPECT_EQ(result.to_integer(), 1); +} + +TEST(const_property_token) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "foo": 1 + })JSON"); + + const sourcemeta::core::Pointer::Token token{"foo"}; + const sourcemeta::core::JSON &result{sourcemeta::core::get(document, token)}; + EXPECT_EQ(result.to_integer(), 1); +} + +TEST(const_index_token) { + const sourcemeta::core::JSON document = + sourcemeta::core::parse_json(R"JSON([ 1, 2 ])JSON"); + + const sourcemeta::core::Pointer::Token token{1}; + const sourcemeta::core::JSON &result{sourcemeta::core::get(document, token)}; + EXPECT_EQ(result.to_integer(), 2); +} + +TEST(const_weak_property_token) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "foo": 1 + })JSON"); + + const std::string property{"foo"}; + const sourcemeta::core::WeakPointer::Token token{std::cref(property)}; + const sourcemeta::core::JSON &result{sourcemeta::core::get(document, token)}; + EXPECT_EQ(result.to_integer(), 1); +} + +TEST(const_weak_index_token) { + const sourcemeta::core::JSON document = + sourcemeta::core::parse_json(R"JSON([ 1, 2 ])JSON"); + + const sourcemeta::core::WeakPointer::Token token{1}; + const sourcemeta::core::JSON &result{sourcemeta::core::get(document, token)}; + EXPECT_EQ(result.to_integer(), 2); +} + +TEST(mutable_property_token) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "foo": 1 + })JSON"); + + const sourcemeta::core::Pointer::Token token{"foo"}; + sourcemeta::core::JSON &result{sourcemeta::core::get(document, token)}; + result = sourcemeta::core::JSON{2}; + EXPECT_EQ(document.at("foo").to_integer(), 2); +} + +TEST(mutable_index_token) { + sourcemeta::core::JSON document = + sourcemeta::core::parse_json(R"JSON([ 1, 2 ])JSON"); + + const sourcemeta::core::Pointer::Token token{0}; + sourcemeta::core::JSON &result{sourcemeta::core::get(document, token)}; + result = sourcemeta::core::JSON{5}; + EXPECT_EQ(document.at(0).to_integer(), 5); +} + +TEST(programmatic_index_token_on_object) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "0": 1 + })JSON"); + + const sourcemeta::core::Pointer pointer{0}; + const sourcemeta::core::JSON &result{ + sourcemeta::core::get(document, pointer)}; + EXPECT_TRUE(result.is_integer()); + EXPECT_EQ(result.to_integer(), 1); +} + +TEST(programmatic_index_token_on_array) { + const sourcemeta::core::JSON document = + sourcemeta::core::parse_json(R"JSON([ 1, 2 ])JSON"); + + const sourcemeta::core::Pointer pointer{1}; + const sourcemeta::core::JSON &result{ + sourcemeta::core::get(document, pointer)}; + EXPECT_EQ(result.to_integer(), 2); +} + +TEST(empty_weak_pointer_returns_document) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "foo": 1 + })JSON"); + + const sourcemeta::core::WeakPointer pointer; + const sourcemeta::core::JSON &result{ + sourcemeta::core::get(document, pointer)}; + EXPECT_TRUE(result.is_object()); + EXPECT_EQ(std::addressof(result), std::addressof(document)); +} diff --git a/test/jsonpointer/jsonpointer_pointer_test.cc b/test/jsonpointer/jsonpointer_pointer_test.cc index 26d19a13f..04dece8fb 100644 --- a/test/jsonpointer/jsonpointer_pointer_test.cc +++ b/test/jsonpointer/jsonpointer_pointer_test.cc @@ -341,3 +341,9 @@ TEST(hash_three_token_consistency) { EXPECT_EQ(hasher(multi_1), hasher(multi_2)); EXPECT_NE(hasher(multi_1), hasher(multi_3)); } + +TEST(to_pointer_from_json_string) { + const sourcemeta::core::JSON document{"/foo/0"}; + const auto pointer{sourcemeta::core::to_pointer(document)}; + EXPECT_EQ(pointer, sourcemeta::core::to_pointer("/foo/0")); +} diff --git a/test/jsonpointer/jsonpointer_set_test.cc b/test/jsonpointer/jsonpointer_set_test.cc index 87beb21b1..0229610f8 100644 --- a/test/jsonpointer/jsonpointer_set_test.cc +++ b/test/jsonpointer/jsonpointer_set_test.cc @@ -219,3 +219,31 @@ TEST(positive_integer_property) { EXPECT_TRUE(document.at("0").is_integer()); EXPECT_EQ(document.at("0").to_integer(), 4); } + +TEST(set_index_token_on_object) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "0": 1 + })JSON"); + + const auto pointer{sourcemeta::core::to_pointer("/0")}; + sourcemeta::core::set(document, pointer, sourcemeta::core::JSON{2}); + EXPECT_EQ(document.at("0").to_integer(), 2); +} + +TEST(set_through_index_token_on_array) { + sourcemeta::core::JSON document = + sourcemeta::core::parse_json(R"JSON([ { "x": 1 } ])JSON"); + + const sourcemeta::core::Pointer pointer{0, "x"}; + sourcemeta::core::set(document, pointer, sourcemeta::core::JSON{2}); + EXPECT_EQ(document.at(0).at("x").to_integer(), 2); +} + +TEST(set_through_index_token_on_object) { + sourcemeta::core::JSON document = + sourcemeta::core::parse_json(R"JSON({ "0": { "x": 1 } })JSON"); + + const sourcemeta::core::Pointer pointer{0, "x"}; + sourcemeta::core::set(document, pointer, sourcemeta::core::JSON{2}); + EXPECT_EQ(document.at("0").at("x").to_integer(), 2); +} diff --git a/test/oauth/oauth_authorization_test.cc b/test/oauth/oauth_authorization_test.cc index f32e36c09..e9147257f 100644 --- a/test/oauth/oauth_authorization_test.cc +++ b/test/oauth/oauth_authorization_test.cc @@ -1211,3 +1211,104 @@ TEST(build_authorization_error_form_post_honors_a_custom_title) { "" ""); } + +TEST(build_authorization_error_form_post_rejects_a_fragment_redirect) { + sourcemeta::core::OAuthAuthorizationResponse response; + response.error = "access_denied"; + std::string page; + EXPECT_FALSE(sourcemeta::core::oauth_build_authorization_error_form_post( + "https://client.example/cb#section", response, page)); + EXPECT_TRUE(page.empty()); +} + +TEST(build_authorization_error_form_post_rejects_an_invalid_iss) { + sourcemeta::core::OAuthAuthorizationResponse response; + response.error = "access_denied"; + response.iss = "https://server.example?x=1"; + std::string page; + EXPECT_FALSE(sourcemeta::core::oauth_build_authorization_error_form_post( + "https://client.example/cb", response, page)); + EXPECT_TRUE(page.empty()); +} + +TEST(build_authorization_error_form_post_emits_iss) { + sourcemeta::core::OAuthAuthorizationResponse response; + response.error = "access_denied"; + response.iss = "https://server.example"; + std::string page; + EXPECT_TRUE(sourcemeta::core::oauth_build_authorization_error_form_post( + "https://client.example/cb", response, page)); + EXPECT_EQ(page, + "Submit This Form" + "" + "
" + "" + "" + "
"); +} + +TEST(build_authorization_form_post_rejects_an_unclosed_bracket_iss) { + sourcemeta::core::OAuthAuthorizationResponse response; + response.code = "abc"; + response.iss = "https://[::1"; + std::string page; + EXPECT_FALSE(sourcemeta::core::oauth_build_authorization_form_post( + "https://client.example/cb", response, page)); + EXPECT_TRUE(page.empty()); +} + +TEST(parse_authorization_response_rejects_a_malformed_name_escape) { + std::string storage; + sourcemeta::core::OAuthAuthorizationResponse response; + EXPECT_FALSE(sourcemeta::core::oauth_parse_authorization_response( + "%GG=x", storage, response)); +} + +TEST(parse_authorization_response_rejects_a_duplicate_error_uri) { + std::string storage; + sourcemeta::core::OAuthAuthorizationResponse response; + EXPECT_FALSE(sourcemeta::core::oauth_parse_authorization_response( + "error=a&error_uri=x&error_uri=y", storage, response)); +} + +TEST(parse_authorization_response_rejects_a_malformed_state_value) { + std::string storage; + sourcemeta::core::OAuthAuthorizationResponse response; + EXPECT_FALSE(sourcemeta::core::oauth_parse_authorization_response( + "code=a&state=x%2", storage, response)); +} + +TEST(parse_authorization_response_rejects_a_malformed_iss_value) { + std::string storage; + sourcemeta::core::OAuthAuthorizationResponse response; + EXPECT_FALSE(sourcemeta::core::oauth_parse_authorization_response( + "code=a&iss=x%2", storage, response)); +} + +TEST(parse_authorization_response_rejects_a_malformed_error_value) { + std::string storage; + sourcemeta::core::OAuthAuthorizationResponse response; + EXPECT_FALSE(sourcemeta::core::oauth_parse_authorization_response( + "error=x%2", storage, response)); +} + +TEST(parse_authorization_response_rejects_a_malformed_error_description) { + std::string storage; + sourcemeta::core::OAuthAuthorizationResponse response; + EXPECT_FALSE(sourcemeta::core::oauth_parse_authorization_response( + "error=a&error_description=x%2", storage, response)); +} + +TEST(parse_authorization_response_rejects_a_malformed_error_uri) { + std::string storage; + sourcemeta::core::OAuthAuthorizationResponse response; + EXPECT_FALSE(sourcemeta::core::oauth_parse_authorization_response( + "error=a&error_uri=x%2", storage, response)); +} + +TEST(redirect_uri_matches_rejects_an_unclosed_bracket_host) { + EXPECT_FALSE(sourcemeta::core::oauth_redirect_uri_matches( + "http://[::1/cb", "http://[::1:8080/cb", + sourcemeta::core::OAuthProfile::Strict)); +} diff --git a/test/oidc/oidc_id_token_test.cc b/test/oidc/oidc_id_token_test.cc index 653db956c..9b7c9e5ca 100644 --- a/test/oidc/oidc_id_token_test.cc +++ b/test/oidc/oidc_id_token_test.cc @@ -823,3 +823,75 @@ TEST(mint_embeds_a_code_hash) { "the-authorization-code", sourcemeta::core::JWSAlgorithm::HS256, token.value().payload().at("c_hash").to_string())); } + +TEST(parse_id_token_rejects_a_non_object) { + const sourcemeta::core::JSON response{"not an object"}; + EXPECT_FALSE(sourcemeta::core::oidc_parse_id_token(response).has_value()); +} + +TEST(parse_id_token_rejects_a_missing_member) { + const auto response{sourcemeta::core::parse_json(R"JSON({ + "access_token": "abc" + })JSON")}; + EXPECT_FALSE(sourcemeta::core::oidc_parse_id_token(response).has_value()); +} + +TEST(mint_emits_optional_claims) { + sourcemeta::core::OIDCIdTokenClaims claims; + claims.issuer = "https://issuer.example"; + claims.subject = "user-1"; + claims.audience = "client-id"; + claims.issued_at = reference_now; + claims.expiration = reference_now + std::chrono::hours{1}; + claims.authorized_party = "client-id"; + claims.authentication_context_class = "urn:mace:incommon:iap:silver"; + claims.authentication_time = reference_now - std::chrono::minutes{5}; + const auto compact{sourcemeta::core::oidc_mint_id_token( + claims, oct_private_key(), sourcemeta::core::JWSAlgorithm::HS256)}; + EXPECT_TRUE(compact.has_value()); + + const auto token{sourcemeta::core::JWT::from(compact.value())}; + EXPECT_TRUE(token.has_value()); + EXPECT_EQ(token.value().payload().at("iss").to_string(), + "https://issuer.example"); + EXPECT_EQ(token.value().payload().at("sub").to_string(), "user-1"); + EXPECT_EQ(token.value().payload().at("aud").to_string(), "client-id"); + EXPECT_EQ(token.value().payload().at("iat").to_integer(), 1700000000); + EXPECT_EQ(token.value().payload().at("exp").to_integer(), 1700003600); + EXPECT_EQ(token.value().payload().at("azp").to_string(), "client-id"); + EXPECT_EQ(token.value().payload().at("acr").to_string(), + "urn:mace:incommon:iap:silver"); + EXPECT_EQ(token.value().payload().at("auth_time").to_integer(), 1699999700); +} + +TEST(validate_rejects_a_missing_acr_when_a_set_was_requested) { + const auto compact{sign_id_token(id_token_issued_at(1699996400))}; + const auto token{sourcemeta::core::JWT::from(compact)}; + EXPECT_TRUE(token.has_value()); + sourcemeta::core::OIDCValidationOptions options; + const std::array classes{{"urn:example:gold"}}; + options.acceptable_authentication_context_classes = classes; + const auto identity{sourcemeta::core::oidc_validate_id_token( + token.value(), oct_key_set(), allowed_hs256, "https://issuer.example", + "client-id", reference_now, options)}; + EXPECT_FALSE(identity.has_value()); +} + +TEST(validate_treats_an_overflowing_auth_time_as_absent) { + const auto compact{sign_id_token(R"JSON({ + "iss": "https://issuer.example", + "sub": "user-1", + "aud": "client-id", + "iat": 1699996400, + "exp": 2000000000, + "auth_time": 1e300 + })JSON")}; + const auto token{sourcemeta::core::JWT::from(compact)}; + EXPECT_TRUE(token.has_value()); + sourcemeta::core::OIDCValidationOptions options; + options.maximum_authentication_age = std::chrono::seconds{60}; + const auto identity{sourcemeta::core::oidc_validate_id_token( + token.value(), oct_key_set(), allowed_hs256, "https://issuer.example", + "client-id", reference_now, options)}; + EXPECT_FALSE(identity.has_value()); +} diff --git a/test/oidc/oidc_registration_test.cc b/test/oidc/oidc_registration_test.cc index 27dc7da27..d2c973c83 100644 --- a/test/oidc/oidc_registration_test.cc +++ b/test/oidc/oidc_registration_test.cc @@ -371,3 +371,80 @@ TEST(from_accepts_a_cleartext_request_uri) { sourcemeta::core::OIDCClientMetadata::from(std::move(document))}; EXPECT_TRUE(metadata.has_value()); } + +TEST(from_rejects_an_unparseable_redirect_uri) { + auto document{ + sourcemeta::core::parse_json(R"JSON({ "redirect_uris": [ "%" ] })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_an_unparseable_sector_identifier_uri) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://client.example/cb" ], + "sector_identifier_uri": "%" + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_an_unparseable_initiate_login_uri) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://client.example/cb" ], + "initiate_login_uri": "%" + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_a_non_integer_default_max_age) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://client.example/cb" ], + "default_max_age": "soon" + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_a_negative_default_max_age) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://client.example/cb" ], + "default_max_age": -1 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_a_non_boolean_require_auth_time) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://client.example/cb" ], + "require_auth_time": 1 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_a_non_array_post_logout_redirect_uris) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://client.example/cb" ], + "post_logout_redirect_uris": 42 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(encrypted_and_userinfo_algorithm_accessors) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://client.example/cb" ], + "id_token_encrypted_response_alg": "RSA-OAEP", + "userinfo_signed_response_alg": "ES256" + })JSON")}; + const auto metadata{ + sourcemeta::core::OIDCClientMetadata::from(std::move(document))}; + EXPECT_TRUE(metadata.has_value()); + EXPECT_TRUE(metadata.value().id_token_encrypted_response_alg().has_value()); + EXPECT_EQ(metadata.value().id_token_encrypted_response_alg().value(), + "RSA-OAEP"); + EXPECT_TRUE(metadata.value().userinfo_signed_response_alg().has_value()); + EXPECT_EQ(metadata.value().userinfo_signed_response_alg().value(), "ES256"); +} diff --git a/test/semver/semver_compare_test.cc b/test/semver/semver_compare_test.cc index 51beece16..a604e730f 100644 --- a/test/semver/semver_compare_test.cc +++ b/test/semver/semver_compare_test.cc @@ -352,3 +352,78 @@ TEST(overflow_pre_release_same_length_lexical) { EXPECT_GT(sourcemeta::core::SemVer{"1.0.0-99999999999999999999"}, sourcemeta::core::SemVer{"1.0.0-99999999999999999998"}); } + +TEST(numeric_pre_release_identifiers_compare_by_value) { + EXPECT_LT(sourcemeta::core::SemVer{"1.0.0-alpha.2"}, + sourcemeta::core::SemVer{"1.0.0-alpha.10"}); +} + +TEST(numeric_pre_release_identifiers_compare_by_value_reversed) { + EXPECT_GT(sourcemeta::core::SemVer{"1.0.0-alpha.10"}, + sourcemeta::core::SemVer{"1.0.0-alpha.2"}); +} + +TEST(alphanumeric_pre_release_identifiers_compare_lexically) { + EXPECT_LT(sourcemeta::core::SemVer{"1.0.0-alpha.beta"}, + sourcemeta::core::SemVer{"1.0.0-alpha.gamma"}); +} + +TEST(alphanumeric_pre_release_identifiers_compare_lexically_reversed) { + EXPECT_GT(sourcemeta::core::SemVer{"1.0.0-alpha.gamma"}, + sourcemeta::core::SemVer{"1.0.0-alpha.beta"}); +} + +TEST(numeric_pre_release_identifier_below_alphanumeric) { + EXPECT_LT(sourcemeta::core::SemVer{"1.0.0-1"}, + sourcemeta::core::SemVer{"1.0.0-alpha"}); +} + +TEST(alphanumeric_pre_release_identifier_above_numeric) { + EXPECT_GT(sourcemeta::core::SemVer{"1.0.0-alpha"}, + sourcemeta::core::SemVer{"1.0.0-1"}); +} + +TEST(shorter_pre_release_below_longer_with_same_prefix) { + EXPECT_LT(sourcemeta::core::SemVer{"1.0.0-alpha"}, + sourcemeta::core::SemVer{"1.0.0-alpha.1"}); +} + +TEST(longer_pre_release_above_shorter_with_same_prefix) { + EXPECT_GT(sourcemeta::core::SemVer{"1.0.0-alpha.1"}, + sourcemeta::core::SemVer{"1.0.0-alpha"}); +} + +TEST(release_not_less_than_pre_release) { + EXPECT_FALSE(sourcemeta::core::SemVer{"1.0.0"} < + sourcemeta::core::SemVer{"1.0.0-alpha"}); +} + +TEST(greater_numeric_pre_release_identifier_not_less) { + EXPECT_FALSE(sourcemeta::core::SemVer{"1.0.0-alpha.10"} < + sourcemeta::core::SemVer{"1.0.0-alpha.2"}); +} + +TEST(greater_alphanumeric_pre_release_identifier_not_less) { + EXPECT_FALSE(sourcemeta::core::SemVer{"1.0.0-alpha.gamma"} < + sourcemeta::core::SemVer{"1.0.0-alpha.beta"}); +} + +TEST(alphanumeric_pre_release_identifier_not_less_than_numeric) { + EXPECT_FALSE(sourcemeta::core::SemVer{"1.0.0-alpha"} < + sourcemeta::core::SemVer{"1.0.0-1"}); +} + +TEST(longer_pre_release_not_less_than_shorter_prefix) { + EXPECT_FALSE(sourcemeta::core::SemVer{"1.0.0-alpha.1"} < + sourcemeta::core::SemVer{"1.0.0-alpha"}); +} + +TEST(overflowing_numeric_pre_release_identifiers_compare_by_digits) { + EXPECT_LT(sourcemeta::core::SemVer{"1.0.0-11111111111111111111111"}, + sourcemeta::core::SemVer{"1.0.0-99999999999999999999999"}); +} + +TEST(overflowing_numeric_pre_release_identifiers_not_less_reversed) { + EXPECT_FALSE(sourcemeta::core::SemVer{"1.0.0-99999999999999999999999"} < + sourcemeta::core::SemVer{"1.0.0-11111111111111111111111"}); +} diff --git a/test/semver/semver_from_test.cc b/test/semver/semver_from_test.cc index 21e9893fe..d7df9b1f5 100644 --- a/test/semver/semver_from_test.cc +++ b/test/semver/semver_from_test.cc @@ -122,3 +122,45 @@ TEST(result_to_string) { TEST(invalid_not_valid) { EXPECT_FALSE(sourcemeta::core::SemVer::from("not valid").has_value()); } + +TEST(invalid_major_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("x.2.3").has_value()); +} + +TEST(missing_minor_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("1").has_value()); +} + +TEST(overflow_minor_returns_nullopt) { + EXPECT_FALSE( + sourcemeta::core::SemVer::from("1.99999999999999999999.0").has_value()); +} + +TEST(invalid_minor_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("1.x.3").has_value()); +} + +TEST(missing_patch_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("1.2").has_value()); +} + +TEST(overflow_patch_returns_nullopt) { + EXPECT_FALSE( + sourcemeta::core::SemVer::from("1.2.99999999999999999999").has_value()); +} + +TEST(invalid_patch_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("1.2.x").has_value()); +} + +TEST(invalid_pre_release_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("1.2.3-!").has_value()); +} + +TEST(invalid_build_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("1.2.3+!").has_value()); +} + +TEST(trailing_garbage_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from("1.2.3x").has_value()); +} diff --git a/test/semver/semver_parse_loose_test.cc b/test/semver/semver_parse_loose_test.cc index 4fd60a95e..d14ede0f8 100644 --- a/test/semver/semver_parse_loose_test.cc +++ b/test/semver/semver_parse_loose_test.cc @@ -213,3 +213,60 @@ TEST(major_only_with_build) { EXPECT_EQ(version.patch(), 0); EXPECT_EQ(version.build(), "build"); } + +TEST(from_loose_invalid_major_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from( + "x.2.3", sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_overflow_major_returns_nullopt) { + EXPECT_FALSE( + sourcemeta::core::SemVer::from("99999999999999999999.0.0", + sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_overflow_minor_returns_nullopt) { + EXPECT_FALSE( + sourcemeta::core::SemVer::from("1.99999999999999999999.0", + sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_invalid_minor_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from( + "1.x.3", sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_overflow_patch_returns_nullopt) { + EXPECT_FALSE( + sourcemeta::core::SemVer::from("1.2.99999999999999999999", + sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_invalid_patch_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from( + "1.2.x", sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_invalid_pre_release_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from( + "1.2.3-!", sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_invalid_build_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from( + "1.2.3+!", sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} + +TEST(from_loose_trailing_garbage_returns_nullopt) { + EXPECT_FALSE(sourcemeta::core::SemVer::from( + "1.2.3x", sourcemeta::core::SemVer::Mode::Loose) + .has_value()); +} diff --git a/test/uri/uri_path_test.cc b/test/uri/uri_path_test.cc index 0cae87a80..7e84bcac1 100644 --- a/test/uri/uri_path_test.cc +++ b/test/uri/uri_path_test.cc @@ -906,3 +906,87 @@ TEST(iri_unicode_path) { EXPECT_TRUE(uri.path().has_value()); EXPECT_EQ(uri.path().value(), "/caf\xC3\xA9"); } + +TEST(set_path_with_authority_prefix) { + sourcemeta::core::URI uri{"https://example.com"}; + try { + uri.path("//evil.com/x"); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ(error.what(), + "You cannot set a path that contains an authority"); + } +} + +TEST(set_path_with_truncated_percent_sequence) { + sourcemeta::core::URI uri{"https://example.com"}; + try { + uri.path("/a%2"); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ( + error.what(), + "You cannot set a path with an invalid percent-encoded sequence"); + } +} + +TEST(set_path_with_non_hex_percent_sequence) { + sourcemeta::core::URI uri{"https://example.com"}; + try { + uri.path("/a%GG"); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ( + error.what(), + "You cannot set a path with an invalid percent-encoded sequence"); + } +} + +TEST(set_path_with_invalid_character) { + sourcemeta::core::URI uri{"https://example.com"}; + try { + uri.path("/a b"); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ(error.what(), + "You cannot set a path that contains an invalid character"); + } +} + +TEST(append_path_reference_without_path) { + sourcemeta::core::URI uri{"https://example.com/foo"}; + const sourcemeta::core::URI reference{""}; + uri.append_path(reference); + EXPECT_EQ(uri.recompose(), "https://example.com/foo"); +} + +TEST(append_path_moved_reference_without_path) { + sourcemeta::core::URI uri{"https://example.com/foo"}; + uri.append_path(sourcemeta::core::URI{""}); + EXPECT_EQ(uri.recompose(), "https://example.com/foo"); +} + +TEST(set_empty_path_from_const_string) { + sourcemeta::core::URI uri{"https://example.com/foo"}; + const std::string empty; + uri.path(empty); + EXPECT_EQ(uri.recompose(), "https://example.com"); +} + +TEST(set_relative_path_from_const_string) { + sourcemeta::core::URI uri{"https://example.com"}; + const std::string relative{"./foo"}; + try { + uri.path(relative); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ(error.what(), + "You cannot set a relative path to an absolute URI"); + } +} + +TEST(append_path_that_normalizes_away) { + sourcemeta::core::URI uri{"a"}; + uri.append_path(".."); + EXPECT_EQ(uri.recompose(), ""); +} diff --git a/test/uritemplate/uritemplate_router_test.cc b/test/uritemplate/uritemplate_router_test.cc index 031b30f17..6492e4294 100644 --- a/test/uritemplate/uritemplate_router_test.cc +++ b/test/uritemplate/uritemplate_router_test.cc @@ -3057,3 +3057,93 @@ TEST(segment_error_message) { FAIL(); } } + +TEST(empty_template_re_registration_replaces_the_root) { + sourcemeta::core::URITemplateRouter router; + router.add("", "op_first", 1, 11); + router.add("", "op_second", 2, 22); + EXPECT_ROUTER_MATCH(router, "", 2, 22, captures); + EXPECT_EQ(captures.size(), 0); + EXPECT_EQ(router.operation("op_second").first, 2); + EXPECT_EQ(router.operation("op_second").second, 22); + EXPECT_EQ(router.operation("op_first").first, 0); + EXPECT_EQ(router.operation("op_first").second, 0); +} + +TEST(empty_template_re_registration_with_arguments) { + sourcemeta::core::URITemplateRouter router; + const std::string argument_value{"value"}; + const std::array arguments{ + {{"key", std::string_view{argument_value}}}}; + router.add("", "op_first", 1, 11); + router.add("", "op_second", 2, 22, arguments); + EXPECT_ROUTER_MATCH(router, "", 2, 22, captures); + bool argument_seen{false}; + router.arguments( + 2, [&argument_seen]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + const auto *content{std::get_if(&value)}; + argument_seen = + name == "key" && content != nullptr && *content == "value"; + }); + EXPECT_TRUE(argument_seen); +} + +TEST(add_rejects_an_unmatched_closing_brace) { + sourcemeta::core::URITemplateRouter router; + try { + router.add("/}x", "op_bad", 1); + FAIL(); + } catch ( + const sourcemeta::core::URITemplateRouterInvalidSegmentError &error) { + EXPECT_STREQ(error.what(), "Unmatched closing brace"); + EXPECT_EQ(error.segment(), "}x"); + } catch (...) { + FAIL(); + } +} + +TEST(add_rejects_an_unclosed_trailing_brace) { + sourcemeta::core::URITemplateRouter router; + try { + router.add("/{", "op_bad", 1); + FAIL(); + } catch ( + const sourcemeta::core::URITemplateRouterInvalidSegmentError &error) { + EXPECT_STREQ(error.what(), "Unclosed brace"); + EXPECT_EQ(error.segment(), "{"); + } catch (...) { + FAIL(); + } +} + +TEST(add_rejects_a_mixed_literal_and_variable_segment) { + sourcemeta::core::URITemplateRouter router; + try { + router.add("/a{x}", "op_bad", 1); + FAIL(); + } catch ( + const sourcemeta::core::URITemplateRouterInvalidSegmentError &error) { + EXPECT_STREQ(error.what(), + "Path segment cannot mix literals and variables"); + EXPECT_EQ(error.segment(), "a{x}"); + } catch (...) { + FAIL(); + } +} + +TEST(add_rejects_a_variable_then_literal_segment) { + sourcemeta::core::URITemplateRouter router; + try { + router.add("/{x}a", "op_bad", 1); + FAIL(); + } catch ( + const sourcemeta::core::URITemplateRouterInvalidSegmentError &error) { + EXPECT_STREQ(error.what(), + "Path segment cannot mix literals and variables"); + EXPECT_EQ(error.segment(), "{x}a"); + } catch (...) { + FAIL(); + } +} diff --git a/test/uritemplate/uritemplate_router_view_test.cc b/test/uritemplate/uritemplate_router_view_test.cc index a0bbc5938..18861e37b 100644 --- a/test/uritemplate/uritemplate_router_view_test.cc +++ b/test/uritemplate/uritemplate_router_view_test.cc @@ -29,6 +29,27 @@ class URITemplateRouterViewTest { "sourcemeta_core_uritemplate_router_test.bin"}; }; +namespace { + +// A well-formed 80 byte serialization holding a single childless root node, +// an empty string table, and the argument, operation, and path table offsets +// the caller chooses, so each test can append a corrupt table at the end +auto corrupt_router_core(const std::uint32_t operations_offset, + const std::uint32_t paths_offset) + -> std::vector { + const std::uint32_t words[] = {0x52544552, 9, 1, 80, 80, 0x50, 0, + 0, 0, 0, 0, 0, 0, 0, + 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + std::vector bytes{ + reinterpret_cast(words), + reinterpret_cast(words) + sizeof(words)}; + std::memcpy(bytes.data() + 20, &operations_offset, sizeof(operations_offset)); + std::memcpy(bytes.data() + 44, &paths_offset, sizeof(paths_offset)); + return bytes; +} + +} // namespace + TEST_F(URITemplateRouterViewTest, single_literal_route) { { sourcemeta::core::URITemplateRouter router; @@ -749,7 +770,8 @@ TEST(corrupt_too_small_for_header) { } TEST(corrupt_wrong_magic) { - const std::uint32_t data[] = {0xDEADBEEF, 5, 1, 64, 64, 0, 0, 0}; + const std::uint32_t data[] = {0xDEADBEEF, 9, 1, 80, 80, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -757,7 +779,8 @@ TEST(corrupt_wrong_magic) { } TEST(corrupt_wrong_version) { - const std::uint32_t data[] = {0x52544552, 99, 1, 64, 64, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 99, 1, 80, 80, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -765,7 +788,7 @@ TEST(corrupt_wrong_version) { } TEST(corrupt_node_count_exceeds_file) { - const std::uint32_t data[] = {0x52544552, 5, 10, 32, 32, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 10, 48, 48, 0, 0, 0, 0, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -773,8 +796,9 @@ TEST(corrupt_node_count_exceeds_file) { } TEST(corrupt_literal_child_out_of_bounds) { - const std::uint32_t data[] = {0x52544552, 5, 1, 64, 64, 0, 0, 0, - 0, 0, 999, 1, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 80, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + 999, 1, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -782,8 +806,9 @@ TEST(corrupt_literal_child_out_of_bounds) { } TEST(corrupt_variable_child_out_of_bounds) { - const std::uint32_t data[] = {0x52544552, 5, 1, 64, 64, 0, 0, 0, - 0, 0, 0xFFFFFFFF, 0, 500, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 80, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + 0xFFFFFFFF, 0, 500, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -791,9 +816,10 @@ TEST(corrupt_variable_child_out_of_bounds) { } TEST(corrupt_string_offset_out_of_bounds) { - const std::uint32_t data[] = { - 0x52544552, 5, 2, 96, 96, 0, 0, 0, 0, 0, 0, 1, 1, - 0xFFFFFFFF, 0, 0, 0, 9999, 5, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 2, 112, 112, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, + 0xFFFFFFFF, 0, 0, 0, 9999, 5, 0xFFFFFFFF, 0, + 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -802,9 +828,9 @@ TEST(corrupt_string_offset_out_of_bounds) { TEST(corrupt_variable_string_offset_out_of_bounds) { const std::uint32_t data[] = { - 0x52544552, 5, 2, 96, 96, 0, 0, 0, - 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, - 9999, 100, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; + 0x52544552, 9, 2, 112, 112, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, + 9999, 100, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -827,7 +853,8 @@ TEST(corrupt_all_ones) { } TEST(corrupt_string_table_offset_overlaps_header) { - const std::uint32_t data[] = {0x52544552, 5, 1, 4, 64, 0, 0, 0, 0, 0, + const std::uint32_t data[] = {0x52544552, 9, 1, 4, 80, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; @@ -836,9 +863,9 @@ TEST(corrupt_string_table_offset_overlaps_header) { } TEST(corrupt_string_table_offset_past_end) { - const std::uint32_t data[] = {0x52544552, 5, 1, 99999, 99999, 0, - 0, 0, 0, 0, 0xFFFFFFFF, 0, - 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 1, 99999, 99999, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -846,7 +873,7 @@ TEST(corrupt_string_table_offset_past_end) { } TEST(corrupt_zero_node_count) { - const std::uint32_t data[] = {0x52544552, 5, 0, 32, 32, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 0, 48, 48, 0, 0, 0, 0, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -867,8 +894,9 @@ TEST(corrupt_empty_data_match_root) { TEST(corrupt_literal_child_count_overflow) { const std::uint32_t data[] = { - 0x52544552, 5, 2, 96, 96, 0, 0, 0, 0, 0, 1, 0xFFFFFFFF, - 0xFFFFFFFF, 0, 0, 0, 0, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + 0x52544552, 9, 2, 112, 112, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0xFFFFFFFF, 0xFFFFFFFF, 0, 0, 0, 0, 0, + 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -876,8 +904,9 @@ TEST(corrupt_literal_child_count_overflow) { } TEST(corrupt_root_literal_child_oob_match_root) { - const std::uint32_t data[] = {0x52544552, 5, 1, 64, 64, 0, 0, 0, - 0, 0, 999, 1, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 80, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + 999, 1, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/", 0, 0, captures); @@ -886,7 +915,8 @@ TEST(corrupt_root_literal_child_oob_match_root) { TEST(corrupt_deep_node_variable_child_oob) { std::vector data; - const std::uint32_t header[] = {0x52544552, 5, 2, 96, 101, 0, 0, 0}; + const std::uint32_t header[] = {0x52544552, 9, 2, 112, 117, 0, + 0, 0, 0, 0, 0, 0}; const std::uint32_t root[] = {0, 0, 1, 1, 0xFFFFFFFF, 0, 0, 0}; const std::uint32_t child[] = {0, 5, 0xFFFFFFFF, 0, 999, 0x00000001, 0, 0}; data.insert(data.end(), reinterpret_cast(header), @@ -904,9 +934,9 @@ TEST(corrupt_deep_node_variable_child_oob) { TEST(corrupt_expansion_string_oob) { const std::uint32_t data[] = { - 0x52544552, 5, 2, 96, 96, 0, 0, 0, - 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, - 5000, 200, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000003, 0, 0}; + 0x52544552, 9, 2, 112, 112, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, + 5000, 200, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000003, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/files/foo/bar", 0, 0, captures); @@ -914,9 +944,10 @@ TEST(corrupt_expansion_string_oob) { } TEST(corrupt_empty_string_table_with_string_ref) { - const std::uint32_t data[] = { - 0x52544552, 5, 2, 96, 96, 0, 0, 0, 0, 0, 1, 1, - 0xFFFFFFFF, 0, 0, 0, 0, 10, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 2, 112, 112, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, + 0xFFFFFFFF, 0, 0, 0, 0, 10, 0xFFFFFFFF, 0, + 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -924,8 +955,8 @@ TEST(corrupt_empty_string_table_with_string_ref) { } TEST(corrupt_node_count_max_uint32) { - const std::uint32_t data[] = {0x52544552, 5, 0xFFFFFFFF, 44, 44, 0, - 0, 0, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 9, 0xFFFFFFFF, 48, 48, 0, + 0, 0, 0, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -934,9 +965,10 @@ TEST(corrupt_node_count_max_uint32) { TEST(corrupt_string_offset_plus_length_overflow) { const std::uint32_t data[] = { - 0x52544552, 5, 2, 96, 96, 0, 0, 0, - 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, - 0x80000000, 0x80000001, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; + 0x52544552, 9, 2, 112, 112, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + 0xFFFFFFFF, 0, 1, 0, 0, 0, 0x80000000, + 0x80000001, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -945,7 +977,8 @@ TEST(corrupt_string_offset_plus_length_overflow) { TEST(corrupt_string_offset_plus_length_overflow_with_data) { std::vector data; - const std::uint32_t header[] = {0x52544552, 5, 2, 96, 97, 0, 0, 0}; + const std::uint32_t header[] = {0x52544552, 9, 2, 112, 113, 0, + 0, 0, 0, 0, 0, 0}; const std::uint32_t root[] = {0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0}; const std::uint32_t variable[] = {0xFFFFFFFF, 2, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; @@ -965,7 +998,8 @@ TEST(corrupt_string_offset_plus_length_overflow_with_data) { TEST(corrupt_literal_string_offset_plus_length_overflow) { std::vector data; - const std::uint32_t header[] = {0x52544552, 5, 2, 96, 97, 0, 0, 0}; + const std::uint32_t header[] = {0x52544552, 9, 2, 112, 113, 0, + 0, 0, 0, 0, 0, 0}; const std::uint32_t root[] = {0, 0, 1, 1, 0xFFFFFFFF, 0, 0, 0}; const std::uint32_t child[] = {0xFFFFFFFF, 2, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; @@ -4042,3 +4076,462 @@ TEST_F(URITemplateRouterViewTest, view_of_a_missing_file_throws) { FAIL(); } } + +TEST(corrupt_too_small_accessors_yield_defaults) { + const std::uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03}; + const sourcemeta::core::URITemplateRouterView view{data, sizeof(data)}; + EXPECT_EQ(view.size(), 0); + EXPECT_TRUE(view.base_path().empty()); + EXPECT_TRUE(view.base_url().empty()); + EXPECT_EQ(view.at(0), 0); + EXPECT_EQ(view.context(1), 0); + EXPECT_EQ(view.path(1), ""); + EXPECT_EQ(view.operation("op_1").first, 0); + EXPECT_EQ(view.operation("op_1").second, 0); + EXPECT_TRUE(view.operation_id(1).empty()); + EXPECT_FALSE(view.describes("/users")); + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_wrong_magic_accessors_yield_defaults) { + const std::uint32_t data[] = {0xDEADBEEF, 9, 1, 64, 64, 64, 0, 5, 0, 0, + 5, 64, 0, 0, 0, 0, 0, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_EQ(view.size(), 0); + EXPECT_TRUE(view.base_path().empty()); + EXPECT_TRUE(view.base_url().empty()); + EXPECT_EQ(view.at(0), 0); + EXPECT_EQ(view.context(1), 0); + EXPECT_EQ(view.path(1), ""); + EXPECT_EQ(view.operation("op_1").first, 0); + EXPECT_EQ(view.operation("op_1").second, 0); + EXPECT_TRUE(view.operation_id(1).empty()); + EXPECT_FALSE(view.describes("/users")); + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_wrong_version_accessors_yield_defaults) { + const std::uint32_t data[] = {0x52544552, 99, 1, 64, 64, 64, 0, 5, 0, 0, + 5, 64, 0, 0, 0, 0, 0, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_EQ(view.size(), 0); + EXPECT_TRUE(view.base_path().empty()); + EXPECT_TRUE(view.base_url().empty()); + EXPECT_EQ(view.at(0), 0); + EXPECT_EQ(view.context(1), 0); + EXPECT_EQ(view.path(1), ""); + EXPECT_EQ(view.operation("op_1").first, 0); + EXPECT_EQ(view.operation("op_1").second, 0); + EXPECT_TRUE(view.operation_id(1).empty()); + EXPECT_FALSE(view.describes("/users")); +} + +TEST(corrupt_offsets_accessors_yield_defaults) { + const std::uint32_t data[] = {0x52544552, 9, 1, 99999, 99999, 99999, 0, + 5, 0, 0, 5, 99999, 0, 0, + 0, 0, 0, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_TRUE(view.base_path().empty()); + EXPECT_TRUE(view.base_url().empty()); + EXPECT_EQ(view.at(0), 0); + EXPECT_EQ(view.context(1), 0); + EXPECT_EQ(view.path(1), ""); + EXPECT_EQ(view.operation("op_1").first, 0); + EXPECT_EQ(view.operation("op_1").second, 0); + EXPECT_TRUE(view.operation_id(1).empty()); + EXPECT_FALSE(view.describes("/users")); + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_zero_node_count_accessors_yield_defaults) { + const std::uint32_t data[] = {0x52544552, 9, 0, 48, 48, 48, + 0, 0, 0, 0, 0, 48}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_EQ(view.size(), 0); + EXPECT_EQ(view.at(0), 0); + EXPECT_FALSE(view.describes("/users")); +} + +TEST(corrupt_literal_child_out_of_bounds_describes_nothing) { + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 80, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + 999, 1, 0xFFFFFFFF, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_FALSE(view.describes("/users")); +} + +TEST(corrupt_variable_child_out_of_bounds_describes_nothing) { + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 80, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + 0xFFFFFFFF, 0, 500, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_FALSE(view.describes("/users")); +} + +TEST(corrupt_variable_string_offset_out_of_bounds_describes_nothing) { + const std::uint32_t data[] = { + 0x52544552, 9, 2, 112, 112, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, + 9999, 100, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_FALSE(view.describes("/users")); +} + +TEST(corrupt_literal_string_offset_describes_nothing) { + const std::uint32_t data[] = {0x52544552, 9, 2, 112, 112, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, + 0xFFFFFFFF, 0, 0, 0, 9999, 5, 0xFFFFFFFF, 0, + 0xFFFFFFFF, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_FALSE(view.describes("/users")); +} + +// The hand-crafted corrupt buffers in this file hardcode the serialization +// version, so a format bump must fail here loudly rather than silently +// downgrading those tests into version mismatch rejections +TEST_F(URITemplateRouterViewTest, serialized_version_matches_corrupt_fixtures) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "op_1", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + std::ifstream input{this->path, std::ios::binary}; + const std::vector bytes{std::istreambuf_iterator{input}, + std::istreambuf_iterator{}}; + std::uint32_t version{0}; + std::memcpy(&version, bytes.data() + sizeof(std::uint32_t), sizeof(version)); + EXPECT_EQ(version, 9); +} + +TEST(corrupt_arguments_offset_before_string_table_describes_nothing) { + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 4, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_FALSE(view.describes("/users")); +} + +TEST(corrupt_match_arguments_offset_before_string_table) { + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 4, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); + EXPECT_EQ(captures.size(), 0); +} + +TEST_F(URITemplateRouterViewTest, match_without_leading_slash_is_otherwise) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "op_1", 1); + router.otherwise(99); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView view{this->path}; + EXPECT_ROUTER_MATCH(view, "users", 0, 99, captures); + EXPECT_EQ(captures.size(), 0); +} + +TEST_F(URITemplateRouterViewTest, lookup_of_unknown_identifiers_misses) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "op_1", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView view{this->path}; + EXPECT_EQ(view.at(5), 0); + EXPECT_EQ(view.context(12345), 0); + EXPECT_EQ(view.path(12345), ""); + EXPECT_TRUE(view.operation_id(12345).empty()); +} + +TEST(corrupt_arguments_section_too_small_for_count) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.push_back(0x00); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_arguments_entries_exceed_buffer) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0xFF, 0xFF}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_blob_too_short) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_blob_truncated_after_count) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x01, 0x00}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_key_length_exceeds_blob) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0xC8, 0x00}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_truncated_after_key) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), + {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 'k'}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_value_length_exceeds_blob) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), + {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 'k', 0x00, 0xC8, 0x00}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_integer_with_wrong_width) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 'k', 0x01, 0x04, 0x00, 0xAA, 0xBB, 0xCC, 0xDD}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_boolean_with_wrong_width) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 'k', 0x02, 0x02, 0x00, 0x01, 0x01}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_argument_unknown_type_tag) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x09, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x01, 0x00, 'k', 0x09, 0x01, 0x00, 0x01}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + bool argument_fired{false}; + view.arguments( + 1, [&argument_fired]( + const std::string_view, + const sourcemeta::core::URITemplateRouter::ArgumentValue &) { + argument_fired = true; + }); + EXPECT_FALSE(argument_fired); +} + +TEST(corrupt_operations_entries_exceed_buffer) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0xFF, 0xFF}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + EXPECT_EQ(view.operation("op_1").first, 0); + EXPECT_EQ(view.operation("op_1").second, 0); + EXPECT_TRUE(view.operation_id(1).empty()); +} + +TEST(corrupt_operation_entry_string_out_of_bounds) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00}); + bytes.insert(bytes.end(), {0x0F, 0x27, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x0B, 0x00}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + EXPECT_EQ(view.operation("op_1").first, 0); + EXPECT_EQ(view.operation("op_1").second, 0); + EXPECT_TRUE(view.operation_id(1).empty()); +} + +TEST(corrupt_paths_entries_exceed_buffer) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0xFF, 0xFF}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + EXPECT_EQ(view.at(0), 0); + EXPECT_EQ(view.context(1), 0); + EXPECT_EQ(view.path(1), ""); +} + +TEST(corrupt_path_entry_string_out_of_bounds) { + auto bytes{corrupt_router_core(0x50, 0x50)}; + bytes.insert(bytes.end(), {0x01, 0x00}); + bytes.insert(bytes.end(), {0x01, 0x00, 0x0B, 0x00, 0x0F, 0x27, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + EXPECT_EQ(view.at(0), 1); + EXPECT_EQ(view.context(1), 11); + EXPECT_EQ(view.path(1), ""); +} + +TEST(corrupt_base_path_outside_string_table) { + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 80, 0, 999, + 5, 0, 0, 0, 0, 0, 0, + 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_TRUE(view.base_path().empty()); +} + +TEST(corrupt_base_url_outside_string_table) { + const std::uint32_t data[] = {0x52544552, 9, 1, 80, 80, 0, 0, + 0, 0, 999, 5, 0, 0, 0, + 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + const sourcemeta::core::URITemplateRouterView view{ + reinterpret_cast(data), sizeof(data)}; + EXPECT_TRUE(view.base_url().empty()); +} + +TEST(corrupt_string_table_with_valid_operation_and_path_tables) { + const std::uint32_t words[] = { + 0x52544552, 9, 1, 99999, 99999, 0x50, 0, 0, 0, 0, + 0, 0, 0, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + std::vector bytes{ + reinterpret_cast(words), + reinterpret_cast(words) + sizeof(words)}; + const std::uint32_t paths_offset{static_cast(bytes.size())}; + std::memcpy(bytes.data() + 44, &paths_offset, sizeof(paths_offset)); + bytes.insert(bytes.end(), {0x01, 0x00}); + bytes.insert(bytes.end(), {0x01, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00}); + const std::uint32_t operations_offset{ + static_cast(bytes.size())}; + std::memcpy(bytes.data() + 20, &operations_offset, sizeof(operations_offset)); + bytes.insert(bytes.end(), {0x01, 0x00}); + bytes.insert(bytes.end(), {0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x0B, 0x00}); + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + EXPECT_EQ(view.operation("op_1").first, 0); + EXPECT_EQ(view.operation("op_1").second, 0); + EXPECT_EQ(view.path(1), ""); + EXPECT_TRUE(view.operation_id(1).empty()); +} + +TEST(corrupt_paths_offset_past_end) { + auto bytes{corrupt_router_core(0x50, 0xFFFF)}; + const sourcemeta::core::URITemplateRouterView view{bytes.data(), + bytes.size()}; + EXPECT_EQ(view.at(0), 0); + EXPECT_EQ(view.context(1), 0); +} diff --git a/test/yaml/yaml_parse_callback_test.cc b/test/yaml/yaml_parse_callback_test.cc index 95c278a71..3bc89774b 100644 --- a/test/yaml/yaml_parse_callback_test.cc +++ b/test/yaml/yaml_parse_callback_test.cc @@ -486,3 +486,61 @@ TEST(scalar_alias_pre_post_balance) { EXPECT_TRACE(4, Post, String, 2, 12, Root, 0, ""); EXPECT_TRACE(5, Post, Object, 3, 0, Root, 0, ""); } + +TEST(parse_stream_in_place_with_callback) { + std::istringstream stream{"foo: 1\n"}; + sourcemeta::core::JSON output{nullptr}; + std::size_t events{0}; + sourcemeta::core::parse_yaml( + stream, output, + [&events](const sourcemeta::core::JSON::ParsePhase, + const sourcemeta::core::JSON::Type, const std::uint64_t, + const std::uint64_t, const sourcemeta::core::JSON::ParseContext, + const std::size_t, + const sourcemeta::core::JSON::String &) { events += 1; }); + EXPECT_TRUE(output.is_object()); + EXPECT_EQ(output.size(), 1); + EXPECT_EQ(output.at("foo").to_integer(), 1); + EXPECT_EQ(events, 4); +} + +TEST(read_in_place_with_callback_invalid) { + sourcemeta::core::JSON output{nullptr}; + try { + sourcemeta::core::read_yaml( + std::filesystem::path{STUBS_PATH} / "invalid.yaml", output, nullptr); + FAIL(); + } catch (const sourcemeta::core::YAMLFileParseError &error) { + EXPECT_EQ(error.path(), std::filesystem::path{STUBS_PATH} / "invalid.yaml"); + EXPECT_EQ(error.line(), 1); + EXPECT_EQ(error.column(), 15); + } +} + +TEST(read_yaml_or_json_in_place_falls_back_to_yaml) { + sourcemeta::core::JSON output{nullptr}; + sourcemeta::core::read_yaml_or_json(std::filesystem::path{STUBS_PATH} / + "test_no_extension_yaml", + output, nullptr); + const auto expected{ + sourcemeta::core::parse_json(R"JSON({ "foo": "bar", "baz": 2 })JSON")}; + EXPECT_EQ(output, expected); +} + +TEST(parse_in_place_with_roundtrip_and_callback) { + const std::string input{"foo: 1\n"}; + sourcemeta::core::YAMLRoundTrip metadata; + sourcemeta::core::JSON output{nullptr}; + std::size_t events{0}; + sourcemeta::core::parse_yaml( + input, metadata, output, + [&events](const sourcemeta::core::JSON::ParsePhase, + const sourcemeta::core::JSON::Type, const std::uint64_t, + const std::uint64_t, const sourcemeta::core::JSON::ParseContext, + const std::size_t, + const sourcemeta::core::JSON::String &) { events += 1; }); + EXPECT_TRUE(output.is_object()); + EXPECT_EQ(output.size(), 1); + EXPECT_EQ(output.at("foo").to_integer(), 1); + EXPECT_EQ(events, 4); +} diff --git a/test/yaml/yaml_roundtrip_test.cc b/test/yaml/yaml_roundtrip_test.cc index 8a21933da..86b2f26ad 100644 --- a/test/yaml/yaml_roundtrip_test.cc +++ b/test/yaml/yaml_roundtrip_test.cc @@ -4927,3 +4927,43 @@ TEST(implicit_null_anchor_with_inline_comment_sequence) { )YAML"}; EXPECT_EQ(roundtrip(input), input); } + +TEST(single_quoted_scalar_with_exponent_shape) { + const std::string input{"foo: '1e+5'\n"}; + EXPECT_EQ(roundtrip(input), input); +} + +TEST(single_quoted_scalar_with_embedded_quote) { + const std::string input{"foo: 'it''s'\n"}; + EXPECT_EQ(roundtrip(input), input); +} + +TEST(double_quoted_scalar_with_carriage_return) { + const std::string input{"foo: \"a\\rb\"\n"}; + EXPECT_EQ(roundtrip(input), input); +} + +TEST(double_quoted_scalar_with_tab_escape) { + const std::string input{"foo: \"a\\tb\"\n"}; + EXPECT_EQ(roundtrip(input), input); +} + +TEST(double_quoted_scalar_with_null_escape) { + const std::string input{"foo: \"a\\0b\"\n"}; + EXPECT_EQ(roundtrip(input), input); +} + +TEST(double_quoted_scalar_with_control_escape) { + const std::string input{"foo: \"a\\x01b\"\n"}; + EXPECT_EQ(roundtrip(input), input); +} + +TEST(anchored_null_alias) { + const std::string input{"a: &x null\nb: *x\n"}; + EXPECT_EQ(roundtrip(input), input); +} + +TEST(flow_sequence_anchor_alias) { + const std::string input{"c: [&y 1, *y]\n"}; + EXPECT_EQ(roundtrip(input), input); +} diff --git a/test/yaml/yaml_stringify_test.cc b/test/yaml/yaml_stringify_test.cc index 9f3808213..964ddd7bb 100644 --- a/test/yaml/yaml_stringify_test.cc +++ b/test/yaml/yaml_stringify_test.cc @@ -876,3 +876,40 @@ TEST(sequence_with_quoted_strings) { sourcemeta::core::stringify_yaml(document, stream); EXPECT_EQ(stream.str(), "- \"true\"\n- \"42\"\n- \"null\"\n"); } + +TEST(string_with_carriage_return) { + const sourcemeta::core::JSON document{"a\rb"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"a\\rb\"\n"); +} + +TEST(string_with_tab) { + const sourcemeta::core::JSON document{"a\tb"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"a\\tb\"\n"); +} + +TEST(string_with_null_byte) { + const sourcemeta::core::JSON document{std::string{"a\0b", 3}}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"a\\0b\"\n"); +} + +TEST(string_with_control_byte) { + const sourcemeta::core::JSON document{std::string{"a\x01" + "b", + 3}}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"a\\x01b\"\n"); +} + +TEST(string_with_quote_backslash_and_newline) { + const sourcemeta::core::JSON document{"a\"b\\c\nd"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"a\\\"b\\\\c\\nd\"\n"); +}