diff --git a/src/records/CMakeLists.txt b/src/records/CMakeLists.txt index 6a4bc9ff2fc..907ec4f24c6 100644 --- a/src/records/CMakeLists.txt +++ b/src/records/CMakeLists.txt @@ -40,7 +40,10 @@ target_link_libraries( ) if(BUILD_TESTING) - add_executable(test_records unit_tests/unit_test_main.cc unit_tests/test_RecHttp.cc unit_tests/test_RecRegister.cc) + add_executable( + test_records unit_tests/unit_test_main.cc unit_tests/test_RecHttp.cc unit_tests/test_RecUtils.cc + unit_tests/test_RecRegister.cc + ) target_link_libraries(test_records PRIVATE records Catch2::Catch2 ts::tscore libswoc::libswoc ts::inkevent) add_catch2_test(NAME test_records COMMAND test_records) endif() diff --git a/src/records/RecUtils.cc b/src/records/RecUtils.cc index 5d1c63ba8e1..5532f233708 100644 --- a/src/records/RecUtils.cc +++ b/src/records/RecUtils.cc @@ -29,6 +29,9 @@ #include "records/RecordsConfig.h" #include "P_RecUtils.h" #include "P_RecCore.h" + +#include +#include //------------------------------------------------------------------------- // RecRecord initializer / Free //------------------------------------------------------------------------- @@ -390,23 +393,56 @@ recordRegexCheck(const char *pattern, const char *value) bool recordRangeCheck(const char *pattern, const char *value) { - char *p = const_cast(pattern); - Tokenizer dashTok("-"); - - if (recordRegexCheck("^[0-9]+$", value)) { - while (*p != '[') { - p++; - } // skip to '[' - if (dashTok.Initialize(++p, COPY_TOKS) == 2) { - int l_limit = atoi(dashTok[0]); - int u_limit = atoi(dashTok[1]); - int val = atoi(value); - if (val >= l_limit && val <= u_limit) { - return true; - } - } + std::string_view sv_pattern(pattern); + + // Find '[' and ']' + auto start = sv_pattern.find('['); + if (start != 0) { + Warning("recordRangeCheck: pattern '%s' does not start with '['", pattern); + return false; // No '[' found } - return false; + + auto end = sv_pattern.find(']', start); + if (end != sv_pattern.size() - 1) { + return false; // No ']' found + } + + // Extract range portion between brackets: "[0-10]" -> "0-10" or "[-123--100]" -> "-123--100" + sv_pattern = sv_pattern.substr(start + 1, end - start - 1); + + RecInt lower_limit; + auto [lower_end, ec1] = std::from_chars(sv_pattern.data(), sv_pattern.data() + sv_pattern.size(), lower_limit); + if (ec1 != std::errc{}) { + Warning("recordRangeCheck: failed to parse lower bound in pattern '%s'", pattern); + return false; // Failed to parse lower bound + } + + if (lower_end >= sv_pattern.data() + sv_pattern.size() || *lower_end != '-') { + Warning("recordRangeCheck: no dash separator found in pattern '%s'", pattern); + return false; // No dash separator found + } + + auto pos = lower_end + 1 - sv_pattern.data(); + sv_pattern.remove_prefix(pos); + RecInt upper_limit; + auto [upper_end, ec2] = std::from_chars(sv_pattern.data(), sv_pattern.data() + sv_pattern.size(), upper_limit); + if (ec2 != std::errc{} || upper_end != sv_pattern.data() + sv_pattern.size()) { + Warning("recordRangeCheck: failed to parse upper bound in pattern '%s'", pattern); + return false; // Failed to parse upper bound or extra characters + } + + if (lower_limit > upper_limit) { + Warning("recordRangeCheck: invalid range in pattern '%s'", pattern); + return false; + } + + RecInt val; + auto [value_end, ec3] = std::from_chars(value, value + strlen(value), val); + if (ec3 != std::errc{} || value_end != value + strlen(value)) { + return false; // Value parse error + } + + return (val >= lower_limit && val <= upper_limit); } bool diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index fbc2d3eb653..31b3724e596 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -25,6 +25,9 @@ #include "tscore/Filenames.h" #include "records/RecordsConfig.h" +#include +#include + #if TS_USE_REMOTE_UNWINDING #define MGMT_CRASHLOG_HELPER "traffic_crashlog" #else @@ -72,11 +75,11 @@ static constexpr RecordElement RecordsConfig[] = {RECT_CONFIG, "proxy.config.dump_mem_info_frequency", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , //# 0 == CLOCK_REALTIME, change carefully - {RECT_CONFIG, "proxy.config.system_clock", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-9]+", RECA_NULL} + {RECT_CONFIG, "proxy.config.system_clock", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , {RECT_CONFIG, "proxy.config.cache.max_disk_errors", RECD_INT, "5", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , - {RECT_CONFIG, "proxy.config.cache.persist_bad_disks", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[01]", RECA_NULL} + {RECT_CONFIG, "proxy.config.cache.persist_bad_disks", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , {RECT_CONFIG, "proxy.config.output.logfile.name", RECD_STRING, "traffic.out", RECU_RESTART_TS, RR_REQUIRED, RECC_NULL, nullptr, RECA_NULL} @@ -204,7 +207,7 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.diags.debug.tags", RECD_STRING, "http|dns", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , - {RECT_CONFIG, "proxy.config.diags.debug.throttling_interval_msec", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.diags.debug.throttling_interval_msec", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , {RECT_CONFIG, "proxy.config.diags.debug.client_ip", RECD_STRING, nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , @@ -409,7 +412,7 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.http.auth_server_session_private", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-2]", RECA_NULL} , - {RECT_CONFIG, "proxy.config.http.max_post_size", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.http.max_post_size", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , // ############################## // # parent proxy configuration # @@ -1075,9 +1078,9 @@ static constexpr RecordElement RecordsConfig[] = {RECT_CONFIG, "proxy.config.log.max_line_size", RECD_INT, "9216", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , // How often periodic tasks get executed in the Log.cc infrastructure - {RECT_CONFIG, "proxy.config.log.periodic_tasks_interval", RECD_INT, "5", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.log.periodic_tasks_interval", RECD_INT, "5", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , - {RECT_CONFIG, "proxy.config.log.throttling_interval_msec", RECD_INT, "60000", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.log.throttling_interval_msec", RECD_INT, "60000", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , //############################################################################## @@ -1097,7 +1100,7 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.plugin.dynamic_reload_mode", RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , - {RECT_CONFIG, "proxy.config.url_remap.min_rules_required", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-9]+", RECA_NULL} + {RECT_CONFIG, "proxy.config.url_remap.min_rules_required", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , {RECT_CONFIG, "proxy.config.url_remap.acl_behavior_policy", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , @@ -1244,16 +1247,16 @@ static constexpr RecordElement RecordsConfig[] = {RECT_CONFIG, "proxy.config.ssl.ocsp.enabled", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , // # Number of seconds before an OCSP response expires in the stapling cache. 3600s (1 hour) by default. - {RECT_CONFIG, "proxy.config.ssl.ocsp.cache_timeout", RECD_INT, "3600", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.ssl.ocsp.cache_timeout", RECD_INT, "3600", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , // # Request method "mode" for queries to OCSP responders; 0 is POST, 1 is "prefer GET." {RECT_CONFIG, "proxy.config.ssl.ocsp.request_mode", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , // # Timeout for queries to OCSP responders. 10s by default. - {RECT_CONFIG, "proxy.config.ssl.ocsp.request_timeout", RECD_INT, "10", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.ssl.ocsp.request_timeout", RECD_INT, "10", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , // # Update period for stapling caches. 60s (1 min) by default. - {RECT_CONFIG, "proxy.config.ssl.ocsp.update_period", RECD_INT, "60", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.ssl.ocsp.update_period", RECD_INT, "60", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , // # Base path for OCSP prefetched responses {RECT_CONFIG, "proxy.config.ssl.ocsp.response.path", RECD_STRING, TS_BUILD_SYSCONFDIR, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL} @@ -1465,13 +1468,13 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.quic.disable_active_migration", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_STR, "[0-1]", RECA_NULL} , - {RECT_CONFIG, "proxy.config.quic.max_recv_udp_payload_size_in", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.quic.max_recv_udp_payload_size_in", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , - {RECT_CONFIG, "proxy.config.quic.max_recv_udp_payload_size_out", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.quic.max_recv_udp_payload_size_out", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , - {RECT_CONFIG, "proxy.config.quic.max_send_udp_payload_size_in", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.quic.max_send_udp_payload_size_in", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , - {RECT_CONFIG, "proxy.config.quic.max_send_udp_payload_size_out", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_INT, "^[0-9]+$", RECA_NULL} + {RECT_CONFIG, "proxy.config.quic.max_send_udp_payload_size_out", RECD_INT, "65527", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL} , {RECT_CONFIG, "proxy.config.quic.disable_http_0_9", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_STR, "[0-1]", RECA_NULL} , @@ -1565,6 +1568,116 @@ validate_check_type_has_regex() return true; } +//------------------------------------------------------------------------- +// Compile-time validation helpers for RECC_INT patterns +//------------------------------------------------------------------------- + +namespace +{ +constexpr bool +is_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +/** + * Parses an integer (possibly negative) from the given string view at compile time. + * Updates the index `i` to point past the parsed number. + * Returns true if a valid integer was parsed, false otherwise. + * + * This function is intended for compile-time validation of integer patterns. + * Note: C++23 introduces std::from_chars for constexpr integer parsing. + * + * @param[in] s The string view to parse. + * @param[inout] i The index into `s` where parsing starts; updated to point past the parsed integer. + * @return true if parsing succeeded, false otherwise. + */ +constexpr bool +parse_int(std::string_view s, std::size_t &i) +{ + std::size_t start = i; + + // Optional negative sign + if (i < s.size() && s[i] == '-') { + ++i; + } + + // Must have at least one digit + if (i >= s.size() || !is_digit(s[i])) { + i = start; // restore position on failure + return false; + } + + // Parse remaining digits + while (i < s.size() && is_digit(s[i])) { + ++i; + } + + return true; // Successfully parsed at least one digit +} + +/** + * @brief Validates whether a string matches the [lower-upper] format with integer bounds. + * + * Supports negative numbers for both bounds. + * Examples of valid formats: "[0-255]", "[-100--50]". + * + * @param s The string to validate. + * @return true if the string matches the format, false otherwise. + */ +constexpr bool +matches_bracketed_int_range(std::string_view s) +{ + std::size_t i = 0; + if (i >= s.size() || s[i++] != '[') { + return false; + } + // Parse first integer (may be negative) + if (!parse_int(s, i)) { + return false; + } + // Next character should be the dash separator + if (i >= s.size() || s[i++] != '-') { + return false; + } + // Parse second integer (may be negative) + if (!parse_int(s, i)) { + return false; + } + if (i >= s.size() || s[i++] != ']') { + return false; + } + return i == s.size(); // must end exactly here +} + +// For string literals: deduces N and strips the trailing '\0' +template +consteval bool +matches_bracketed_int_range(const char (&lit)[N]) +{ + // N includes the null terminator + return matches_bracketed_int_range(std::string_view{lit, N - 1}); +} + +// Validate all RECC_INT entries in the RecordsConfig array at compile time +template +consteval bool +validate_recc_int_patterns(const RecordElement (&config)[N]) +{ + for (std::size_t i = 0; i < N; ++i) { + if (config[i].check == RECC_INT) { + if (config[i].regex == nullptr) { + return false; // RECC_INT must have a pattern + } + if (!matches_bracketed_int_range(config[i].regex)) { + return false; // Pattern must match [lower-upper] format + } + } + } + return true; +} +} // namespace + static_assert(validate_regex_has_check_type(), "RecordsConfig validation failed: found RecordElement with regex pattern but no check type (RECC_NULL). " "Specify appropriate check type like RECC_INT, RECC_STRING, etc."); @@ -1573,6 +1686,11 @@ static_assert(validate_check_type_has_regex(), "RecordsConfig validation failed: found RecordElement with check type but no regex pattern. " "Provide a regex pattern to validate the input or use RECC_NULL if no validation is needed."); +static_assert(validate_recc_int_patterns(RecordsConfig), + "RecordsConfig validation failed: found RECC_INT with invalid pattern. " + "RECC_INT patterns must be in format [lower-upper] with integer bounds (negative numbers supported), " + "e.g., \"[0-2]\", \"[1-256]\", or \"[-100--50]\"."); + void RecordsConfigIterate(RecordElementCallback callback, void *data) { diff --git a/src/records/unit_tests/test_RecUtils.cc b/src/records/unit_tests/test_RecUtils.cc new file mode 100644 index 00000000000..30acc44a4b1 --- /dev/null +++ b/src/records/unit_tests/test_RecUtils.cc @@ -0,0 +1,201 @@ +/** @file + + Catch-based tests for RecUtils.cc + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include + +#include "../P_RecUtils.h" + +TEST_CASE("recordRangeCheck via RecordValidityCheck", "[librecords][RecUtils]") +{ + SECTION("valid ranges") + { + // Basic range checks + REQUIRE(RecordValidityCheck("0", RECC_INT, "[0-1]")); + REQUIRE(RecordValidityCheck("1", RECC_INT, "[0-1]")); + REQUIRE(RecordValidityCheck("5", RECC_INT, "[0-10]")); + REQUIRE(RecordValidityCheck("10", RECC_INT, "[0-10]")); + + // Larger ranges + REQUIRE(RecordValidityCheck("100", RECC_INT, "[0-255]")); + REQUIRE(RecordValidityCheck("255", RECC_INT, "[0-255]")); + REQUIRE(RecordValidityCheck("1024", RECC_INT, "[1-2048]")); + } + + SECTION("boundary conditions") + { + // Lower boundary + REQUIRE(RecordValidityCheck("0", RECC_INT, "[0-100]")); + REQUIRE(RecordValidityCheck("1", RECC_INT, "[1-100]")); + + // Upper boundary + REQUIRE(RecordValidityCheck("100", RECC_INT, "[0-100]")); + REQUIRE(RecordValidityCheck("99", RECC_INT, "[0-99]")); + + // Single value range + REQUIRE(RecordValidityCheck("5", RECC_INT, "[5-5]")); + } + + SECTION("out of range values") + { + // Below lower bound + REQUIRE_FALSE(RecordValidityCheck("-1", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck("0", RECC_INT, "[1-10]")); + + // Above upper bound + REQUIRE_FALSE(RecordValidityCheck("11", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck("256", RECC_INT, "[0-255]")); + + // Way out of bounds + REQUIRE_FALSE(RecordValidityCheck("1000", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck("-1000", RECC_INT, "[0-10]")); + } + + SECTION("invalid input formats") + { + // Non-numeric values (should fail parse_int validation) + REQUIRE_FALSE(RecordValidityCheck("abc", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck("12abc", RECC_INT, "[0-100]")); + REQUIRE_FALSE(RecordValidityCheck("abc12", RECC_INT, "[0-100]")); + REQUIRE_FALSE(RecordValidityCheck("1.5", RECC_INT, "[0-10]")); + + // Empty string + REQUIRE_FALSE(RecordValidityCheck("", RECC_INT, "[0-10]")); + + // Whitespace + REQUIRE_FALSE(RecordValidityCheck(" 5", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck("5 ", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck(" 5 ", RECC_INT, "[0-10]")); + } + + SECTION("negative ranges supported") + { + // Patterns with negative numbers are now supported + // Parsed left-to-right to handle dash as separator vs negative sign + REQUIRE(RecordValidityCheck("-5", RECC_INT, "[-10-0]")); + REQUIRE(RecordValidityCheck("0", RECC_INT, "[-10-10]")); + REQUIRE(RecordValidityCheck("-1", RECC_INT, "[-5--1]")); + REQUIRE(RecordValidityCheck("-100", RECC_INT, "[-123--100]")); + REQUIRE(RecordValidityCheck("-50", RECC_INT, "[-100-0]")); + + // Positive value in negative range + REQUIRE(RecordValidityCheck("5", RECC_INT, "[-10-20]")); + + // Out of range negative values + REQUIRE_FALSE(RecordValidityCheck("-11", RECC_INT, "[-10-0]")); + REQUIRE_FALSE(RecordValidityCheck("-6", RECC_INT, "[-5--1]")); + REQUIRE_FALSE(RecordValidityCheck("-124", RECC_INT, "[-123--100]")); + + // Boundary conditions with negative numbers + REQUIRE(RecordValidityCheck("-123", RECC_INT, "[-123--100]")); // lower bound + REQUIRE(RecordValidityCheck("-100", RECC_INT, "[-123--100]")); // upper bound + REQUIRE_FALSE(RecordValidityCheck("-99", RECC_INT, "[-123--100]")); // just above upper bound + } + + SECTION("invalid pattern formats") + { + // Missing brackets + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "0-10")); + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "[0-10")); + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "0-10]")); + + // No bracket at all + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "invalid")); + + // Invalid range format (no dash) + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "[010]")); + + // Non-numeric range + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "[a-z]")); + } + + SECTION("edge cases from actual ATS config") + { + // From RecordsConfig.cc examples + REQUIRE(RecordValidityCheck("0", RECC_INT, "[0-1]")); + REQUIRE(RecordValidityCheck("1", RECC_INT, "[0-1]")); + REQUIRE(RecordValidityCheck("2", RECC_INT, "[0-2]")); + REQUIRE(RecordValidityCheck("3", RECC_INT, "[0-3]")); + REQUIRE(RecordValidityCheck("256", RECC_INT, "[1-256]")); + + // Common boolean patterns + REQUIRE(RecordValidityCheck("0", RECC_INT, "[0-1]")); // false + REQUIRE(RecordValidityCheck("1", RECC_INT, "[0-1]")); // true + REQUIRE_FALSE(RecordValidityCheck("2", RECC_INT, "[0-1]")); // invalid bool + } + + SECTION("std::from_chars advantages - strict parsing") + { + // These would pass with atoi("123abc") -> 123 + // But fail with from_chars (correct behavior) + REQUIRE_FALSE(RecordValidityCheck("5extra", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck("10garbage", RECC_INT, "[0-100]")); + REQUIRE_FALSE(RecordValidityCheck("0x10", RECC_INT, "[0-100]")); // hex not supported in this context + } + + SECTION("zero handling") + { + // Ensure "0" is properly distinguished from parse errors + // (atoi returns 0 for both "0" and parse errors) + REQUIRE(RecordValidityCheck("0", RECC_INT, "[0-10]")); + REQUIRE_FALSE(RecordValidityCheck("invalid", RECC_INT, "[0-10]")); // Also would be 0 with atoi + } + + SECTION("large numbers") + { + // Test with realistic config values + REQUIRE(RecordValidityCheck("65535", RECC_INT, "[1-65535]")); // max uint16 + REQUIRE(RecordValidityCheck("8080", RECC_INT, "[1-65535]")); // typical port + REQUIRE(RecordValidityCheck("32768", RECC_INT, "[1024-65535]")); + + // Out of typical ranges + REQUIRE_FALSE(RecordValidityCheck("65536", RECC_INT, "[1-65535]")); + REQUIRE_FALSE(RecordValidityCheck("100000", RECC_INT, "[1-65535]")); + } + + SECTION("overflow and underflow handling") + { + // RecInt is int64_t: + // INT64_MAX = 9223372036854775807 + // INT64_MIN = -9223372036854775808 + + // Valid boundary values for int64_t + REQUIRE(RecordValidityCheck("9223372036854775807", RECC_INT, "[0-9223372036854775807]")); // INT64_MAX + REQUIRE(RecordValidityCheck("-9223372036854775808", RECC_INT, "[-9223372036854775808-0]")); // INT64_MIN + + // Values that overflow INT64_MAX (should fail to parse) + REQUIRE_FALSE(RecordValidityCheck("9223372036854775808", RECC_INT, "[0-9999999999999999999]")); // INT64_MAX + 1 + REQUIRE_FALSE(RecordValidityCheck("99999999999999999999", RECC_INT, "[0-99999999999999999999]")); // Way over + + // Values that underflow INT64_MIN (should fail to parse) + REQUIRE_FALSE(RecordValidityCheck("-9223372036854775809", RECC_INT, "[-9999999999999999999-0]")); // INT64_MIN - 1 + REQUIRE_FALSE(RecordValidityCheck("-99999999999999999999", RECC_INT, "[-99999999999999999999-0]")); // Way under + + // Pattern bounds that overflow (should fail to parse the pattern itself) + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "[0-9223372036854775808]")); // upper bound overflows + REQUIRE_FALSE(RecordValidityCheck("5", RECC_INT, "[-9223372036854775809-100]")); // lower bound underflows + + // Valid values near the boundaries + REQUIRE(RecordValidityCheck("9223372036854775806", RECC_INT, "[0-9223372036854775807]")); // INT64_MAX - 1 + REQUIRE(RecordValidityCheck("-9223372036854775807", RECC_INT, "[-9223372036854775808-0]")); // INT64_MIN + 1 + } +}