diff --git a/README.md b/README.md index a67f8cd..219bc4d 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ modules. |---|---| | `mcpplibs.os.platform` | `PLATFORM`, `ARCH`, `is_linux` / `is_macos` / `is_windows` / `is_posix`, `exe_suffix`, `path_list_separator`; `host()`, a constant expression on a native target | | `mcpplibs.os.error` | `errc` (openkal's closed set of conditions), `error`, `result` | -| `mcpplibs.os.env` | variables, arguments, the program's own image, PATH search with PATHEXT | -| `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `locate`, `absolute`, `names_a_place` | +| `mcpplibs.os.env` | variables (names without case on Windows), arguments, the program's own image, lists of directories in the host's separator (`split_list`, `join_list`), PATH search with PATHEXT | +| `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `locate`, `absolute`, `names_a_place`; names in the host's rules without the file system: `generic`, `native`, `is_absolute`, `same`, `within` | | `mcpplibs.os.process` | `spawn`, `run`, pipes, output onto this program's error output, units and `end_unit`, bound lifetime, bounded waits and reads; `self`, `describe`, `alive` | | `mcpplibs.os.interrupt` | `requested`, `wait`, `end_as_requested` | | `mcpplibs.os.stdio` | the three streams as bytes | diff --git a/src/env.cpp b/src/env.cpp index 3d56fa1..1102aad 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -72,22 +72,36 @@ std::optional system() { return counted([](char* o, kal_uintptr cap) { return kal_env_system(o, cap); }); } -std::vector search_path() { +std::vector split_list(std::string_view value) { std::vector out; - const auto value = get("PATH"); - if (!value) return out; + const bool windows = host() == platform::windows; const char sep = conventions().path_list_separator; std::size_t start = 0; - while (start <= value->size()) { - const auto end = value->find(sep, start); - const auto piece = value->substr(start, end == std::string::npos ? std::string::npos : end - start); - if (!piece.empty()) out.emplace_back(piece); - if (end == std::string::npos) break; + while (start <= value.size()) { + const auto end = value.find(sep, start); + auto piece = value.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start); + while (!piece.empty() && (piece.front() == ' ' || piece.front() == '\t')) piece.remove_prefix(1); + while (!piece.empty() && (piece.back() == ' ' || piece.back() == '\t')) piece.remove_suffix(1); + if (windows && piece.size() >= 2 && piece.front() == '"' && piece.back() == '"') piece = piece.substr(1, piece.size() - 2); + if (!piece.empty()) out.emplace_back(std::string(piece)); + if (end == std::string_view::npos) break; start = end + 1; } return out; } +std::string join_list(std::span directories) { + std::string out; + const char sep = conventions().path_list_separator; + for (const auto& d : directories) { + if (!out.empty()) out += sep; + out += fs::native(d); + } + return out; +} + +std::vector search_path() { return split_list(get("PATH").value_or("")); } + namespace { bool is_program(const std::filesystem::path& p) { std::error_code ec; @@ -134,19 +148,11 @@ std::optional find_executable(std::string_view name, std: } return std::nullopt; } - const char sep = conventions().path_list_separator; - std::size_t start = 0; - while (start <= path_list.size()) { - const auto end = path_list.find(sep, start); - const auto dir = path_list.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start); - if (!dir.empty()) - for (const auto& s : suffixes) { - auto candidate = std::filesystem::path(dir) / given; candidate += s; - if (is_program(candidate)) return candidate; - } - if (end == std::string_view::npos) break; - start = end + 1; - } + for (const auto& dir : split_list(path_list)) + for (const auto& s : suffixes) { + auto candidate = dir / given; candidate += s; + if (is_program(candidate)) return candidate; + } return std::nullopt; } diff --git a/src/env.cppm b/src/env.cppm index 878afac..a5e9655 100644 --- a/src/env.cppm +++ b/src/env.cppm @@ -39,7 +39,15 @@ std::vector self_command(); // For conventions only, as openkal restricts it. std::optional system(); -// The directories `PATH' names, in order. +// A list of directories as a variable holds one (`PATH', `PKG_CONFIG_PATH', +// `LD_LIBRARY_PATH'), split at the host's separator (`;' on Windows, `:' +// elsewhere): empty entries skipped, and on Windows an entry quoted whole +// unquoted, as the system's own search reads it. `join_list' is its inverse, +// in the host's spelling. +std::vector split_list(std::string_view value); +std::string join_list(std::span directories); + +// The directories `PATH' names, in order (`split_list' of its value). std::vector search_path(); // A program by name, as a shell would find it: a name with a directory is diff --git a/src/fs.cpp b/src/fs.cpp index 3cf0bdd..3fed3ff 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -38,7 +38,7 @@ std::vector table() { bool separator(char c) { return c == '/' || c == '\\'; } -std::string generic(std::string s) { +std::string slashed(std::string s) { for (auto& c : s) if (c == '\\') c = '/'; while (s.size() > 1 && s.back() == '/' && !(s.size() == 2 && s[0] == '/')) s.pop_back(); return s; @@ -111,7 +111,84 @@ std::filesystem::path absolute(const std::filesystem::path& name) { std::error_code ec; const auto cwd = std::filesystem::current_path(ec); if (ec) return name; - return std::filesystem::path(generic(cwd.generic_string()) + "/" + s); + return std::filesystem::path(slashed(cwd.generic_string()) + "/" + s); +} + +namespace { +// The root a name begins with, in the generic spelling: `C:/' or `C:' (a drive +// and the directory current on it), `//server/share/' on Windows; `/' elsewhere; +// empty for a relative name. +std::size_t root_length(std::string_view g, bool windows) { + if (windows) { + if (g.size() >= 2 && std::isalpha(static_cast(g[0])) && g[1] == ':') + return g.size() >= 3 && g[2] == '/' ? 3 : 2; + if (g.size() >= 2 && g[0] == '/' && g[1] == '/') { + const auto server = g.find('/', 2); + if (server == std::string_view::npos) return g.size(); + const auto share = g.find('/', server + 1); + return share == std::string_view::npos ? g.size() : share + 1; + } + } + return !g.empty() && g[0] == '/' ? 1 : 0; +} + +bool fold_equal(std::string_view a, std::string_view b, bool fold) { + return a.size() == b.size() && same_prefix(a, b, fold); +} +} // namespace + +std::string generic(const std::filesystem::path& name) { + const bool windows = host() == platform::windows; + std::string s = name.generic_string(); + if (windows) std::ranges::replace(s, '\\', '/'); + const std::size_t rootLen = root_length(s, windows); + const std::string root = s.substr(0, rootLen); + std::vector parts; + std::string_view rest = std::string_view(s).substr(rootLen); + while (!rest.empty()) { + const auto slash = rest.find('/'); + const auto part = rest.substr(0, slash); + rest = slash == std::string_view::npos ? std::string_view{} : rest.substr(slash + 1); + if (part.empty() || part == ".") continue; + if (part == "..") { + if (!parts.empty() && parts.back() != "..") { parts.pop_back(); continue; } + if (!root.empty()) continue; // nothing is above a root + } + parts.push_back(part); + } + std::string out = root; + for (std::size_t i = 0; i < parts.size(); ++i) { + if (i) out += '/'; + out += parts[i]; + } + if (out.empty()) out = "."; + return out; +} + +std::string native(const std::filesystem::path& name) { + std::string g = generic(name); + if (host() == platform::windows) std::ranges::replace(g, '/', '\\'); + return g; +} + +bool is_absolute(const std::filesystem::path& name) { + const bool windows = host() == platform::windows; + const std::string g = generic(name); + const auto r = root_length(g, windows); + // `C:x' names a place relative to the drive's current directory: not absolute. + return windows ? r >= 3 || (r > 0 && g[0] == '/' && g.size() > 1 && g[1] == '/') : r == 1; +} + +bool same(const std::filesystem::path& a, const std::filesystem::path& b) { + return fold_equal(generic(a), generic(b), conventions().case_insensitive_names); +} + +bool within(const std::filesystem::path& name, const std::filesystem::path& directory) { + const bool fold = conventions().case_insensitive_names; + const std::string n = generic(name), d = generic(directory); + if (fold_equal(n, d, fold)) return true; + if (!same_prefix(n, d, fold)) return false; + return d.back() == '/' || n[d.size()] == '/'; } bool names_a_place(std::string_view name) { @@ -123,13 +200,13 @@ bool names_a_place(std::string_view name) { std::optional locate(const std::filesystem::path& name) { const auto whole = mcpplibs::os::fs::absolute(name); - const std::string path = generic(whole.lexically_normal().generic_string()); + const std::string path = slashed(whole.lexically_normal().generic_string()); const bool fold = conventions().case_insensitive_names; std::optional best; std::size_t best_len = 0; for (const auto& s : table()) { if (!s.dir.h || s.name.empty()) continue; - const std::string prefix = generic(s.name); + const std::string prefix = slashed(s.name); if (!same_prefix(path, prefix, fold)) continue; const bool root = separator(prefix.back()); if (!root && path.size() > prefix.size() && !separator(path[prefix.size()])) continue; diff --git a/src/fs.cppm b/src/fs.cppm index 396ea16..83323ea 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -39,6 +39,26 @@ std::vector preopens(); // the image runs upon. std::filesystem::path absolute(const std::filesystem::path& name); +// NAMES IN THE HOST'S RULES, AND WITHOUT ASKING THE FILE SYSTEM. Each is lexical: +// it reads the name and never the node, so a link is not followed and a name +// that does not exist is answered as well as one that does (`identity_of' says +// whether two names reach one node). All ask `host()'. +// +// `generic': the spelling every interface here reads --- `/' between +// components, `\' read as one on Windows, `.' and `..' resolved, no trailing +// separator; the root kept as written (`/', `C:/', `//server/share/'). +// `native': the same in the host's own separator, for a program's argument +// vector or a person (`C:\Users\x'). +// `is_absolute': begins with a root (on Windows a drive and a separator, or +// `//server/share'; `C:x' is relative to the drive's current directory). +// `same': one spelling, compared without case where the host's names are. +// `within': the name is the directory or beneath it, by components. +std::string generic(const std::filesystem::path& name); +std::string native(const std::filesystem::path& name); +bool is_absolute(const std::filesystem::path& name); +bool same(const std::filesystem::path& a, const std::filesystem::path& b); +bool within(const std::filesystem::path& name, const std::filesystem::path& directory); + // Whether a name says where a file is, rather than naming one to look for on a // list of directories: it holds a separator in the host's spelling (on Windows // `\' as well as `/', or a drive). Asked of `host()' for the reason above. diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index c332cae..7c274ae 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -63,6 +63,34 @@ int main() { && os::fs::names_a_place("bin\\tool") == (os::host() == os::platform::windows), "a drive and a backslash say where a file is on Windows and nowhere else"); + { + // Names in the host's rules, without the file system. + const bool windows = os::host() == os::platform::windows; + check(os::fs::generic("a/./b/../c/") == "a/c", "a name is spelt with its dots resolved and no trailing separator"); + check(os::fs::generic("/x/../../y") == "/y", "and nothing is above the root"); + check(os::fs::is_absolute("/usr/bin") == !windows, "a leading separator is a root on POSIX"); + if (windows) { + check(os::fs::generic("C:\\Users\\x\\") == "C:/Users/x", "a Windows name is spelt with forward separators"); + check(os::fs::native("C:/Users/x") == "C:\\Users\\x", "and the host's separator for a person or a program"); + check(os::fs::is_absolute("C:/x") && !os::fs::is_absolute("C:x") && os::fs::is_absolute("//srv/share/x"), + "a drive and a separator, or a share, is a root; a drive alone is not"); + check(os::fs::same("C:/Users/X", "c:\\users\\x"), "names are the same without regard to case"); + } else { + check(os::fs::native("a/b") == "a/b", "a POSIX name is its own native spelling"); + check(os::fs::same("/a/b", "/a/./b/") && (os::fs::same("/a/B", "/a/b") == (os::host() == os::platform::macos)), + "names compare by components, and by case where the host's names do"); + } + check(os::fs::within("/a/b/c", "/a/b") && os::fs::within("/a/b", "/a/b") && !os::fs::within("/a/bc", "/a/b"), + "a name is within a directory by components"); + const char sep = os::conventions().path_list_separator; + const std::string list = std::string("/x") + sep + sep + " /y " + sep + (windows ? "\"C:/with space\"" : "/z"); + const auto parts = os::env::split_list(list); + check(parts.size() == 3 && parts[0] == "/x" && parts[1] == "/y", "a list splits at the host's separator and skips empty entries"); + if (windows) check(parts.size() == 3 && parts[2] == "C:/with space", "and an entry quoted whole is unquoted"); + const std::vector dirs{ "/p", "/q" }; + check(os::env::split_list(os::env::join_list(dirs)).size() == 2, "and joining is its inverse"); + } + if (caps && caps->remove_open) { std::error_code ec; stdfs::remove_all(dir, ec);