wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
filesystem.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <format>
6#include <ranges>
7
8namespace wowlib::fs {
9 namespace {
10 // The client's archive directory is canonically `Data/`, but repacks exist
11 // that ship it lowercase, which matters on case-sensitive filesystems.
12 // Prefer the canonical spelling; otherwise take any case variant present;
13 // fall back to the canonical path so the storage produces its natural
14 // "nothing there" error.
15 std::filesystem::path resolveDataDir(const std::filesystem::path& clientPath) {
16 std::error_code ec;
17 if (std::filesystem::is_directory(clientPath / "Data", ec)) return clientPath / "Data";
18 for (const auto& entry : std::filesystem::directory_iterator{clientPath, ec}) {
19 const std::string name = entry.path().filename().string();
20 if (name.size() == 4 && entry.is_directory(ec) && std::tolower(static_cast<unsigned char>(name[0])) == 'd' &&
21 std::tolower(static_cast<unsigned char>(name[1])) == 'a' && std::tolower(static_cast<unsigned char>(name[2]))
22 == 't' && std::tolower(static_cast<unsigned char>(name[3])) == 'a') return entry.path();
23 }
24 return clientPath / "Data";
25 }
26 }
27
30 std::optional<std::filesystem::path> projectDirectory,
31 std::optional<std::filesystem::path> listfileCsv,
33 auto install = ClientInstall::detect(std::move(clientPath));
34 if (!install) return std::unexpected(install.error());
35
36 return FileSystemSettings{
37 .clientPath = std::move(install->path),
38 .version = install->version,
39 .locale = locale,
40 .projectDirectory = std::move(projectDirectory),
41 .listfileCsv = std::move(listfileCsv),
42 .customFdidStart = customFdidStart,
43 .cascProduct = std::move(install->cascProduct)
44 };
45 }
46
48 std::optional<ProjectDirectory> project;
49 if (settings.projectDirectory) {
50 auto opened = ProjectDirectory::open(*settings.projectDirectory);
51 if (!opened) return std::unexpected(opened.error());
52 project = std::move(*opened);
53 }
54
55 if (settings.version.storageKind() == StorageKind::Mpq) {
56 auto storage = MpqStorage::open({
57 .dataDir = resolveDataDir(settings.clientPath),
58 .version = settings.version,
59 .locale = settings.locale
60 });
61 if (!storage) return std::unexpected(storage.error());
62
63 return FileSystem{MpqFileSystem{std::move(*storage), NullListfile{}, std::move(project)}, settings.version};
64 }
65
66 // The supplied CSV is the working database: lookups read it, registrations
67 // append to it. Without one, FDID-only access still works.
68 CsvListfile listfile;
69 if (settings.listfileCsv) {
70 auto loaded = CsvListfile::load(*settings.listfileCsv, {.customFdidStart = settings.customFdidStart});
71 if (!loaded) return std::unexpected(loaded.error());
72 listfile = std::move(*loaded);
73 }
74
75 auto storage = CascStorage::open({
76 .clientRoot = settings.clientPath,
77 .product = settings.cascProduct.value_or(std::string{settings.version.defaultCascProduct()}),
78 .locale = settings.locale,
79 .build = settings.version.build
80 });
81 if (!storage) return std::unexpected(storage.error());
82
83 return FileSystem{CascFileSystem{std::move(*storage), std::move(listfile), std::move(project)}, settings.version};
84 }
85
86 namespace {
87 // The monostate alternative is the closed state; only close() (scripting
88 // languages) can reach it, and every entry point degrades to this error.
89 std::unexpected<Error> closedError() {
90 return makeError(ErrorCode::StorageNotOpen, "the filesystem is closed");
91 }
92
93 template <typename T> concept IsComposition = !std::is_same_v<std::remove_cvref_t<T>, std::monostate>;
94 }
95
97 return std::visit([&](auto& fs) -> Result<FileBuffer> {
98 if constexpr (IsComposition<decltype(fs)>) return fs.readFile(key);
99 else return closedError();
100 }, _impl);
101 }
102
103 bool FileSystem::exists(const FileKey& key) {
104 return std::visit([&](auto& fs) {
105 if constexpr (IsComposition<decltype(fs)>) return fs.exists(key);
106 else return false;
107 }, _impl);
108 }
109
111 if (auto* mpqFs = std::get_if<MpqFileSystem>(&_impl)) return mpqFs->backend().enumeratePaths();
112
113 if (auto* cascFs = std::get_if<CascFileSystem>(&_impl)) {
114 // CASC storages are id-addressed; the listing is every id the listfile
115 // can name. Unnamed ids are dropped — a path listing is only as
116 // complete as the listfile, which is the CASC reality anyway.
117 auto fdids = cascFs->backend().enumerateFdids();
118 if (!fdids) return std::unexpected(fdids.error());
119
120 std::vector<std::string> paths;
121 paths.reserve(fdids->size());
122 for (const FileDataID fdid : *fdids)
123 if (auto path = cascFs->listfile().fdidToPath(fdid)) paths.push_back(std::move(*path));
124 std::ranges::sort(paths);
125 const auto duplicates = std::ranges::unique(paths);
126 paths.erase(duplicates.begin(), duplicates.end());
127 return paths;
128 }
129
130 return closedError();
131 }
132
134 return std::visit([&](const auto& fs) -> FileKey {
135 if constexpr (IsComposition<decltype(fs)>) return fs.resolve(key);
136 else return key; // closed: nothing to consult, the key passes unchanged
137 }, _impl);
138 }
139
140 Result<FileDataID> FileSystem::addFile(std::string_view path, std::span<const std::byte> content) {
141 return std::visit([&](auto& fs) -> Result<FileDataID> {
142 if constexpr (IsComposition<decltype(fs)>) return fs.addFile(path, content);
143 else return closedError();
144 }, _impl);
145 }
146
147 // The concrete compositions; instantiated here so the static lib always
148 // carries them for C++ consumers.
151}
static Result< CascStorage > open(Options options)
Open the local storage.
The static composition of one client's file access: storage backend + listfile database + optional pr...
The CSV-backed path<->FileDataID database ('fileDataId;filepath' per line, the community listfile for...
static Result< CsvListfile > load(const std::filesystem::path &csv, Options options={})
Load a listfile CSV; the file becomes the working database that registerPath appends to.
FileKey resolve(const FileKey &key) const
FileSystem(MpqFileSystem impl, ClientVersion version)
C++-only; target languages construct through open().
static Result< FileSystem > open(FileSystemSettings settings)
Result< FileBuffer > readFile(const FileKey &key)
Result< std::vector< std::string > > enumeratePaths()
bool exists(const FileKey &key)
Whether a file is reachable in the overlay or the storage.
Result< FileDataID > addFile(std::string_view path, std::span< const std::byte > content)
static Result< MpqStorage > open(Options options)
Expand the version's chain and open every archive present on disk.
static Result< ProjectDirectory > open(std::filesystem::path root)
Open (and index) a project directory; it is created if missing.
The runtime facade over the static compositions — the primary welder binding surface of the fs layer.
ClientFileSystem< CascStorage, CsvListfile > CascFileSystem
The concrete composition for CASC-era clients (listfile-resolved FileDataIDs).
ClientFileSystem< MpqStorage, NullListfile > MpqFileSystem
The concrete composition for MPQ-era clients (path-addressed, no listfile).
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
Definition error.hpp:100
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
Definition error.hpp:107
@ StorageNotOpen
Operation on a closed or moved-from storage.
Definition error.hpp:21
@ Mpq
Pre-WoD retail clients (< 6.0), StormLib.
std::uint32_t build
Exact client build number, e.g.
static Result< ClientInstall > detect(std::filesystem::path clientPath)
const ClientVersion version
The client version; selects the storage backend and MPQ chain.
static Result< FileSystemSettings > detect(std::filesystem::path clientPath, Locale locale=Locale::enUS, std::optional< std::filesystem::path > projectDirectory={}, std::optional< std::filesystem::path > listfileCsv={}, FileDataID customFdidStart=DefaultCustomFdidStart)
const FileDataID customFdidStart
const std::optional< std::filesystem::path > listfileCsv
const std::optional< std::string > cascProduct
const std::optional< std::filesystem::path > projectDirectory
const std::filesystem::path clientPath
The client installation root (the directory containing Data/).
The no-database provider for clients that need none (MPQ-era).
Definition listfile.hpp:33