17 namespace fsys = std::filesystem;
22 LPCSTR cascFdidName(std::uint32_t fdid) {
23 return reinterpret_cast<LPCSTR
>(
static_cast<std::uintptr_t
>(fdid));
26 struct BuildConfigCandidate {
28 std::uint32_t build = 0;
34 std::vector<BuildConfigCandidate> scanBuildConfigs(
const fsys::path& configDir) {
35 std::vector<BuildConfigCandidate> found;
38 for (fsys::recursive_directory_iterator it{configDir, ec}, end; !ec && it != end; it.increment(ec)) {
39 if (!it->is_regular_file(ec))
continue;
41 std::ifstream file{it->path()};
43 if (!std::getline(file, line) || !line.starts_with(
"# Build Configuration"))
continue;
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);
52 found.push_back(std::move(candidate));
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);
67 fsys::create_directories(shim, ec);
70 std::format(
"cannot create CASC shim '{}': {}", shim.string(), ec.message()));
72 fsys::remove(shim /
"Data", ec);
73 fsys::create_directory_symlink(clientData, shim /
"Data", ec);
76 std::format(
"cannot link '{}' into CASC shim: {}", clientData.string(), ec.message()));
78 std::ofstream info{shim /
".build.info", std::ios::trunc};
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);
90 std::string cascName(
const std::string& canonicalPath) {
96 if (!CascGetFileSize64(file, &size)) {
97 const auto native = GetCascError();
100 static_cast<std::uint32_t
>(native));
105 if (!buffer.empty() && !CascReadFile(file, buffer.data(),
static_cast<DWORD
>(buffer.size()), &read)) {
106 const auto native = GetCascError();
108 const bool encrypted = native == ERROR_FILE_ENCRYPTED;
111 ? std::format(
"{} is behind an unknown TACT key", what)
112 : std::format(
"CascReadFile failed for {}", what),
static_cast<std::uint32_t
>(native));
122 if (
auto opened = storage._openStorage(); !opened)
return std::unexpected(opened.error());
127 std::scoped_lock lock{_mtx};
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;
143 for (
const auto& root : {_options.clientRoot, _options.clientRoot /
"Data"})
144 if (HANDLE handle = tryOpen(root)) {
148 const std::uint32_t plainNative = GetCascError();
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;
161 return a.build > b.build;
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());
168 if (HANDLE handle = tryOpen(*shim)) {
176 "CascOpenStorage failed for '{}' (product '{}'; no .build.info and "
177 "{} build config candidate(s) tried)", _options.clientRoot.string(), _options.product,
178 candidates.size()), plainNative);
181 void CascStorage::_close() noexcept {
182 std::scoped_lock lock{_mtx};
184 CascCloseStorage(_storage);
190 std::scoped_lock lock{_mtx};
193 HANDLE file =
nullptr;
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));
200 const std::uint32_t native = GetCascError();
202 std::format(
"FileDataID {} was not found in the CASC storage", key.
fdid->value), native);
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));
210 const std::uint32_t native = GetCascError();
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),
222 std::scoped_lock lock{_mtx};
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()))
234 std::scoped_lock lock{_mtx};
236 const std::string list{keyList};
237 if (!CascImportKeysFromString(_storage, list.c_str()))
243 std::scoped_lock lock{_mtx};
246 CASC_FIND_DATA found{};
247 HANDLE find = CascFindFirstFile(_storage,
"*", &found,
nullptr);
251 std::vector<FileDataID> fdids;
255 if (found.bFileAvailable && found.dwFileDataId != CASC_INVALID_ID) fdids.
258 while (CascFindNextFile(find, &found));
261 std::ranges::sort(fdids);
262 const auto duplicates = std::ranges::unique(fdids);
263 fdids.erase(duplicates.begin(), duplicates.end());
268 std::scoped_lock lock{_mtx};
269 if (!_storage)
return false;
271 HANDLE file =
nullptr;
275 ok = CascOpenFile(_storage, cascFdidName(key.
fdid->value),
cascLocaleFlag(_options.locale),
276 CASC_OPEN_BY_FILEID, &file);
278 ok = CascOpenFile(_storage, cascName(*key.
path).c_str(),
cascLocaleFlag(_options.locale), CASC_OPEN_BY_NAME,
281 if (ok) CascCloseFile(file);
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-...
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...
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ FileNotFound
The file exists nowhere in the overlay or storage.
@ StorageOpenFailed
The client storage (MPQ chain / CASC) failed to initialize.
@ EncryptedContent
Content is behind an unknown TACT encryption key.
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
@ InvalidPath
A path that cannot be normalized/used.
@ BackendError
Unclassified StormLib/CascLib failure; see nativeError.
@ StorageNotOpen
Operation on a closed or moved-from storage.
@ IoError
Generic filesystem I/O failure (project directory).
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.
Client-internal path canonicalization.
std::optional< FileDataID > fdid
The numeric identifier, if known.
std::optional< std::string > path
The canonical client-internal path, if known.
Locale locale
Locale mask for content selection.
std::string product
TACT product code.