diff --git a/examples/glob.cc b/examples/glob.cc index 06f411b..20548ca 100644 --- a/examples/glob.cc +++ b/examples/glob.cc @@ -6,15 +6,15 @@ #include #include -#include +#include using namespace ostd; int main() { writeln("-- all example sources (examples/*.cc) --\n"); { - auto app = appender>(); - glob_match(app, "examples/*.cc"); + auto app = appender>(); + fs::glob_match(app, "examples/*.cc"); for (auto &ex: app.get()) { writefln("found: %s", ex); @@ -22,8 +22,8 @@ int main() { } writeln("\n-- recursive source files (src/**/*.cc) --\n"); { - auto app = appender>(); - glob_match(app, "src/**/*.cc"); + auto app = appender>(); + fs::glob_match(app, "src/**/*.cc"); for (auto &ex: app.get()) { writefln("found: %s", ex); @@ -31,8 +31,8 @@ int main() { } writeln("\n-- 5-character headers (ostd/?????.hh) --\n"); { - auto app = appender>(); - glob_match(app, "ostd/?????.hh"); + auto app = appender>(); + fs::glob_match(app, "ostd/?????.hh"); for (auto &ex: app.get()) { writefln("found: %s", ex); @@ -40,8 +40,8 @@ int main() { } writeln("\n-- examples starting with f-r (examples/[f-r]*.cc) --\n"); { - auto app = appender>(); - glob_match(app, "examples/[f-r]*.cc"); + auto app = appender>(); + fs::glob_match(app, "examples/[f-r]*.cc"); for (auto &ex: app.get()) { writefln("found: %s", ex); @@ -49,8 +49,8 @@ int main() { } writeln("\n-- examples not starting with f-r (examples/[!f-r]*.cc) --\n"); { - auto app = appender>(); - glob_match(app, "examples/[!f-r]*.cc"); + auto app = appender>(); + fs::glob_match(app, "examples/[!f-r]*.cc"); for (auto &ex: app.get()) { writefln("found: %s", ex); @@ -58,8 +58,8 @@ int main() { } writeln("\n-- headers starting with c, f or s (ostd/[cfs]*.hh) --\n"); { - auto app = appender>(); - glob_match(app, "ostd/[cfs]*.hh"); + auto app = appender>(); + fs::glob_match(app, "ostd/[cfs]*.hh"); for (auto &ex: app.get()) { writefln("found: %s", ex); diff --git a/ostd/path.hh b/ostd/path.hh index 7363e47..e1332b4 100644 --- a/ostd/path.hh +++ b/ostd/path.hh @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -50,6 +51,10 @@ namespace ostd { namespace detail { struct path_range; struct path_parent_range; + + OSTD_EXPORT bool glob_match_path_impl( + char const *fname, char const *wname + ) noexcept; } struct path { @@ -357,6 +362,43 @@ struct path { return append_concat(p); } + /** @brief Checks if the given path matches the given glob pattern. + * + * This matches the given filename against POSIX-style glob patterns. + * The following patterns are supported: + * + * | Pattern | Description | + * |---------|----------------------------------------------------| + * | * | 0 or more characters | + * | ? | any single character | + * | [abc] | one character in the brackets | + * | [a-z] | one character within the range in the brackets | + * | [!abc] | one character not in the brackets | + * | [!a-z] | one character not within the range in the brackets | + * + * The behavior is the same as in POSIX. You can combine ranges and + * individual characters in the `[]` pattern together as well as define + * multiple ranges in one (e.g. `[a-zA-Z_?]` matching alphabetics, + * an underscore and a question mark). The behavior of the range varies + * by locale. If the second character in the range is lower in value + * than the first one, a match will never happen. To match the `]` + * character in the brackets, make it the first one. To match the + * dash character, make it the first or the last. + * + * You can also use the brackets to escape metacharacters. So to + * match a literal `*`, use `[*]`. + * + * Keep in mind that an invalid bracket syntax (unterminated) will + * always cause this to return `false`. + * + * This function is used in ostd::glob_match(). + */ + bool match(path const &pattern) noexcept { + return detail::glob_match_path_impl( + string().data(), pattern.string().data() + ); + } + string_range string() const noexcept { return p_path; } @@ -1076,6 +1118,8 @@ OSTD_EXPORT path current_path(); OSTD_EXPORT path home_path(); OSTD_EXPORT path temp_path(); +OSTD_EXPORT void current_path(path const &p); + OSTD_EXPORT path absolute(path const &p); OSTD_EXPORT path canonical(path const &p); @@ -1091,6 +1135,61 @@ OSTD_EXPORT bool exists(path const &p); OSTD_EXPORT bool equivalent(path const &p1, path const &p2); +OSTD_EXPORT bool create_directory(path const &p); +OSTD_EXPORT bool create_directory(path const &p, path const &ep); +OSTD_EXPORT bool create_directories(path const &p); + +OSTD_EXPORT bool remove(path const &p); +OSTD_EXPORT std::uintmax_t remove_all(path const &p); + +OSTD_EXPORT void rename(path const &op, path const &np); + +using file_time_t = std::chrono::time_point; + +OSTD_EXPORT file_time_t last_write_time(path const &p); +OSTD_EXPORT void last_write_time(path const &p, file_time_t new_time); + +namespace detail { + OSTD_EXPORT void glob_match_impl( + void (*out)(path const &, void *), + typename path::range r, path pre, void *data + ); +} /* namespace detail */ + +/** @brief Expands a path with glob patterns. + * + * Individual expanded paths are put in `out` and are of the standard + * std::filesystem::path type. It supports standard patterns as defined + * in ostd::glob_match_filename(). + * + * So for example, `*.cc` will expand to `one.cc`, `two.cc` and so on. + * A pattern like `foo/[cb]at.txt` will match `foo/cat.txt` and `foo/bat.txt` + * but not `foo/Cat.txt`. The `foo/?at.txt` will match `foo/cat.txt`, + * `foo/Cat.txt`, `foo/pat.txt`, `foo/vat.txt` or any other character + * in the place. + * + * Additionally, a special `**` pattern is also supported which is not + * matched by ostd::glob_match_filename(). It's only allowed if the entire + * filename or directory name is `**`. When used as a directory name, it + * will expand to all directories in the location and all subdirectories + * of those directories. If used as a filename (at the end of the path), + * then it expands to directories and subdirectories aswell as all files + * in the location and in the directories or subdirectories. Keep in mind + * that it is not a regular pattern and a `**` when found in a regular + * context (i.e. not as entire filename/directory name) will be treated + * as two regular `*` patterns. + * + * @throws std::filesystem_error if a filesystem error occurs. + * @returns The forwarded `out`. + */ +template +inline OutputRange &&glob_match(OutputRange &&out, path const &pattern) { + detail::glob_match_impl([](path const &p, void *outp) { + static_cast *>(outp)->put(p); + }, pattern.iter(), path{}, &out); + return std::forward(out); +} + /** @} */ } /* namesapce fs */ diff --git a/src/path.cc b/src/path.cc index 383ce24..73a96e8 100644 --- a/src/path.cc +++ b/src/path.cc @@ -3,6 +3,8 @@ * This file is part of libostd. See COPYING.md for futher information. */ +#include + #include "ostd/platform.hh" #if defined(OSTD_PLATFORM_WIN32) @@ -12,3 +14,180 @@ #else # error "Unsupported platform" #endif + +namespace ostd { +namespace detail { + +inline char const *glob_match_brackets(char match, char const *wp) noexcept { + bool neg = (*wp == '!'); + if (neg) { + ++wp; + } + + /* grab the first character as it can be ] */ + auto c = *wp++; + if (!c) { + /* unterminated */ + return nullptr; + } + + /* make sure it's terminated somewhere */ + auto *eb = wp; + for (; *eb != ']'; ++eb) { + if (!*eb) { + return nullptr; + } + } + ++eb; + + /* no need to worry about \0 from now on */ + do { + /* character range */ + if ((*wp == '-') && (*(wp + 1) != ']')) { + auto lc = *(wp + 1); + wp += 2; + if ((match >= c) && (match <= lc)) { + return neg ? nullptr : eb; + } + c = *wp++; + continue; + } + /* single-char match */ + if (match == c) { + return neg ? nullptr : eb; + } + c = *wp++; + } while (c != ']'); + + /* loop ended, so no match */ + return neg ? eb : nullptr; +} + +OSTD_EXPORT bool glob_match_path_impl( + char const *fname, char const *wname +) noexcept { + /* skip any matching prefix if present */ + while (*wname && (*wname != '*')) { + if (!*wname || (*wname == '*')) { + break; + } + if (*fname) { + /* ? wildcard matches any character */ + if (*wname == '?') { + ++wname; + ++fname; + continue; + } + /* [...] wildcard */ + if (*wname == '[') { + wname = glob_match_brackets(*fname, wname + 1); + if (!wname) { + return false; + } + ++fname; + continue; + } + } + if ((*wname == '?') && *fname) { + ++wname; + ++fname; + continue; + } + if (*fname++ != *wname++) { + return false; + } + } + /* skip * wildcards; a wildcard matches 0 or more */ + if (*wname == '*') { + while (*wname == '*') { + ++wname; + } + /* was trailing so everything matches */ + if (!*wname) { + return true; + } + } + /* prefix skipping matched entire filename */ + if (!*fname) { + return true; + } + /* empty pattern and non-empty filename */ + if (!*wname) { + return false; + } + /* keep incrementing filename until it matches */ + while (*fname) { + if (glob_match_path_impl(fname, wname)) { + return true; + } + ++fname; + } + return false; +} + +} /* namespace detail */ +} /* namespace ostd */ + +namespace ostd { +namespace fs { +namespace detail { + +OSTD_EXPORT void glob_match_impl( + void (*out)(path const &, void *), typename path::range r, + path pre, void *data +) { + while (!r.empty()) { + auto cur = std::string{r.front()}; + auto *cs = cur.c_str(); + /* this part of the path might contain wildcards */ + for (auto c = *cs; c; c = *++cs) { + /* ** as a name does recursive expansion */ + if ((c == '*') && (*(cs + 1) == '*') && !*(cs + 2)) { + r.pop_front(); + auto ip = pre.empty() ? "." : pre; + if (!is_directory(ip)) { + return; + } + recursive_directory_range dr{ip}; + /* include "current" dir in the match */ + if (!r.empty()) { + glob_match_impl(out, r, pre, data); + } + for (auto &de: dr) { + /* followed by more path, only consider dirs */ + auto dp = de.path(); + if (!r.empty() && !is_directory(dp)) { + continue; + } + /* otherwise also match files */ + glob_match_impl(out, r, dp, data); + } + return; + } + /* wildcards *, ?, [...] */ + if ((c == '*') || (c == '?') || (c == '[')) { + r.pop_front(); + auto ip = pre.empty() ? "." : pre; + if (!is_directory(ip)) { + return; + } + directory_range dr{ip}; + for (auto &de: dr) { + auto p = path{de.path().name()}; + if (!p.match(cur)) { + continue; + } + glob_match_impl(out, r, pre / p, data); + } + return; + } + } + pre /= cur; + r.pop_front(); + } + out(pre, data); +} + +} /* namespace detail */ +} /* namesapce fs */ +} /* namespace ostd */ \ No newline at end of file diff --git a/src/posix/path.cc b/src/posix/path.cc index 3decbcf..fb043c2 100644 --- a/src/posix/path.cc +++ b/src/posix/path.cc @@ -4,6 +4,7 @@ */ #include +#include #include #include #include @@ -17,38 +18,6 @@ namespace ostd { namespace fs { -static perms mode_to_perms(mode_t mode) { - perms ret = perms::none; - switch (mode & S_IRWXU) { - case S_IRUSR: ret |= perms::owner_read; - case S_IWUSR: ret |= perms::owner_write; - case S_IXUSR: ret |= perms::owner_exec; - case S_IRWXU: ret |= perms::owner_all; - } - switch (mode & S_IRWXG) { - case S_IRGRP: ret |= perms::group_read; - case S_IWGRP: ret |= perms::group_write; - case S_IXGRP: ret |= perms::group_exec; - case S_IRWXG: ret |= perms::group_all; - } - switch (mode & S_IRWXO) { - case S_IROTH: ret |= perms::others_read; - case S_IWOTH: ret |= perms::others_write; - case S_IXOTH: ret |= perms::others_exec; - case S_IRWXO: ret |= perms::others_all; - } - if (mode & S_ISUID) { - ret |= perms::set_uid; - } - if (mode & S_ISGID) { - ret |= perms::set_gid; - } - if (mode & S_ISVTX) { - ret |= perms::sticky_bit; - } - return ret; -} - static file_type mode_to_type(mode_t mode) { switch (mode & S_IFMT) { case S_IFBLK: return file_type::block; @@ -71,7 +40,7 @@ OSTD_EXPORT file_mode mode(path const &p) { /* FIXME: throw */ abort(); } - return file_mode{mode_to_type(sb.st_mode), mode_to_perms(sb.st_mode)}; + return file_mode{mode_to_type(sb.st_mode), perms(sb.st_mode & 07777)}; } OSTD_EXPORT file_mode symlink_mode(path const &p) { @@ -83,7 +52,7 @@ OSTD_EXPORT file_mode symlink_mode(path const &p) { /* FIXME: throw */ abort(); } - return file_mode{mode_to_type(sb.st_mode), mode_to_perms(sb.st_mode)}; + return file_mode{mode_to_type(sb.st_mode), perms(sb.st_mode & 07777)}; } } /* namespace fs */ @@ -268,6 +237,12 @@ OSTD_EXPORT path temp_path() { return path{"/tmp"}; } +OSTD_EXPORT void current_path(path const &p) { + if (chdir(p.string().data())) { + abort(); + } +} + OSTD_EXPORT path absolute(path const &p) { if (p.is_absolute()) { return p; @@ -338,5 +313,129 @@ OSTD_EXPORT bool equivalent(path const &p1, path const &p2) { return ((sb.st_dev == stdev) && (sb.st_ino == stino)); } +static bool mkdir_p(path const &p, mode_t mode) { + if (mkdir(p.string().data(), mode)) { + if (errno != EEXIST) { + abort(); + } + auto tp = fs::mode(p); + if (tp.type() != file_type::directory) { + abort(); + } + return false; + } + return true; +} + +OSTD_EXPORT bool create_directory(path const &p) { + return mkdir_p(p, 0777); +} + +OSTD_EXPORT bool create_directory(path const &p, path const &ep) { + return mkdir_p(p, mode_t(fs::mode(ep).permissions())); +} + +OSTD_EXPORT bool create_directories(path const &p) { + if (p.has_parent()) { + create_directories(p.parent()); + } + return create_directory(p); +} + +OSTD_EXPORT bool remove(path const &p) { + if (!exists(p)) { + return false; + } + if (::remove(p.string().data())) { + abort(); + } + return true; +} + +OSTD_EXPORT std::uintmax_t remove_all(path const &p) { + std::uintmax_t ret = 0; + if (is_directory(p)) { + fs::directory_range ds{p}; + for (auto &v: ds) { + ret += remove_all(v.path()); + } + } + ret += remove(p); + return ret; +} + +OSTD_EXPORT void rename(path const &op, path const &np) { + if (::rename(op.string().data(), np.string().data())) { + abort(); + } +} + +/* ugly test for whether nanosecond precision is available in stat + * could check for existence of st_mtime macro, but this is more reliable + */ + +template +struct test_mtim { + template struct test_stat; + + struct fake_stat { + struct timespec st_mtim; + }; + + struct stat_test: fake_stat, T {}; + + template + static char test(test_stat *); + + template + static int test(...); + + static constexpr bool value = (sizeof(test(0)) == sizeof(int)); +}; + +template +struct mtime_impl { + template + static file_time_t get(S const &st) { + return std::chrono::system_clock::from_time_t(st.st_mtime); + } +}; + +template<> +struct mtime_impl { + template + static file_time_t get(S const &st) { + struct timespec ts = st.st_mtim; + auto d = std::chrono::seconds{ts.tv_sec} + + std::chrono::nanoseconds{ts.tv_nsec}; + return file_time_t{ + std::chrono::duration_cast(d) + }; + } +}; + +using mtime = mtime_impl::value>; + +OSTD_EXPORT file_time_t last_write_time(path const &p) { + struct stat sb; + if (stat(p.string().data(), &sb) < 0) { + abort(); + } + return mtime::get(sb); +} + +/* TODO: somehow feature-test for utimensat and fallback to utimes */ +OSTD_EXPORT void last_write_time(path const &p, file_time_t new_time) { + auto d = new_time.time_since_epoch(); + auto sec = std::chrono::floor(d); + auto nsec = std::chrono::duration_cast(d - sec); + struct timespec times[2] = { + {0, UTIME_OMIT}, {time_t(sec.count()), long(nsec.count())} + }; + if (utimensat(0, p.string().data(), times, 0)) { + abort(); + } +} + } /* namespace fs */ } /* namespace ostd */