diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-10-07 19:58:28 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-10-19 00:13:47 +0200 |
| commit | 4f6e76493fb74f5385d5a14dce3a01c9901802ed (patch) | |
| tree | a38722ec832fd44ad34257730e075e8b07825bd0 | |
| parent | c87f9627efc8b612eb9b000acfcc6731cad15765 (diff) | |
paths: New module
Path utilities (doh)
| -rw-r--r-- | meson.build | 35 | ||||
| -rw-r--r-- | src/paths.cc | 100 | ||||
| -rw-r--r-- | src/paths.hh | 20 | ||||
| -rw-r--r-- | test/paths.cc | 63 | ||||
| -rw-r--r-- | test/testenv.cc | 44 | ||||
| -rw-r--r-- | test/testenv.hh | 22 |
6 files changed, 284 insertions, 0 deletions
diff --git a/meson.build b/meson.build index f98276c..291b3f9 100644 --- a/meson.build +++ b/meson.build @@ -83,6 +83,20 @@ str_lib = library( ) str_dep = declare_dependency(link_with: str_lib) +paths_lib = library( + 'paths', + sources: [ + 'src/paths.cc', + 'src/paths.hh', + ], + include_directories: inc, + dependencies: [str_dep], +) +paths_dep = declare_dependency( + link_with: paths_lib, + dependencies: [str_dep], +) + bluetooth_jukebox = executable( 'bluetooth-jukebox', sources: [ @@ -127,6 +141,16 @@ io_test_helper_dep = declare_dependency( dependencies: io_dep, ) +testenv_lib = library( + 'testenv', + sources: [ + 'test/testenv.cc', + 'test/testenv.hh', + ], + include_directories: inc, +) +testenv_dep = declare_dependency(link_with: testenv_lib) + test('line', executable( 'test_line', sources: ['test/line.cc'], @@ -169,6 +193,17 @@ test('buffer', executable( ], )) +test('paths', executable( + 'test_paths', + sources: ['test/paths.cc'], + include_directories: inc, + dependencies : [ + paths_dep, + test_dependencies, + testenv_dep, + ], +)) + run_clang_tidy = find_program('run-clang-tidy', required: false) if run_clang_tidy.found() diff --git a/src/paths.cc b/src/paths.cc new file mode 100644 index 0000000..091be6e --- /dev/null +++ b/src/paths.cc @@ -0,0 +1,100 @@ +#include "paths.hh" + +#include "str.hh" + +#include <algorithm> +#include <cerrno> +#include <cstddef> +#include <cstdlib> +#include <iterator> +#include <memory> +#include <pwd.h> +#include <string_view> +#include <unistd.h> +#include <unordered_set> +#include <vector> + +namespace paths { + +namespace { + +std::vector<std::filesystem::path> xdg_read_dirs( + const char* userdir_env_name, std::string_view userdir_home_default, + const char* dirs_env_name, + std::vector<std::filesystem::path> const& dirs_default_value) { + std::vector<std::filesystem::path> ret; + std::unordered_set<std::filesystem::path> tmp; + auto* env_userdir = getenv(userdir_env_name); + if (env_userdir != nullptr && env_userdir[0] != '\0') { + ret.emplace_back(env_userdir); + } else { + ret.emplace_back(home() / userdir_home_default); + } + tmp.insert(ret.back()); + auto* env_dirs = getenv(dirs_env_name); + if (env_dirs != nullptr && env_dirs[0] != '\0') { + for (auto dir : str::split(env_dirs, ':')) { + if (tmp.emplace(dir).second) { + ret.emplace_back(dir); + } + } + } else { + std::ranges::copy_if( + dirs_default_value, std::back_inserter(ret), + [&tmp](auto const& dir) { return tmp.emplace(dir).second; }); + } + return ret; +} + +} // namespace + +std::filesystem::path home() { + { + auto* str = getenv("HOME"); + if (str != nullptr && str[0] != '\0') + return str; + } + + { + auto maybe_size = sysconf(_SC_GETPW_R_SIZE_MAX); + size_t size = maybe_size > 0 ? static_cast<size_t>(maybe_size) : 1024; + auto buffer = std::make_unique<char[]>(size); + struct passwd pwd; + struct passwd* ret; + int err; + while (true) { + err = getpwuid_r(geteuid(), &pwd, buffer.get(), size, &ret); + if (err == 0) + break; + if (err != ERANGE) + break; + auto new_size = size * 2; + if (new_size < size) + break; + buffer = std::make_unique<char[]>(new_size); + size = new_size; + } + if (err == 0 && ret) { + if (ret->pw_dir != nullptr && ret->pw_dir[0] != '\0') { + return ret->pw_dir; + } + } + } + + return "/"; +} + +std::vector<std::filesystem::path> config_dirs() { + static const std::vector<std::filesystem::path> fallback{"/etc/xdg"}; + return xdg_read_dirs("XDG_CONFIG_HOME", ".config", "XDG_CONFIG_DIRS", + fallback); +} + +std::vector<std::filesystem::path> data_dirs() { + static const std::vector<std::filesystem::path> fallback{"/usr/local/share/", + "/usr/share/"}; + return xdg_read_dirs("XDG_DATA_HOME", ".local/share", "XDG_DATA_DIRS", + fallback); +} + +} // namespace paths diff --git a/src/paths.hh b/src/paths.hh new file mode 100644 index 0000000..8bd4c75 --- /dev/null +++ b/src/paths.hh @@ -0,0 +1,20 @@ +#ifndef PATHS_HH +#define PATHS_HH + +#include <filesystem> // IWYU pragma: export +#include <vector> + +namespace paths { + +// Return home directory, goes HOME, /etc/passwd entry, / in that order. +std::filesystem::path home(); + +// Return config directories for reading, in order of priority. +std::vector<std::filesystem::path> config_dirs(); + +// Return data directories for reading, in order of priority. +std::vector<std::filesystem::path> data_dirs(); + +} // namespace paths + +#endif // PATHS_HH diff --git a/test/paths.cc b/test/paths.cc new file mode 100644 index 0000000..b07e459 --- /dev/null +++ b/test/paths.cc @@ -0,0 +1,63 @@ +#include "paths.hh" + +#include "testenv.hh" + +#include <gtest/gtest.h> + +namespace { + +class PathsTest : public TestEnv {}; + +} // namespace + +TEST_F(PathsTest, home) { + setenv("HOME", "foo-bar"); + EXPECT_EQ("foo-bar", paths::home()); + unsetenv("HOME"); + EXPECT_NE("foo-bar", paths::home()); + setenv("HOME", ""); + EXPECT_NE("", paths::home()); +} + +TEST_F(PathsTest, config_dirs) { + setenv("XDG_CONFIG_HOME", "foo"); + setenv("XDG_CONFIG_DIRS", "bar:fum::foo"); + auto dirs = paths::config_dirs(); + EXPECT_EQ(3, dirs.size()); + if (dirs.size() >= 3) { + EXPECT_EQ("foo", dirs[0]); + EXPECT_EQ("bar", dirs[1]); + EXPECT_EQ("fum", dirs[2]); + } + setenv("HOME", "home"); + unsetenv("XDG_CONFIG_HOME"); + unsetenv("XDG_CONFIG_DIRS"); + dirs = paths::config_dirs(); + EXPECT_EQ(2, dirs.size()); + if (dirs.size() >= 2) { + EXPECT_EQ("home/.config", dirs[0]); + EXPECT_EQ("/etc/xdg", dirs[1]); + } +} + +TEST_F(PathsTest, data_dirs) { + setenv("XDG_DATA_HOME", "foo"); + setenv("XDG_DATA_DIRS", "bar:fum::foo"); + auto dirs = paths::data_dirs(); + EXPECT_EQ(3, dirs.size()); + if (dirs.size() >= 3) { + EXPECT_EQ("foo", dirs[0]); + EXPECT_EQ("bar", dirs[1]); + EXPECT_EQ("fum", dirs[2]); + } + setenv("HOME", "home"); + unsetenv("XDG_DATA_HOME"); + unsetenv("XDG_DATA_DIRS"); + dirs = paths::data_dirs(); + EXPECT_EQ(3, dirs.size()); + if (dirs.size() >= 3) { + EXPECT_EQ("home/.local/share", dirs[0]); + EXPECT_EQ("/usr/local/share/", dirs[1]); + EXPECT_EQ("/usr/share/", dirs[2]); + } +} diff --git a/test/testenv.cc b/test/testenv.cc new file mode 100644 index 0000000..56701a4 --- /dev/null +++ b/test/testenv.cc @@ -0,0 +1,44 @@ +#include "testenv.hh" + +#include <cstdlib> +#include <optional> +#include <string> + +void TestEnv::setenv(std::string const& name, std::string const& value) { + saveenv(name); + + // NOLINTNEXTLINE(misc-include-cleaner) + ::setenv(name.c_str(), value.c_str(), 1); +} + +void TestEnv::unsetenv(std::string const& name) { + saveenv(name); + + // NOLINTNEXTLINE(misc-include-cleaner) + ::unsetenv(name.c_str()); +} + +void TestEnv::TearDown() { + for (auto const& pair : env_) { + if (pair.second.has_value()) { + // NOLINTNEXTLINE(misc-include-cleaner) + ::setenv(pair.first.c_str(), pair.second->c_str(), 1); + } else { + // NOLINTNEXTLINE(misc-include-cleaner) + ::unsetenv(pair.first.c_str()); + } + } +} + +void TestEnv::saveenv(std::string const& name) { + auto it = env_.find(name); + if (it != env_.end()) + return; + + auto* str = getenv(name.c_str()); + if (str == nullptr) { + env_.emplace(name, std::nullopt); + } else { + env_.emplace(name, str); + } +} diff --git a/test/testenv.hh b/test/testenv.hh new file mode 100644 index 0000000..abe0bc8 --- /dev/null +++ b/test/testenv.hh @@ -0,0 +1,22 @@ +#ifndef TESTENV_HH +#define TESTENV_HH + +#include <gtest/gtest.h> +#include <map> +#include <optional> +#include <string> + +class TestEnv : public testing::Test { + protected: + void setenv(std::string const& name, std::string const& value); + void unsetenv(std::string const& name); + + void TearDown() override; + + private: + void saveenv(std::string const& name); + + std::map<std::string, std::optional<std::string>> env_; +}; + +#endif // TESTENV_HH |
