#include "mars/vfs/vfs.h" #include #include #include #include #include #include #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 files; std::unordered_map index; // key -> files[] Result 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 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 zip; // kind == Zip std::unique_ptr native; // kind == Native }; Result> 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 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 mounts; std::vector 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 search_order() const { std::vector 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(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_->order = order; } Vfs::~Vfs() = default; Vfs::Vfs(Vfs&&) noexcept = default; Vfs& Vfs::operator=(Vfs&&) noexcept = default; Order Vfs::order() const { return impl_->order; } Result Vfs::mount_zip(const std::string& archive_path) { Result z = ZipArchive::open(archive_path); if (!z) return z.error(); Mount m; m.info.id = static_cast(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(std::move(z).value()); impl_->mounts.push_back(std::move(m)); impl_->refresh_infos(); return impl_->mounts.back().info.id; } Result Vfs::mount_native(const std::string& directory) { auto nm = std::make_unique(); nm->root = fs::path(directory); Result n = nm->scan(); if (!n) return n.error(); Mount m; m.info.id = static_cast(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 Vfs::rescan_native(MountId id) { if (id < 0 || static_cast(id) >= impl_->mounts.size() || !impl_->mounts[static_cast(id)].native) return Error{Error::Code::NotFound, "mount " + std::to_string(id) + " is not a native mount"}; Mount& m = impl_->mounts[static_cast(id)]; Result n = m.native->scan(); if (n) { m.info.file_count = n.value(); impl_->refresh_infos(); } return n; } const std::vector& Vfs::mounts() const { return impl_->infos; } std::vector Vfs::search_order() const { return impl_->search_order(); } const ZipArchive* Vfs::archive(MountId id) const { if (id < 0 || static_cast(id) >= impl_->mounts.size()) return nullptr; return impl_->mounts[static_cast(id)].zip.get(); } bool Vfs::exists(std::string_view rel) const { return impl_->resolve(rel).mount != nullptr; } std::optional 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> 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 Vfs::read_text(std::string_view rel) const { Result> r = read(rel); if (!r) return r.error(); return std::string(r->begin(), r->end()); } std::vector Vfs::list(std::string_view prefix) const { const std::string pfx = normalize_key(prefix); std::map seen; // key -> winner; map keeps the output sorted for (MountId id : impl_->search_order()) { const Mount& m = impl_->mounts[static_cast(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 out; out.reserve(seen.size()); for (auto& kv : seen) out.push_back(std::move(kv.second)); return out; } } // namespace mars::vfs