246 lines
8.6 KiB
C++
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
|