This is an automated email from the ASF dual-hosted git repository.
mochen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 46838c88c2 Fix wrong checks for some integer config records (#12643)
46838c88c2 is described below
commit 46838c88c2e8b9583674002e15b843308de34d77
Author: Mo Chen <[email protected]>
AuthorDate: Wed Jan 14 10:34:42 2026 -0600
Fix wrong checks for some integer config records (#12643)
RECC_INT expects a numeric range, and not a regex. Regex should be used
with RECC_STR.
1. Fix configs where RECC_INT is used with an invalid range.
2. Add a static_assert so that RECC_INT is always used with a range.
3. Make RECC_INT range check more robust.
4. Add unit tests for range check.
---
src/records/CMakeLists.txt | 5 +-
src/records/RecUtils.cc | 68 ++++++++---
src/records/RecordsConfig.cc | 146 ++++++++++++++++++++---
src/records/unit_tests/test_RecUtils.cc | 201 ++++++++++++++++++++++++++++++++
4 files changed, 389 insertions(+), 31 deletions(-)
diff --git a/src/records/CMakeLists.txt b/src/records/CMakeLists.txt
index 6a4bc9ff2f..907ec4f24c 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 5d1c63ba8e..5532f23370 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 <charconv>
+#include <string_view>
//-------------------------------------------------------------------------
// 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<char *>(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 7ca85a7553..096f522ca9 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -26,6 +26,9 @@
#include "tscore/Filenames.h"
#include "records/RecordsConfig.h"
+#include <string_view>
+#include <cstddef>
+
#if TS_USE_REMOTE_UNWINDING
#define MGMT_CRASHLOG_HELPER "traffic_crashlog"
#else
@@ -73,11 +76,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}
@@ -207,7 +210,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}
,
@@ -414,7 +417,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 #
@@ -1082,9 +1085,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}
,
//##############################################################################
@@ -1104,7 +1107,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}
,
@@ -1251,16 +1254,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}
@@ -1472,13 +1475,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}
,
@@ -1577,6 +1580,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[in,out] 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 <std::size_t N>
+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 <std::size_t N>
+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.");
@@ -1585,6 +1698,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 0000000000..30acc44a4b
--- /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 <catch2/catch_test_macros.hpp>
+
+#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
+ }
+}