// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include #include #include #include #include #include #include #include #include module Crafter.Review:Platform_impl; import :Platform; import :Json; import :Text; import :Crypto; import std; namespace fs = std::filesystem; namespace Crafter::Review::Platform { namespace { Mode CurrentModeValue = Mode::Native; SystemPaths& PathsValue() { static SystemPaths paths = [] { SystemPaths defaults; if (const char* runtime = std::getenv("XDG_RUNTIME_DIR"); runtime && *runtime) { defaults.runtimeDir = runtime; } else { defaults.runtimeDir = std::format("/run/user/{}", getuid()); } return defaults; }(); return paths; } // [section] key=value, as GLib's key files are written. std::optional KeyFileValue(std::string_view text, std::string_view section, std::string_view key) { bool inSection = false; for (const std::string& line : Dyn::SplitLines(text)) { const std::string_view view(line); if (view.starts_with('[')) { inSection = view == std::format("[{}]", section); continue; } if (!inSection) continue; const std::size_t equals = view.find('='); if (equals == std::string_view::npos) continue; if (Dyn::Strip(view.substr(0, equals)) == key) return Dyn::Strip(view.substr(equals + 1)); } return std::nullopt; } std::optional ToInt(std::string_view text) { std::int64_t value = 0; const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); if (error != std::errc() || end != text.data() + text.size()) return std::nullopt; return value; } } void Init() { CurrentModeValue = fs::exists(PathsValue().flatpakInfo) ? Mode::Sandbox : Mode::Native; if (CurrentModeValue == Mode::Sandbox) { RestoreHostXdg(); RestoreShell(); } } Mode CurrentMode() { return CurrentModeValue; } bool InSandbox() { return CurrentModeValue == Mode::Sandbox; } void SetMode(Mode mode) { CurrentModeValue = mode; } const SystemPaths& Paths() { return PathsValue(); } void SetPaths(SystemPaths paths) { PathsValue() = std::move(paths); } std::optional GetEnv(std::string_view name) { const std::string key(name); const char* value = std::getenv(key.c_str()); if (value == nullptr) return std::nullopt; return std::string(value); } std::map Environment() { std::map out; for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) { const std::string_view text(*entry); const std::size_t equals = text.find('='); if (equals == std::string_view::npos) continue; out.emplace(std::string(text.substr(0, equals)), std::string(text.substr(equals + 1))); } return out; } std::string Home() { if (const std::optional home = GetEnv("HOME"); home && !home->empty()) return *home; if (const passwd* entry = getpwuid(getuid()); entry != nullptr && entry->pw_dir != nullptr) return entry->pw_dir; return "/"; } void RestoreHostXdg() { const std::string home = Home(); const std::array, 4> defaults = { { { "CONFIG", ".config" }, { "CACHE", ".cache" }, { "DATA", ".local/share" }, { "STATE", ".local/state" }, } }; for (const auto& [kind, relative] : defaults) { const std::string name = std::format("XDG_{}_HOME", kind); const std::optional host = GetEnv(std::format("HOST_XDG_{}_HOME", kind)); const std::string value = host && !host->empty() ? *host : Dyn::JoinPath(home, relative); setenv(name.c_str(), value.c_str(), 1); } } void RestoreShell() { const std::string shell = GetEnv("SHELL").value_or(""); const bool usable = (shell.ends_with("/bash") || shell.ends_with("/zsh")) && access(shell.c_str(), X_OK) == 0; if (!usable && access("/bin/bash", X_OK) == 0) setenv("SHELL", "/bin/bash", 1); } fs::path SelfExe() { std::error_code error; const fs::path exe = fs::read_symlink("/proc/self/exe", error); return error ? fs::path() : exe; } double Now() { const auto since = std::chrono::system_clock::now().time_since_epoch(); return std::chrono::duration(since).count(); } std::string LocalTime(std::string_view format, double epochSeconds) { const std::time_t seconds = static_cast(std::floor(epochSeconds)); std::tm local {}; localtime_r(&seconds, &local); std::array buffer {}; const std::string pattern(format); const std::size_t length = std::strftime(buffer.data(), buffer.size(), pattern.c_str(), &local); return std::string(buffer.data(), length); } std::string LocalTime(std::string_view format) { return LocalTime(format, Now()); } std::string TodayIso() { return LocalTime("%Y-%m-%d"); } std::optional ReadFile(const fs::path& path) { std::ifstream in(path, std::ios::binary); if (!in) return std::nullopt; std::string bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); if (in.bad()) return std::nullopt; return bytes; } void WriteAtomic(const fs::path& path, std::string_view content, std::uint32_t mode) { if (path.has_parent_path()) fs::create_directories(path.parent_path()); const fs::path temp = std::format("{}.{}.tmp", path.string(), PublishedPid()); const std::int32_t fd = open(temp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, static_cast(mode)); if (fd < 0) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(temp.string()))); std::size_t written = 0; while (written < content.size()) { const ssize_t step = write(fd, content.data() + written, content.size() - written); if (step < 0) { if (errno == EINTR) continue; const std::int32_t failure = errno; close(fd); unlink(temp.c_str()); throw std::runtime_error(Dyn::ErrnoText(failure)); } written += static_cast(step); } close(fd); if (rename(temp.c_str(), path.c_str()) != 0) { const std::int32_t failure = errno; unlink(temp.c_str()); throw std::runtime_error(std::format("{}: {} -> {}", Dyn::ErrnoText(failure), Dyn::Repr(temp.string()), Dyn::Repr(path.string()))); } } void MakeDirs(const fs::path& path, std::uint32_t mode) { const fs::path name = path.filename().empty() && path.has_parent_path() ? path.parent_path() : path; const fs::path head = name.parent_path(); std::error_code error; if (!head.empty() && head != name && !fs::exists(head, error)) { MakeDirs(head, 0777); if (name.filename() == ".") return; } if (mkdir(name.c_str(), static_cast(mode)) != 0) { const std::int32_t failure = errno; if (!fs::is_directory(name, error)) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(failure), Dyn::Repr(name.string()))); } } FileLock::FileLock(const fs::path& lockPath) { std::error_code error; if (lockPath.has_parent_path()) fs::create_directories(lockPath.parent_path(), error); fd = open(lockPath.c_str(), O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0666); if (fd < 0) { problem = std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(lockPath.string())); return; } while (flock(fd, LOCK_EX) != 0) { if (errno == EINTR) continue; problem = Dyn::ErrnoText(errno); close(fd); fd = -1; return; } } FileLock::~FileLock() { if (fd >= 0) { flock(fd, LOCK_UN); close(fd); } } bool FileLock::Held() const { return fd >= 0; } const std::string& FileLock::Problem() const { return problem; } Json LockedUpdate(const fs::path& path, const std::function& change, const Json& fallback, std::uint32_t mode, const std::function& warn) { const FileLock lock(std::format("{}.lock", path.string())); if (!lock.Held() && warn) { warn(std::format("{}: no lock ({}) - best-effort write", path.filename().string(), lock.Problem())); } Json data; try { if (const std::optional text = ReadFile(path)) data = ParseLoose(*text); } catch (const std::exception&) { data = Json(); } if (!data.is_object()) data = fallback.is_object() ? fallback : Json::object(); if (change(data)) { try { WriteAtomic(path, DumpFile(data), mode); } catch (const std::exception& e) { if (warn) warn(std::format("{}: not saved ({})", path.filename().string(), e.what())); } } return data; } std::optional ProcStart(std::int64_t pid) { const std::optional stat = ReadFile(Paths().proc / std::to_string(pid) / "stat"); if (!stat) return std::nullopt; const std::size_t close = stat->rfind(')'); if (close == std::string::npos) return std::nullopt; const std::vector rest = Dyn::Split(std::string_view(*stat).substr(close + 1)); if (rest.size() < 20) return std::nullopt; if (rest[0] == "Z" || rest[0] == "X") return std::nullopt; return ToInt(rest[19]); } std::string BootId() { const std::optional text = ReadFile(Paths().proc / "sys" / "kernel" / "random" / "boot_id"); if (!text) return ""; return Dyn::Slice(Dyn::Strip(*text), 0, 8); } std::map MemInfo(const fs::path& path) { std::map out; const std::optional text = ReadFile(path); if (!text) return out; for (const std::string& line : Dyn::SplitLines(*text)) { const std::size_t colon = line.find(':'); if (colon == std::string::npos) continue; const std::vector parts = Dyn::Split(std::string_view(line).substr(colon + 1)); if (parts.empty()) continue; const std::optional value = ToInt(parts[0]); if (!value) continue; out[line.substr(0, colon)] = parts.size() > 1 && parts[1] == "kB" ? *value / 1024 : *value; } return out; } std::map> PpidMap() { std::map> out; std::error_code error; for (const fs::directory_entry& entry : fs::directory_iterator("/proc", error)) { const std::string name = entry.path().filename().string(); const std::optional pid = ToInt(name); if (!pid) continue; const std::optional stat = ReadFile(entry.path() / "stat"); if (!stat) continue; const std::size_t close = stat->rfind(')'); if (close == std::string::npos) continue; const std::vector rest = Dyn::Split(std::string_view(*stat).substr(close + 1)); if (rest.size() < 2) continue; if (const std::optional ppid = ToInt(rest[1])) out[*ppid].push_back(*pid); } return out; } std::vector Tree(std::int64_t pid, const std::map>& kids) { std::vector out; std::vector todo { pid }; std::set seen; while (!todo.empty()) { const std::int64_t next = todo.back(); todo.pop_back(); if (!seen.insert(next).second) continue; out.push_back(next); if (const auto found = kids.find(next); found != kids.end()) todo.insert(todo.end(), found->second.begin(), found->second.end()); } return out; } double PssMb(std::span pids) { std::int64_t kb = 0; for (const std::int64_t pid : pids) { const std::optional text = ReadFile(std::format("/proc/{}/smaps_rollup", pid)); if (!text) continue; for (const std::string& line : Dyn::SplitLines(*text)) { if (!line.starts_with("Pss:")) continue; const std::vector parts = Dyn::Split(line); if (parts.size() > 1) { if (const std::optional value = ToInt(parts[1])) kb += *value; } break; } } return static_cast(kb) / 1024; } std::string Identity::Key() const { return std::format("{}:{}", pid, start); } std::optional Self() { if (!InSandbox()) { const std::int64_t pid = getpid(); return Identity { .pid = pid, .start = ProcStart(pid).value_or(0) }; } const std::optional info = ReadFile(Paths().flatpakInfo); const std::optional instance = info ? KeyFileValue(*info, "Instance", "instance-id") : std::nullopt; if (!instance) return std::nullopt; const fs::path bwrapinfo = Paths().runtimeDir / ".flatpak" / *instance / "bwrapinfo.json"; // flatpak writes the file once bwrap reports the child's pid, which // can be a moment after the application is already running. for (std::int32_t attempt = 0; attempt < 20; ++attempt) { if (const std::optional text = ReadFile(bwrapinfo)) { try { const Json parsed = Json::parse(*text); if (parsed.contains("child-pid") && parsed["child-pid"].is_number_integer()) { const std::optional start = ProcStart(1); if (start) return Identity { .pid = parsed["child-pid"].get(), .start = *start }; } } catch (const std::exception&) { } } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return std::nullopt; } std::int64_t PublishedPid() { static const std::int64_t pid = [] { if (const std::optional self = Self()) return self->pid; return static_cast(getpid()); }(); return pid; } fs::path LifetimeLockFile(const fs::path& dir, std::int64_t pid, std::int64_t start) { return dir / std::format("{}-{}.lock", pid, start); } LifetimeLock::LifetimeLock(const fs::path& dir, const Identity& identity) : file(LifetimeLockFile(dir, identity.pid, identity.start)) { fs::create_directories(dir); if (ProbeLifetimeLock(dir, identity.pid, identity.start) == true) { throw std::runtime_error(std::format("another driver already runs as {} (its lock {} is held): start each driver with its own `flatpak run`", identity.Key(), file.string())); } const fs::path temp = dir / std::format(".{}-{}.{}.tmp", identity.pid, identity.start, TokenHex(4)); fd = open(temp.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0644); if (fd < 0) throw std::runtime_error(std::format("{}: {}", temp.string(), Dyn::ErrnoText(errno))); if (flock(fd, LOCK_EX | LOCK_NB) != 0 || rename(temp.c_str(), file.c_str()) != 0) { const std::int32_t failure = errno; close(fd); fd = -1; unlink(temp.c_str()); throw std::runtime_error(std::format("{}: {}", file.string(), Dyn::ErrnoText(failure))); } } LifetimeLock::~LifetimeLock() { if (fd < 0) return; // Unlinked while still held: a prober that opens the file after this // finds none, which every rule reads as "this driver is gone or never // took a lock" and the board's own leave() has already removed it. unlink(file.c_str()); flock(fd, LOCK_UN); close(fd); } const fs::path& LifetimeLock::File() const { return file; } std::optional ProbeLifetimeLock(const fs::path& dir, std::int64_t pid, std::int64_t start) { const fs::path file = LifetimeLockFile(dir, pid, start); const std::int32_t fd = open(file.c_str(), O_RDONLY | O_CLOEXEC); if (fd < 0) return std::nullopt; // Shared: two probers looking at one dead driver both see it dead, // where an exclusive probe would show each the other's lock. const bool free = flock(fd, LOCK_SH | LOCK_NB) == 0; if (free) flock(fd, LOCK_UN); close(fd); return !free; } }