Skip to content

stoul/stoull accept negative strings for unsigned types (follow-up to #206) #217

Description

@MarkLee131

Description:

Follow-up to #206 (silent truncation of narrow types). The range check added in #210 covers signed char, unsigned char, short, and unsigned short, but the wider unsigned types (unsigned, unsigned long, unsigned long long) still accept negative string inputs silently.

std::stoul("-1") returns ULONG_MAX per the C++ standard (it delegates to strtoul, which wraps negatives). So GetCell<unsigned> on a cell containing "-1" returns 4294967295 instead of throwing. This is inconsistent with the narrow-type behavior where GetCell<unsigned char> on "-1" correctly throws std::out_of_range after #210.

How to reproduce it:

#include <rapidcsv.h>
#include <sstream>
#include <iostream>
#include <climits>

int main()
{
  std::istringstream ss("val\n-1\n-100\n");
  rapidcsv::Document doc(ss);

  unsigned u = doc.GetCell<unsigned>(0, 0);
  std::cout << "-1 as unsigned = " << u << std::endl;
  // Output: -1 as unsigned = 4294967295 (UINT_MAX, no exception!)

  unsigned long ul = doc.GetCell<unsigned long>(0, 0);
  std::cout << "-1 as unsigned long = " << ul << std::endl;
  // Output: -1 as unsigned long = 18446744073709551615 (ULONG_MAX)

  unsigned long long ull = doc.GetCell<unsigned long long>(0, 0);
  std::cout << "-1 as unsigned long long = " << ull << std::endl;
  // Output: -1 as unsigned long long = 18446744073709551615 (ULLONG_MAX)

  // Compare: narrow types correctly throw after #210
  try
  {
    unsigned char uc = doc.GetCell<unsigned char>(0, 0);
    std::cout << "unsigned char didn't throw?! " << static_cast<int>(uc) << std::endl;
  }
  catch (const std::out_of_range& e)
  {
    std::cout << "unsigned char correctly threw: " << e.what() << std::endl;
  }
}

Environment:

  • Version: 8.97
  • OS / distro: macOS Sonoma / Ubuntu 24.04 (header-only, platform-independent)

Proposed fix:

Check if the input string starts with - (after skipping whitespace) in the stoul and stoull branches (lines 216-230), and throw std::out_of_range if so. This matches what the narrow-type path already does via std::numeric_limits bounds checking.

Something like:

// After stoul/stoull call, before return:
const auto firstNonSpace = pStr.find_first_not_of(" \t");
if (firstNonSpace != std::string::npos && pStr[firstNonSpace] == '-')
{
  throw std::out_of_range("conversion: negative value for unsigned type");
}

Happy to send a PR if you'd like.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions