return result;
}
-// Write file data from a string.
+void
+traverse(const std::string& path, const TraverseVisitor& visitor)
+{
+ DIR* dir = opendir(path.c_str());
+ if (dir) {
+ struct dirent* entry;
+ while ((entry = readdir(dir))) {
+ if (strcmp(entry->d_name, "") == 0 || strcmp(entry->d_name, ".") == 0
+ || strcmp(entry->d_name, "..") == 0) {
+ continue;
+ }
+
+ std::string entry_path = path + "/" + entry->d_name;
+ bool is_dir;
+#ifdef _DIRENT_HAVE_D_TYPE
+ if (entry->d_type != DT_UNKNOWN) {
+ is_dir = entry->d_type == DT_DIR;
+ } else
+#endif
+ {
+ auto stat = Stat::lstat(entry_path);
+ if (!stat) {
+ if (stat.error_number() == ENOENT || stat.error_number() == ESTALE) {
+ continue;
+ }
+ throw Error(fmt::format("failed to lstat {}: {}",
+ entry_path,
+ strerror(stat.error_number())));
+ }
+ is_dir = stat.is_directory();
+ }
+ if (is_dir) {
+ traverse(entry_path, visitor);
+ } else {
+ visitor(entry_path, false);
+ }
+ }
+ closedir(dir);
+ visitor(path, true);
+ } else if (errno == ENOTDIR) {
+ visitor(path, false);
+ } else {
+ throw Error(
+ fmt::format("failed to open directory {}: {}", path, strerror(errno)));
+ }
+}
+
void
write_file(const std::string& path, const std::string& data, bool binary)
{
using SubdirVisitor =
std::function<void(const std::string& /*dir_path*/,
const ProgressReceiver& /*progress_receiver*/)>;
+using TraverseVisitor =
+ std::function<void(const std::string& path, bool is_dir)>;
// Get base name of path.
nonstd::string_view base_name(nonstd::string_view path);
// Convert a string to lowercase.
[[gnu::warn_unused_result]] std::string to_lowercase(const std::string& string);
+// Traverse `path` recursively (postorder, i.e. files are visited before their
+// parent directory).
+//
+// Throws Error on error.
+void traverse(const std::string& path, const TraverseVisitor& visitor);
+
// Write file data from a string.
//
// Throws `Error` on error. The description contains the error message without
CHECK(Util::to_lowercase("X") == "x");
CHECK(Util::to_lowercase(" x_X@") == " x_x@");
}
+
+TEST_CASE("Util::traverse")
+{
+ REQUIRE(Util::create_dir("traverse/dir-with-subdir-and-file/subdir"));
+ Util::write_file("traverse/dir-with-subdir-and-file/subdir/f", "");
+ REQUIRE(Util::create_dir("traverse/dir-with-files"));
+ Util::write_file("traverse/dir-with-files/f1", "");
+ Util::write_file("traverse/dir-with-files/f2", "");
+ REQUIRE(Util::create_dir("traverse/empty-dir"));
+
+ std::vector<std::string> visited;
+ auto visitor = [&visited](const std::string& path, bool is_dir) {
+ visited.push_back(fmt::format("[{}] {}", is_dir ? 'd' : 'f', path));
+ };
+
+ SECTION("traverse nonexistent path")
+ {
+ CHECK_THROWS_WITH(
+ Util::traverse("nonexistent", visitor),
+ "failed to open directory nonexistent: No such file or directory");
+ }
+
+ SECTION("traverse file")
+ {
+ CHECK_NOTHROW(
+ Util::traverse("traverse/dir-with-subdir-and-file/subdir/f", visitor));
+ REQUIRE(visited.size() == 1);
+ CHECK(visited[0] == "[f] traverse/dir-with-subdir-and-file/subdir/f");
+ }
+
+ SECTION("traverse empty directory")
+ {
+ CHECK_NOTHROW(Util::traverse("traverse/empty-dir", visitor));
+ REQUIRE(visited.size() == 1);
+ CHECK(visited[0] == "[d] traverse/empty-dir");
+ }
+
+ SECTION("traverse directory with files")
+ {
+ CHECK_NOTHROW(Util::traverse("traverse/dir-with-files", visitor));
+ REQUIRE(visited.size() == 3);
+ std::string f1 = "[f] traverse/dir-with-files/f1";
+ std::string f2 = "[f] traverse/dir-with-files/f2";
+ CHECK(((visited[0] == f1 && visited[1] == f2)
+ || (visited[0] == f2 && visited[1] == f1)));
+ CHECK(visited[2] == "[d] traverse/dir-with-files");
+ }
+
+ SECTION("traverse directory hierarchy")
+ {
+ CHECK_NOTHROW(Util::traverse("traverse/dir-with-subdir-and-file", visitor));
+ REQUIRE(visited.size() == 3);
+ CHECK(visited[0] == "[f] traverse/dir-with-subdir-and-file/subdir/f");
+ CHECK(visited[1] == "[d] traverse/dir-with-subdir-and-file/subdir");
+ CHECK(visited[2] == "[d] traverse/dir-with-subdir-and-file");
+ }
+}