]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
Move parse_signed and parse_unsigned to util
authorJoel Rosdahl <joel@rosdahl.net>
Tue, 13 Jul 2021 11:04:49 +0000 (13:04 +0200)
committerJoel Rosdahl <joel@rosdahl.net>
Mon, 19 Jul 2021 06:43:52 +0000 (08:43 +0200)
src/Config.cpp
src/Util.cpp
src/Util.hpp
src/ccache.cpp
src/storage/secondary/HttpStorage.cpp
src/storage/secondary/RedisStorage.cpp
src/util/string_utils.cpp
src/util/string_utils.hpp
unittest/test_Util.cpp
unittest/test_util_string_utils.cpp

index 110c04995114af34ef2ef4efc790f799460620dd..931d247600c04b0af5a203c3eff8c52437ea6b24 100644 (file)
@@ -28,6 +28,7 @@
 #include "fmtmacros.hpp"
 
 #include <core/wincompat.hpp>
+#include <util/expected_utils.hpp>
 #include <util/path_utils.hpp>
 #include <util/string_utils.hpp>
 
@@ -859,11 +860,10 @@ Config::set_item(const std::string& key,
     m_compression = parse_bool(value, env_var_key, negate);
     break;
 
-  case ConfigItem::compression_level: {
-    m_compression_level =
-      Util::parse_signed(value, INT8_MIN, INT8_MAX, "compression_level");
+  case ConfigItem::compression_level:
+    m_compression_level = util::value_or_throw<Error>(
+      util::parse_signed(value, INT8_MIN, INT8_MAX, "compression_level"));
     break;
-  }
 
   case ConfigItem::cpp_extension:
     m_cpp_extension = value;
@@ -930,7 +930,8 @@ Config::set_item(const std::string& key,
     break;
 
   case ConfigItem::max_files:
-    m_max_files = Util::parse_unsigned(value, nullopt, nullopt, "max_files");
+    m_max_files = util::value_or_throw<Error>(
+      util::parse_unsigned(value, nullopt, nullopt, "max_files"));
     break;
 
   case ConfigItem::max_size:
index 36a6bf4e3e4d80b1312a9c3d13423523b2b5e056..cce67c8b0fcdff441ff50c1cd765a7e4befe33f6 100644 (file)
@@ -1027,36 +1027,13 @@ parse_duration(const std::string& duration)
                 duration);
   }
 
-  return factor * parse_unsigned(duration.substr(0, duration.length() - 1));
-}
-
-int64_t
-parse_signed(const std::string& value,
-             optional<int64_t> min_value,
-             optional<int64_t> max_value,
-             string_view description)
-{
-  std::string stripped_value = util::strip_whitespace(value);
-
-  size_t end = 0;
-  long long result = 0;
-  bool failed = false;
-  try {
-    // Note: sizeof(long long) is guaranteed to be >= sizeof(int64_t)
-    result = std::stoll(stripped_value, &end, 10);
-  } catch (std::exception&) {
-    failed = true;
-  }
-  if (failed || end != stripped_value.size()) {
-    throw Error("invalid integer: \"{}\"", stripped_value);
-  }
-
-  int64_t min = min_value ? *min_value : INT64_MIN;
-  int64_t max = max_value ? *max_value : INT64_MAX;
-  if (result < min || result > max) {
-    throw Error("{} must be between {} and {}", description, min, max);
+  const auto value =
+    util::parse_unsigned(duration.substr(0, duration.length() - 1));
+  if (value) {
+    return factor * *value;
+  } else {
+    throw Error(value.error());
   }
-  return result;
 }
 
 uint64_t
@@ -1100,43 +1077,6 @@ parse_size(const std::string& value)
   return static_cast<uint64_t>(result);
 }
 
-uint64_t
-parse_unsigned(const std::string& value,
-               optional<uint64_t> min_value,
-               optional<uint64_t> max_value,
-               string_view description,
-               int base)
-{
-  std::string stripped_value = util::strip_whitespace(value);
-
-  size_t end = 0;
-  unsigned long long result = 0;
-  bool failed = false;
-  if (util::starts_with(stripped_value, "-")) {
-    failed = true;
-  } else {
-    try {
-      // Note: sizeof(unsigned long long) is guaranteed to be >=
-      // sizeof(uint64_t)
-      result = std::stoull(stripped_value, &end, base);
-    } catch (std::exception&) {
-      failed = true;
-    }
-  }
-  if (failed || end != stripped_value.size()) {
-    const auto base_info = base == 8 ? "octal " : "";
-    throw Error(
-      "invalid unsigned {}integer: \"{}\"", base_info, stripped_value);
-  }
-
-  uint64_t min = min_value ? *min_value : 0;
-  uint64_t max = max_value ? *max_value : UINT64_MAX;
-  if (result < min || result > max) {
-    throw Error("{} must be between {} and {}", description, min, max);
-  }
-  return result;
-}
-
 bool
 read_fd(int fd, DataReceiver data_receiver)
 {
index 853891d8d3f68a63871b56208baf8411d3919a9d..f41707bf184cb8e0caa35abd4584d78a76448d68 100644 (file)
@@ -315,34 +315,11 @@ std::string normalize_absolute_path(nonstd::string_view path);
 // into seconds. Throws `Error` on error.
 uint64_t parse_duration(const std::string& duration);
 
-// Parse a string into a signed integer.
-//
-// Throws `Error` if `value` cannot be parsed as an int64_t or if the value
-// falls out of the range [`min_value`, `max_value`]. `min_value` and
-// `max_value` default to min and max values of int64_t. `description` is
-// included in the error message for range violations.
-int64_t parse_signed(const std::string& value,
-                     nonstd::optional<int64_t> min_value = nonstd::nullopt,
-                     nonstd::optional<int64_t> max_value = nonstd::nullopt,
-                     nonstd::string_view description = "integer");
-
 // Parse a "size value", i.e. a string that can end in k, M, G, T (10-based
 // suffixes) or Ki, Mi, Gi, Ti (2-based suffixes). For backward compatibility, K
 // is also recognized as a synonym of k. Throws `Error` on parse error.
 uint64_t parse_size(const std::string& value);
 
-// Parse a string into an unsigned integer.
-//
-// Throws `Error` if `value` cannot be parsed as an uint64_t with base `base`,
-// or if the value falls out of the range [`min_value`, `max_value`].
-// `min_value` and `max_value` default to min and max values of uint64_t.
-// `description` is included in the error message for range violations.
-uint64_t parse_unsigned(const std::string& value,
-                        nonstd::optional<uint64_t> min_value = nonstd::nullopt,
-                        nonstd::optional<uint64_t> max_value = nonstd::nullopt,
-                        nonstd::string_view description = "integer",
-                        int base = 10);
-
 // Read data from `fd` until end of file and call `data_receiver` with the read
 // data. Returns whether reading was successful, i.e. whether the read(2) call
 // did not return -1.
index c9f9253b81fd6382fe0e6e139ce8e07ddd0bc4ba..7b942ce8f6ea1f67571859a00ef6286a1c49e064 100644 (file)
@@ -57,6 +57,7 @@
 
 #include <core/types.hpp>
 #include <core/wincompat.hpp>
+#include <util/expected_utils.hpp>
 #include <util/path_utils.hpp>
 #include <util/string_utils.hpp>
 
@@ -2397,7 +2398,7 @@ handle_main_options(int argc, const char* const* argv)
       break;
 
     case 'F': { // --max-files
-      auto files = Util::parse_unsigned(arg);
+      auto files = util::value_or_throw<Error>(util::parse_unsigned(arg));
       Config::set_value_in_file(
         ctx.config.primary_config_path(), "max_files", arg);
       if (files == 0) {
@@ -2482,8 +2483,8 @@ handle_main_options(int argc, const char* const* argv)
       if (arg == "uncompressed") {
         wanted_level = nullopt;
       } else {
-        wanted_level =
-          Util::parse_signed(arg, INT8_MIN, INT8_MAX, "compression level");
+        wanted_level = util::value_or_throw<Error>(
+          util::parse_signed(arg, INT8_MIN, INT8_MAX, "compression level"));
       }
 
       ProgressBar progress_bar("Recompressing...");
index a9cb39bc3104a43c29357cf207fb24a1e18a2966..c5c4a6220749bff2bd9e330ce5d09d023b29070c 100644 (file)
 
 #include <Digest.hpp>
 #include <Logging.hpp>
-#include <Util.hpp>
 #include <ccache.hpp>
 #include <exceptions.hpp>
 #include <fmtmacros.hpp>
+#include <util/expected_utils.hpp>
 #include <util/string_utils.hpp>
 
 #include <third_party/httplib.h>
@@ -142,8 +142,8 @@ parse_timeout_attribute(const AttributeMap& attributes,
   if (it == attributes.end()) {
     return default_value;
   } else {
-    auto timeout_in_ms =
-      Util::parse_unsigned(it->second, 1, 1000 * 3600, "timeout");
+    const auto timeout_in_ms = util::value_or_throw<Error>(
+      util::parse_unsigned(it->second, 1, 1000 * 3600, "timeout"));
     return std::chrono::milliseconds{timeout_in_ms};
   }
 }
index a05dc1b4609f747fd67b24740c172a202184fec8..c181817f2ae49c5bdadba5ddd23d135969515843 100644 (file)
@@ -21,6 +21,7 @@
 #include <Digest.hpp>
 #include <Logging.hpp>
 #include <fmtmacros.hpp>
+#include <util/expected_utils.hpp>
 #include <util/string_utils.hpp>
 
 #include <hiredis/hiredis.h>
@@ -65,7 +66,8 @@ parse_timeout_attribute(const AttributeMap& attributes,
   if (it == attributes.end()) {
     return default_value;
   } else {
-    return Util::parse_unsigned(it->second, 1, 1000 * 3600, "timeout");
+    return util::value_or_throw<Error>(
+      util::parse_unsigned(it->second, 1, 1000 * 3600, "timeout"));
   }
 }
 
@@ -129,17 +131,18 @@ RedisStorage::connect()
 
   ASSERT(m_url.scheme() == "redis");
   const std::string host = m_url.host().empty() ? "localhost" : m_url.host();
-  const uint32_t port =
-    m_url.port().empty() ? DEFAULT_PORT
-                         : Util::parse_unsigned(m_url.port(), 1, 65535, "port");
+  const uint32_t port = m_url.port().empty()
+                          ? DEFAULT_PORT
+                          : util::value_or_throw<::Error>(util::parse_unsigned(
+                            m_url.port(), 1, 65535, "port"));
   ASSERT(m_url.path().empty() || m_url.path()[0] == '/');
   const uint32_t db_number =
-    m_url.path().empty()
-      ? 0
-      : Util::parse_unsigned(m_url.path().substr(1),
-                             0,
-                             std::numeric_limits<uint32_t>::max(),
-                             "db number");
+    m_url.path().empty() ? 0
+                         : util::value_or_throw<::Error>(util::parse_unsigned(
+                           m_url.path().substr(1),
+                           0,
+                           std::numeric_limits<uint32_t>::max(),
+                           "db number"));
 
   const auto connect_timeout = milliseconds_to_timeval(m_connect_timeout);
 
index 0e439779674adc7495110a9bb2aae6080239de11..237c61a011fde47bbdcb2950e82509cace0616e6 100644 (file)
 #include "string_utils.hpp"
 
 #include <FormatNonstdStringView.hpp>
-#include <Util.hpp>
 #include <fmtmacros.hpp>
 
 #include <cctype>
 
 namespace util {
 
+nonstd::expected<int64_t, std::string>
+parse_signed(const std::string& value,
+             const nonstd::optional<int64_t> min_value,
+             const nonstd::optional<int64_t> max_value,
+             const nonstd::string_view description)
+{
+  const std::string stripped_value = strip_whitespace(value);
+
+  size_t end = 0;
+  long long result = 0;
+  bool failed = false;
+  try {
+    // Note: sizeof(long long) is guaranteed to be >= sizeof(int64_t)
+    result = std::stoll(stripped_value, &end, 10);
+  } catch (std::exception&) {
+    failed = true;
+  }
+  if (failed || end != stripped_value.size()) {
+    return nonstd::make_unexpected(
+      FMT("invalid integer: \"{}\"", stripped_value));
+  }
+
+  const int64_t min = min_value ? *min_value : INT64_MIN;
+  const int64_t max = max_value ? *max_value : INT64_MAX;
+  if (result < min || result > max) {
+    return nonstd::make_unexpected(
+      FMT("{} must be between {} and {}", description, min, max));
+  } else {
+    return result;
+  }
+}
+
 nonstd::expected<mode_t, std::string>
 parse_umask(const std::string& value)
 {
-  try {
-    return Util::parse_unsigned(value, 0, 0777, "umask", 8);
-  } catch (const Error& e) {
-    return nonstd::make_unexpected(e.what());
+  return util::parse_unsigned(value, 0, 0777, "umask", 8);
+}
+
+nonstd::expected<uint64_t, std::string>
+parse_unsigned(const std::string& value,
+               const nonstd::optional<uint64_t> min_value,
+               const nonstd::optional<uint64_t> max_value,
+               const nonstd::string_view description,
+               const int base)
+{
+  const std::string stripped_value = strip_whitespace(value);
+
+  size_t end = 0;
+  unsigned long long result = 0;
+  bool failed = false;
+  if (starts_with(stripped_value, "-")) {
+    failed = true;
+  } else {
+    try {
+      // Note: sizeof(unsigned long long) is guaranteed to be >=
+      // sizeof(uint64_t)
+      result = std::stoull(stripped_value, &end, base);
+    } catch (std::exception&) {
+      failed = true;
+    }
+  }
+  if (failed || end != stripped_value.size()) {
+    const auto base_info = base == 8 ? "octal " : "";
+    return nonstd::make_unexpected(
+      FMT("invalid unsigned {}integer: \"{}\"", base_info, stripped_value));
+  }
+
+  const uint64_t min = min_value ? *min_value : 0;
+  const uint64_t max = max_value ? *max_value : UINT64_MAX;
+  if (result < min || result > max) {
+    return nonstd::make_unexpected(
+      FMT("{} must be between {} and {}", description, min, max));
+  } else {
+    return result;
   }
 }
 
index e6aa19b209cbda13d79008b784100661fef1931c..ac27704e1268afdfb5fbea329cd759c17e3516c8 100644 (file)
@@ -37,9 +37,34 @@ ends_with(const nonstd::string_view string, const nonstd::string_view suffix)
   return string.ends_with(suffix);
 }
 
+// Parse a string into a signed integer.
+//
+// Return an error string if `value` cannot be parsed as an int64_t or if the
+// value falls out of the range [`min_value`, `max_value`]. `min_value` and
+// `max_value` default to min and max values of int64_t. `description` is
+// included in the error message for range violations.
+nonstd::expected<int64_t, std::string>
+parse_signed(const std::string& value,
+             nonstd::optional<int64_t> min_value = nonstd::nullopt,
+             nonstd::optional<int64_t> max_value = nonstd::nullopt,
+             nonstd::string_view description = "integer");
+
 // Parse `value` (an octal integer).
 nonstd::expected<mode_t, std::string> parse_umask(const std::string& value);
 
+// Parse a string into an unsigned integer.
+//
+// Returns an error string if `value` cannot be parsed as an uint64_t with base
+// `base`, or if the value falls out of the range [`min_value`, `max_value`].
+// `min_value` and `max_value` default to min and max values of uint64_t.
+// `description` is included in the error message for range violations.
+nonstd::expected<uint64_t, std::string>
+parse_unsigned(const std::string& value,
+               nonstd::optional<uint64_t> min_value = nonstd::nullopt,
+               nonstd::optional<uint64_t> max_value = nonstd::nullopt,
+               nonstd::string_view description = "integer",
+               int base = 10);
+
 // Percent-decode[1] `string`.
 //
 // [1]: https://en.wikipedia.org/wiki/Percent-encoding
index f34bcbb292d5fba7bf9d45537aa74a0ab69b5ddb..32e14687aa40d7eb5066c607462419cc1f5039b7 100644 (file)
@@ -671,46 +671,6 @@ TEST_CASE("Util::parse_duration")
     "invalid suffix (supported: d (day) and s (second)): \"2\"");
 }
 
-TEST_CASE("Util::parse_signed")
-{
-  CHECK(Util::parse_signed("0") == 0);
-  CHECK(Util::parse_signed("2") == 2);
-  CHECK(Util::parse_signed("-17") == -17);
-  CHECK(Util::parse_signed("42") == 42);
-  CHECK(Util::parse_signed("0666") == 666);
-  CHECK(Util::parse_signed(" 777 ") == 777);
-
-  CHECK_THROWS_WITH(Util::parse_signed(""), "invalid integer: \"\"");
-  CHECK_THROWS_WITH(Util::parse_signed("x"), "invalid integer: \"x\"");
-  CHECK_THROWS_WITH(Util::parse_signed("0x"), "invalid integer: \"0x\"");
-  CHECK_THROWS_WITH(Util::parse_signed("0x4"), "invalid integer: \"0x4\"");
-
-  // Custom description not used for invalid value.
-  CHECK_THROWS_WITH(Util::parse_signed("apple", nullopt, nullopt, "banana"),
-                    "invalid integer: \"apple\"");
-
-  // Boundary values.
-  CHECK_THROWS_WITH(Util::parse_signed("-9223372036854775809"),
-                    "invalid integer: \"-9223372036854775809\"");
-  CHECK(Util::parse_signed("-9223372036854775808") == INT64_MIN);
-  CHECK(Util::parse_signed("9223372036854775807") == INT64_MAX);
-  CHECK_THROWS_WITH(Util::parse_signed("9223372036854775808"),
-                    "invalid integer: \"9223372036854775808\"");
-
-  // Min and max values.
-  CHECK_THROWS_WITH(Util::parse_signed("-2", -1, 1),
-                    "integer must be between -1 and 1");
-  CHECK(Util::parse_signed("-1", -1, 1) == -1);
-  CHECK(Util::parse_signed("0", -1, 1) == 0);
-  CHECK(Util::parse_signed("1", -1, 1) == 1);
-  CHECK_THROWS_WITH(Util::parse_signed("2", -1, 1),
-                    "integer must be between -1 and 1");
-
-  // Custom description used for boundary violation.
-  CHECK_THROWS_WITH(Util::parse_signed("0", 1, 2, "banana"),
-                    "banana must be between 1 and 2");
-}
-
 TEST_CASE("Util::parse_size")
 {
   CHECK(Util::parse_size("0") == 0);
@@ -735,40 +695,6 @@ TEST_CASE("Util::parse_size")
   CHECK_THROWS_WITH(Util::parse_size("10x"), "invalid size: \"10x\"");
 }
 
-TEST_CASE("Util::parse_unsigned")
-{
-  CHECK(Util::parse_unsigned("0") == 0);
-  CHECK(Util::parse_unsigned("2") == 2);
-  CHECK(Util::parse_unsigned("42") == 42);
-  CHECK(Util::parse_unsigned("0666") == 666);
-  CHECK(Util::parse_unsigned(" 777 ") == 777);
-
-  CHECK_THROWS_WITH(Util::parse_unsigned(""), "invalid unsigned integer: \"\"");
-  CHECK_THROWS_WITH(Util::parse_unsigned("x"),
-                    "invalid unsigned integer: \"x\"");
-  CHECK_THROWS_WITH(Util::parse_unsigned("0x"),
-                    "invalid unsigned integer: \"0x\"");
-  CHECK_THROWS_WITH(Util::parse_unsigned("0x4"),
-                    "invalid unsigned integer: \"0x4\"");
-
-  // Custom description not used for invalid value.
-  CHECK_THROWS_WITH(Util::parse_unsigned("apple", nullopt, nullopt, "banana"),
-                    "invalid unsigned integer: \"apple\"");
-
-  // Boundary values.
-  CHECK_THROWS_WITH(Util::parse_unsigned("-1"),
-                    "invalid unsigned integer: \"-1\"");
-  CHECK(Util::parse_unsigned("0") == 0);
-  CHECK(Util::parse_unsigned("18446744073709551615") == UINT64_MAX);
-  CHECK_THROWS_WITH(Util::parse_unsigned("18446744073709551616"),
-                    "invalid unsigned integer: \"18446744073709551616\"");
-
-  // Base
-  CHECK(Util::parse_unsigned("0666", nullopt, nullopt, "", 8) == 0666);
-  CHECK(Util::parse_unsigned("0666", nullopt, nullopt, "", 10) == 666);
-  CHECK(Util::parse_unsigned("0666", nullopt, nullopt, "", 16) == 0x666);
-}
-
 TEST_CASE("Util::read_file and Util::write_file")
 {
   TestContext test_context;
index f4bef5b4990da5f8b8a1b442152c18c5ff3059e9..25d195656f375f0e8c67c7c6783329b81c3b42cb 100644 (file)
@@ -48,6 +48,47 @@ TEST_CASE("util::ends_with")
   CHECK_FALSE(util::ends_with("x", "xy"));
 }
 
+TEST_CASE("util::parse_signed")
+{
+  CHECK(*util::parse_signed("0") == 0);
+  CHECK(*util::parse_signed("2") == 2);
+  CHECK(*util::parse_signed("-17") == -17);
+  CHECK(*util::parse_signed("42") == 42);
+  CHECK(*util::parse_signed("0666") == 666);
+  CHECK(*util::parse_signed(" 777 ") == 777);
+
+  CHECK(util::parse_signed("").error() == "invalid integer: \"\"");
+  CHECK(util::parse_signed("x").error() == "invalid integer: \"x\"");
+  CHECK(util::parse_signed("0x").error() == "invalid integer: \"0x\"");
+  CHECK(util::parse_signed("0x4").error() == "invalid integer: \"0x4\"");
+
+  // Custom description not used for invalid value.
+  CHECK(util::parse_signed("apple", nonstd::nullopt, nonstd::nullopt, "banana")
+          .error()
+        == "invalid integer: \"apple\"");
+
+  // Boundary values.
+  CHECK(util::parse_signed("-9223372036854775809").error()
+        == "invalid integer: \"-9223372036854775809\"");
+  CHECK(*util::parse_signed("-9223372036854775808") == INT64_MIN);
+  CHECK(*util::parse_signed("9223372036854775807") == INT64_MAX);
+  CHECK(util::parse_signed("9223372036854775808").error()
+        == "invalid integer: \"9223372036854775808\"");
+
+  // Min and max values.
+  CHECK(util::parse_signed("-2", -1, 1).error()
+        == "integer must be between -1 and 1");
+  CHECK(*util::parse_signed("-1", -1, 1) == -1);
+  CHECK(*util::parse_signed("0", -1, 1) == 0);
+  CHECK(*util::parse_signed("1", -1, 1) == 1);
+  CHECK(util::parse_signed("2", -1, 1).error()
+        == "integer must be between -1 and 1");
+
+  // Custom description used for boundary violation.
+  CHECK(util::parse_signed("0", 1, 2, "banana").error()
+        == "banana must be between 1 and 2");
+}
+
 TEST_CASE("util::parse_umask")
 {
   CHECK(util::parse_umask("1") == 01u);
@@ -63,6 +104,44 @@ TEST_CASE("util::parse_umask")
         == "invalid unsigned octal integer: \"088\"");
 }
 
+TEST_CASE("util::parse_unsigned")
+{
+  CHECK(*util::parse_unsigned("0") == 0);
+  CHECK(*util::parse_unsigned("2") == 2);
+  CHECK(*util::parse_unsigned("42") == 42);
+  CHECK(*util::parse_unsigned("0666") == 666);
+  CHECK(*util::parse_unsigned(" 777 ") == 777);
+
+  CHECK(util::parse_unsigned("").error() == "invalid unsigned integer: \"\"");
+  CHECK(util::parse_unsigned("x").error() == "invalid unsigned integer: \"x\"");
+  CHECK(util::parse_unsigned("0x").error()
+        == "invalid unsigned integer: \"0x\"");
+  CHECK(util::parse_unsigned("0x4").error()
+        == "invalid unsigned integer: \"0x4\"");
+
+  // Custom description not used for invalid value.
+  CHECK(
+    util::parse_unsigned("apple", nonstd::nullopt, nonstd::nullopt, "banana")
+      .error()
+    == "invalid unsigned integer: \"apple\"");
+
+  // Boundary values.
+  CHECK(util::parse_unsigned("-1").error()
+        == "invalid unsigned integer: \"-1\"");
+  CHECK(*util::parse_unsigned("0") == 0);
+  CHECK(*util::parse_unsigned("18446744073709551615") == UINT64_MAX);
+  CHECK(util::parse_unsigned("18446744073709551616").error()
+        == "invalid unsigned integer: \"18446744073709551616\"");
+
+  // Base
+  CHECK(*util::parse_unsigned("0666", nonstd::nullopt, nonstd::nullopt, "", 8)
+        == 0666);
+  CHECK(*util::parse_unsigned("0666", nonstd::nullopt, nonstd::nullopt, "", 10)
+        == 666);
+  CHECK(*util::parse_unsigned("0666", nonstd::nullopt, nonstd::nullopt, "", 16)
+        == 0x666);
+}
+
 TEST_CASE("util::percent_decode")
 {
   CHECK(util::percent_decode("") == "");