diff options
| -rw-r--r-- | nihil/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | nihil/errc.cc | 49 | ||||
| -rw-r--r-- | nihil/errc.ccm | 34 | ||||
| -rw-r--r-- | nihil/nihil.ccm | 2 | ||||
| -rw-r--r-- | nihil/parse_size.ccm | 105 | ||||
| -rw-r--r-- | nihil/tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | nihil/tests/parse_size.cc | 167 |
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); + } +} |
