summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-07 19:58:28 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-19 00:13:47 +0200
commit4f6e76493fb74f5385d5a14dce3a01c9901802ed (patch)
treea38722ec832fd44ad34257730e075e8b07825bd0
parentc87f9627efc8b612eb9b000acfcc6731cad15765 (diff)
paths: New module
Path utilities (doh)
-rw-r--r--meson.build35
-rw-r--r--src/paths.cc100
-rw-r--r--src/paths.hh20
-rw-r--r--test/paths.cc63
-rw-r--r--test/testenv.cc44
-rw-r--r--test/testenv.hh22
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