From 5adeb648f74c1771164c0686d6e0fc584cf36d9e Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Wed, 2 Jul 2025 04:00:06 +0100 Subject: move everything from util to core --- nihil.core/CMakeLists.txt | 16 + nihil.core/capture_stream.ccm | 55 +++ nihil.core/capture_stream.test.cc | 44 ++ nihil.core/construct.ccm | 22 + nihil.core/ctype.ccm | 72 +++ nihil.core/ctype.test.cc | 376 ++++++++++++++++ nihil.core/error.ccm | 321 +++++++++++++ nihil.core/error.test.cc | 273 +++++++++++ nihil.core/flagset.ccm | 200 +++++++++ nihil.core/flagset.test.cc | 144 ++++++ nihil.core/guard.ccm | 41 ++ nihil.core/guard.test.cc | 16 + nihil.core/match.ccm | 18 + nihil.core/match.test.cc | 32 ++ nihil.core/monad.ccm | 282 ++++++++++++ nihil.core/monad.test.cc | 65 +++ nihil.core/next_word.ccm | 35 ++ nihil.core/next_word.test.cc | 65 +++ nihil.core/nihil.core.ccm | 15 + nihil.core/parse_size.ccm | 90 ++++ nihil.core/parse_size.test.cc | 165 +++++++ nihil.core/save_errno.ccm | 35 ++ nihil.core/skipws.ccm | 26 ++ nihil.core/skipws.test.cc | 48 ++ nihil.core/sys_error.ccm | 18 + nihil.core/tabulate.ccm | 298 ++++++++++++ nihil.core/tabulate.test.cc | 72 +++ nihil.core/uuid.ccm | 768 +++++++++++++++++++++++++++++++ nihil.core/uuid.test.cc | 923 ++++++++++++++++++++++++++++++++++++++ 29 files changed, 4535 insertions(+) create mode 100644 nihil.core/capture_stream.ccm create mode 100644 nihil.core/capture_stream.test.cc create mode 100644 nihil.core/construct.ccm create mode 100644 nihil.core/ctype.ccm create mode 100644 nihil.core/ctype.test.cc create mode 100644 nihil.core/error.ccm create mode 100644 nihil.core/error.test.cc create mode 100644 nihil.core/flagset.ccm create mode 100644 nihil.core/flagset.test.cc create mode 100644 nihil.core/guard.ccm create mode 100644 nihil.core/guard.test.cc create mode 100644 nihil.core/match.ccm create mode 100644 nihil.core/match.test.cc create mode 100644 nihil.core/monad.ccm create mode 100644 nihil.core/monad.test.cc create mode 100644 nihil.core/next_word.ccm create mode 100644 nihil.core/next_word.test.cc create mode 100644 nihil.core/parse_size.ccm create mode 100644 nihil.core/parse_size.test.cc create mode 100644 nihil.core/save_errno.ccm create mode 100644 nihil.core/skipws.ccm create mode 100644 nihil.core/skipws.test.cc create mode 100644 nihil.core/sys_error.ccm create mode 100644 nihil.core/tabulate.ccm create mode 100644 nihil.core/tabulate.test.cc create mode 100644 nihil.core/uuid.ccm create mode 100644 nihil.core/uuid.test.cc (limited to 'nihil.core') diff --git a/nihil.core/CMakeLists.txt b/nihil.core/CMakeLists.txt index 637b825..e9998a3 100644 --- a/nihil.core/CMakeLists.txt +++ b/nihil.core/CMakeLists.txt @@ -6,6 +6,22 @@ target_include_directories(nihil.core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_sources(nihil.core PUBLIC FILE_SET modules TYPE CXX_MODULES FILES nihil.core.ccm + + capture_stream.ccm + construct.ccm + ctype.ccm errc.ccm + error.ccm features.ccm + flagset.ccm + guard.ccm + match.ccm + monad.ccm + next_word.ccm + parse_size.ccm + save_errno.ccm + skipws.ccm + sys_error.ccm + tabulate.ccm + uuid.ccm ) diff --git a/nihil.core/capture_stream.ccm b/nihil.core/capture_stream.ccm new file mode 100644 index 0000000..5ab4f2d --- /dev/null +++ b/nihil.core/capture_stream.ccm @@ -0,0 +1,55 @@ +// This source code is released into the public domain. +export module nihil.core:capture_stream; + +import nihil.std; + +namespace nihil { + +// Capture output written to a stream and redirect it to an internal string +// buffer. Call .str() to get the data written. Call .release() to stop +// capturing (or simply delete the capture_stream object). +export template +struct capture_stream { + capture_stream(std::basic_ostream &stream) + : m_stream(&stream) + { + m_old_streambuf = m_stream->rdbuf(); + m_stream->rdbuf(m_buffer.rdbuf()); + } + + ~capture_stream() + { + if (m_old_streambuf == nullptr) + return; + m_stream->rdbuf(m_old_streambuf); + } + + /* + * Release this capture, returning the stream to its previous state. + */ + auto release(this capture_stream &self) -> void + { + if (self.m_old_streambuf == nullptr) + throw std::logic_error( + "release() called on empty capture_stream"); + + self.m_stream->rdbuf(self.m_old_streambuf); + self.m_old_streambuf = nullptr; + } + + /* + * Get the data which has been written to the stream. + */ + [[nodiscard]] auto str(this capture_stream const &self) + -> std::basic_string_view + { + return self.m_buffer.view(); + } + +private: + std::basic_ostringstream m_buffer; + std::basic_ostream *m_stream; + std::streambuf *m_old_streambuf; +}; + +} // namespace nihil diff --git a/nihil.core/capture_stream.test.cc b/nihil.core/capture_stream.test.cc new file mode 100644 index 0000000..4dd7db6 --- /dev/null +++ b/nihil.core/capture_stream.test.cc @@ -0,0 +1,44 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { +TEST_CASE("nihil.util: capture", "[nihil][nihil.util]") +{ + SECTION("std::cout with release()") { + auto cap = nihil::capture_stream(std::cout); + + std::cout << 1 << '+' << 1 << '=' << (1 + 1) << '\n'; + REQUIRE(cap.str() == "1+1=2\n"); + + cap.release(); + REQUIRE(cap.str() == "1+1=2\n"); + } + + SECTION("std::cout with dtor") { + auto cap = nihil::capture_stream(std::cout); + std::cout << 1 << '+' << 1 << '=' << (1 + 1) << '\n'; + REQUIRE(cap.str() == "1+1=2\n"); + } + + SECTION("std::cerr with release()") { + auto cap = nihil::capture_stream(std::cerr); + + std::cerr << 1 << '+' << 1 << '=' << (1 + 1) << '\n'; + REQUIRE(cap.str() == "1+1=2\n"); + + cap.release(); + REQUIRE(cap.str() == "1+1=2\n"); + } + + SECTION("std::cerr with dtor") { + auto cap = nihil::capture_stream(std::cerr); + std::cerr << 1 << '+' << 1 << '=' << (1 + 1) << '\n'; + REQUIRE(cap.str() == "1+1=2\n"); + } +} + +} // anonymous namespace diff --git a/nihil.core/construct.ccm b/nihil.core/construct.ccm new file mode 100644 index 0000000..5dd4422 --- /dev/null +++ b/nihil.core/construct.ccm @@ -0,0 +1,22 @@ +// This source code is released into the public domain. +export module nihil.core:construct; + +import nihil.std; + +namespace nihil { + +// A functor that constructs objects of an arbitrary type. +// Useful for std::views::transform. +template +struct construct_fn final +{ + [[nodiscard]] auto operator()(this construct_fn const &, auto &&...args) -> T + { + return T(std::forward(args)...); + } +}; + +export template +inline constexpr auto construct = construct_fn{}; + +} // namespace nihil diff --git a/nihil.core/ctype.ccm b/nihil.core/ctype.ccm new file mode 100644 index 0000000..365edc0 --- /dev/null +++ b/nihil.core/ctype.ccm @@ -0,0 +1,72 @@ +// This source code is released into the public domain. +export module nihil.core:ctype; + +import nihil.std; + +namespace nihil { + +/* + * ctype_is: wrap std::ctype::is() in a form suitable for use as an algorithm + * predicate, i.e., ctype_is(m) will return a functor object that takes any char + * type as an argument and returns bool. + * + * If the locale is not specified, the current global locale is used by default. + * + * ctype_is copies the locale, so passing a temporary is fine. + */ + +export struct ctype_is final +{ + explicit ctype_is(std::ctype_base::mask mask_, + std::locale const &locale_ = std::locale()) noexcept + : m_mask(mask_) + , m_locale(locale_) + { + } + + [[nodiscard]] auto operator()(this ctype_is const &self, std::integral auto c) + { + using ctype = std::ctype; + auto &facet = std::use_facet(self.m_locale); + return facet.is(self.m_mask, c); + } + +private: + std::ctype_base::mask m_mask; + std::locale m_locale; +}; + +// Predefined tests for the current global locale. + +export inline auto is_space = ctype_is(std::ctype_base::space); +export inline auto is_print = ctype_is(std::ctype_base::print); +export inline auto is_cntrl = ctype_is(std::ctype_base::cntrl); +export inline auto is_upper = ctype_is(std::ctype_base::upper); +export inline auto is_lower = ctype_is(std::ctype_base::lower); +export inline auto is_alpha = ctype_is(std::ctype_base::alpha); +export inline auto is_digit = ctype_is(std::ctype_base::digit); +export inline auto is_punct = ctype_is(std::ctype_base::punct); +export inline auto is_xdigit = ctype_is(std::ctype_base::xdigit); +export inline auto is_blank = ctype_is(std::ctype_base::blank); +export inline auto is_alnum = ctype_is(std::ctype_base::alnum); +export inline auto is_graph = ctype_is(std::ctype_base::graph); + +// Predefined tests for the C locale. The C locale is guaranteed to always be +// available, so this doesn't create lifetime issues. + +//NOLINTBEGIN: Technically, std::locale::classic() can throw. Assume it doesn't. +export inline auto is_c_space = ctype_is(std::ctype_base::space, std::locale::classic()); +export inline auto is_c_print = ctype_is(std::ctype_base::print, std::locale::classic()); +export inline auto is_c_cntrl = ctype_is(std::ctype_base::cntrl, std::locale::classic()); +export inline auto is_c_upper = ctype_is(std::ctype_base::upper, std::locale::classic()); +export inline auto is_c_lower = ctype_is(std::ctype_base::lower, std::locale::classic()); +export inline auto is_c_alpha = ctype_is(std::ctype_base::alpha, std::locale::classic()); +export inline auto is_c_digit = ctype_is(std::ctype_base::digit, std::locale::classic()); +export inline auto is_c_punct = ctype_is(std::ctype_base::punct, std::locale::classic()); +export inline auto is_c_xdigit = ctype_is(std::ctype_base::xdigit, std::locale::classic()); +export inline auto is_c_blank = ctype_is(std::ctype_base::blank, std::locale::classic()); +export inline auto is_c_alnum = ctype_is(std::ctype_base::alnum, std::locale::classic()); +export inline auto is_c_graph = ctype_is(std::ctype_base::graph, std::locale::classic()); +//NOLINTEND + +} // namespace nihil diff --git a/nihil.core/ctype.test.cc b/nihil.core/ctype.test.cc new file mode 100644 index 0000000..df73707 --- /dev/null +++ b/nihil.core/ctype.test.cc @@ -0,0 +1,376 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { + +TEST_CASE("ctype: space", "[ctype]") { + auto is_utf8_space = + nihil::ctype_is(std::ctype_base::space, + std::locale("C.UTF-8")); + + // '\v' (vertical tab) is a space + REQUIRE(nihil::is_space('\v') == true); + REQUIRE(nihil::is_space(L'\v') == true); + + REQUIRE(nihil::is_c_space('\v') == true); + REQUIRE(nihil::is_c_space(L'\v') == true); + + REQUIRE(is_utf8_space('\v') == true); + REQUIRE(is_utf8_space(L'\v') == true); + + // 'x' is not a space + REQUIRE(nihil::is_space('x') == false); + REQUIRE(nihil::is_space(L'x') == false); + + REQUIRE(nihil::is_c_space('x') == false); + REQUIRE(nihil::is_c_space(L'x') == false); + + REQUIRE(is_utf8_space('x') == false); + REQUIRE(is_utf8_space(L'x') == false); + + // U+2003 EM SPACE is a space + REQUIRE(nihil::is_space(L'\u2003') == false); + REQUIRE(nihil::is_c_space(L'\u2003') == false); + REQUIRE(is_utf8_space(L'\u2003') == true); +} + +TEST_CASE("ctype: print", "[ctype]") { + auto is_utf8_print = + nihil::ctype_is(std::ctype_base::print, + std::locale("C.UTF-8")); + + // 'x' is printable + REQUIRE(nihil::is_print('x') == true); + REQUIRE(nihil::is_print(L'x') == true); + + REQUIRE(nihil::is_c_print('x') == true); + REQUIRE(nihil::is_c_print(L'x') == true); + + REQUIRE(is_utf8_print('x') == true); + REQUIRE(is_utf8_print(L'x') == true); + + // '\003' is not printable + REQUIRE(nihil::is_print('\003') == false); + REQUIRE(nihil::is_print(L'\003') == false); + + REQUIRE(nihil::is_c_print('\003') == false); + REQUIRE(nihil::is_c_print(L'\003') == false); + + REQUIRE(is_utf8_print('\003') == false); + REQUIRE(is_utf8_print(L'\003') == false); + + // U+0410 CYRILLIC CAPITAL LETTER A is printable + REQUIRE(nihil::is_print(L'\u0410') == false); + REQUIRE(nihil::is_c_print(L'\u0410') == false); + REQUIRE(is_utf8_print(L'\u0410') == true); +} + +TEST_CASE("ctype: cntrl", "[ctype]") { + auto is_utf8_cntrl = + nihil::ctype_is(std::ctype_base::cntrl, + std::locale("C.UTF-8")); + + // '\003' is a control character + REQUIRE(nihil::is_cntrl('\003') == true); + REQUIRE(nihil::is_cntrl(L'\003') == true); + + REQUIRE(nihil::is_c_cntrl('\003') == true); + REQUIRE(nihil::is_c_cntrl(L'\003') == true); + + REQUIRE(is_utf8_cntrl('\003') == true); + REQUIRE(is_utf8_cntrl(L'\003') == true); + + + // 'x' is not a control character + REQUIRE(nihil::is_cntrl('x') == false); + REQUIRE(nihil::is_cntrl(L'x') == false); + + REQUIRE(nihil::is_c_cntrl('x') == false); + REQUIRE(nihil::is_c_cntrl(L'x') == false); + + REQUIRE(is_utf8_cntrl('x') == false); + REQUIRE(is_utf8_cntrl(L'x') == false); + + // U+00AD SOFT HYPHEN is a control character. + REQUIRE(nihil::is_cntrl(L'\u00ad') == false); + REQUIRE(nihil::is_c_cntrl(L'\u00ad') == false); + REQUIRE(is_utf8_cntrl(L'\u00ad') == true); +} + +TEST_CASE("ctype: upper", "[ctype]") { + auto is_utf8_upper = + nihil::ctype_is(std::ctype_base::upper, + std::locale("C.UTF-8")); + + // 'A' is upper case + REQUIRE(nihil::is_upper('A') == true); + REQUIRE(nihil::is_upper(L'A') == true); + + REQUIRE(nihil::is_c_upper('A') == true); + REQUIRE(nihil::is_c_upper(L'A') == true); + + REQUIRE(is_utf8_upper('A') == true); + REQUIRE(is_utf8_upper(L'A') == true); + + // 'a' is not upper case + REQUIRE(nihil::is_upper('a') == false); + REQUIRE(nihil::is_upper(L'a') == false); + + REQUIRE(nihil::is_c_upper('a') == false); + REQUIRE(nihil::is_c_upper(L'a') == false); + + REQUIRE(is_utf8_upper('a') == false); + REQUIRE(is_utf8_upper(L'a') == false); + + // U+0410 CYRILLIC CAPITAL LETTER A is upper case + REQUIRE(nihil::is_upper(L'\u0410') == false); + REQUIRE(nihil::is_c_upper(L'\u0410') == false); + REQUIRE(is_utf8_upper(L'\u0410') == true); +} + +TEST_CASE("ctype: lower", "[ctype]") { + auto is_utf8_lower = + nihil::ctype_is(std::ctype_base::lower, + std::locale("C.UTF-8")); + + // 'a' is lower case + REQUIRE(nihil::is_lower('a') == true); + REQUIRE(nihil::is_lower(L'a') == true); + + REQUIRE(nihil::is_c_lower('a') == true); + REQUIRE(nihil::is_c_lower(L'a') == true); + + REQUIRE(is_utf8_lower('a') == true); + REQUIRE(is_utf8_lower(L'a') == true); + + // 'A' is not lower case + REQUIRE(nihil::is_lower('A') == false); + REQUIRE(nihil::is_lower(L'A') == false); + + REQUIRE(nihil::is_c_lower('A') == false); + REQUIRE(nihil::is_c_lower(L'A') == false); + + REQUIRE(is_utf8_lower('A') == false); + REQUIRE(is_utf8_lower(L'A') == false); + + // U+0430 CYRILLIC SMALL LETTER A + REQUIRE(nihil::is_lower(L'\u0430') == false); + REQUIRE(nihil::is_c_lower(L'\u0430') == false); + REQUIRE(is_utf8_lower(L'\u0430') == true); +} + +TEST_CASE("ctype: alpha", "[ctype]") { + auto is_utf8_alpha = + nihil::ctype_is(std::ctype_base::alpha, + std::locale("C.UTF-8")); + + // 'a' is alphabetical + REQUIRE(nihil::is_alpha('a') == true); + REQUIRE(nihil::is_alpha(L'a') == true); + + REQUIRE(nihil::is_c_alpha('a') == true); + REQUIRE(nihil::is_c_alpha(L'a') == true); + + REQUIRE(is_utf8_alpha('a') == true); + REQUIRE(is_utf8_alpha(L'a') == true); + + // '1' is not alphabetical + REQUIRE(nihil::is_alpha('1') == false); + REQUIRE(nihil::is_alpha(L'1') == false); + + REQUIRE(nihil::is_c_alpha('1') == false); + REQUIRE(nihil::is_c_alpha(L'1') == false); + + REQUIRE(is_utf8_alpha('1') == false); + REQUIRE(is_utf8_alpha(L'1') == false); + + // U+0430 CYRILLIC SMALL LETTER A + REQUIRE(nihil::is_alpha(L'\u0430') == false); + REQUIRE(nihil::is_c_alpha(L'\u0430') == false); + REQUIRE(is_utf8_alpha(L'\u0430') == true); +} + +TEST_CASE("ctype: digit", "[ctype]") { + auto is_utf8_digit = + nihil::ctype_is(std::ctype_base::digit, + std::locale("C.UTF-8")); + + // '1' is a digit + REQUIRE(nihil::is_digit('1') == true); + REQUIRE(nihil::is_digit(L'1') == true); + + REQUIRE(nihil::is_c_digit('1') == true); + REQUIRE(nihil::is_c_digit(L'1') == true); + + REQUIRE(is_utf8_digit('1') == true); + REQUIRE(is_utf8_digit(L'1') == true); + + // 'a' is not a digit + REQUIRE(nihil::is_digit('a') == false); + REQUIRE(nihil::is_digit(L'a') == false); + + REQUIRE(nihil::is_c_digit('a') == false); + REQUIRE(nihil::is_c_digit(L'a') == false); + + REQUIRE(is_utf8_digit('a') == false); + REQUIRE(is_utf8_digit(L'a') == false); + + // U+0660 ARABIC-INDIC DIGIT ZERO + REQUIRE(nihil::is_digit(L'\u0660') == false); + REQUIRE(nihil::is_c_digit(L'\u0660') == false); + REQUIRE(is_utf8_digit(L'\u0660') == true); +} + +TEST_CASE("ctype: punct", "[ctype]") { + auto is_utf8_punct = + nihil::ctype_is(std::ctype_base::punct, + std::locale("C.UTF-8")); + + // ';' is punctuation + REQUIRE(nihil::is_punct(';') == true); + REQUIRE(nihil::is_punct(L';') == true); + + REQUIRE(nihil::is_c_punct(';') == true); + REQUIRE(nihil::is_c_punct(L';') == true); + + REQUIRE(is_utf8_punct(';') == true); + REQUIRE(is_utf8_punct(L';') == true); + + // 'a' is not punctuation + REQUIRE(nihil::is_punct('a') == false); + REQUIRE(nihil::is_punct(L'a') == false); + + REQUIRE(nihil::is_c_punct('a') == false); + REQUIRE(nihil::is_c_punct(L'a') == false); + + REQUIRE(is_utf8_punct('a') == false); + REQUIRE(is_utf8_punct(L'a') == false); + + // U+00A1 INVERTED EXCLAMATION MARK + REQUIRE(nihil::is_punct(L'\u00A1') == false); + REQUIRE(nihil::is_c_punct(L'\u00A1') == false); + REQUIRE(is_utf8_punct(L'\u00A1') == true); +} + +TEST_CASE("ctype: xdigit", "[ctype]") { + auto is_utf8_xdigit = + nihil::ctype_is(std::ctype_base::xdigit, + std::locale("C.UTF-8")); + + // 'f' is an xdigit + REQUIRE(nihil::is_xdigit('f') == true); + REQUIRE(nihil::is_xdigit(L'f') == true); + + REQUIRE(nihil::is_c_xdigit('f') == true); + REQUIRE(nihil::is_c_xdigit(L'f') == true); + + REQUIRE(is_utf8_xdigit('f') == true); + REQUIRE(is_utf8_xdigit(L'f') == true); + + // 'g' is not an xdigit + REQUIRE(nihil::is_xdigit('g') == false); + REQUIRE(nihil::is_xdigit(L'g') == false); + + REQUIRE(nihil::is_c_xdigit('g') == false); + REQUIRE(nihil::is_c_xdigit(L'g') == false); + + REQUIRE(is_utf8_xdigit('g') == false); + REQUIRE(is_utf8_xdigit(L'g') == false); +} + +TEST_CASE("ctype: blank", "[ctype]") { + auto is_utf8_blank = + nihil::ctype_is(std::ctype_base::blank, + std::locale("C.UTF-8")); + + // '\t' is a blank + REQUIRE(nihil::is_blank('\t') == true); + REQUIRE(nihil::is_blank(L'\t') == true); + + REQUIRE(nihil::is_c_blank('\t') == true); + REQUIRE(nihil::is_c_blank(L'\t') == true); + + REQUIRE(is_utf8_blank('\t') == true); + REQUIRE(is_utf8_blank(L'\t') == true); + + // '\v' is not a blank + REQUIRE(nihil::is_blank('\v') == false); + REQUIRE(nihil::is_blank(L'\v') == false); + + REQUIRE(nihil::is_c_blank('\v') == false); + REQUIRE(nihil::is_c_blank(L'\v') == false); + + REQUIRE(is_utf8_blank('\v') == false); + REQUIRE(is_utf8_blank(L'\v') == false); + + // There don't seem to be any UTF-8 blank characters, at least + // in FreeBSD libc. +} + +TEST_CASE("ctype: alnum", "[ctype]") { + auto is_utf8_alnum = + nihil::ctype_is(std::ctype_base::alnum, + std::locale("C.UTF-8")); + + // 'a' is alphanumeric + REQUIRE(nihil::is_alnum('a') == true); + REQUIRE(nihil::is_alnum(L'a') == true); + + REQUIRE(nihil::is_c_alnum('a') == true); + REQUIRE(nihil::is_c_alnum(L'a') == true); + + REQUIRE(is_utf8_alnum('a') == true); + REQUIRE(is_utf8_alnum(L'a') == true); + + // '\t' is not a alnum + REQUIRE(nihil::is_alnum('\t') == false); + REQUIRE(nihil::is_alnum(L'\t') == false); + + REQUIRE(nihil::is_c_alnum('\t') == false); + REQUIRE(nihil::is_c_alnum(L'\t') == false); + + REQUIRE(is_utf8_alnum('\t') == false); + REQUIRE(is_utf8_alnum(L'\t') == false); + + // U+0430 CYRILLIC SMALL LETTER A + REQUIRE(nihil::is_alnum(L'\u0430') == false); + REQUIRE(nihil::is_c_alnum(L'\u0430') == false); + REQUIRE(is_utf8_alnum(L'\u0430') == true); +} + +TEST_CASE("ctype: graph", "[ctype]") { + auto is_utf8_graph = + nihil::ctype_is(std::ctype_base::graph, + std::locale("C.UTF-8")); + + // 'a' is graphical + REQUIRE(nihil::is_graph('a') == true); + REQUIRE(nihil::is_graph(L'a') == true); + + REQUIRE(nihil::is_c_graph('a') == true); + REQUIRE(nihil::is_c_graph(L'a') == true); + + REQUIRE(is_utf8_graph('a') == true); + REQUIRE(is_utf8_graph(L'a') == true); + + // '\t' is not graphical + REQUIRE(nihil::is_graph('\t') == false); + REQUIRE(nihil::is_graph(L'\t') == false); + + REQUIRE(nihil::is_c_graph('\t') == false); + REQUIRE(nihil::is_c_graph(L'\t') == false); + + REQUIRE(is_utf8_graph('\t') == false); + REQUIRE(is_utf8_graph(L'\t') == false); + + // U+0430 CYRILLIC SMALL LETTER A + REQUIRE(nihil::is_graph(L'\u0430') == false); + REQUIRE(nihil::is_c_graph(L'\u0430') == false); + REQUIRE(is_utf8_graph(L'\u0430') == true); +} + +} // anonymous namespace diff --git a/nihil.core/error.ccm b/nihil.core/error.ccm new file mode 100644 index 0000000..5aa23f5 --- /dev/null +++ b/nihil.core/error.ccm @@ -0,0 +1,321 @@ +// This source code is released into the public domain. +export module nihil.core:error; + +// error: a type representing an error. +// +// An error consists of an immediate cause, which may be a string or +// std:error_code, and an optional proximate cause, which is another error +// object. Any number of error objects may be stacked. +// +// For example, a failure to open a file might be a stack of two errors: +// +// - string, "failed to open /etc/somefile", +// - std::error_code, "No such file or directory". +// +// Calling .str() will format the entire stack starting at that error, +// for example: "failed to open /etc/somefile: No such file or directory". +// +// Errors may be moved and (relatively) cheaply copied, since the cause +// chain is refcounted. +// +// error derives from std::exception, so it may be thrown and caught and +// provides a useful what(). When throwing errors, creating a derived +// error will make it easier to distinguish errors when catching them. + +import nihil.std; + +import :match; + +namespace nihil { + +// Things which can be errors. +using error_t = std::variant; + +// A proxy class used when constructing errors. This has implicit constructors from various types, +// which means we don't have to handle every possible combination of types in error itself. +export struct error_proxy +{ + // Construct from... + + // ... a string_view + error_proxy(std::string_view const what) // NOLINT + : m_error(std::string(what)) + { + } + + // ... an std::string; so we can move the string into place if it's an rvalue. + error_proxy(auto &&what) // NOLINT + requires(std::same_as, std::string>) + : m_error(std::forward(what)) + { + } + + // ... a C string + error_proxy(char const *what) // NOLINT + : m_error(std::string(what)) + { + } + + // ... an std::error_code + error_proxy(std::error_code const what) // NOLINT + : m_error(what) + { + } + + // ... an std::error_condition + error_proxy(std::error_condition const what) // NOLINT + : m_error(what) + { + } + + // ... an error code enum + template + requires(std::is_error_code_enum::value) + error_proxy(T what) // NOLINT + : m_error(make_error_code(what)) + { + } + + // ... an error condition enum + template + requires(std::is_error_condition_enum::value) + error_proxy(T what) // NOLINT + : m_error(make_error_condition(what)) + { + } + + // Not copyable. + error_proxy(error_proxy const &) = delete; + auto operator=(error_proxy const &) -> error_proxy & = delete; + + // Not movable. + error_proxy(error_proxy &&) = delete; + auto operator=(error_proxy &&) -> error_proxy & = delete; + + ~error_proxy() = default; + + // Let error extract the error_t. + [[nodiscard]] auto error() && -> error_t && + { + return std::move(m_error); + } + +private: + // The error. + error_t m_error; +}; + +// The error class. +export struct error : std::exception +{ + // Create an empty error, representing success. + error() = default; + + // Destroy an error. + ~error() override = default; + + // Create an error from an error proxy. + explicit error(error_proxy &&proxy) + : m_error(std::move(std::move(proxy).error())) + { + } + + // Create an error from an error proxy and an error cause. + error(error_proxy &&proxy, error cause) + : m_error(std::move(std::move(proxy).error())) + , m_cause(std::make_shared(std::move(cause))) + { + } + + // Create an error from an error proxy and an error_proxy cause. + // For example, error("cannot open file", std::errc::permission_denied). + error(error_proxy &&proxy, error_proxy &&cause) + : m_error(std::move(std::move(proxy).error())) + , m_cause(std::make_shared(std::move(cause))) + { + } + + // Copyable. + error(error const &) = default; + auto operator=(error const &) -> error & = default; + + // Movable. + error(error &&) noexcept = default; + auto operator=(error &&) noexcept -> error & = default; + + // Return the cause of this error. + [[nodiscard]] auto cause(this error const &self) -> std::shared_ptr const & + { + return self.m_cause; + } + + // Return the root cause of this error, which may be this object. + // For errors caused by an OS error, this will typically be the + // error_code error. + [[nodiscard]] auto root_cause(this error const &self) -> error const & + { + auto const *cause = &self; + while (cause->m_cause) + cause = cause->m_cause.get(); + return *cause; + } + + // Format this error without its cause as a string. + [[nodiscard]] auto this_str(this error const &self) -> std::string + { + return self.m_error | match { + [] (std::monostate) -> std::string { + return "No error"; + }, + [] (std::error_code const &m) { + return m.message(); + }, + [] (std::error_condition const &m) { + return m.message(); + }, + [] (std::string const &m) { + return m; + } + }; + } + + // Format this error and its cause as a string. + [[nodiscard]] auto full_str(this error const &self) -> std::string + { + auto str = self.this_str(); + + auto cause = self.cause(); + while (cause) { + str += ": " + cause->this_str(); + cause = cause->cause(); + } + + return str; + } + + // Return this error's error_code, if any. + [[nodiscard]] auto code(this error const &self) -> std::optional + { + auto const *code = std::get_if(&self.m_error); + if (code != nullptr) + return {*code}; + return {}; + } + + // Return this error's error_condition, if any. + [[nodiscard]] auto condition(this error const &self) -> std::optional + { + auto const *condition = std::get_if(&self.m_error); + if (condition != nullptr) + return {*condition}; + return {}; + } + + // Format this error and its cause as a C string and return it. This is for + // compatibility with std::exception. The lifetime of the returned string + // is the same as the error object. + [[nodiscard]] auto what() const noexcept -> char const * final + { + if (!m_what) + m_what = this->full_str(); + return m_what->c_str(); + } + + // Allow error to be implicitly converted to std::expected and std::unexpected, to make + // using it with std::expected easier. + + template + operator std::expected () && // NOLINT + { + return std::unexpected{std::move(*this)}; + } + + template + operator std::expected () const // NOLINT + { + return std::unexpected{*this}; + } + + operator std::unexpected () && // NOLINT + { + return std::unexpected{std::move(*this)}; + } + + operator std::unexpected () const // NOLINT + { + return std::unexpected{*this}; + } + +private: + // This error. + error_t m_error = make_error_code(std::errc()); + + // The cause of this error, if any. + std::shared_ptr m_cause; + + // For std::exception::what(), we need to keep the string valid + // until we're destroyed. + mutable std::optional m_what; + + // Equality comparison. + [[nodiscard]] friend auto operator==(error const &lhs, error const &rhs) -> bool + { + return lhs.m_error == rhs.m_error; + } + + [[nodiscard]] friend auto operator<=>(error const &lhs, error const &rhs) -> std::strong_ordering + { + return lhs.m_error <=> rhs.m_error; + } + + // Compare an error with an std::error_code. + [[nodiscard]] friend auto operator==(error const &lhs, std::error_code const &rhs) -> bool + { + return lhs.code() == rhs; + } + + // Compare an error to an std::error_condition. + [[nodiscard]] friend auto operator==(error const &lhs, std::error_condition const &rhs) -> bool + { + return lhs.condition() == rhs; + } + + // Compare an error to an std::error_code enum. + [[nodiscard]] friend auto operator==(error const &lhs, auto rhs) -> bool + requires(std::is_error_code_enum::value) + { + return lhs.code() == rhs; + } + + // Compare an error to an std::error_condition enum. + [[nodiscard]] friend auto operator==(error const &lhs, auto rhs) -> bool + requires(std::is_error_condition_enum::value) + { + return lhs.condition() == rhs; + } + + // Print an error to a stream. + friend auto operator<<(std::ostream &strm, error const &e) -> std::ostream & + { + return strm << e.full_str(); + } +}; + +} // namespace nihil + +// Make error formattable. +export template <> +struct std::formatter +{ + template + constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator + { + return ctx.begin(); + } + + template + auto format(nihil::error const &e, FormatContext &ctx) const -> FormatContext::iterator + { + return std::ranges::copy(e.full_str(), ctx.out()).out; + } +}; diff --git a/nihil.core/error.test.cc b/nihil.core/error.test.cc new file mode 100644 index 0000000..f4ec1ee --- /dev/null +++ b/nihil.core/error.test.cc @@ -0,0 +1,273 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; +import nihil.util; + +namespace { +inline constexpr auto *test_tags = "[nihil][nihil.error]"; + +TEST_CASE("error: invariants", test_tags) +{ + static_assert(std::destructible); + static_assert(std::default_initializable); + static_assert(std::move_constructible); + static_assert(std::copy_constructible); + static_assert(std::equality_comparable); + static_assert(std::totally_ordered); + static_assert(std::swappable); + static_assert(std::regular); +} + +SCENARIO("A nihil::error can be constructed from a C string", test_tags) +{ + GIVEN ("An error object constructed from a string") { + auto e = nihil::error("an error"); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string lvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string lvalue") { + auto s = std::string("an error"); + auto e = nihil::error(s); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string rvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string rvalue") { + auto e = nihil::error(std::string("an error")); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string_view lvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string_view lvalue") { + auto s = std::string_view("an error"); + auto e = nihil::error(s); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string_view rvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string_view rvalue") { + auto e = nihil::error(std::string_view("an error")); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::error_condition", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(std::error_condition(std::errc::invalid_argument)); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Invalid argument"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == std::errc::invalid_argument); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == std::errc::invalid_argument); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::errc", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(std::errc::invalid_argument); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Invalid argument"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == std::errc::invalid_argument); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == std::errc::invalid_argument); + } + } +} + +SCENARIO("A nihil::error can be constructed from a nihil::errc", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(nihil::errc::incomplete_command); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Incomplete command"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == nihil::errc::incomplete_command); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == nihil::errc::incomplete_command); + } + } +} + +SCENARIO("A nihil::error can be constructed with a cause", test_tags) +{ + GIVEN ("An error object constructed with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error: Invalid argument"); + } + + AND_THEN ("cause() should return the cause") { + REQUIRE(e.cause()); + REQUIRE(*e.cause() == std::errc::invalid_argument); + } + } +} + +SCENARIO("std::format with a nihil::error", test_tags) +{ + GIVEN ("A nihil::error with no cause") { + auto e = nihil::error("an error"); + + THEN ("std::format should return the string") { + REQUIRE(std::format("{}", e) == "an error"); + } + + AND_THEN ("std::format should return the same as full_str()") { + REQUIRE(std::format("{}", e) == e.full_str()); + } + } + + GIVEN ("A nihil::error with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("std::format should return the string") { + REQUIRE(std::format("{}", e) == "an error: Invalid argument"); + } + + AND_THEN ("std::format should return the same as full_str()") { + REQUIRE(std::format("{}", e) == e.full_str()); + } + } +} + +SCENARIO("Print a nihil::error to an std::ostream", test_tags) +{ + GIVEN ("A nihil::error with no cause") { + auto e = nihil::error("an error"); + + THEN ("The error should be printed to the stream") { + auto ss = std::stringstream(); + ss << e; + REQUIRE(ss.str() == "an error"); + } + } + + GIVEN ("A nihil::error with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("The error should be printed to the stream") { + auto ss = std::stringstream(); + ss << e; + REQUIRE(ss.str() == "an error: Invalid argument"); + } + } +} + +SCENARIO("Comparison of nihil::error with operator==", test_tags) +{ + GIVEN ("Two nihil::error objects constructed from the same string") { + auto e1 = nihil::error("an error"); + auto e2 = nihil::error("an error"); + + THEN ("The two objects should be equal") { + REQUIRE(e1 == e2); + } + } + + GIVEN ("Two nihil::error objects constructed from different strings") { + auto e1 = nihil::error("an error"); + auto e2 = nihil::error("another error"); + + THEN ("The two objects should not be equal") { + REQUIRE(e1 != e2); + } + } + + GIVEN ("Two nihil::error objects constructed from the same error code") { + auto e1 = nihil::error(std::errc::invalid_argument); + auto e2 = nihil::error(std::errc::invalid_argument); + + THEN ("The two objects should be equal") { + REQUIRE(e1 == e2); + } + } + + GIVEN ("Two nihil::error objects constructed from different error codes") { + auto e1 = nihil::error(std::errc::invalid_argument); + auto e2 = nihil::error(std::errc::permission_denied); + + THEN ("The two objects should not be equal") { + REQUIRE(e1 != e2); + } + } +} + +SCENARIO("Comparison of nihil::error with operator<", test_tags) +{ + GIVEN ("Two nihil::error objects constructed from the same string") { + auto e1 = nihil::error("aaa"); + auto e2 = nihil::error("zzz"); + + THEN ("aaa should be less than zzz") { + REQUIRE(e1 < e2); + } + } +} + +SCENARIO("Throwing and catching a nihil::error object", test_tags) +{ + GIVEN ("A nihil::error object") { + THEN ("We should be able to throw and catch the error") { + REQUIRE_THROWS_AS(throw nihil::error("an error"), nihil::error); + + try { + throw nihil::error("an error"); + } catch (nihil::error const &e) { + REQUIRE(e.full_str() == "an error"); + }; + } + } +} + +} // anonymous namespace diff --git a/nihil.core/flagset.ccm b/nihil.core/flagset.ccm new file mode 100644 index 0000000..cea9889 --- /dev/null +++ b/nihil.core/flagset.ccm @@ -0,0 +1,200 @@ +// This source code is released into the public domain. +export module nihil.core:flagset; + +/* + * flagset: a type-safe flags type. + */ + +import nihil.std; + +namespace nihil { + +export template +struct flagset final +{ + using underlying_type = base_type; + + /* + * Create an empty flags. + */ + flagset() noexcept = default; + + /* + * Copyable. + */ + flagset(flagset const &other) noexcept + : m_value(other.m_value) + { + } + + /* + * Create flags from an integer mask. + */ + template + [[nodiscard]] static constexpr auto mask() noexcept -> flagset + { + return flagset(flag); + } + + /* + * Create flags for a specific bit. + */ + template + [[nodiscard]] static constexpr auto bit() noexcept -> flagset + { + static_assert(bitnr < std::numeric_limits::digits); + return flagset(static_cast(1) << bitnr); + } + + /* + * Create flags from a runtime value. + */ + [[nodiscard]] static auto from_int(base_type value) noexcept -> flagset + { + return flagset(value); + } + + /* + * Assign this flagset. + */ + auto operator=(this flagset &lhs, flagset rhs) noexcept -> flagset & + { + if (&lhs != &rhs) + lhs.m_value = rhs.m_value; + return lhs; + } + + /* + * The integer value of this flagset. + */ + [[nodiscard]] constexpr auto value(this flagset self) noexcept -> base_type + { + return self.m_value; + } + + /* + * True if this flagset has any bits set. + */ + [[nodiscard]] explicit constexpr operator bool(this flagset self) noexcept + { + return self.m_value != 0; + } + + /* + * Set bits. + */ + constexpr auto operator|=(this flagset &lhs, flagset rhs) noexcept -> flagset & + { + lhs.m_value |= rhs.value(); + return lhs; + } + + /* + * Mask bits. + */ + constexpr auto operator&=(this flagset &lhs, flagset rhs) noexcept -> flagset & + { + lhs.m_value &= rhs.value(); + return lhs; + } + + /* + * Invert bits. + */ + [[nodiscard]] constexpr auto operator~(this flagset self) noexcept -> flagset + { + return flagset(~self.m_value); + } + + /* + * xor bits. + */ + constexpr auto operator^=(this flagset &lhs, flagset rhs) noexcept -> flagset + { + lhs.m_value ^= rhs.value(); + return lhs; + } + +private: + base_type m_value = 0; + + explicit constexpr flagset(base_type mask) noexcept + : m_value(mask) + { + } + + [[nodiscard]] friend auto operator|(flagset lhs, flagset rhs) noexcept -> flagset + { + return (lhs |= rhs); + } + + [[nodiscard]] friend auto operator&(flagset lhs, flagset rhs) noexcept -> flagset + { + return (lhs &= rhs); + } + + [[nodiscard]] friend auto operator^(flagset lhs, flagset rhs) noexcept -> flagset + { + return (lhs ^= rhs); + } + + [[nodiscard]] friend auto + operator==(flagset lhs, flagset rhs) noexcept -> bool + { + return lhs.value() == rhs.value(); + } + + friend auto operator<<(std::ostream &strm, flagset flags) -> std::ostream & + { + std::print(strm, "{}", flags); + return strm; + } +}; + +} // namespace nihil + +/* + * Formatting for flagset. + */ +export template +struct std::formatter, Char> +{ + using flags_t = nihil::flagset; + + template + constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator + { + return ctx.begin(); + } + + template + auto format(flags_t flags, FmtContext &ctx) const -> FmtContext::iterator + { + auto constexpr digits = std::numeric_limits::digits; + auto value = flags.value(); + auto it = ctx.out(); + *it++ = Char{'<'}; + + auto printed_any = false; + + for (unsigned i = 0; i < digits; ++i) { + unsigned bit = (digits - 1) - i; + + auto this_bit = static_cast(1) << bit; + if ((value & this_bit) == 0) + continue; + + if (printed_any) + *it++ = Char{','}; + + if (bit > 10) + *it++ = Char{'0'} + (bit / 10); + *it++ = Char{'0'} + (bit % 10); + + printed_any = true; + } + + *it++ = Char{'>'}; + return it; + } +}; diff --git a/nihil.core/flagset.test.cc b/nihil.core/flagset.test.cc new file mode 100644 index 0000000..353b638 --- /dev/null +++ b/nihil.core/flagset.test.cc @@ -0,0 +1,144 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { +struct test_tag +{ +}; +using testflags = nihil::flagset; + +constexpr auto zero = testflags::bit<0>(); +constexpr auto one = testflags::bit<1>(); +constexpr auto two = testflags::bit<2>(); +constexpr auto twelve = testflags::bit<12>(); + +TEST_CASE("flagset: invariant", "[nihil]") +{ + static_assert(std::regular); + static_assert(sizeof(testflags) == sizeof(testflags::underlying_type)); +} + +TEST_CASE("flagset: bit<>", "[nihil]") +{ + REQUIRE(zero.value() == 0x1); + REQUIRE(one.value() == 0x2); +} + +TEST_CASE("flagset: mask<>", "[nihil]") +{ + auto zero_ = testflags::mask<0x1>(); + auto one_ = testflags::mask<0x2>(); + + REQUIRE(zero_ == zero); + REQUIRE(one_ == one); +} + +TEST_CASE("flagset: constructor", "[nihil]") +{ + SECTION ("default construct") { + auto flags = testflags(); + REQUIRE(flags.value() == 0); + } + + SECTION ("construct from int") { + auto flags = testflags::from_int(one.value() | zero.value()); + REQUIRE(flags == (one | zero)); + } + + SECTION ("copy construct") { + auto flags = one; + auto flags2(flags); + REQUIRE(flags == flags2); + } +} + +TEST_CASE("flagset: operators", "[nihil]") +{ + SECTION ("operator|") { + REQUIRE((zero | one).value() == 0x3); + } + + SECTION ("operator|=") { + auto flags = zero; + flags |= one; + REQUIRE(flags.value() == 0x3); + } + + SECTION ("operator&") { + auto flags = zero | one; + + REQUIRE((flags & zero) == zero); + } + + SECTION ("operator&=") { + auto flags = zero | one | two; + REQUIRE(flags.value() == 0x7); + flags &= (zero | one); + REQUIRE(flags.value() == 0x3); + } + + SECTION ("operator^") { + auto flags = zero | one; + REQUIRE((flags ^ (one | two)) == (zero | two)); + } + + SECTION ("operator^=") { + auto flags = zero | one; + flags ^= (one | two); + REQUIRE(flags == (zero | two)); + } + + SECTION ("operator~") { + auto flags = ~zero; + REQUIRE(flags.value() == ~static_cast(1)); + } + + SECTION ("operator==") { + auto flags = zero; + REQUIRE(flags == zero); + REQUIRE(flags != one); + } +} + +TEST_CASE("flagset: assignment", "[nihil]") +{ + auto flags = zero; + REQUIRE(flags == zero); + + flags = one; + REQUIRE(flags == one); + REQUIRE(flags != zero); +} + +TEST_CASE("flagset: format", "[nihil]") +{ + REQUIRE(std::format("{}", testflags()) == "<>"); + REQUIRE(std::format("{}", zero) == "<0>"); + REQUIRE(std::format("{}", one) == "<1>"); + REQUIRE(std::format("{}", zero | one) == "<1,0>"); + + REQUIRE(std::format("{}", twelve) == "<12>"); + REQUIRE(std::format("{}", twelve | one) == "<12,1>"); +} + +TEST_CASE("flagset: ostream operator<<", "[nihil]") +{ + auto write = [](testflags flags) -> std::string { + auto strm = std::ostringstream(); + strm << flags; + return strm.str(); + }; + + REQUIRE(write(testflags()) == "<>"); + REQUIRE(write(zero) == "<0>"); + REQUIRE(write(one) == "<1>"); + REQUIRE(write(zero | one) == "<1,0>"); + + REQUIRE(write(twelve) == "<12>"); + REQUIRE(write(twelve | one) == "<12,1>"); +} +} // anonymous namespace diff --git a/nihil.core/guard.ccm b/nihil.core/guard.ccm new file mode 100644 index 0000000..0576042 --- /dev/null +++ b/nihil.core/guard.ccm @@ -0,0 +1,41 @@ +// This source code is released into the public domain. +export module nihil.core:guard; + +import nihil.std; + +namespace nihil { + +// guard: invoke a callable when this object is destroyed; this is similar to +// scope_exit from the library fundamentals TS, which LLVM doesn't implement. +export template +struct guard final { + // Initialise the guard with a callable we will invoke later. + explicit guard(F func) : m_func(std::move(func)) {} + + // We are being destroyed, so call the callable. + // If the callable throws, std::terminate() will be called. + ~guard() + { + if (m_func) + std::invoke(*m_func); + } + + // Release the guard. This turns the destructor into a no-op. + auto release(this guard &self) noexcept -> void + { + self.m_func.reset(); + } + + // Not default-constructible, movable or copyable. + guard() = delete; + guard(guard const &) = delete; + guard(guard &&) noexcept = delete; + auto operator=(this guard &, guard const &) -> guard & = delete; + auto operator=(this guard &, guard &&) noexcept -> guard & = delete; + +private: + // The callable to be invoked when we are destroyed. + std::optional m_func; +}; + +} // namespace nihil diff --git a/nihil.core/guard.test.cc b/nihil.core/guard.test.cc new file mode 100644 index 0000000..43076f5 --- /dev/null +++ b/nihil.core/guard.test.cc @@ -0,0 +1,16 @@ +// This source code is released into the public domain. + +#include + +import nihil.core; + +TEST_CASE("guard: basic", "[guard]") { + int n = 0; + + { + auto guard = nihil::guard([&] { n = 1; }); + REQUIRE(n == 0); + } + + REQUIRE(n == 1); +} diff --git a/nihil.core/match.ccm b/nihil.core/match.ccm new file mode 100644 index 0000000..e1c894c --- /dev/null +++ b/nihil.core/match.ccm @@ -0,0 +1,18 @@ +// This source code is released into the public domain. +export module nihil.core:match; + +import nihil.std; + +namespace nihil { + +export template +struct match : Ts... { using Ts::operator()...; }; + +export template +[[nodiscard]] constexpr decltype(auto) operator| + (std::variant const &v, match const &match) +{ + return std::visit(match, v); +} + +} // namespace nihil diff --git a/nihil.core/match.test.cc b/nihil.core/match.test.cc new file mode 100644 index 0000000..2394651 --- /dev/null +++ b/nihil.core/match.test.cc @@ -0,0 +1,32 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { +TEST_CASE("match", "[nihil]") +{ + using namespace nihil; + using namespace std::literals; + + auto v = std::variant(42); + + auto s = v | match { + [](int) { return "int"s; }, + [](std::string const &) { return "string"s; } + }; + + REQUIRE(s == "int"); + + v = "test"s; + + s = v | match { + [](int) { return "int"s; }, + [](std::string const &) { return "string"s; } + }; + + REQUIRE(s == "string"); +} +} // anonymous namespace diff --git a/nihil.core/monad.ccm b/nihil.core/monad.ccm new file mode 100644 index 0000000..ae26416 --- /dev/null +++ b/nihil.core/monad.ccm @@ -0,0 +1,282 @@ +/* + * From https://github.com/toby-allsopp/coroutine_monad + * + * Copyright (c) 2017 Toby Allsopp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export module nihil.core:monad; + +import nihil.std; + +namespace nihil { + +/********************************************************************** + * return_object_holder + */ + +// An object that starts out unitialized. Initialized by a call to emplace. +export template +using deferred = std::optional; + +export template +struct return_object_holder { + // The staging object that is returned (by copy/move) to the caller of + // the coroutine. + deferred stage; + return_object_holder*& p; + + // When constructed, we assign a pointer to ourselves to the supplied + // reference to pointer. + return_object_holder(return_object_holder*& p) + : stage{} + , p(p) + { + p = this; + } + + // Copying doesn't make any sense (which copy should the pointer refer + // to?). + return_object_holder(return_object_holder const&) = delete; + + // To move, we just update the pointer to point at the new object. + return_object_holder(return_object_holder&& other) + : stage(std::move(other.stage)) + , p(other.p) + { + p = this; + } + + // Assignment doesn't make sense. + void operator=(return_object_holder const&) = delete; + void operator=(return_object_holder&&) = delete; + + // A non-trivial destructor is required until + // https://bugs.llvm.org//show_bug.cgi?id=28593 is fixed. + ~return_object_holder() {} + + // Construct the staging value; arguments are perfect forwarded to T's + // constructor. + template + void emplace(Args&&... args) + { + stage.emplace(std::forward(args)...); + } + + // We assume that we will be converted only once, so we can move from + // the staging object. We also assume that `emplace` has been called + // at least once. + operator T() + { + return std::move(*stage); + } +}; + +export template +auto make_return_object_holder(return_object_holder*& p) +{ + return return_object_holder{p}; +} + +/********************************************************************** + * std::optional + */ + +template +struct optional_promise { + return_object_holder>* data; + + auto get_return_object() + { + return make_return_object_holder(data); + } + + auto initial_suspend() noexcept -> std::suspend_never + { + return {}; + } + + auto final_suspend() noexcept -> std::suspend_never + { + return {}; + } + + void return_value(T x) + { + data->emplace(std::move(x)); + } + + void unhandled_exception() + { + std::rethrow_exception(std::current_exception()); + } +}; + +} // namespace nihil + +export template +struct std::coroutine_traits, Args...> { + using promise_type = nihil::optional_promise; +}; + +namespace nihil { + +template +struct optional_awaitable { + std::optional o; + + auto await_ready() + { + return o.has_value(); + } + + auto await_resume() + { + return *o; + } + + template + void await_suspend(std::coroutine_handle> h) + { + h.promise().data->emplace(std::nullopt); + h.destroy(); + } +}; + +} // namespace nihil + +namespace std { + +export template +auto operator co_await(std::optional o) { + return nihil::optional_awaitable{std::move(o)}; +} + +} // namespace std + +/********************************************************************** + * std::expected + */ + +namespace nihil { + +export template +struct expected_promise_base { + return_object_holder>* data; + + auto get_return_object() + { + return make_return_object_holder(data); + } + + auto initial_suspend() noexcept -> std::suspend_never + { + return {}; + } + + auto final_suspend() noexcept -> std::suspend_never + { + return {}; + } + + void unhandled_exception() + { + std::rethrow_exception(std::current_exception()); + } +}; + +export template +struct expected_promise : expected_promise_base { + void return_value(this expected_promise &self, std::unexpected err) + { + self.data->emplace(std::move(err)); + } + + void return_value(this expected_promise &self, T o) + { + self.data->emplace(std::move(o)); + } +}; + +export template +struct expected_promise : expected_promise_base { + void return_value(this expected_promise &self, std::unexpected err) + { + self.data->emplace(std::move(err)); + } + + void return_value(this expected_promise &self, std::expected o) + { + self.data->emplace(std::move(o)); + } +}; + +} // namespace nihil + +export template +struct std::coroutine_traits, Args...> { + using promise_type = nihil::expected_promise; +}; + +namespace nihil { + +export template +struct expected_awaitable_base { + std::expected o; + + auto await_ready() + { + return o.has_value(); + } + + template + void await_suspend(std::coroutine_handle

h) + { + h.promise().data->emplace(std::unexpected(o.error())); + h.destroy(); + } +}; + +export template +struct expected_awaitable : expected_awaitable_base { + auto await_resume(this expected_awaitable &self) + { + return std::move(*self.o); + } +}; + +export template +struct expected_awaitable : expected_awaitable_base { + auto await_resume(this expected_awaitable &) + { + return std::expected(); + } +}; + +} // namespace nihil + +namespace std { + +export template +auto operator co_await(std::expected o) { + return nihil::expected_awaitable{std::move(o)}; +} + +} // namespace std diff --git a/nihil.core/monad.test.cc b/nihil.core/monad.test.cc new file mode 100644 index 0000000..e5003ac --- /dev/null +++ b/nihil.core/monad.test.cc @@ -0,0 +1,65 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { +TEST_CASE("monad: co_await std::optional<> with value", "[nihil]") +{ + auto get_value = [] -> std::optional { + return 42; + }; + + auto try_get_value = [&get_value] -> std::optional { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(o == 42); +} + +TEST_CASE("monad: co_await std::optional<> without value", "[nihil]") +{ + auto get_value = [] -> std::optional { + return {}; + }; + + auto try_get_value = [&get_value] -> std::optional { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(!o.has_value()); +} + +TEST_CASE("monad: co_await std::expected<> with value", "[nihil]") +{ + auto get_value = [] -> std::expected { + return 42; + }; + + auto try_get_value = [&get_value] -> std::expected { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(o == 42); +} + +TEST_CASE("monad: co_await std::expected<> with error", "[nihil]") +{ + auto get_value = [] -> std::expected { + return std::unexpected("error"); + }; + + auto try_get_value = [&get_value] -> std::expected { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(!o); + REQUIRE(o.error() == "error"); +} +} // anonymous namespace diff --git a/nihil.core/next_word.ccm b/nihil.core/next_word.ccm new file mode 100644 index 0000000..7fcb2be --- /dev/null +++ b/nihil.core/next_word.ccm @@ -0,0 +1,35 @@ +// This source code is released into the public domain. +export module nihil.core:next_word; + +import nihil.std; + +import :ctype; +import :skipws; + +namespace nihil { + +// Return the next word from a string_view. Skips leading whitespace, so +// calling this repeatedly will return each word from the string. +export template +[[nodiscard]] +auto next_word(std::basic_string_view text, std::locale const &locale = std::locale()) + -> std::pair, std::basic_string_view> +{ + text = skipws(text, locale); + + auto is_space = ctype_is(std::ctype_base::space, locale); + auto split_pos = std::ranges::find_if(text, is_space); + + return {{std::ranges::begin(text), split_pos}, {split_pos, std::ranges::end(text)}}; +} + +export template +auto next_word(std::basic_string_view *text, std::locale const &locale = std::locale()) + -> std::basic_string_view +{ + auto [word, rest] = next_word(*text, locale); + *text = rest; + return word; +} + +} // namespace nihil diff --git a/nihil.core/next_word.test.cc b/nihil.core/next_word.test.cc new file mode 100644 index 0000000..2566ea6 --- /dev/null +++ b/nihil.core/next_word.test.cc @@ -0,0 +1,65 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { + +TEST_CASE("next_word: basic", "[next_word]") +{ + using namespace std::literals; + auto s = "foo bar baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == "foo"); + REQUIRE(words.second == " bar baz"); + + auto word = nihil::next_word(&s); + REQUIRE(word == "foo"); + REQUIRE(s == " bar baz"); +} + +TEST_CASE("next_word: multiple spaces", "[next_word]") +{ + using namespace std::literals; + auto s = "foo bar baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == "foo"); + REQUIRE(words.second == " bar baz"); + + auto word = nihil::next_word(&s); + REQUIRE(word == "foo"); + REQUIRE(s == " bar baz"); +} + +TEST_CASE("next_word: leading spaces", "[next_word]") +{ + using namespace std::literals; + auto s = " \tfoo bar baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == "foo"); + REQUIRE(words.second == " bar baz"); + + auto word = nihil::next_word(&s); + REQUIRE(word == "foo"); + REQUIRE(s == " bar baz"); +} + +TEST_CASE("next_word: locale", "[next_word]") +{ + using namespace std::literals; + auto s = L"\u2003foo\u2003bar\u2003baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == s); + + words = nihil::next_word(s, std::locale("C.UTF-8")); + REQUIRE(words.first == L"foo"); + REQUIRE(words.second == L"\u2003bar\u2003baz"); +} + +} // anonymous namespace diff --git a/nihil.core/nihil.core.ccm b/nihil.core/nihil.core.ccm index 0aa5402..37ad032 100644 --- a/nihil.core/nihil.core.ccm +++ b/nihil.core/nihil.core.ccm @@ -1,5 +1,20 @@ // This source code is released into the public domain. export module nihil.core; +export import :capture_stream; +export import :construct; +export import :ctype; export import :errc; +export import :error; export import :features; +export import :flagset; +export import :guard; +export import :match; +export import :monad; +export import :next_word; +export import :parse_size; +export import :save_errno; +export import :skipws; +export import :sys_error; +export import :tabulate; +export import :uuid; diff --git a/nihil.core/parse_size.ccm b/nihil.core/parse_size.ccm new file mode 100644 index 0000000..5f80755 --- /dev/null +++ b/nihil.core/parse_size.ccm @@ -0,0 +1,90 @@ +// This source code is released into the public domain. +export module nihil.core:parse_size; + +import nihil.std; + +import :ctype; +import :errc; +import :error; +import :monad; + +namespace nihil { + +export template +auto get_multiplier(Char c) -> std::expected +{ + auto ret = std::uint64_t{1}; + + // clang-format off + switch (c) { + case 'p': case 'P': ret *= 1024; // NOLINT + case 't': case 'T': ret *= 1024; // NOLINT + case 'g': case 'G': ret *= 1024; // NOLINT + case 'm': case 'M': ret *= 1024; // NOLINT + case 'k': case 'K': ret *= 1024; // NOLINT + return ret; + + default: + return error(errc::invalid_unit); + } + // clang-format on +} + +// 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 +[[nodiscard]] +auto parse_size(std::basic_string_view str) -> std::expected +{ + // 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(std::ranges::begin(str), it); + + if (num_str.empty()) + co_return error(errc::empty_string); + + auto ret = T{0}; + + for (auto c : num_str) { + if (ret > (std::numeric_limits::max() / 10)) + co_return error(std::errc::result_out_of_range); + ret *= 10; + + auto digit = static_cast(c - '0'); + if ((std::numeric_limits::max() - digit) < ret) + co_return 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 error(errc::invalid_unit); + + auto mult = co_await get_multiplier(mchar); + + if (std::cmp_greater(ret, std::numeric_limits::max() / mult)) + co_return error(std::errc::result_out_of_range); + + co_return ret *mult; +} + +export template +[[nodiscard]] auto parse_size(char const *s) +{ + return parse_size(std::string_view(s)); +} + +export template +[[nodiscard]] auto parse_size(wchar_t const *s) +{ + return parse_size(std::wstring_view(s)); +} + +} // namespace nihil diff --git a/nihil.core/parse_size.test.cc b/nihil.core/parse_size.test.cc new file mode 100644 index 0000000..6676543 --- /dev/null +++ b/nihil.core/parse_size.test.cc @@ -0,0 +1,165 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { +TEST_CASE("parse_size: empty value", "[nihil]") +{ + using namespace nihil; + + auto n = parse_size(""); + 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("1024").value(); + REQUIRE(n == 1024); + } + + SECTION ("max value, unsigned") { + auto n = parse_size("65535").value(); + REQUIRE(n == 65535); + } + + SECTION ("max value, signed") { + auto n = parse_size("32767").value(); + REQUIRE(n == 32767); + } + + SECTION ("overflow by 1, unsigned") { + auto n = parse_size("65536"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION ("overflow by 1, signed") { + auto n = parse_size("32768"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION ("overflow by many, unsigned") { + auto n = parse_size("100000"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION ("overflow by many, signed") { + auto n = parse_size("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("4z"); + REQUIRE(!n); + REQUIRE(n.error() == nihil::errc::invalid_unit); + + n = parse_size("4kz"); + REQUIRE(!n); + REQUIRE(n.error() == nihil::errc::invalid_unit); +} + +TEST_CASE("parse_size: multipliers", "[nihil]") +{ + using namespace nihil; + + auto sf = static_cast(4); + + SECTION ("k") { + auto n = parse_size("4k").value(); + REQUIRE(n == sf * 1024); + } + + SECTION ("m") { + auto n = parse_size("4m").value(); + REQUIRE(n == sf * 1024 * 1024); + } + + SECTION ("g") { + auto n = parse_size("4g").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024); + } + + SECTION ("t") { + auto n = parse_size("4t").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); + } + + SECTION ("p") { + auto n = parse_size("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("64k"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION ("unsigned") { + auto n = parse_size("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(L"1024").value(); + REQUIRE(n == 1024); + } +} + +TEST_CASE("parse_size: wide multipliers", "[nihil]") +{ + using namespace nihil; + + auto sf = static_cast(4); + + SECTION ("k") { + auto n = parse_size(L"4k").value(); + REQUIRE(n == sf * 1024); + } + + SECTION ("m") { + auto n = parse_size(L"4m").value(); + REQUIRE(n == sf * 1024 * 1024); + } + + SECTION ("g") { + auto n = parse_size(L"4g").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024); + } + + SECTION ("t") { + auto n = parse_size(L"4t").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); + } + + SECTION ("p") { + auto n = parse_size(L"4p").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024); + } +} +} // anonymous namespace diff --git a/nihil.core/save_errno.ccm b/nihil.core/save_errno.ccm new file mode 100644 index 0000000..56eafa8 --- /dev/null +++ b/nihil.core/save_errno.ccm @@ -0,0 +1,35 @@ +// This source code is released into the public domain. +module; + +#include + +export module nihil.core:save_errno; + +// save_errno: save the current value of errno and restore it when we're destroyed. +// this allows wrappers around C functions that use errno to preserve the caller's +// errno value. + +namespace nihil { + +export struct save_errno final +{ + save_errno() : m_errno(errno) {} + + ~save_errno() + { + errno = m_errno; + } + + // Not copyable + save_errno(const save_errno&) = delete; + auto operator=(const save_errno&) -> save_errno & = delete; + + // Not movable + save_errno(save_errno&&) = delete; + auto operator=(save_errno&&) -> save_errno & = delete; + +private: + int m_errno; +}; + +} // namespace nihil diff --git a/nihil.core/skipws.ccm b/nihil.core/skipws.ccm new file mode 100644 index 0000000..3901120 --- /dev/null +++ b/nihil.core/skipws.ccm @@ -0,0 +1,26 @@ +// This source code is released into the public domain. +export module nihil.core:skipws; + +import nihil.std; +import :ctype; + +namespace nihil { + +// Remove leading whitespace from a string. +export template +[[nodiscard]] +auto skipws(std::basic_string_view text, std::locale const &locale = std::locale()) + -> std::basic_string_view +{ + auto is_space = ctype_is(std::ctype_base::space, locale); + auto nonws = std::ranges::find_if_not(text, is_space); + return {nonws, std::ranges::end(text)}; +} + +export template +auto skipws(std::basic_string_view *text, std::locale const &locale = std::locale()) -> void +{ + *text = skipws(*text, locale); +} + +} // namespace nihil diff --git a/nihil.core/skipws.test.cc b/nihil.core/skipws.test.cc new file mode 100644 index 0000000..c8163ae --- /dev/null +++ b/nihil.core/skipws.test.cc @@ -0,0 +1,48 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +namespace { +TEST_CASE("skipws: basic", "[skipws]") +{ + using namespace std::literals; + + REQUIRE(nihil::skipws("foo"sv) == "foo"); + REQUIRE(nihil::skipws(" foo"sv) == "foo"); + REQUIRE(nihil::skipws("foo "sv) == "foo "); + REQUIRE(nihil::skipws("foo bar"sv) == "foo bar"); +} + +TEST_CASE("skipws: pointer", "[skipws]") +{ + using namespace std::literals; + + auto s = "foo"sv; + nihil::skipws(&s); + REQUIRE(s == "foo"); + + s = " foo"sv; + nihil::skipws(&s); + REQUIRE(s == "foo"); + + s = "foo "sv; + nihil::skipws(&s); + REQUIRE(s == "foo "); + + s = "foo bar"sv; + nihil::skipws(&s); + REQUIRE(s == "foo bar"); +} + +TEST_CASE("skipws: locale", "[skipws]") +{ + using namespace std::literals; + + // Assume the default locale is C. + REQUIRE(nihil::skipws(L"\u2003foo"sv) == L"\u2003foo"); + REQUIRE(nihil::skipws(L"\u2003foo"sv, std::locale("C.UTF-8")) == L"foo"); +} +} // anonymous namespace diff --git a/nihil.core/sys_error.ccm b/nihil.core/sys_error.ccm new file mode 100644 index 0000000..3e5911f --- /dev/null +++ b/nihil.core/sys_error.ccm @@ -0,0 +1,18 @@ +// This source code is released into the public domain. +module; + +#include + +export module nihil.core:sys_error; + +import nihil.std; + +namespace nihil { + +// Allow access to errno without having to include . +export [[nodiscard]] auto sys_error() -> std::errc +{ + return static_cast(errno); // NOLINT +} + +} // namespace nihil diff --git a/nihil.core/tabulate.ccm b/nihil.core/tabulate.ccm new file mode 100644 index 0000000..98a957c --- /dev/null +++ b/nihil.core/tabulate.ccm @@ -0,0 +1,298 @@ +// This source code is released into the public domain. +export module nihil.core:tabulate; + +import nihil.std; + +import :ctype; +import :error; + +namespace nihil { + +// tabulate: format the given range in an ASCII table and write the output +// to the given output iterator. The range's values will be converted to +// strings as if by std::format. +// +// tabulate is implemented by copying the range; this allows it to work on +// input/forward ranges at the cost of slightly increased memory use. +// +// The table spec is a string consisting of zero or more field formats, +// formatted as {flags:fieldname}; both flags and fieldname are optional. +// If there are fewer field formats than fields, the remaining fields +// are formatted as if by {:}. +// +// The following flags are supported: +// +// < left-align this column (default) +// > right-align this column + +// Exception thrown when a table spec is invalid. +export struct table_spec_error : error { + explicit table_spec_error(std::string_view what) + : error(what) + { + } +}; + +// The specification for a single field. +template +struct field_spec { + enum align_t { left, right }; + + // Get the name of this field. + auto name(this field_spec const &self) + -> std::basic_string_view + { + return self.m_name; + } + + // Set the name of this field. + auto name(this field_spec &self, + std::basic_string_view new_name) + -> void + { + self.m_name = new_name; + } + + // Set this field's alignment. + auto align(this field_spec &self, align_t new_align) -> void + { + self.m_align = new_align; + } + + // Ensure the length of this field is at least the given width. + auto ensure_width(this field_spec &self, std::size_t newwidth) + -> void + { + self.m_width = std::max(self.m_width, newwidth); + } + + // Format an object to a string based on our field spec. + [[nodiscard]] auto format(this field_spec const &, auto &&obj) + -> std::basic_string + { + auto format_string = std::basic_string{'{', '}'}; + return std::format(std::runtime_format(format_string), obj); + } + + // Print a column value to an output iterator according to our field + // spec. If is_last is true, this is the last field on the line, so + // we won't output any trailling padding. + auto print(this field_spec const &self, + std::basic_string_view value, + std::output_iterator auto &out, + bool is_last) + -> void + { + auto padding = self.m_width - value.size(); + + if (self.m_align == right) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + + std::ranges::copy(value, out); + + if (!is_last && self.m_align == left) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + } + +private: + std::basic_string_view m_name; + std::size_t m_width = 0; + align_t m_align = left; +}; + +/* + * The specification for an entire table. + */ +template +struct table_spec { + // Add a new field spec to this table. + auto add(this table_spec &self, field_spec field) -> void + { + self.m_fields.emplace_back(std::move(field)); + } + + // Return the field spec for a given field. If the field doesn't + // exist, this field and any intermediate fields will be created. + [[nodiscard]] auto field(this table_spec &self, std::size_t fieldnr) + -> field_spec & + { + if (fieldnr >= self.m_fields.size()) + self.m_fields.resize(fieldnr + 1); + return self.m_fields.at(fieldnr); + } + + // The number of columns in this table. + [[nodiscard]] auto columns(this table_spec const &self) -> std::size_t + { + return self.m_fields.size(); + } + + // Return all the fields in this table. + [[nodiscard]] auto fields(this table_spec const &self) + -> std::vector> const & + { + return self.m_fields; + } + +private: + std::vector> m_fields; +}; + +// Parse the field flags, e.g. '<'. +template Sentinel> +auto parse_field_flags(field_spec &field, Iterator &pos, Sentinel end) + -> void +{ + while (pos < end) { + switch (*pos) { + case '<': + field.align(field_spec::left); + break; + case '>': + field.align(field_spec::right); + break; + case ':': + ++pos; + /*FALLTHROUGH*/ + case '}': + return; + default: + throw table_spec_error("Invalid table spec: " + "unknown flag character"); + } + + if (++pos == end) + throw table_spec_error("Invalid table spec: " + "unterminated field"); + } +} + +// Parse a complete field spec, e.g. "{<:NAME}". +template Sentinel> +[[nodiscard]] auto parse_field(Iterator &pos, Sentinel end) + -> field_spec +{ + auto field = field_spec{}; + + if (pos == end) + throw table_spec_error("Invalid table spec: empty field"); + + // The field spec should start with a '{'. + if (*pos != '{') + throw table_spec_error("Invalid table spec: expected '{'"); + + if (++pos == end) + throw table_spec_error("Invalid table spec: unterminated field"); + + // This consumes 'pos' up to and including the ':'. + parse_field_flags(field, pos, end); + + auto brace = std::ranges::find(pos, end, '}'); + if (brace == end) + throw table_spec_error("Invalid table spec: expected '}'"); + + field.name(std::basic_string_view(pos, brace)); + pos = std::next(brace); + + // The field must be at least as wide as its header. + field.ensure_width(field.name().size()); + + return field; +} + +template +[[nodiscard]] auto parse_table_spec(std::basic_string_view spec) + -> table_spec +{ + auto table = table_spec(); + + auto pos = std::ranges::begin(spec); + auto end = std::ranges::end(spec); + + for (;;) { + // Skip leading whitespace + while (pos < end && is_c_space(*pos)) + ++pos; + + if (pos == end) + break; + + table.add(parse_field(pos, end)); + } + + return table; +} + +export template Iterator> +auto basic_tabulate(std::basic_string_view table_spec, + Range &&range, + Iterator &&out) + -> void +{ + // Parse the table spec. + auto table = parse_table_spec(table_spec); + + // Create our copy of the input data. + auto data = std::vector>>(); + // Reserve the first row for the header. + data.resize(1); + + // Find the required length of each field. + for (auto &&row : range) { + // LLVM doesn't have std::enumerate_view yet + auto i = std::size_t{0}; + auto &this_row = data.emplace_back(); + + for (auto &&column : row) { + auto &field = table.field(i); + auto &str = this_row.emplace_back(field.format(column)); + field.ensure_width(str.size()); + ++i; + } + } + + // Add the header row. + for (auto &&field : table.fields()) + data.at(0).emplace_back(std::from_range, field.name()); + + // Print the values. + for (auto &&row : data) { + for (std::size_t i = 0; i < row.size(); ++i) { + auto &field = table.field(i); + bool is_last = (i == row.size() - 1); + + field.print(row[i], out, is_last); + + if (!is_last) + *out++ = ' '; + } + + *out++ = '\n'; + } +} + +export auto tabulate(std::string_view table_spec, + std::ranges::range auto &&range, + std::output_iterator auto &&out) +{ + return basic_tabulate(table_spec, + std::forward(range), + std::forward(out)); +} + +export auto wtabulate(std::wstring_view table_spec, + std::ranges::range auto &&range, + std::output_iterator auto &&out) +{ + return basic_tabulate(table_spec, + std::forward(range), + std::forward(out)); +} + +} // namespace nihil diff --git a/nihil.core/tabulate.test.cc b/nihil.core/tabulate.test.cc new file mode 100644 index 0000000..e1ea32f --- /dev/null +++ b/nihil.core/tabulate.test.cc @@ -0,0 +1,72 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; + +using namespace std::literals; +using namespace nihil; + +namespace { +TEST_CASE("tabulate: basic", "[tabulate]") +{ + auto input = std::vector{ + std::vector{"a", "foo", "b"}, + std::vector{"bar", "c", "baz"}, + }; + + auto result = std::string(); + tabulate("{:1} {:2} {:3}", input, std::back_inserter(result)); + REQUIRE(result == +"1 2 3\n" +"a foo b\n" +"bar c baz\n"); +} + +TEST_CASE("tabulate: basic wide", "[tabulate]") +{ + auto input = std::vector{ + std::vector{L"a", L"foo", L"b"}, + std::vector{L"bar", L"c", L"baz"}, + }; + + auto result = std::wstring(); + wtabulate(L"{:1} {:2} {:3}", input, std::back_inserter(result)); + + REQUIRE(result == +L"1 2 3\n" +"a foo b\n" +"bar c baz\n"); +} + +TEST_CASE("tabulate: jagged", "[tabulate]") +{ + auto input = std::vector{ + std::vector{"a", "foo", "b"}, + std::vector{"bar", "baz"}, + }; + + auto result = std::string(); + tabulate("{:1} {:2} {:3}", input, std::back_inserter(result)); + REQUIRE(result == +"1 2 3\n" +"a foo b\n" +"bar baz\n"); +} + +TEST_CASE("tabulate: align", "[tabulate]") +{ + auto input = std::vector{ + std::vector{"a", "longvalue", "s"}, + std::vector{"a", "s", "longvalue"}, + }; + + auto result = std::string(); + tabulate("{:1} {<:2} {>:3}", input, std::back_inserter(result)); + REQUIRE(result == +"1 2 3\n" +"a longvalue s\n" +"a s longvalue\n"); +} +} // anonymous namespace diff --git a/nihil.core/uuid.ccm b/nihil.core/uuid.ccm new file mode 100644 index 0000000..b1b5d5f --- /dev/null +++ b/nihil.core/uuid.ccm @@ -0,0 +1,768 @@ +// From https://github.com/mariusbancila/stduuid +// +// Copyright (c) 2017 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +export module nihil.core:uuid; + +import nihil.std; + +namespace nihil { + +template +[[nodiscard]] constexpr auto hex2char(TChar const ch) noexcept -> unsigned char +{ + if (ch >= static_cast('0') && ch <= static_cast('9')) + return static_cast(ch - static_cast('0')); + + if (ch >= static_cast('a') && ch <= static_cast('f')) + return static_cast(10 + ch - static_cast('a')); + + if (ch >= static_cast('A') && ch <= static_cast('F')) + return static_cast(10 + ch - static_cast('A')); + + return 0; +} + +template +[[nodiscard]] constexpr auto is_hex(TChar const ch) noexcept -> bool +{ + return (ch >= static_cast('0') && ch <= static_cast('9')) || + (ch >= static_cast('a') && ch <= static_cast('f')) || + (ch >= static_cast('A') && ch <= static_cast('F')); +} + +template +[[nodiscard]] constexpr auto +to_string_view(TChar const *str) noexcept -> std::basic_string_view +{ + if (str) + return str; + return {}; +} + +template +[[nodiscard]] constexpr auto to_string_view(StringType const &str) noexcept + -> std::basic_string_view +{ + return str; +} + +struct sha1 +{ + using digest32_t = std::array; + using digest8_t = std::array; + + static constexpr unsigned int block_bytes = 64; + + sha1() + { + reset(); + } + + [[nodiscard]] static auto + left_rotate(std::uint32_t value, std::size_t const count) noexcept -> std::uint32_t + { + return (value << count) ^ (value >> (32 - count)); + } + + auto reset(this sha1 &self) noexcept -> void + { + self.m_digest[0] = 0x67452301; + self.m_digest[1] = 0xEFCDAB89; + self.m_digest[2] = 0x98BADCFE; + self.m_digest[3] = 0x10325476; + self.m_digest[4] = 0xC3D2E1F0; + self.m_blockByteIndex = 0; + self.m_byteCount = 0; + } + + auto process_byte(this sha1 &self, std::uint8_t octet) -> void + { + self.m_block.at(self.m_blockByteIndex++) = octet; + ++self.m_byteCount; + + if (self.m_blockByteIndex == block_bytes) { + self.m_blockByteIndex = 0; + self.process_block(); + } + } + + auto process_block(this sha1 &self, void const *const start, void const *const end) -> void + { + auto const *first = static_cast(start); + auto const *last = static_cast(end); + + while (first != last) { + self.process_byte(*first); + first++; + } + } + + auto process_bytes(this sha1 &self, void const *const data, std::size_t const len) -> void + { + auto *block = static_cast(data); + self.process_block(block, block + len); + } + + auto get_digest(this sha1 &self) -> digest32_t + { + auto const bit_count = self.m_byteCount * 8; + + self.process_byte(0x80); + if (self.m_blockByteIndex > 56) { + while (self.m_blockByteIndex != 0) + self.process_byte(0); + + while (self.m_blockByteIndex < 56) + self.process_byte(0); + } else { + while (self.m_blockByteIndex < 56) + self.process_byte(0); + } + + self.process_byte(0); + self.process_byte(0); + self.process_byte(0); + self.process_byte(0); + self.process_byte(static_cast((bit_count >> 24U) & 0xFFU)); + self.process_byte(static_cast((bit_count >> 16U) & 0xFFU)); + self.process_byte(static_cast((bit_count >> 8U) & 0xFFU)); + self.process_byte(static_cast((bit_count) & 0xFFU)); + + return self.m_digest; + } + + auto get_digest_bytes(this sha1 &self) -> digest8_t + { + auto d32 = self.get_digest(); + + return { + static_cast(d32[0] >> 24U), + static_cast(d32[0] >> 16U), + static_cast(d32[0] >> 8U), + static_cast(d32[0] >> 0U), + + static_cast(d32[1] >> 24U), + static_cast(d32[1] >> 16U), + static_cast(d32[1] >> 8U), + static_cast(d32[1] >> 0U), + + static_cast(d32[2] >> 24U), + static_cast(d32[2] >> 16U), + static_cast(d32[2] >> 8U), + static_cast(d32[2] >> 0U), + + static_cast(d32[3] >> 24U), + static_cast(d32[3] >> 16U), + static_cast(d32[3] >> 8U), + static_cast(d32[3] >> 0U), + + static_cast(d32[4] >> 24U), + static_cast(d32[4] >> 16U), + static_cast(d32[4] >> 8U), + static_cast(d32[4] >> 0U), + }; + } + +private: + auto process_block(this sha1 &self) -> void + { + auto w = std::array{}; + + for (std::size_t i = 0; i < 16; i++) { + w.at(i) = static_cast(self.m_block.at((i * 4) + 0)) << 24U; + w.at(i) |= static_cast(self.m_block.at((i * 4) + 1)) << 16U; + w.at(i) |= static_cast(self.m_block.at((i * 4) + 2)) << 8U; + w.at(i) |= static_cast(self.m_block.at((i * 4) + 3)); + } + + for (std::size_t i = 16; i < 80; i++) { + w.at(i) = left_rotate( + (w.at(i - 3) ^ w.at(i - 8) ^ w.at(i - 14) ^ w.at(i - 16)), 1); + } + + auto a = self.m_digest[0]; + auto b = self.m_digest[1]; + auto c = self.m_digest[2]; + auto d = self.m_digest[3]; + auto e = self.m_digest[4]; + + for (std::size_t i = 0; i < 80; ++i) { + auto f = std::uint32_t{0}; + auto k = std::uint32_t{0}; + + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + + auto temp = std::uint32_t{left_rotate(a, 5) + f + e + k + w.at(i)}; + e = d; + d = c; + c = left_rotate(b, 30); + b = a; + a = temp; + } + + self.m_digest[0] += a; + self.m_digest[1] += b; + self.m_digest[2] += c; + self.m_digest[3] += d; + self.m_digest[4] += e; + } + + digest32_t m_digest{}; + std::array m_block{}; + std::size_t m_blockByteIndex{}; + std::size_t m_byteCount{}; +}; + +template +inline constexpr std::string_view empty_guid = "00000000-0000-0000-0000-000000000000"; + +template <> +inline constexpr std::wstring_view empty_guid = L"00000000-0000-0000-0000-000000000000"; + +template +inline constexpr std::string_view guid_encoder = "0123456789abcdef"; + +template <> +inline constexpr std::wstring_view guid_encoder = L"0123456789abcdef"; + +// --------------------------------------------------------------------- +// UUID format https://tools.ietf.org/html/rfc4122 +// --------------------------------------------------------------------- + +// --------------------------------------------------------------------- +// Field NDR Data Type Octet # Note +// Note +// --------------------------------------------------------------------- +// time_low unsigned long 0 - 3 +// The low field of the timestamp. +// time_mid unsigned short 4 - 5 +// The middle field of the timestamp. +// time_hi_and_version unsigned short 6 - 7 +// The high field of the timestamp multiplexed with the version number. +// clock_seq_hi_and_reserved unsigned small 8 +// The high field of the clock sequence multiplexed with the variant. +// clock_seq_low unsigned small 9 +// The low field of the clock sequence. +// node character 10 - 15 +// The spatially unique node identifier. +// --------------------------------------------------------------------- +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | time_low | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | time_mid | time_hi_and_version | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |clk_seq_hi_res | clk_seq_low | node (0-1) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | node (2-5) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +// --------------------------------------------------------------------- +// enumerations +// --------------------------------------------------------------------- + +// indicated by a bit pattern in octet 8, marked with N in +// xxxxxxxx-xxxx-xxxx-Nxxx-xxxxxxxxxxxx +export enum struct uuid_variant : std::uint8_t { + // NCS backward compatibility (with the obsolete Apollo Network + // Computing System 1.5 UUID format). + // N bit pattern: 0xxx + // > the first 6 octets of the UUID are a 48-bit timestamp (the number + // of 4 microsecond units of time since 1 Jan 1980 UTC); + // > the next 2 octets are reserved; + // > the next octet is the "address family"; + // > the final 7 octets are a 56-bit host ID in the form specified by + // the address family + ncs, + + // RFC 4122/DCE 1.1 + // N bit pattern: 10xx + // > big-endian byte order + rfc, + + // Microsoft Corporation backward compatibility + // N bit pattern: 110x + // > little endian byte order + // > formely used in the Component Object Model (COM) library + microsoft, + + // reserved for possible future definition + // N bit pattern: 111x + reserved +}; + +// indicated by a bit pattern in octet 6, marked with M in +// xxxxxxxx-xxxx-Mxxx-xxxx-xxxxxxxxxxxx +export enum struct uuid_version : std::uint8_t { + // only possible for nil or invalid uuids + none = 0, + // The time-based version specified in RFC 4122 + time_based = 1, + // DCE Security version, with embedded POSIX UIDs. + dce_security = 2, + // The name-based version specified in RFS 4122 with MD5 hashing + name_based_md5 = 3, + // The randomly or pseudo-randomly generated version specified in RFS 4122 + random_number_based = 4, + // The name-based version specified in RFS 4122 with SHA1 hashing + name_based_sha1 = 5 +}; + +// Forward declare uuid and to_string so that we can declare to_string as a +// friend later. +export struct uuid; +export template , + typename Allocator = std::allocator> +auto to_string(uuid const &id) -> std::basic_string; + +// -------------------------------------------------------------------------------------------------------------------------- +// uuid class +// -------------------------------------------------------------------------------------------------------------------------- +export struct uuid +{ + using value_type = std::uint8_t; + + constexpr uuid() noexcept = default; + + uuid(value_type (&arr)[16]) noexcept // NOLINT + { + std::ranges::copy(arr, std::ranges::begin(data)); + } + + explicit constexpr uuid(std::array const &arr) noexcept + : data{arr} + { + } + + explicit uuid(std::span bytes) + { + std::ranges::copy(bytes, std::ranges::begin(data)); + } + + explicit uuid(std::span bytes) + { + if (bytes.size() != 16) + throw std::logic_error("wrong size for uuid"); + std::ranges::copy(bytes, std::ranges::begin(data)); + } + + template + explicit uuid(ForwardIterator first, ForwardIterator last) + { + if (std::distance(first, last) != 16) + throw std::logic_error("wrong size for uuid"); + + std::copy(first, last, std::begin(data)); + } + + [[nodiscard]] constexpr auto variant() const noexcept -> uuid_variant + { + if ((data[8] & 0x80U) == 0x00U) + return uuid_variant::ncs; + else if ((data[8] & 0xC0U) == 0x80U) + return uuid_variant::rfc; + else if ((data[8] & 0xE0U) == 0xC0U) + return uuid_variant::microsoft; + else + return uuid_variant::reserved; + } + + [[nodiscard]] constexpr auto version() const noexcept -> uuid_version + { + if ((data[6] & 0xF0U) == 0x10U) + return uuid_version::time_based; + else if ((data[6] & 0xF0U) == 0x20U) + return uuid_version::dce_security; + else if ((data[6] & 0xF0U) == 0x30U) + return uuid_version::name_based_md5; + else if ((data[6] & 0xF0U) == 0x40U) + return uuid_version::random_number_based; + else if ((data[6] & 0xF0U) == 0x50U) + return uuid_version::name_based_sha1; + else + return uuid_version::none; + } + + [[nodiscard]] constexpr auto is_nil() const noexcept -> bool + { + return std::ranges::all_of(data, [](auto i) { return i == 0; }); + } + + auto swap(uuid &other) noexcept -> void + { + data.swap(other.data); + } + + [[nodiscard]] auto as_bytes() const -> std::span + { + return std::span( + reinterpret_cast(data.data()), 16); + } + + template + [[nodiscard]] constexpr static auto is_valid_uuid(StringType const &in_str) noexcept -> bool + { + auto str = to_string_view(in_str); + auto firstDigit = true; + auto hasBraces = std::size_t{0}; + auto index = std::size_t{0}; + + if (str.empty()) + return false; + + if (str.front() == '{') + hasBraces = 1; + + if (hasBraces && str.back() != '}') + return false; + + for (std::size_t i = hasBraces; i < str.size() - hasBraces; ++i) { + if (str[i] == '-') + continue; + + if (index >= 16 || !is_hex(str[i])) + return false; + + if (firstDigit) { + firstDigit = false; + } else { + index++; + firstDigit = true; + } + } + + if (index < 16) + return false; + + return true; + } + + template + [[nodiscard]] constexpr static auto + from_string(StringType const &in_str) -> std::optional + { + auto str = to_string_view(in_str); + bool firstDigit = true; + auto hasBraces = std::size_t{0}; + auto index = std::size_t{0}; + + auto data = std::array{}; + + if (str.empty()) + return {}; + + if (str.front() == '{') + hasBraces = 1; + if (hasBraces && str.back() != '}') + return {}; + + for (std::size_t i = hasBraces; i < str.size() - hasBraces; ++i) { + if (str[i] == '-') + continue; + + if (index >= 16 || !is_hex(str[i])) { + return {}; + } + + if (firstDigit) { + data.at(index) = static_cast(hex2char(str[i]) << 4); + firstDigit = false; + } else { + data.at(index) = + static_cast(data.at(index) | hex2char(str[i])); + index++; + firstDigit = true; + } + } + + if (index < 16) { + return {}; + } + + return uuid{data}; + } + +private: + std::array data{{0}}; + + friend auto operator==(uuid const &, uuid const &) noexcept -> bool; + friend auto operator<(uuid const &, uuid const &) noexcept -> bool; + + template + friend auto operator<<(std::basic_ostream &s, uuid const &id) + -> std::basic_ostream &; + + template + friend auto to_string(uuid const &id) -> std::basic_string; + + friend std::hash; +}; + +// -------------------------------------------------------------------------------------------------------------------------- +// operators and non-member functions +// -------------------------------------------------------------------------------------------------------------------------- + +export [[nodiscard]] +auto operator==(uuid const &lhs, uuid const &rhs) noexcept -> bool +{ + return lhs.data == rhs.data; +} + +export [[nodiscard]] +auto operator!=(uuid const &lhs, uuid const &rhs) noexcept -> bool +{ + return !(lhs == rhs); +} + +export [[nodiscard]] +auto operator<(uuid const &lhs, uuid const &rhs) noexcept -> bool +{ + return lhs.data < rhs.data; +} + +export template +[[nodiscard]] auto to_string(uuid const &id) -> std::basic_string +{ + auto uustr = + std::basic_string(std::from_range, empty_guid); + + for (std::size_t i = 0, index = 0; i < 36; ++i) { + if (i == 8 || i == 13 || i == 18 || i == 23) + continue; + + uustr[i] = guid_encoder[id.data.at(index) >> 4U & 0x0FU]; + uustr[++i] = guid_encoder[id.data.at(index) & 0x0FU]; + index++; + } + + return uustr; +} + +export template +auto operator<<(std::basic_ostream &s, uuid const &id) + -> std::basic_ostream & +{ + return s << to_string(id); +} + +export auto swap(uuid &lhs, uuid &rhs) noexcept -> void +{ + lhs.swap(rhs); +} + +/*********************************************************************** + * namespace IDs that could be used for generating name-based uuids + */ + +// Name string is a fully-qualified domain name +export uuid uuid_namespace_dns{ + {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, + 0xc8} +}; + +// Name string is a URL +export uuid uuid_namespace_url{ + {0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, + 0xc8} +}; + +// Name string is an ISO OID (See https://oidref.com/, +// https://en.wikipedia.org/wiki/Object_identifier) +export uuid uuid_namespace_oid{ + {0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, + 0xc8} +}; + +// Name string is an X.500 DN, in DER or a text output format (See +// https://en.wikipedia.org/wiki/X.500, +// https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One) +export uuid uuid_namespace_x500{ + {0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, + 0xc8} +}; + +/*********************************************************************** + * uuid generators + */ + +export template +struct basic_uuid_random_generator +{ + using engine_type = UniformRandomNumberGenerator; + + explicit basic_uuid_random_generator(engine_type &gen) + : generator(&gen, [](auto) {}) + { + } + + explicit basic_uuid_random_generator(engine_type *gen) + : generator(gen, [](auto) {}) + { + } + + [[nodiscard]] auto operator()() -> uuid + { + auto bytes = std::array{}; + std::ranges::generate(bytes, [&] { return distribution(*generator); }); + + // variant must be 10xxxxxx + bytes[8] &= 0xBFU; + bytes[8] |= 0x80U; + + // version must be 0100xxxx + bytes[6] &= 0x4FU; + bytes[6] |= 0x40U; + + return uuid{std::begin(bytes), std::end(bytes)}; + } + +private: + std::uniform_int_distribution distribution; + std::shared_ptr generator; +}; + +export using uuid_random_generator = basic_uuid_random_generator; + +export struct uuid_name_generator +{ + explicit uuid_name_generator(uuid const &namespace_uuid) noexcept + : nsuuid(namespace_uuid) + { + } + + template + [[nodiscard]] auto operator()(StringType const &name) -> uuid + { + reset(); + process_characters(to_string_view(name)); + return make_uuid(); + } + +private: + auto reset() -> void + { + hasher.reset(); + + auto nsbytes = nsuuid.as_bytes(); + + auto bytes = std::array(); + std::ranges::copy(nsbytes, std::ranges::begin(bytes)); + + hasher.process_bytes(bytes.data(), bytes.size()); + } + + template + auto process_characters(std::basic_string_view const str) -> void + { + for (std::uint32_t c : str) { + hasher.process_byte(static_cast(c & 0xFFU)); + if constexpr (!std::is_same_v) { + hasher.process_byte(static_cast((c >> 8U) & 0xFFU)); + hasher.process_byte(static_cast((c >> 16U) & 0xFFU)); + hasher.process_byte(static_cast((c >> 24U) & 0xFFU)); + } + } + } + + [[nodiscard]] auto make_uuid() -> uuid + { + auto digest = hasher.get_digest_bytes(); + + // variant must be 0b10xxxxxx + digest[8] &= 0xBFU; + digest[8] |= 0x80U; + + // version must be 0b0101xxxx + digest[6] &= 0x5FU; + digest[6] |= 0x50U; + + return uuid(std::span(digest).subspan(0, 16)); + } + + uuid nsuuid; + sha1 hasher; +}; + +/* + * Create a random UUID. + */ +export auto random_uuid() -> uuid +{ + auto rd = std::random_device(); + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + + auto seq = std::seed_seq(std::ranges::begin(seed_data), std::ranges::end(seed_data)); + auto generator = std::mt19937(seq); + auto gen = uuid_random_generator{generator}; + + return gen(); +} + +} // namespace nihil + +namespace std { + +export template <> +struct hash +{ + using argument_type = nihil::uuid; + using result_type = std::size_t; + + [[nodiscard]] auto operator()(argument_type const &uuid) const noexcept -> result_type + { + auto const l = static_cast(uuid.data[0]) << 56U | + static_cast(uuid.data[1]) << 48U | + static_cast(uuid.data[2]) << 40U | + static_cast(uuid.data[3]) << 32U | + static_cast(uuid.data[4]) << 24U | + static_cast(uuid.data[5]) << 16U | + static_cast(uuid.data[6]) << 8U | + static_cast(uuid.data[7]); + + auto const h = static_cast(uuid.data[8]) << 56U | + static_cast(uuid.data[9]) << 48U | + static_cast(uuid.data[10]) << 40U | + static_cast(uuid.data[11]) << 32U | + static_cast(uuid.data[12]) << 24U | + static_cast(uuid.data[13]) << 16U | + static_cast(uuid.data[14]) << 8U | + static_cast(uuid.data[15]); + + return std::hash{}(l ^ h); + } +}; + +} // namespace std diff --git a/nihil.core/uuid.test.cc b/nihil.core/uuid.test.cc new file mode 100644 index 0000000..407f4e1 --- /dev/null +++ b/nihil.core/uuid.test.cc @@ -0,0 +1,923 @@ +/* + * From https://github.com/mariusbancila/stduuid + * + * Copyright (c) 2017 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include + +import nihil.std; +import nihil.core; + +// NOLINTBEGIN(bugprone-unchecked-optional-access) + +namespace { +// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0205r0.html +template +void seed_rng(EngineT &engine) +{ + using engine_type = typename EngineT::result_type; + using device_type = std::random_device::result_type; + using seedseq_type = std::seed_seq::result_type; + + constexpr auto bytes_needed = StateSize * sizeof(engine_type); + constexpr auto numbers_needed = (sizeof(device_type) < sizeof(seedseq_type)) + ? (bytes_needed / sizeof(device_type)) + : (bytes_needed / sizeof(seedseq_type)); + + auto numbers = std::array{}; + auto rnddev = std::random_device{}; + std::ranges::generate(numbers, std::ref(rnddev)); + + auto seedseq = std::seed_seq(std::cbegin(numbers), std::cend(numbers)); + engine.seed(seedseq); +} + +using namespace nihil; + +TEST_CASE("uuid: Test multiple default generators", "[uuid]") +{ + uuid id1; + uuid id2; + + { + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + std::mt19937 generator(seq); + + id1 = uuid_random_generator{generator}(); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::random_number_based); + REQUIRE(id1.variant() == uuid_variant::rfc); + } + + { + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + std::mt19937 generator(seq); + + id2 = uuid_random_generator{generator}(); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::random_number_based); + REQUIRE(id2.variant() == uuid_variant::rfc); + } + + REQUIRE(id1 != id2); +} + +TEST_CASE("uuid: Test default generator", "[uuid]") +{ + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + std::mt19937 generator(seq); + + uuid const guid = uuid_random_generator{generator}(); + REQUIRE(!guid.is_nil()); + REQUIRE(guid.version() == uuid_version::random_number_based); + REQUIRE(guid.variant() == uuid_variant::rfc); +} + +TEST_CASE("uuid: Test random generator (conversion ctor w/ smart ptr)", "[uuid]") +{ + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + std::mt19937 generator(seq); + + uuid_random_generator dgen(&generator); + auto id1 = dgen(); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::random_number_based); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen(); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::random_number_based); + REQUIRE(id2.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); +} + +TEST_CASE("uuid: Test random generator (conversion ctor w/ ptr)", "[uuid]") +{ + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + auto generator = std::make_unique(seq); + + uuid_random_generator dgen(generator.get()); + auto id1 = dgen(); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::random_number_based); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen(); + REQUIRE(!id1.is_nil()); + REQUIRE(id2.version() == uuid_version::random_number_based); + REQUIRE(id2.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); +} + +TEST_CASE("uuid: Test random generator (conversion ctor w/ ref)", "[uuid]") +{ + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + std::mt19937 generator(seq); + + uuid_random_generator dgen(generator); + auto id1 = dgen(); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::random_number_based); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen(); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::random_number_based); + REQUIRE(id2.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); +} + +TEST_CASE("uuid: Test basic random generator (conversion ctor w/ ptr) " + "w/ ranlux48_base", + "[uuid]") +{ + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + std::ranlux48_base generator(seq); + + basic_uuid_random_generator dgen(&generator); + auto id1 = dgen(); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::random_number_based); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen(); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::random_number_based); + REQUIRE(id2.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); +} + +TEST_CASE("uuid: Test basic random generator (conversion ctor w/ smart ptr) " + "w/ ranlux48_base", + "[uuid]") +{ + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + auto generator = std::make_unique(seq); + + basic_uuid_random_generator dgen(generator.get()); + auto id1 = dgen(); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::random_number_based); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen(); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::random_number_based); + REQUIRE(id2.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); +} + +TEST_CASE("uuid: Test basic random generator (conversion ctor w/ ref) " + "w/ ranlux48_base", + "[uuid]") +{ + std::random_device rd; + auto seed_data = std::array{}; + std::ranges::generate(seed_data, std::ref(rd)); + std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); + std::ranlux48_base generator(seq); + + basic_uuid_random_generator dgen(generator); + auto id1 = dgen(); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::random_number_based); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen(); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::random_number_based); + REQUIRE(id2.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); +} + +TEST_CASE("uuid: Test namespaces", "[uuid]") +{ + REQUIRE(uuid_namespace_dns == uuid::from_string("6ba7b810-9dad-11d1-80b4-00c04fd430c8")); + REQUIRE(uuid_namespace_url == uuid::from_string("6ba7b811-9dad-11d1-80b4-00c04fd430c8")); + REQUIRE(uuid_namespace_oid == uuid::from_string("6ba7b812-9dad-11d1-80b4-00c04fd430c8")); + REQUIRE(uuid_namespace_x500 == uuid::from_string("6ba7b814-9dad-11d1-80b4-00c04fd430c8")); +} + +TEST_CASE("uuid: Test name generator (char*)", "[uuid]") +{ + uuid_name_generator dgen(uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value()); + + auto id1 = dgen("john"); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::name_based_sha1); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen("jane"); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::name_based_sha1); + REQUIRE(id2.variant() == uuid_variant::rfc); + + auto id3 = dgen("jane"); + REQUIRE(!id3.is_nil()); + REQUIRE(id3.version() == uuid_version::name_based_sha1); + REQUIRE(id3.variant() == uuid_variant::rfc); + + auto id4 = dgen(L"jane"); + REQUIRE(!id4.is_nil()); + REQUIRE(id4.version() == uuid_version::name_based_sha1); + REQUIRE(id4.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); + REQUIRE(id2 == id3); + REQUIRE(id3 != id4); +} + +TEST_CASE("uuid: Test name generator (std::string)", "[uuid]") +{ + using namespace std::string_literals; + + uuid_name_generator dgen(uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value()); + auto id1 = dgen("john"s); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::name_based_sha1); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen("jane"s); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::name_based_sha1); + REQUIRE(id2.variant() == uuid_variant::rfc); + + auto id3 = dgen("jane"s); + REQUIRE(!id3.is_nil()); + REQUIRE(id3.version() == uuid_version::name_based_sha1); + REQUIRE(id3.variant() == uuid_variant::rfc); + + auto id4 = dgen(L"jane"s); + REQUIRE(!id4.is_nil()); + REQUIRE(id4.version() == uuid_version::name_based_sha1); + REQUIRE(id4.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); + REQUIRE(id2 == id3); + REQUIRE(id3 != id4); +} + +TEST_CASE("uuid: Test name generator (std::string_view)", "[uuid]") +{ + using namespace std::string_view_literals; + + uuid_name_generator dgen(uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value()); + auto id1 = dgen("john"sv); + REQUIRE(!id1.is_nil()); + REQUIRE(id1.version() == uuid_version::name_based_sha1); + REQUIRE(id1.variant() == uuid_variant::rfc); + + auto id2 = dgen("jane"sv); + REQUIRE(!id2.is_nil()); + REQUIRE(id2.version() == uuid_version::name_based_sha1); + REQUIRE(id2.variant() == uuid_variant::rfc); + + auto id3 = dgen("jane"sv); + REQUIRE(!id3.is_nil()); + REQUIRE(id3.version() == uuid_version::name_based_sha1); + REQUIRE(id3.variant() == uuid_variant::rfc); + + auto id4 = dgen(L"jane"sv); + REQUIRE(!id4.is_nil()); + REQUIRE(id4.version() == uuid_version::name_based_sha1); + REQUIRE(id4.variant() == uuid_variant::rfc); + + REQUIRE(id1 != id2); + REQUIRE(id2 == id3); + REQUIRE(id3 != id4); +} + +TEST_CASE("uuid: Test name generator equality (char const*, std::string, " + "std::string_view)", + "[uuid]") +{ + using namespace std::literals; + + auto dgen = uuid_name_generator( + uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value()); + auto id1 = dgen("john"); + auto id2 = dgen("john"s); + auto id3 = dgen("john"sv); + + REQUIRE(id1 == id2); + REQUIRE(id2 == id3); +} + +TEST_CASE("uuid: Test default constructor", "[uuid]") +{ + auto empty = uuid(); + REQUIRE(empty.is_nil()); +} + +TEST_CASE("uuid: Test string conversion", "[uuid]") +{ + auto empty = uuid(); + REQUIRE(to_string(empty) == "00000000-0000-0000-0000-000000000000"); + REQUIRE(to_string(empty) == L"00000000-0000-0000-0000-000000000000"); +} + +TEST_CASE("uuid: Test is_valid_uuid(char*)", "[uuid]") +{ + REQUIRE(uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43")); + REQUIRE(uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43}")); + REQUIRE(uuid::is_valid_uuid(L"47183823-2574-4bfd-b411-99ed177d3e43")); + REQUIRE(uuid::is_valid_uuid(L"{47183823-2574-4bfd-b411-99ed177d3e43}")); + REQUIRE(uuid::is_valid_uuid("00000000-0000-0000-0000-000000000000")); + REQUIRE(uuid::is_valid_uuid("{00000000-0000-0000-0000-000000000000}")); + REQUIRE(uuid::is_valid_uuid(L"00000000-0000-0000-0000-000000000000")); + REQUIRE(uuid::is_valid_uuid(L"{00000000-0000-0000-0000-000000000000}")); +} + +TEST_CASE("uuid: Test is_valid_uuid(basic_string)", "[uuid]") +{ + using namespace std::string_literals; + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e43"s; + REQUIRE(uuid::is_valid_uuid(str)); + } + + { + auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}"s; + REQUIRE(uuid::is_valid_uuid(str)); + } + + { + auto str = L"47183823-2574-4bfd-b411-99ed177d3e43"s; + REQUIRE(uuid::is_valid_uuid(str)); + } + + { + auto str = L"{47183823-2574-4bfd-b411-99ed177d3e43}"s; + REQUIRE(uuid::is_valid_uuid(str)); + } + + { + auto str = "00000000-0000-0000-0000-000000000000"s; + REQUIRE(uuid::is_valid_uuid(str)); + } + + { + auto str = "{00000000-0000-0000-0000-000000000000}"s; + REQUIRE(uuid::is_valid_uuid(str)); + } + + { + auto str = L"00000000-0000-0000-0000-000000000000"s; + REQUIRE(uuid::is_valid_uuid(str)); + } + + { + auto str = L"{00000000-0000-0000-0000-000000000000}"s; + REQUIRE(uuid::is_valid_uuid(str)); + } +} + +TEST_CASE("uuid: Test is_valid_uuid(basic_string_view)", "[uuid]") +{ + using namespace std::string_view_literals; + + REQUIRE(uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43"sv)); + REQUIRE(uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43}"sv)); + REQUIRE(uuid::is_valid_uuid(L"47183823-2574-4bfd-b411-99ed177d3e43"sv)); + REQUIRE(uuid::is_valid_uuid(L"{47183823-2574-4bfd-b411-99ed177d3e43}"sv)); + REQUIRE(uuid::is_valid_uuid("00000000-0000-0000-0000-000000000000"sv)); + REQUIRE(uuid::is_valid_uuid("{00000000-0000-0000-0000-000000000000}"sv)); + REQUIRE(uuid::is_valid_uuid(L"00000000-0000-0000-0000-000000000000"sv)); + REQUIRE(uuid::is_valid_uuid(L"{00000000-0000-0000-0000-000000000000}"sv)); +} + +TEST_CASE("uuid: Test is_valid_uuid(char*) invalid format", "[uuid]") +{ + REQUIRE(!uuid::is_valid_uuid("")); + REQUIRE(!uuid::is_valid_uuid("{}")); + REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e4")); + REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e430")); + REQUIRE(!uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43")); + REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43}")); +} + +TEST_CASE("uuid: Test is_valid_uuid(basic_string) invalid format", "[uuid]") +{ + using namespace std::string_literals; + + { + auto str = ""s; + REQUIRE(!uuid::is_valid_uuid(str)); + } + + { + auto str = "{}"s; + REQUIRE(!uuid::is_valid_uuid(str)); + } + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e4"s; + REQUIRE(!uuid::is_valid_uuid(str)); + } + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e430"s; + REQUIRE(!uuid::is_valid_uuid(str)); + } + + { + auto str = "{47183823-2574-4bfd-b411-99ed177d3e43"s; + REQUIRE(!uuid::is_valid_uuid(str)); + } + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e43}"s; + REQUIRE(!uuid::is_valid_uuid(str)); + } +} + +TEST_CASE("uuid: Test is_valid_uuid(basic_string_view) invalid format", "[uuid]") +{ + using namespace std::string_view_literals; + + REQUIRE(!uuid::is_valid_uuid(""sv)); + REQUIRE(!uuid::is_valid_uuid("{}"sv)); + REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e4"sv)); + REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e430"sv)); + REQUIRE(!uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43"sv)); + REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43}"sv)); +} + +TEST_CASE("uuid: Test from_string(char*)", "[uuid]") +{ + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e43"; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == str); + } + + { + auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}"; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"); + } + + { + auto guid = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value(); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"); + REQUIRE(to_string(guid) == L"47183823-2574-4bfd-b411-99ed177d3e43"); + } + + { + auto str = L"47183823-2574-4bfd-b411-99ed177d3e43"; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == str); + } + + { + auto str = "4718382325744bfdb41199ed177d3e43"; + REQUIRE_NOTHROW(uuid::from_string(str)); + REQUIRE(uuid::from_string(str).has_value()); + } + + { + auto str = "00000000-0000-0000-0000-000000000000"; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = "{00000000-0000-0000-0000-000000000000}"; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = L"00000000-0000-0000-0000-000000000000"; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = L"{00000000-0000-0000-0000-000000000000}"; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } +} + +TEST_CASE("uuid: Test from_string(basic_string)", "[uuid]") +{ + using namespace std::string_literals; + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e43"s; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == str); + } + + { + auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}"s; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"); + } + + { + auto guid = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43"s).value(); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"); + REQUIRE(to_string(guid) == L"47183823-2574-4bfd-b411-99ed177d3e43"); + } + + { + auto str = L"47183823-2574-4bfd-b411-99ed177d3e43"s; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == str); + } + + { + auto str = "4718382325744bfdb41199ed177d3e43"s; + REQUIRE_NOTHROW(uuid::from_string(str)); + REQUIRE(uuid::from_string(str).has_value()); + } + + { + auto str = "00000000-0000-0000-0000-000000000000"s; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = "{00000000-0000-0000-0000-000000000000}"s; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = L"00000000-0000-0000-0000-000000000000"s; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = L"{00000000-0000-0000-0000-000000000000}"s; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } +} + +TEST_CASE("uuid: Test from_string(basic_string_view)", "[uuid]") +{ + using namespace std::string_view_literals; + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e43"sv; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == str); + } + + { + auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}"sv; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"); + } + + { + auto guid = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43"sv).value(); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"); + REQUIRE(to_string(guid) == L"47183823-2574-4bfd-b411-99ed177d3e43"); + } + + { + auto str = L"47183823-2574-4bfd-b411-99ed177d3e43"sv; + auto guid = uuid::from_string(str).value(); + REQUIRE(to_string(guid) == str); + } + + { + auto str = "4718382325744bfdb41199ed177d3e43"sv; + REQUIRE_NOTHROW(uuid::from_string(str)); + REQUIRE(uuid::from_string(str).has_value()); + } + + { + auto str = "00000000-0000-0000-0000-000000000000"sv; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = "{00000000-0000-0000-0000-000000000000}"sv; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = L"00000000-0000-0000-0000-000000000000"sv; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } + + { + auto str = L"{00000000-0000-0000-0000-000000000000}"sv; + auto guid = uuid::from_string(str).value(); + REQUIRE(guid.is_nil()); + } +} + +TEST_CASE("uuid: Test constexpr from_string", "[uuid]") +{ + constexpr uuid value = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value(); + static_assert(!value.is_nil()); + static_assert(value.variant() == uuid_variant::rfc); + static_assert(value.version() != uuid_version::none); +} + +TEST_CASE("uuid: Test from_string(char*) invalid format", "[uuid]") +{ + REQUIRE(!uuid::from_string("").has_value()); + REQUIRE(!uuid::from_string("{}").has_value()); + REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e4").has_value()); + REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e430").has_value()); + REQUIRE(!uuid::from_string("{47183823-2574-4bfd-b411-99ed177d3e43").has_value()); + REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43}").has_value()); +} + +TEST_CASE("uuid: Test from_string(basic_string) invalid format", "[uuid]") +{ + using namespace std::string_literals; + + { + auto str = ""s; + REQUIRE(!uuid::from_string(str).has_value()); + } + + { + auto str = "{}"s; + REQUIRE(!uuid::from_string(str).has_value()); + } + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e4"s; + REQUIRE(!uuid::from_string(str).has_value()); + } + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e430"s; + REQUIRE(!uuid::from_string(str).has_value()); + } + + { + auto str = "{47183823-2574-4bfd-b411-99ed177d3e43"s; + REQUIRE(!uuid::from_string(str).has_value()); + } + + { + auto str = "47183823-2574-4bfd-b411-99ed177d3e43}"s; + REQUIRE(!uuid::from_string(str).has_value()); + } +} + +TEST_CASE("uuid: Test from_string(basic_string_view) invalid format", "[uuid]") +{ + using namespace std::string_view_literals; + + REQUIRE(!uuid::from_string(""sv).has_value()); + REQUIRE(!uuid::from_string("{}"sv).has_value()); + REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e4"sv).has_value()); + REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e430"sv).has_value()); + REQUIRE(!uuid::from_string("{47183823-2574-4bfd-b411-99ed177d3e43"sv).has_value()); + REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43}"sv).has_value()); +} + +TEST_CASE("uuid: Test iterators constructor", "[uuid]") +{ + using namespace std::string_literals; + + { + std::array arr{ + {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed, + 0x17, 0x7d, 0x3e, 0x43} + }; + + auto const guid = uuid(std::begin(arr), std::end(arr)); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s); + } + + { + uuid::value_type arr[16] = {// NOLINT + 0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, + 0xb4, 0x11, 0x99, 0xed, 0x17, 0x7d, 0x3e, 0x43}; + + auto const guid = uuid(std::begin(arr), std::end(arr)); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s); + } +} + +TEST_CASE("uuid: Test array constructors", "[uuid]") +{ + using namespace std::string_literals; + + { + auto const guid = uuid { + {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed, + 0x17, 0x7d, 0x3e, 0x43} + }; + + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s); + } + + { + auto arr = std::array{ + {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed, + 0x17, 0x7d, 0x3e, 0x43} + }; + + auto const guid = uuid(arr); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s); + } + + { + uuid::value_type arr[16]{// NOLINT + 0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, + 0xb4, 0x11, 0x99, 0xed, 0x17, 0x7d, 0x3e, 0x43}; + + auto const guid = uuid(arr); + REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s); + } +} + +TEST_CASE("uuid: Test equality", "[uuid]") +{ + uuid empty; + + auto engine = uuid_random_generator::engine_type{}; + seed_rng(engine); + uuid guid = uuid_random_generator{engine}(); + + REQUIRE(empty == empty); + REQUIRE(guid == guid); + REQUIRE(empty != guid); +} + +TEST_CASE("Test comparison", "[uuid]") +{ + auto empty = uuid{}; + + auto engine = uuid_random_generator::engine_type{}; + seed_rng(engine); + + auto gen = uuid_random_generator{engine}; + auto id = gen(); + + REQUIRE(empty < id); + + auto ids = std::set{uuid{}, gen(), gen(), gen(), gen()}; + + REQUIRE(ids.size() == 5); + REQUIRE(ids.contains(uuid{}) == true); +} + +TEST_CASE("uuid: Test hashing", "[uuid]") +{ + using namespace std::string_literals; + + auto str = "47183823-2574-4bfd-b411-99ed177d3e43"s; + auto guid = uuid::from_string(str).value(); + + auto h1 = std::hash{}; + auto h2 = std::hash{}; + REQUIRE(h1(str) != h2(guid)); + + auto engine = uuid_random_generator::engine_type{}; + seed_rng(engine); + uuid_random_generator gen{engine}; + + std::unordered_set ids{uuid{}, gen(), gen(), gen(), gen()}; + + REQUIRE(ids.size() == 5); + REQUIRE(ids.find(uuid{}) != ids.end()); +} + +TEST_CASE("uuid: Test swap", "[uuid]") +{ + uuid empty; + + auto engine = uuid_random_generator::engine_type{}; + seed_rng(engine); + uuid guid = uuid_random_generator{engine}(); + + REQUIRE(empty.is_nil()); + REQUIRE(!guid.is_nil()); + + std::swap(empty, guid); + + REQUIRE(!empty.is_nil()); + REQUIRE(guid.is_nil()); + + empty.swap(guid); + + REQUIRE(empty.is_nil()); + REQUIRE(!guid.is_nil()); +} + +TEST_CASE("uuid: Test constexpr", "[uuid]") +{ + constexpr uuid empty; + static_assert(empty.is_nil()); + static_assert(empty.variant() == uuid_variant::ncs); + static_assert(empty.version() == uuid_version::none); +} + +TEST_CASE("uuid: Test size", "[uuid]") +{ + REQUIRE(sizeof(uuid) == 16); +} + +TEST_CASE("uuid: Test assignment", "[uuid]") +{ + auto id1 = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value(); + auto id2 = id1; + REQUIRE(id1 == id2); + + id1 = uuid::from_string("{fea43102-064f-4444-adc2-02cec42623f8}").value(); + REQUIRE(id1 != id2); + + auto id3 = std::move(id2); + REQUIRE(to_string(id3) == "47183823-2574-4bfd-b411-99ed177d3e43"); +} + +TEST_CASE("uuid: Test trivial", "[uuid]") +{ + REQUIRE(std::is_trivially_copyable_v); +} + +TEST_CASE("uuid: Test as_bytes", "[uuid]") +{ + std::array arr{ + {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed, 0x17, 0x7d, + 0x3e, 0x43} + }; + + { + uuid id{arr}; + REQUIRE(!id.is_nil()); + + auto view = id.as_bytes(); + REQUIRE(memcmp(view.data(), arr.data(), arr.size()) == 0); + } + + { + const uuid id{arr}; + REQUIRE(!id.is_nil()); + + auto view = id.as_bytes(); + REQUIRE(memcmp(view.data(), arr.data(), arr.size()) == 0); + } +} +} // anonymous namespace + +// NOLINTEND(bugprone-unchecked-optional-access) -- cgit v1.2.3