From 034cd404a129103a8dd7747e6bd00ffd5550da93 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Mon, 30 Jun 2025 07:51:23 +0100 Subject: refactoring --- .clang-format | 36 +- .clang-tidy | 33 ++ clang-tidy.conf | 8 - nihil.generator/CMakeLists.txt | 7 +- nihil.generator/byte_allocator.ccm | 27 ++ nihil.generator/coroutine_traits.ccm | 70 +++ nihil.generator/forward.ccm | 29 ++ nihil.generator/generator.ccm | 717 +++++++++++------------------ nihil.generator/generator.test.cc | 56 +++ nihil.generator/generator_promise.ccm | 65 +++ nihil.generator/generator_promise_base.ccm | 205 +++++++++ nihil.generator/manual_lifetime.ccm | 8 +- nihil.generator/nihil.generator.ccm | 7 +- nihil.generator/test.cc | 56 --- nihil.guard/guard.ccm | 19 +- nihil.posix/CMakeLists.txt | 30 +- nihil.posix/argv.cc | 65 --- nihil.posix/argv.ccm | 97 ++-- nihil.posix/argv.test.cc | 256 ++++++++++ nihil.posix/ensure_dir.cc | 30 -- nihil.posix/ensure_dir.ccm | 16 +- nihil.posix/exec.cc | 22 - nihil.posix/exec.ccm | 53 --- nihil.posix/execl.ccm | 40 ++ nihil.posix/execl.test.cc | 55 +++ nihil.posix/execlp.ccm | 18 +- nihil.posix/execlp.test.cc | 67 +++ nihil.posix/execshell.ccm | 21 + nihil.posix/execshell.test.cc | 55 +++ nihil.posix/execv.cc | 42 -- nihil.posix/execv.ccm | 38 +- nihil.posix/execv.test.cc | 55 +++ nihil.posix/execvp.cc | 50 -- nihil.posix/execvp.ccm | 28 +- nihil.posix/fd.cc | 220 --------- nihil.posix/fd.ccm | 278 ++++++++--- nihil.posix/fd.test.cc | 205 +++++++++ nihil.posix/fexecv.cc | 47 -- nihil.posix/fexecv.ccm | 33 +- nihil.posix/fexecvp.ccm | 37 ++ nihil.posix/find_in_path.cc | 52 --- nihil.posix/find_in_path.ccm | 43 +- nihil.posix/getenv.cc | 60 --- nihil.posix/getenv.ccm | 55 ++- nihil.posix/getenv.test.cc | 50 ++ nihil.posix/open.cc | 31 -- nihil.posix/open.ccm | 85 +++- nihil.posix/open_in_path.cc | 51 -- nihil.posix/open_in_path.ccm | 46 +- nihil.posix/posix.ccm | 16 +- nihil.posix/process.cc | 102 ---- nihil.posix/process.ccm | 132 ++++-- nihil.posix/read_file.ccm | 2 +- nihil.posix/rename.cc | 34 -- nihil.posix/rename.ccm | 21 +- nihil.posix/spawn.ccm | 15 +- nihil.posix/tempfile.cc | 128 ----- nihil.posix/tempfile.ccm | 162 +++++-- nihil.posix/tempfile.test.cc | 90 ++++ nihil.posix/test.fd.cc | 204 -------- nihil.posix/test.getenv.cc | 50 -- nihil.posix/test.spawn.cc | 40 -- nihil.posix/test.tempfile.cc | 90 ---- nihil.posix/write_file.ccm | 2 +- 64 files changed, 2527 insertions(+), 2235 deletions(-) create mode 100644 .clang-tidy delete mode 100644 clang-tidy.conf create mode 100644 nihil.generator/byte_allocator.ccm create mode 100644 nihil.generator/coroutine_traits.ccm create mode 100644 nihil.generator/forward.ccm create mode 100644 nihil.generator/generator.test.cc create mode 100644 nihil.generator/generator_promise.ccm create mode 100644 nihil.generator/generator_promise_base.ccm delete mode 100644 nihil.generator/test.cc delete mode 100644 nihil.posix/argv.cc create mode 100644 nihil.posix/argv.test.cc delete mode 100644 nihil.posix/ensure_dir.cc delete mode 100644 nihil.posix/exec.cc delete mode 100644 nihil.posix/exec.ccm create mode 100644 nihil.posix/execl.ccm create mode 100644 nihil.posix/execl.test.cc create mode 100644 nihil.posix/execlp.test.cc create mode 100644 nihil.posix/execshell.ccm create mode 100644 nihil.posix/execshell.test.cc delete mode 100644 nihil.posix/execv.cc create mode 100644 nihil.posix/execv.test.cc delete mode 100644 nihil.posix/execvp.cc delete mode 100644 nihil.posix/fd.cc create mode 100644 nihil.posix/fd.test.cc delete mode 100644 nihil.posix/fexecv.cc create mode 100644 nihil.posix/fexecvp.ccm delete mode 100644 nihil.posix/find_in_path.cc delete mode 100644 nihil.posix/getenv.cc create mode 100644 nihil.posix/getenv.test.cc delete mode 100644 nihil.posix/open.cc delete mode 100644 nihil.posix/open_in_path.cc delete mode 100644 nihil.posix/process.cc delete mode 100644 nihil.posix/rename.cc delete mode 100644 nihil.posix/tempfile.cc create mode 100644 nihil.posix/tempfile.test.cc delete mode 100644 nihil.posix/test.fd.cc delete mode 100644 nihil.posix/test.getenv.cc delete mode 100644 nihil.posix/test.tempfile.cc diff --git a/.clang-format b/.clang-format index aeed635..390acb9 100644 --- a/.clang-format +++ b/.clang-format @@ -4,9 +4,41 @@ PPIndentWidth: -1 UseTab: AlignWithSpaces IndentPPDirectives: AfterHash ColumnLimit: 100 +AccessModifierOffset: -8 +IndentRequiresClause: false +RequiresClausePosition: OwnLine +ContinuationIndentWidth: 8 -SeparateDefinitionBlocks: Always +ReflowComments: Always + +SeparateDefinitionBlocks: Leave +AllowShortFunctionsOnASingleLine: None + +BreakConstructorInitializers: BeforeComma +ConstructorInitializerIndentWidth: 8 + +BreakAfterReturnType: Automatic +PenaltyReturnTypeOnItsOwnLine: 0 +BreakTemplateDeclarations: Yes + +BracedInitializerIndentWidth: 8 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Never + AfterEnum: true + AfterFunction: true + AfterNamespace: false + AfterStruct: true + AfterUnion: true + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + SplitEmptyNamespace: true + SplitEmptyRecord: true NamespaceIndentation: None FixNamespaceComments: true -WrapNamespaceBodyWithEmptyLines: Always +#WrapNamespaceBodyWithEmptyLines: Always diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..c84c230 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,33 @@ +--- +WarningsAsErrors: '*' +Checks: > + -*, + clang-analyzer-*, + bugprone-*, + -bugprone-reserved-identifier, + -bugprone-easily-swappable-parameters, + cert-*, + concurrency-*, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-do-while, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-reference-coroutine-parameters, + -cppcoreguidelines-pro-type-*, + hicpp-*, + -hicpp-braces-around-statements, + -hicpp-named-parameter, + -hicpp-vararg, + misc-*, + modernize-*, + readability-*, + -readability-braces-around-statements, + -readability-convert-member-functions-to-static, + -readability-magic-numbers, + -readability-named-parameter, + -readability-identifier-length, + -readability-function-cognitive-complexity, + performance-*, + +CheckOptions: + performance-unnecessary-copy-initialization.AllowedTypes: 'nihil::open_flags;tempfile_flags' + performance-unnecessary-value-param.AllowedTypes: 'nihil::open_flags;tempfile_flags' diff --git a/clang-tidy.conf b/clang-tidy.conf deleted file mode 100644 index 92a59bc..0000000 --- a/clang-tidy.conf +++ /dev/null @@ -1,8 +0,0 @@ ---- -WarningsAsErrors: '*' -Checks: > - -*, - bugprone-*, - -bugprone-reserved-identifier, - -bugprone-easily-swappable-parameters, - modernize-*, diff --git a/nihil.generator/CMakeLists.txt b/nihil.generator/CMakeLists.txt index 7464785..e521159 100644 --- a/nihil.generator/CMakeLists.txt +++ b/nihil.generator/CMakeLists.txt @@ -6,7 +6,12 @@ target_sources(nihil.generator nihil.generator.ccm generator.ccm + byte_allocator.ccm + coroutine_traits.ccm elements_of.ccm + forward.ccm + generator_promise_base.ccm + generator_promise.ccm manual_lifetime.ccm promise_base_alloc.ccm util.ccm @@ -15,7 +20,7 @@ target_sources(nihil.generator if(NIHIL_TESTS) enable_testing() - add_executable(nihil.generator.test test.cc) + add_executable(nihil.generator.test generator.test.cc) target_link_libraries(nihil.generator.test PRIVATE nihil.generator Catch2::Catch2WithMain diff --git a/nihil.generator/byte_allocator.ccm b/nihil.generator/byte_allocator.ccm new file mode 100644 index 0000000..6d46ec6 --- /dev/null +++ b/nihil.generator/byte_allocator.ccm @@ -0,0 +1,27 @@ +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +module; + +#include +#include + +export module nihil.generator:byte_allocator; + +namespace nihil { + +template +using byte_allocator_t = typename std::allocator_traits< + std::remove_cvref_t>::template rebind_alloc; + +} // namespace nihil diff --git a/nihil.generator/coroutine_traits.ccm b/nihil.generator/coroutine_traits.ccm new file mode 100644 index 0000000..2a9d51d --- /dev/null +++ b/nihil.generator/coroutine_traits.ccm @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +module; + +#include +#include + +export module nihil.generator:coroutine_traits; + +import :byte_allocator; +import :forward; +import :generator_promise; + +namespace std { + +// Type-erased allocator with default allocator behaviour. +export template +struct coroutine_traits, Args...> +{ + using promise_type = + nihil::generator_promise, std::allocator>; +}; + +// Type-erased allocator with std::allocator_arg parameter +export template +struct coroutine_traits, allocator_arg_t, Alloc, Args...> +{ +private: + using byte_allocator = nihil::byte_allocator_t; + +public: + using promise_type = nihil::generator_promise, byte_allocator, + true /*explicit Allocator*/>; +}; + +// Type-erased allocator with std::allocator_arg parameter (non-static member functions) +export template +struct coroutine_traits, This, allocator_arg_t, Alloc, Args...> +{ +private: + using byte_allocator = nihil::byte_allocator_t; + +public: + using promise_type = nihil::generator_promise, byte_allocator, + true /*explicit Allocator*/>; +}; + +// Generator with specified allocator type +export template +struct coroutine_traits, Args...> +{ + using byte_allocator = nihil::byte_allocator_t; + +public: + using promise_type = + nihil::generator_promise, byte_allocator>; +}; + +} // namespace std diff --git a/nihil.generator/forward.ccm b/nihil.generator/forward.ccm new file mode 100644 index 0000000..8d5ca4d --- /dev/null +++ b/nihil.generator/forward.ccm @@ -0,0 +1,29 @@ +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +module; + +#include + +export module nihil.generator:forward; + +import :util; + +namespace nihil { + +export template , + typename Allocator = use_allocator_arg> +class generator; + +} // namespace nihil diff --git a/nihil.generator/generator.ccm b/nihil.generator/generator.ccm index 27e8103..96790a8 100644 --- a/nihil.generator/generator.ccm +++ b/nihil.generator/generator.ccm @@ -23,485 +23,306 @@ module; export module nihil.generator:generator; +import :byte_allocator; +import :coroutine_traits; import :elements_of; +import :forward; +import :generator_promise_base; +import :generator_promise; import :manual_lifetime; import :promise_base_alloc; import :util; namespace nihil { -export template , - typename _Allocator = use_allocator_arg> -class generator; - - -template -struct __generator_promise_base +// TODO : make layout compatible promise casts possible +export template +class generator { - template - friend class generator; - - __generator_promise_base* __root_; - std::coroutine_handle<> __parentOrLeaf_; - // Note: Using manual_lifetime here to avoid extra calls to exception_ptr - // constructor/destructor in cases where it is not needed (i.e. where this - // generator coroutine is not used as a nested coroutine). - // This member is lazily constructed by the __yield_sequence_awaiter::await_suspend() - // method if this generator is used as a nested generator. - manual_lifetime __exception_; - manual_lifetime<_Ref> __value_; - - explicit __generator_promise_base(std::coroutine_handle<> thisCoro) noexcept - : __root_(this) - , __parentOrLeaf_(thisCoro) - {} - - ~__generator_promise_base() { - if (__root_ != this) { - // This coroutine was used as a nested generator and so will - // have constructed its __exception_ member which needs to be - // destroyed here. - __exception_.destruct(); - } - } - - std::suspend_always initial_suspend() noexcept { - return {}; - } - - void return_void() noexcept {} - - void unhandled_exception() { - if (__root_ != this) { - __exception_.get() = std::current_exception(); - } else { - throw; - } - } - - // Transfers control back to the parent of a nested coroutine - struct __final_awaiter { - bool await_ready() noexcept { - return false; - } - - template - std::coroutine_handle<> - await_suspend(std::coroutine_handle<_Promise> __h) noexcept { - _Promise& __promise = __h.promise(); - __generator_promise_base& __root = *__promise.__root_; - if (&__root != &__promise) { - auto __parent = __promise.__parentOrLeaf_; - __root.__parentOrLeaf_ = __parent; - return __parent; - } - return std::noop_coroutine(); - } - - void await_resume() noexcept {} - }; - - __final_awaiter final_suspend() noexcept { - return {}; - } - - std::suspend_always yield_value(_Ref&& __x) - noexcept(std::is_nothrow_move_constructible_v<_Ref>) { - __root_->__value_.construct((_Ref&&)__x); - return {}; - } - - template - requires - (!std::is_reference_v<_Ref>) && - std::is_convertible_v<_T, _Ref> - std::suspend_always yield_value(_T&& __x) - noexcept(std::is_nothrow_constructible_v<_Ref, _T>) { - __root_->__value_.construct((_T&&)__x); - return {}; - } - - template - struct __yield_sequence_awaiter { - _Gen __gen_; - - __yield_sequence_awaiter(_Gen&& __g) noexcept - // Taking ownership of the generator ensures frame are destroyed - // in the reverse order of their execution. - : __gen_((_Gen&&)__g) { - } - - bool await_ready() noexcept { - return false; - } - - // set the parent, root and exceptions pointer and - // resume the nested - template - std::coroutine_handle<> - await_suspend(std::coroutine_handle<_Promise> __h) noexcept { - __generator_promise_base& __current = __h.promise(); - __generator_promise_base& __nested = *__gen_.__get_promise(); - __generator_promise_base& __root = *__current.__root_; - - __nested.__root_ = __current.__root_; - __nested.__parentOrLeaf_ = __h; - - // Lazily construct the __exception_ member here now that we - // know it will be used as a nested generator. This will be - // destroyed by the promise destructor. - __nested.__exception_.construct(); - __root.__parentOrLeaf_ = __gen_.__get_coro(); - - // Immediately resume the nested coroutine (nested generator) - return __gen_.__get_coro(); - } - - void await_resume() { - __generator_promise_base& __nestedPromise = *__gen_.__get_promise(); - if (__nestedPromise.__exception_.get()) { - std::rethrow_exception(std::move(__nestedPromise.__exception_.get())); - } - } - }; - - template - __yield_sequence_awaiter> - yield_value(nihil::elements_of> __g) noexcept { - return std::move(__g).get(); - } - - template - __yield_sequence_awaiter, _Allocator>> - yield_value(nihil::elements_of<_Rng, _Allocator> && __x) { - return [](std::allocator_arg_t, _Allocator, auto && __rng) -> generator<_Ref, std::remove_cvref_t<_Ref>, _Allocator> { - for(auto && e: __rng) - co_yield static_cast(e); - }(std::allocator_arg, __x.get_allocator(), std::forward<_Rng>(__x.get())); - } - - void resume() { - __parentOrLeaf_.resume(); - } - - // Disable use of co_await within this coroutine. - void await_transform() = delete; -}; + using byte_allocator = byte_allocator_t; -template -struct __generator_promise; - -template -struct __generator_promise, _ByteAllocator, _ExplicitAllocator> final - : public __generator_promise_base<_Ref> - , public promise_base_alloc<_ByteAllocator> { - __generator_promise() noexcept - : __generator_promise_base<_Ref>(std::coroutine_handle<__generator_promise>::from_promise(*this)) - {} - - generator<_Ref, _Value, _Alloc> get_return_object() noexcept { - return generator<_Ref, _Value, _Alloc>{ - std::coroutine_handle<__generator_promise>::from_promise(*this) - }; - } - - using __generator_promise_base<_Ref>::yield_value; - - template - typename __generator_promise_base<_Ref>::template __yield_sequence_awaiter> - yield_value(nihil::elements_of<_Rng> && __x) { - static_assert (!_ExplicitAllocator, - "This coroutine has an explicit allocator specified with std::allocator_arg so an allocator needs to be passed " - "explicitely to std::elements_of"); - return [](auto && __rng) -> generator<_Ref, _Value, _Alloc> { - for(auto && e: __rng) - co_yield static_cast(e); - }(std::forward<_Rng>(__x.get())); - } -}; - -template -using __byte_allocator_t = typename std::allocator_traits>::template rebind_alloc; - -} // namespace nihil - -namespace std { - -// Type-erased allocator with default allocator behaviour. -export template -struct coroutine_traits, _Args...> { - using promise_type = nihil::__generator_promise, std::allocator>; -}; - -// Type-erased allocator with std::allocator_arg parameter -export template -struct coroutine_traits, allocator_arg_t, _Alloc, _Args...> { -private: - using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; public: - using promise_type = nihil::__generator_promise, __byte_allocator, true /*explicit Allocator*/>; -}; + using promise_type = generator_promise, byte_allocator>; + friend promise_type; -// Type-erased allocator with std::allocator_arg parameter (non-static member functions) -export template -struct coroutine_traits, _This, allocator_arg_t, _Alloc, _Args...> { private: - using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; -public: - using promise_type = nihil::__generator_promise, __byte_allocator, true /*explicit Allocator*/>; -}; + using coroutine_handle = std::coroutine_handle; -// Generator with specified allocator type -export template -struct coroutine_traits, _Args...> { - using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; public: - using promise_type = nihil::__generator_promise, __byte_allocator>; -}; - -} // namespace std - -namespace nihil { + generator() noexcept = default; + + generator(generator &&other) noexcept + : m_coro(std::exchange(other.m_coro, {})) + , m_started(std::exchange(other.m_started, false)) + { + } + + ~generator() noexcept + { + if (m_coro) { + if (m_started && !m_coro.done()) { + m_coro.promise().__value_.destruct(); + } + m_coro.destroy(); + } + } + + auto operator=(generator &&g) noexcept -> generator & + { + swap(g); + return *this; + } + + void swap(generator &other) noexcept + { + std::swap(m_coro, other.m_coro); + std::swap(m_started, other.m_started); + } + + struct sentinel + { + }; + + struct iterator + { + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = Value; + using reference = Ref; + using pointer = std::add_pointer_t; + + iterator() noexcept = default; + iterator(const iterator &) = delete; + + iterator(iterator &&other) noexcept + : m_coro(std::exchange(other.m_coro, {})) + { + } + + auto operator=(iterator &&other) noexcept -> iterator & + { + std::swap(m_coro, other.m_coro); + return *this; + } + + ~iterator() = default; + + friend auto operator==(const iterator &it, sentinel) noexcept -> bool + { + return it.m_coro.done(); + } + + auto operator++() -> iterator & + { + m_coro.promise().m_value.destruct(); + m_coro.promise().resume(); + return *this; + } + + void operator++(int) + { + (void)operator++(); + } + + auto operator*() const noexcept -> reference + { + return static_cast(m_coro.promise().m_value.get()); + } + + private: + friend generator; + + explicit iterator(coroutine_handle coro) noexcept + : m_coro(coro) + { + } + + coroutine_handle m_coro; + }; + + auto begin() -> iterator + { + assert(m_coro); + assert(!m_started); + m_started = true; + m_coro.resume(); + return iterator{m_coro}; + } + + auto end() noexcept -> sentinel + { + return {}; + } -// TODO : make layout compatible promise casts possible -export template -class generator { - using __byte_allocator = __byte_allocator_t<_Alloc>; -public: - using promise_type = __generator_promise, __byte_allocator>; - friend promise_type; private: - using __coroutine_handle = std::coroutine_handle; -public: - - generator() noexcept = default; - - generator(generator&& __other) noexcept - : __coro_(std::exchange(__other.__coro_, {})) - , __started_(std::exchange(__other.__started_, false)) { - } - - ~generator() noexcept { - if (__coro_) { - if (__started_ && !__coro_.done()) { - __coro_.promise().__value_.destruct(); - } - __coro_.destroy(); - } - } - - generator& operator=(generator && g) noexcept { - swap(g); - return *this; - } - - void swap(generator& __other) noexcept { - std::swap(__coro_, __other.__coro_); - std::swap(__started_, __other.__started_); - } - - struct sentinel {}; - - class iterator { - public: - using iterator_category = std::input_iterator_tag; - using difference_type = std::ptrdiff_t; - using value_type = _Value; - using reference = _Ref; - using pointer = std::add_pointer_t<_Ref>; - - iterator() noexcept = default; - iterator(const iterator &) = delete; - - iterator(iterator&& __other) noexcept - : __coro_(std::exchange(__other.__coro_, {})) { - } - - iterator& operator=(iterator&& __other) { - std::swap(__coro_, __other.__coro_); - return *this; - } - - ~iterator() { - } - - friend bool operator==(const iterator &it, sentinel) noexcept { - return it.__coro_.done(); - } - - iterator &operator++() { - __coro_.promise().__value_.destruct(); - __coro_.promise().resume(); - return *this; - } - void operator++(int) { - (void)operator++(); - } - - reference operator*() const noexcept { - return static_cast(__coro_.promise().__value_.get()); - } - - private: - friend generator; - - explicit iterator(__coroutine_handle __coro) noexcept - : __coro_(__coro) {} - - __coroutine_handle __coro_; - }; - - iterator begin() { - assert(__coro_); - assert(!__started_); - __started_ = true; - __coro_.resume(); - return iterator{__coro_}; - } - - sentinel end() noexcept { - return {}; - } - -private: - explicit generator(__coroutine_handle __coro) noexcept - : __coro_(__coro) { - } + explicit generator(coroutine_handle coro) noexcept + : m_coro(coro) + { + } public: // to get around access restrictions for __yield_sequence_awaitable - std::coroutine_handle<> __get_coro() noexcept { return __coro_; } - promise_type* __get_promise() noexcept { return std::addressof(__coro_.promise()); } + auto get_coro() noexcept -> std::coroutine_handle<> + { + return m_coro; + } + auto get_promise() noexcept -> promise_type * + { + return std::addressof(m_coro.promise()); + } private: - __coroutine_handle __coro_; - bool __started_ = false; + coroutine_handle m_coro; + bool m_started = false; }; // Specialisation for type-erased allocator implementation. -export template -class generator<_Ref, _Value, use_allocator_arg> { - using __promise_base = __generator_promise_base<_Ref>; -public: +export template +class generator +{ + using promise_base = generator_promise_base; - generator() noexcept - : __promise_(nullptr) - , __coro_() - , __started_(false) - {} - - generator(generator&& __other) noexcept - : __promise_(std::exchange(__other.__promise_, nullptr)) - , __coro_(std::exchange(__other.__coro_, {})) - , __started_(std::exchange(__other.__started_, false)) { - } - - ~generator() noexcept { - if (__coro_) { - if (__started_ && !__coro_.done()) { - __promise_->__value_.destruct(); - } - __coro_.destroy(); - } - } - - generator& operator=(generator g) noexcept { - swap(g); - return *this; - } - - void swap(generator& __other) noexcept { - std::swap(__promise_, __other.__promise_); - std::swap(__coro_, __other.__coro_); - std::swap(__started_, __other.__started_); - } - - struct sentinel {}; - - class iterator { - public: - using iterator_category = std::input_iterator_tag; - using difference_type = std::ptrdiff_t; - using value_type = _Value; - using reference = _Ref; - using pointer = std::add_pointer_t<_Ref>; - - iterator() noexcept = default; - iterator(const iterator &) = delete; - - iterator(iterator&& __other) noexcept - : __promise_(std::exchange(__other.__promise_, nullptr)) - , __coro_(std::exchange(__other.__coro_, {})) - {} - - iterator& operator=(iterator&& __other) { - __promise_ = std::exchange(__other.__promise_, nullptr); - __coro_ = std::exchange(__other.__coro_, {}); - return *this; - } - - ~iterator() = default; - - friend bool operator==(const iterator &it, sentinel) noexcept { - return it.__coro_.done(); - } - - iterator& operator++() { - __promise_->__value_.destruct(); - __promise_->resume(); - return *this; - } - - void operator++(int) { - (void)operator++(); - } - - reference operator*() const noexcept { - return static_cast(__promise_->__value_.get()); - } - - private: - friend generator; - - explicit iterator(__promise_base* __promise, std::coroutine_handle<> __coro) noexcept - : __promise_(__promise) - , __coro_(__coro) - {} - - __promise_base* __promise_; - std::coroutine_handle<> __coro_; - }; - - iterator begin() { - assert(__coro_); - assert(!__started_); - __started_ = true; - __coro_.resume(); - return iterator{__promise_, __coro_}; - } - - sentinel end() noexcept { - return {}; - } +public: + generator() noexcept + : m_promise(nullptr) + { + } + + generator(generator &&other) noexcept + : m_promise(std::exchange(other.m_promise, nullptr)) + , m_coro(std::exchange(other.m_coro, {})) + , m_started(std::exchange(other.m_started, false)) + { + } + + ~generator() noexcept + { + if (m_coro) { + if (m_started && !m_coro.done()) { + m_promise->m_value.destruct(); + } + m_coro.destroy(); + } + } + + auto operator=(generator g) noexcept -> generator & + { + swap(g); + return *this; + } + + void swap(generator &other) noexcept + { + std::swap(m_promise, other.m_promise); + std::swap(m_coro, other.m_coro); + std::swap(m_started, other.m_started); + } + + struct sentinel + { + }; + + struct iterator + { + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = Value; + using reference = Ref; + using pointer = std::add_pointer_t; + + iterator() noexcept = default; + iterator(const iterator &) = delete; + + iterator(iterator &&other) noexcept + : m_promise(std::exchange(other.m_promise, nullptr)) + , m_coro(std::exchange(other.m_coro, {})) + { + } + + auto operator=(iterator &&other) noexcept -> iterator & + { + m_promise = std::exchange(other.m_promise, nullptr); + m_coro = std::exchange(other.m_coro, {}); + return *this; + } + + ~iterator() = default; + + friend auto operator==(const iterator &it, sentinel) noexcept -> bool + { + return it.m_coro.done(); + } + + auto operator++() -> iterator & + { + m_promise->m_value.destruct(); + m_promise->resume(); + return *this; + } + + void operator++(int) + { + (void)operator++(); + } + + auto operator*() const noexcept -> reference + { + return static_cast(m_promise->m_value.get()); + } + + private: + friend generator; + + explicit iterator(promise_base *promise, + std::coroutine_handle<> coro) noexcept + : m_promise(promise) + , m_coro(coro) + { + } + + promise_base *m_promise; + std::coroutine_handle<> m_coro; + }; + + auto begin() -> iterator + { + assert(m_coro); + assert(!m_started); + m_started = true; + m_coro.resume(); + return iterator{m_promise, m_coro}; + } + + auto end() noexcept -> sentinel + { + return {}; + } private: - template - friend struct __generator_promise; + template + friend struct generator_promise; - template - explicit generator(std::coroutine_handle<_Promise> __coro) noexcept - : __promise_(std::addressof(__coro.promise())) - , __coro_(__coro) - {} + template + explicit generator(std::coroutine_handle coro) noexcept + : m_promise(std::addressof(coro.promise())) + , m_coro(coro) + { + } public: // to get around access restrictions for __yield_sequence_awaitable - std::coroutine_handle<> __get_coro() noexcept { return __coro_; } - __promise_base* __get_promise() noexcept { return __promise_; } + auto get_coro() noexcept -> std::coroutine_handle<> + { + return m_coro; + } + + auto get_promise() noexcept -> promise_base * + { + return m_promise; + } private: - __promise_base* __promise_; - std::coroutine_handle<> __coro_; - bool __started_ = false; + promise_base *m_promise; + std::coroutine_handle<> m_coro; + bool m_started = false; }; } // namespace nihil diff --git a/nihil.generator/generator.test.cc b/nihil.generator/generator.test.cc new file mode 100644 index 0000000..49272b4 --- /dev/null +++ b/nihil.generator/generator.test.cc @@ -0,0 +1,56 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +import nihil.generator; + +TEST_CASE("generator: basic", "[generator]") +{ + auto fn = [] () -> nihil::generator { + co_yield 1; + co_yield 2; + co_yield 3; + }; + + auto values = std::vector(); + std::ranges::copy(fn(), std::back_inserter(values)); + + REQUIRE(values == std::vector{1, 2, 3}); +} + +TEST_CASE("generator: exceptions", "[generator]") +{ + auto fn = [] () -> nihil::generator { + co_yield 1; + throw std::runtime_error("test"); + }; + + auto range = fn(); + auto it = std::ranges::begin(range); + REQUIRE(*it == 1); + REQUIRE_THROWS_AS(it++, std::runtime_error); +} + +TEST_CASE("generator: elements_of", "[generator]") +{ + auto fn1 = [] -> nihil::generator { + co_yield 1; + co_yield 2; + co_yield 3; + }; + + auto fn2 = [&fn1] -> nihil::generator { + co_yield nihil::elements_of(fn1()); + }; + + auto values = std::vector(); + std::ranges::copy(fn2(), std::back_inserter(values)); + + REQUIRE(values == std::vector{1, 2, 3}); +} diff --git a/nihil.generator/generator_promise.ccm b/nihil.generator/generator_promise.ccm new file mode 100644 index 0000000..b0fd4b1 --- /dev/null +++ b/nihil.generator/generator_promise.ccm @@ -0,0 +1,65 @@ +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +module; + +#include +#include + +export module nihil.generator:generator_promise; + +import :forward; +import :generator_promise_base; +import :promise_base_alloc; + +namespace nihil { + +export template +struct generator_promise; + +export template +struct generator_promise, ByteAllocator, ExplicitAllocator> final + : public generator_promise_base, + public promise_base_alloc +{ + generator_promise() noexcept + : generator_promise_base( + std::coroutine_handle::from_promise(*this)) + { + } + + auto get_return_object() noexcept -> generator + { + return generator{ + std::coroutine_handle::from_promise(*this)}; + } + + using generator_promise_base::yield_value; + + template + auto yield_value(nihil::elements_of &&x) -> typename generator_promise_base< + Ref>::template yield_sequence_awaiter> + { + static_assert(!ExplicitAllocator, + "This coroutine has an explicit allocator specified with " + "std::allocator_arg so an allocator needs to be passed " + "explicitely to std::elements_of"); + return [](auto &&rng) -> generator { + for (auto &&e : rng) + co_yield static_cast(e); + }(std::forward(x.get())); + } +}; + +} // namespace nihil diff --git a/nihil.generator/generator_promise_base.ccm b/nihil.generator/generator_promise_base.ccm new file mode 100644 index 0000000..fec9b1b --- /dev/null +++ b/nihil.generator/generator_promise_base.ccm @@ -0,0 +1,205 @@ +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +module; + +#include +#include +#include + +export module nihil.generator:generator_promise_base; + +import :elements_of; +import :forward; +import :manual_lifetime; + +namespace nihil { + +template +struct generator_promise_base +{ + template + friend class generator; + + generator_promise_base *m_root; + std::coroutine_handle<> m_parent_or_leaf; + + // Note: Using manual_lifetime here to avoid extra calls to exception_ptr + // constructor/destructor in cases where it is not needed (i.e. where this + // generator coroutine is not used as a nested coroutine). + // This member is lazily constructed by the __yield_sequence_awaiter::await_suspend() + // method if this generator is used as a nested generator. + manual_lifetime m_exception; + manual_lifetime m_value; + + explicit generator_promise_base(std::coroutine_handle<> this_coro) noexcept + : m_root(this) + , m_parent_or_leaf(this_coro) + { + } + + ~generator_promise_base() + { + if (m_root != this) { + // This coroutine was used as a nested generator and so will + // have constructed its __exception_ member which needs to be + // destroyed here. + m_exception.destruct(); + } + } + + auto initial_suspend() noexcept -> std::suspend_always + { + return {}; + } + + auto return_void() noexcept -> void + { + } + + auto unhandled_exception() -> void + { + if (m_root != this) + m_exception.get() = std::current_exception(); + else + throw; + } + + // Transfers control back to the parent of a nested coroutine + struct final_awaiter + { + auto await_ready() noexcept -> bool + { + return false; + } + + template + auto + await_suspend(std::coroutine_handle h) noexcept -> std::coroutine_handle<> + { + Promise &promise = h.promise(); + generator_promise_base &root = *promise.m_root; + if (&root != &promise) { + auto parent = promise.m_parent_or_leaf; + root.m_parent_or_leaf = parent; + return parent; + } + return std::noop_coroutine(); + } + + void await_resume() noexcept + { + } + }; + + auto final_suspend() noexcept -> final_awaiter + { + return {}; + } + + auto yield_value(Ref &&x) noexcept(std::is_nothrow_move_constructible_v) + -> std::suspend_always + { + m_root->m_value.construct((Ref &&)x); + return {}; + } + + template + requires(!std::is_reference_v) && std::is_convertible_v + auto + yield_value(T &&x) noexcept(std::is_nothrow_constructible_v) -> std::suspend_always + { + m_root->m_value.construct((T &&)x); + return {}; + } + + template + struct yield_sequence_awaiter + { + Gen m_gen; + + yield_sequence_awaiter(Gen &&g) noexcept + // Taking ownership of the generator ensures frame are destroyed + // in the reverse order of their execution. + : m_gen((Gen &&)g) + { + } + + auto await_ready() noexcept -> bool + { + return false; + } + + // set the parent, root and exceptions pointer and + // resume the nested + template + auto + await_suspend(std::coroutine_handle h) noexcept -> std::coroutine_handle<> + { + generator_promise_base ¤t = h.promise(); + generator_promise_base &nested = *m_gen.get_promise(); + generator_promise_base &root = *current.m_root; + + nested.m_root = current.m_root; + nested.m_parent_or_leaf = h; + + // Lazily construct the __exception_ member here now that we + // know it will be used as a nested generator. This will be + // destroyed by the promise destructor. + nested.m_exception.construct(); + root.m_parent_or_leaf = m_gen.get_coro(); + + // Immediately resume the nested coroutine (nested generator) + return m_gen.get_coro(); + } + + void await_resume() + { + generator_promise_base &nested_promise = *m_gen.get_promise(); + + if (nested_promise.m_exception.get()) { + std::rethrow_exception(nested_promise.m_exception.get()); + } + } + }; + + template + auto yield_value(nihil::elements_of> g) noexcept + -> yield_sequence_awaiter> + + { + return std::move(g).get(); + } + + template + auto yield_value(nihil::elements_of &&x) + -> yield_sequence_awaiter, Allocator>> + + { + return [](std::allocator_arg_t, Allocator, + auto &&rng) -> generator, Allocator> { + for (auto &&e : rng) + co_yield static_cast(e); + }(std::allocator_arg, x.get_allocator(), std::forward(x.get())); + } + + void resume() + { + m_parent_or_leaf.resume(); + } + + // Disable use of co_await within this coroutine. + void await_transform() = delete; +}; + +} // namespace nihil diff --git a/nihil.generator/manual_lifetime.ccm b/nihil.generator/manual_lifetime.ccm index d249e99..963e6c9 100644 --- a/nihil.generator/manual_lifetime.ccm +++ b/nihil.generator/manual_lifetime.ccm @@ -68,8 +68,8 @@ private: template class manual_lifetime { - manual_lifetime() noexcept {} - ~manual_lifetime() {} + manual_lifetime() noexcept = default; + ~manual_lifetime() = default; auto construct(this manual_lifetime &self, T &value) noexcept -> T & { @@ -92,8 +92,8 @@ private: template class manual_lifetime { - manual_lifetime() noexcept {} - ~manual_lifetime() {} + manual_lifetime() noexcept = default; + ~manual_lifetime() = default; auto construct(this manual_lifetime &self, T &&value) noexcept -> T && { diff --git a/nihil.generator/nihil.generator.ccm b/nihil.generator/nihil.generator.ccm index 9eec5b4..ac9076b 100644 --- a/nihil.generator/nihil.generator.ccm +++ b/nihil.generator/nihil.generator.ccm @@ -17,16 +17,19 @@ module; export module nihil.generator; +export import :byte_allocator; +export import :coroutine_traits; export import :elements_of; +export import :forward; export import :generator; +export import :generator_promise_base; export import :manual_lifetime; export import :promise_base_alloc; export import :util; -export namespace std::ranges { +export namespace std::ranges { // NOLINT template constexpr inline bool enable_view> = true; } // namespace std::ranges - diff --git a/nihil.generator/test.cc b/nihil.generator/test.cc deleted file mode 100644 index 49272b4..0000000 --- a/nihil.generator/test.cc +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include -#include - -#include - -import nihil.generator; - -TEST_CASE("generator: basic", "[generator]") -{ - auto fn = [] () -> nihil::generator { - co_yield 1; - co_yield 2; - co_yield 3; - }; - - auto values = std::vector(); - std::ranges::copy(fn(), std::back_inserter(values)); - - REQUIRE(values == std::vector{1, 2, 3}); -} - -TEST_CASE("generator: exceptions", "[generator]") -{ - auto fn = [] () -> nihil::generator { - co_yield 1; - throw std::runtime_error("test"); - }; - - auto range = fn(); - auto it = std::ranges::begin(range); - REQUIRE(*it == 1); - REQUIRE_THROWS_AS(it++, std::runtime_error); -} - -TEST_CASE("generator: elements_of", "[generator]") -{ - auto fn1 = [] -> nihil::generator { - co_yield 1; - co_yield 2; - co_yield 3; - }; - - auto fn2 = [&fn1] -> nihil::generator { - co_yield nihil::elements_of(fn1()); - }; - - auto values = std::vector(); - std::ranges::copy(fn2(), std::back_inserter(values)); - - REQUIRE(values == std::vector{1, 2, 3}); -} diff --git a/nihil.guard/guard.ccm b/nihil.guard/guard.ccm index 7b6cf66..84ff401 100644 --- a/nihil.guard/guard.ccm +++ b/nihil.guard/guard.ccm @@ -1,7 +1,4 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include @@ -13,19 +10,15 @@ export module nihil.guard; 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. - */ +// 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. - guard(F func) : m_func(std::move(func)) {} + 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. - */ + // We are being destroyed, so call the callable. + // If the callable throws, std::terminate() will be called. ~guard() { if (m_func) diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt index b04018e..61e83df 100644 --- a/nihil.posix/CMakeLists.txt +++ b/nihil.posix/CMakeLists.txt @@ -10,13 +10,15 @@ target_sources(nihil.posix argv.ccm ensure_dir.ccm - exec.ccm + execl.ccm execlp.ccm + execshell.ccm execv.ccm execvp.ccm executor.ccm fd.ccm fexecv.ccm + fexecvp.ccm find_in_path.ccm getenv.ccm open.ccm @@ -27,31 +29,21 @@ target_sources(nihil.posix spawn.ccm tempfile.ccm write_file.ccm - - PRIVATE - argv.cc - ensure_dir.cc - exec.cc - execv.cc - execvp.cc - getenv.cc - fd.cc - find_in_path.cc - open.cc - open_in_path.cc - process.cc - rename.cc - tempfile.cc ) if(NIHIL_TESTS) enable_testing() add_executable(nihil.posix.test - test.fd.cc - test.getenv.cc + argv.test.cc + execl.test.cc + execlp.test.cc + execshell.test.cc + execv.test.cc + fd.test.cc + getenv.test.cc test.spawn.cc - test.tempfile.cc + tempfile.test.cc ) target_link_libraries(nihil.posix.test PRIVATE diff --git a/nihil.posix/argv.cc b/nihil.posix/argv.cc deleted file mode 100644 index e6b1389..0000000 --- a/nihil.posix/argv.cc +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -module nihil.posix; - -namespace nihil { - -argv::argv() = default; -argv::argv(argv &&) noexcept = default; -auto argv::operator=(this argv &, argv &&) -> argv & = default; - -argv::~argv() -{ - for (auto *arg : m_args) - delete[] arg; -} - -auto argv::data(this argv const &self) -> char const * const * -{ - return self.m_args.data(); -} - -auto argv::data(this argv &self) -> char * const * -{ - return self.m_args.data(); -} - -auto argv::size(this argv const &self) -{ - return self.m_args.size(); -} - -auto argv::begin(this argv const &self) -{ - return self.m_args.begin(); -} - -auto argv::end(this argv const &self) -{ - return self.m_args.end(); -} - - -auto argv::add_arg(this argv &self, std::string_view arg) -> void -{ - // Create a nul-terminated C string. - auto ptr = std::make_unique(arg.size() + 1); - std::ranges::copy(arg, ptr.get()); - ptr[arg.size()] = '\0'; - - // Ensure we won't throw when emplacing the pointer. - self.m_args.reserve(self.m_args.size() + 1); - self.m_args.emplace_back(ptr.release()); -} - -} // namespace nihil - diff --git a/nihil.posix/argv.ccm b/nihil.posix/argv.ccm index 6f60f4b..de75770 100644 --- a/nihil.posix/argv.ccm +++ b/nihil.posix/argv.ccm @@ -16,19 +16,35 @@ namespace nihil { /* * argv: stores a null-terminated array of nul-terminated C strings. * argv::data() is suitable for passing to ::execv(). - * - * Create an argv using argv::from_range(), which takes a range of - * string-like objects. */ -export struct argv { +export struct argv +{ + using value_type = char *; + using iterator = value_type const *; + using const_iterator = value_type const *; + /* * Create a new argv from a range. + * + * Delegate to the default constructor to ensure the pointers in + * m_args are deleted if we throw when allocating. */ argv(std::from_range_t, std::ranges::range auto &&args) + : argv() { - for (auto &&arg : args) - add_arg(std::string_view(arg)); + for (auto &&arg : args) { + auto str = std::string_view(arg); + + // Create a nul-terminated C string. + auto ptr = std::make_unique(str.size() + 1); // NOLINT + std::ranges::copy(str, ptr.get()); + ptr[str.size()] = '\0'; + + // Ensure we won't throw when emplacing the pointer. + m_args.reserve(m_args.size() + 1); + m_args.emplace_back(ptr.release()); + } m_args.push_back(nullptr); } @@ -36,43 +52,68 @@ export struct argv { /* * Create an argv from an initializer list. */ - template - explicit argv(std::initializer_list &&args) - : argv(std::from_range, std::forward(args)) + template + argv(std::initializer_list const &args) + : argv(std::from_range, args) { } + template + argv(std::initializer_list &&args) + : argv(std::from_range, std::move(args)) + { + } + + // Destructor. + ~argv() + { + for (auto *arg : m_args) + delete[] arg; // NOLINT + } + // Movable. - argv(argv &&) noexcept; - auto operator=(this argv &, argv &&other) -> argv &; + argv(argv &&) noexcept = default; + auto operator=(argv &&other) noexcept -> argv & = default; // Not copyable. TODO: for completeness, it probably should be. argv(argv const &) = delete; - auto operator=(this argv &, argv const &other) -> argv& = delete; - - ~argv(); + auto operator=(argv const &other) -> argv & = delete; // Access the stored arguments. - [[nodiscard]] auto data(this argv const &self) -> char const * const *; - [[nodiscard]] auto data(this argv &self) -> char * const *; - [[nodiscard]] auto size(this argv const &self); + [[nodiscard]] auto data(this argv const &self) -> value_type const * + { + return self.m_args.data(); + } + + [[nodiscard]] auto size(this argv const &self) -> std::size_t + { + return self.m_args.size(); + } + + // Access an element by index. Throws std::out_of_range if the index is out of range. + [[nodiscard]] auto operator[](this argv const &self, std::size_t index) -> value_type + { + return self.m_args.at(index); + } // Range access - [[nodiscard]] auto begin(this argv const &self); - [[nodiscard]] auto end(this argv const &self); + [[nodiscard]] auto begin(this argv const &self) -> const_iterator + { + return self.data(); + } + + [[nodiscard]] auto end(this argv const &self) -> const_iterator + { + return self.data() + self.size(); // NOLINT + } private: - // Use the from_range() factory method to create new instances. - argv(); + // Private since default-constructing an argv isn't useful. + argv() = default; - // The argument pointers, including the null terminator. - // This can't be a vector because we need an array of - // char pointers to pass to exec. + // The argument pointers, including the null terminator. This can't be a + // vector because we need an array of char pointers to pass to exec. std::vector m_args; - - // Add a new argument to the array. - auto add_arg(this argv &self, std::string_view arg) -> void; }; } // namespace nihil - diff --git a/nihil.posix/argv.test.cc b/nihil.posix/argv.test.cc new file mode 100644 index 0000000..3cc218d --- /dev/null +++ b/nihil.posix/argv.test.cc @@ -0,0 +1,256 @@ +/* + * This source code is released into the public domain + */ + +#include +#include +#include +#include + +#include + +import nihil.posix; + +namespace { +constexpr auto *test_tags = "[nihil][nihil.posix][nihil.posix.argv]"; + +TEST_CASE("nihil.posix: argv: invariants", test_tags) +{ + static_assert(std::movable); + static_assert(std::move_constructible); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + static_assert(!std::copy_constructible); + + static_assert(std::destructible); + + static_assert(std::swappable); + static_assert(std::is_nothrow_swappable_v); + + static_assert(std::constructible_from>); + static_assert(std::constructible_from>); + static_assert(std::constructible_from>); + static_assert(std::constructible_from>); + + static_assert(std::ranges::sized_range); + static_assert(std::ranges::contiguous_range); +} + +SCENARIO("nihil::argv::size() works") +{ + GIVEN("An argv constructed from 3 elements") + { + auto argv = nihil::argv{"one", "two", "three"}; + + THEN("size() returns 4") + { + REQUIRE(std::ranges::size(argv) == 4); + } + } +} + +SCENARIO("nihil::argv::operator[] works") +{ + using namespace std::literals; + + GIVEN("An argv constructed from 3 elements") + { + auto argv = nihil::argv{"one", "two", "three"}; + + THEN("operator[] returns the expected elements") + { + REQUIRE(argv[0] == "one"sv); + REQUIRE(argv[1] == "two"sv); + REQUIRE(argv[2] == "three"sv); + REQUIRE(argv[3] == nullptr); + } + + AND_THEN("operator[] on a non-existent element throws std::out_of_range") + { + REQUIRE_THROWS_AS(argv[4], std::out_of_range); + } + } +} + +SCENARIO("nihil::argv can be constructed from an initializer_list") +{ + using namespace std::literals; + + GIVEN("An argv constructed from an initializer_list") + { + auto argv = nihil::argv{"one", "two", "three"}; + + THEN("size() returns 4") + { + REQUIRE(std::ranges::size(argv) == 4); + } + + AND_THEN("The stored values match the initializer_list") + { + REQUIRE(argv[0] == "one"sv); + REQUIRE(argv[1] == "two"sv); + REQUIRE(argv[2] == "three"sv); + REQUIRE(argv[3] == nullptr); + } + } +} + +SCENARIO("nihil::argv can be constructed from an initializer_list") +{ + using namespace std::literals; + + GIVEN("An argv constructed from an initializer_list") + { + auto argv = nihil::argv{"one"sv, "two"sv, "three"sv}; + + THEN("size() returns 4") + { + REQUIRE(std::ranges::size(argv) == 4); + } + + AND_THEN("The stored values match the initializer_list") + { + REQUIRE(argv[0] == "one"sv); + REQUIRE(argv[1] == "two"sv); + REQUIRE(argv[2] == "three"sv); + REQUIRE(argv[3] == nullptr); + } + } +} + +SCENARIO("nihil::argv can be constructed from a range of std::string") +{ + using namespace std::literals; + + GIVEN("An argv constructed from an initializer_list") + { + auto vec = std::vector{"one"s, "two"s, "three"s}; + auto argv = nihil::argv(std::from_range, vec); + + THEN("size() returns 4") + { + REQUIRE(std::ranges::size(argv) == 4); + } + + AND_THEN("The stored values match the range") + { + REQUIRE(argv[0] == "one"sv); + REQUIRE(argv[1] == "two"sv); + REQUIRE(argv[2] == "three"sv); + REQUIRE(argv[3] == nullptr); + } + } +} + +SCENARIO("nihil::data() returns the correct data") +{ + using namespace std::literals; + + GIVEN("An argv") + { + auto argv = nihil::argv{"one", "two", "three"}; + + THEN("The values in data() match the provided data") + { + REQUIRE(argv.data()[0] == "one"sv); + REQUIRE(argv.data()[1] == "two"sv); + REQUIRE(argv.data()[2] == "three"sv); + REQUIRE(argv.data()[3] == nullptr); + } + } +} + +SCENARIO("nihil::argv can be used as a range") +{ + using namespace std::literals; + + GIVEN("An argv") + { + auto argv = nihil::argv{"one"sv, "two"sv, "three"sv}; + + WHEN("An std::vector is constructed from the argv") + { + auto vec = std::vector(std::from_range, argv); + + THEN("The argv and the vector contain the same data") + { + REQUIRE(std::ranges::equal(argv, vec)); + } + } + + WHEN("The argv is copied to an std::vector using a range-based for") + { + auto vec = std::vector(); + + for (auto &&arg: argv) + vec.push_back(arg); + + THEN("The argv and the vector contain the same data") + { + REQUIRE(std::ranges::equal(argv, vec)); + } + } + } +} + +SCENARIO("nihil::argv can be move-constructed") +{ + using namespace std::literals; + + GIVEN("An argv object") + { + auto argv = nihil::argv{"one", "two", "three"}; + + WHEN("The argv is moved") + { + auto argv2 = std::move(argv); + + THEN("The new object contains the data") + { + REQUIRE(argv2.size() == 4); + REQUIRE(argv2[0] == "one"sv); + } + + AND_THEN("The old object does not") + { + REQUIRE(argv.size() == 0); + } + } + } +} + +SCENARIO("nihil::argv can be move-assigned") +{ + using namespace std::literals; + + GIVEN("Two argv objects") + { + auto argv1 = nihil::argv{"one", "two", "three"}; + auto argv2 = nihil::argv{"x", "y"}; + + WHEN("argv2 is move-assigned to argv1") + { + argv1 = std::move(argv2); + + THEN("argv1 contains the data from argv2") + { + REQUIRE(argv1.size() == 3); + REQUIRE(argv1[0] == "x"sv); + REQUIRE(argv1[1] == "y"sv); + REQUIRE(argv1[2] == nullptr); + } + + AND_THEN("argv2 is empty") + { + REQUIRE(argv2.size() == 0); + } + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/ensure_dir.cc b/nihil.posix/ensure_dir.cc deleted file mode 100644 index 88e8898..0000000 --- a/nihil.posix/ensure_dir.cc +++ /dev/null @@ -1,30 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -module nihil.posix; - -import nihil.error; - -namespace nihil { - -auto ensure_dir(std::filesystem::path const &dir) -> std::expected -{ - auto err = std::error_code(); - - std::filesystem::create_directories(dir, err); - - if (err) - return std::unexpected(error(err)); - - return {}; -} - -} // namespace nihil diff --git a/nihil.posix/ensure_dir.ccm b/nihil.posix/ensure_dir.ccm index fa92a90..7eecea8 100644 --- a/nihil.posix/ensure_dir.ccm +++ b/nihil.posix/ensure_dir.ccm @@ -6,6 +6,7 @@ module; #include #include +#include export module nihil.posix:ensure_dir; @@ -16,8 +17,17 @@ namespace nihil { /* * Create the given directory and any parent directories. */ -export [[nodiscard]] auto ensure_dir(std::filesystem::path const &dir) - -> std::expected; +export [[nodiscard]] auto +ensure_dir(std::filesystem::path const &dir) -> std::expected +{ + auto err = std::error_code(); -} // namespace nihil + std::filesystem::create_directories(dir, err); + + if (err) + return std::unexpected(error(err)); + return {}; +} + +} // namespace nihil diff --git a/nihil.posix/exec.cc b/nihil.posix/exec.cc deleted file mode 100644 index b4e8732..0000000 --- a/nihil.posix/exec.cc +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -module nihil.posix; - -import nihil.error; -import nihil.monad; - -namespace nihil { - -auto shell(std::string_view const &command) -> std::expected -{ - return execl("/bin/sh", "sh", "-c", command); -} - -} // namespace nihil diff --git a/nihil.posix/exec.ccm b/nihil.posix/exec.ccm deleted file mode 100644 index cd03117..0000000 --- a/nihil.posix/exec.ccm +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -/* - * Exec providers, mostly used for spawn(). - */ - -#include -#include - -#include "nihil.hh" - -export module nihil.posix:exec; - -import nihil.error; -import :execv; -import :fexecv; -import :argv; -import :fd; - -namespace nihil { - -/* - * The lowest-level executor type, which is returned by other executors. - * Prefer fexecve() if it's available, otherwise use execve(). - */ -#ifdef NIHIL_HAVE_FEXECVE -using base_executor_type = fexecv; -#else -using base_executor_type = execv; -#endif - -/* - * execl: equivalent to execv, except the arguments are passed as a - * variadic pack of string-like objects. - */ -export [[nodiscard]] auto execl(std::string_view path, auto &&...args) - -> std::expected -{ - return execv(path, argv({std::string_view(args)...})); -} - -/* - * shell: run the process by invoking /bin/sh -c with the single argument, - * equivalent to system(3). - */ -export [[nodiscard]] auto shell(std::string_view const &command) - -> std::expected; - -} // namespace nihil diff --git a/nihil.posix/execl.ccm b/nihil.posix/execl.ccm new file mode 100644 index 0000000..f3cbf9a --- /dev/null +++ b/nihil.posix/execl.ccm @@ -0,0 +1,40 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +#include "nihil.hh" + +export module nihil.posix:execl; + +import nihil.error; +import :argv; +import :execv; +import :fexecv; + +namespace nihil { + +/* + * execl: equivalent to (f)execv, except the arguments are passed as a + * variadic pack of string-like objects. + */ + +export [[nodiscard]] auto execl(std::string_view path, auto &&...args) -> execv +{ + return execv(path, argv({std::string_view(args)...})); +} + +#ifdef NIHIL_HAVE_FEXECVE + +export [[nodiscard]] auto execl(fd &&executable, auto &&...args) -> fexecv +{ + return fexecv(std::move(executable), argv({std::string_view(args)...})); +} + +#endif // NIHIL_HAVE_FEXECVE + +} // namespace nihil diff --git a/nihil.posix/execl.test.cc b/nihil.posix/execl.test.cc new file mode 100644 index 0000000..5aaaa25 --- /dev/null +++ b/nihil.posix/execl.test.cc @@ -0,0 +1,55 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil.posix; + +namespace { + +SCENARIO("nihil::execl() can be used to spawn a shell") +{ + GIVEN("An execl object") + { + auto exec = nihil::execl("/bin/sh", "sh", "-c", "x=1; echo $x"); + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec, capture).value().wait().value(); + + THEN("The exit code is 0") + { + REQUIRE(status.status() == 0); + } + AND_THEN("The expected output was captured") + { + REQUIRE(output == "1\n"); + } + } + } +} + +SCENARIO("nihil::execl() returns the shell's exit code") +{ + GIVEN("An execshell object") + { + auto exec = nihil::execl("/bin/sh", "sh", "-c", "x=42; exit $x"); + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec, capture).value().wait().value(); + + THEN("The exit code is 1") + { + REQUIRE(status.status() == 42); + } + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/execlp.ccm b/nihil.posix/execlp.ccm index d0d88d5..ab3737c 100644 --- a/nihil.posix/execlp.ccm +++ b/nihil.posix/execlp.ccm @@ -1,28 +1,22 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; -#include #include #include +#include export module nihil.posix:execlp; import nihil.error; -import :exec; import :argv; import :execvp; namespace nihil { -/* - * execlp: equivalent to execvp, except the arguments are passed as a - * variadic pack of string-like objects. - */ -export [[nodiscard]] auto execlp(std::string_view file, auto &&...args) - -> std::expected +// execlp: equivalent to execvp, except the arguments are passed as a +// variadic pack of string-like objects. +export [[nodiscard]] auto +execlp(std::string_view file, auto &&...args) -> std::expected { return execvp(file, argv({std::string_view(args)...})); } diff --git a/nihil.posix/execlp.test.cc b/nihil.posix/execlp.test.cc new file mode 100644 index 0000000..cedf871 --- /dev/null +++ b/nihil.posix/execlp.test.cc @@ -0,0 +1,67 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil.posix; + +namespace { + +SCENARIO("nihil::execlp() can be used to spawn a shell") +{ + GIVEN("An execlp object") + { + auto exec = nihil::execlp("sh", "sh", "-c", "x=1; echo $x"); + + THEN("sh was found in $PATH") + { + if (!exec) + FAIL(exec.error()); + } + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec.value(), capture).value().wait().value(); + + THEN("The exit code is 0") + { + REQUIRE(status.status() == 0); + } + AND_THEN("The expected output was captured") + { + REQUIRE(output == "1\n"); + } + } + } +} + +SCENARIO("nihil::execlp() returns the shell's exit code") +{ + GIVEN("An execlp object") + { + auto exec = nihil::execlp("sh", "sh", "-c", "x=42; exit $x"); + + THEN("sh was found in $PATH") + { + if (!exec) + FAIL(exec.error()); + } + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec.value(), capture).value().wait().value(); + + THEN("The exit code is 1") + { + REQUIRE(status.status() == 42); + } + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/execshell.ccm b/nihil.posix/execshell.ccm new file mode 100644 index 0000000..1fbfccf --- /dev/null +++ b/nihil.posix/execshell.ccm @@ -0,0 +1,21 @@ +// This source code is released into the public domain. +module; + +#include + +export module nihil.posix:execshell; + +import nihil.error; +import :execv; +import :execl; + +namespace nihil { + +// execshell: a spawn executor which runs the process by invoking /bin/sh -c with the +// single argument, equivalent to system(3). +export [[nodiscard]] auto execshell(std::string_view command) -> execv +{ + return execl("/bin/sh", "sh", "-c", command); +} + +} // namespace nihil diff --git a/nihil.posix/execshell.test.cc b/nihil.posix/execshell.test.cc new file mode 100644 index 0000000..b64953a --- /dev/null +++ b/nihil.posix/execshell.test.cc @@ -0,0 +1,55 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil.posix; + +namespace { + +SCENARIO("nihil::execshell() can be used to spawn a shell") +{ + GIVEN("An execshell object") + { + auto exec = nihil::execshell("x=1; echo $x"); + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec, capture).value().wait().value(); + + THEN("The exit code is 0") + { + REQUIRE(status.status() == 0); + } + AND_THEN("The expected output was captured") + { + REQUIRE(output == "1\n"); + } + } + } +} + +SCENARIO("nihil::execshell() returns the shell's exit code") +{ + GIVEN("An execshell object") + { + auto exec = nihil::execshell("x=42; exit $x"); + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec, capture).value().wait().value(); + + THEN("The exit code is 1") + { + REQUIRE(status.status() == 42); + } + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/execv.cc b/nihil.posix/execv.cc deleted file mode 100644 index 752a96d..0000000 --- a/nihil.posix/execv.cc +++ /dev/null @@ -1,42 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -extern char **environ; - -module nihil.posix; - -import nihil.error; -import nihil.monad; - -namespace nihil { - -execv::execv(std::filesystem::path path, argv &&args) noexcept - : m_path(std::move(path)) - , m_args(std::move(args)) -{ -} - -auto execv::exec(this execv &self) -> std::expected -{ - ::execve(self.m_path.string().c_str(), self.m_args.data(), environ); - return std::unexpected(error("execve failed", error(std::errc(errno)))); -} - -execv::execv(execv &&) noexcept = default; -auto execv::operator=(this execv &, execv &&) noexcept -> execv & = default; - -} // namespace nihil diff --git a/nihil.posix/execv.ccm b/nihil.posix/execv.ccm index d97754e..ef9d259 100644 --- a/nihil.posix/execv.ccm +++ b/nihil.posix/execv.ccm @@ -1,13 +1,12 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include #include #include +#include + export module nihil.posix:execv; import nihil.error; @@ -16,32 +15,37 @@ import :executor; namespace nihil { -/* - * execv: use a filename and an argument vector to call ::execve(). - * This is the lowest-level executor which all others are implemented - * in terms of, if fexecve is not available. - * - * TODO: Should have a way to pass the environment (envp). - */ +// execv: use a filename and an argument vector to call ::execv(). export struct execv final { using tag = exec_tag; - execv(std::filesystem::path, argv &&) noexcept; + execv(std::filesystem::path path, argv &&args) noexcept + : m_path(std::move(path)) + , m_args(std::move(args)) + { + } - [[nodiscard]] auto exec(this execv &) -> std::expected; + ~execv() = default; // Movable - execv(execv &&) noexcept; - auto operator=(this execv &, execv &&) noexcept -> execv &; + execv(execv &&) noexcept = default; + auto operator=(execv &&) noexcept -> execv & = default; // Not copyable (because m_args isn't copyable). execv(execv const &) = delete; auto operator=(this execv &, execv const &) -> execv & = delete; + // Perform the execv(). This only returns on failure. + [[nodiscard]] auto exec(this execv &self) -> std::expected + { + ::execv(self.m_path.string().c_str(), self.m_args.data()); + return std::unexpected(error("execve failed", error(std::errc(errno)))); + } + private: - std::filesystem::path m_path; - argv m_args; + std::filesystem::path m_path; + argv m_args; }; } // namespace nihil diff --git a/nihil.posix/execv.test.cc b/nihil.posix/execv.test.cc new file mode 100644 index 0000000..aaeead7 --- /dev/null +++ b/nihil.posix/execv.test.cc @@ -0,0 +1,55 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil.posix; + +namespace { + +SCENARIO("nihil::execv() can be used to spawn a shell") +{ + GIVEN("An execv object") + { + auto exec = nihil::execv("/bin/sh", nihil::argv{"sh", "-c", "x=1; echo $x"}); + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec, capture).value().wait().value(); + + THEN("The exit code is 0") + { + REQUIRE(status.status() == 0); + } + AND_THEN("The expected output was captured") + { + REQUIRE(output == "1\n"); + } + } + } +} + +SCENARIO("nihil::execv() returns the shell's exit code") +{ + GIVEN("An execshell object") + { + auto exec = nihil::execv("/bin/sh", nihil::argv{"sh", "-c", "x=42; exit $x"}); + + WHEN("The shell is executed") + { + auto output = std::string(); + auto capture = nihil::make_capture(nihil::stdout_fileno, output).value(); + auto status = nihil::spawn(exec, capture).value().wait().value(); + + THEN("The exit code is 1") + { + REQUIRE(status.status() == 42); + } + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/execvp.cc b/nihil.posix/execvp.cc deleted file mode 100644 index 5eac315..0000000 --- a/nihil.posix/execvp.cc +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "nihil.hh" - -module nihil.posix; - -import nihil.error; -import nihil.monad; - -namespace nihil { -#ifdef NIHIL_HAVE_FEXECVE - -auto execvp(std::string_view file, argv &&argv) -> std::expected -{ - auto execfd = open_in_path(file); - if (!execfd) - return std::unexpected(error( - std::format("executable not found in path: {}", file))); - return fexecv(std::move(*execfd), std::move(argv)); -} - -#else // !NIHIL_HAVE_FEXECVE - -auto execvp(std::string_view file, nihil::argv &&argv) -> std::expected -{ - auto filename = nihil::find_in_path(file); - if (!filename) - return std::unexpected(nihil::error( - std::format("executable not found in path: {}", file))); - return execv(std::move(*filename), std::move(argv)); -} - -#endif // NIHIL_HAVE_FEXECVE - -} // namespace nihil diff --git a/nihil.posix/execvp.ccm b/nihil.posix/execvp.ccm index 688ecb6..680a13e 100644 --- a/nihil.posix/execvp.ccm +++ b/nihil.posix/execvp.ccm @@ -1,7 +1,4 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include @@ -12,17 +9,22 @@ export module nihil.posix:execvp; import nihil.error; import :argv; -import :exec; +import :execv; +import :find_in_path; namespace nihil { -/* - * execvp: equivalent to fexecv(), except the command is passed as - * a filename instead of a file descriptor. If the filename is not - * an absolute path, it will be searched for in $PATH. - */ - -export [[nodiscard]] auto execvp(std::string_view file, argv &&argv) - -> std::expected; +// execvp: equivalent to execv, except the command is passed as +// a filename instead of a file descriptor. If the filename is not +// an absolute path, it will be searched for in $PATH. +export [[nodiscard]] auto +execvp(std::string_view file, argv &&argv) -> std::expected +{ + auto filename = nihil::find_in_path(file); + if (!filename) + return std::unexpected(nihil::error( + std::format("executable not found in path: {}", file))); + return execv(std::move(*filename), std::move(argv)); +} } // namespace nihil diff --git a/nihil.posix/fd.cc b/nihil.posix/fd.cc deleted file mode 100644 index 6d5e54f..0000000 --- a/nihil.posix/fd.cc +++ /dev/null @@ -1,220 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -#include -#include -#include -#include -#include - -module nihil.posix; - -import nihil.error; -import nihil.monad; - -namespace nihil { - -fd::fd() noexcept = default; - -fd::fd(int fileno) noexcept - : m_fileno(fileno) -{ -} - -fd::~fd() -{ - if (*this) - std::ignore = this->close(); -} - -fd::fd(fd &&other) noexcept - : m_fileno(std::exchange(other.m_fileno, invalid_fileno)) -{ -} - -auto fd::operator=(this fd &self, fd &&other) noexcept -> fd & -{ - if (&self != &other) - self.m_fileno = std::exchange(other.m_fileno, invalid_fileno); - return self; -} - -fd::operator bool(this fd const &self) noexcept -{ - return self.m_fileno != invalid_fileno; -} - -auto fd::close(this fd &self) -> std::expected -{ - auto const ret = ::close(self.get()); - self.m_fileno = invalid_fileno; - - if (ret == 0) - return {}; - - return std::unexpected(error(std::errc(errno))); -} - -auto fd::get(this fd const &self) -> int -{ - if (self) - return self.m_fileno; - throw std::logic_error("Attempt to call get() on invalid fd"); -} - -auto fd::release(this fd &&self) -> int -{ - if (self) - return std::exchange(self.m_fileno, invalid_fileno); - throw std::logic_error("Attempt to release an invalid fd"); -} - -auto dup(fd const &self) -> std::expected -{ - auto const newfd = ::dup(self.get()); - if (newfd != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto dup(fd const &self, int newfd) -> std::expected -{ - auto const ret = ::dup2(self.get(), newfd); - if (ret != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto raw_dup(fd const &self) -> std::expected -{ - auto const newfd = ::dup(self.get()); - if (newfd != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto raw_dup(fd const &self, int newfd) -> std::expected -{ - auto const ret = ::dup2(self.get(), newfd); - if (ret != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto getflags(fd const &self) -> std::expected -{ - auto const flags = ::fcntl(self.get(), F_GETFL); - if (flags != -1) - return flags; - - return std::unexpected(error(std::errc(errno))); -} - -auto replaceflags(fd &self, int newflags) -> std::expected -{ - auto const ret = ::fcntl(self.get(), F_SETFL, newflags); - if (ret == 0) - return {}; - - return std::unexpected(error(std::errc(errno))); -} - -auto setflags(fd &self, int newflags) -> std::expected -{ - auto flags = co_await getflags(self); - - flags |= newflags; - co_await replaceflags(self, flags); - - co_return flags; -} - -auto clearflags(fd &self, int clrflags) -> std::expected -{ - auto flags = co_await getflags(self); - - flags &= ~clrflags; - co_await replaceflags(self, flags); - - co_return flags; -} - -auto getfdflags(fd const &self) -> std::expected -{ - auto const flags = ::fcntl(self.get(), F_GETFD); - if (flags != -1) - return flags; - - return std::unexpected(error(std::errc(errno))); -} - -auto replacefdflags(fd &self, int newflags) -> std::expected -{ - auto const ret = ::fcntl(self.get(), F_SETFD, newflags); - if (ret != -1) - return {}; - - return std::unexpected(error(std::errc(errno))); -} - -auto setfdflags(fd &self, int newflags) -> std::expected -{ - auto flags = co_await getfdflags(self); - - flags |= newflags; - co_await replacefdflags(self, flags); - - co_return flags; -} - -auto clearfdflags(fd &self, int clrflags) -> std::expected -{ - auto flags = co_await getfdflags(self); - - flags &= ~clrflags; - co_await replacefdflags(self, flags); - - co_return flags; -} - -auto pipe() -> std::expected, error> -{ - auto fds = std::array{}; - - if (auto const ret = ::pipe(fds.data()); ret != 0) - return std::unexpected(error(std::errc(errno))); - - return {{fd(fds[0]), fd(fds[1])}}; -} - -auto fd::write(this fd &self, std::span buffer) - -> std::expected -{ - auto const ret = ::write(self.get(), buffer.data(), buffer.size()); - if (ret >= 0) - return ret; - - return std::unexpected(error(std::errc(errno))); -} - -auto fd::read(this fd &self, std::span buffer) - -> std::expected, error> -{ - auto const ret = ::read(self.get(), buffer.data(), buffer.size()); - if (ret >= 0) - return buffer.subspan(0, ret); - - return std::unexpected(error(std::errc(errno))); -} - -} // namespace nihil diff --git a/nihil.posix/fd.ccm b/nihil.posix/fd.ccm index b937f46..7faf2f1 100644 --- a/nihil.posix/fd.ccm +++ b/nihil.posix/fd.ccm @@ -1,7 +1,4 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include @@ -11,56 +8,136 @@ module; #include #include +#include +#include + export module nihil.posix:fd; +import nihil.flagset; import nihil.error; import nihil.monad; namespace nihil { -/* - * fd: a file descriptor. - */ +// F_{GET,SET}FL flags +struct fd_flags_tag +{ +}; +export using fd_flags = nihil::flagset; + +export inline constexpr auto fd_none = fd_flags(); +export inline constexpr auto fd_nonblock = fd_flags::mask(); +export inline constexpr auto fd_append = fd_flags::mask(); +export inline constexpr auto fd_async = fd_flags::mask(); +export inline constexpr auto fd_sync = fd_flags::mask(); +export inline constexpr auto fd_dsync = fd_flags::mask(); + +#ifdef O_DIRECT +export inline constexpr auto fl_direct = O_DIRECT; +#endif + +// F_{GET,SET}FD flags +struct fd_fdflags_tag +{ +}; +export using fd_fdflags = nihil::flagset; -export struct fd final { +export inline constexpr auto fd_fd_none = fd_fdflags(); +export inline constexpr auto fd_fd_cloexec = fd_fdflags::mask(); + +// fd: a file descriptor. +export struct fd final +{ // Construct an empty (invalid) fd. - fd() noexcept; + fd() noexcept = default; - // Construct an fd from an exising file destrictor, taking ownership. - fd(int fd_) noexcept; + // Construct an fd from an exising file descriptor, taking ownership. + explicit fd(int fileno) noexcept + : m_fileno(fileno) + { + } // Destructor. Close the fd, discarding any errors. - ~fd(); + ~fd() + { + if (*this) + std::ignore = this->close(); + } // Move from another fd, leaving the moved-from fd in an invalid state. - fd(fd &&other) noexcept; - auto operator=(this fd &, fd &&other) noexcept -> fd &; + fd(fd &&other) noexcept + : m_fileno(std::exchange(other.m_fileno, invalid_fileno)) + { + } + + auto operator=(this fd &self, fd &&other) noexcept -> fd & + { + if (&self != &other) + self.m_fileno = std::exchange(other.m_fileno, invalid_fileno); + return self; // NOLINT + } // Not copyable. fd(fd const &) = delete; - fd& operator=(this fd &, fd const &) = delete; + auto operator=(this fd &, fd const &) -> fd & = delete; // Return true if this fd is valid (open). - [[nodiscard]] explicit operator bool(this fd const &self) noexcept; + [[nodiscard]] explicit operator bool(this fd const &self) noexcept + { + return self.m_fileno != invalid_fileno; + } // Close the wrapped fd. - [[nodiscard]] auto close(this fd &self) -> std::expected; + [[nodiscard]] auto close(this fd &self) -> std::expected + { + auto const ret = ::close(self.get()); + self.m_fileno = invalid_fileno; + + if (ret == 0) + return {}; + + return std::unexpected(error(std::errc(errno))); + } // Return the stored fd. - [[nodiscard]] auto get(this fd const &self) -> int; + [[nodiscard]] auto get(this fd const &self) -> int + { + if (self) + return self.m_fileno; + throw std::logic_error("Attempt to call get() on invalid fd"); + } // Release the stored fd and return it. The caller must close it. - [[nodiscard]] auto release(this fd &&self) -> int; + [[nodiscard]] auto release(this fd &&self) -> int + { + if (self) + return std::exchange(self.m_fileno, invalid_fileno); + throw std::logic_error("Attempt to release an invalid fd"); + } // Write data from the provided buffer to the fd. Returns the // number of bytes written. - [[nodiscard]] auto write(this fd &self, std::span) - -> std::expected; + [[nodiscard]] auto + write(this fd &self, std::span buffer) -> std::expected + { + auto const ret = ::write(self.get(), buffer.data(), buffer.size()); + if (ret >= 0) + return ret; + + return std::unexpected(error(std::errc(errno))); + } // Read data from the fd to the provided buffer. Returns a // subspan containing the data which was read. - [[nodiscard]] auto read(this fd &self, std::span) - -> std::expected, error>; + [[nodiscard]] auto read(this fd &self, std::span buffer) + -> std::expected, error> + { + auto const ret = ::read(self.get(), buffer.data(), buffer.size()); + if (ret >= 0) + return buffer.subspan(0, ret); + + return std::unexpected(error(std::errc(errno))); + } private: static constexpr int invalid_fileno = -1; @@ -69,7 +146,14 @@ private: }; // Create a copy of this fd by calling dup(). -export [[nodiscard]] auto dup(fd const &self) -> std::expected; +export [[nodiscard]] auto dup(fd const &self) -> std::expected +{ + auto const newfd = ::dup(self.get()); + if (newfd != -1) + return fd(newfd); + + return std::unexpected(error(std::errc(errno))); +} // Create a copy of this fd by calling dup2(). Note that because this results // in the existing fd and the new fd both being managed by an fd instance, @@ -80,73 +164,137 @@ export [[nodiscard]] auto dup(fd const &self) -> std::expected; // // In both of these cases, either use raw_dup() instead, or immediately call // release() on the returned fd to prevent the fd instance from closing it. -export [[nodiscard]] auto dup(fd const &self, int newfd) - -> std::expected; +export [[nodiscard]] auto dup(fd const &self, int newfd) -> std::expected +{ + auto const ret = ::dup2(self.get(), newfd); + if (ret != -1) + return fd(newfd); + + return std::unexpected(error(std::errc(errno))); +} // Create a copy of this fd by calling dup(). -export [[nodiscard]] auto raw_dup(fd const &self) - -> std::expected; +export [[nodiscard]] auto raw_dup(fd const &self) -> std::expected +{ + auto const newfd = ::dup(self.get()); + if (newfd != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} // Create a copy of this fd by calling dup2(). -export [[nodiscard]] auto raw_dup(fd const &self, int newfd) - -> std::expected; +export [[nodiscard]] auto raw_dup(fd const &self, int newfd) -> std::expected +{ + auto const ret = ::dup2(self.get(), newfd); + if (ret != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +// Call fcntl() on this fd. Prefer one of the type-safe wrappers to this, if available. +export [[nodiscard]] auto fcntl(fd const &fd, int op, int arg = 0) + -> std::expected +{ + auto const ret = ::fcntl(fd.get(), op, arg); + if (ret == -1) + return std::unexpected(error(std::errc(errno))); + return ret; +} // Return the fnctl flags for this fd. -export [[nodiscard]] auto getflags(fd const &self) - -> std::expected; +export [[nodiscard]] auto getflags(fd const &fd) -> std::expected +{ + auto flags = co_await fcntl(fd, F_GETFL); + co_return fd_flags::from_int(flags); +} // Replace the fnctl flags for this fd. -export [[nodiscard]] auto replaceflags(fd &self, int newflags) - -> std::expected; +export [[nodiscard]] auto replaceflags(fd &fd, fd_flags newflags) -> std::expected +{ + co_await fcntl(fd, F_SETFL, newflags.value()); + co_return {}; +} // Add bits to the fcntl flags for this fd. Returns the new flags. -export [[nodiscard]] auto setflags(fd &self, int newflags) - -> std::expected; +export [[nodiscard]] auto setflags(fd &fd, fd_flags newflags) -> std::expected +{ + auto flags = co_await getflags(fd); + + flags |= newflags; + co_await replaceflags(fd, flags); + co_return flags; +} // Remove bits from the fcntl flags for this fd. Returns the new flags. -export [[nodiscard]] auto clearflags(fd &self, int clrflags) - -> std::expected; +export [[nodiscard]] auto clearflags(fd &fd, fd_flags clrflags) -> std::expected +{ + auto flags = co_await getflags(fd); + + flags &= ~clrflags; + co_await replaceflags(fd, flags); + co_return flags; +} // Return the fd flags for this fd. -export [[nodiscard]] auto getfdflags(fd const &self) - -> std::expected; +export [[nodiscard]] auto getfdflags(fd const &fd) -> std::expected +{ + auto const flags = co_await fcntl(fd, F_GETFD); + co_return fd_fdflags::from_int(flags); +} // Replace the fd flags for this fd. -export [[nodiscard]] auto replacefdflags(fd &self, int newflags) - -> std::expected; +export [[nodiscard]] auto replacefdflags(fd &fd, fd_fdflags newflags) -> std::expected +{ + co_await fcntl(fd, F_SETFD, newflags.value()); + co_return {}; +} // Add bits to the fd flags for this fd. Returns the new flags. -export [[nodiscard]] auto setfdflags(fd &self, int newflags) - -> std::expected; +export [[nodiscard]] auto setfdflags(fd &fd, fd_fdflags newflags) -> std::expected +{ + auto flags = co_await getfdflags(fd); + + flags |= newflags; + co_await replacefdflags(fd, flags); + co_return flags; +} // Remove bits from the fd flags for this fd. Returns the new flags. -export [[nodiscard]] auto clearfdflags(fd &self, int clrflags) - -> std::expected; +export [[nodiscard]] auto clearfdflags(fd &fd, fd_fdflags clrflags) -> std::expected +{ + auto flags = co_await getfdflags(fd); + + flags &= ~clrflags; + co_await replacefdflags(fd, flags); + co_return flags; +} // Create two fds by calling pipe() and return them. -export [[nodiscard]] auto pipe() -> std::expected, error>; - -/* - * Write data to a file descriptor from the provided range. Returns the - * number of bytes written. - */ -export [[nodiscard]] auto write(fd &file, - std::ranges::contiguous_range auto &&range) - -> std::expected -requires(sizeof(std::ranges::range_value_t) == 1) +export [[nodiscard]] auto pipe() -> std::expected, error> +{ + auto fds = std::array{}; + + if (auto const ret = ::pipe(fds.data()); ret != 0) + return std::unexpected(error(std::errc(errno))); + + return {{fd(fds[0]), fd(fds[1])}}; +} + +// Write data to a file descriptor from the provided range. Returns the +//number of bytes written. +export [[nodiscard]] auto +write(fd &file, std::ranges::contiguous_range auto &&range) -> std::expected + requires(sizeof(std::ranges::range_value_t) == 1) { return file.write(as_bytes(std::span(range))); } -/* - * Read data from a file descriptor into the provided buffer. Returns a - * span containing the data that was read. - */ -export [[nodiscard]] auto read(fd &file, - std::ranges::contiguous_range auto &&range) - -> std::expected< - std::span>, - error> +// Read data from a file descriptor into the provided buffer. Returns a +// span containing the data that was read. +export [[nodiscard]] auto read(fd &file, std::ranges::contiguous_range auto &&range) + -> std::expected>, error> requires(sizeof(std::ranges::range_value_t) == 1) { auto bspan = as_writable_bytes(std::span(range)); diff --git a/nihil.posix/fd.test.cc b/nihil.posix/fd.test.cc new file mode 100644 index 0000000..870ddde --- /dev/null +++ b/nihil.posix/fd.test.cc @@ -0,0 +1,205 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +#include + +import nihil.error; +import nihil.posix; + +using namespace std::literals; + +namespace { + +// Test if an fd is open. +auto fd_is_open(int fd) -> bool { + auto const ret = ::fcntl(fd, F_GETFL); + return ret == 0; +} + +TEST_CASE("fd: construct empty", "[fd]") { + auto const fd = nihil::fd(); + + REQUIRE(!fd); + REQUIRE_THROWS_AS(fd.get(), std::logic_error); +} + +TEST_CASE("fd: construct from fd", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + { + auto fd = nihil::fd(file); + REQUIRE(fd_is_open(fd.get())); + } + + REQUIRE(!fd_is_open(file)); +} + +TEST_CASE("fd: close", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + REQUIRE(fd); + + auto const ret = fd.close(); + REQUIRE(ret); + REQUIRE(!fd_is_open(file)); +} + +TEST_CASE("fd: move construct", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd1 = nihil::fd(file); + REQUIRE(fd_is_open(fd1.get())); + + auto fd2(std::move(fd1)); + REQUIRE(!fd1); //NOLINT + REQUIRE(fd2); + REQUIRE(fd2.get() == file); + REQUIRE(fd_is_open(file)); +} + +TEST_CASE("fd: move assign", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd1 = nihil::fd(file); + REQUIRE(fd_is_open(fd1.get())); + + auto fd2 = nihil::fd(); + REQUIRE(!fd2); + + fd2 = std::move(fd1); + + REQUIRE(!fd1); //NOLINT + REQUIRE(fd2); + REQUIRE(fd2.get() == file); + REQUIRE(fd_is_open(file)); +} + +TEST_CASE("fd: release", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + auto fdesc = std::move(fd).release(); + REQUIRE(!fd); //NOLINT + REQUIRE(fdesc == file); +} + +TEST_CASE("fd: dup", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + REQUIRE(fd); + + auto fd2 = dup(fd); + REQUIRE(fd2); + REQUIRE(fd.get() != fd2->get()); +} + +TEST_CASE("fd: dup2", "[fd]") { + auto constexpr test_fd = 666; + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + REQUIRE(!fd_is_open(test_fd)); + + auto fd = nihil::fd(file); + auto fd2 = dup(fd, test_fd); + + REQUIRE(fd); + REQUIRE(fd2); + REQUIRE(fd2->get() == test_fd); +} + +TEST_CASE("fd: flags", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + + { + auto const ret = replaceflags(fd, nihil::fd_none); + REQUIRE(ret); + REQUIRE(getflags(fd) == nihil::fd_none); + } + + { + auto const ret = setflags(fd, nihil::fd_nonblock); + REQUIRE(ret == nihil::fd_nonblock); + REQUIRE(getflags(fd) == nihil::fd_nonblock); + } + + { + auto const ret = setflags(fd, nihil::fd_sync); + REQUIRE(ret == (nihil::fd_nonblock | nihil::fd_sync)); + REQUIRE(getflags(fd) == (nihil::fd_nonblock | nihil::fd_sync)); + } + + { + auto const ret = clearflags(fd, nihil::fd_nonblock); + REQUIRE(ret == nihil::fd_sync); + REQUIRE(getflags(fd) == nihil::fd_sync); + } +} + +TEST_CASE("fd: fdflags", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + + { + auto const ret = replacefdflags(fd, nihil::fd_fd_none); + REQUIRE(ret); + REQUIRE(getfdflags(fd) == nihil::fd_fd_none); + } + + { + auto const ret = setfdflags(fd, nihil::fd_fd_cloexec); + REQUIRE(ret == nihil::fd_fd_cloexec); + REQUIRE(getfdflags(fd) == nihil::fd_fd_cloexec); + } + + { + auto const ret = clearfdflags(fd, nihil::fd_fd_cloexec); + REQUIRE(ret == nihil::fd_fd_none); + REQUIRE(getfdflags(fd) == nihil::fd_fd_none); + } +} + +TEST_CASE("fd: pipe, read, write", "[fd]") { + auto fds = nihil::pipe(); + REQUIRE(fds); + + /* + * Note: traditionally, the first fd is the reading side, and the second fd + * is the writing side. Some platforms (e.g., macOS) still behave this way. + */ + + auto [fd1, fd2] = std::move(*fds); + + auto constexpr test_string = "test string"sv; + + auto ret = write(fd2, test_string); + REQUIRE(ret); + REQUIRE(*ret == test_string.size()); + + auto readbuf = std::array{}; + auto read_buf = read(fd1, readbuf); + REQUIRE(read_buf); + REQUIRE(std::string_view(*read_buf) == test_string); +} + +} // anonymous namespace diff --git a/nihil.posix/fexecv.cc b/nihil.posix/fexecv.cc deleted file mode 100644 index ad57d14..0000000 --- a/nihil.posix/fexecv.cc +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -#include - -#include "nihil.h" - -#ifdef NIHIL_HAVE_FEXECVE - -extern char **environ; - -module nihil.posix; - -import nihil.error; -import nihil.monad; - -namespace nihil { - -fexecv::fexecv(fd &&execfd, argv &&args) noexcept - : m_execfd(std::move(execfd)) - , m_args(std::move(args)) -{ -} - -auto fexecv::exec(this fexecv &self) - -> std::expected -{ - ::fexecve(self.m_execfd.get(), self.m_args.data(), environ); - return std::unexpected(error("fexecve failed", - error(std::errc(errno)))); -} - -fexecv::fexecv(fexecv &&) noexcept = default; -auto fexecv::operator=(this fexecv &, fexecv &&) noexcept -> fexecv& = default; - -} // namespace nihil - -#endif // NIHIL_HAVE_FEXECVE diff --git a/nihil.posix/fexecv.ccm b/nihil.posix/fexecv.ccm index 1fe57a8..4001726 100644 --- a/nihil.posix/fexecv.ccm +++ b/nihil.posix/fexecv.ccm @@ -1,7 +1,4 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include @@ -27,27 +24,35 @@ namespace nihil { * * TODO: Should have a way to pass the environment (envp). */ -export struct fexecv final { +export struct fexecv final +{ using tag = exec_tag; - fexecv(fd &&execfd, argv &&args) noexcept; + fexecv(fd &&execfd, argv &&args) noexcept + : m_execfd(std::move(execfd)) + , m_args(std::move(args)) + { + } - [[nodiscard]] auto exec(this fexecv &self) - -> std::expected; + [[nodiscard]] auto exec(this fexecv &self) -> std::expected + { + ::fexecve(self.m_execfd.get(), self.m_args.data(), environ); + return std::unexpected(error("fexecve failed", error(std::errc(errno)))); + } // Movable - fexecv(fexecv &&) noexcept; - auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv&; + fexecv(fexecv &&) noexcept = default; + auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv & = default; // Not copyable (because we hold the open fd object) fexecv(fexecv const &) = delete; - auto operator=(this fexecv &, fexecv const &) -> fexecv& = delete; + auto operator=(this fexecv &, fexecv const &) -> fexecv & = delete; private: - fd m_execfd; - argv m_args; + fd m_execfd; + argv m_args; }; -#endif // NIHIL_HAVE_FEXECVE +#endif // NIHIL_HAVE_FEXECVE } // namespace nihil diff --git a/nihil.posix/fexecvp.ccm b/nihil.posix/fexecvp.ccm new file mode 100644 index 0000000..d61240c --- /dev/null +++ b/nihil.posix/fexecvp.ccm @@ -0,0 +1,37 @@ +// This source code is released into the public domain. +module; + +#include +#include +#include +#include + +#include "nihil.hh" + +export module nihil.posix:fexecvp; + +#ifdef NIHIL_HAVE_FEXECVE + +import nihil.error; +import :argv; +import :execv; +import :open_in_path; + +namespace nihil { + +// execvp: equivalent to execv, except the command is passed as +// a filename instead of a file descriptor. If the filename is not +// an absolute path, it will be searched for in $PATH. +export [[nodiscard]] auto +fexecvp(std::filesystem::path const &file, argv &&argv) -> std::expected +{ + auto execfd = open_in_path(file); + if (!execfd) + return std::unexpected(error( + std::format("executable not found in path: {}", file))); + return fexecv(std::move(*execfd), std::move(argv)); +} + +} // namespace nihil + +#endif // NIHIL_HAVE_FEXECVE diff --git a/nihil.posix/find_in_path.cc b/nihil.posix/find_in_path.cc deleted file mode 100644 index 7b03faa..0000000 --- a/nihil.posix/find_in_path.cc +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -#include -#include -#include - -module nihil.posix; - -namespace nihil { - -auto find_in_path(std::filesystem::path const &file) -> std::optional -{ - using namespace std::literals; - - auto try_return = [](std::filesystem::path file) - -> std::optional - { - auto ret = ::access(file.string().c_str(), X_OK); - if (ret == 0) - return {std::move(file)}; - return {}; - }; - - // Absolute pathname skips the search. - if (file.is_absolute()) - return try_return(file); - - auto path = getenv("PATH").value_or(_PATH_DEFPATH); - - for (auto &&dir : path | std::views::split(':')) { - // An empty $PATH element means cwd. - auto sdir = dir.empty() - ? std::filesystem::path(".") - : std::filesystem::path(std::string_view(dir)); - - if (auto ret = try_return(sdir / file); ret) - return ret; - } - - return {}; -} - -} // namespace nihil diff --git a/nihil.posix/find_in_path.ccm b/nihil.posix/find_in_path.ccm index 4988a12..7bfa3b9 100644 --- a/nihil.posix/find_in_path.ccm +++ b/nihil.posix/find_in_path.ccm @@ -1,24 +1,53 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include #include +#include + +#include +#include export module nihil.posix:find_in_path; import nihil.error; import :fd; +import :getenv; namespace nihil { /* * Find an executable in $PATH and return the full path. If $PATH is not set, uses _PATH_DEFPATH. - * If the file can't be found or opened, returns std::nullopt. + * If the file can't be found or is not executable, returns std::nullopt. */ -export [[nodiscard]] auto find_in_path(std::filesystem::path const &file) - -> std::optional; +export [[nodiscard]] auto +find_in_path(std::filesystem::path const &file) -> std::optional +{ + using namespace std::literals; + + auto try_return = [](std::filesystem::path file) -> std::optional { + auto ret = ::access(file.string().c_str(), X_OK); + if (ret == 0) + return {std::move(file)}; + return {}; + }; + + // Absolute pathname skips the search. + if (file.is_absolute()) + return try_return(file); + + auto const path = getenv("PATH").value_or(_PATH_DEFPATH); // NOLINT + + for (auto &&dir : path | std::views::split(':')) { + // An empty $PATH element means cwd. + auto sdir = dir.empty() ? std::filesystem::path(".") + : std::filesystem::path(std::string_view(dir)); + + if (auto ret = try_return(sdir / file); ret) + return {ret}; + } + + return {}; +} } // namespace nihil diff --git a/nihil.posix/getenv.cc b/nihil.posix/getenv.cc deleted file mode 100644 index c596902..0000000 --- a/nihil.posix/getenv.cc +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -#include - -#include "nihil.hh" - -module nihil.posix; - -import nihil.error; - -namespace nihil { - -auto getenv(std::string_view varname) -> std::expected -{ - auto cvarname = std::string(varname); - -#ifdef NIHIL_HAVE_GETENV_R - // Start with a buffer of this size, and double it every iteration. - constexpr auto bufinc = std::size_t{1024}; - - auto buf = std::vector(bufinc); - for (;;) { - auto const ret = ::getenv_r(cvarname.c_str(), - buf.data(), buf.size()); - - if (ret == 0) - return {std::string(buf.data())}; - - if (ret == -1 && errno == ERANGE) { - buf.resize(buf.size() * 2); - continue; - } - - return std::unexpected(error(std::errc(errno))); - } -#else // NIHIL_HAVE_GETENV_R - errno = 0; - auto *v = ::getenv(cvarname.c_str()); - - if (v != nullptr) - return {std::string(v)}; - - if (errno != 0) - return std::unexpected(error(std::errc(errno))); - - return std::unexpected(error(std::errc::no_such_file_or_directory)); -#endif // NIHIL_HAVE_GETENV_R -} - -} // namespace nihil diff --git a/nihil.posix/getenv.ccm b/nihil.posix/getenv.ccm index 465f7e7..5967bf7 100644 --- a/nihil.posix/getenv.ccm +++ b/nihil.posix/getenv.ccm @@ -1,11 +1,15 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; +#include #include #include +#include +#include + +#include + +#include "nihil.hh" export module nihil.posix:getenv; @@ -13,11 +17,44 @@ import nihil.error; namespace nihil { -/* - * Find a variable by the given name in the environment by calling getenv_r(). - */ -export [[nodiscard]] auto getenv(std::string_view varname) - -> std::expected; +// Find a variable by the given name in the environment by calling getenv_r() if available, +// or getenv() if not. In either case the value is copied, so will not be affected by +// future calls to setenv(). +export [[nodiscard]] auto getenv(std::string_view varname) -> std::expected +{ + auto cvarname = std::string(varname); + +#ifdef NIHIL_HAVE_GETENV_R + // Start with a buffer of this size, and double it every iteration. + constexpr auto bufinc = std::size_t{1024}; + + auto buf = std::vector(bufinc); + for (;;) { + auto const ret = ::getenv_r(cvarname.c_str(), buf.data(), buf.size()); + + if (ret == 0) + return {std::string(buf.data())}; + + if (ret == -1 && errno == ERANGE) { + buf.resize(buf.size() * 2); + continue; + } + + return std::unexpected(error(std::errc(errno))); + } +#else // NIHIL_HAVE_GETENV_R + errno = 0; + auto *v = ::getenv(cvarname.c_str()); // NOLINT + + if (v != nullptr) + return {std::string(v)}; + + if (errno != 0) + return std::unexpected(error(std::errc(errno))); + + return std::unexpected(error(std::errc::no_such_file_or_directory)); +#endif // NIHIL_HAVE_GETENV_R +} } // namespace nihil diff --git a/nihil.posix/getenv.test.cc b/nihil.posix/getenv.test.cc new file mode 100644 index 0000000..9e10c16 --- /dev/null +++ b/nihil.posix/getenv.test.cc @@ -0,0 +1,50 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +#include + +import nihil.error; +import nihil.posix; + +TEST_CASE("getenv: existing value", "[getenv]") +{ + auto constexpr *name = "NIHIL_TEST_VAR"; + auto constexpr *value = "test is a test"; + + REQUIRE(::setenv(name, value, 1) == 0); + + auto const s = nihil::getenv(name); + REQUIRE(s); + REQUIRE(*s == value); +} + +TEST_CASE("getenv: non-existing value", "[getenv]") +{ + auto constexpr *name = "NIHIL_TEST_VAR"; + + REQUIRE(::unsetenv(name) == 0); + + auto const s = nihil::getenv(name); + REQUIRE(!s); + REQUIRE(s.error() == std::errc::no_such_file_or_directory); +} + +// Force the call to getenv_r() to reallocate. +TEST_CASE("getenv: long value") +{ + auto constexpr *name = "NIHIL_TEST_VAR"; + auto const value = std::string(4096, 'a'); + + REQUIRE(::setenv(name, value.c_str(), 1) == 0); + + auto const s = nihil::getenv(name); + REQUIRE(s); + REQUIRE(*s == value); +} diff --git a/nihil.posix/open.cc b/nihil.posix/open.cc deleted file mode 100644 index 9ef6538..0000000 --- a/nihil.posix/open.cc +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include - -#include -#include - -module nihil.posix; - -import nihil.error; -import :fd; - -namespace nihil { - -auto open(std::filesystem::path const &filename, int flags, int mode) - -> std::expected -{ - auto fdno = ::open(filename.c_str(), flags, mode); - if (fdno != -1) - return fd(fdno); - - return std::unexpected(error(std::errc(errno))); -} - -} // namespace nihil diff --git a/nihil.posix/open.ccm b/nihil.posix/open.ccm index eaedacd..59f80af 100644 --- a/nihil.posix/open.ccm +++ b/nihil.posix/open.ccm @@ -1,24 +1,87 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include #include +#include +#include + export module nihil.posix:open; import nihil.error; +import nihil.flagset; import :fd; -export namespace nihil { +namespace nihil { + +struct open_flags_tag +{ +}; +export using open_flags = nihil::flagset; + +export inline constexpr auto open_none = open_flags(); + +// Basic flags, exactly one of these is required. +export inline constexpr auto open_read = open_flags::mask(); +export inline constexpr auto open_write = open_flags::mask(); +export inline constexpr auto open_readwrite = open_flags::mask(); +export inline constexpr auto open_search = open_flags::mask(); +export inline constexpr auto open_exec = open_flags::mask(); + +// Modifiers +export inline constexpr auto open_nonblock = open_flags::mask(); +export inline constexpr auto open_append = open_flags::mask(); +export inline constexpr auto open_create = open_flags::mask(); +export inline constexpr auto open_truncate = open_flags::mask(); +export inline constexpr auto open_exclusive = open_flags::mask(); +export inline constexpr auto open_shared_lock = open_flags::mask(); +export inline constexpr auto open_exclusive_lock = open_flags::mask(); +export inline constexpr auto open_directory = open_flags::mask(); +export inline constexpr auto open_nofollow = open_flags::mask(); +export inline constexpr auto open_nofollow_any = open_flags::mask(); +export inline constexpr auto open_symlink = open_flags::mask(); +export inline constexpr auto open_eventonly = open_flags::mask(); +export inline constexpr auto open_close_on_exec = open_flags::mask(); +export inline constexpr auto open_resolve_beneath = open_flags::mask(); + +// FreeBSD +#ifdef O_DIRECT +export inline constexpr auto open_direct = open_flags::mask(); +#endif + +#ifdef O_VERIFY +export inline constexpr auto open_verify = open_flags::mask(); +#endif + +#ifdef O_PATH +export inline constexpr auto open_path = open_flags::mask(); +#endif + +#ifdef O_EMPTY_PATH +export inline constexpr auto open_empty_path = open_flags::mask(); +#endif + +// Open the given file and return an fd for it. +export [[nodiscard]] auto open(std::filesystem::path const &filename, open_flags flags, + int mode = 0777) -> std::expected +{ + auto fdno = ::open(filename.c_str(), flags.value(), mode); + if (fdno != -1) + return fd(fdno); + + return std::unexpected(error(std::errc(errno))); +} + +// Like open(), but resolve relative to an open file descriptor, which must refer to a directory. +export [[nodiscard]] auto openat(fd &where, std::filesystem::path const &filename, open_flags flags, + int mode = 0777) -> std::expected +{ + auto fdno = ::openat(where.get(), filename.c_str(), flags.value(), mode); + if (fdno != -1) + return fd(fdno); -/* - * Open the given file and return an fd for it. - */ -[[nodiscard]] auto open(std::filesystem::path const &filename, - int flags, int mode = 0777) - -> std::expected; + return std::unexpected(error(std::errc(errno))); +} } // namespace nihil diff --git a/nihil.posix/open_in_path.cc b/nihil.posix/open_in_path.cc deleted file mode 100644 index 30021ca..0000000 --- a/nihil.posix/open_in_path.cc +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -#include -#include - -module nihil.posix; - -namespace nihil { - -auto open_in_path(std::filesystem::path const &file) -> std::optional -{ - using namespace std::literals; - - auto try_open = - [](std::filesystem::path const &file) -> std::optional - { - auto ret = open(file, O_EXEC); - if (ret) - return {std::move(*ret)}; - return {}; - }; - - // Absolute pathname skips the search. - if (file.is_absolute()) - return try_open(file); - - auto path = getenv("PATH").value_or(_PATH_DEFPATH); - - for (auto &&dir : path | std::views::split(':')) { - // An empty $PATH element means cwd. - auto sdir = dir.empty() - ? std::filesystem::path(".") - : std::filesystem::path(std::string_view(dir)); - - if (auto ret = try_open(sdir / file); ret) - return ret; - } - - return {}; -} - -} // namespace nihil diff --git a/nihil.posix/open_in_path.ccm b/nihil.posix/open_in_path.ccm index d4c090d..7ff5812 100644 --- a/nihil.posix/open_in_path.ccm +++ b/nihil.posix/open_in_path.ccm @@ -1,23 +1,51 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include #include +#include +#include + +#include export module nihil.posix:open_in_path; import nihil.error; import :fd; +import :getenv; +import :open; namespace nihil { -/* - * Find an executable in $PATH and open it with O_EXEC. If $PATH is not set, uses _PATH_DEFPATH. - * If the file can't be found or opened, returns std::nullopt. - */ -export [[nodiscard]] auto open_in_path(std::filesystem::path const &file) -> std::optional; +// Find an executable in $PATH and open it with O_EXEC. If $PATH is not set, uses _PATH_DEFPATH. +// If the file can't be found or opened, returns std::nullopt. +export [[nodiscard]] auto open_in_path(std::filesystem::path const &file) -> std::optional +{ + using namespace std::literals; + + auto try_open = [](std::filesystem::path const &file) -> std::optional { + auto ret = open(file, open_exec); + if (ret) + return {std::move(*ret)}; + return {}; + }; + + // Absolute pathname skips the search. + if (file.is_absolute()) + return try_open(file); + + auto path = getenv("PATH").value_or(_PATH_DEFPATH); // NOLINT + + for (auto &&dir : path | std::views::split(':')) { + // An empty $PATH element means cwd. + auto sdir = dir.empty() ? std::filesystem::path(".") + : std::filesystem::path(std::string_view(dir)); + + if (auto ret = try_open(sdir / file); ret) + return ret; + } + + return {}; +} } // namespace nihil diff --git a/nihil.posix/posix.ccm b/nihil.posix/posix.ccm index 2ebc1b8..3e13d5a 100644 --- a/nihil.posix/posix.ccm +++ b/nihil.posix/posix.ccm @@ -1,28 +1,20 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; -#include -#include -#include -#include - -#include "nihil.hh" - export module nihil.posix; import nihil.error; export import :argv; export import :ensure_dir; -export import :exec; +export import :execl; export import :execlp; +export import :execshell; export import :execv; export import :execvp; export import :fd; export import :fexecv; +export import :fexecvp; export import :find_in_path; export import :getenv; export import :open; diff --git a/nihil.posix/process.cc b/nihil.posix/process.cc deleted file mode 100644 index 02642bc..0000000 --- a/nihil.posix/process.cc +++ /dev/null @@ -1,102 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -module nihil.posix; - -import nihil.error; - -namespace nihil { - -auto wait_result::okay(this wait_result const &self) -> bool -{ - return self.status() == 0; -} - -wait_result::operator bool(this wait_result const &self) -{ - return self.okay(); -} - -auto wait_result::status(this wait_result const &self) -> std::optional -{ - if (WIFEXITED(self._status)) - return WEXITSTATUS(self._status); - return {}; -} - -auto wait_result::signal(this wait_result const &self) -> std::optional -{ - if (WIFSIGNALED(self._status)) - return WTERMSIG(self._status); - return {}; -} - -wait_result::wait_result(int status) - : _status(status) -{} - -process::process(::pid_t pid) - : m_pid(pid) -{} - -process::~process() { - if (m_pid == -1) - return; - - auto status = int{}; - std::ignore = waitpid(m_pid, &status, WEXITED); -} - -process::process(process &&other) noexcept - : m_pid(std::exchange(other.m_pid, -1)) -{ -} - -auto process::operator=(this process &self, process &&other) noexcept - -> process & -{ - if (&self != &other) { - self.m_pid = std::exchange(other.m_pid, -1); - } - - return self; -} - -// Get the child's process id. -auto process::pid(this process const &self) noexcept -> ::pid_t -{ - return self.m_pid; -} - -auto process::wait(this process &&self) -> std::expected -{ - auto status = int{}; - auto ret = waitpid(self.m_pid, &status, 0); - if (ret == -1) - return std::unexpected(error(std::errc(errno))); - - return wait_result(status); -} - -auto process::release(this process &&self) -> ::pid_t -{ - auto const ret = self.pid(); - self.m_pid = -1; - return ret; -} - -} // namespace nihil diff --git a/nihil.posix/process.ccm b/nihil.posix/process.ccm index 425deac..ee7de15 100644 --- a/nihil.posix/process.ccm +++ b/nihil.posix/process.ccm @@ -1,7 +1,4 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include @@ -9,7 +6,7 @@ module; #include #include -#include +#include export module nihil.posix:process; @@ -17,72 +14,119 @@ import nihil.error; namespace nihil { -/* - * wait_result: the exit status of a process. - */ -export struct wait_result final { +// wait_result: the exit status of a process. +export struct wait_result final +{ // Return true if the process exited normally with an exit code of // zero, otherwise false. - [[nodiscard]] auto okay(this wait_result const &self) -> bool; - [[nodiscard]] explicit operator bool(this wait_result const &self); + [[nodiscard]] auto okay(this wait_result const &self) -> bool + { + return self.status() == 0; + } + + [[nodiscard]] explicit operator bool(this wait_result const &self) + { + return self.okay(); + } // Return the exit status, if any. - [[nodiscard]] auto status(this wait_result const &self) - -> std::optional; + [[nodiscard]] auto status(this wait_result const &self) -> std::optional + { + if (WIFEXITED(self.m_status)) + return WEXITSTATUS(self.m_status); + return {}; + } // Return the exit signal, if any. - [[nodiscard]] auto signal(this wait_result const &self) - -> std::optional; + [[nodiscard]] auto signal(this wait_result const &self) -> std::optional + { + if (WIFSIGNALED(self.m_status)) + return WTERMSIG(self.m_status); + return {}; + } private: friend struct process; - int _status; + int m_status; // Construct a new wait_result from the output of waitpid(). - wait_result(int status); + explicit wait_result(int status) + : m_status(status) + { + } }; -/* - * process: represents a process we created, which can be waited for. - */ -export struct process final { +// Represents a process we created, which can be waited for. +export struct process final +{ process() = delete; - /* - * Create a new process from a pid, which must be a child of the - * current process. - */ - process(::pid_t pid); + // Create a new process from a pid, which must be a child of the + // current process. + explicit process(::pid_t pid) + : m_pid(pid) + { + } // When destroyed, we automatically wait for the process to // avoid creating zombie processes. - ~process(); + ~process() + { + if (m_pid == -1) + return; + + auto status = int{}; + std::ignore = ::waitpid(m_pid, &status, WEXITED); + } // Movable. - process(process &&) noexcept; - auto operator=(this process &, process &&) noexcept -> process &; + process(process &&other) noexcept + : m_pid(std::exchange(other.m_pid, -1)) + {} + + auto operator=(this process &self, process &&other) noexcept -> process & + { + if (&self != &other) { + self.m_pid = std::exchange(other.m_pid, -1); + } + + return self; // NOLINT + } // Not copyable. process(process const &) = delete; auto operator=(this process &, process const &) -> process & = delete; // Get the child's process id. - [[nodiscard]] auto pid(this process const &self) noexcept -> ::pid_t; - - /* - * Wait for this process to exit (by calling waitpid()) and return - * its exit status. This destroys the process state, leaving this - * object in a moved-from state. - */ - [[nodiscard]] auto wait(this process &&self) - -> std::expected; - - /* - * Release this process so we won't try to wait for it when - * destroying this object. - */ - [[nodiscard]] auto release(this process &&self) -> ::pid_t; + [[nodiscard]] auto pid(this process const &self) noexcept -> ::pid_t + { + return self.m_pid; + } + + // Wait for this process to exit (by calling waitpid()) and return + // its exit status. This destroys the process state, leaving this + // object in a moved-from state. + [[nodiscard]] auto wait(this process &&self) // NOLINT + -> std::expected + { + auto status = int{}; + auto ret = waitpid(self.m_pid, &status, 0); + if (ret == -1) + return std::unexpected(error(std::errc(errno))); + + return wait_result(status); + } + + // Release this process so we won't try to wait for it when + // destroying this object. This will leave a zombie process + // unless the wait is done another way. + [[nodiscard]] auto release(this process &&self) -> ::pid_t // NOLINT + { + auto const ret = self.pid(); + self.m_pid = -1; + return ret; + } private: ::pid_t m_pid; diff --git a/nihil.posix/read_file.ccm b/nihil.posix/read_file.ccm index be9e102..3b4fd5b 100644 --- a/nihil.posix/read_file.ccm +++ b/nihil.posix/read_file.ccm @@ -32,7 +32,7 @@ read_file(std::filesystem::path const &filename, std::output_iterator auto &&iter) -> std::expected { - auto file = co_await open(filename, O_RDONLY); + auto file = co_await open(filename, open_read); auto constexpr bufsize = std::size_t{1024}; auto buffer = std::array{}; diff --git a/nihil.posix/rename.cc b/nihil.posix/rename.cc deleted file mode 100644 index 9203d08..0000000 --- a/nihil.posix/rename.cc +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -module nihil.posix; - -import nihil.error; - -namespace nihil { - -/* - * Rename a file. - */ -auto rename_file(std::filesystem::path const &oldp, - std::filesystem::path const &newp) - -> std::expected -{ - auto err = std::error_code(); - - std::filesystem::rename(oldp, newp, err); - - if (err) - return std::unexpected(error(err)); - - return {}; -} - - -} // namespace nihil diff --git a/nihil.posix/rename.ccm b/nihil.posix/rename.ccm index 796ec5b..a1b292e 100644 --- a/nihil.posix/rename.ccm +++ b/nihil.posix/rename.ccm @@ -1,7 +1,4 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; #include @@ -13,11 +10,19 @@ import nihil.error; namespace nihil { -/* - * Rename a file (or directory). - */ +// Rename a file (or directory). export [[nodiscard]] auto rename(std::filesystem::path const &oldp, std::filesystem::path const &newp) - -> std::expected; + -> std::expected +{ + auto err = std::error_code(); + + std::filesystem::rename(oldp, newp, err); + + if (err) + return std::unexpected(error(err)); + + return {}; +} } // namespace nihil diff --git a/nihil.posix/spawn.ccm b/nihil.posix/spawn.ccm index 9fa24e3..a185bb3 100644 --- a/nihil.posix/spawn.ccm +++ b/nihil.posix/spawn.ccm @@ -1,7 +1,4 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; /* @@ -29,7 +26,7 @@ export module nihil.posix:spawn; import nihil.monad; import :argv; -import :exec; +import :executor; import :open; import :process; @@ -152,7 +149,7 @@ private: export [[nodiscard]] auto make_fd_file(int fdno, std::filesystem::path const &file, - int flags, int mode = 0777) + open_flags flags, int mode = 0777) -> std::expected { auto fd = co_await open(file, flags, mode); @@ -166,19 +163,19 @@ make_fd_file(int fdno, std::filesystem::path const &file, export [[nodiscard]] inline auto stdin_devnull() -> std::expected { - return make_fd_file(stdin_fileno, "/dev/null", O_RDONLY); + return make_fd_file(stdin_fileno, "/dev/null", open_read); } export [[nodiscard]] inline auto stdout_devnull() -> std::expected { - return make_fd_file(stdout_fileno, "/dev/null", O_WRONLY); + return make_fd_file(stdout_fileno, "/dev/null", open_write); } export [[nodiscard]] inline auto stderr_devnull() -> std::expected { - return make_fd_file(stderr_fileno, "/dev/null", O_WRONLY); + return make_fd_file(stderr_fileno, "/dev/null", open_write); } /* diff --git a/nihil.posix/tempfile.cc b/nihil.posix/tempfile.cc deleted file mode 100644 index b1d3dee..0000000 --- a/nihil.posix/tempfile.cc +++ /dev/null @@ -1,128 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include - -#include -#include - -module nihil.posix; - -import nihil.flagset; -import :getenv; -import :open; - -namespace nihil { - -temporary_file::temporary_file(nihil::fd &&fd, - std::filesystem::path path) noexcept - : m_fd(std::move(fd)) - , m_path(std::move(path)) -{ -} - -temporary_file::temporary_file(nihil::fd &&fd) noexcept - : m_fd(std::move(fd)) -{ -} - -temporary_file::temporary_file(temporary_file &&other) noexcept - : m_fd(std::move(other.m_fd)) - , m_path(std::move(other.m_path)) -{ -} - -temporary_file::~temporary_file() //NOLINT(bugprone-exception-escape) -{ - if (m_fd) - release(); -} - -auto temporary_file::release(this temporary_file &self) -> void -{ - if (!self.m_fd) - throw std::logic_error( - "release() called on already-released tempfile"); - - if (!self.m_path.empty()) { - auto ec = std::error_code(); // ignore errors - remove(self.path(), ec); - - self.m_path.clear(); - } - - std::ignore = self.m_fd.close(); -} - -auto temporary_file::path(this temporary_file const &self) - -> std::filesystem::path const & -{ - if (self.m_path.empty()) - throw std::logic_error( - "path() called on unlinked temporary_file"); - - return self.m_path; -} - -auto temporary_file::fd(this temporary_file &self) -> nihil::fd & -{ - if (!self.m_fd) - throw std::logic_error("fd() called on empty temporary_file"); - - return self.m_fd; -} - -auto tempfile(tempfile_flags_t flags) -> std::expected -{ - auto rng = std::default_random_engine(std::random_device{}()); - - auto random_name = [&] -> std::string { - auto constexpr length = std::size_t{12}; - auto constexpr randchars = std::string_view( - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "0123456789"); - - auto dist = std::uniform_int_distribution<>( - 0, randchars.size() - 1); - auto ret = std::string(length, 0); - std::ranges::generate_n(ret.begin(), length, - [&] -> char { - return randchars[dist(rng)]; - }); - return ret; - }; - - auto dir = std::filesystem::path(getenv("TMPDIR").value_or("/tmp")); - - // Keep trying until we don't get EEXIST. - for (;;) { - auto filename = dir / (random_name() + ".tmp"); - auto fd = nihil::open(filename, O_RDWR | O_CREAT | O_EXCL, - 0600); - if (!fd) { - if (fd.error() == std::errc::file_exists) - continue; - return std::unexpected(fd.error()); - } - - if (flags & tempfile_unlink) { - auto ec = std::error_code(); - remove(filename, ec); - return temporary_file(std::move(*fd)); - } else { - return temporary_file(std::move(*fd), - std::move(filename)); - } - } -} - -} // namespace nihil diff --git a/nihil.posix/tempfile.ccm b/nihil.posix/tempfile.ccm index 82f3be4..e1510e5 100644 --- a/nihil.posix/tempfile.ccm +++ b/nihil.posix/tempfile.ccm @@ -1,16 +1,13 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; -/* - * tempfile: create a temporary file. - */ +// tempfile: create a temporary file. +#include #include #include #include +#include #include export module nihil.posix:tempfile; @@ -18,70 +15,139 @@ export module nihil.posix:tempfile; import nihil.error; import nihil.flagset; import :fd; +import :getenv; +import :open; namespace nihil { -struct tempfile_flags_tag {}; -export using tempfile_flags_t = flagset; +struct tempfile_flags_tag +{ +}; +export using tempfile_flags = flagset; // No flags. -export inline constexpr auto tempfile_none = tempfile_flags_t(); +export inline constexpr auto tempfile_none = tempfile_flags(); // Unlink the tempfile immediately after creating it -export inline constexpr auto tempfile_unlink = tempfile_flags_t::bit<0>(); - -export struct temporary_file final { - /* - * Fetch the file's fd. - */ - [[nodiscard]] auto fd(this temporary_file &) -> nihil::fd &; - - /* - * Fetch the name of this file. If tempfile_unlink was specified, - * throws std::logic_error. - */ - [[nodiscard]] auto path(this temporary_file const &) - -> std::filesystem::path const &; - - /* - * Release this temporary file, causing it to be deleted immediately. - * Throws std::logic_error if the file has already been released. - */ - auto release(this temporary_file &) -> void; - - /* - * Destructor; unlink the file if we didn't already. - */ - ~temporary_file(); +export inline constexpr auto tempfile_unlink = tempfile_flags::bit<0>(); + +export struct temporary_file final +{ + // Fetch the file's fd. + [[nodiscard]] auto fd(this temporary_file &self) -> nihil::fd & + { + if (!self.m_fd) + throw std::logic_error("fd() called on empty temporary_file"); + + return self.m_fd; + } + + // Fetch the name of this file. If tempfile_unlink was specified, + // throws std::logic_error. + [[nodiscard]] auto path(this temporary_file const &self) -> std::filesystem::path const & + { + if (self.m_path.empty()) + throw std::logic_error("path() called on unlinked temporary_file"); + + return self.m_path; + } + + // Release this temporary file, causing it to be closed and deleted immediately + // Throws std::logic_error if the file has already been released. + auto release(this temporary_file &self) -> void + { + if (!self.m_fd) + throw std::logic_error("release() called on already-released tempfile"); + + if (!self.m_path.empty()) { + auto ec = std::error_code(); // ignore errors + remove(self.path(), ec); + + self.m_path.clear(); + } + + std::ignore = self.m_fd.close(); + } + + // Destructor; unlink the file if we didn't already. + ~temporary_file() // NOLINT + { + if (m_fd) + release(); + } // Not copyable. temporary_file(temporary_file const &) = delete; // Movable. - temporary_file(temporary_file &&other) noexcept; + temporary_file(temporary_file &&other) noexcept = default; // Not assignable. - auto operator=(this temporary_file &, temporary_file const &) - -> temporary_file & = delete; - auto operator=(this temporary_file &, temporary_file &&) noexcept - -> temporary_file & = delete; + auto operator=(this temporary_file &, temporary_file const &) -> temporary_file & = delete; + auto + operator=(this temporary_file &, temporary_file &&) noexcept -> temporary_file & = delete; private: // The file descriptor for the file. - nihil::fd m_fd; - std::filesystem::path m_path; + nihil::fd m_fd; + std::filesystem::path m_path; + + temporary_file(nihil::fd &&fd, std::filesystem::path path) noexcept + : m_fd(std::move(fd)) + , m_path(std::move(path)) + { + } - temporary_file(nihil::fd &&fd, std::filesystem::path) noexcept; - temporary_file(nihil::fd &&fd) noexcept; + explicit temporary_file(nihil::fd &&fd) noexcept + : m_fd(std::move(fd)) + { + } - friend auto tempfile(tempfile_flags_t flags) - -> std::expected; + friend auto tempfile(tempfile_flags flags) -> std::expected; }; /* * Create a temporary file and return it. */ -export [[nodiscard]] auto tempfile(tempfile_flags_t flags = tempfile_none) - -> std::expected; +export [[nodiscard]] auto +tempfile(tempfile_flags flags = tempfile_none) -> std::expected +{ + auto rng = std::default_random_engine(std::random_device{}()); + + auto random_name = [&] -> std::string { + auto constexpr length = std::size_t{12}; + auto constexpr randchars = std::string_view("abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789"); + + auto dist = std::uniform_int_distribution<>(0, randchars.size() - 1); + auto ret = std::string(length, 0); + std::ranges::generate_n(ret.begin(), length, + [&] -> char { return randchars[dist(rng)]; }); + return ret; + }; + + auto dir = std::filesystem::path(getenv("TMPDIR").value_or("/tmp")); // NOLINT + + // Keep trying until we don't get EEXIST. + for (;;) { + auto filename = dir / (random_name() + ".tmp"); + auto fd = + nihil::open(filename, open_readwrite | open_create | open_exclusive, 0600); + if (!fd) { + if (fd.error() == std::errc::file_exists) + continue; + return std::unexpected(fd.error()); + } + + if (flags & tempfile_unlink) { + auto ec = std::error_code(); + remove(filename, ec); + return temporary_file(std::move(*fd)); + } + + return temporary_file(std::move(*fd), std::move(filename)); + } +} } // namespace nihil diff --git a/nihil.posix/tempfile.test.cc b/nihil.posix/tempfile.test.cc new file mode 100644 index 0000000..b1c7604 --- /dev/null +++ b/nihil.posix/tempfile.test.cc @@ -0,0 +1,90 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.posix; + +TEST_CASE("posix.tempfile: create", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); +} + +TEST_CASE("posix.tempfile: create and release", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); + + file->release(); + REQUIRE(exists(path) == false); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create and double release", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); + + file->release(); + REQUIRE(exists(path) == false); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->release(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked and release", + "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); + + file->release(); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked and double release", + "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); + + file->release(); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->release(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} diff --git a/nihil.posix/test.fd.cc b/nihil.posix/test.fd.cc deleted file mode 100644 index 5c282af..0000000 --- a/nihil.posix/test.fd.cc +++ /dev/null @@ -1,204 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include - -#include -#include - -#include - -import nihil.error; -import nihil.posix; - -using namespace std::literals; - -namespace { - -// Test if an fd is open. -auto fd_is_open(int fd) -> bool { - auto const ret = ::fcntl(fd, F_GETFL); - return ret == 0; -} - -} // anonymous namespace - -TEST_CASE("fd: construct empty", "[fd]") { - nihil::fd fd; - - REQUIRE(!fd); - REQUIRE_THROWS_AS(fd.get(), std::logic_error); -} - -TEST_CASE("fd: construct from fd", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - { - auto fd = nihil::fd(file); - REQUIRE(fd_is_open(fd.get())); - } - - REQUIRE(!fd_is_open(file)); -} - -TEST_CASE("fd: close", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - auto fd = nihil::fd(file); - REQUIRE(fd); - - auto const ret = fd.close(); - REQUIRE(ret); - REQUIRE(!fd_is_open(file)); -} - -TEST_CASE("fd: move construct", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - auto fd1 = nihil::fd(file); - REQUIRE(fd_is_open(fd1.get())); - - auto fd2(std::move(fd1)); - REQUIRE(!fd1); //NOLINT - REQUIRE(fd2); - REQUIRE(fd2.get() == file); - REQUIRE(fd_is_open(file)); -} - -TEST_CASE("fd: move assign", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - auto fd1 = nihil::fd(file); - REQUIRE(fd_is_open(fd1.get())); - - auto fd2 = nihil::fd(); - REQUIRE(!fd2); - - fd2 = std::move(fd1); - - REQUIRE(!fd1); //NOLINT - REQUIRE(fd2); - REQUIRE(fd2.get() == file); - REQUIRE(fd_is_open(file)); -} - -TEST_CASE("fd: release", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - auto fd = nihil::fd(file); - auto fdesc = std::move(fd).release(); - REQUIRE(!fd); //NOLINT - REQUIRE(fdesc == file); -} - -TEST_CASE("fd: dup", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - auto fd = nihil::fd(file); - REQUIRE(fd); - - auto fd2 = dup(fd); - REQUIRE(fd2); - REQUIRE(fd.get() != fd2->get()); -} - -TEST_CASE("fd: dup2", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - REQUIRE(!fd_is_open(666)); - - auto fd = nihil::fd(file); - auto fd2 = dup(fd, 666); - - REQUIRE(fd); - REQUIRE(fd2); - REQUIRE(fd2->get() == 666); -} - -TEST_CASE("fd: flags", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - auto fd = nihil::fd(file); - - { - auto const ret = replaceflags(fd, 0); - REQUIRE(ret); - REQUIRE(getflags(fd) == 0); - } - - { - auto const ret = setflags(fd, O_NONBLOCK); - REQUIRE(ret == O_NONBLOCK); - REQUIRE(getflags(fd) == O_NONBLOCK); - } - - { - auto const ret = setflags(fd, O_SYNC); - REQUIRE(ret == (O_NONBLOCK|O_SYNC)); - REQUIRE(getflags(fd) == (O_NONBLOCK|O_SYNC)); - } - - { - auto const ret = clearflags(fd, O_NONBLOCK); - REQUIRE(ret == O_SYNC); - REQUIRE(getflags(fd) == O_SYNC); - } -} - -TEST_CASE("fd: fdflags", "[fd]") { - auto file = ::open("/dev/null", O_RDONLY); - REQUIRE(file > 0); - - auto fd = nihil::fd(file); - - { - auto const ret = replacefdflags(fd, 0); - REQUIRE(ret); - REQUIRE(getfdflags(fd) == 0); - } - - { - auto const ret = setfdflags(fd, FD_CLOEXEC); - REQUIRE(ret == FD_CLOEXEC); - REQUIRE(getfdflags(fd) == FD_CLOEXEC); - } - - { - auto const ret = clearfdflags(fd, FD_CLOEXEC); - REQUIRE(ret == 0); - REQUIRE(getfdflags(fd) == 0); - } -} - -TEST_CASE("fd: pipe, read, write", "[fd]") { - auto fds = nihil::pipe(); - REQUIRE(fds); - - /* - * Note: traditionally, the first fd is the reading side, and the second fd - * is the writing side. Some platforms (e.g., macOS) still behave this way. - */ - - auto [fd1, fd2] = std::move(*fds); - - auto constexpr test_string = "test string"sv; - - auto ret = write(fd2, test_string); - REQUIRE(ret); - REQUIRE(*ret == test_string.size()); - - auto readbuf = std::array{}; - auto read_buf = read(fd1, readbuf); - REQUIRE(read_buf); - REQUIRE(std::string_view(*read_buf) == test_string); -} diff --git a/nihil.posix/test.getenv.cc b/nihil.posix/test.getenv.cc deleted file mode 100644 index 9e10c16..0000000 --- a/nihil.posix/test.getenv.cc +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include -#include - -#include - -#include - -import nihil.error; -import nihil.posix; - -TEST_CASE("getenv: existing value", "[getenv]") -{ - auto constexpr *name = "NIHIL_TEST_VAR"; - auto constexpr *value = "test is a test"; - - REQUIRE(::setenv(name, value, 1) == 0); - - auto const s = nihil::getenv(name); - REQUIRE(s); - REQUIRE(*s == value); -} - -TEST_CASE("getenv: non-existing value", "[getenv]") -{ - auto constexpr *name = "NIHIL_TEST_VAR"; - - REQUIRE(::unsetenv(name) == 0); - - auto const s = nihil::getenv(name); - REQUIRE(!s); - REQUIRE(s.error() == std::errc::no_such_file_or_directory); -} - -// Force the call to getenv_r() to reallocate. -TEST_CASE("getenv: long value") -{ - auto constexpr *name = "NIHIL_TEST_VAR"; - auto const value = std::string(4096, 'a'); - - REQUIRE(::setenv(name, value.c_str(), 1) == 0); - - auto const s = nihil::getenv(name); - REQUIRE(s); - REQUIRE(*s == value); -} diff --git a/nihil.posix/test.spawn.cc b/nihil.posix/test.spawn.cc index ca6c076..c5b4f53 100644 --- a/nihil.posix/test.spawn.cc +++ b/nihil.posix/test.spawn.cc @@ -6,27 +6,6 @@ import nihil.posix; -TEST_CASE("spawn: system", "[spawn]") -{ - using namespace nihil; - - auto exec = shell("x=1; echo $x"); - REQUIRE(exec); - - auto output = std::string(); - auto capture = make_capture(stdout_fileno, output); - REQUIRE(capture); - - auto proc = spawn(*exec, *capture); - REQUIRE(proc); - - auto status = std::move(*proc).wait(); - REQUIRE(status); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} - TEST_CASE("spawn: execv", "[spawn]") { using namespace nihil; @@ -69,25 +48,6 @@ TEST_CASE("spawn: execvp", "[spawn]") { REQUIRE(output == "1\n"); } -TEST_CASE("spawn: execl", "[spawn]") { - using namespace nihil; - - auto exec = execl("/bin/sh", "sh", "-c", "x=1; echo $x"); - REQUIRE(exec); - - auto output = std::string(); - auto capture = make_capture(stdout_fileno, output); - REQUIRE(capture); - - auto proc = spawn(*exec, *capture); - REQUIRE(proc); - - auto status = std::move(*proc).wait(); - REQUIRE(status); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} TEST_CASE("spawn: execlp", "[spawn]") { using namespace nihil; diff --git a/nihil.posix/test.tempfile.cc b/nihil.posix/test.tempfile.cc deleted file mode 100644 index b1c7604..0000000 --- a/nihil.posix/test.tempfile.cc +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include - -#include - -import nihil.posix; - -TEST_CASE("posix.tempfile: create", "[nihil][nihil.posix]") -{ - auto file = nihil::tempfile(); - REQUIRE(file); - REQUIRE(file->fd()); - - auto path = file->path(); - REQUIRE(exists(path) == true); -} - -TEST_CASE("posix.tempfile: create and release", "[nihil][nihil.posix]") -{ - auto file = nihil::tempfile(); - REQUIRE(file); - REQUIRE(file->fd()); - - auto path = file->path(); - REQUIRE(exists(path) == true); - - file->release(); - REQUIRE(exists(path) == false); - - REQUIRE_THROWS_AS(file->fd(), std::logic_error); - REQUIRE_THROWS_AS(file->path(), std::logic_error); -} - -TEST_CASE("posix.tempfile: create and double release", "[nihil][nihil.posix]") -{ - auto file = nihil::tempfile(); - REQUIRE(file->fd()); - - auto path = file->path(); - REQUIRE(exists(path) == true); - - file->release(); - REQUIRE(exists(path) == false); - - REQUIRE_THROWS_AS(file->fd(), std::logic_error); - REQUIRE_THROWS_AS(file->release(), std::logic_error); - REQUIRE_THROWS_AS(file->path(), std::logic_error); -} - -TEST_CASE("posix.tempfile: create unlinked", "[nihil][nihil.posix]") -{ - auto file = nihil::tempfile(nihil::tempfile_unlink); - REQUIRE(file); - REQUIRE(file->fd()); - - REQUIRE_THROWS_AS(file->path(), std::logic_error); -} - -TEST_CASE("posix.tempfile: create unlinked and release", - "[nihil][nihil.posix]") -{ - auto file = nihil::tempfile(nihil::tempfile_unlink); - REQUIRE(file); - REQUIRE(file->fd()); - - REQUIRE_THROWS_AS(file->path(), std::logic_error); - - file->release(); - - REQUIRE_THROWS_AS(file->fd(), std::logic_error); - REQUIRE_THROWS_AS(file->path(), std::logic_error); -} - -TEST_CASE("posix.tempfile: create unlinked and double release", - "[nihil][nihil.posix]") -{ - auto file = nihil::tempfile(nihil::tempfile_unlink); - REQUIRE(file->fd()); - - REQUIRE_THROWS_AS(file->path(), std::logic_error); - - file->release(); - - REQUIRE_THROWS_AS(file->fd(), std::logic_error); - REQUIRE_THROWS_AS(file->release(), std::logic_error); - REQUIRE_THROWS_AS(file->path(), std::logic_error); -} diff --git a/nihil.posix/write_file.ccm b/nihil.posix/write_file.ccm index 867e0db..ce21e6b 100644 --- a/nihil.posix/write_file.ccm +++ b/nihil.posix/write_file.ccm @@ -35,7 +35,7 @@ auto write_file(std::filesystem::path const &filename, int mode = 0777) -> std::expected { - auto file = co_await open(filename, O_CREAT|O_WRONLY, mode); + auto file = co_await open(filename, open_write | open_create, mode); auto nbytes = co_await write(file, range); co_return nbytes; } -- cgit v1.2.3