mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-17 16:02:21 +02:00
Co-authored-by: Lydacious <81742991+lydacious@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
3.7 KiB
C++
117 lines
3.7 KiB
C++
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See portable.h.
|
|
#include "portable.h"
|
|
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <format>
|
|
#include <system_error>
|
|
|
|
namespace portable
|
|
{
|
|
namespace
|
|
{
|
|
namespace fs = std::filesystem;
|
|
|
|
// Normalized, no trailing separator.
|
|
fs::path Normal(const fs::path& path)
|
|
{
|
|
fs::path p = path.lexically_normal();
|
|
while (p.has_relative_path() && p.filename().empty())
|
|
p = p.parent_path();
|
|
return p;
|
|
}
|
|
|
|
// `relative` stays inside the folder it's relative to (not "", ".",
|
|
// or "../...").
|
|
bool Inside(const fs::path& relative)
|
|
{
|
|
return !relative.empty() && relative != "." && *relative.begin() != "..";
|
|
}
|
|
}
|
|
|
|
Layout Resolve(const std::filesystem::path& root, bool marker, bool supported)
|
|
{
|
|
Layout layout;
|
|
if (!supported || !marker || root.empty() || !root.is_absolute())
|
|
return layout;
|
|
layout.root = Normal(root);
|
|
layout.data = layout.root / kDataFolder;
|
|
layout.cache = layout.root / kCacheFolder;
|
|
return layout;
|
|
}
|
|
|
|
bool MarkerIn(const std::filesystem::path& root)
|
|
{
|
|
std::error_code ec;
|
|
return !root.empty() && fs::is_regular_file(root / kMarker, ec);
|
|
}
|
|
|
|
const Layout& Detect(const std::filesystem::path& root)
|
|
{
|
|
CurrentLayout() = Resolve(root, kSupported && MarkerIn(root));
|
|
return Current();
|
|
}
|
|
|
|
std::string RecordText(const std::filesystem::path& folder, const Layout& layout)
|
|
{
|
|
fs::path path = Normal(folder);
|
|
if (layout.Active())
|
|
if (fs::path relative = path.lexically_relative(layout.root); Inside(relative))
|
|
return relative.generic_string();
|
|
return path.string();
|
|
}
|
|
|
|
std::optional<std::filesystem::path> ParseRecord(std::string_view text, const Layout& layout)
|
|
{
|
|
while (!text.empty() && (text.back() == '\n' || text.back() == '\r'))
|
|
text.remove_suffix(1);
|
|
if (text.empty())
|
|
return std::nullopt;
|
|
fs::path path{ std::string(text) };
|
|
// (Records before v0.1.1 kept `--dest dir/`'s trailing separator.)
|
|
if (path.is_absolute())
|
|
return Normal(path);
|
|
if (!layout.Active())
|
|
return std::nullopt;
|
|
fs::path relative = path.lexically_normal();
|
|
if (!Inside(relative) || relative.has_root_path())
|
|
return std::nullopt;
|
|
return Normal(layout.root / relative);
|
|
}
|
|
|
|
int CheckWritable(const Layout& layout)
|
|
{
|
|
if (!layout.Active())
|
|
return 0;
|
|
std::error_code ec;
|
|
fs::create_directories(layout.data, ec);
|
|
if (ec)
|
|
{
|
|
std::error_condition condition = ec.default_error_condition();
|
|
return condition.category() == std::generic_category() ? condition.value() : EIO;
|
|
}
|
|
// A real write: a read-only mount, a locked card or permissions all
|
|
// show here, whatever the file system.
|
|
fs::path probe = layout.data / ".write-check";
|
|
errno = 0;
|
|
FILE* f = fopen(probe.string().c_str(), "wb");
|
|
if (!f)
|
|
return errno ? errno : EACCES;
|
|
bool ok = fputc('1', f) != EOF;
|
|
int error = errno;
|
|
ok = fclose(f) == 0 && ok;
|
|
if (!ok && !error)
|
|
error = errno;
|
|
fs::remove(probe, ec);
|
|
return ok ? 0 : (error ? error : EIO);
|
|
}
|
|
|
|
std::string Describe(const Layout& layout)
|
|
{
|
|
if (!layout.Active())
|
|
return {};
|
|
return std::format("[paths] portable mode ({} in {}): data in {}, caches in {}\n", kMarker, layout.root.string(),
|
|
layout.data.string(), layout.cache.string());
|
|
}
|
|
}
|