11#define STORMLIB_NO_AUTO_LINK
15 namespace fsys = std::filesystem;
20 std::ifstream in{path, std::ios::binary | std::ios::ate};
23 std::format(
"failed to open loose file '{}'", path.string()));
24 const std::streamoff size = in.tellg();
27 std::format(
"failed to size loose file '{}'", path.string()));
30 if (!buffer.empty() && !in.read(
reinterpret_cast<char*
>(buffer.data()), size))
32 std::format(
"failed to read loose file '{}'", path.string()));
40 bool isSyntheticName(std::string_view canonical) {
41 for (
const std::string_view metadata : {
"(listfile)",
"(attributes)",
"(signature)",
"(patch_metadata)"})
42 if (canonical == metadata)
return true;
44 if (canonical.size() < 13 || !canonical.starts_with(
"file") || canonical[12] !=
'.')
return false;
45 for (std::size_t i = 4; i < 12; ++i)
46 if (canonical[i] <
'0' || canonical[i] >
'9')
return false;
54 if (
auto opened = storage._openChain(); !opened)
return std::unexpected(opened.error());
72 if (_options.
version.
major >= 2 && !std::filesystem::is_directory(_options.
dataDir / code, ec))
74 std::format(
"locale directory '{}' not found under '{}'", code, _options.
dataDir.string()));
77 if (!chain)
return std::unexpected(chain.error());
83 for (
const detail::ChainMember& member : *chain) {
84 if (member.incremental) {
104 const std::string patchPath = member.path.string();
105 for (OpenedArchive& archive : _archives) {
106 if (archive.isDirectory || archive.path.parent_path() != member.path.parent_path())
continue;
107 if (!SFileOpenPatchArchive(archive.handle, patchPath.c_str(),
nullptr, 0)) {
108 const auto native = SErrGetLastError();
111 std::format(
"SFileOpenPatchArchive failed attaching '{}' to '{}'", member.path.string(),
112 archive.path.string()),
static_cast<std::uint32_t
>(native));
114 archive.patched =
true;
119 if (member.isDirectory) {
124 std::error_code walkEc;
125 for (
const auto& entry : std::filesystem::recursive_directory_iterator{member.path, walkEc}) {
126 if (!entry.is_regular_file(walkEc))
continue;
127 const auto relative = std::filesystem::relative(entry.path(), member.path, walkEc);
128 if (walkEc)
continue;
129 slot.loose.emplace(
normalizePath(relative.generic_string()), entry.path());
131 _archives.push_back(std::move(slot));
139 constexpr DWORD openFlags = MPQ_OPEN_READ_ONLY | MPQ_OPEN_NO_LISTFILE | MPQ_OPEN_NO_ATTRIBUTES |
140 MPQ_OPEN_NO_HEADER_SEARCH;
142 HANDLE handle =
nullptr;
143 const std::string archivePath = member.path.string();
144 if (!SFileOpenArchive(archivePath.c_str(), 0, openFlags, &handle)) {
145 const auto native = SErrGetLastError();
148 std::format(
"SFileOpenArchive failed for '{}'", member.path.string()),
149 static_cast<std::uint32_t
>(native));
151 _archives.push_back(
OpenedArchive{.path = member.path, .handle = handle, .mtx = std::make_unique<std::mutex>()});
156 void MpqStorage::_close() noexcept {
157 for (
auto& archive : _archives)
158 if (archive.handle) SFileCloseArchive(archive.handle);
166 "MPQ storage is path-addressed; resolve the FileDataID to a " "path through a listfile first");
169 const std::string& name = *key.
path;
172 for (
const OpenedArchive& archive : _archives | std::views::reverse) {
173 if (archive.isDirectory) {
174 const auto it = archive.loose.find(name);
175 if (it == archive.loose.end())
continue;
176 return readLooseFile(it->second);
179 std::scoped_lock lock{*archive.mtx};
184 if (!archive.patched && !SFileHasFile(archive.handle, name.c_str()))
continue;
186 HANDLE file =
nullptr;
187 if (!SFileOpenFileEx(archive.handle, name.c_str(), SFILE_OPEN_FROM_MPQ, &file)) {
188 const auto native = SErrGetLastError();
189 if (archive.patched && native == ERROR_FILE_NOT_FOUND)
continue;
191 std::format(
"SFileOpenFileEx failed for '{}' in '{}'", name, archive.path.string()),
192 static_cast<std::uint32_t
>(native));
196 const DWORD sizeLow = SFileGetFileSize(file, &sizeHigh);
197 if (sizeLow == SFILE_INVALID_SIZE) {
198 const auto native = SErrGetLastError();
199 SFileCloseFile(file);
201 static_cast<std::uint32_t
>(native));
204 FileBuffer buffer((
static_cast<std::uint64_t
>(sizeHigh) << 32) | sizeLow);
206 if (!buffer.empty() && !SFileReadFile(file, buffer.data(),
static_cast<DWORD
>(buffer.size()), &read,
nullptr)) {
207 const auto native = SErrGetLastError();
208 SFileCloseFile(file);
210 static_cast<std::uint32_t
>(native));
213 SFileCloseFile(file);
221 if (!_isOpen() || !key.
path)
return false;
223 for (
const OpenedArchive& archive : _archives | std::views::reverse) {
224 if (archive.isDirectory) {
225 if (archive.loose.contains(*key.
path))
return true;
229 std::scoped_lock lock{*archive.mtx};
230 if (archive.patched) {
233 HANDLE file =
nullptr;
234 if (SFileOpenFileEx(archive.handle, key.
path->c_str(), SFILE_OPEN_FROM_MPQ, &file)) {
235 SFileCloseFile(file);
240 if (SFileHasFile(archive.handle, key.
path->c_str()))
return true;
250 std::set<std::string> paths;
253 if (archive.isDirectory) {
255 for (
const auto& name : archive.loose | std::views::keys) paths.insert(name);
259 std::scoped_lock lock{*archive.mtx};
266 std::ignore = SFileAddListFile(archive.handle,
nullptr);
268 SFILE_FIND_DATA found{};
269 HANDLE find = SFileFindFirstFile(archive.handle,
"*", &found,
nullptr);
273 if (!isSyntheticName(canonical)) paths.insert(std::move(canonical));
275 while (SFileFindNextFile(find, &found));
276 SFileFindClose(find);
279 return std::vector<std::string>{std::make_move_iterator(paths.begin()), std::make_move_iterator(paths.end())};
bool exists(const FileKey &key)
Whether the file exists in any archive of the chain.
Result< std::vector< std::string > > enumeratePaths()
Enumerate every file path reachable through the chain: the members of each archive (named by its inte...
static Result< MpqStorage > open(Options options)
Expand the version's chain and open every archive present on disk.
Result< FileBuffer > readFile(const FileKey &key)
Read a file into memory.
MpqStorage(const MpqStorage &)=delete
The StormLib-backed storage for MPQ-era clients.
const MpqChainSpec * findChainSpec(const ClientVersion &version)
The chain spec for version: exact build match first, then major.minor.patch.
Result< std::vector< ChainMember > > expandChain(const MpqChainSpec &spec, const std::filesystem::path &dataDir, Locale locale)
Expand a chain spec against a Data directory into concrete members in load order (lowest -> highest p...
std::string normalizePath(std::string_view path)
Canonicalize a client-internal file path.
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
std::string_view localeCode(Locale locale)
The four-letter code of locale ("enUS", ...) as used in MPQ locale directory and archive names.
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ FdidNotResolvable
No path is known for the given FileDataID.
@ FileNotFound
The file exists nowhere in the overlay or storage.
@ StorageOpenFailed
The client storage (MPQ chain / CASC) failed to initialize.
@ BackendError
Unclassified StormLib/CascLib failure; see nativeError.
@ StorageNotOpen
Operation on a closed or moved-from storage.
@ ArchiveOpenFailed
A single archive within an MPQ chain failed to open.
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
Client-internal path canonicalization.
std::uint16_t patch
Patch version within the minor release.
std::uint32_t build
Exact client build number, e.g.
std::uint16_t major
Expansion number, e.g.
std::uint16_t minor
Minor version within the expansion.
std::optional< std::string > path
The canonical client-internal path, if known.
One opened member of the chain, for introspection and tests.
Locale locale
The locale to open; its Data/{code}/ directory must exist on disk.
std::filesystem::path dataDir
The client's Data/ directory.
ClientVersion version
Selects the chain table.
A client version's archive chain: the fixed base tier plus how its patch tier is found.