Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 52 additions & 148 deletions args.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@
#include <cstdlib>
#include <limits>
#include <iostream>
#if __cplusplus >= 201703L
#include <charconv>
#include <system_error>
#endif

#if defined(_MSC_VER) && _MSC_VER <= 1800
#define noexcept
Expand Down Expand Up @@ -266,67 +262,6 @@ namespace args
return true;
}

/** Parse an untrusted decimal size_t without locale-sensitive conversions.
* Leading and trailing whitespace are accepted, but signs and embedded
* whitespace are rejected. Returns false on overflow.
*/
inline bool ParseSizeT(const std::string &value, size_t &out)
{
auto it = std::find_if_not(value.begin(), value.end(), [](char c)
{
return std::isspace(static_cast<unsigned char>(c)) != 0;
});

if (it == value.end() || *it == '+' || *it == '-')
{
return false;
}

size_t parsed = 0;
bool sawDigit = false;
for (; it != value.end(); ++it)
{
const unsigned char ch = static_cast<unsigned char>(*it);
if (std::isspace(ch) != 0)
{
break;
}
if (!std::isdigit(ch))
{
return false;
}

size_t multiplied = 0;
if (!SafeMultiply<size_t>(parsed, static_cast<size_t>(10), multiplied))
{
return false;
}

const size_t digit = static_cast<size_t>(ch - static_cast<unsigned char>('0'));
if (!SafeAdd<size_t>(multiplied, digit, parsed))
{
return false;
}
sawDigit = true;
}

if (!sawDigit)
{
return false;
}

for (; it != value.end(); ++it)
{
if (std::isspace(static_cast<unsigned char>(*it)) == 0)
{
return false;
}
}

out = parsed;
return true;
}

/** (INTERNAL) Wrap a vector of words into a vector of lines
*
* Empty words are skipped. Word "\n" forces wrapping.
Expand Down Expand Up @@ -1545,9 +1480,45 @@ namespace args
{
syntax = value_.at(0);
const std::string &raw = value_.at(1);
bool failed = false;

const auto firstNonSpace = std::find_if_not(raw.begin(), raw.end(), [](char c)
{
return std::isspace(static_cast<unsigned char>(c)) != 0;
});

// Reject explicit signs: cword must be a plain non-negative
// decimal index. istringstream would otherwise silently
// accept "+1".
if (firstNonSpace != raw.end() && (*firstNonSpace == '-' || *firstNonSpace == '+'))
{
failed = true;
}

size_t parsed = 0;
if (!ParseSizeT(raw, parsed))
if (!failed)
{
std::istringstream ss(raw);
ss >> parsed;
if (ss.fail())
{
failed = true;
}
else
{
char extra;
if (ss >> extra)
{
failed = true;
}
else if (!ss.eof())
{
failed = true;
}
}
}

if (failed)
{
#ifdef ARGS_NOEXCEPT
error = Error::Parse;
Expand Down Expand Up @@ -3757,99 +3728,32 @@ namespace args
}

const char *begin = value.c_str();

// Preserve existing parsing semantics with std::from_chars
#if __cplusplus >= 201703L
const char *end_pos = value.c_str() + value.length();
while (begin != end_pos && std::isspace(static_cast<unsigned char>(*begin)))
{
++begin;
}
const bool negative = begin != end_pos && *begin == '-';
if (negative || (begin != end_pos && *begin == '+'))
{
++begin;
}

int base = 10;
if (end_pos - begin > 1 && *begin == '0')
{
if (*(begin + 1) == 'x' || *(begin + 1) == 'X')
{
begin += 2;
base = 16;
}
else
{
base = 8;
}
}

typedef typename std::make_unsigned<T>::type UnsignedT;
UnsignedT parsed = 0;
auto result = std::from_chars(begin, end_pos, parsed, base);

// Find end of valid parse
end_pos = result.ptr;

// Skip trailing whitespace following std::from_chars parse
while (end_pos != value.c_str() + value.length() &&
std::isspace(static_cast<unsigned char>(*end_pos)))
{
++end_pos;
}

if (end_pos != value.c_str() + value.length() ||
result.ec == std::errc::invalid_argument ||
result.ec == std::errc::result_out_of_range)
{
return false;
}

if (negative)
{
const UnsignedT minMagnitude =
static_cast<UnsignedT>(std::numeric_limits<T>::max()) + static_cast<UnsignedT>(1);
if (parsed > minMagnitude)
{
return false;
}
destination = parsed == minMagnitude ?
std::numeric_limits<T>::min() :
static_cast<T>(-static_cast<T>(parsed));
}
else
{
if (parsed > static_cast<UnsignedT>(std::numeric_limits<T>::max()))
{
return false;
}
destination = static_cast<T>(parsed);
}
return true;
#else
// C++11/14 fallback using strtoll/strtoull

// Save errno to detect if strtoull/strtoll sets it
// C++11-compatible: use strtoull/strtoll. Hardening retained from
// the original from_chars draft (errno save/restore, ERANGE check,
// narrowing range check, trailing-whitespace tolerance). No
// unconditional dependency on <charconv> / C++17.
const int saved_errno = errno;
errno = 0;

char *end = nullptr;

if (std::is_unsigned<T>::value)
{
const unsigned long long parsed = std::strtoull(begin, &end, 0);
if (end == begin)
{
errno = saved_errno;
return false;
}
while (end != nullptr && *end != '\0' && std::isspace(static_cast<unsigned char>(*end)))
while (*end != '\0' && std::isspace(static_cast<unsigned char>(*end)))
{
++end;
}
if (*end != '\0' || errno == ERANGE || parsed > static_cast<unsigned long long>(std::numeric_limits<T>::max()))
if (*end != '\0' || errno == ERANGE ||
parsed > static_cast<unsigned long long>(std::numeric_limits<T>::max()))
{
errno = saved_errno; // Restore errno on error
errno = saved_errno;
return false;
}

Expand All @@ -3860,26 +3764,26 @@ namespace args
const long long parsed = std::strtoll(begin, &end, 0);
if (end == begin)
{
errno = saved_errno;
return false;
}
while (end != nullptr && *end != '\0' && std::isspace(static_cast<unsigned char>(*end)))
while (*end != '\0' && std::isspace(static_cast<unsigned char>(*end)))
{
++end;
}
if (*end != '\0' || errno == ERANGE ||
parsed < static_cast<long long>(std::numeric_limits<T>::min()) ||
parsed > static_cast<long long>(std::numeric_limits<T>::max()))
{
errno = saved_errno; // Restore errno on error
errno = saved_errno;
return false;
}

destination = static_cast<T>(parsed);
}
errno = saved_errno; // Restore errno on success

errno = saved_errno;
return true;
#endif
}

template <typename T>
Expand Down
51 changes: 51 additions & 0 deletions fuzz/fuzz_numeric_parser.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include <string>
#include <cstdint>
#include <cstring>
#include <sstream>
#include "../args.hxx"

// Expose the ValueReader class for fuzzing
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size == 0) return 0;

// Create a null-terminated string from fuzzer input
std::string input(reinterpret_cast<const char*>(data), size);

// Test numeric parsing with various types
args::ValueReader reader;

// Test signed integer types
{
int dest_int = 0;
reader("fuzz_test", input, dest_int); // Should not crash

short dest_short = 0;
reader("fuzz_test", input, dest_short); // Should not crash

long long dest_ll = 0;
reader("fuzz_test", input, dest_ll); // Should not crash
}

// Test unsigned integer types
{
unsigned int dest_uint = 0;
reader("fuzz_test", input, dest_uint); // Should not crash

unsigned short dest_ushort = 0;
reader("fuzz_test", input, dest_ushort); // Should not crash

unsigned long long dest_ull = 0;
reader("fuzz_test", input, dest_ull); // Should not crash
}

// Test floating point types
{
float dest_float = 0.0f;
reader("fuzz_test", input, dest_float); // Should not crash

double dest_double = 0.0;
reader("fuzz_test", input, dest_double); // Should not crash
}

return 0;
}
27 changes: 0 additions & 27 deletions test/safe_arithmetic.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -261,30 +261,6 @@ void TestWrapLargeStringInput()
std::cout << "PASS: Wrap long string with width 1" << std::endl;
}

// Test bounded decimal parsing used for shell-provided completion indices
void TestParseSizeT()
{
size_t parsed = 0;

test::require(args::ParseSizeT("42", parsed));
test::require(parsed == 42);
std::cout << "PASS: ParseSizeT parses decimal input" << std::endl;

test::require(args::ParseSizeT(" \t42\n", parsed));
test::require(parsed == 42);
std::cout << "PASS: ParseSizeT permits surrounding whitespace" << std::endl;

test::require_false(args::ParseSizeT("+42", parsed));
test::require_false(args::ParseSizeT("-1", parsed));
test::require_false(args::ParseSizeT("4 2", parsed));
std::cout << "PASS: ParseSizeT rejects signed and embedded-whitespace input" << std::endl;

std::ostringstream overflow;
overflow << std::numeric_limits<size_t>::max() << "0";
test::require_false(args::ParseSizeT(overflow.str(), parsed));
std::cout << "PASS: ParseSizeT detects overflow" << std::endl;
}

// Main test runner
int main()
{
Expand All @@ -310,9 +286,6 @@ int main()
TestWrapBoundaryConditions();
TestWrapLargeStringInput();

std::cout << "\n=== ParseSizeT Tests ===" << std::endl;
TestParseSizeT();

std::cout << "\n=== All Tests Passed ===" << std::endl;
return 0;
}
Loading