sots-engine/src/mars/vfs/vfs.cpp

246 lines
8.6 KiB
C++

#include "mars/vfs/vfs.h"
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <map>
#include <system_error>
#include <unordered_map>
#include "mars/vfs/path.h"
namespace fs = std::filesystem;
namespace mars::vfs {
namespace {
struct NativeFile {
std::string name; // relative path, '/'-separated, as spelled on disk
fs::path full; // absolute/mount-joined path used to open it
std::uint64_t size = 0;
};
struct NativeMount {
fs::path root;
std::vector<NativeFile> files;
std::unordered_map<std::string, std::size_t> index; // key -> files[]
Result<std::size_t> scan() {
files.clear();
index.clear();
std::error_code ec;
if (!fs::is_directory(root, ec)) return Error{Error::Code::Io, root.string() + " is not a directory"};
std::vector<NativeFile> found;
fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec);
if (ec) return Error{Error::Code::Io, root.string() + ": " + ec.message()};
for (const fs::recursive_directory_iterator end; it != end; it.increment(ec)) {
if (ec) return Error{Error::Code::Io, root.string() + ": " + ec.message()};
const fs::directory_entry& de = *it;
std::error_code ec2;
if (!de.is_regular_file(ec2)) continue;
NativeFile f;
f.full = de.path();
f.name = normalize_name(fs::relative(de.path(), root, ec2).generic_string());
if (ec2) continue;
f.size = de.file_size(ec2);
if (ec2) continue;
found.push_back(std::move(f));
}
// Deterministic precedence when two on-disk names fold to one key:
// the lexicographically first spelling wins.
std::sort(found.begin(), found.end(), [](const NativeFile& a, const NativeFile& b) { return a.name < b.name; });
for (auto& f : found) {
const std::string key = normalize_key(f.name);
if (index.count(key)) continue;
index.emplace(key, files.size());
files.push_back(std::move(f));
}
return files.size();
}
};
struct Mount {
MountInfo info;
std::unique_ptr<ZipArchive> zip; // kind == Zip
std::unique_ptr<NativeMount> native; // kind == Native
};
Result<std::vector<std::uint8_t>> read_native(const NativeFile& f) {
std::FILE* fp = std::fopen(f.full.string().c_str(), "rb");
if (!fp) return Error{Error::Code::Io, "cannot open " + f.full.string()};
std::vector<std::uint8_t> out;
std::uint8_t buf[1 << 16];
for (;;) {
const std::size_t n = std::fread(buf, 1, sizeof buf, fp);
out.insert(out.end(), buf, buf + n);
if (n < sizeof buf) break;
}
const bool bad = std::ferror(fp) != 0;
std::fclose(fp);
if (bad) return Error{Error::Code::Io, "read error on " + f.full.string()};
return out;
}
} // namespace
struct Vfs::Impl {
Order order;
std::vector<Mount> mounts;
std::vector<MountInfo> infos; // mirrors mounts[].info for the public accessor
void refresh_infos() {
infos.clear();
for (const Mount& m : mounts) infos.push_back(m.info);
}
std::vector<MountId> search_order() const {
std::vector<MountId> ids;
if (order == Order::NativeFirst) {
for (const Mount& m : mounts)
if (m.info.kind == MountKind::Native) ids.push_back(m.info.id);
for (const Mount& m : mounts)
if (m.info.kind == MountKind::Zip) ids.push_back(m.info.id);
} else {
for (const Mount& m : mounts) ids.push_back(m.info.id);
}
return ids;
}
// The winning mount for a key, plus what it found there.
struct Hit {
const Mount* mount = nullptr;
const ZipEntry* zip = nullptr;
const NativeFile* native = nullptr;
};
Hit resolve(std::string_view rel) const {
const std::string key = normalize_key(rel);
for (MountId id : search_order()) {
const Mount& m = mounts[static_cast<std::size_t>(id)];
if (m.zip) {
if (const ZipEntry* e = m.zip->find(key)) return Hit{&m, e, nullptr};
} else if (m.native) {
auto it = m.native->index.find(key);
if (it != m.native->index.end()) return Hit{&m, nullptr, &m.native->files[it->second]};
}
}
return {};
}
};
Vfs::Vfs(Order order) : impl_(std::make_unique<Impl>()) { impl_->order = order; }
Vfs::~Vfs() = default;
Vfs::Vfs(Vfs&&) noexcept = default;
Vfs& Vfs::operator=(Vfs&&) noexcept = default;
Order Vfs::order() const { return impl_->order; }
Result<MountId> Vfs::mount_zip(const std::string& archive_path) {
Result<ZipArchive> z = ZipArchive::open(archive_path);
if (!z) return z.error();
Mount m;
m.info.id = static_cast<MountId>(impl_->mounts.size());
m.info.kind = MountKind::Zip;
m.info.path = archive_path;
m.info.file_count = z->file_count();
m.zip = std::make_unique<ZipArchive>(std::move(z).value());
impl_->mounts.push_back(std::move(m));
impl_->refresh_infos();
return impl_->mounts.back().info.id;
}
Result<MountId> Vfs::mount_native(const std::string& directory) {
auto nm = std::make_unique<NativeMount>();
nm->root = fs::path(directory);
Result<std::size_t> n = nm->scan();
if (!n) return n.error();
Mount m;
m.info.id = static_cast<MountId>(impl_->mounts.size());
m.info.kind = MountKind::Native;
m.info.path = directory;
m.info.file_count = n.value();
m.native = std::move(nm);
impl_->mounts.push_back(std::move(m));
impl_->refresh_infos();
return impl_->mounts.back().info.id;
}
Result<std::size_t> Vfs::rescan_native(MountId id) {
if (id < 0 || static_cast<std::size_t>(id) >= impl_->mounts.size() || !impl_->mounts[static_cast<std::size_t>(id)].native)
return Error{Error::Code::NotFound, "mount " + std::to_string(id) + " is not a native mount"};
Mount& m = impl_->mounts[static_cast<std::size_t>(id)];
Result<std::size_t> n = m.native->scan();
if (n) {
m.info.file_count = n.value();
impl_->refresh_infos();
}
return n;
}
const std::vector<MountInfo>& Vfs::mounts() const { return impl_->infos; }
std::vector<MountId> Vfs::search_order() const { return impl_->search_order(); }
const ZipArchive* Vfs::archive(MountId id) const {
if (id < 0 || static_cast<std::size_t>(id) >= impl_->mounts.size()) return nullptr;
return impl_->mounts[static_cast<std::size_t>(id)].zip.get();
}
bool Vfs::exists(std::string_view rel) const { return impl_->resolve(rel).mount != nullptr; }
std::optional<Stat> Vfs::stat(std::string_view rel) const {
const Impl::Hit hit = impl_->resolve(rel);
if (!hit.mount) return std::nullopt;
Stat s;
s.mount = hit.mount->info.id;
s.kind = hit.mount->info.kind;
if (hit.zip) {
s.name = normalize_name(hit.zip->name);
s.size = hit.zip->uncompressed_size;
s.compressed = hit.zip->method != 0;
} else {
s.name = hit.native->name;
s.size = hit.native->size;
}
return s;
}
Result<std::vector<std::uint8_t>> Vfs::read(std::string_view rel) const {
const Impl::Hit hit = impl_->resolve(rel);
if (!hit.mount) return Error{Error::Code::NotFound, std::string(rel) + " not found in any mount"};
if (hit.zip) return hit.mount->zip->read(*hit.zip);
return read_native(*hit.native);
}
Result<std::string> Vfs::read_text(std::string_view rel) const {
Result<std::vector<std::uint8_t>> r = read(rel);
if (!r) return r.error();
return std::string(r->begin(), r->end());
}
std::vector<ListEntry> Vfs::list(std::string_view prefix) const {
const std::string pfx = normalize_key(prefix);
std::map<std::string, ListEntry> seen; // key -> winner; map keeps the output sorted
for (MountId id : impl_->search_order()) {
const Mount& m = impl_->mounts[static_cast<std::size_t>(id)];
if (m.zip) {
for (const ZipEntry& e : m.zip->entries()) {
if (e.is_directory || !key_has_prefix(e.key, pfx)) continue;
seen.emplace(e.key, ListEntry{normalize_name(e.name), e.uncompressed_size, id});
}
} else if (m.native) {
for (const auto& [key, idx] : m.native->index) {
if (!key_has_prefix(key, pfx)) continue;
const NativeFile& f = m.native->files[idx];
seen.emplace(key, ListEntry{f.name, f.size, id});
}
}
}
std::vector<ListEntry> out;
out.reserve(seen.size());
for (auto& kv : seen) out.push_back(std::move(kv.second));
return out;
}
} // namespace mars::vfs