diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 0208597..280aff3 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -46,21 +46,18 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create prerelease - uses: softprops/action-gh-release@v2 - with: - tag_name: dev - name: Dev (Pre-release) - prerelease: true - files: | - mpqcli-linux-amd64-glibc - mpqcli-linux-amd64-musl - mpqcli-linux-arm64-glibc - mpqcli-linux-arm64-musl + run: | + gh release create dev \ + --title "Dev (Pre-release)" \ + --prerelease \ + --notes "**Commit:** ${{ github.sha }}" \ + mpqcli-linux-amd64-glibc \ + mpqcli-linux-amd64-musl \ + mpqcli-linux-arm64-glibc \ + mpqcli-linux-arm64-musl \ mpqcli-windows-amd64.exe - body: | - **Commit:** ${{ github.sha }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} prerelease_docker: needs: prerelease_binaries diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bb09bd..581a99b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,17 +48,17 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Release package - uses: softprops/action-gh-release@v2 - with: - files: | - mpqcli-linux-amd64-glibc - mpqcli-linux-amd64-musl - mpqcli-linux-arm64-glibc - mpqcli-linux-arm64-musl + run: | + gh release create ${{ github.ref_name }} \ + --title "${{ github.ref_name }}" \ + --notes "${{ steps.changelog.outputs.content }}" \ + mpqcli-linux-amd64-glibc \ + mpqcli-linux-amd64-musl \ + mpqcli-linux-arm64-glibc \ + mpqcli-linux-arm64-musl \ mpqcli-windows-amd64.exe - body: ${{ steps.changelog.outputs.content }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} release_docker: needs: release_binaries diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 42c07a3..721c58f 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -12,12 +12,16 @@ permissions: jobs: build: uses: ./.github/workflows/build.yml + lint: + uses: ./.github/workflows/lint.yml + needs: build test: uses: ./.github/workflows/test.yml needs: build release: uses: ./.github/workflows/release.yml - needs: + needs: - build + - lint - test secrets: inherit diff --git a/CHANGELOG.md b/CHANGELOG.md index 474e4d2..6d2d57d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.9.10 - 2026-04-27 + +### Fixed + +- Extract command now reports an error when the output directory cannot be created +- Path traversal guard in extract uses fully resolved paths, closing a potential bypass +- Crash when reading strong signatures from malformed or truncated archives + +### Updated + +- Docker glibc image updated to ubuntu:24.04 + ## 0.9.9 - 2026-04-05 ### Fixed diff --git a/CMakeLists.txt b/CMakeLists.txt index dae57b4..39d3caa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.10) -project(MPQCLI VERSION 0.9.9) +project(MPQCLI VERSION 0.9.10) # Options option(BUILD_MPQCLI "Build the mpqcli CLI app" ON) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 606d8d1..9d9ba29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,6 +112,19 @@ if (flags & MPQ_FILE_COMPRESS) result += 'c'; // clang-format on ``` +## Known Design Constraints + +### StormLib locale state is global and not thread-safe + +`SFileSetLocale` sets a process-wide locale variable (`g_lcFileLocale`) inside StormLib. All locale-sensitive operations in `mpq.cpp` — file open, add, remove, read, extract, and list — call `SFileSetLocale` immediately before the relevant StormLib call. There is no locale-explicit alternative in StormLib's public API (`SFileOpenFileEx`, `SFileAddFileEx`, etc. all read `g_lcFileLocale` internally). + +This means: + +- The `SFileSetLocale` + StormLib-call sequence is **not atomic** and would be unsafe under concurrency +- mpqcli is intentionally **single-threaded**; do not introduce threads or async I/O without auditing every locale-sensitive call site in `mpq.cpp` + +If you add a new StormLib call that is locale-sensitive, follow the existing pattern: call `SFileSetLocale` immediately before it, with no intervening calls between the two. + ## Workflow Summary 1. Fork the repository and create a branch for your change diff --git a/Dockerfile.glibc b/Dockerfile.glibc index 866f44e..6a06e39 100644 --- a/Dockerfile.glibc +++ b/Dockerfile.glibc @@ -1,5 +1,5 @@ # Stage 1: Build and Test -FROM ubuntu:22.04 AS builder +FROM ubuntu:24.04 AS builder RUN apt-get update && apt-get install -y \ build-essential \ @@ -21,11 +21,7 @@ RUN cmake --build build RUN strip build/bin/mpqcli # Stage 2: Create a minimal runtime image -FROM ubuntu:22.04 AS runtime - -RUN apt-get update && apt-get install -y \ - libstdc++6 \ - && rm -rf /var/lib/apt/lists/* +FROM ubuntu:24.04 AS runtime COPY --from=builder /mpqcli/build/bin/mpqcli /usr/local/bin/mpqcli diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4577f55..21bca10 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,7 @@ endif() # Create the main executable add_executable(mpqcli main.cpp + commands.cpp mpq.cpp helpers.cpp locales.cpp diff --git a/src/commands.cpp b/src/commands.cpp new file mode 100644 index 0000000..384d3e3 --- /dev/null +++ b/src/commands.cpp @@ -0,0 +1,327 @@ +#include "commands.h" + +#include +#include + +#include + +#include "gamerules.h" +#include "helpers.h" +#include "locales.h" +#include "mpq.h" +#include "mpqcli.h" + +namespace fs = std::filesystem; + +int HandleVersion() { + std::cout << MPQCLI_VERSION << "-" << GIT_COMMIT_HASH << std::endl; + return 0; +} + +int HandleAbout() { + std::cout << "Name: mpqcli" << std::endl; + std::cout << "Version: " << MPQCLI_VERSION << "-" << GIT_COMMIT_HASH << std::endl; + std::cout << "Author: Thomas Laurenson" << std::endl; + std::cout << "License: MIT" << std::endl; + std::cout << "GitHub: https://github.com/TheGrayDot/mpqcli" << std::endl; + std::cout << "Dependencies:" << std::endl; + std::cout << " - StormLib (https://github.com/ladislav-zezula/StormLib)" << std::endl; + std::cout << " - CLI11 (https://github.com/CLIUtils/CLI11)" << std::endl; + return 0; +} + +int HandleInfo(const std::string &target, const std::optional &property) { + HANDLE hArchive; + if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + std::cerr << "[!] Failed to open MPQ archive." << std::endl; + return 1; + } + PrintMpqInfo(hArchive, property); + CloseMpqArchive(hArchive); + return 0; +} + +int HandleCreate(const std::string &target, const std::optional &nameInArchive, + const std::optional &output, bool signArchive, + const std::optional &locale, + const std::optional &gameProfile, int32_t mpqVersion, + int64_t streamFlags, int64_t sectorSize, int64_t rawChunkSize, int64_t fileFlags1, + int64_t fileFlags2, int64_t fileFlags3, int64_t attrFlags, int64_t fileDwFlags, + int64_t fileDwCompression, int64_t fileDwCompressionNext) { + if (!fs::is_regular_file(target) && nameInArchive.has_value()) { + std::cerr << "[!] Cannot specify --name-in-archive when adding a directory." << std::endl; + return 1; + } + + fs::path outputFilePath; + if (output.has_value()) { + outputFilePath = fs::absolute(output.value()); + } else { + outputFilePath = fs::path(target); + // If the path ends with a separator (e.g. "dir/"), strip the + // trailing separator first so we get "dir.mpq" + if (outputFilePath.filename().empty()) { + outputFilePath = outputFilePath.parent_path(); + } + outputFilePath.replace_extension(".mpq"); + } + std::string outputFile = outputFilePath.u8string(); + + GameProfile profile; + if (gameProfile.has_value()) { + profile = GameRules::StringToProfile(gameProfile.value()); + } else { + profile = GameRules::GetDefaultProfile(); + } + GameRules gameRules(profile); + + std::cout << "[*] Game profile: " << gameProfile.value_or("default") + << ", Output file: " << outputFile << std::endl; + + if (mpqVersion > 0) { + mpqVersion--; // We label versions 1-4, but StormLib uses 0-3 + } + + // Apply MpqCreateSettings overrides if provided + MpqCreateSettingsOverrides overrides; + if (mpqVersion >= 0) overrides.mpqVersion = static_cast(mpqVersion); + if (streamFlags >= 0) overrides.streamFlags = static_cast(streamFlags); + if (fileFlags1 >= 0) overrides.fileFlags1 = static_cast(fileFlags1); + if (fileFlags2 >= 0) overrides.fileFlags2 = static_cast(fileFlags2); + if (fileFlags3 >= 0) overrides.fileFlags3 = static_cast(fileFlags3); + if (attrFlags >= 0) overrides.attrFlags = static_cast(attrFlags); + if (sectorSize >= 0) overrides.sectorSize = static_cast(sectorSize); + if (rawChunkSize >= 0) overrides.rawChunkSize = static_cast(rawChunkSize); + gameRules.OverrideCreateSettings(overrides); + + // Determine the number of files we are going to add + uint32_t fileCount = CalculateMpqMaxFileValue(target); + + // Create the MPQ archive and add files + HANDLE hArchive = CreateMpqArchive(outputFile, fileCount, gameRules); + if (hArchive) { + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + + // Apply AddFileSettings overrides if provided + CompressionSettingsOverrides addOverrides; + if (fileDwFlags >= 0) addOverrides.dwFlags = static_cast(fileDwFlags); + if (fileDwCompression >= 0) + addOverrides.dwCompression = static_cast(fileDwCompression); + if (fileDwCompressionNext >= 0) + addOverrides.dwCompressionNext = static_cast(fileDwCompressionNext); + + if (fs::is_regular_file(target)) { + // Default: use the filename as path, saves file to root of MPQ + fs::path filePath = fs::path(target); + std::string archivePath = filePath.filename().u8string(); + if (nameInArchive.has_value()) { // Optional: specified filename inside archive + filePath = fs::path(nameInArchive.value()); + archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ + } + AddFile(hArchive, target, archivePath, lcid, gameRules, addOverrides); + } else { + AddFiles(hArchive, target, lcid, gameRules, addOverrides); + } + if (signArchive) { + SignMpqArchive(hArchive); + } + CloseMpqArchive(hArchive); + } else { + std::cerr << "[!] Failed to create MPQ archive." << std::endl; + return 1; + } + + return 0; +} + +int HandleAdd(const std::string &file, const std::string &target, + const std::optional &path, + const std::optional &dirInArchive, + const std::optional &nameInArchive, bool overwrite, + const std::optional &locale, + const std::optional &gameProfile, int64_t fileDwFlags, + int64_t fileDwCompression, int64_t fileDwCompressionNext) { + HANDLE hArchive; + // Open the MPQ archive for writing (this is why we set flag as 0) + if (!OpenMpqArchive(target, &hArchive, 0)) { + std::cerr << "[!] Failed to open MPQ archive." << std::endl; + return 1; + } + + // Path to file on disk + fs::path filePath = fs::path(file); + + std::string archivePath = + filePath.filename() + .u8string(); // Default: use the filename as path, saves file to root of MPQ + if (path.has_value() && (dirInArchive.has_value() || nameInArchive.has_value())) { + // Return error since providing --path together with --name-in-archive or + // --directory-in-archive makes no sense and is a user error + std::cerr << "[!] Cannot specify --path together with --name-in-archive or " + "--directory-in-archive." + << std::endl; + CloseMpqArchive(hArchive); + return 1; + + } else if (path.has_value()) { // Optional: specified whole path inside archive + filePath = fs::path(path.value()); + archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ + + } else if (dirInArchive.has_value() || + nameInArchive.has_value()) { // Optional: specified filename inside archive + std::string effectiveDir = dirInArchive.value_or(fs::path(file).parent_path().u8string()); + std::string effectiveName = nameInArchive.value_or(archivePath); + filePath = fs::path(effectiveDir) / fs::path(effectiveName); + archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ + } + + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + + GameProfile profile; + if (gameProfile.has_value()) { + profile = GameRules::StringToProfile(gameProfile.value()); + std::cout << "[*] Using game profile: " << gameProfile.value() << std::endl; + } else { + profile = GameRules::GetDefaultProfile(); + } + GameRules gameRules(profile); + + // Apply AddFileSettings overrides if provided + CompressionSettingsOverrides addOverrides; + if (fileDwFlags >= 0) addOverrides.dwFlags = static_cast(fileDwFlags); + if (fileDwCompression >= 0) addOverrides.dwCompression = static_cast(fileDwCompression); + if (fileDwCompressionNext >= 0) + addOverrides.dwCompressionNext = static_cast(fileDwCompressionNext); + + AddFile(hArchive, file, archivePath, lcid, gameRules, addOverrides, overwrite); + CloseMpqArchive(hArchive); + return 0; +} + +int HandleRemove(const std::string &file, const std::string &target, + const std::optional &locale) { + HANDLE hArchive; + // Open the MPQ archive for writing (this is why we set flag as 0) + if (!OpenMpqArchive(target, &hArchive, 0)) { + std::cerr << "[!] Failed to open MPQ archive." << std::endl; + return 1; + } + + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + int result = RemoveFile(hArchive, file, lcid); + CloseMpqArchive(hArchive); + return result; +} + +int HandleList(const std::string &target, const std::optional &listfileName, + bool listAll, bool listDetailed, const std::vector &properties) { + HANDLE hArchive; + if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + std::cerr << "[!] Failed to open MPQ archive." << std::endl; + return 1; + } + ListFiles(hArchive, listfileName, listAll, listDetailed, properties); + CloseMpqArchive(hArchive); + return 0; +} + +int HandleExtract(const std::string &target, const std::optional &output, + const std::optional &file, bool keepFolderStructure, + const std::optional &listfileName, + const std::optional &locale) { + // If no output directory specified, use MPQ path without extension + // If output directory specified, create it if it doesn't exist + std::string effectiveOutput; + if (!output.has_value()) { + fs::path outputPathAbsolute = fs::canonical(target); + fs::path outputPath = outputPathAbsolute.parent_path() / outputPathAbsolute.stem(); + effectiveOutput = outputPath.u8string(); + } else { + effectiveOutput = output.value(); + } + if (!fs::create_directory(effectiveOutput) && !fs::is_directory(effectiveOutput)) { + std::cerr << "[!] Failed to create output directory: " << effectiveOutput << std::endl; + return 1; + } + + HANDLE hArchive; + if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + std::cerr << "[!] Failed to open MPQ archive." << std::endl; + return 1; + } + + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + if (locale.has_value() && lcid == defaultLocale) { + std::cout << "[!] Warning: The locale '" << locale.value() + << "' is unknown. Will use default locale instead." << std::endl; + } + + int result; + if (file.has_value()) { + result = ExtractFile(hArchive, effectiveOutput, file.value(), keepFolderStructure, lcid); + } else { + result = ExtractFiles(hArchive, effectiveOutput, listfileName, lcid); + } + CloseMpqArchive(hArchive); + + if (result != 0) { + std::cerr << std::endl << "[!] Failed to extract all files." << std::endl; + } + return result; +} + +int HandleRead(const std::string &file, const std::string &target, + const std::optional &locale) { + HANDLE hArchive; + if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + std::cerr << "[!] Failed to open MPQ archive." << std::endl; + return 1; + } + + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + if (locale.has_value() && lcid == defaultLocale) { + std::cout << "[!] Warning: The locale '" << locale.value() + << "' is unknown. Will use default locale instead." << std::endl; + } + + uint32_t fileSize; + auto fileContent = ReadFile(hArchive, file.c_str(), &fileSize, lcid); + if (!fileContent) { + return 1; + } + + PrintAsBinary(fileContent.get(), fileSize); + + CloseMpqArchive(hArchive); + return 0; +} + +int HandleVerify(const std::string &target, bool printSignature) { + HANDLE hArchive; + if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + std::cerr << "[!] Failed to open MPQ archive." << std::endl; + return 1; + } + + int result = 0; + uint32_t verifyResult = VerifyMpqArchive(hArchive); + if (verifyResult == ERROR_WEAK_SIGNATURE_OK || verifyResult == ERROR_STRONG_SIGNATURE_OK || + verifyResult == ERROR_WEAK_SIGNATURE_ERROR || + verifyResult == ERROR_STRONG_SIGNATURE_ERROR) { + if (printSignature) { + // If printing the signature, don't print success message + // because the user might want to pipe/redirect the signature data + PrintMpqSignature(hArchive, target); + } else { + // Just print verification success + std::cout << "[*] Verify success" << std::endl; + } + result = 0; + } else { + // Any other verify result is no signature, or error verifying + std::cout << "[!] Verify failed" << std::endl; + result = 1; + } + CloseMpqArchive(hArchive); + return result; +} diff --git a/src/commands.h b/src/commands.h new file mode 100644 index 0000000..601e9d7 --- /dev/null +++ b/src/commands.h @@ -0,0 +1,38 @@ +#ifndef COMMANDS_H +#define COMMANDS_H + +#include +#include +#include +#include + +int HandleVersion(); +int HandleAbout(); +int HandleInfo(const std::string &target, const std::optional &property); +int HandleCreate(const std::string &target, const std::optional &nameInArchive, + const std::optional &output, bool signArchive, + const std::optional &locale, + const std::optional &gameProfile, int32_t mpqVersion, + int64_t streamFlags, int64_t sectorSize, int64_t rawChunkSize, int64_t fileFlags1, + int64_t fileFlags2, int64_t fileFlags3, int64_t attrFlags, int64_t fileDwFlags, + int64_t fileDwCompression, int64_t fileDwCompressionNext); +int HandleAdd(const std::string &file, const std::string &target, + const std::optional &path, + const std::optional &dirInArchive, + const std::optional &nameInArchive, bool overwrite, + const std::optional &locale, + const std::optional &gameProfile, int64_t fileDwFlags, + int64_t fileDwCompression, int64_t fileDwCompressionNext); +int HandleRemove(const std::string &file, const std::string &target, + const std::optional &locale); +int HandleList(const std::string &target, const std::optional &listfileName, + bool listAll, bool listDetailed, const std::vector &properties); +int HandleExtract(const std::string &target, const std::optional &output, + const std::optional &file, bool keepFolderStructure, + const std::optional &listfileName, + const std::optional &locale); +int HandleRead(const std::string &file, const std::string &target, + const std::optional &locale); +int HandleVerify(const std::string &target, bool printSignature); + +#endif // COMMANDS_H diff --git a/src/gamerules.cpp b/src/gamerules.cpp index 1b5c769..e8bde83 100644 --- a/src/gamerules.cpp +++ b/src/gamerules.cpp @@ -266,14 +266,19 @@ std::string GameRules::ProfileToString(GameProfile profile) { // Get list of canonical game profile names (for display purposes) std::vector GameRules::GetCanonicalProfiles() { - // Iterate through all GameProfile enum values and get their canonical names - std::vector profiles; + static const std::vector kAllProfiles = { + GameProfile::GENERIC, GameProfile::DIABLO1, GameProfile::LORDSOFMAGIC, + GameProfile::STARCRAFT1, GameProfile::WARCRAFT2, GameProfile::DIABLO2, + GameProfile::WARCRAFT3, GameProfile::WARCRAFT3_MAP, GameProfile::WOW_1X, + GameProfile::WOW_2X, GameProfile::WOW_3X, GameProfile::WOW_4X, + GameProfile::WOW_5X, GameProfile::STARCRAFT2, GameProfile::DIABLO3, + }; - for (int i = static_cast(GameProfile::GENERIC); - i <= static_cast(GameProfile::DIABLO3); ++i) { - profiles.push_back(ProfileToString(static_cast(i))); + std::vector profiles; + profiles.reserve(kAllProfiles.size()); + for (const auto &p : kAllProfiles) { + profiles.push_back(ProfileToString(p)); } - return profiles; } diff --git a/src/helpers.cpp b/src/helpers.cpp index 14c07d8..553bb83 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -47,8 +47,8 @@ std::string WindowsifyFilePath(const fs::path &path) { return filePath; } -int32_t CalculateMpqMaxFileValue(const std::string &path) { - int32_t fileCount = 0; +uint32_t CalculateMpqMaxFileValue(const std::string &path) { + uint32_t fileCount = 0; // Determine the number of files in the target directory, recusively if (!fs::is_regular_file(path)) { @@ -74,7 +74,7 @@ int32_t CalculateMpqMaxFileValue(const std::string &path) { return NextPowerOfTwo(fileCount); } -int32_t NextPowerOfTwo(int32_t n) { +uint32_t NextPowerOfTwo(uint32_t n) { n--; n |= n >> 1; n |= n >> 2; diff --git a/src/helpers.h b/src/helpers.h index b4db7a9..52ae7e3 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -9,8 +9,8 @@ namespace fs = std::filesystem; std::string FileTimeToLsTime(int64_t fileTime); std::string NormalizeFilePath(const fs::path &path); std::string WindowsifyFilePath(const fs::path &path); -int32_t CalculateMpqMaxFileValue(const std::string &path); -int32_t NextPowerOfTwo(int32_t n); +uint32_t CalculateMpqMaxFileValue(const std::string &path); +uint32_t NextPowerOfTwo(uint32_t n); void PrintAsBinary(const char *buffer, uint32_t size); #endif diff --git a/src/locales.h b/src/locales.h index 2c6d6ea..e18fd82 100644 --- a/src/locales.h +++ b/src/locales.h @@ -7,7 +7,7 @@ #include -const LCID defaultLocale = 0; +inline constexpr LCID defaultLocale = 0; std::string LocaleToLang(uint16_t locale); LCID LangToLocale(const std::string &lang); diff --git a/src/main.cpp b/src/main.cpp index 65a9c81..a293ab9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,14 +1,13 @@ -#include #include +#include +#include +#include +#include #include -#include +#include "commands.h" #include "gamerules.h" -#include "helpers.h" -#include "locales.h" -#include "mpq.h" -#include "mpqcli.h" #include "validators.h" int main(int argc, char **argv) { @@ -21,18 +20,18 @@ int main(int argc, char **argv) { // CLI: base // These are reused in multiple subcommands - std::string baseTarget = "default"; // all subcommands - std::string baseFile = "default"; // add, remove, extract, read - std::string basePath = "default"; // add, create - std::string baseLocale = "default"; // create, add, remove, extract, read - std::string baseNameInArchive = "default"; // add, create - std::string baseOutput = "default"; // create, extract - std::string baseListfileName = "default"; // list, extract - std::string baseGameProfile = "default"; // create, add + std::string baseTarget; // all subcommands + std::string baseFile; // add, remove, extract, read + std::optional basePath; // add + std::optional baseLocale; // create, add, remove, extract, read + std::optional baseNameInArchive; // add, create + std::optional baseOutput; // create, extract + std::optional baseListfileName; // list, extract + std::optional baseGameProfile; // create, add // CLI: info - std::string infoProperty = "default"; + std::optional infoProperty; // CLI: add - std::string baseDirInArchive = "default"; // add + std::optional baseDirInArchive; // add bool addOverwrite = false; // CLI: extract bool extractKeepFolderStructure = false; @@ -233,321 +232,53 @@ int main(int argc, char **argv) { return app.exit(e); } - // Handle subcommand: Version if (app.got_subcommand(version)) { - std::cout << MPQCLI_VERSION << "-" << GIT_COMMIT_HASH << std::endl; + return HandleVersion(); } - // Handle subcommand: About if (app.got_subcommand(about)) { - std::cout << "Name: mpqcli" << std::endl; - std::cout << "Version: " << MPQCLI_VERSION << "-" << GIT_COMMIT_HASH << std::endl; - std::cout << "Author: Thomas Laurenson" << std::endl; - std::cout << "License: MIT" << std::endl; - std::cout << "GitHub: https://github.com/TheGrayDot/mpqcli" << std::endl; - std::cout << "Dependencies:" << std::endl; - std::cout << " - StormLib (https://github.com/ladislav-zezula/StormLib)" << std::endl; - std::cout << " - CLI11 (https://github.com/CLIUtils/CLI11)" << std::endl; + return HandleAbout(); } - // Handle subcommand: Info if (app.got_subcommand(info)) { - HANDLE hArchive; - if (!OpenMpqArchive(baseTarget, &hArchive, MPQ_OPEN_READ_ONLY)) { - std::cerr << "[!] Failed to open MPQ archive." << std::endl; - return 1; - } - PrintMpqInfo(hArchive, infoProperty); - CloseMpqArchive(hArchive); + return HandleInfo(baseTarget, infoProperty); } - // Handle subcommand: Create if (app.got_subcommand(create)) { - if (!fs::is_regular_file(baseTarget) && baseNameInArchive != "default") { - std::cerr << "[!] Cannot specify --name-in-archive when adding a directory." - << std::endl; - return 1; - } - fs::path outputFilePath; - if (baseOutput != "default") { - outputFilePath = fs::absolute(baseOutput); - } else { - outputFilePath = fs::path(baseTarget); - // If the path ends with a separator (e.g. "dir/"), strip the - // trailing separator first so we get "dir.mpq" - if (outputFilePath.filename().empty()) { - outputFilePath = outputFilePath.parent_path(); - } - outputFilePath.replace_extension(".mpq"); - } - std::string outputFile = outputFilePath.u8string(); - - GameProfile profile; - if (baseGameProfile != "default") { - profile = GameRules::StringToProfile(baseGameProfile); - } else { - profile = GameRules::GetDefaultProfile(); - } - GameRules gameRules(profile); - - std::cout << "[*] Game profile: " << baseGameProfile << ", Output file: " << outputFile - << std::endl; - - if (createMpqVersion > 0) { - createMpqVersion--; // We label versions 1-4, but StormLib uses 0-3 - } - // Apply MpqCreateSettings overrides if provided - MpqCreateSettingsOverrides overrides; - if (createMpqVersion >= 0) { - overrides.mpqVersion = static_cast(createMpqVersion); - } - if (createStreamFlags >= 0) { - overrides.streamFlags = static_cast(createStreamFlags); - } - if (createFileFlags1 >= 0) { - overrides.fileFlags1 = static_cast(createFileFlags1); - } - if (createFileFlags2 >= 0) { - overrides.fileFlags2 = static_cast(createFileFlags2); - } - if (createFileFlags3 >= 0) { - overrides.fileFlags3 = static_cast(createFileFlags3); - } - if (createAttrFlags >= 0) { - overrides.attrFlags = static_cast(createAttrFlags); - } - if (createSectorSize >= 0) { - overrides.sectorSize = static_cast(createSectorSize); - } - if (createRawChunkSize >= 0) { - overrides.rawChunkSize = static_cast(createRawChunkSize); - } - gameRules.OverrideCreateSettings(overrides); - - // Determine the number of files we are going to add - int32_t fileCount = CalculateMpqMaxFileValue(baseTarget); - - // Create the MPQ archive and add files - HANDLE hArchive = CreateMpqArchive(outputFile, fileCount, gameRules); - if (hArchive) { - LCID locale = LangToLocale(baseLocale); - - // Apply AddFileSettings overrides if provided - CompressionSettingsOverrides addOverrides; - if (fileDwFlags >= 0) addOverrides.dwFlags = static_cast(fileDwFlags); - if (fileDwCompression >= 0) - addOverrides.dwCompression = static_cast(fileDwCompression); - if (fileDwCompressionNext >= 0) - addOverrides.dwCompressionNext = static_cast(fileDwCompressionNext); - - if (fs::is_regular_file(baseTarget)) { - // Default: use the filename as path, saves file to root of MPQ - fs::path filePath = fs::path(baseTarget); - std::string archivePath = filePath.filename().u8string(); - if (baseNameInArchive != - "default") { // Optional: specified filename inside archive - filePath = fs::path(baseNameInArchive); - archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ - } - - AddFile(hArchive, baseTarget, archivePath, locale, gameRules, addOverrides); - } else { - AddFiles(hArchive, baseTarget, locale, gameRules, addOverrides); - } - if (createSignArchive) { - SignMpqArchive(hArchive); - } - CloseMpqArchive(hArchive); - } else { - std::cerr << "[!] Failed to create MPQ archive." << std::endl; - return 1; - } + return HandleCreate(baseTarget, baseNameInArchive, baseOutput, createSignArchive, + baseLocale, baseGameProfile, createMpqVersion, createStreamFlags, + createSectorSize, createRawChunkSize, createFileFlags1, + createFileFlags2, createFileFlags3, createAttrFlags, fileDwFlags, + fileDwCompression, fileDwCompressionNext); } - // Handle subcommand: Add if (app.got_subcommand(add)) { - HANDLE hArchive; - // Open the MPQ archive for writing (this is why we set flag as 0) - if (!OpenMpqArchive(baseTarget, &hArchive, 0)) { - std::cerr << "[!] Failed to open MPQ archive." << std::endl; - return 1; - } - - // Path to file on disk - fs::path filePath = fs::path(baseFile); - - std::string archivePath = - filePath.filename() - .u8string(); // Default: use the filename as path, saves file to root of MPQ - if (basePath != "default" && baseDirInArchive != "default" || - basePath != "default" && baseNameInArchive != "default") { - // Return error since providing --path together --name-in-archive or - // --directory-in-archive makes no sense and is a user error - std::cerr << "[!] Cannot specify --path together with --name-in-archive or " - "--directory-in-archive." - << std::endl; - return 1; - - } else if (basePath != "default") { // Optional: specified whole path inside archive - filePath = fs::path(basePath); - archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ - - } else if (baseDirInArchive != "default" || - baseNameInArchive != "default") { // Optional: specified filename inside archive - if (baseDirInArchive == "default") { - baseDirInArchive = fs::path(baseFile).parent_path().u8string(); - } - if (baseNameInArchive == "default") { - baseNameInArchive = archivePath; - } - filePath = fs::path(baseDirInArchive) / fs::path(baseNameInArchive); - archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ - } - - LCID locale = LangToLocale(baseLocale); - - GameProfile profile; - if (baseGameProfile != "default") { - profile = GameRules::StringToProfile(baseGameProfile); - std::cout << "[*] Using game profile: " << baseGameProfile << std::endl; - } else { - profile = GameRules::GetDefaultProfile(); - } - GameRules gameRules(profile); - - // Apply AddFileSettings overrides if provided - CompressionSettingsOverrides addOverrides; - if (fileDwFlags >= 0) addOverrides.dwFlags = static_cast(fileDwFlags); - if (fileDwCompression >= 0) - addOverrides.dwCompression = static_cast(fileDwCompression); - if (fileDwCompressionNext >= 0) - addOverrides.dwCompressionNext = static_cast(fileDwCompressionNext); - - AddFile(hArchive, baseFile, archivePath, locale, gameRules, addOverrides, addOverwrite); - CloseMpqArchive(hArchive); + return HandleAdd(baseFile, baseTarget, basePath, baseDirInArchive, baseNameInArchive, + addOverwrite, baseLocale, baseGameProfile, fileDwFlags, fileDwCompression, + fileDwCompressionNext); } - // Handle subcommand: Remove if (app.got_subcommand(remove)) { - HANDLE hArchive; - // Open the MPQ archive for writing (this is why we set flag as 0) - if (!OpenMpqArchive(baseTarget, &hArchive, 0)) { - std::cerr << "[!] Failed to open MPQ archive." << std::endl; - return 1; - } - - LCID locale = LangToLocale(baseLocale); - int result = RemoveFile(hArchive, baseFile, locale); - CloseMpqArchive(hArchive); - return result; + return HandleRemove(baseFile, baseTarget, baseLocale); } - // Handle subcommand: List if (app.got_subcommand(list)) { - HANDLE hArchive; - if (!OpenMpqArchive(baseTarget, &hArchive, MPQ_OPEN_READ_ONLY)) { - std::cerr << "[!] Failed to open MPQ archive." << std::endl; - return 1; - } - ListFiles(hArchive, baseListfileName, listAll, listDetailed, listProperties); - CloseMpqArchive(hArchive); + return HandleList(baseTarget, baseListfileName, listAll, listDetailed, listProperties); } - // Handle subcommand: Extract if (app.got_subcommand(extract)) { - // If no output directory specified, use MPQ path without extension - // If output directory specified, create it if it doesn't exist - if (baseOutput == "default") { - fs::path outputPathAbsolute = fs::canonical(baseTarget); - fs::path outputPath = outputPathAbsolute.parent_path() / outputPathAbsolute.stem(); - std::string outputString{outputPath.u8string()}; - baseOutput = outputString; - } - fs::create_directory(baseOutput); - - HANDLE hArchive; - if (!OpenMpqArchive(baseTarget, &hArchive, MPQ_OPEN_READ_ONLY)) { - std::cerr << "[!] Failed to open MPQ archive." << std::endl; - return 1; - } - - LCID locale = LangToLocale(baseLocale); - if (baseLocale != "default" && locale == defaultLocale) { - std::cout << "[!] Warning: The locale '" << baseLocale - << "' is unknown. Will use default locale instead." << std::endl; - } - - int result; - if (baseFile != "default") { - result = - ExtractFile(hArchive, baseOutput, baseFile, extractKeepFolderStructure, locale); - } else { - result = ExtractFiles(hArchive, baseOutput, baseListfileName, locale); - } - CloseMpqArchive(hArchive); - - if (result != 0) { - std::cerr << std::endl << "[!] Failed to extract all files." << std::endl; - } - return result; + std::optional extractFile = + baseFile.empty() ? std::nullopt : std::make_optional(baseFile); + return HandleExtract(baseTarget, baseOutput, extractFile, extractKeepFolderStructure, + baseListfileName, baseLocale); } - // Handle subcommand: Read if (app.got_subcommand(read)) { - HANDLE hArchive; - if (!OpenMpqArchive(baseTarget, &hArchive, MPQ_OPEN_READ_ONLY)) { - std::cerr << "[!] Failed to open MPQ archive." << std::endl; - return 1; - } - - LCID locale = LangToLocale(baseLocale); - if (baseLocale != "default" && locale == defaultLocale) { - std::cout << "[!] Warning: The locale '" << baseLocale - << "' is unknown. Will use default locale instead." << std::endl; - } - - uint32_t fileSize; - auto fileContent = ReadFile(hArchive, baseFile.c_str(), &fileSize, locale); - if (!fileContent) { - return 1; - } - - PrintAsBinary(fileContent.get(), fileSize); - - CloseMpqArchive(hArchive); - return 0; + return HandleRead(baseFile, baseTarget, baseLocale); } - // Handle subcommand: Verify if (app.got_subcommand(verify)) { - HANDLE hArchive; - if (!OpenMpqArchive(baseTarget, &hArchive, MPQ_OPEN_READ_ONLY)) { - std::cerr << "[!] Failed to open MPQ archive." << std::endl; - return 1; - } - - int result = 0; - uint32_t verifyResult = VerifyMpqArchive(hArchive); - if (verifyResult == ERROR_WEAK_SIGNATURE_OK || verifyResult == ERROR_STRONG_SIGNATURE_OK || - verifyResult == ERROR_WEAK_SIGNATURE_ERROR || - verifyResult == ERROR_STRONG_SIGNATURE_ERROR) { - if (verifyPrintSignature) { - // If printing the signature, don't print success message - // because the user might want to pipe/redirect the signature data - PrintMpqSignature(hArchive, baseTarget); - } else { - // Just print verification success - std::cout << "[*] Verify success" << std::endl; - } - - result = 0; - } else { - // Any other verify result is no signature, or error verifying - std::cout << "[!] Verify failed" << std::endl; - result = 1; - } - CloseMpqArchive(hArchive); - return result; + return HandleVerify(baseTarget, verifyPrintSignature); } return 0; diff --git a/src/mpq.cpp b/src/mpq.cpp index 52d640a..9e6e628 100644 --- a/src/mpq.cpp +++ b/src/mpq.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -20,20 +21,20 @@ namespace fs = std::filesystem; static const std::vector kSpecialMpqFiles = {"(listfile)", "(signature)", "(attributes)"}; -int OpenMpqArchive(const std::string &filename, HANDLE *hArchive, int32_t flags) { +bool OpenMpqArchive(const std::string &filename, HANDLE *hArchive, int32_t flags) { if (!SFileOpenArchive(filename.c_str(), 0, flags, hArchive)) { std::cerr << "[!] Failed to open: " << filename << std::endl; - return 0; + return false; } - return 1; + return true; } -int CloseMpqArchive(HANDLE hArchive) { +bool CloseMpqArchive(HANDLE hArchive) { if (!SFileCloseArchive(hArchive)) { std::cerr << "[!] Failed to close MPQ archive." << std::endl; - return 0; + return false; } - return 1; + return true; } bool FileExistsInArchiveForLocale(const HANDLE hArchive, const std::string &filePath, @@ -51,19 +52,19 @@ bool FileExistsInArchiveForLocale(const HANDLE hArchive, const std::string &file return fileExists; } -int SignMpqArchive(HANDLE hArchive) { +bool SignMpqArchive(HANDLE hArchive) { if (!SFileSignArchive(hArchive, SIGNATURE_TYPE_WEAK)) { std::cerr << "[!] Failed to sign MPQ archive." << std::endl; - return 0; + return false; } - return 1; + return true; } -int ExtractFiles(HANDLE hArchive, const std::string &output, const std::string &listfileName, - LCID preferredLocale) { +int ExtractFiles(HANDLE hArchive, const std::string &output, + const std::optional &listfileName, LCID preferredLocale) { SFileSetLocale(preferredLocale); // Check if the user provided a listfile input - const char *listfile = (listfileName == "default") ? nullptr : listfileName.c_str(); + const char *listfile = listfileName.has_value() ? listfileName->c_str() : nullptr; SFILE_FIND_DATA findData; HANDLE findHandle = SFileFindFirstFile(hArchive, "*", &findData, listfile); @@ -111,12 +112,14 @@ int ExtractFile(HANDLE hArchive, const std::string &output, const std::string &f fs::path outputPathBase = outputPathAbsolute.parent_path() / outputPathAbsolute.filename(); std::filesystem::create_directories(fs::path(outputPathBase).parent_path()); - // Ensure sub-directories for folder-nested files exist + // Ensure sub-directories for folder-nested files exist before calling canonical fs::path outputFilePathName = outputPathBase / fileNameString; + std::filesystem::create_directories(outputFilePathName.parent_path()); - // Guard against path traversal attacks: resolve any ".." components and verify - // the output path is a descendant of the intended base directory - fs::path resolvedOutput = fs::weakly_canonical(outputFilePathName); + // Guard against path traversal attacks: resolve symlinks and ".." with canonical + // (requires path to exist, hence create_directories above) + fs::path resolvedOutput = + fs::canonical(outputFilePathName.parent_path()) / outputFilePathName.filename(); if (std::mismatch(outputPathBase.begin(), outputPathBase.end(), resolvedOutput.begin(), resolvedOutput.end()) .first != outputPathBase.end()) { @@ -125,8 +128,7 @@ int ExtractFile(HANDLE hArchive, const std::string &output, const std::string &f return 1; } - std::string outputFileName{outputFilePathName.u8string()}; - std::filesystem::create_directories(outputFilePathName.parent_path()); + std::string outputFileName{resolvedOutput.u8string()}; if (SFileExtractFile(hArchive, szFileName, outputFileName.c_str(), 0)) { std::cout << "[*] Extracted: " << fileNameString << std::endl; @@ -139,7 +141,7 @@ int ExtractFile(HANDLE hArchive, const std::string &output, const std::string &f return 0; } -HANDLE CreateMpqArchive(const std::string &outputArchiveName, const int32_t fileCount, +HANDLE CreateMpqArchive(const std::string &outputArchiveName, const uint32_t fileCount, const GameRules &gameRules) { // Check if file already exists if (fs::exists(outputArchiveName)) { @@ -239,7 +241,7 @@ int AddFile(HANDLE hArchive, const fs::path &localFile, const std::string &archi int32_t maxFiles = GetFileInfo(hArchive, SFileMpqMaxFileCount); if (numberOfFiles + 1 > maxFiles) { - int32_t newMaxFiles = NextPowerOfTwo(numberOfFiles + 1); + uint32_t newMaxFiles = NextPowerOfTwo(static_cast(numberOfFiles + 1)); bool setMaxFileCount = SFileSetMaxFileCount(hArchive, newMaxFiles); if (!setMaxFileCount) { int32_t error = SErrGetLastError(); @@ -328,10 +330,10 @@ std::string GetFlagString(uint32_t flags) { return result; } -int ListFiles(HANDLE hArchive, const std::string &listfileName, bool listAll, bool listDetailed, - std::vector &propertiesToPrint) { +int ListFiles(HANDLE hArchive, const std::optional &listfileName, bool listAll, + bool listDetailed, const std::vector &properties) { // Check if the user provided a listfile input - const char *listfile = (listfileName == "default") ? nullptr : listfileName.c_str(); + const char *listfile = listfileName.has_value() ? listfileName->c_str() : nullptr; SFILE_FIND_DATA findData; HANDLE findHandle = SFileFindFirstFile(hArchive, "*", &findData, listfile); @@ -341,18 +343,31 @@ int ListFiles(HANDLE hArchive, const std::string &listfileName, bool listAll, bo return -1; } - if (propertiesToPrint.empty()) { - propertiesToPrint = { - // Default properties, if the user didn't specify any - "file-size", - "locale", - "file-time", - }; - } else { + std::vector propertiesToPrint = + properties.empty() ? std::vector{"file-size", "locale", "file-time"} + : properties; + if (!properties.empty()) { listDetailed = true; // If the user specified properties, we need to print the detailed output } + // Map of property name to SFileInfoClass — defined once, outside the loop + static const std::map kPropertyInfoClass = { + {"hash-index", SFileInfoHashIndex}, + {"name-hash1", SFileInfoNameHash1}, + {"name-hash2", SFileInfoNameHash2}, + {"name-hash3", SFileInfoNameHash3}, + {"locale", SFileInfoLocale}, + {"file-index", SFileInfoFileIndex}, + {"byte-offset", SFileInfoByteOffset}, + {"file-time", SFileInfoFileTime}, + {"file-size", SFileInfoFileSize}, + {"compressed-size", SFileInfoCompressedSize}, + {"flags", SFileInfoFlags}, + {"encryption-key", SFileInfoEncryptionKey}, + {"encryption-key-raw", SFileInfoEncryptionKeyRaw}, + }; + std::set seenFileNames; // Used to prevent printing the same file name multiple times // Loop through all files in the MPQ archive @@ -411,87 +426,39 @@ int ListFiles(HANDLE hArchive, const std::string &listfileName, bool listAll, bo continue; // Skip to the next file } - std::vector>> propertyActions = { - {"hash-index", - [&]() { - std::cout << std::setw(5) - << GetFileInfo(hFile, SFileInfoHashIndex) << " "; - }}, - {"name-hash1", - [&]() { - std::cout << std::setfill('0') << std::hex << std::setw(8) - << GetFileInfo(hFile, SFileInfoNameHash1) - << std::setfill(' ') << std::dec << " "; - }}, - {"name-hash2", - [&]() { - std::cout << std::setfill('0') << std::hex << std::setw(8) - << GetFileInfo(hFile, SFileInfoNameHash2) - << std::setfill(' ') << std::dec << " "; - }}, - {"name-hash3", - [&]() { - std::cout << std::setfill('0') << std::hex << std::setw(16) - << GetFileInfo(hFile, SFileInfoNameHash3) - << std::setfill(' ') << std::dec << " "; - }}, - {"locale", - [&]() { - int32_t fileLocale = GetFileInfo(hFile, SFileInfoLocale); - std::string fileLocaleStr = LocaleToLang(fileLocale); - std::cout << std::setw(4) << fileLocaleStr << " "; - }}, - {"file-index", - [&]() { - std::cout << std::setw(5) - << GetFileInfo(hFile, SFileInfoFileIndex) << " "; - }}, - {"byte-offset", - [&]() { - std::cout << std::hex << std::setw(8) - << GetFileInfo(hFile, SFileInfoByteOffset) << std::dec - << " "; - }}, - {"file-time", - [&]() { - int64_t fileTime = GetFileInfo(hFile, SFileInfoFileTime); - std::string fileTimeStr = FileTimeToLsTime(fileTime); - std::cout << std::setw(19) << fileTimeStr << " "; - }}, - {"file-size", - [&]() { - std::cout << std::setw(8) << GetFileInfo(hFile, SFileInfoFileSize) - << " "; - }}, - {"compressed-size", - [&]() { - std::cout << std::setw(8) - << GetFileInfo(hFile, SFileInfoCompressedSize) << " "; - }}, - {"flags", - [&]() { - int32_t flags = GetFileInfo(hFile, SFileInfoFlags); - std::cout << std::setw(8) << GetFlagString(flags) << " "; - }}, - {"encryption-key", - [&]() { - std::cout << std::setfill('0') << std::hex << std::setw(8) - << GetFileInfo(hFile, SFileInfoEncryptionKey) - << std::setfill(' ') << std::dec << " "; - }}, - {"encryption-key-raw", - [&]() { - std::cout << std::setfill('0') << std::hex << std::setw(8) - << GetFileInfo(hFile, SFileInfoEncryptionKeyRaw) - << std::setfill(' ') << std::dec << " "; - }}, - }; - for (const auto &prop : propertiesToPrint) { - for (const auto &[key, action] : propertyActions) { - if (prop == key) { - action(); // Print property - } + auto it = kPropertyInfoClass.find(prop); + if (it == kPropertyInfoClass.end()) continue; + + if (prop == "hash-index" || prop == "file-index") { + std::cout << std::setw(5) << GetFileInfo(hFile, it->second) << " "; + } else if (prop == "name-hash1" || prop == "name-hash2") { + std::cout << std::setfill('0') << std::hex << std::setw(8) + << GetFileInfo(hFile, it->second) << std::setfill(' ') + << std::dec << " "; + } else if (prop == "name-hash3") { + std::cout << std::setfill('0') << std::hex << std::setw(16) + << GetFileInfo(hFile, it->second) << std::setfill(' ') + << std::dec << " "; + } else if (prop == "locale") { + std::cout << std::setw(4) + << LocaleToLang(GetFileInfo(hFile, it->second)) << " "; + } else if (prop == "byte-offset") { + std::cout << std::hex << std::setw(8) + << GetFileInfo(hFile, it->second) << std::dec << " "; + } else if (prop == "file-time") { + std::cout << std::setw(19) + << FileTimeToLsTime(GetFileInfo(hFile, it->second)) + << " "; + } else if (prop == "file-size" || prop == "compressed-size") { + std::cout << std::setw(8) << GetFileInfo(hFile, it->second) << " "; + } else if (prop == "flags") { + std::cout << std::setw(8) + << GetFlagString(GetFileInfo(hFile, it->second)) << " "; + } else if (prop == "encryption-key" || prop == "encryption-key-raw") { + std::cout << std::setfill('0') << std::hex << std::setw(8) + << GetFileInfo(hFile, it->second) << std::setfill(' ') + << std::dec << " "; } } @@ -546,7 +513,7 @@ std::unique_ptr ReadFile(HANDLE hArchive, const char *szFileName, unsign return fileContent; } -void PrintMpqInfo(HANDLE hArchive, const std::string &infoProperty) { +void PrintMpqInfo(HANDLE hArchive, const std::optional &infoProperty) { // Map of property names to their corresponding actions std::map> propertyActions = { {"format-version", @@ -613,31 +580,20 @@ void PrintMpqInfo(HANDLE hArchive, const std::string &infoProperty) { } }}}; - // If infoProperty is "default", print all properties with their names (key) - // Otherwise, print only the property value - if (infoProperty == "default") { + // If infoProperty is not set, print all properties with their names (key) + // Otherwise, print only the specified property value + if (!infoProperty.has_value()) { for (const auto &[key, action] : propertyActions) { action(true); // Print property name and value } } else { - auto it = propertyActions.find(infoProperty); + auto it = propertyActions.find(infoProperty.value()); if (it != propertyActions.end()) { it->second(false); // Print only the value } } } -template -T GetFileInfo(HANDLE hFile, SFileInfoClass infoClass) { - T value{}; - if (!SFileGetFileInfo(hFile, infoClass, &value, sizeof(T), nullptr)) { - int32_t error = SErrGetLastError(); - // std::cerr << "[!] GetFileInfo failed (Error: " << error << ")" << std::endl; - return T{}; // Return default value for the type - } - return value; -} - uint32_t VerifyMpqArchive(HANDLE hArchive) { return SFileVerifyArchive(hArchive); } @@ -674,9 +630,14 @@ int32_t PrintMpqSignature(HANDLE hArchive, const std::string &target) { std::uintmax_t fileSize = fs::file_size(archivePath); int64_t signatureLength = fileSize - archiveOffset - archiveSize; + if (signatureLength <= 0) { + std::cerr << "[!] Invalid signature length: " << signatureLength << std::endl; + return -1; + } + std::ifstream file_mpq(archivePath, std::ios::binary); file_mpq.seekg(archiveOffset + archiveSize, std::ios::beg); - signatureContent.resize(signatureLength); + signatureContent.resize(static_cast(signatureLength)); file_mpq.read(signatureContent.data(), signatureContent.size()); file_mpq.close(); diff --git a/src/mpq.h b/src/mpq.h index 3d8e311..00296b3 100644 --- a/src/mpq.h +++ b/src/mpq.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -11,14 +12,14 @@ namespace fs = std::filesystem; -int OpenMpqArchive(const std::string &filename, HANDLE *hArchive, int32_t flags); -int CloseMpqArchive(HANDLE hArchive); -int SignMpqArchive(HANDLE hArchive); -int ExtractFiles(HANDLE hArchive, const std::string &output, const std::string &listfileName, - LCID preferredLocale); +bool OpenMpqArchive(const std::string &filename, HANDLE *hArchive, int32_t flags); +bool CloseMpqArchive(HANDLE hArchive); +bool SignMpqArchive(HANDLE hArchive); +int ExtractFiles(HANDLE hArchive, const std::string &output, + const std::optional &listfileName, LCID preferredLocale); int ExtractFile(HANDLE hArchive, const std::string &output, const std::string &fileName, bool keepFolderStructure, LCID preferredLocale); -HANDLE CreateMpqArchive(const std::string &outputArchiveName, int32_t fileCount, +HANDLE CreateMpqArchive(const std::string &outputArchiveName, uint32_t fileCount, const GameRules &gameRules); int AddFiles(HANDLE hArchive, const std::string &inputPath, LCID locale, const GameRules &gameRules, const CompressionSettingsOverrides &overrides = CompressionSettingsOverrides()); @@ -27,15 +28,21 @@ int AddFile(HANDLE hArchive, const fs::path &localFile, const std::string &archi const CompressionSettingsOverrides &overrides = CompressionSettingsOverrides(), bool overwrite = false); int RemoveFile(HANDLE hArchive, const std::string &archiveFilePath, LCID locale); -int ListFiles(HANDLE hArchive, const std::string &listfileName, bool listAll, bool listDetailed, - std::vector &propertiesToPrint); +int ListFiles(HANDLE hArchive, const std::optional &listfileName, bool listAll, + bool listDetailed, const std::vector &properties); std::unique_ptr ReadFile(HANDLE hArchive, const char *szFileName, unsigned int *fileSize, LCID preferredLocale); -void PrintMpqInfo(HANDLE hArchive, const std::string &infoProperty); +void PrintMpqInfo(HANDLE hArchive, const std::optional &infoProperty); uint32_t VerifyMpqArchive(HANDLE hArchive); int32_t PrintMpqSignature(HANDLE hArchive, const std::string &target); template -T GetFileInfo(HANDLE hFile, SFileInfoClass infoClass); +T GetFileInfo(HANDLE hFile, SFileInfoClass infoClass) { + T value{}; + if (!SFileGetFileInfo(hFile, infoClass, &value, sizeof(T), nullptr)) { + return T{}; + } + return value; +} #endif