diff options
Diffstat (limited to 'nihil.posix')
| -rw-r--r-- | nihil.posix/CMakeLists.txt | 7 | ||||
| -rw-r--r-- | nihil.posix/execlp.test.cc | 14 | ||||
| -rw-r--r-- | nihil.posix/execvp.ccm | 9 | ||||
| -rw-r--r-- | nihil.posix/execvp.test.cc | 79 | ||||
| -rw-r--r-- | nihil.posix/find_in_path.ccm | 39 | ||||
| -rw-r--r-- | nihil.posix/find_in_path.test.cc | 70 | ||||
| -rw-r--r-- | nihil.posix/open.test.cc | 38 | ||||
| -rw-r--r-- | nihil.posix/open_in_path.ccm | 39 | ||||
| -rw-r--r-- | nihil.posix/open_in_path.test.cc | 82 | ||||
| -rw-r--r-- | nihil.posix/posix.ccm | 1 | ||||
| -rw-r--r-- | nihil.posix/stat.ccm | 36 | ||||
| -rw-r--r-- | nihil.posix/stat.test.cc | 59 | ||||
| -rw-r--r-- | nihil.posix/test.spawn.cc | 77 |
13 files changed, 438 insertions, 112 deletions
diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt index 61e83df..1fcc365 100644 --- a/nihil.posix/CMakeLists.txt +++ b/nihil.posix/CMakeLists.txt @@ -27,6 +27,7 @@ target_sources(nihil.posix read_file.ccm rename.ccm spawn.ccm + stat.ccm tempfile.ccm write_file.ccm ) @@ -40,9 +41,13 @@ if(NIHIL_TESTS) execlp.test.cc execshell.test.cc execv.test.cc + execvp.test.cc fd.test.cc + find_in_path.test.cc getenv.test.cc - test.spawn.cc + open.test.cc + open_in_path.test.cc + stat.test.cc tempfile.test.cc ) diff --git a/nihil.posix/execlp.test.cc b/nihil.posix/execlp.test.cc index cedf871..aa32253 100644 --- a/nihil.posix/execlp.test.cc +++ b/nihil.posix/execlp.test.cc @@ -64,4 +64,18 @@ SCENARIO("nihil::execlp() returns the shell's exit code") } } +SCENARIO("nihil::execlp() returns an error if the executable is not in $PATH") +{ + GIVEN("An execlp object for a non-existent executable") + { + auto exec = nihil::execlp("nihil_no_such_executable", "x"); + + THEN("A no_such_file_or_directory error is returned") + { + REQUIRE(!exec); + REQUIRE(exec.error().root_cause() == std::errc::no_such_file_or_directory); + } + } +} + } // anonymous namespace diff --git a/nihil.posix/execvp.ccm b/nihil.posix/execvp.ccm index 680a13e..270e311 100644 --- a/nihil.posix/execvp.ccm +++ b/nihil.posix/execvp.ccm @@ -1,6 +1,7 @@ // This source code is released into the public domain. module; +#include <coroutine> #include <string> #include <expected> #include <format> @@ -8,6 +9,7 @@ module; export module nihil.posix:execvp; import nihil.error; +import nihil.monad; import :argv; import :execv; import :find_in_path; @@ -20,11 +22,8 @@ namespace nihil { export [[nodiscard]] auto execvp(std::string_view file, argv &&argv) -> std::expected<execv, error> { - 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)); + auto filename = co_await find_in_path(file); + co_return execv(std::move(filename), std::move(argv)); } } // namespace nihil diff --git a/nihil.posix/execvp.test.cc b/nihil.posix/execvp.test.cc new file mode 100644 index 0000000..e34823d --- /dev/null +++ b/nihil.posix/execvp.test.cc @@ -0,0 +1,79 @@ +// This source code is released into the public domain. + +#include <catch2/catch_test_macros.hpp> + +import nihil.posix; + +namespace { + +SCENARIO("nihil::execvp() can be used to spawn a shell") +{ + GIVEN("An execvp object") + { + auto exec = nihil::execvp("sh", nihil::argv{"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::execvp() returns the shell's exit code") +{ + GIVEN("An execvp object") + { + auto exec = nihil::execvp("sh", nihil::argv{"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); + } + } + } +} + +SCENARIO("nihil::execvp() returns an error if the executable is not in $PATH") +{ + GIVEN("An execvp object for a non-existent executable") + { + auto exec = nihil::execvp("nihil_no_such_executable", nihil::argv{"x"}); + + THEN("A no_such_file_or_directory error is returned") + { + REQUIRE(!exec); + REQUIRE(exec.error().root_cause() == std::errc::no_such_file_or_directory); + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/find_in_path.ccm b/nihil.posix/find_in_path.ccm index 7bfa3b9..61df669 100644 --- a/nihil.posix/find_in_path.ccm +++ b/nihil.posix/find_in_path.ccm @@ -1,6 +1,7 @@ // This source code is released into the public domain. module; +#include <expected> #include <filesystem> #include <optional> #include <ranges> @@ -16,27 +17,26 @@ 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 is not executable, returns std::nullopt. - */ -export [[nodiscard]] auto -find_in_path(std::filesystem::path const &file) -> std::optional<std::filesystem::path> +// Find an executable by searching the given path string, which should be a colon-separated list of +// directories, and return the full path. If the file can't be found or is not executable, returns +// an appropriate error. +export [[nodiscard]] auto find_in_path(std::filesystem::path const &file, std::string_view path) + -> std::expected<std::filesystem::path, error> { - using namespace std::literals; - - auto try_return = [](std::filesystem::path file) -> std::optional<std::filesystem::path> { + auto try_return = + [](std::filesystem::path file) -> std::expected<std::filesystem::path, error> { auto ret = ::access(file.string().c_str(), X_OK); if (ret == 0) return {std::move(file)}; - return {}; + return std::unexpected(error(std::errc(errno))); }; // Absolute pathname skips the search. if (file.is_absolute()) return try_return(file); - auto const path = getenv("PATH").value_or(_PATH_DEFPATH); // NOLINT + // Default to ENOENT as the error. + auto err = error(std::errc::no_such_file_or_directory); for (auto &&dir : path | std::views::split(':')) { // An empty $PATH element means cwd. @@ -44,10 +44,23 @@ find_in_path(std::filesystem::path const &file) -> std::optional<std::filesystem : std::filesystem::path(std::string_view(dir)); if (auto ret = try_return(sdir / file); ret) - return {ret}; + return ret; + // If we get an error other than ENOENT, cache it to return to the caller. + // This means we can propagate access() errors. + else if (ret.error().root_cause() != std::errc::no_such_file_or_directory) + err = std::move(ret.error()); } - return {}; + return std::unexpected(std::move(err)); +} + +// 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 is not executable, returns an appropriate error. +export [[nodiscard]] auto +find_in_path(std::filesystem::path const &file) -> std::expected<std::filesystem::path, error> +{ + auto const path = getenv("PATH").value_or(_PATH_DEFPATH); // NOLINT + return find_in_path(file, path); } } // namespace nihil diff --git a/nihil.posix/find_in_path.test.cc b/nihil.posix/find_in_path.test.cc new file mode 100644 index 0000000..b2f6240 --- /dev/null +++ b/nihil.posix/find_in_path.test.cc @@ -0,0 +1,70 @@ +// This source code is released into the public domain. + +#include <catch2/catch_test_macros.hpp> + +import nihil.error; +import nihil.posix; + +namespace { + +SCENARIO("nihil::find_in_path can find /bin/sh in the default path") +{ + GIVEN ("A call to find_in_path for /bin/sh") { + auto ret = nihil::find_in_path("sh"); + + THEN ("The return value should be /bin/sh") { + REQUIRE(ret); + REQUIRE(ret.value() == "/bin/sh"); + } + } +} + +SCENARIO("nihil::find_in_path works with an explicit path") +{ + GIVEN ("A call to find_in_path with an explicit path") { + auto ret = nihil::find_in_path("sh", "/bin:/usr/bin"); + + THEN ("The return value should be /bin/sh") { + REQUIRE(ret); + REQUIRE(ret.value() == "/bin/sh"); + } + } +} + +SCENARIO("nihil::find_in_path works with an absolute path") +{ + GIVEN ("A call to find_in_path with an absolute filename and an empty path") { + auto ret = nihil::find_in_path("/bin/sh", ""); + + THEN ("The return value should be /bin/sh") { + REQUIRE(ret); + REQUIRE(ret.value() == "/bin/sh"); + } + } +} + +SCENARIO("nihil::find_in_path returns ENOENT when the executable is not found") +{ + GIVEN ("A call to find_in_path for a non-existent executable") { + auto ret = nihil::find_in_path("nihil_no_such_executable"); + + THEN ("The return value should be std::errc::no_such_file_or_directory") { + REQUIRE(!ret); + REQUIRE(ret.error() == std::errc::no_such_file_or_directory); + } + } +} + +SCENARIO("nihil::find_in_path returns errors from access(2)") +{ + GIVEN ("A call to find_in_path for a non-executable file") { + auto ret = nihil::find_in_path("passwd", "/etc"); + + THEN ("The return value should be std::errc::permission_denied") { + REQUIRE(!ret); + REQUIRE(ret.error() == std::errc::permission_denied); + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/open.test.cc b/nihil.posix/open.test.cc new file mode 100644 index 0000000..bb8bcc9 --- /dev/null +++ b/nihil.posix/open.test.cc @@ -0,0 +1,38 @@ +// This source code is released into the public domain. + +#include <catch2/catch_test_macros.hpp> + +import nihil.error; +import nihil.posix; + +namespace { + +SCENARIO("nihil::open() can open an existing file") +{ + GIVEN ("A call to open() for /bin/sh") { + auto ret = nihil::open("/bin/sh", nihil::open_read); + + THEN ("The returned file descriptor should be /bin/sh") { + REQUIRE(ret); + + auto sb1 = nihil::stat(*ret).value(); + auto sb2 = nihil::stat("/bin/sh").value(); + REQUIRE(sb1.st_ino == sb2.st_ino); + REQUIRE(sb1.st_dev == sb2.st_dev); + } + } +} + +SCENARIO("nihil::open_in_path returns ENOENT when the file is not found") +{ + GIVEN ("A call to open for a non-existent file") { + auto ret = nihil::open("/nihil_no_such_file", nihil::open_read); + + THEN ("The return value should be std::errc::no_such_file_or_directory") { + REQUIRE(!ret); + REQUIRE(ret.error() == std::errc::no_such_file_or_directory); + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/open_in_path.ccm b/nihil.posix/open_in_path.ccm index 7ff5812..e8c1761 100644 --- a/nihil.posix/open_in_path.ccm +++ b/nihil.posix/open_in_path.ccm @@ -1,6 +1,7 @@ // This source code is released into the public domain. module; +#include <expected> #include <filesystem> #include <optional> #include <ranges> @@ -17,35 +18,41 @@ 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<fd> +// Find an executable in the given path, which should be a colon-separated list of directories, and +// open it with O_EXEC. If the file can't be found or can't be opened, returns an appropriate error. +export [[nodiscard]] auto +open_in_path(std::filesystem::path const &file, std::string_view path) -> std::expected<fd, error> { - using namespace std::literals; - - auto try_open = [](std::filesystem::path const &file) -> std::optional<fd> { - 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); + return open(file, open_exec); - auto path = getenv("PATH").value_or(_PATH_DEFPATH); // NOLINT + // Default to ENOENT as the error. + auto err = error(std::errc::no_such_file_or_directory); 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) + if (auto ret = open(sdir / file, open_exec); ret) return ret; + // If we get an error other than ENOENT, cache it to return to the caller. + // This means we can propagate open() errors. + else if (ret.error().root_cause() != std::errc::no_such_file_or_directory) + err = std::move(ret.error()); } - return {}; + return std::unexpected(std::move(err)); +} + +// Find an executable in $PATH and open it. If $PATH is not set, uses _PATH_DEFPATH. +// If the file can't be found or can't be opened, returns an appropriate error. +export [[nodiscard]] auto +open_in_path(std::filesystem::path const &file) -> std::expected<fd, error> +{ + auto const path = getenv("PATH").value_or(_PATH_DEFPATH); // NOLINT + return open_in_path(file, path); } } // namespace nihil diff --git a/nihil.posix/open_in_path.test.cc b/nihil.posix/open_in_path.test.cc new file mode 100644 index 0000000..13d6b49 --- /dev/null +++ b/nihil.posix/open_in_path.test.cc @@ -0,0 +1,82 @@ +// This source code is released into the public domain. + +#include <catch2/catch_test_macros.hpp> + +import nihil.error; +import nihil.posix; + +namespace { + +SCENARIO("nihil::open_in_path can find /bin/sh in the default path") +{ + GIVEN ("A call to open_in_path for /bin/sh") { + auto ret = nihil::open_in_path("sh"); + + THEN ("The returned file descriptor should be /bin/sh") { + REQUIRE(ret); + + auto sb1 = nihil::stat(*ret).value(); + auto sb2 = nihil::stat("/bin/sh").value(); + REQUIRE(sb1.st_ino == sb2.st_ino); + REQUIRE(sb1.st_dev == sb2.st_dev); + } + } +} + +SCENARIO("nihil::open_in_path works with an explicit path") +{ + GIVEN ("A call to open_in_path with an explicit path") { + auto ret = nihil::open_in_path("sh", "/bin:/usr/bin"); + + THEN ("The returned file descriptor should be /bin/sh") { + REQUIRE(ret); + + auto sb1 = nihil::stat(*ret).value(); + auto sb2 = nihil::stat("/bin/sh").value(); + REQUIRE(sb1.st_ino == sb2.st_ino); + REQUIRE(sb1.st_dev == sb2.st_dev); + } + } +} + +SCENARIO("nihil::open_in_path works with an absolute path") +{ + GIVEN ("A call to open_in_path with an absolute filename and an empty path") { + auto ret = nihil::open_in_path("/bin/sh", ""); + + THEN ("The returned file descriptor should be /bin/sh") { + REQUIRE(ret); + + auto sb1 = nihil::stat(*ret).value(); + auto sb2 = nihil::stat("/bin/sh").value(); + REQUIRE(sb1.st_ino == sb2.st_ino); + REQUIRE(sb1.st_dev == sb2.st_dev); + } + } +} + +SCENARIO("nihil::open_in_path returns ENOENT when the executable is not found") +{ + GIVEN ("A call to open_in_path for a non-existent executable") { + auto ret = nihil::open_in_path("nihil_no_such_executable"); + + THEN ("The return value should be std::errc::no_such_file_or_directory") { + REQUIRE(!ret); + REQUIRE(ret.error() == std::errc::no_such_file_or_directory); + } + } +} + +SCENARIO("nihil::open_in_path returns errors from open(2)") +{ + GIVEN ("A call to open_in_path for a non-executable file") { + auto ret = nihil::open_in_path("passwd", "/etc"); + + THEN ("The return value should be std::errc::permission_denied") { + REQUIRE(!ret); + REQUIRE(ret.error() == std::errc::permission_denied); + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/posix.ccm b/nihil.posix/posix.ccm index 3e13d5a..c49a992 100644 --- a/nihil.posix/posix.ccm +++ b/nihil.posix/posix.ccm @@ -23,5 +23,6 @@ export import :process; export import :read_file; export import :rename; export import :spawn; +export import :stat; export import :tempfile; export import :write_file; diff --git a/nihil.posix/stat.ccm b/nihil.posix/stat.ccm new file mode 100644 index 0000000..6a0cabf --- /dev/null +++ b/nihil.posix/stat.ccm @@ -0,0 +1,36 @@ +// This source code is released into the public domain. +module; + +// Basic wrappers around stat() and fstat(). + +#include <expected> +#include <filesystem> + +#include <sys/stat.h> + +export module nihil.posix:stat; + +import :fd; + +namespace nihil { + +export [[nodiscard]] auto +stat(std::filesystem::path const &path) -> std::expected<struct ::stat, error> +{ + struct ::stat sb{}; + auto ret = ::stat(path.string().c_str(), &sb); + if (ret == -1) + return std::unexpected(error(std::errc(errno))); + return sb; +} + +export [[nodiscard]] auto stat(fd const &fd) -> std::expected<struct ::stat, error> +{ + struct ::stat sb{}; + auto ret = ::fstat(fd.get(), &sb); + if (ret == -1) + return std::unexpected(error(std::errc(errno))); + return sb; +} + +} // namespace nihil diff --git a/nihil.posix/stat.test.cc b/nihil.posix/stat.test.cc new file mode 100644 index 0000000..cf1e29c --- /dev/null +++ b/nihil.posix/stat.test.cc @@ -0,0 +1,59 @@ +// This source code is released into the public domain. + +import nihil.error; +import nihil.posix; + +#include <sys/stat.h> + +#include <catch2/catch_test_macros.hpp> + +namespace { + +SCENARIO("nihil::stat() on an existing file") +{ + GIVEN("A call to stat() on /etc/passwd") + { + auto sb = nihil::stat("/etc/passwd"); + + THEN("The returned struct is correct") + { + REQUIRE(sb); + REQUIRE(S_ISREG(sb->st_mode)); + } + } +} + +SCENARIO("nihil::stat() on a non-existent file") +{ + GIVEN("A call to stat() on /nonesuchfile") + { + auto sb = nihil::stat("/nonesuchfile"); + + THEN("std::errc::no_such_file_or_directory is returned") + { + REQUIRE(!sb); + REQUIRE(sb.error() == std::errc::no_such_file_or_directory); + } + } +} + +SCENARIO("nihil::stat() on an open file descriptor") +{ + GIVEN("An fd referring to /etc/password") + { + auto fd = nihil::open("/etc/passwd", nihil::open_read).value(); + + WHEN("nihil::stat() is called on the fd") + { + auto sb = nihil::stat(fd); + + THEN("The returned struct is correct") + { + REQUIRE(sb); + REQUIRE(S_ISREG(sb->st_mode)); + } + } + } +} + +} // anonymous namespace diff --git a/nihil.posix/test.spawn.cc b/nihil.posix/test.spawn.cc deleted file mode 100644 index c5b4f53..0000000 --- a/nihil.posix/test.spawn.cc +++ /dev/null @@ -1,77 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include <catch2/catch_test_macros.hpp> - -import nihil.posix; - -TEST_CASE("spawn: execv", "[spawn]") { - using namespace nihil; - - auto args = argv({"sh", "-c", "x=1; echo $x"}); - auto exec = execv("/bin/sh", std::move(args)); - - 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(); - if (!status) - FAIL(to_string(status.error())); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} - -TEST_CASE("spawn: execvp", "[spawn]") { - using namespace nihil; - - auto args = argv({"sh", "-c", "x=1; echo $x"}); - auto exec = execvp("sh", std::move(args)); - 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; - - auto exec = execlp("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 failure", "[spawn]") { - using namespace nihil; - - auto exec = execlp("nihil_no_such_executable", "x"); - REQUIRE(!exec); -} |
