aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-27 14:39:08 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-27 14:39:24 +0100
commitf565a14f584f1342dd919361dc2f719c3ef45306 (patch)
treec71e08a4d6349a4543f3e1c8227a54f63455e97c
parentf7c62a0abad862149ab1a70e8610476a331f8ed5 (diff)
downloadnihil-f565a14f584f1342dd919361dc2f719c3ef45306.tar.gz
nihil-f565a14f584f1342dd919361dc2f719c3ef45306.tar.bz2
nihil: add parse_size() and nihil::errc
-rw-r--r--nihil/CMakeLists.txt3
-rw-r--r--nihil/errc.cc49
-rw-r--r--nihil/errc.ccm34
-rw-r--r--nihil/nihil.ccm2
-rw-r--r--nihil/parse_size.ccm105
-rw-r--r--nihil/tests/CMakeLists.txt1
-rw-r--r--nihil/tests/parse_size.cc167
7 files changed, 361 insertions, 0 deletions
diff --git a/nihil/CMakeLists.txt b/nihil/CMakeLists.txt
index cb0cd21..49955c8 100644
--- a/nihil/CMakeLists.txt
+++ b/nihil/CMakeLists.txt
@@ -8,6 +8,7 @@ target_sources(nihil
command_map.ccm
ctype.ccm
ensure_dir.ccm
+ errc.ccm
error.ccm
exec.ccm
fd.ccm
@@ -20,6 +21,7 @@ target_sources(nihil
monad.ccm
next_word.ccm
open_file.ccm
+ parse_size.ccm
process.ccm
read_file.ccm
rename_file.ccm
@@ -34,6 +36,7 @@ target_sources(nihil
argv.cc
command_map.cc
ensure_dir.cc
+ errc.cc
error.cc
exec.cc
fd.cc
diff --git a/nihil/errc.cc b/nihil/errc.cc
new file mode 100644
index 0000000..1d4e6fa
--- /dev/null
+++ b/nihil/errc.cc
@@ -0,0 +1,49 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <string>
+#include <system_error>
+
+module nihil;
+
+namespace nihil {
+
+struct nihil_error_category final : std::error_category {
+ auto name() const noexcept -> char const * override;
+ auto message(int err) const -> std::string override;
+};
+
+auto nihil_category() noexcept -> std::error_category &
+{
+ static auto category = nihil_error_category();
+ return category;
+}
+
+auto make_error_condition(errc ec) -> std::error_condition
+{
+ return {static_cast<int>(ec), nihil_category()};
+}
+
+auto nihil_error_category::name() const noexcept -> char const *
+{
+ return "nihil";
+}
+
+auto nihil_error_category::message(int err) const -> std::string
+{
+ switch (static_cast<errc>(err)) {
+ case errc::no_error:
+ return "No error";
+ case errc::empty_string:
+ return "Empty string is not permitted";
+ case errc::invalid_unit:
+ return "Invalid unit specifier";
+ default:
+ return "Undefined error";
+ }
+}
+
+} // namespace nihil
diff --git a/nihil/errc.ccm b/nihil/errc.ccm
new file mode 100644
index 0000000..eb0389e
--- /dev/null
+++ b/nihil/errc.ccm
@@ -0,0 +1,34 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <string>
+#include <system_error>
+
+export module nihil:errc;
+
+namespace nihil {
+
+export enum struct errc {
+ no_error = 0,
+
+ // Empty string is not allowed.
+ empty_string,
+
+ // Invalid unit, e.g. in parse_size()
+ invalid_unit,
+};
+
+export auto nihil_category() noexcept -> std::error_category &;
+export auto make_error_condition(errc ec) -> std::error_condition;
+
+} // namespace nihil
+
+namespace std {
+
+export template<>
+struct is_error_condition_enum<nihil::errc> : true_type {};
+
+} // namespace std
diff --git a/nihil/nihil.ccm b/nihil/nihil.ccm
index 61e096c..7d2ac7f 100644
--- a/nihil/nihil.ccm
+++ b/nihil/nihil.ccm
@@ -10,6 +10,7 @@ export import :argv;
export import :command_map;
export import :ctype;
export import :ensure_dir;
+export import :errc;
export import :error;
export import :exec;
export import :fd;
@@ -22,6 +23,7 @@ export import :match;
export import :monad;
export import :next_word;
export import :open_file;
+export import :parse_size;
export import :process;
export import :read_file;
export import :rename_file;
diff --git a/nihil/parse_size.ccm b/nihil/parse_size.ccm
new file mode 100644
index 0000000..a449431
--- /dev/null
+++ b/nihil/parse_size.ccm
@@ -0,0 +1,105 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <algorithm>
+#include <coroutine>
+#include <cstdint>
+#include <expected>
+#include <ranges>
+#include <string>
+#include <system_error>
+#include <utility>
+
+export module nihil:parse_size;
+
+import :ctype;
+import :errc;
+import :error;
+
+namespace nihil {
+
+template<typename Char>
+auto get_multiplier(Char c) -> std::expected<std::uint64_t, error>
+{
+ auto ret = std::uint64_t{1};
+
+ switch (c) {
+ case 'p': case 'P': ret *= 1024;
+ case 't': case 'T': ret *= 1024;
+ case 'g': case 'G': ret *= 1024;
+ case 'm': case 'M': ret *= 1024;
+ case 'k': case 'K': ret *= 1024;
+ return ret;
+
+ default:
+ return std::unexpected(error(errc::invalid_unit));
+ }
+}
+
+/*
+ * Parse a string containing a human-formatted size, such as "1024"
+ * or "4g". Parsing is always done in the "C" locale and does not
+ * recognise thousands separators or negative numbers.
+ */
+export template<typename T, typename Char> [[nodiscard]]
+auto parse_size(std::basic_string_view<Char> str)
+ -> std::expected<T, error>
+{
+ // Extract the numeric part of the string.
+ auto it = std::ranges::find_if_not(str, is_c_digit);
+ auto num_str = std::basic_string_view<Char>(
+ std::ranges::begin(str), it);
+
+ if (num_str.empty())
+ co_return std::unexpected(error(errc::empty_string));
+
+ auto ret = T{0};
+
+ for (auto c : num_str) {
+ if (ret > (std::numeric_limits<T>::max() / 10))
+ co_return std::unexpected(error(
+ std::errc::result_out_of_range));
+ ret *= 10;
+
+ auto digit = static_cast<T>(c - '0');
+ if ((std::numeric_limits<T>::max() - digit) < ret)
+ co_return std::unexpected(error(
+ std::errc::result_out_of_range));
+ ret += digit;
+ }
+
+ if (it == str.end())
+ // No multiplier.
+ co_return ret;
+
+ auto mchar = *it++;
+
+ if (it != str.end())
+ // Multiplier is more than one character.
+ co_return std::unexpected(error(errc::invalid_unit));
+
+ auto mult = co_await get_multiplier(mchar);
+
+ if (std::cmp_greater(ret, std::numeric_limits<T>::max() / mult))
+ co_return std::unexpected(error(
+ std::errc::result_out_of_range));
+
+ co_return ret * mult;
+}
+
+export template<typename T>
+[[nodiscard]] inline auto parse_size(char const *s)
+{
+ return parse_size<T>(std::string_view(s));
+}
+
+export template<typename T>
+[[nodiscard]] inline auto parse_size(wchar_t const *s)
+{
+ return parse_size<T>(std::wstring_view(s));
+}
+
+}
diff --git a/nihil/tests/CMakeLists.txt b/nihil/tests/CMakeLists.txt
index 1c4635f..a57d844 100644
--- a/nihil/tests/CMakeLists.txt
+++ b/nihil/tests/CMakeLists.txt
@@ -10,6 +10,7 @@ add_executable(nihil.test
guard.cc
monad.cc
next_word.cc
+ parse_size.cc
skipws.cc
spawn.cc
tabulate.cc
diff --git a/nihil/tests/parse_size.cc b/nihil/tests/parse_size.cc
new file mode 100644
index 0000000..fb8188d
--- /dev/null
+++ b/nihil/tests/parse_size.cc
@@ -0,0 +1,167 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <cstdint>
+#include <system_error>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("parse_size: empty value", "[nihil]")
+{
+ using namespace nihil;
+
+ auto n = parse_size<std::uint64_t>("");
+ REQUIRE(!n);
+ REQUIRE(n.error() == nihil::errc::empty_string);
+}
+
+TEST_CASE("parse_size: basic", "[nihil]")
+{
+ using namespace nihil;
+
+ SECTION("bare number") {
+ auto n = parse_size<std::uint64_t>("1024").value();
+ REQUIRE(n == 1024);
+ }
+
+ SECTION("max value, unsigned") {
+ auto n = parse_size<std::uint16_t>("65535").value();
+ REQUIRE(n == 65535);
+ }
+
+ SECTION("max value, signed") {
+ auto n = parse_size<std::uint16_t>("32767").value();
+ REQUIRE(n == 32767);
+ }
+
+ SECTION("overflow by 1, unsigned") {
+ auto n = parse_size<std::uint16_t>("65536");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION("overflow by 1, signed") {
+ auto n = parse_size<std::int16_t>("32768");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION("overflow by many, unsigned") {
+ auto n = parse_size<std::uint16_t>("100000");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION("overflow by many, signed") {
+ auto n = parse_size<std::int16_t>("100000");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+}
+
+TEST_CASE("parse_size: invalid multiplier", "[nihil]")
+{
+ using namespace nihil;
+
+ auto n = parse_size<std::uint64_t>("4z");
+ REQUIRE(!n);
+ REQUIRE(n.error() == nihil::errc::invalid_unit);
+
+ n = parse_size<std::uint64_t>("4kz");
+ REQUIRE(!n);
+ REQUIRE(n.error() == nihil::errc::invalid_unit);
+}
+
+TEST_CASE("parse_size: multipliers", "[nihil]")
+{
+ using namespace nihil;
+
+ auto sf = static_cast<std::uint64_t>(4);
+
+ SECTION("k") {
+ auto n = parse_size<std::uint64_t>("4k").value();
+ REQUIRE(n == sf * 1024);
+ }
+
+ SECTION("m") {
+ auto n = parse_size<std::uint64_t>("4m").value();
+ REQUIRE(n == sf * 1024 * 1024);
+ }
+
+ SECTION("g") {
+ auto n = parse_size<std::uint64_t>("4g").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024);
+ }
+
+ SECTION("t") {
+ auto n = parse_size<std::uint64_t>("4t").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024);
+ }
+
+ SECTION("p") {
+ auto n = parse_size<std::uint64_t>("4p").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024);
+ }
+}
+
+TEST_CASE("parse_size: multiplier overflow", "[nihil]")
+{
+ using namespace nihil;
+
+ SECTION("signed") {
+ auto n = parse_size<std::uint16_t>("64k");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION("unsigned") {
+ auto n = parse_size<std::int16_t>("32k");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+}
+
+TEST_CASE("parse_size: wide", "[nihil]")
+{
+ using namespace nihil;
+
+ SECTION("bare number") {
+ auto n = parse_size<std::uint64_t>(L"1024").value();
+ REQUIRE(n == 1024);
+ }
+}
+
+TEST_CASE("parse_size: wide multipliers", "[nihil]")
+{
+ using namespace nihil;
+
+ auto sf = static_cast<std::uint64_t>(4);
+
+ SECTION("k") {
+ auto n = parse_size<std::uint64_t>(L"4k").value();
+ REQUIRE(n == sf * 1024);
+ }
+
+ SECTION("m") {
+ auto n = parse_size<std::uint64_t>(L"4m").value();
+ REQUIRE(n == sf * 1024 * 1024);
+ }
+
+ SECTION("g") {
+ auto n = parse_size<std::uint64_t>(L"4g").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024);
+ }
+
+ SECTION("t") {
+ auto n = parse_size<std::uint64_t>(L"4t").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024);
+ }
+
+ SECTION("p") {
+ auto n = parse_size<std::uint64_t>(L"4p").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024);
+ }
+}