wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
filesystem.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <filesystem>
8#include <optional>
9#include <span>
10#include <string>
11#include <string_view>
12#include <variant>
13#include <vector>
14
15#include <welder/vocabulary.hpp>
16
19#include <wowlib/core/error.hpp>
26
27namespace wowlib::fs {
30
33
36 inline constexpr FileDataID DefaultCustomFdidStart{1'000'000'000};
37
38 struct [[
39 =welder::weld,
40 =welder::doc(R"(
41 Everything needed to open a client filesystem: where the client is, which
42 version it is, and the optional listfile / project directory / locale
43 configuration. An immutable value, fully described at construction — a
44 settings object can never be half-edited into an inconsistent state. In
45 C++ it stays an aggregate (designated initializers; const members); the
46 scripting languages construct through the synthesized field constructor,
47 where everything after the version is optional.)")
49 [[=welder::doc(
50 "The client installation root (the directory containing Data/).")]]
51 const std::filesystem::path clientPath;
52
53 [[=welder::doc(
54 "The client version; selects the storage backend and MPQ chain.")]]
56
57 [[=welder::doc(R"(
58 Client locale. For MPQ clients its Data/{code}/ directory must exist; for
59 CASC clients it is the locale mask for content selection.)")]]
61
62 [[=welder::doc(R"(
63 The project-directory overlay: the ultimate patch, where new files are
64 added. Optional.)")]]
65 const std::optional<std::filesystem::path> projectDirectory{};
66
67 [[=welder::doc(R"(
68 The working listfile CSV ('fileDataId;filepath'): read for path<->FileDataID
69 resolution and appended to when new files are added. Effectively mandatory
70 for modern CASC clients — without it files can only be requested by
71 FileDataID.)")]]
72 const std::optional<std::filesystem::path> listfileCsv{};
73
74 [[=welder::doc(R"(
75 First FileDataID handed to files added to the project; keep far above
76 Blizzard's ~6-7M so future official content never collides.)")]]
77 const FileDataID customFdidStart{DefaultCustomFdidStart};
79 [[=welder::doc(R"(
80 TACT product code for CASC storages ('wow', 'wow_classic_era', 'wowt',
81 ...). Left unset it is derived from the version's flavor
82 (ClientVersion.default_casc_product), which is right for every ordinary
83 install; set it explicitly for PTR, beta and one-off products
84 ('wow_classic_titan'), or let ClientInstall.detect read the exact code
85 off the installation.)")]]
86 const std::optional<std::string> cascProduct{};
87
88 [[=welder::doc(R"(
89 Settings for a client whose version is read off the installation itself
90 (see ClientInstall.detect) instead of being spelled out — the reliable
91 way to open anything Classic, where the build number decides which
92 engine's file formats you get. The remaining arguments are the same
93 optional configuration the constructor takes.
94
95 Example:
96 ```python
97 settings = FileSystemSettings.detect("/Games/World of Warcraft/_classic_era_",
98 listfile_csv="listfile.csv")
99 with FileSystem.open(settings) as fs:
100 ... # formats resolve to the engine the install really is
101 ```)"),
102 =welder::returns("the settings, or the error ClientInstall.detect raised")
103 ]]
104 static Result<FileSystemSettings> detect(
105 std::filesystem::path clientPath [[=welder::doc("the installation directory holding Data/")]],
106 Locale locale [[=welder::doc("client locale")]] = Locale::enUS,
107 std::optional<std::filesystem::path> projectDirectory [[=welder::doc("the project-directory overlay")]] = {},
108 std::optional<std::filesystem::path> listfileCsv [[=welder::doc("the working listfile CSV")]] = {},
109 FileDataID customFdidStart [[=welder::doc("first FileDataID for added files")]] = DefaultCustomFdidStart);
110 };
111
112 class [[
113 =welder::weld,
114 =welder::policy::weld_protected,
115 =welder::doc(R"(
116 The runtime gateway to one client's files. Picks the storage backend from
117 the client version and hides the static composition behind one bindable
118 type; C++ code that wants zero dispatch overhead can use
119 MpqFileSystem/CascFileSystem directly.
120
121 Lifetime: open() returns an open filesystem and destruction releases it —
122 in C++ that is the whole story (RAII; no public close, so no reachable
123 wrong state beyond a moved-from object). The scripting languages, where
124 finalization timing belongs to the runtime, additionally get explicit
125 control: close() and is_open (welded from the protected surface), and in
126 Python the with-statement.
127
128 Examples:
129 Preferred: scope the storage with a `with` block — `open()` returns
130 the filesystem, `__enter__` hands it to the block, and `__exit__`
131 calls `close()` on the way out, exception or not:
132
133 ```python
134 from wowlib import versions
135 from wowlib.fs import FileSystem, FileSystemSettings
136
137 settings = FileSystemSettings("/Games/WoW 3.3.5a", versions.wotlk)
138 with FileSystem.open(settings) as fs:
139 data = fs.read_file("Interface/FrameXML/UIParent.lua")
140 ```
141
142 Equivalent explicit form, when the lifetime cannot be a single block
143 (a viewer holding the storage across UI events):
145 ```python
146 fs = FileSystem.open(settings)
147 try:
148 data = fs.read_file("Interface/FrameXML/UIParent.lua")
149 finally:
150 fs.close() # storage released now, not at GC time
151 ```)")]]
152 FileSystem {
153 public:
154 [[=welder::doc(R"(
155 Initialize a client filesystem: open the storage (the full MPQ chain or
156 the CASC storage), load the listfile if given, and attach the
157 project-directory overlay.)"),
158 =welder::returns("the opened filesystem")]]
159 static Result<FileSystem> open(FileSystemSettings settings [[=welder::doc("what to open and how")]]);
160
161 [[=welder::doc(R"(
162 Read a file by FileKey — the generic identity for version-independent
163 code: whichever half the key carries (path, FileDataID or both), the
164 backend uses what it can address and the listfile fills the gap.)"),
165 =welder::returns("the file bytes")]]
167 const FileKey& key [[=welder::doc("the file identity (path, id, or both)")
168 ]]);
169
170 [[=welder::doc("Read a file by client-internal path."),
171 =welder::returns("the file bytes")]]
172 Result<FileBuffer> readFile(std::string_view path [[=welder::doc("the client-internal file path")]]) {
173 return readFile(FileKey{path});
174 }
175
176 [[=welder::doc("Read a file by FileDataID."),
177 =welder::returns("the file bytes")]]
178 Result<FileBuffer> readFile(FileDataID fdid [[=welder::doc("the numeric file identifier")]]) {
179 return readFile(FileKey{fdid});
180 }
181
182 [[=welder::doc("Whether a file is reachable in the overlay or the storage.")
183 ,
184 =welder::returns("true if a read would find it")]]
185 bool exists(
186 const FileKey& key [[=welder::doc("the file identity (path, id, or both)")
187 ]]);
188
189 [[=welder::doc("Whether a path is reachable."),
190 =welder::returns("true if a read would find it")]]
191 bool exists(std::string_view path [[=welder::doc("the client-internal file path")]]) {
192 return exists(FileKey{path});
193 }
194
195 [[=welder::doc("Whether a FileDataID is reachable."),
196 =welder::returns("true if a read would find it")]]
197 bool exists(FileDataID fdid [[=welder::doc("the numeric file identifier")]]) {
198 return exists(FileKey{fdid});
199 }
200
201 [[=welder::doc(R"(
202 Enumerate every client-internal file path reachable in the storage:
203 the union of the MPQ chain's archive listings (loose directories
204 included), or — on CASC clients — every FileDataID the storage holds
205 that the loaded listfile can name (unnamed ids are skipped, and
206 without a listfile the listing is empty). Paths come back canonical,
207 deduplicated and sorted; the project-directory overlay is not
208 included.)"),
209 =welder::returns("the sorted canonical paths")]]
210 Result<std::vector<std::string>> enumeratePaths();
211
212 [[=welder::doc(R"(
213 Fill the missing half of a key (path or FileDataID) from the listfile,
214 best-effort: present halves are preserved, unknown halves stay empty
215 (on MPQ-era clients there is no FileDataID space at all). Lets generic
216 tools learn a file's full identity without storage-specific code.)"),
217 =welder::returns("the completed key")]]
218 FileKey resolve(const FileKey& key [[=welder::doc("the file identity to complete")]]) const;
219
220 [[=welder::doc(R"(
221 Add (or overwrite) a file in the project directory; on CASC-era clients
222 new paths get a custom FileDataID, persisted in the working listfile.)")
223 ,
224 =welder::returns("the file's FileDataID (0 on MPQ-era clients)")]]
225 Result<FileDataID> addFile(
226 std::string_view path [[=welder::doc(
227 "the client-internal path of the file")]],
228 std::span<const std::byte> content [[=welder::doc("the file contents")]]);
229
236 [[=welder::doc(
237 "Register TACT encryption keys (CASC only) from the community "
238 "'KeyName KeyHex' per-line text, so encrypted .db2 sections "
239 "decrypt and decode."),
240 =welder::returns("nothing; raises on an MPQ client or a malformed list")]]
241 Result<void> importKeys(std::string_view keyList [[=welder::doc("newline-separated 'KeyName KeyHex' lines")]]) {
242 if (auto* casc = std::get_if<CascFileSystem>(&_impl)) return casc->backend().importKeys(keyList);
243 return makeError(ErrorCode::NotSupported, "TACT encryption keys apply only to CASC (WoD+) clients");
244 }
245
251 [[=welder::mark::exclude]]
252 Result<void> addEncryptionKey(std::uint64_t keyName, std::span<const std::byte, 16> key) {
253 if (auto* casc = std::get_if<CascFileSystem>(&_impl)) return casc->backend().addEncryptionKey(keyName, key);
254 return makeError(ErrorCode::NotSupported, "TACT encryption keys apply only to CASC (WoD+) clients");
255 }
256
257 [[=welder::getter,
258 =welder::doc(
259 "Which storage technology backs this filesystem: Mpq or Casc. "
260 "A static fact of the opened client; remains valid after close().")]]
261 StorageKind kind() const { return _kind; }
262
263 [[=welder::getter,
264 =welder::doc("The client version this filesystem was opened for — the "
265 "anchor of version-agnostic code (expansion_of, for_version, "
266 "Table.open). A static fact of the opened client; remains "
267 "valid after close().")]]
268 ClientVersion version() const { return _version; }
269
272 [[=welder::mark::exclude]]
273 MpqFileSystem* mpq() { return std::get_if<MpqFileSystem>(&_impl); }
274
277 [[=welder::mark::exclude]]
278 CascFileSystem* casc() { return std::get_if<CascFileSystem>(&_impl); }
279
283 [[=welder::mark::exclude]]
284 explicit FileSystem(MpqFileSystem impl, ClientVersion version)
285 : _impl(std::move(impl)), _kind(StorageKind::Mpq), _version(version) {}
286
290 [[=welder::mark::exclude]]
291 explicit FileSystem(CascFileSystem impl, ClientVersion version)
292 : _impl(std::move(impl)), _kind(StorageKind::Casc), _version(version) {}
293
294 protected:
295 // Welded through policy::weld_protected, uncallable from C++ (where RAII is
296 // the lifetime story); scripting languages get deterministic release.
297
298 [[=welder::doc(R"(
299 Release the client storage now — every MPQ archive handle or the CASC
300 storage handle — instead of waiting for garbage collection. Safe to call
301 repeatedly; afterwards reads and writes raise StorageNotOpen. In Python,
302 prefer the with-statement, which calls this on block exit.)")]]
303 void close() { _impl = std::monostate{}; }
304
305 [[=welder::getter,
306 =welder::doc(
307 "Whether the filesystem still holds its storage (false after "
308 "close()).")]]
309 bool isOpen() const {
310 return !std::holds_alternative<std::monostate>(_impl);
311 }
312
313 private:
314 // monostate = closed; reachable only through close() above, so C++ callers
315 // never observe it (a moved-from FileSystem still holds a composition).
316 std::variant<std::monostate, MpqFileSystem, CascFileSystem> _impl;
317 StorageKind _kind;
318 ClientVersion _version{};
319 };
320}
The owning byte buffer file contents are read into.
The CascLib-backed storage for CASC-era clients.
The static composition of one client's file access: storage backend + listfile database + optional pr...
FileKey resolve(const FileKey &key) const
Result< FileBuffer > readFile(std::string_view path)
Read a file by client-internal path.
FileSystem(MpqFileSystem impl, ClientVersion version)
C++-only; target languages construct through open().
bool exists(std::string_view path)
Whether a path is reachable.
static Result< FileSystem > open(FileSystemSettings settings)
ClientVersion version() const
The client version this filesystem was opened for — the anchor of version-agnostic code (expansion_of...
Result< FileBuffer > readFile(const FileKey &key)
MpqFileSystem * mpq()
The MPQ composition, for C++ callers that want the static types.
Result< FileBuffer > readFile(FileDataID fdid)
Read a file by FileDataID.
bool exists(FileDataID fdid)
Whether a FileDataID is reachable.
CascFileSystem * casc()
The CASC composition, for C++ callers that want the static types.
Result< std::vector< std::string > > enumeratePaths()
StorageKind kind() const
Which storage technology backs this filesystem: Mpq or Casc.
bool exists(const FileKey &key)
Whether a file is reachable in the overlay or the storage.
Result< void > addEncryptionKey(std::uint64_t keyName, std::span< const std::byte, 16 > key)
Register one TACT encryption key (CASC clients only).
Result< FileDataID > addFile(std::string_view path, std::span< const std::byte > content)
Result< void > importKeys(std::string_view keyList)
Register TACT encryption keys (CASC clients only) so the storage can decrypt content behind them — in...
bool isOpen() const
Whether the filesystem still holds its storage (false after close()).
FileSystem(CascFileSystem impl, ClientVersion version)
C++-only; target languages construct through open().
The static composition of one client's file access.
Reading a client installation's own identity off disk: which product it is, which build,...
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
The CSV-backed listfile provider: one working file ('fileDataId;filepath' per line) that is both read...
The error-handling vocabulary: ErrorCode, Error and the Result<T> alias every fallible wowlib operati...
File identity types: the strong FileDataID and the FileKey a read request travels as.
The StormLib-backed storage for MPQ-era clients.
constexpr FileDataID DefaultCustomFdidStart
The first FileDataID handed to project files by default — far above Blizzard's ~6-7M so future offici...
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
@ NotSupported
Operation not supported by this backend/provider.
Definition error.hpp:32
@ Mpq
Pre-WoD retail clients (< 6.0), StormLib.
@ Casc
WoD+ retail and all Classic clients, CascLib.
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/).