Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` |
| `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 |
Expand Down
48 changes: 27 additions & 21 deletions src/env.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,36 @@ std::optional<std::string> system() {
return counted([](char* o, kal_uintptr cap) { return kal_env_system(o, cap); });
}

std::vector<std::filesystem::path> search_path() {
std::vector<std::filesystem::path> split_list(std::string_view value) {
std::vector<std::filesystem::path> 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<const std::filesystem::path> 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<std::filesystem::path> search_path() { return split_list(get("PATH").value_or("")); }

namespace {
bool is_program(const std::filesystem::path& p) {
std::error_code ec;
Expand Down Expand Up @@ -134,19 +148,11 @@ std::optional<std::filesystem::path> 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;
}

Expand Down
10 changes: 9 additions & 1 deletion src/env.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@ std::vector<std::string> self_command();
// For conventions only, as openkal restricts it.
std::optional<std::string> 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<std::filesystem::path> split_list(std::string_view value);
std::string join_list(std::span<const std::filesystem::path> directories);

// The directories `PATH' names, in order (`split_list' of its value).
std::vector<std::filesystem::path> search_path();

// A program by name, as a shell would find it: a name with a directory is
Expand Down
85 changes: 81 additions & 4 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ std::vector<supplied> 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;
Expand Down Expand Up @@ -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<unsigned char>(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<std::string_view> 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) {
Expand All @@ -123,13 +200,13 @@ bool names_a_place(std::string_view name) {

std::optional<located> 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<located> 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;
Expand Down
20 changes: 20 additions & 0 deletions src/fs.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,26 @@ std::vector<std::string> 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.
Expand Down
28 changes: 28 additions & 0 deletions tests/fs_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::filesystem::path> 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);
Expand Down
Loading