wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
casc_storage.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <charconv>
6#include <cstring>
7#include <format>
8#include <fstream>
9#include <vector>
10
11#include <CascLib.h>
12
13#include <wowlib/core/path.hpp>
14
15namespace wowlib::fs {
16 namespace {
17 namespace fsys = std::filesystem;
18
22 LPCSTR cascFdidName(std::uint32_t fdid) {
23 return reinterpret_cast<LPCSTR>(static_cast<std::uintptr_t>(fdid));
24 }
25
26 struct BuildConfigCandidate {
27 std::string key; // hex file name under Data/config/xx/yy/
28 std::uint32_t build = 0; // parsed from "build-name = WOW-<build>..."
29 };
30
31 // Scans Data/config for "# Build Configuration" files; repacks carry several
32 // (old builds, modified variants) and only trying them tells which one the
33 // storage actually matches.
34 std::vector<BuildConfigCandidate> scanBuildConfigs(const fsys::path& configDir) {
35 std::vector<BuildConfigCandidate> found;
36
37 std::error_code ec;
38 for (fsys::recursive_directory_iterator it{configDir, ec}, end; !ec && it != end; it.increment(ec)) {
39 if (!it->is_regular_file(ec)) continue;
40
41 std::ifstream file{it->path()};
42 std::string line;
43 if (!std::getline(file, line) || !line.starts_with("# Build Configuration")) continue;
44
45 BuildConfigCandidate candidate{.key = it->path().filename().string()};
46 while (std::getline(file, line))
47 if (const auto pos = line.find("build-name"); pos != std::string::npos) {
48 if (const auto wow = line.find("WOW-"); wow != std::string::npos)
49 std::from_chars(line.data() + wow + 4, line.data() + line.size(), candidate.build);
50 break;
51 }
52 found.push_back(std::move(candidate));
53 }
54 return found;
55 }
56
57 // Synthesizes the minimal .build.info CascLib needs (Active, Build Key, CDN
58 // Key, Product) in a shim directory whose Data symlink points back into the
59 // client, leaving the client untouched.
60 Result<fsys::path> writeShim(const fsys::path& clientData,
61 const std::string& buildKey,
62 const std::string& product) {
63 const auto shim = fsys::temp_directory_path() / "wowlib-casc-shim" / std::format(
64 "{:016x}-{}", fsys::hash_value(clientData), buildKey);
65
66 std::error_code ec;
67 fsys::create_directories(shim, ec);
68 if (ec)
70 std::format("cannot create CASC shim '{}': {}", shim.string(), ec.message()));
71
72 fsys::remove(shim / "Data", ec);
73 fsys::create_directory_symlink(clientData, shim / "Data", ec);
74 if (ec)
76 std::format("cannot link '{}' into CASC shim: {}", clientData.string(), ec.message()));
77
78 std::ofstream info{shim / ".build.info", std::ios::trunc};
79 // the CDN key is required by the parser but unused for local storages; the
80 // build key doubles as a syntactically valid stand-in
81 info << "Active!DEC:1|Build Key!HEX:16|CDN Key!HEX:16|Product!STRING:0\n" << std::format(
82 "1|{}|{}|{}\n", buildKey, buildKey, product);
83 if (!info.flush())
84 return makeError(ErrorCode::IoError, std::format("cannot write '{}'", (shim / ".build.info").string()));
85
86 return shim;
87 }
88
89 // CASC stores paths with forward slashes; convert from wowlib canonical form.
90 std::string cascName(const std::string& canonicalPath) {
91 return toNativeRelative(canonicalPath);
92 }
93
94 Result<FileBuffer> readOpenFile(HANDLE file, const std::string& what) {
95 ULONGLONG size = 0;
96 if (!CascGetFileSize64(file, &size)) {
97 const auto native = GetCascError();
98 CascCloseFile(file);
99 return makeError(ErrorCode::BackendError, std::format("CascGetFileSize64 failed for {}", what),
100 static_cast<std::uint32_t>(native));
101 }
102
103 FileBuffer buffer(size);
104 DWORD read = 0;
105 if (!buffer.empty() && !CascReadFile(file, buffer.data(), static_cast<DWORD>(buffer.size()), &read)) {
106 const auto native = GetCascError();
107 CascCloseFile(file);
108 const bool encrypted = native == ERROR_FILE_ENCRYPTED;
110 encrypted
111 ? std::format("{} is behind an unknown TACT key", what)
112 : std::format("CascReadFile failed for {}", what), static_cast<std::uint32_t>(native));
113 }
114
115 CascCloseFile(file);
116 return buffer;
117 }
118 }
119
121 CascStorage storage{std::move(options)};
122 if (auto opened = storage._openStorage(); !opened) return std::unexpected(opened.error());
123 return storage;
124 }
125
126 Result<void> CascStorage::_openStorage() {
127 std::scoped_lock lock{_mtx};
128
129 const auto localeMask = cascLocaleFlag(_options.locale);
130
131 const auto tryOpen = [&](const fsys::path& localPath) -> HANDLE {
132 CASC_OPEN_STORAGE_ARGS args{};
133 args.Size = sizeof(args);
134 const std::string path = localPath.string();
135 args.szLocalPath = path.c_str();
136 args.szCodeName = _options.product.c_str();
137 args.dwLocaleMask = localeMask;
138 HANDLE handle = nullptr;
139 return CascOpenStorageEx(nullptr, &args, false, &handle) ? handle : nullptr;
140 };
141
142 // (a) a proper install: .build.info at the root (or discoverable from Data/)
143 for (const auto& root : {_options.clientRoot, _options.clientRoot / "Data"})
144 if (HANDLE handle = tryOpen(root)) {
145 _storage = handle;
146 return {};
147 }
148 const std::uint32_t plainNative = GetCascError();
149
150 // (b) a repack without .build.info: synthesize one per build config candidate
151 // in a shim directory and try until the storage opens. Only opening tells
152 // which config the local data actually matches — repacks carry stale and
153 // modified configs side by side.
154 auto candidates = scanBuildConfigs(_options.clientRoot / "Data" / "config");
155 std::ranges::stable_sort(candidates, [&](const auto& a, const auto& b) {
156 if (_options.build) {
157 const bool aMatch = a.build == *_options.build;
158 const bool bMatch = b.build == *_options.build;
159 if (aMatch != bMatch) return aMatch;
160 }
161 return a.build > b.build;
162 });
163
164 for (const auto& candidate : candidates) {
165 auto shim = writeShim(_options.clientRoot / "Data", candidate.key, _options.product);
166 if (!shim) return std::unexpected(shim.error());
167
168 if (HANDLE handle = tryOpen(*shim)) {
169 _storage = handle;
170 return {};
171 }
172 }
173
175 std::format(
176 "CascOpenStorage failed for '{}' (product '{}'; no .build.info and "
177 "{} build config candidate(s) tried)", _options.clientRoot.string(), _options.product,
178 candidates.size()), plainNative);
179 }
180
181 void CascStorage::_close() noexcept {
182 std::scoped_lock lock{_mtx};
183 if (_storage) {
184 CascCloseStorage(_storage);
185 _storage = nullptr;
186 }
187 }
188
190 std::scoped_lock lock{_mtx};
191 if (!_storage) return makeError(ErrorCode::StorageNotOpen, "CASC storage is not open");
192
193 HANDLE file = nullptr;
194
195 if (key.fdid) {
196 if (CascOpenFile(_storage, cascFdidName(key.fdid->value), cascLocaleFlag(_options.locale),
197 CASC_OPEN_BY_FILEID, &file))
198 return readOpenFile(file, std::format("FileDataID {}", key.fdid->value));
199
200 const std::uint32_t native = GetCascError();
202 std::format("FileDataID {} was not found in the CASC storage", key.fdid->value), native);
203 }
204
205 if (key.path) {
206 const std::string name = cascName(*key.path);
207 if (CascOpenFile(_storage, name.c_str(), cascLocaleFlag(_options.locale), CASC_OPEN_BY_NAME, &file)) return
208 readOpenFile(file, std::format("'{}'", name));
209
210 const std::uint32_t native = GetCascError();
212 std::format(
213 "'{}' could not be opened by name — this client's "
214 "root manifest likely has no name hashes; resolve " "the path through a listfile", name),
215 native);
216 }
217
218 return makeError(ErrorCode::InvalidPath, "empty FileKey");
219 }
220
221 Result<void> CascStorage::addEncryptionKey(std::uint64_t keyName, std::span<const std::byte, 16> key) {
222 std::scoped_lock lock{_mtx};
223 if (!_storage) return makeError(ErrorCode::StorageNotOpen, "CASC storage is not open");
224 // CascLib takes the key as a mutable LPBYTE but only reads it.
225 auto bytes = std::array<std::uint8_t, 16>{};
226 std::memcpy(bytes.data(), key.data(), bytes.size());
227 if (!CascAddEncryptionKey(_storage, keyName, bytes.data()))
228 return makeError(ErrorCode::BackendError, std::format("CascLib rejected TACT key {:016X}", keyName),
229 GetCascError());
230 return {};
231 }
232
233 Result<void> CascStorage::importKeys(std::string_view keyList) {
234 std::scoped_lock lock{_mtx};
235 if (!_storage) return makeError(ErrorCode::StorageNotOpen, "CASC storage is not open");
236 const std::string list{keyList};
237 if (!CascImportKeysFromString(_storage, list.c_str()))
238 return makeError(ErrorCode::BackendError, "CascLib rejected the TACT key list", GetCascError());
239 return {};
240 }
241
243 std::scoped_lock lock{_mtx};
244 if (!_storage) return makeError(ErrorCode::StorageNotOpen, "CASC storage is not open");
245
246 CASC_FIND_DATA found{};
247 HANDLE find = CascFindFirstFile(_storage, "*", &found, nullptr);
248 if (!find)
249 return makeError(ErrorCode::BackendError, "CascFindFirstFile failed", GetCascError());
250
251 std::vector<FileDataID> fdids;
252 do {
253 // Only content that is actually present locally and addressable by id;
254 // WoD-era roots are name-hash keyed and report CASC_INVALID_ID.
255 if (found.bFileAvailable && found.dwFileDataId != CASC_INVALID_ID) fdids.
256 push_back(FileDataID{found.dwFileDataId});
257 }
258 while (CascFindNextFile(find, &found));
259 CascFindClose(find);
260
261 std::ranges::sort(fdids);
262 const auto duplicates = std::ranges::unique(fdids);
263 fdids.erase(duplicates.begin(), duplicates.end());
264 return fdids;
265 }
266
267 bool CascStorage::exists(const FileKey& key) {
268 std::scoped_lock lock{_mtx};
269 if (!_storage) return false;
270
271 HANDLE file = nullptr;
272 bool ok = false;
273
274 if (key.fdid)
275 ok = CascOpenFile(_storage, cascFdidName(key.fdid->value), cascLocaleFlag(_options.locale),
276 CASC_OPEN_BY_FILEID, &file);
277 else if (key.path)
278 ok = CascOpenFile(_storage, cascName(*key.path).c_str(), cascLocaleFlag(_options.locale), CASC_OPEN_BY_NAME,
279 &file);
280
281 if (ok) CascCloseFile(file);
282 return ok;
283 }
284}
The CascLib-backed storage for CASC-era clients.
CascStorage(const CascStorage &)=delete
Result< std::vector< FileDataID > > enumerateFdids()
Enumerate every FileDataID the storage holds: entries whose content is locally available and that car...
Result< FileBuffer > readFile(const FileKey &key)
Read a file into memory.
Result< void > importKeys(std::string_view keyList)
Register TACT keys from a text list — the community "KeyName KeyHex" per line format (16 hex nibbles ...
bool exists(const FileKey &key)
Whether the file can be opened (probe open + _close).
static Result< CascStorage > open(Options options)
Open the local storage.
Result< void > addEncryptionKey(std::uint64_t keyName, std::span< const std::byte, 16 > key)
Register a TACT encryption key so the storage can decrypt content behind it (encrypted BLTE blocks,...
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::string toNativeRelative(std::string_view canonical)
Convert a canonical path to a forward-slash relative path for use on the native filesystem (project-d...
Definition path.cpp:23
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
@ FileNotFound
The file exists nowhere in the overlay or storage.
Definition error.hpp:22
@ StorageOpenFailed
The client storage (MPQ chain / CASC) failed to initialize.
Definition error.hpp:19
@ EncryptedContent
Content is behind an unknown TACT encryption key.
Definition error.hpp:31
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
Definition error.hpp:23
@ InvalidPath
A path that cannot be normalized/used.
Definition error.hpp:29
@ BackendError
Unclassified StormLib/CascLib failure; see nativeError.
Definition error.hpp:34
@ StorageNotOpen
Operation on a closed or moved-from storage.
Definition error.hpp:21
@ IoError
Generic filesystem I/O failure (project directory).
Definition error.hpp:30
std::uint32_t cascLocaleFlag(Locale locale)
The CASC_LOCALE_* bit of locale for CascOpenStorage/CascOpenFile locale masks.
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
Definition buffer.hpp:16
Client-internal path canonicalization.
std::optional< FileDataID > fdid
The numeric identifier, if known.
Definition file_key.hpp:28
std::optional< std::string > path
The canonical client-internal path, if known.
Definition file_key.hpp:31
Locale locale
Locale mask for content selection.
std::string product
TACT product code.