diff --git a/.clang-format b/.clang-format index 2d2c904..b978417 100644 --- a/.clang-format +++ b/.clang-format @@ -1,9 +1,8 @@ -BasedOnStyle: Google -PointerAlignment: Right -DerivePointerAlignment: false -ColumnLimit: 100 +BasedOnStyle: LLVM IndentWidth: 4 +ColumnLimit: 100 AccessModifierOffset: -4 +AllowShortFunctionsOnASingleLine: InlineOnly IncludeBlocks: Regroup IncludeIsMainRegex: '([-_]test)?$' IncludeCategories: @@ -13,8 +12,3 @@ IncludeCategories: Priority: 1 - Regex: '^"' Priority: 3 -ReflowComments: true -BreakBeforeBraces: Attach -Cpp11BracedListStyle: true -AllowShortFunctionsOnASingleLine: InlineOnly -SortIncludes: true diff --git a/.clang-tidy b/.clang-tidy index 4ac2c0c..94e5730 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,3 +15,16 @@ Checks: > readability-container-size-empty WarningsAsErrors: "*" FormatStyle: file +CheckOptions: + - { key: readability-identifier-naming.FunctionCase, value: CamelCase } + - { key: readability-identifier-naming.MethodCase, value: CamelCase } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.EnumCase, value: CamelCase } + - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.VariableCase, value: lower_case } + - { key: readability-identifier-naming.ParameterCase, value: lower_case } + - { key: readability-identifier-naming.MemberCase, value: lower_case } + - { key: readability-identifier-naming.PrivateMemberCase, value: lower_case } + - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } + - { key: readability-identifier-naming.ConstantCase, value: lower_case } diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 678b3ce..cdc8167 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,7 +14,7 @@ on: - "Dockerfile.musl" permissions: - contents: write + contents: read jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b5c65b..403c985 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,10 +10,16 @@ permissions: jobs: release_binaries: runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} steps: - name: Checkout code uses: actions/checkout@v6 + - name: Resolve release version + id: version + run: echo "version=$(make get_project_version)" >> "$GITHUB_OUTPUT" + - name: Download Linux GLIBC binary uses: actions/download-artifact@v8 with: @@ -39,13 +45,14 @@ jobs: with: name: mpqcli-windows-amd64.exe - - name: Extract latest changelog section + - name: Extract changelog for this tag id: changelog run: | - CHANGELOG_CONTENT=$(awk '/^## /{i++} i==1{print}' CHANGELOG.md | tail -n +2) - echo "content<> $GITHUB_OUTPUT - echo "$CHANGELOG_CONTENT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + { + echo "content<> "$GITHUB_OUTPUT" - name: Release package run: | @@ -74,17 +81,13 @@ jobs: run: | docker load -i mpqcli-docker.tar - - name: Extract version without "v" - id: version - run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV - - name: Tag Docker image with version run: | - docker tag mpqcli ghcr.io/thegraydot/mpqcli:${{ env.VERSION }} + docker tag mpqcli ghcr.io/thegraydot/mpqcli:${{ needs.release_binaries.outputs.version }} docker tag mpqcli ghcr.io/thegraydot/mpqcli:latest - name: Push Docker image to GitHub Container Registry run: | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - docker push ghcr.io/thegraydot/mpqcli:${{ env.VERSION }} + docker push ghcr.io/thegraydot/mpqcli:${{ needs.release_binaries.outputs.version }} docker push ghcr.io/thegraydot/mpqcli:latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c48608..ea49e8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: workflow_call permissions: - contents: write + contents: read jobs: test_linux: diff --git a/CHANGELOG.md b/CHANGELOG.md index 627c9e1..c8beba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.10.1 - 2026-07-03 + +### Added + +- Command completion for common shells (bash, zsh, fish and powershell) + +### Fixed + +- Memory leak in read subcommand +- Inconsistencies in documentation + +### Updated + +- Code style to conform to Google CPP style with slight variations +- StormLib dependency for a variety of fixes + +### Thanks + +- Thanks to @sjoblomj for the contributions in this release + ## 0.10.0 - 2026-06-07 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index c140c8a..be7dcdf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.10) -project(MPQCLI VERSION 0.10.0) +project(MPQCLI VERSION 0.10.1) # Options option(BUILD_MPQCLI "Build the mpqcli CLI app" ON) @@ -25,11 +25,17 @@ if(BUILD_STATIC) endif() # Determine git commit hash -execute_process ( +execute_process( COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE GIT_COMMIT_HASH + RESULT_VARIABLE GIT_COMMIT_RESULT + ERROR_QUIET ) +if(NOT GIT_COMMIT_RESULT EQUAL 0 OR NOT GIT_COMMIT_HASH) + set(GIT_COMMIT_HASH "unknown") +endif() # Handle StormLib dependency if (NOT EXISTS "${CMAKE_SOURCE_DIR}/extern/StormLib/CMakeLists.txt") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 666714a..e8c5810 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,7 @@ Run `make help` to list all available targets. Common ones: | Target | Description | |----------------------------|--------------------------------------------------------------------| | `make install_clang_tools` | Install clang-format and clang-tidy via apt | +| `make configure` | Configure cmake build with clang (required before `make lint`) | | `make build_linux` | Build for Linux using cmake | | `make build_windows` | Build for Windows using cmake | | `make build_clean` | Remove the cmake build directory | @@ -72,9 +73,10 @@ If your change adds or modifies user-facing functionality - such as a new subcom ### 4. Linting must pass -All C++ code is formatted with clang-format and analysed with clang-tidy. Run the full suite before submitting: +All C++ code is formatted with clang-format and analysed with clang-tidy. `clang-tidy` needs a compile database generated with clang, so run `make configure` first (`make build_linux`/`make build_windows` alone will not work, since they don't set up the compiler flags clang-tidy needs): ``` +make configure make lint ``` @@ -88,7 +90,7 @@ Then re-run `make lint` to confirm everything passes. ### 5. Match the existing code style -C++ formatting is enforced by `.clang-format` (Google style base). Static analysis is enforced by `.clang-tidy`. Both configs live in the repo root. Python tests should follow the style of the existing test files. +C++ formatting is enforced by `.clang-format` (LLVM style base). Static analysis is enforced by `.clang-tidy`. Both configs live in the repo root. Python tests should follow the style of the existing test files. #### Suppression policy @@ -106,7 +108,8 @@ Suppressions are occasionally necessary for third-party code or intentional patt Use `// clang-format off` / `// clang-format on` only when the default formatting genuinely hurts readability (e.g. column-aligned tables). Add a brief comment explaining the intent: ```cpp -// clang-format off: preserve column-aligned flag-to-char mappings for readability +// Preserve column-aligned flag-to-char mappings for readability +// clang-format off if (flags & MPQ_FILE_IMPLODE) result += 'i'; if (flags & MPQ_FILE_COMPRESS) result += 'c'; // clang-format on @@ -131,6 +134,6 @@ If you add a new StormLib call that is locale-sensitive, follow the existing pat 2. Run `git submodule update --init --recursive` after cloning 3. Run `make install_clang_tools` to install lint dependencies 4. Make your changes and verify they build: `make build_linux` -5. Run `make lint` and fix any issues +5. Run `make configure` and then `make lint`, fixing any issues 6. Run `make test_mpqcli` and confirm all tests pass 7. Open a pull request with a clear description of what was changed and why diff --git a/README.md b/README.md index c691339..3ce33e4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Release Version](https://img.shields.io/github/v/release/thegraydot/mpqcli?style=flat) -![Release downloads](https://img.shields.io/github/downloads/thegraydot/mpqcli/total?label=release_downloads) ![Package downloads](https://img.shields.io/badge/package_downloads-894-green) +![Release downloads](https://img.shields.io/github/downloads/thegraydot/mpqcli/total?label=release_downloads) ![Package downloads](https://img.shields.io/badge/package_downloads-996-green) A command-line tool to create, add, remove, list, extract, read, and verify MPQ archives using the [StormLib library](https://github.com/ladislav-zezula/StormLib). diff --git a/docs/commands/completion.md b/docs/commands/completion.md index afe5a63..4b2639a 100644 --- a/docs/commands/completion.md +++ b/docs/commands/completion.md @@ -21,7 +21,7 @@ $ source ~/.bash_completion.d/mpqcli Alternatively, write the script to a system-wide completions directory (requires root): ```bash -$ mpqcli completion bash > /etc/bash_completion.d/mpqcli +$ mpqcli completion bash | sudo tee /etc/bash_completion.d/mpqcli > /dev/null ``` ## Zsh @@ -32,14 +32,6 @@ Write the completion script to a directory that is on your `$fpath`. $ mpqcli completion zsh > "${fpath[1]}/_mpqcli" ``` -## PowerShell - -Append the completion script to your PowerShell profile so it loads automatically. - -```powershell -PS> mpqcli completion powershell >> $PROFILE -``` - ## Fish Write the completion script to the fish completions directory. @@ -47,3 +39,11 @@ Write the completion script to the fish completions directory. ```fish $ mpqcli completion fish > ~/.config/fish/completions/mpqcli.fish ``` + +## PowerShell + +Append the completion script to your PowerShell profile so it loads automatically. + +```powershell +PS> mpqcli completion powershell >> $PROFILE +``` diff --git a/docs/contributing.md b/docs/contributing.md index 2e520c6..e8c5810 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -21,26 +21,27 @@ git submodule update --init --recursive Install the clang lint tools: ``` -make setup +make install_clang_tools ``` ## Makefile Reference Run `make help` to list all available targets. Common ones: -| Target | Description | -|---|---| -| `make setup` | Install clang-format and clang-tidy via apt | -| `make build_linux` | Build for Linux using cmake | -| `make build_windows` | Build for Windows using cmake | -| `make build_clean` | Remove the cmake build directory | -| `make test_create_venv` | Create Python venv and install test dependencies (first-time only) | -| `make test_mpqcli` | Run the pytest test suite | -| `make lint` | Run all C++ linters (clang-format + clang-tidy) | -| `make lint_format` | Check formatting only (dry run) | -| `make lint_format_fix` | Auto-fix formatting in-place | -| `make lint_cpp` | Run clang-tidy static analysis | -| `make clean` | Remove all build and test artifacts | +| Target | Description | +|----------------------------|--------------------------------------------------------------------| +| `make install_clang_tools` | Install clang-format and clang-tidy via apt | +| `make configure` | Configure cmake build with clang (required before `make lint`) | +| `make build_linux` | Build for Linux using cmake | +| `make build_windows` | Build for Windows using cmake | +| `make build_clean` | Remove the cmake build directory | +| `make test_create_venv` | Create Python venv and install test dependencies (first-time only) | +| `make test_mpqcli` | Run the pytest test suite | +| `make lint` | Run all C++ linters (clang-format + clang-tidy) | +| `make fmt_check` | Check formatting only (dry run) | +| `make fmt` | Auto-fix formatting in-place | +| `make lint_cpp` | Run clang-tidy static analysis | +| `make clean` | Remove all build and test artifacts | ## Requirements for a Pull Request @@ -72,23 +73,24 @@ If your change adds or modifies user-facing functionality - such as a new subcom ### 4. Linting must pass -All C++ code is formatted with clang-format and analysed with clang-tidy. Run the full suite before submitting: +All C++ code is formatted with clang-format and analysed with clang-tidy. `clang-tidy` needs a compile database generated with clang, so run `make configure` first (`make build_linux`/`make build_windows` alone will not work, since they don't set up the compiler flags clang-tidy needs): ``` +make configure make lint ``` If there are formatting violations, auto-fix them with: ``` -make lint_format_fix +make fmt ``` Then re-run `make lint` to confirm everything passes. ### 5. Match the existing code style -C++ formatting is enforced by `.clang-format` (Google style base). Static analysis is enforced by `.clang-tidy`. Both configs live in the repo root. Python tests should follow the style of the existing test files. +C++ formatting is enforced by `.clang-format` (LLVM style base). Static analysis is enforced by `.clang-tidy`. Both configs live in the repo root. Python tests should follow the style of the existing test files. #### Suppression policy @@ -106,18 +108,32 @@ Suppressions are occasionally necessary for third-party code or intentional patt Use `// clang-format off` / `// clang-format on` only when the default formatting genuinely hurts readability (e.g. column-aligned tables). Add a brief comment explaining the intent: ```cpp -// clang-format off: preserve column-aligned flag-to-char mappings for readability +// Preserve column-aligned flag-to-char mappings for readability +// clang-format off if (flags & MPQ_FILE_IMPLODE) result += 'i'; 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 2. Run `git submodule update --init --recursive` after cloning 3. Run `make install_clang_tools` to install lint dependencies 4. Make your changes and verify they build: `make build_linux` -5. Run `make lint` and fix any issues +5. Run `make configure` and then `make lint`, fixing any issues 6. Run `make test_mpqcli` and confirm all tests pass 7. Open a pull request with a clear description of what was changed and why diff --git a/extern/CLI11 b/extern/CLI11 index 74d72dd..0bc9bde 160000 --- a/extern/CLI11 +++ b/extern/CLI11 @@ -1 +1 @@ -Subproject commit 74d72dd38f7ccb122c9439c79d6c2c294d10f3bc +Subproject commit 0bc9bde61e2068766192e28cf4eb09ef4edcf540 diff --git a/extern/StormLib b/extern/StormLib index 570734e..06d13e5 160000 --- a/extern/StormLib +++ b/extern/StormLib @@ -1 +1 @@ -Subproject commit 570734e0ac4b1389361f1be3d1ec98f7235beea2 +Subproject commit 06d13e5c1b4d6ddf2a57b80c4331f78740d2f462 diff --git a/src/commands.cpp b/src/commands.cpp index 330f8b9..804b08a 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -15,13 +15,13 @@ namespace fs = std::filesystem; std::string ResolveArchiveName(const std::string &f, const std::optional &path, - const bool treatAsDirectory = false) { - fs::path filePath = path.value_or(fs::path(f).filename().u8string()); - if (treatAsDirectory) { + const bool treat_as_directory = false) { + fs::path file_path = path.value_or(fs::path(f).filename().u8string()); + if (treat_as_directory) { const std::string filename = fs::path(f).filename().u8string(); - filePath = path.value_or("") / fs::path(filename); + file_path = path.value_or("") / fs::path(filename); } - return WindowsifyFilePath(filePath); + return WindowsifyFilePath(file_path); } int HandleVersion() { @@ -42,97 +42,106 @@ int HandleAbout() { } int HandleInfo(const std::string &target, const std::optional &property) { - HANDLE hArchive; - if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, MPQ_OPEN_READ_ONLY)) { return 1; } - PrintMpqInfo(hArchive, property); - CloseMpqArchive(hArchive); + PrintMpqInfo(archive, property); + CloseMpqArchive(archive); return 0; } int HandleCreate(const std::string &target, const std::optional &path, - const std::optional &output, bool signArchive, + const std::optional &output, bool sign_archive, 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) { - fs::path outputFilePath; + const std::optional &game_profile, int32_t mpq_version, + int64_t stream_flags, int64_t sector_size, int64_t raw_chunk_size, + int64_t file_flags1, int64_t file_flags2, int64_t file_flags3, int64_t attr_flags, + int64_t file_flags, int64_t file_compression, int64_t file_compression_next) { + fs::path output_file_path; if (output.has_value()) { - outputFilePath = fs::absolute(output.value()); + output_file_path = fs::absolute(output.value()); } else { - outputFilePath = fs::path(target); + output_file_path = 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(); + if (output_file_path.filename().empty()) { + output_file_path = output_file_path.parent_path(); } - outputFilePath.replace_extension(".mpq"); + output_file_path.replace_extension(".mpq"); } - std::string outputFile = outputFilePath.u8string(); + std::string output_file = output_file_path.u8string(); GameProfile profile; - if (gameProfile.has_value()) { - profile = GameRules::StringToProfile(gameProfile.value()); + if (game_profile.has_value()) { + profile = GameRules::StringToProfile(game_profile.value()); } else { profile = GameRules::GetDefaultProfile(); } - GameRules gameRules(profile); + GameRules game_rules(profile); - std::cout << "[*] Game profile: " << gameProfile.value_or("default") - << ", Output file: " << outputFile << std::endl; + std::cout << "[*] Game profile: " << game_profile.value_or("default") + << ", Output file: " << output_file << std::endl; - if (mpqVersion > 0) { - mpqVersion--; // We label versions 1-4, but StormLib uses 0-3 + if (mpq_version > 0) { + mpq_version--; // 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); + if (mpq_version >= 0) + overrides.mpq_version = static_cast(mpq_version); + if (stream_flags >= 0) + overrides.stream_flags = static_cast(stream_flags); + if (file_flags1 >= 0) + overrides.file_flags1 = static_cast(file_flags1); + if (file_flags2 >= 0) + overrides.file_flags2 = static_cast(file_flags2); + if (file_flags3 >= 0) + overrides.file_flags3 = static_cast(file_flags3); + if (attr_flags >= 0) + overrides.attr_flags = static_cast(attr_flags); + if (sector_size >= 0) + overrides.sector_size = static_cast(sector_size); + if (raw_chunk_size >= 0) + overrides.raw_chunk_size = static_cast(raw_chunk_size); + game_rules.OverrideCreateSettings(overrides); // Determine the number of files we are going to add - uint32_t fileCount = CalculateMpqMaxFileValue(target); + uint32_t file_count = CalculateMpqMaxFileValue(target); // Create the MPQ archive and add files int result = 0; - HANDLE hArchive = CreateMpqArchive(outputFile, fileCount, gameRules); - if (hArchive) { - LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + HANDLE archive = CreateMpqArchive(output_file, file_count, game_rules); + if (archive) { + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : default_locale; // 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); + CompressionSettingsOverrides add_overrides; + if (file_flags >= 0) + add_overrides.flags = static_cast(file_flags); + if (file_compression >= 0) + add_overrides.compression = static_cast(file_compression); + if (file_compression_next >= 0) + add_overrides.compression_next = static_cast(file_compression_next); if (fs::is_directory(target)) { const std::string prefix = path.value_or(""); - result |= AddFiles(hArchive, target, prefix, lcid, gameRules, addOverrides); + result |= AddFiles(archive, target, prefix, lcid, game_rules, add_overrides); } else if (fs::is_regular_file(target)) { - std::string archivePath = ResolveArchiveName(target, path); - result |= AddFile(hArchive, target, archivePath, lcid, gameRules, addOverrides); + std::string archive_path = ResolveArchiveName(target, path); + result |= AddFile(archive, target, archive_path, lcid, game_rules, add_overrides); } else { std::cerr << "[!] Not a file or directory: " << target << std::endl; result |= 1; } - if (signArchive) { - SignMpqArchive(hArchive); + if (sign_archive) { + SignMpqArchive(archive); } - CloseMpqArchive(hArchive); + CloseMpqArchive(archive); } else { std::cerr << "[!] Failed to create MPQ archive." << std::endl; return 1; @@ -144,39 +153,41 @@ int HandleCreate(const std::string &target, const std::optional &pa int HandleAdd(const std::vector &files, const std::string &target, const std::optional &path, bool overwrite, bool update, const std::optional &locale, - const std::optional &gameProfile, int64_t fileDwFlags, - int64_t fileDwCompression, int64_t fileDwCompressionNext) { - HANDLE hArchive; - if (!OpenMpqArchive(target, &hArchive, 0)) { + const std::optional &game_profile, int64_t file_flags, + int64_t file_compression, int64_t file_compression_next) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, 0)) { return 1; } - LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : default_locale; GameProfile profile; - if (gameProfile.has_value()) { - profile = GameRules::StringToProfile(gameProfile.value()); - std::cout << "[*] Using game profile: " << gameProfile.value() << std::endl; + if (game_profile.has_value()) { + profile = GameRules::StringToProfile(game_profile.value()); + std::cout << "[*] Using game profile: " << game_profile.value() << std::endl; } else { profile = GameRules::GetDefaultProfile(); } - GameRules gameRules(profile); + GameRules game_rules(profile); - 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); + CompressionSettingsOverrides add_overrides; + if (file_flags >= 0) + add_overrides.flags = static_cast(file_flags); + if (file_compression >= 0) + add_overrides.compression = static_cast(file_compression); + if (file_compression_next >= 0) + add_overrides.compression_next = static_cast(file_compression_next); - bool hasDirectory = false; + bool has_directory = false; for (const auto &f : files) { if (fs::is_directory(f)) { - hasDirectory = true; + has_directory = true; break; } } - if (update && !hasDirectory) { + if (update && !has_directory) { std::cerr << "[!] Warning: --update is only meaningful when adding a directory" << std::endl; } @@ -191,93 +202,93 @@ int HandleAdd(const std::vector &files, const std::string &target, if (fs::is_directory(f)) { std::string prefix = path.value_or(""); result |= - AddFiles(hArchive, f, prefix, lcid, gameRules, addOverrides, overwrite, update); + AddFiles(archive, f, prefix, lcid, game_rules, add_overrides, overwrite, update); } else if (fs::is_regular_file(f)) { - const bool treatAsDirectory = hasDirectory || files.size() > 1; - std::string archivePath = ResolveArchiveName(f, path, treatAsDirectory); - result |= AddFile(hArchive, f, archivePath, lcid, gameRules, addOverrides, overwrite); + const bool treat_as_directory = has_directory || files.size() > 1; + std::string archive_path = ResolveArchiveName(f, path, treat_as_directory); + result |= AddFile(archive, f, archive_path, lcid, game_rules, add_overrides, overwrite); } else { std::cerr << "[!] Not a file or directory: " << f << std::endl; } } - CloseMpqArchive(hArchive); + CloseMpqArchive(archive); return result; } int HandleRemove(const std::vector &files, const std::string &target, const std::optional &locale) { - HANDLE hArchive; - if (!OpenMpqArchive(target, &hArchive, 0)) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, 0)) { return 1; } - LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : default_locale; std::unordered_set seen; - int overallResult = 0; + int overall_result = 0; for (const auto &f : files) { if (!seen.insert(f).second) { continue; } - int result = RemoveFile(hArchive, f, lcid); + int result = RemoveFile(archive, f, lcid); if (result != 0) { - overallResult = result; + overall_result = result; } } - CloseMpqArchive(hArchive); - return overallResult; + CloseMpqArchive(archive); + return overall_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)) { +int HandleList(const std::string &target, const std::optional &listfile_name, + bool list_all, bool list_detailed, const std::vector &properties) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, MPQ_OPEN_READ_ONLY)) { return 1; } - ListFiles(hArchive, listfileName, listAll, listDetailed, properties); - CloseMpqArchive(hArchive); + ListFiles(archive, listfile_name, list_all, list_detailed, properties); + CloseMpqArchive(archive); 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 &file, bool keep_folder_structure, + const std::optional &listfile_name, 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; + std::string effective_output; if (!output.has_value()) { - fs::path outputPathAbsolute = fs::canonical(target); - fs::path outputPath = outputPathAbsolute.parent_path() / outputPathAbsolute.stem(); - effectiveOutput = outputPath.u8string(); + fs::path output_path_absolute = fs::canonical(target); + fs::path output_path = output_path_absolute.parent_path() / output_path_absolute.stem(); + effective_output = output_path.u8string(); } else { - effectiveOutput = output.value(); + effective_output = output.value(); } - if (!fs::create_directory(effectiveOutput) && !fs::is_directory(effectiveOutput)) { - std::cerr << "[!] Failed to create output directory: " << effectiveOutput << std::endl; + if (!fs::create_directory(effective_output) && !fs::is_directory(effective_output)) { + std::cerr << "[!] Failed to create output directory: " << effective_output << std::endl; return 1; } - HANDLE hArchive; - if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, MPQ_OPEN_READ_ONLY)) { return 1; } - LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; - if (locale.has_value() && lcid == defaultLocale) { + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : default_locale; + if (locale.has_value() && lcid == default_locale) { 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); + result = ExtractFile(archive, effective_output, file.value(), keep_folder_structure, lcid); } else { - result = ExtractFiles(hArchive, effectiveOutput, listfileName, lcid); + result = ExtractFiles(archive, effective_output, listfile_name, lcid); } - CloseMpqArchive(hArchive); + CloseMpqArchive(archive); if (result != 0) { std::cerr << std::endl << "[!] Failed to extract all files." << std::endl; @@ -287,45 +298,45 @@ int HandleExtract(const std::string &target, const std::optional &o int HandleRead(const std::string &file, const std::string &target, const std::optional &locale) { - HANDLE hArchive; - if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, MPQ_OPEN_READ_ONLY)) { return 1; } - LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale; - if (locale.has_value() && lcid == defaultLocale) { + LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : default_locale; + if (locale.has_value() && lcid == default_locale) { 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) { - CloseMpqArchive(hArchive); + uint32_t file_size; + auto file_content = ReadFile(archive, file.c_str(), &file_size, lcid); + if (!file_content) { + CloseMpqArchive(archive); return 1; } - PrintAsBinary(fileContent.get(), fileSize); + PrintAsBinary(file_content.get(), file_size); - CloseMpqArchive(hArchive); + CloseMpqArchive(archive); return 0; } -int HandleVerify(const std::string &target, bool printSignature) { - HANDLE hArchive; - if (!OpenMpqArchive(target, &hArchive, MPQ_OPEN_READ_ONLY)) { +int HandleVerify(const std::string &target, bool print_signature) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, MPQ_OPEN_READ_ONLY)) { 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) { + uint32_t verify_result = VerifyMpqArchive(archive); + if (verify_result == ERROR_WEAK_SIGNATURE_OK || verify_result == ERROR_STRONG_SIGNATURE_OK || + verify_result == ERROR_WEAK_SIGNATURE_ERROR || + verify_result == ERROR_STRONG_SIGNATURE_ERROR) { + if (print_signature) { // If printing the signature, don't print success message // because the user might want to pipe/redirect the signature data - PrintMpqSignature(hArchive, target); + PrintMpqSignature(archive, target); } else { // Just print verification success std::cout << "[*] Verify success" << std::endl; @@ -336,17 +347,17 @@ int HandleVerify(const std::string &target, bool printSignature) { std::cout << "[!] Verify failed" << std::endl; result = 1; } - CloseMpqArchive(hArchive); + CloseMpqArchive(archive); return result; } -int HandleCompact(const std::string &target, const std::optional &listfileName) { - HANDLE hArchive; - if (!OpenMpqArchive(target, &hArchive, 0)) { +int HandleCompact(const std::string &target, const std::optional &listfile_name) { + HANDLE archive; + if (!OpenMpqArchive(target, &archive, 0)) { return 1; } - const int result = CompactMpqArchive(hArchive, listfileName); - CloseMpqArchive(hArchive); + const int result = CompactMpqArchive(archive, listfile_name); + CloseMpqArchive(archive); return result; } diff --git a/src/commands.h b/src/commands.h index 277b513..3b23f44 100644 --- a/src/commands.h +++ b/src/commands.h @@ -10,28 +10,28 @@ int HandleVersion(); int HandleAbout(); int HandleInfo(const std::string &target, const std::optional &property); int HandleCreate(const std::string &target, const std::optional &path, - const std::optional &output, bool signArchive, + const std::optional &output, bool sign_archive, 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); + const std::optional &game_profile, int32_t mpq_version, + int64_t stream_flags, int64_t sector_size, int64_t raw_chunk_size, + int64_t file_flags1, int64_t file_flags2, int64_t file_flags3, int64_t attr_flags, + int64_t file_flags, int64_t file_compression, int64_t file_compression_next); int HandleAdd(const std::vector &files, const std::string &target, const std::optional &path, bool overwrite, bool update, const std::optional &locale, - const std::optional &gameProfile, int64_t fileDwFlags, - int64_t fileDwCompression, int64_t fileDwCompressionNext); + const std::optional &game_profile, int64_t file_flags, + int64_t file_compression, int64_t file_compression_next); int HandleRemove(const std::vector &files, 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 HandleList(const std::string &target, const std::optional &listfile_name, + bool list_all, bool list_detailed, 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 &file, bool keep_folder_structure, + const std::optional &listfile_name, 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); -int HandleCompact(const std::string &target, const std::optional &listfileName); +int HandleVerify(const std::string &target, bool print_signature); +int HandleCompact(const std::string &target, const std::optional &listfile_name); -#endif // COMMANDS_H +#endif // COMMANDS_H diff --git a/src/completion.h b/src/completion.h index 990aa79..926a176 100644 --- a/src/completion.h +++ b/src/completion.h @@ -6,4 +6,4 @@ void HandleCompletionZsh(); void HandleCompletionPs(); void HandleCompletionFish(); -#endif // COMPLETION_H +#endif // COMPLETION_H diff --git a/src/gamerules.cpp b/src/gamerules.cpp index e8bde83..f7b49e2 100644 --- a/src/gamerules.cpp +++ b/src/gamerules.cpp @@ -7,7 +7,7 @@ #include // Constructor -GameRules::GameRules(GameProfile gameProfile) : profile(gameProfile) { +GameRules::GameRules(GameProfile game_profile) : profile_(game_profile) { InitializeRules(); } @@ -22,47 +22,47 @@ static std::string ToLower(const std::string &str) { // Helper function to match wildcards (* and ?) bool GameRules::MatchFileMask(const std::string &filename, const std::string &mask) { // Convert both to lowercase for case-insensitive matching - std::string lowerFilename = ToLower(filename); - std::string lowerMask = ToLower(mask); + std::string lower_filename = ToLower(filename); + std::string lower_mask = ToLower(mask); // Replace backslashes with forward slashes for consistent path handling - std::replace(lowerFilename.begin(), lowerFilename.end(), '\\', '/'); - std::replace(lowerMask.begin(), lowerMask.end(), '\\', '/'); + std::replace(lower_filename.begin(), lower_filename.end(), '\\', '/'); + std::replace(lower_mask.begin(), lower_mask.end(), '\\', '/'); // Simple wildcard matching - size_t maskPos = 0; - size_t filePos = 0; - size_t starPos = std::string::npos; - size_t matchPos = 0; - - while (filePos < lowerFilename.length()) { - if (maskPos < lowerMask.length() && - (lowerMask[maskPos] == '?' || lowerMask[maskPos] == lowerFilename[filePos])) { - maskPos++; - filePos++; - } else if (maskPos < lowerMask.length() && lowerMask[maskPos] == '*') { - starPos = maskPos; - matchPos = filePos; - maskPos++; - } else if (starPos != std::string::npos) { - maskPos = starPos + 1; - matchPos++; - filePos = matchPos; + size_t mask_pos = 0; + size_t file_pos = 0; + size_t star_pos = std::string::npos; + size_t match_pos = 0; + + while (file_pos < lower_filename.length()) { + if (mask_pos < lower_mask.length() && + (lower_mask[mask_pos] == '?' || lower_mask[mask_pos] == lower_filename[file_pos])) { + mask_pos++; + file_pos++; + } else if (mask_pos < lower_mask.length() && lower_mask[mask_pos] == '*') { + star_pos = mask_pos; + match_pos = file_pos; + mask_pos++; + } else if (star_pos != std::string::npos) { + mask_pos = star_pos + 1; + match_pos++; + file_pos = match_pos; } else { return false; } } - while (maskPos < lowerMask.length() && lowerMask[maskPos] == '*') { - maskPos++; + while (mask_pos < lower_mask.length() && lower_mask[mask_pos] == '*') { + mask_pos++; } - return maskPos == lowerMask.length(); + return mask_pos == lower_mask.length(); } -void GameRules::AddRuleByFileMask(const std::string &fileMask, DWORD mpqFlags, - DWORD compressionFirst, DWORD compressionNext) { - rules.emplace_back(fileMask, mpqFlags, compressionFirst, compressionNext); +void GameRules::AddRuleByFileMask(const std::string &file_mask, DWORD mpq_flags, + DWORD compression_first, DWORD compression_next) { + rules_.emplace_back(file_mask, mpq_flags, compression_first, compression_next); } // Use UINT32_MAX for sizeMax to indicate "no upper limit" @@ -70,41 +70,41 @@ void GameRules::AddRuleByFileMask(const std::string &fileMask, DWORD mpqFlags, // AddRuleByFileSize(0, 0, ...) - Match files with exactly 0 bytes // AddRuleByFileSize(0, 0x4000, ...) - Match files from 0 to 16KB // AddRuleByFileSize(0x4000, UINT32_MAX, ...) - Match files from 16KB onwards -void GameRules::AddRuleByFileSize(DWORD sizeMin, DWORD sizeMax, DWORD mpqFlags, - DWORD compressionFirst, DWORD compressionNext) { - rules.emplace_back(sizeMin, sizeMax, mpqFlags, compressionFirst, compressionNext); +void GameRules::AddRuleByFileSize(DWORD size_min, DWORD size_max, DWORD mpq_flags, + DWORD compression_first, DWORD compression_next) { + rules_.emplace_back(size_min, size_max, mpq_flags, compression_first, compression_next); } -void GameRules::AddRuleDefault(DWORD mpqFlags, DWORD compressionFirst, DWORD compressionNext) { - rules.emplace_back(mpqFlags, compressionFirst, compressionNext); +void GameRules::AddRuleDefault(DWORD mpq_flags, DWORD compression_first, DWORD compression_next) { + rules_.emplace_back(mpq_flags, compression_first, compression_next); } // Get compression settings for a specific file CompressionSettings GameRules::GetCompressionSettings(const std::string &filename, - const DWORD fileSize) const { + const DWORD file_size) const { // Iterate through rules in order (first match wins) - for (const auto &rule : rules) { + for (const auto &rule : rules_) { switch (rule.type) { - case RuleType::FILE_MASK: - if (MatchFileMask(filename, rule.fileMask)) { - return {rule.mpqFlags, rule.compressionFirst, rule.compressionNext}; - } - break; - - case RuleType::FILE_SIZE: { - // Use UINT32_MAX to indicate "no upper limit" - bool hasUpperLimit = (rule.sizeMax != UINT32_MAX); - bool inRange = - fileSize >= rule.sizeMin && (!hasUpperLimit || fileSize <= rule.sizeMax); - - if (inRange) { - return {rule.mpqFlags, rule.compressionFirst, rule.compressionNext}; - } - break; + case RuleType::FILE_MASK: + if (MatchFileMask(filename, rule.file_mask)) { + return {rule.mpq_flags, rule.compression_first, rule.compression_next}; } + break; + + case RuleType::FILE_SIZE: { + // Use UINT32_MAX to indicate "no upper limit" + bool has_upper_limit = (rule.size_max != UINT32_MAX); + bool in_range = + file_size >= rule.size_min && (!has_upper_limit || file_size <= rule.size_max); + + if (in_range) { + return {rule.mpq_flags, rule.compression_first, rule.compression_next}; + } + break; + } - case RuleType::DEFAULT: - return {rule.mpqFlags, rule.compressionFirst, rule.compressionNext}; + case RuleType::DEFAULT: + return {rule.mpq_flags, rule.compression_first, rule.compression_next}; } } @@ -116,43 +116,43 @@ CompressionSettings GameRules::GetCompressionSettings(const std::string &filenam // Override MPQ creation settings with user-provided values void GameRules::OverrideCreateSettings(const MpqCreateSettingsOverrides &overrides) { // Track whether user explicitly set fileFlags2 (needed for automatic adjustment logic) - bool userSetFileFlags2 = false; + bool user_set_file_flags2 = false; // Step 1: Apply user overrides // User-provided values always take priority, even if they might be incorrect. // We only apply override if the optional has a value (i.e., user specified it) - if (overrides.mpqVersion.has_value()) { - createSettings.mpqVersion = overrides.mpqVersion.value(); + if (overrides.mpq_version.has_value()) { + create_settings_.mpq_version = overrides.mpq_version.value(); } - if (overrides.streamFlags.has_value()) { - createSettings.streamFlags = overrides.streamFlags.value(); + if (overrides.stream_flags.has_value()) { + create_settings_.stream_flags = overrides.stream_flags.value(); } - if (overrides.sectorSize.has_value()) { - createSettings.sectorSize = overrides.sectorSize.value(); + if (overrides.sector_size.has_value()) { + create_settings_.sector_size = overrides.sector_size.value(); } - if (overrides.rawChunkSize.has_value()) { - createSettings.rawChunkSize = overrides.rawChunkSize.value(); + if (overrides.raw_chunk_size.has_value()) { + create_settings_.raw_chunk_size = overrides.raw_chunk_size.value(); } - if (overrides.fileFlags1.has_value()) { - createSettings.fileFlags1 = overrides.fileFlags1.value(); + if (overrides.file_flags1.has_value()) { + create_settings_.file_flags1 = overrides.file_flags1.value(); } - if (overrides.fileFlags2.has_value()) { - createSettings.fileFlags2 = overrides.fileFlags2.value(); - userSetFileFlags2 = true; // User explicitly set this value + if (overrides.file_flags2.has_value()) { + create_settings_.file_flags2 = overrides.file_flags2.value(); + user_set_file_flags2 = true; // User explicitly set this value } - if (overrides.fileFlags3.has_value()) { - createSettings.fileFlags3 = overrides.fileFlags3.value(); + if (overrides.file_flags3.has_value()) { + create_settings_.file_flags3 = overrides.file_flags3.value(); } - if (overrides.attrFlags.has_value()) { - createSettings.attrFlags = overrides.attrFlags.value(); + if (overrides.attr_flags.has_value()) { + create_settings_.attr_flags = overrides.attr_flags.value(); } // Step 2: Apply automatic adjustments based on dependencies @@ -164,10 +164,11 @@ void GameRules::OverrideCreateSettings(const MpqCreateSettingsOverrides &overrid // - If attrFlags is set but fileFlags2 is still 0 (not overridden by user or profile), // we should set fileFlags2 to MPQ_FILE_DEFAULT_INTERNAL to enable the attributes file - if (!userSetFileFlags2 && createSettings.fileFlags2 == 0 && createSettings.attrFlags != 0) { + if (!user_set_file_flags2 && create_settings_.file_flags2 == 0 && + create_settings_.attr_flags != 0) { // User wants attributes (attrFlags is set) but hasn't specified how to store // the (attributes) file itself. Use the default internal file flags. - createSettings.fileFlags2 = MPQ_FILE_DEFAULT_INTERNAL; + create_settings_.file_flags2 = MPQ_FILE_DEFAULT_INTERNAL; } // Note: If user explicitly sets fileFlags2 to 0 via override, we respect that choice @@ -176,7 +177,7 @@ void GameRules::OverrideCreateSettings(const MpqCreateSettingsOverrides &overrid // Get the profile name map (single source of truth for all valid profile names) static const std::map &GetProfileMap() { - static const std::map profileMap = { + static const std::map profile_map = { {"generic", GameProfile::GENERIC}, {"diablo1", GameProfile::DIABLO1}, {"diablo", GameProfile::DIABLO1}, @@ -212,15 +213,15 @@ static const std::map &GetProfileMap() { {"sc2", GameProfile::STARCRAFT2}, {"diablo3", GameProfile::DIABLO3}, {"d3", GameProfile::DIABLO3}}; - return profileMap; + return profile_map; } // Convert string to GameProfile enum -GameProfile GameRules::StringToProfile(const std::string &profileName) { - const auto &profileMap = GetProfileMap(); - std::string lower = ToLower(profileName); - auto it = profileMap.find(lower); - if (it != profileMap.end()) { +GameProfile GameRules::StringToProfile(const std::string &profile_name) { + const auto &profile_map = GetProfileMap(); + std::string lower = ToLower(profile_name); + auto it = profile_map.find(lower); + if (it != profile_map.end()) { return it->second; } return GameProfile::GENERIC; @@ -229,44 +230,44 @@ GameProfile GameRules::StringToProfile(const std::string &profileName) { // Convert GameProfile enum to string std::string GameRules::ProfileToString(GameProfile profile) { switch (profile) { - case GameProfile::GENERIC: - return "generic"; - case GameProfile::DIABLO1: - return "diablo1"; - case GameProfile::LORDSOFMAGIC: - return "lordsofmagic"; - case GameProfile::WARCRAFT2: - return "warcraft2"; - case GameProfile::STARCRAFT1: - return "starcraft1"; - case GameProfile::DIABLO2: - return "diablo2"; - case GameProfile::WARCRAFT3: - return "warcraft3"; - case GameProfile::WARCRAFT3_MAP: - return "warcraft3-map"; - case GameProfile::WOW_1X: - return "wow-vanilla"; - case GameProfile::WOW_2X: - return "wow-tbc"; - case GameProfile::WOW_3X: - return "wow-wotlk"; - case GameProfile::WOW_4X: - return "wow-cataclysm"; - case GameProfile::WOW_5X: - return "wow-mop"; - case GameProfile::STARCRAFT2: - return "starcraft2"; - case GameProfile::DIABLO3: - return "diablo3"; - default: - return "generic"; + case GameProfile::GENERIC: + return "generic"; + case GameProfile::DIABLO1: + return "diablo1"; + case GameProfile::LORDSOFMAGIC: + return "lordsofmagic"; + case GameProfile::WARCRAFT2: + return "warcraft2"; + case GameProfile::STARCRAFT1: + return "starcraft1"; + case GameProfile::DIABLO2: + return "diablo2"; + case GameProfile::WARCRAFT3: + return "warcraft3"; + case GameProfile::WARCRAFT3_MAP: + return "warcraft3-map"; + case GameProfile::WOW_1X: + return "wow-vanilla"; + case GameProfile::WOW_2X: + return "wow-tbc"; + case GameProfile::WOW_3X: + return "wow-wotlk"; + case GameProfile::WOW_4X: + return "wow-cataclysm"; + case GameProfile::WOW_5X: + return "wow-mop"; + case GameProfile::STARCRAFT2: + return "starcraft2"; + case GameProfile::DIABLO3: + return "diablo3"; + default: + return "generic"; } } // Get list of canonical game profile names (for display purposes) std::vector GameRules::GetCanonicalProfiles() { - static const std::vector kAllProfiles = { + static const std::vector all_profiles = { GameProfile::GENERIC, GameProfile::DIABLO1, GameProfile::LORDSOFMAGIC, GameProfile::STARCRAFT1, GameProfile::WARCRAFT2, GameProfile::DIABLO2, GameProfile::WARCRAFT3, GameProfile::WARCRAFT3_MAP, GameProfile::WOW_1X, @@ -275,8 +276,8 @@ std::vector GameRules::GetCanonicalProfiles() { }; std::vector profiles; - profiles.reserve(kAllProfiles.size()); - for (const auto &p : kAllProfiles) { + profiles.reserve(all_profiles.size()); + for (const auto &p : all_profiles) { profiles.push_back(ProfileToString(p)); } return profiles; @@ -298,20 +299,21 @@ std::string GameRules::GetAvailableProfiles() { } // Validator for CLI11 - accepts all profile names but only displays canonical ones -extern const CLI::Validator GameProfileValid = CLI::Validator( +extern const CLI::Validator game_profile_valid = CLI::Validator( [](const std::string &str) { - if (str == "default") return std::string(); + if (str == "default") + return std::string(); // Try to convert the string to a profile GameProfile profile = GameRules::StringToProfile(str); // If it's GENERIC and the input wasn't "generic", it means the profile wasn't found if (profile == GameProfile::GENERIC && str != "generic") { - std::string validProfiles = "Game profile must be one of:"; + std::string valid_profiles = "Game profile must be one of:"; for (const auto &p : GameRules::GetCanonicalProfiles()) { - validProfiles += " " + p; + valid_profiles += " " + p; } - return validProfiles; + return valid_profiles; } return std::string(); }, @@ -319,234 +321,234 @@ extern const CLI::Validator GameProfileValid = CLI::Validator( // Initialize rules for the selected game profile void GameRules::InitializeRules() { - rules.clear(); - - switch (profile) { - case GameProfile::DIABLO1: - case GameProfile::LORDSOFMAGIC: - // File rules when adding files to archive: - AddRuleByFileMask("*.wav", MPQ_FILE_ENCRYPTED, 0x00, 0x00); - AddRuleByFileMask("*.smk", 0x00000000, 0x00, 0x00); - AddRuleByFileMask("*.bik", 0x00000000, 0x00, 0x00); - AddRuleByFileMask("*.mpq", MPQ_FILE_ENCRYPTED, 0x00, 0x00); - AddRuleByFileMask("game", MPQ_FILE_IMPLODE, 0x00, 0x00); - AddRuleByFileMask("hero", MPQ_FILE_IMPLODE, 0x00, 0x00); - AddRuleDefault(MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED, MPQ_COMPRESSION_PKWARE); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_1; - createSettings.sectorSize = 0x1000; - break; - - case GameProfile::WARCRAFT2: - case GameProfile::STARCRAFT1: - // File rules when adding files to archive: - AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_PKWARE, - MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_STEREO); - AddRuleByFileMask("*.smk", 0x00000000, 0x00, 0x00); - AddRuleByFileMask("*.bik", 0x00000000, 0x00, 0x00); - AddRuleByFileMask("*.mpq", 0x00000000, 0x00, 0x00); - AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_PKWARE); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_1; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC; - createSettings.sectorSize = 0x1000; - break; - - case GameProfile::DIABLO2: - // File rules when adding files to archive: - AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_PKWARE, - MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_STEREO); - AddRuleByFileMask("*.d2", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.txt", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.dc6", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.tbl", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.map", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.key", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.dat", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.ds1", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.dcc", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.cof", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.dt1", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.pl2", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.dn1", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleByFileMask("*.ico", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); - AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_PKWARE); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_1; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.sectorSize = 0x1000; - break; - - case GameProfile::WARCRAFT3: - // File rules when adding files to archive: - AddRuleByFileMask("Abilities\\*.wav", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, - MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_MONO); - AddRuleByFileMask("Buildings\\*.wav", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, - MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_MONO); - AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_ZLIB, - MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_MONO); - - AddRuleByFileMask("ReplaceableTextures\\WorldEditUI\\*.blp", MPQ_FILE_COMPRESS, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("ReplaceableTextures\\Selection\\*.blp", MPQ_FILE_COMPRESS, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("ReplaceableTextures\\Shadows\\*.blp", MPQ_FILE_COMPRESS, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("UI\\Glues\\Loading\\Backgrounds\\*.blp", 0, 0); - AddRuleByFileMask("UI\\Glues\\Loading\\Multiplayer\\*.blp", 0, 0); - AddRuleByFileMask("UI\\*.blp", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.blp", 0, 0); - - AddRuleByFileMask("Maps\\Campaign\\*.w3m", 0, 0); - AddRuleByFileMask("*.w3m", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_PKWARE); - - AddRuleByFileMask("*.toc", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.ifl", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.mdx", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.tga", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.slk", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.ai", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.j", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.txt", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.fdf", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.pld", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.mid", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.dls", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.mpq", 0, 0); - AddRuleByFileMask("*.mp3", 0, 0); - - AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, - MPQ_COMPRESSION_PKWARE); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_1; - createSettings.sectorSize = 0x1000; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.attrFlags = MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32; - break; - - case GameProfile::WARCRAFT3_MAP: // Warcraft III Map files - // File rules when adding files to archive: - AddRuleDefault(MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_1; - createSettings.sectorSize = 0x1000; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.attrFlags = MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32; - break; - - case GameProfile::WOW_1X: - // File rules when adding files to archive: - AddRuleByFileMask("*.mp3", 0, 0); - AddRuleDefault(MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_1; - createSettings.sectorSize = 0x1000; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.attrFlags = - MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; - break; - - case GameProfile::WOW_2X: - case GameProfile::WOW_3X: - // File rules when adding files to archive: - AddRuleByFileMask("*.mp3", 0, 0); - AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC, MPQ_COMPRESSION_ZLIB); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_2; - createSettings.sectorSize = 0x1000; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.attrFlags = - MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; - break; - - case GameProfile::WOW_4X: - case GameProfile::WOW_5X: - // File rules when adding files to archive: - AddRuleByFileSize(0, 0, MPQ_FILE_DELETE_MARKER, 0); - AddRuleByFileMask("*.mp3", 0, 0); - AddRuleByFileMask("*.ogg", 0, 0); - AddRuleByFileMask("*.ogv", 0, 0); - AddRuleByFileSize(0, 0x4000, MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, - MPQ_COMPRESSION_ZLIB); - AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC, MPQ_COMPRESSION_ZLIB); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_4; - createSettings.rawChunkSize = 0x4000; - createSettings.sectorSize = 0x4000; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.attrFlags = MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; - break; - - case GameProfile::STARCRAFT2: - // File rules when adding files to archive: - AddRuleByFileSize(0, 0, MPQ_FILE_DELETE_MARKER, 0); - AddRuleByFileMask("*.mp3", 0, 0); - AddRuleByFileMask("*.ogg", 0, 0); - AddRuleByFileMask("*.ogv", 0, 0); - AddRuleByFileSize(0, 0x4000, MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, - MPQ_COMPRESSION_ZLIB); - AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC, MPQ_COMPRESSION_ZLIB); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_2; - createSettings.sectorSize = 0x4000; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.attrFlags = MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; - break; - - case GameProfile::DIABLO3: - // File rules when adding files to archive: - AddRuleByFileSize(0, 0, MPQ_FILE_DELETE_MARKER, 0); - AddRuleByFileMask("*.mp3", 0, 0); - AddRuleByFileMask("*.ogg", 0, 0); - AddRuleByFileMask("*.ogv", 0, 0); - AddRuleByFileSize(0, 0x4000, MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, - MPQ_COMPRESSION_ZLIB); - AddRuleDefault(MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); - - // Settings for archive creation: - createSettings.mpqVersion = MPQ_FORMAT_VERSION_4; - createSettings.rawChunkSize = 0x4000; - createSettings.sectorSize = 0x4000; - createSettings.fileFlags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.fileFlags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; - createSettings.attrFlags = MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; - break; - - case GameProfile::GENERIC: - default: - // File rules when adding files to archive: - AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED, MPQ_COMPRESSION_PKWARE); - - // For settings for archive creation, use defaults from MpqCreateSettings constructor - break; + rules_.clear(); + + switch (profile_) { + case GameProfile::DIABLO1: + case GameProfile::LORDSOFMAGIC: + // File rules when adding files to archive: + AddRuleByFileMask("*.wav", MPQ_FILE_ENCRYPTED, 0x00, 0x00); + AddRuleByFileMask("*.smk", 0x00000000, 0x00, 0x00); + AddRuleByFileMask("*.bik", 0x00000000, 0x00, 0x00); + AddRuleByFileMask("*.mpq", MPQ_FILE_ENCRYPTED, 0x00, 0x00); + AddRuleByFileMask("game", MPQ_FILE_IMPLODE, 0x00, 0x00); + AddRuleByFileMask("hero", MPQ_FILE_IMPLODE, 0x00, 0x00); + AddRuleDefault(MPQ_FILE_IMPLODE | MPQ_FILE_ENCRYPTED, MPQ_COMPRESSION_PKWARE); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_1; + create_settings_.sector_size = 0x1000; + break; + + case GameProfile::WARCRAFT2: + case GameProfile::STARCRAFT1: + // File rules when adding files to archive: + AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_PKWARE, + MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_STEREO); + AddRuleByFileMask("*.smk", 0x00000000, 0x00, 0x00); + AddRuleByFileMask("*.bik", 0x00000000, 0x00, 0x00); + AddRuleByFileMask("*.mpq", 0x00000000, 0x00, 0x00); + AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_PKWARE); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_1; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC; + create_settings_.sector_size = 0x1000; + break; + + case GameProfile::DIABLO2: + // File rules when adding files to archive: + AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_PKWARE, + MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_STEREO); + AddRuleByFileMask("*.d2", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.txt", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.dc6", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.tbl", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.map", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.key", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.dat", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.ds1", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.dcc", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.cof", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.dt1", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.pl2", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.dn1", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleByFileMask("*.ico", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_PKWARE); + AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_PKWARE); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_1; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.sector_size = 0x1000; + break; + + case GameProfile::WARCRAFT3: + // File rules when adding files to archive: + AddRuleByFileMask("Abilities\\*.wav", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, + MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_MONO); + AddRuleByFileMask("Buildings\\*.wav", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB, + MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_MONO); + AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_ZLIB, + MPQ_COMPRESSION_HUFFMANN | MPQ_COMPRESSION_ADPCM_MONO); + + AddRuleByFileMask("ReplaceableTextures\\WorldEditUI\\*.blp", MPQ_FILE_COMPRESS, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("ReplaceableTextures\\Selection\\*.blp", MPQ_FILE_COMPRESS, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("ReplaceableTextures\\Shadows\\*.blp", MPQ_FILE_COMPRESS, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("UI\\Glues\\Loading\\Backgrounds\\*.blp", 0, 0); + AddRuleByFileMask("UI\\Glues\\Loading\\Multiplayer\\*.blp", 0, 0); + AddRuleByFileMask("UI\\*.blp", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.blp", 0, 0); + + AddRuleByFileMask("Maps\\Campaign\\*.w3m", 0, 0); + AddRuleByFileMask("*.w3m", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_PKWARE); + + AddRuleByFileMask("*.toc", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.ifl", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.mdx", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.tga", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.slk", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.ai", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.j", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.txt", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.fdf", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.pld", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.mid", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.dls", MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.mpq", 0, 0); + AddRuleByFileMask("*.mp3", 0, 0); + + AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED | MPQ_FILE_KEY_V2, + MPQ_COMPRESSION_PKWARE); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_1; + create_settings_.sector_size = 0x1000; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.attr_flags = MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32; + break; + + case GameProfile::WARCRAFT3_MAP: // Warcraft III Map files + // File rules when adding files to archive: + AddRuleDefault(MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_1; + create_settings_.sector_size = 0x1000; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.attr_flags = MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32; + break; + + case GameProfile::WOW_1X: + // File rules when adding files to archive: + AddRuleByFileMask("*.mp3", 0, 0); + AddRuleDefault(MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_1; + create_settings_.sector_size = 0x1000; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.attr_flags = + MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; + break; + + case GameProfile::WOW_2X: + case GameProfile::WOW_3X: + // File rules when adding files to archive: + AddRuleByFileMask("*.mp3", 0, 0); + AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC, MPQ_COMPRESSION_ZLIB); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_2; + create_settings_.sector_size = 0x1000; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.attr_flags = + MPQ_ATTRIBUTE_FILETIME | MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; + break; + + case GameProfile::WOW_4X: + case GameProfile::WOW_5X: + // File rules when adding files to archive: + AddRuleByFileSize(0, 0, MPQ_FILE_DELETE_MARKER, 0); + AddRuleByFileMask("*.mp3", 0, 0); + AddRuleByFileMask("*.ogg", 0, 0); + AddRuleByFileMask("*.ogv", 0, 0); + AddRuleByFileSize(0, 0x4000, MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, + MPQ_COMPRESSION_ZLIB); + AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC, MPQ_COMPRESSION_ZLIB); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_4; + create_settings_.raw_chunk_size = 0x4000; + create_settings_.sector_size = 0x4000; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.attr_flags = MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; + break; + + case GameProfile::STARCRAFT2: + // File rules when adding files to archive: + AddRuleByFileSize(0, 0, MPQ_FILE_DELETE_MARKER, 0); + AddRuleByFileMask("*.mp3", 0, 0); + AddRuleByFileMask("*.ogg", 0, 0); + AddRuleByFileMask("*.ogv", 0, 0); + AddRuleByFileSize(0, 0x4000, MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, + MPQ_COMPRESSION_ZLIB); + AddRuleByFileMask("*.wav", MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_SECTOR_CRC, MPQ_COMPRESSION_ZLIB); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_2; + create_settings_.sector_size = 0x4000; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.attr_flags = MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; + break; + + case GameProfile::DIABLO3: + // File rules when adding files to archive: + AddRuleByFileSize(0, 0, MPQ_FILE_DELETE_MARKER, 0); + AddRuleByFileMask("*.mp3", 0, 0); + AddRuleByFileMask("*.ogg", 0, 0); + AddRuleByFileMask("*.ogv", 0, 0); + AddRuleByFileSize(0, 0x4000, MPQ_FILE_COMPRESS | MPQ_FILE_SINGLE_UNIT, + MPQ_COMPRESSION_ZLIB); + AddRuleDefault(MPQ_FILE_COMPRESS, MPQ_COMPRESSION_ZLIB); + + // Settings for archive creation: + create_settings_.mpq_version = MPQ_FORMAT_VERSION_4; + create_settings_.raw_chunk_size = 0x4000; + create_settings_.sector_size = 0x4000; + create_settings_.file_flags1 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.file_flags2 = MPQ_FILE_EXISTS | MPQ_FILE_COMPRESS; + create_settings_.attr_flags = MPQ_ATTRIBUTE_CRC32 | MPQ_ATTRIBUTE_MD5; + break; + + case GameProfile::GENERIC: + default: + // File rules when adding files to archive: + AddRuleDefault(MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED, MPQ_COMPRESSION_PKWARE); + + // For settings for archive creation, use defaults from MpqCreateSettings constructor + break; } } diff --git a/src/gamerules.h b/src/gamerules.h index db6273d..1a26fdb 100644 --- a/src/gamerules.h +++ b/src/gamerules.h @@ -9,140 +9,121 @@ #include enum class GameProfile { - GENERIC, // Default/generic MPQ with basic compression - DIABLO1, // Diablo I / Hellfire (1997) - LORDSOFMAGIC, // Lords of Magic SE (1998) - STARCRAFT1, // StarCraft / Brood War (1998) - WARCRAFT2, // Warcraft II: Battle.net Edition (1999) - DIABLO2, // Diablo II / Lords of Destruction (2000) - WARCRAFT3, // Warcraft III / The Frozen Throne (2002) - WARCRAFT3_MAP, // Warcraft III Map files (2002) - WOW_1X, // World of Warcraft 1 - Vanilla (2004) - WOW_2X, // World of Warcraft 2 - The Burning Crusade (2007) - WOW_3X, // World of Warcraft 3 - Wrath of the Lich King (2008) - WOW_4X, // World of Warcraft 4 - Cataclysm (2010) - WOW_5X, // World of Warcraft 5 - Mists of Pandaria (2012) - STARCRAFT2, // StarCraft II (2010) - DIABLO3 // Diablo III (2012) + GENERIC, // Default/generic MPQ with basic compression + DIABLO1, // Diablo I / Hellfire (1997) + LORDSOFMAGIC, // Lords of Magic SE (1998) + STARCRAFT1, // StarCraft / Brood War (1998) + WARCRAFT2, // Warcraft II: Battle.net Edition (1999) + DIABLO2, // Diablo II / Lords of Destruction (2000) + WARCRAFT3, // Warcraft III / The Frozen Throne (2002) + WARCRAFT3_MAP, // Warcraft III Map files (2002) + WOW_1X, // World of Warcraft 1 - Vanilla (2004) + WOW_2X, // World of Warcraft 2 - The Burning Crusade (2007) + WOW_3X, // World of Warcraft 3 - Wrath of the Lich King (2008) + WOW_4X, // World of Warcraft 4 - Cataclysm (2010) + WOW_5X, // World of Warcraft 5 - Mists of Pandaria (2012) + STARCRAFT2, // StarCraft II (2010) + DIABLO3 // Diablo III (2012) }; enum class RuleType { - FILE_MASK, // Rule based on file pattern (e.g., "*.wav") - FILE_SIZE, // Rule based on file size range - DEFAULT // Default rule (fallback) + FILE_MASK, // Rule based on file pattern (e.g., "*.wav") + FILE_SIZE, // Rule based on file size range + DEFAULT // Default rule (fallback) }; // Structure representing a single compression rule struct CompressionRule { RuleType type; - std::string fileMask; // For FILE_MASK rules (e.g., "*.wav", "UI\\*.blp") - DWORD sizeMin; // For FILE_SIZE rules - DWORD sizeMax; // For FILE_SIZE rules - DWORD mpqFlags; // MPQ file flags (compression, encryption, etc.) - DWORD compressionFirst; // Compression for first sector - DWORD compressionNext; // Compression for subsequent sectors - - CompressionRule(std::string mask, const DWORD flags, const DWORD compFirst, - const DWORD compNext = MPQ_COMPRESSION_NEXT_SAME) - : type(RuleType::FILE_MASK), - fileMask(std::move(mask)), - sizeMin(0), - sizeMax(0), - mpqFlags(flags), - compressionFirst(compFirst), - compressionNext(compNext) {} - - CompressionRule(const DWORD minSize, const DWORD maxSize, const DWORD flags, - const DWORD compFirst, const DWORD compNext = MPQ_COMPRESSION_NEXT_SAME) - : type(RuleType::FILE_SIZE), - fileMask(""), - sizeMin(minSize), - sizeMax(maxSize), - mpqFlags(flags), - compressionFirst(compFirst), - compressionNext(compNext) {} - - CompressionRule(const DWORD flags, const DWORD compFirst, - const DWORD compNext = MPQ_COMPRESSION_NEXT_SAME) - : type(RuleType::DEFAULT), - fileMask(""), - sizeMin(0), - sizeMax(0), - mpqFlags(flags), - compressionFirst(compFirst), - compressionNext(compNext) {} + std::string file_mask; // For FILE_MASK rules (e.g., "*.wav", "UI\\*.blp") + DWORD size_min; // For FILE_SIZE rules + DWORD size_max; // For FILE_SIZE rules + DWORD mpq_flags; // MPQ file flags (compression, encryption, etc.) + DWORD compression_first; // Compression for first sector + DWORD compression_next; // Compression for subsequent sectors + + CompressionRule(std::string mask, const DWORD flags, const DWORD comp_first, + const DWORD comp_next = MPQ_COMPRESSION_NEXT_SAME) + : type(RuleType::FILE_MASK), file_mask(std::move(mask)), size_min(0), size_max(0), + mpq_flags(flags), compression_first(comp_first), compression_next(comp_next) {} + + CompressionRule(const DWORD min_size, const DWORD max_size, const DWORD flags, + const DWORD comp_first, const DWORD comp_next = MPQ_COMPRESSION_NEXT_SAME) + : type(RuleType::FILE_SIZE), file_mask(""), size_min(min_size), size_max(max_size), + mpq_flags(flags), compression_first(comp_first), compression_next(comp_next) {} + + CompressionRule(const DWORD flags, const DWORD comp_first, + const DWORD comp_next = MPQ_COMPRESSION_NEXT_SAME) + : type(RuleType::DEFAULT), file_mask(""), size_min(0), size_max(0), mpq_flags(flags), + compression_first(comp_first), compression_next(comp_next) {} }; // Structure to hold compression settings for a file struct CompressionSettings { - DWORD mpqFlags; - DWORD compressionFirst; - DWORD compressionNext; + DWORD mpq_flags; + DWORD compression_first; + DWORD compression_next; }; // Structure to hold optional override settings for adding files struct CompressionSettingsOverrides { - std::optional dwFlags; - std::optional dwCompression; - std::optional dwCompressionNext; + std::optional flags; + std::optional compression; + std::optional compression_next; }; // Structure to hold MPQ archive creation settings struct MpqCreateSettings { - DWORD mpqVersion; // MPQ format version (1, 2, 3, or 4) - DWORD streamFlags; // Stream flags (e.g., STREAM_PROVIDER_FLAT) - DWORD fileFlags1; // File flags for (listfile) - DWORD fileFlags2; // File flags for (attributes) - DWORD fileFlags3; // File flags for (signature) - DWORD attrFlags; // Attribute flags (CRC32, FILETIME, MD5, etc.) - DWORD sectorSize; // Sector size (typically 0x1000 or 0x4000) - DWORD rawChunkSize; // Raw chunk size (for MPQ v4, typically 0x4000) + DWORD mpq_version; // MPQ format version (1, 2, 3, or 4) + DWORD stream_flags; // Stream flags (e.g., STREAM_PROVIDER_FLAT) + DWORD file_flags1; // File flags for (listfile) + DWORD file_flags2; // File flags for (attributes) + DWORD file_flags3; // File flags for (signature) + DWORD attr_flags; // Attribute flags (CRC32, FILETIME, MD5, etc.) + DWORD sector_size; // Sector size (typically 0x1000 or 0x4000) + DWORD raw_chunk_size; // Raw chunk size (for MPQ v4, typically 0x4000) // Constructor with defaults MpqCreateSettings() - : mpqVersion(MPQ_FORMAT_VERSION_1), - streamFlags(STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE), - fileFlags1(MPQ_FILE_DEFAULT_INTERNAL), - fileFlags2(0), - fileFlags3(0), - attrFlags(0), - sectorSize(0x1000), - rawChunkSize(0) {} + : mpq_version(MPQ_FORMAT_VERSION_1), + stream_flags(STREAM_PROVIDER_FLAT | BASE_PROVIDER_FILE), + file_flags1(MPQ_FILE_DEFAULT_INTERNAL), file_flags2(0), file_flags3(0), attr_flags(0), + sector_size(0x1000), raw_chunk_size(0) {} }; // Structure to hold optional override settings for MPQ archive creation struct MpqCreateSettingsOverrides { - std::optional mpqVersion; - std::optional streamFlags; - std::optional fileFlags1; - std::optional fileFlags2; - std::optional fileFlags3; - std::optional attrFlags; - std::optional sectorSize; - std::optional rawChunkSize; + std::optional mpq_version; + std::optional stream_flags; + std::optional file_flags1; + std::optional file_flags2; + std::optional file_flags3; + std::optional attr_flags; + std::optional sector_size; + std::optional raw_chunk_size; }; // Game rules class that manages compression rules for different games class GameRules { private: - GameProfile profile; - std::vector rules; - MpqCreateSettings createSettings; + GameProfile profile_; + std::vector rules_; + MpqCreateSettings create_settings_; // Helper function to match file mask pattern static bool MatchFileMask(const std::string &filename, const std::string &mask); // Add rule by file mask - void AddRuleByFileMask(const std::string &fileMask, DWORD mpqFlags, DWORD compressionFirst, - DWORD compressionNext = MPQ_COMPRESSION_NEXT_SAME); + void AddRuleByFileMask(const std::string &file_mask, DWORD mpq_flags, DWORD compression_first, + DWORD compression_next = MPQ_COMPRESSION_NEXT_SAME); // Add rule by file size - void AddRuleByFileSize(DWORD sizeMin, DWORD sizeMax, DWORD mpqFlags, DWORD compressionFirst, - DWORD compressionNext = MPQ_COMPRESSION_NEXT_SAME); + void AddRuleByFileSize(DWORD size_min, DWORD size_max, DWORD mpq_flags, DWORD compression_first, + DWORD compression_next = MPQ_COMPRESSION_NEXT_SAME); // Add default rule - void AddRuleDefault(DWORD mpqFlags, DWORD compressionFirst, - DWORD compressionNext = MPQ_COMPRESSION_NEXT_SAME); + void AddRuleDefault(DWORD mpq_flags, DWORD compression_first, + DWORD compression_next = MPQ_COMPRESSION_NEXT_SAME); // Initialize rules for the selected game profile void InitializeRules(); @@ -152,20 +133,20 @@ class GameRules { public: // Constructor - explicit GameRules(GameProfile gameProfile); + explicit GameRules(GameProfile game_profile); // Get compression settings for a specific file [[nodiscard]] CompressionSettings GetCompressionSettings(const std::string &filename, - DWORD fileSize) const; + DWORD file_size) const; // Get MPQ creation settings - [[nodiscard]] const MpqCreateSettings &GetCreateSettings() const { return createSettings; } + [[nodiscard]] const MpqCreateSettings &GetCreateSettings() const { return create_settings_; } // Override MPQ creation settings void OverrideCreateSettings(const MpqCreateSettingsOverrides &overrides); // Convert string to GameProfile enum - static GameProfile StringToProfile(const std::string &profileName); + static GameProfile StringToProfile(const std::string &profile_name); // Get list of canonical game profile names (for display purposes) static std::vector GetCanonicalProfiles(); @@ -177,4 +158,4 @@ class GameRules { static GameProfile GetDefaultProfile() { return GameProfile::GENERIC; } }; -#endif // GAMERULES_H +#endif // GAMERULES_H diff --git a/src/helpers.cpp b/src/helpers.cpp index cd62ca5..cbdb877 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -15,42 +15,42 @@ namespace fs = std::filesystem; -std::string FileTimeToLsTime(int64_t fileTime) { - if (fileTime == 0) { +std::string FileTimeToLsTime(int64_t file_time) { + if (file_time == 0) { return ""; } - constexpr int64_t EPOCH_DIFF = 11644473600LL; - int64_t unixTime = (fileTime / 10000000) - EPOCH_DIFF; + constexpr int64_t epoch_diff = 11644473600LL; + int64_t unix_time = (file_time / 10000000) - epoch_diff; char buf[20]; struct tm tm_buf; #ifdef _WIN32 - localtime_s(&tm_buf, &unixTime); + localtime_s(&tm_buf, &unix_time); #else - localtime_r(&unixTime, &tm_buf); + localtime_r(&unix_time, &tm_buf); #endif strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf); return std::string(buf); } std::string NormalizeFilePath(const fs::path &path) { - std::string filePath = path.u8string(); + std::string file_path = path.u8string(); #ifndef _WIN32 - std::replace(filePath.begin(), filePath.end(), '\\', '/'); - return filePath; + std::replace(file_path.begin(), file_path.end(), '\\', '/'); + return file_path; #else - return filePath; + return file_path; #endif } std::string WindowsifyFilePath(const fs::path &path) { - std::string filePath = path.u8string(); - std::replace(filePath.begin(), filePath.end(), '/', '\\'); - return filePath; + std::string file_path = path.u8string(); + std::replace(file_path.begin(), file_path.end(), '/', '\\'); + return file_path; } std::string StormErrorString(uint32_t err) { switch (err) { - // clang-format off + // clang-format off case ERROR_SUCCESS: return "Success"; case ERROR_FILE_NOT_FOUND: return "File not found"; case ERROR_ACCESS_DENIED: return "Access denied (archive may be read-only or have open files)"; @@ -81,35 +81,35 @@ std::string StormErrorString(uint32_t err) { case ERROR_FAKE_MPQ_HEADER: return "Fake MPQ header at this position"; case ERROR_FILE_DELETED: return "File contains delete marker"; default: return std::strerror(static_cast(err)); - // clang-format on + // clang-format on } } uint32_t CalculateMpqMaxFileValue(const std::string &path) { - uint32_t fileCount = 0; + uint32_t file_count = 0; // Determine the number of files in the target directory, recusively if (!fs::is_regular_file(path)) { for (const auto &entry : fs::recursive_directory_iterator(path)) { if (fs::is_regular_file(entry.path())) { - ++fileCount; + ++file_count; } } } // Always add 3 for "special" files - fileCount += 3; + file_count += 3; // Based on file count, determine the max number of files an MPQ archive can hold // We always have a minimum of 32 // Anything over is rounded up to the closest power of 2 // For example: 64, 128, 256 // This is examples behavior of WoW MPQ archives (patches and installs) - if (fileCount <= 32) { + if (file_count <= 32) { return 32; } - return NextPowerOfTwo(fileCount); + return NextPowerOfTwo(file_count); } uint32_t NextPowerOfTwo(uint32_t n) { diff --git a/src/helpers.h b/src/helpers.h index 8a1fb2b..2a3a537 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -6,7 +6,7 @@ namespace fs = std::filesystem; -std::string FileTimeToLsTime(int64_t fileTime); +std::string FileTimeToLsTime(int64_t file_time); std::string NormalizeFilePath(const fs::path &path); std::string WindowsifyFilePath(const fs::path &path); std::string StormErrorString(uint32_t err); diff --git a/src/locales.cpp b/src/locales.cpp index 97a699d..257c221 100644 --- a/src/locales.cpp +++ b/src/locales.cpp @@ -17,58 +17,58 @@ namespace { // https://winprotocoldoc.z19.web.core.windows.net/MS-LCID/%5bMS-LCID%5d.pdf // Define a bidirectional map for locale-language mappings -const std::map localeToLangMap = { - {0x000, "enUS"}, // Default - English (US) - {0x404, "zhTW"}, // Chinese (Taiwan) - {0x405, "csCZ"}, // Czech - {0x407, "deDE"}, // German - {0x409, "enUS"}, // English (US) - {0x40a, "esES"}, // Spanish (Spain) - {0x40c, "frFR"}, // French - {0x410, "itIT"}, // Italian - {0x411, "jaJP"}, // Japanese - {0x412, "koKR"}, // Korean - {0x413, "nlNL"}, // Dutch - {0x415, "plPL"}, // Polish - {0x416, "ptBR"}, // Portuguese (Brazil) - {0x419, "ruRU"}, // Russian - {0x804, "zhCN"}, // Chinese (Simplified) - {0x809, "enGB"}, // English (UK) - {0x80A, "esMX"}, // Spanish (Mexico) - {0x816, "ptPT"}, // Portuguese (Portugal) +const std::map locale_to_lang_map = { + {0x000, "enUS"}, // Default - English (US) + {0x404, "zhTW"}, // Chinese (Taiwan) + {0x405, "csCZ"}, // Czech + {0x407, "deDE"}, // German + {0x409, "enUS"}, // English (US) + {0x40a, "esES"}, // Spanish (Spain) + {0x40c, "frFR"}, // French + {0x410, "itIT"}, // Italian + {0x411, "jaJP"}, // Japanese + {0x412, "koKR"}, // Korean + {0x413, "nlNL"}, // Dutch + {0x415, "plPL"}, // Polish + {0x416, "ptBR"}, // Portuguese (Brazil) + {0x419, "ruRU"}, // Russian + {0x804, "zhCN"}, // Chinese (Simplified) + {0x809, "enGB"}, // English (UK) + {0x80A, "esMX"}, // Spanish (Mexico) + {0x816, "ptPT"}, // Portuguese (Portugal) }; // Create a reverse map for language-to-locale lookups -const std::map langToLocaleMap = []() { - std::map reverseMap; - for (const auto &[locale, lang] : localeToLangMap) { - if (locale != defaultLocale) { // Skip the default locale to avoid duplication - reverseMap[lang] = locale; +const std::map lang_to_locale_map = []() { + std::map reverse_map; + for (const auto &[locale, lang] : locale_to_lang_map) { + if (locale != default_locale) { // Skip the default locale to avoid duplication + reverse_map[lang] = locale; } } - return reverseMap; + return reverse_map; }(); std::string FormatLocaleAsHex(const LCID locale) { std::stringstream ss; ss << std::hex << std::uppercase << locale; - const std::string hexStr = ss.str(); + const std::string hex_str = ss.str(); // Prepend 0s if needed - return std::string(4 - hexStr.length(), '0') + hexStr; + return std::string(4 - hex_str.length(), '0') + hex_str; } -} // namespace +} // namespace // Check if a string is a 4-character hexadecimal number and parse it // Returns the parsed LCID if valid, otherwise returns defaultLocale (0) LCID ParseHexLocale(const std::string &str) { if (str.length() != 4) { - return defaultLocale; + return default_locale; } // Check if all characters are hexadecimal for (char c : str) { if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) { - return defaultLocale; + return default_locale; } } @@ -81,29 +81,29 @@ LCID ParseHexLocale(const std::string &str) { } std::string LocaleToLang(uint16_t locale) { - auto it = localeToLangMap.find(locale); - return it != localeToLangMap.end() ? it->second : FormatLocaleAsHex(locale); + auto it = locale_to_lang_map.find(locale); + return it != locale_to_lang_map.end() ? it->second : FormatLocaleAsHex(locale); } LCID LangToLocale(const std::string &lang) { - auto it = langToLocaleMap.find(lang); - if (it != langToLocaleMap.end()) { + auto it = lang_to_locale_map.find(lang); + if (it != lang_to_locale_map.end()) { return it->second; } // Try parsing as a hexadecimal LCID - LCID hexLocale = ParseHexLocale(lang); - if (hexLocale != defaultLocale) { - return hexLocale; + LCID hex_locale = ParseHexLocale(lang); + if (hex_locale != default_locale) { + return hex_locale; } - return defaultLocale; + return default_locale; } std::vector GetAllLocales() { std::vector locales; - for (const auto &[locale, lang] : localeToLangMap) { - if (locale != defaultLocale) { // Skip the default locale to avoid duplication + for (const auto &[locale, lang] : locale_to_lang_map) { + if (locale != default_locale) { // Skip the default locale to avoid duplication locales.push_back(lang); } } @@ -112,8 +112,8 @@ std::vector GetAllLocales() { return locales; } -std::string PrettyPrintLocale(const LCID locale, const std::string &prefix, bool alwaysPrint) { - if (locale == defaultLocale && !alwaysPrint) { +std::string PrettyPrintLocale(const LCID locale, const std::string &prefix, bool always_print) { + if (locale == default_locale && !always_print) { return ""; } const auto lang = LocaleToLang(locale); diff --git a/src/locales.h b/src/locales.h index e18fd82..ff07736 100644 --- a/src/locales.h +++ b/src/locales.h @@ -7,13 +7,13 @@ #include -inline constexpr LCID defaultLocale = 0; +inline constexpr LCID default_locale = 0; std::string LocaleToLang(uint16_t locale); LCID LangToLocale(const std::string &lang); LCID ParseHexLocale(const std::string &str); std::vector GetAllLocales(); std::string PrettyPrintLocale(LCID locale, const std::string &prefix = "", - bool alwaysPrint = false); + bool always_print = false); -#endif // LOCALES_H +#endif // LOCALES_H diff --git a/src/main.cpp b/src/main.cpp index 9ec5999..796a3d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,53 +21,69 @@ int main(int argc, char **argv) { // CLI: base // These are reused in multiple subcommands // clang-format off - std::string baseTarget; // all subcommands - std::string baseFile; // extract, read - std::optional baseLocale; // add, create, extract, read, remove - std::optional basePath; // add, create - std::optional baseOutput; // create, extract - std::optional baseListfileName; // extract, list, compact - std::optional baseGameProfile; // add, create - int64_t fileDwFlags = -1; // add, create - int64_t fileDwCompression = -1; // add, create - int64_t fileDwCompressionNext = -1; // add, create + std::string base_target; // all subcommands + std::string base_file; // extract, read + std::optional base_locale; // add, create, extract, read, remove + std::optional base_path; // add, create + std::optional base_output; // create, extract + std::optional base_listfile_name; // extract, list, compact + std::optional base_game_profile; // add, create + int64_t file_flags = -1; // add, create + int64_t file_compression = -1; // add, create + int64_t file_compression_next = -1; // add, create // clang-format on // CLI: info - std::optional infoProperty; + std::optional info_property; // CLI: add - bool addOverwrite = false; - bool addUpdate = false; - std::vector addFiles; + bool add_overwrite = false; + bool add_update = false; + std::vector add_files; // CLI: remove - std::vector removeFiles; + std::vector remove_files; // CLI: extract - bool extractKeepFolderStructure = false; + bool extract_keep_folder_structure = false; // CLI: create - bool createSignArchive = false; - int32_t createMpqVersion = -1; - int64_t createStreamFlags = -1; - int64_t createSectorSize = -1; - int64_t createRawChunkSize = -1; - int64_t createFileFlags1 = -1; - int64_t createFileFlags2 = -1; - int64_t createFileFlags3 = -1; - int64_t createAttrFlags = -1; + bool create_sign_archive = false; + int32_t create_mpq_version = -1; + int64_t create_stream_flags = -1; + int64_t create_sector_size = -1; + int64_t create_raw_chunk_size = -1; + int64_t create_file_flags1 = -1; + int64_t create_file_flags2 = -1; + int64_t create_file_flags3 = -1; + int64_t create_attr_flags = -1; // CLI: list - bool listDetailed = false; - bool listAll = false; - std::vector listProperties; + bool list_detailed = false; + bool list_all = false; + std::vector list_properties; // CLI: verify - bool verifyPrintSignature = false; + bool verify_print_signature = false; - // clang-format off -- preserve vertical alignment of string set initialisers - std::set validInfoProperties = { - "format-version", "header-offset", "header-size", "archive-size", - "file-count", "max-files", "signature-type", + // Preserve vertical alignment of string set initialisers + // clang-format off + std::set valid_info_properties = { + "format-version", + "header-offset", + "header-size", + "archive-size", + "file-count", + "max-files", + "signature-type", }; - std::set validFileListProperties = { - "hash-index", "name-hash1", "name-hash2", "name-hash3", "locale", - "file-index", "byte-offset", "file-time", "file-size", "compressed-size", - "flags", "encryption-key", "encryption-key-raw", + std::set valid_file_list_properties = { + "hash-index", + "name-hash1", + "name-hash2", + "name-hash3", + "locale", + "file-index", + "byte-offset", + "file-time", + "file-size", + "compressed-size", + "flags", + "encryption-key", + "encryption-key-raw", }; // clang-format on @@ -79,157 +95,163 @@ int main(int argc, char **argv) { // Subcommand: Info CLI::App *info = app.add_subcommand("info", "Prints info about an MPQ archive"); - info->add_option("target", baseTarget, "Target MPQ archive") + info->add_option("target", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - info->add_option("-p,--property", infoProperty, "Prints only a specific property value") - ->check(CLI::IsMember(validInfoProperties)); + info->add_option("-p,--property", info_property, "Prints only a specific property value") + ->check(CLI::IsMember(valid_info_properties)); // Subcommand: Create CLI::App *create = app.add_subcommand("create", "Create an MPQ archive from target file or directory"); - create->add_option("target", baseTarget, "Directory or file to put in MPQ archive") + create->add_option("target", base_target, "Directory or file to put in MPQ archive") ->required() ->check(CLI::ExistingPath); - create->add_option("-p,--path", basePath, + create->add_option("-p,--path", base_path, "Archive path for a single file, or prefix for a directory"); - create->add_option("-o,--output", baseOutput, "Output MPQ archive"); - create->add_flag("-s,--sign", createSignArchive, "Sign the MPQ archive (default false)"); - create->add_option("--locale", baseLocale, "Locale to use for added files")->check(LocaleValid); + create->add_option("-o,--output", base_output, "Output MPQ archive"); + create->add_flag("-s,--sign", create_sign_archive, "Sign the MPQ archive (default false)"); + create->add_option("--locale", base_locale, "Locale to use for added files") + ->check(locale_valid); create - ->add_option( - "-g,--game", baseGameProfile, - "Game profile for MPQ creation. Valid options:\n" + GameRules::GetAvailableProfiles()) - ->check(GameProfileValid); + ->add_option("-g,--game", base_game_profile, + "Game profile for MPQ creation. Valid options:\n" + + GameRules::GetAvailableProfiles()) + ->check(game_profile_valid); // MPQ creation settings overrides - create->add_option("--version", createMpqVersion, "Override the MPQ archive version") + create->add_option("--version", create_mpq_version, "Override the MPQ archive version") ->check(CLI::Range(1, 4)) ->group("Game setting overrides"); - create->add_option("--stream-flags", createStreamFlags, "Override stream flags") + create->add_option("--stream-flags", create_stream_flags, "Override stream flags") ->group("Game setting overrides"); - create->add_option("--sector-size", createSectorSize, "Override sector size") + create->add_option("--sector-size", create_sector_size, "Override sector size") ->group("Game setting overrides"); - create->add_option("--raw-chunk-size", createRawChunkSize, "Override raw chunk size for MPQ v4") + create + ->add_option("--raw-chunk-size", create_raw_chunk_size, + "Override raw chunk size for MPQ v4") ->group("Game setting overrides"); - create->add_option("--file-flags1", createFileFlags1, "Override file flags for (listfile)") + create->add_option("--file-flags1", create_file_flags1, "Override file flags for (listfile)") ->group("Game setting overrides"); - create->add_option("--file-flags2", createFileFlags2, "Override file flags for (attributes)") + create->add_option("--file-flags2", create_file_flags2, "Override file flags for (attributes)") ->group("Game setting overrides"); - create->add_option("--file-flags3", createFileFlags3, "Override file flags for (signature)") + create->add_option("--file-flags3", create_file_flags3, "Override file flags for (signature)") ->group("Game setting overrides"); create - ->add_option("--attr-flags", createAttrFlags, + ->add_option("--attr-flags", create_attr_flags, "Override attribute flags (CRC32, FILETIME, MD5)") ->group("Game setting overrides"); // Compression settings overrides for files being added - create->add_option("--flags", fileDwFlags, "Override MPQ file flags for added files") + create->add_option("--flags", file_flags, "Override MPQ file flags for added files") ->group("Game setting overrides"); create - ->add_option("--compression", fileDwCompression, + ->add_option("--compression", file_compression, "Override compression for first sector of added files") ->group("Game setting overrides"); create - ->add_option("--compression-next", fileDwCompressionNext, + ->add_option("--compression-next", file_compression_next, "Override compression for subsequent sectors of added files") ->group("Game setting overrides"); // Subcommand: Add CLI::App *add = app.add_subcommand("add", "Add files to an existing MPQ archive"); - add->add_option("archive", baseTarget, "Target MPQ archive") + add->add_option("archive", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - add->add_option("files", addFiles, + add->add_option("files", add_files, "Files or directories to add; pass - to read paths from stdin") ->required() ->expected(-1); - add->add_option("-p,--path", basePath, + add->add_option("-p,--path", base_path, "Archive path for a single file, or prefix for a directory"); - add->add_flag("-w,--overwrite", addOverwrite, "Overwrite file if it already is in MPQ archive"); - add->add_flag("-u,--update", addUpdate, + add->add_flag("-w,--overwrite", add_overwrite, + "Overwrite file if it already is in MPQ archive"); + add->add_flag("-u,--update", add_update, "Skip files whose archived size matches the on-disk size (directory add only)"); - add->add_option("--locale", baseLocale, "Locale to use for added file")->check(LocaleValid); - add->add_option("-g,--game", baseGameProfile, + add->add_option("--locale", base_locale, "Locale to use for added file")->check(locale_valid); + add->add_option("-g,--game", base_game_profile, "Game profile for compression rules. Valid options:\n" + GameRules::GetAvailableProfiles()) - ->check(GameProfileValid); + ->check(game_profile_valid); // Compression settings overrides - add->add_option("--flags", fileDwFlags, "Override MPQ file flags") + add->add_option("--flags", file_flags, "Override MPQ file flags") ->group("Game setting overrides"); - add->add_option("--compression", fileDwCompression, "Override compression for first sector") + add->add_option("--compression", file_compression, "Override compression for first sector") ->group("Game setting overrides"); - add->add_option("--compression-next", fileDwCompressionNext, + add->add_option("--compression-next", file_compression_next, "Override compression for subsequent sectors") ->group("Game setting overrides"); // Subcommand: Remove CLI::App *remove = app.add_subcommand("remove", "Remove files from an existing MPQ archive"); - remove->add_option("archive", baseTarget, "Target MPQ archive") + remove->add_option("archive", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); remove - ->add_option("files", removeFiles, + ->add_option("files", remove_files, "Archive paths of files to remove; pass - to read paths from stdin") ->required() ->expected(-1); - remove->add_option("--locale", baseLocale, "Locale of file to remove")->check(LocaleValid); + remove->add_option("--locale", base_locale, "Locale of file to remove")->check(locale_valid); // Subcommand: List CLI::App *list = app.add_subcommand("list", "List files from the MPQ archive"); - list->add_option("target", baseTarget, "Target MPQ archive") + list->add_option("target", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - list->add_option("-l,--listfile", baseListfileName, "File listing content of an MPQ archive") + list->add_option("-l,--listfile", base_listfile_name, "File listing content of an MPQ archive") ->check(CLI::ExistingFile); - list->add_flag("-d,--detailed", listDetailed, + list->add_flag("-d,--detailed", list_detailed, "File listing with additional columns (default false)"); - list->add_flag("-a,--all", listAll, "File listing including hidden files (default false)"); - list->add_option("-p,--property", listProperties, "Prints only specific property values") - ->check(CLI::IsMember(validFileListProperties)); + list->add_flag("-a,--all", list_all, "File listing including hidden files (default false)"); + list->add_option("-p,--property", list_properties, "Prints only specific property values") + ->check(CLI::IsMember(valid_file_list_properties)); // Subcommand: Extract CLI::App *extract = app.add_subcommand("extract", "Extract files from the MPQ archive"); - extract->add_option("target", baseTarget, "Target MPQ archive") + extract->add_option("target", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - extract->add_option("-o,--output", baseOutput, "Output directory"); - extract->add_option("-f,--file", baseFile, "Target file to extract"); - extract->add_flag("-k,--keep", extractKeepFolderStructure, + extract->add_option("-o,--output", base_output, "Output directory"); + extract->add_option("-f,--file", base_file, "Target file to extract"); + extract->add_flag("-k,--keep", extract_keep_folder_structure, "Keep folder structure (default false)"); - extract->add_option("-l,--listfile", baseListfileName, "File listing content of an MPQ archive") + extract + ->add_option("-l,--listfile", base_listfile_name, "File listing content of an MPQ archive") ->check(CLI::ExistingFile); - extract->add_option("--locale", baseLocale, "Preferred locale for extracted file"); + extract->add_option("--locale", base_locale, "Preferred locale for extracted file"); // Subcommand: Read CLI::App *read = app.add_subcommand("read", "Read a file from an MPQ archive"); - read->add_option("file", baseFile, "File to read")->required(); - read->add_option("target", baseTarget, "Target MPQ archive") + read->add_option("file", base_file, "File to read")->required(); + read->add_option("target", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - read->add_option("--locale", baseLocale, "Preferred locale for read file"); + read->add_option("--locale", base_locale, "Preferred locale for read file"); // Subcommand: Verify CLI::App *verify = app.add_subcommand("verify", "Verify the MPQ archive"); - verify->add_option("target", baseTarget, "Target MPQ archive") + verify->add_option("target", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - verify->add_flag("-p,--print", verifyPrintSignature, "Print the digital signature (in hex)"); + verify->add_flag("-p,--print", verify_print_signature, "Print the digital signature (in hex)"); // Subcommand: Compact CLI::App *compact = app.add_subcommand("compact", "Compact the MPQ archive"); - compact->add_option("target", baseTarget, "Target MPQ archive") + compact->add_option("target", base_target, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - compact->add_option("-l,--listfile", baseListfileName, "File listing content of an MPQ archive") + compact + ->add_option("-l,--listfile", base_listfile_name, "File listing content of an MPQ archive") ->check(CLI::ExistingFile); // Subcommand: Completion CLI::App *completion = app.add_subcommand("completion", "Generate shell completion script"); - CLI::App *completionBash = + CLI::App *completion_bash = completion->add_subcommand("bash", "Generate bash completion script"); - CLI::App *completionZsh = completion->add_subcommand("zsh", "Generate zsh completion script"); - CLI::App *completionPs = + CLI::App *completion_zsh = completion->add_subcommand("zsh", "Generate zsh completion script"); + CLI::App *completion_ps = completion->add_subcommand("powershell", "Generate PowerShell completion script"); - CLI::App *completionFish = + CLI::App *completion_fish = completion->add_subcommand("fish", "Generate fish completion script"); try { @@ -252,86 +274,89 @@ int main(int argc, char **argv) { } if (app.got_subcommand(info)) { - return HandleInfo(baseTarget, infoProperty); + return HandleInfo(base_target, info_property); } if (app.got_subcommand(create)) { - return HandleCreate(baseTarget, basePath, baseOutput, createSignArchive, baseLocale, - baseGameProfile, createMpqVersion, createStreamFlags, createSectorSize, - createRawChunkSize, createFileFlags1, createFileFlags2, - createFileFlags3, createAttrFlags, fileDwFlags, fileDwCompression, - fileDwCompressionNext); + return HandleCreate(base_target, base_path, base_output, create_sign_archive, base_locale, + base_game_profile, create_mpq_version, create_stream_flags, + create_sector_size, create_raw_chunk_size, create_file_flags1, + create_file_flags2, create_file_flags3, create_attr_flags, file_flags, + file_compression, file_compression_next); } if (app.got_subcommand(add)) { - std::vector resolvedAddFiles; - for (const auto &f : addFiles) { + std::vector resolved_add_files; + for (const auto &f : add_files) { if (f == "-") { std::string line; while (std::getline(std::cin, line)) { - if (!line.empty()) resolvedAddFiles.push_back(line); + if (!line.empty()) + resolved_add_files.push_back(line); } } else { - resolvedAddFiles.push_back(f); + resolved_add_files.push_back(f); } } - return HandleAdd(resolvedAddFiles, baseTarget, basePath, addOverwrite, addUpdate, - baseLocale, baseGameProfile, fileDwFlags, fileDwCompression, - fileDwCompressionNext); + return HandleAdd(resolved_add_files, base_target, base_path, add_overwrite, add_update, + base_locale, base_game_profile, file_flags, file_compression, + file_compression_next); } if (app.got_subcommand(remove)) { - std::vector resolvedRemoveFiles; - for (const auto &f : removeFiles) { + std::vector resolved_remove_files; + for (const auto &f : remove_files) { if (f == "-") { std::string line; while (std::getline(std::cin, line)) { - if (!line.empty()) resolvedRemoveFiles.push_back(line); + if (!line.empty()) + resolved_remove_files.push_back(line); } } else { - resolvedRemoveFiles.push_back(f); + resolved_remove_files.push_back(f); } } - return HandleRemove(resolvedRemoveFiles, baseTarget, baseLocale); + return HandleRemove(resolved_remove_files, base_target, base_locale); } if (app.got_subcommand(list)) { - return HandleList(baseTarget, baseListfileName, listAll, listDetailed, listProperties); + return HandleList(base_target, base_listfile_name, list_all, list_detailed, + list_properties); } if (app.got_subcommand(extract)) { - std::optional extractFile = - baseFile.empty() ? std::nullopt : std::make_optional(baseFile); - return HandleExtract(baseTarget, baseOutput, extractFile, extractKeepFolderStructure, - baseListfileName, baseLocale); + std::optional extract_file = + base_file.empty() ? std::nullopt : std::make_optional(base_file); + return HandleExtract(base_target, base_output, extract_file, extract_keep_folder_structure, + base_listfile_name, base_locale); } if (app.got_subcommand(read)) { - return HandleRead(baseFile, baseTarget, baseLocale); + return HandleRead(base_file, base_target, base_locale); } if (app.got_subcommand(verify)) { - return HandleVerify(baseTarget, verifyPrintSignature); + return HandleVerify(base_target, verify_print_signature); } if (app.got_subcommand(compact)) { - return HandleCompact(baseTarget, baseListfileName); + return HandleCompact(base_target, base_listfile_name); } if (app.got_subcommand(completion)) { - if (completion->got_subcommand(completionBash)) { + if (completion->got_subcommand(completion_bash)) { HandleCompletionBash(); return 0; } - if (completion->got_subcommand(completionZsh)) { + if (completion->got_subcommand(completion_zsh)) { HandleCompletionZsh(); return 0; } - if (completion->got_subcommand(completionPs)) { + if (completion->got_subcommand(completion_ps)) { HandleCompletionPs(); return 0; } - if (completion->got_subcommand(completionFish)) { + if (completion->got_subcommand(completion_fish)) { HandleCompletionFish(); return 0; } diff --git a/src/mpq.cpp b/src/mpq.cpp index 606f4d2..6e99232 100644 --- a/src/mpq.cpp +++ b/src/mpq.cpp @@ -18,11 +18,11 @@ namespace fs = std::filesystem; -static const std::vector kSpecialMpqFiles = {"(listfile)", "(signature)", - "(attributes)"}; +static const std::vector special_mpq_files = {"(listfile)", "(signature)", + "(attributes)"}; -bool OpenMpqArchive(const std::string &filename, HANDLE *hArchive, int32_t flags) { - if (!SFileOpenArchive(filename.c_str(), 0, flags, hArchive)) { +bool OpenMpqArchive(const std::string &filename, HANDLE *archive, int32_t flags) { + if (!SFileOpenArchive(filename.c_str(), 0, flags, archive)) { const auto error = SErrGetLastError(); std::cerr << "[!] Failed to open MPQ archive: " << filename << ": (" << error << ") " << StormErrorString(error) << std::endl; @@ -31,8 +31,8 @@ bool OpenMpqArchive(const std::string &filename, HANDLE *hArchive, int32_t flags return true; } -bool CloseMpqArchive(HANDLE hArchive) { - if (!SFileCloseArchive(hArchive)) { +bool CloseMpqArchive(HANDLE archive) { + if (!SFileCloseArchive(archive)) { const auto error = SErrGetLastError(); std::cerr << "[!] Failed to close MPQ archive: (" << error << ") " << StormErrorString(error) << std::endl; @@ -41,23 +41,23 @@ bool CloseMpqArchive(HANDLE hArchive) { return true; } -bool FileExistsInArchiveForLocale(const HANDLE hArchive, const std::string &filePath, +bool FileExistsInArchiveForLocale(const HANDLE archive, const std::string &file_path, const LCID locale) { - bool fileExists = false; + bool file_exists = false; SFileSetLocale(locale); - HANDLE hFile; - if (SFileOpenFileEx(hArchive, filePath.c_str(), SFILE_OPEN_FROM_MPQ, &hFile)) { - const auto fileLocale = GetFileInfo(hFile, SFileInfoLocale); - if (fileLocale == locale) { - fileExists = true; + HANDLE file; + if (SFileOpenFileEx(archive, file_path.c_str(), SFILE_OPEN_FROM_MPQ, &file)) { + const auto file_locale = GetFileInfo(file, SFileInfoLocale); + if (file_locale == locale) { + file_exists = true; } - SFileCloseFile(hFile); + SFileCloseFile(file); } - return fileExists; + return file_exists; } -bool SignMpqArchive(HANDLE hArchive) { - if (!SFileSignArchive(hArchive, SIGNATURE_TYPE_WEAK)) { +bool SignMpqArchive(HANDLE archive) { + if (!SFileSignArchive(archive, SIGNATURE_TYPE_WEAK)) { const auto error = SErrGetLastError(); std::cerr << "[!] Failed to sign MPQ archive: (" << error << ") " << StormErrorString(error) << std::endl; @@ -66,132 +66,133 @@ bool SignMpqArchive(HANDLE hArchive) { return true; } -int ExtractFiles(HANDLE hArchive, const std::string &output, - const std::optional &listfileName, LCID preferredLocale) { - SFileSetLocale(preferredLocale); +int ExtractFiles(HANDLE archive, const std::string &output, + const std::optional &listfile_name, LCID preferred_locale) { + SFileSetLocale(preferred_locale); // Check if the user provided a listfile input - const char *listfile = listfileName.has_value() ? listfileName->c_str() : nullptr; + const char *listfile = listfile_name.has_value() ? listfile_name->c_str() : nullptr; - SFILE_FIND_DATA findData; - HANDLE findHandle = SFileFindFirstFile(hArchive, "*", &findData, listfile); - if (findHandle == nullptr) { + SFILE_FIND_DATA find_data; + HANDLE find_handle = SFileFindFirstFile(archive, "*", &find_data, listfile); + if (find_handle == nullptr) { std::cerr << "[!] Failed to find first file in MPQ archive." << std::endl; return 1; } int32_t result = 0; do { - result |= ExtractFile(hArchive, output, findData.cFileName, - true, // Keep folder structure - preferredLocale); - } while (SFileFindNextFile(findHandle, &findData)); + result |= ExtractFile(archive, output, find_data.cFileName, + true, // Keep folder structure + preferred_locale); + } while (SFileFindNextFile(find_handle, &find_data)); - SFileFindClose(findHandle); + SFileFindClose(find_handle); return result; } -int ExtractFile(HANDLE hArchive, const std::string &output, const std::string &fileName, - bool keepFolderStructure, LCID preferredLocale) { - SFileSetLocale(preferredLocale); - const char *szFileName = fileName.c_str(); - if (!FileExistsInArchiveForLocale(hArchive, szFileName, preferredLocale) && - !FileExistsInArchiveForLocale(hArchive, szFileName, defaultLocale)) { +int ExtractFile(HANDLE archive, const std::string &output, const std::string &file_name, + bool keep_folder_structure, LCID preferred_locale) { + SFileSetLocale(preferred_locale); + if (!FileExistsInArchiveForLocale(archive, file_name.c_str(), preferred_locale) && + !FileExistsInArchiveForLocale(archive, file_name.c_str(), default_locale)) { std::cerr << "[!] Failed: File doesn't exist" - << PrettyPrintLocale(preferredLocale, " for locale ", true) << ": " << szFileName + << PrettyPrintLocale(preferred_locale, " for locale ", true) << ": " << file_name << std::endl; return 1; } // Change forward slashes on non-Windows systems - fs::path fileNamePath(fileName); - std::string fileNameString = NormalizeFilePath(fileNamePath); + fs::path file_name_path(file_name); + std::string file_name_string = NormalizeFilePath(file_name_path); // Remove folder structure if keepFolderStructure is false - if (!keepFolderStructure) { - fileNamePath = fs::path(fileNameString); - fileNameString = fileNamePath.filename().u8string(); + if (!keep_folder_structure) { + file_name_path = fs::path(file_name_string); + file_name_string = file_name_path.filename().u8string(); } // Create output directory - fs::path outputPathAbsolute = fs::canonical(output); - fs::path outputPathBase = outputPathAbsolute.parent_path() / outputPathAbsolute.filename(); - std::filesystem::create_directories(fs::path(outputPathBase).parent_path()); + fs::path output_path_absolute = fs::canonical(output); + fs::path output_path_base = + output_path_absolute.parent_path() / output_path_absolute.filename(); + std::filesystem::create_directories(fs::path(output_path_base).parent_path()); // Ensure sub-directories for folder-nested files exist before calling canonical - fs::path outputFilePathName = outputPathBase / fileNameString; - std::filesystem::create_directories(outputFilePathName.parent_path()); + fs::path output_file_path_name = output_path_base / file_name_string; + std::filesystem::create_directories(output_file_path_name.parent_path()); // 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()) { - std::cerr << "[!] Blocked: path traversal attempt detected: " << fileNameString + fs::path resolved_output = + fs::canonical(output_file_path_name.parent_path()) / output_file_path_name.filename(); + if (std::mismatch(output_path_base.begin(), output_path_base.end(), resolved_output.begin(), + resolved_output.end()) + .first != output_path_base.end()) { + std::cerr << "[!] Blocked: path traversal attempt detected: " << file_name_string << std::endl; return 1; } - std::string outputFileName{resolvedOutput.u8string()}; + std::string output_file_name{resolved_output.u8string()}; - if (SFileExtractFile(hArchive, szFileName, outputFileName.c_str(), 0)) { - std::cout << "[*] Extracted: " << fileNameString << std::endl; + if (SFileExtractFile(archive, file_name.c_str(), output_file_name.c_str(), 0)) { + std::cout << "[*] Extracted: " << file_name_string << std::endl; } else { const auto error = SErrGetLastError(); std::cerr << "[!] Failed: (" << error << ") " << StormErrorString(error) << ": " - << szFileName << std::endl; + << file_name << std::endl; return 1; } return 0; } -HANDLE CreateMpqArchive(const std::string &outputArchiveName, const uint32_t fileCount, - const GameRules &gameRules) { +HANDLE CreateMpqArchive(const std::string &output_archive_name, const uint32_t file_count, + const GameRules &game_rules) { // Check if file already exists - if (fs::exists(outputArchiveName)) { - std::cerr << "[!] File already exists: " << outputArchiveName << " Exiting..." << std::endl; + if (fs::exists(output_archive_name)) { + std::cerr << "[!] File already exists: " << output_archive_name << " Exiting..." + << std::endl; return nullptr; } - HANDLE hMpq; + HANDLE archive; // Use game-specific create settings - const MpqCreateSettings &settings = gameRules.GetCreateSettings(); + const MpqCreateSettings &settings = game_rules.GetCreateSettings(); - SFILE_CREATE_MPQ createInfo = {}; + SFILE_CREATE_MPQ create_info = {}; // All logic for defaults and dependencies is handled in GameRules::OverrideCreateSettings - createInfo.cbSize = sizeof(SFILE_CREATE_MPQ); - createInfo.dwMpqVersion = settings.mpqVersion; - createInfo.dwStreamFlags = settings.streamFlags; - createInfo.dwFileFlags1 = settings.fileFlags1; - createInfo.dwFileFlags2 = settings.fileFlags2; - createInfo.dwFileFlags3 = settings.fileFlags3; - createInfo.dwAttrFlags = settings.attrFlags; - createInfo.dwSectorSize = settings.sectorSize; - createInfo.dwRawChunkSize = settings.rawChunkSize; - createInfo.dwMaxFileCount = fileCount; - - const bool result = SFileCreateArchive2(outputArchiveName.c_str(), &createInfo, &hMpq); + create_info.cbSize = sizeof(SFILE_CREATE_MPQ); + create_info.dwMpqVersion = settings.mpq_version; + create_info.dwStreamFlags = settings.stream_flags; + create_info.dwFileFlags1 = settings.file_flags1; + create_info.dwFileFlags2 = settings.file_flags2; + create_info.dwFileFlags3 = settings.file_flags3; + create_info.dwAttrFlags = settings.attr_flags; + create_info.dwSectorSize = settings.sector_size; + create_info.dwRawChunkSize = settings.raw_chunk_size; + create_info.dwMaxFileCount = file_count; + + const bool result = SFileCreateArchive2(output_archive_name.c_str(), &create_info, &archive); if (!result) { const auto error = SErrGetLastError(); - std::cerr << "[!] Failed to create MPQ archive: " << outputArchiveName << ": (" << error + std::cerr << "[!] Failed to create MPQ archive: " << output_archive_name << ": (" << error << ") " << StormErrorString(error) << std::endl; return nullptr; } - return hMpq; + return archive; } -int AddFiles(HANDLE hArchive, const std::string &inputPath, const std::string &pathPrefix, - LCID locale, const GameRules &gameRules, const CompressionSettingsOverrides &overrides, - bool overwrite, bool update) { - fs::path targetPath = fs::path(inputPath); +int AddFiles(HANDLE archive, const std::string &input_path, const std::string &path_prefix, + LCID locale, const GameRules &game_rules, + const CompressionSettingsOverrides &overrides, bool overwrite, bool update) { + fs::path target_path = fs::path(input_path); std::vector entries; - for (const auto &entry : fs::recursive_directory_iterator(inputPath)) { + for (const auto &entry : fs::recursive_directory_iterator(input_path)) { if (fs::is_regular_file(entry.path())) { entries.push_back(entry); } @@ -201,138 +202,139 @@ int AddFiles(HANDLE hArchive, const std::string &inputPath, const std::string &p return a.path() < b.path(); }); - int filesAdded = 0; - int filesSkipped = 0; - int filesFailed = 0; + int files_added = 0; + int files_skipped = 0; + int files_failed = 0; for (const auto &entry : entries) { - fs::path inputFilePath = fs::relative(entry, targetPath); - std::string archiveFilePath; + fs::path input_file_path = fs::relative(entry, target_path); + std::string archive_file_path; - if (pathPrefix.empty()) { - archiveFilePath = WindowsifyFilePath(inputFilePath.u8string()); + if (path_prefix.empty()) { + archive_file_path = WindowsifyFilePath(input_file_path.u8string()); } else { - archiveFilePath = WindowsifyFilePath((fs::path(pathPrefix) / inputFilePath).u8string()); + archive_file_path = + WindowsifyFilePath((fs::path(path_prefix) / input_file_path).u8string()); } - if (std::find(kSpecialMpqFiles.begin(), kSpecialMpqFiles.end(), archiveFilePath) != - kSpecialMpqFiles.end()) { - std::cout << "[*] Skipping special MPQ file: " << archiveFilePath << std::endl; + if (std::find(special_mpq_files.begin(), special_mpq_files.end(), archive_file_path) != + special_mpq_files.end()) { + std::cout << "[*] Skipping special MPQ file: " << archive_file_path << std::endl; continue; } if (update) { SFileSetLocale(locale); - HANDLE hFile; - if (SFileOpenFileEx(hArchive, archiveFilePath.c_str(), SFILE_OPEN_FROM_MPQ, &hFile)) { - int32_t fileLocale = GetFileInfo(hFile, SFileInfoLocale); - if (fileLocale == locale) { - DWORD archivedSize = SFileGetFileSize(hFile, nullptr); - SFileCloseFile(hFile); - uintmax_t diskSize = fs::file_size(entry.path()); - if (diskSize == static_cast(archivedSize)) { - std::cout << "[~] Skipping unchanged file: " << archiveFilePath + HANDLE file; + if (SFileOpenFileEx(archive, archive_file_path.c_str(), SFILE_OPEN_FROM_MPQ, &file)) { + int32_t file_locale = GetFileInfo(file, SFileInfoLocale); + if (file_locale == locale) { + DWORD archived_size = SFileGetFileSize(file, nullptr); + SFileCloseFile(file); + uintmax_t disk_size = fs::file_size(entry.path()); + if (disk_size == static_cast(archived_size)) { + std::cout << "[~] Skipping unchanged file: " << archive_file_path << std::endl; - filesSkipped++; + files_skipped++; continue; } } else { - SFileCloseFile(hFile); + SFileCloseFile(file); } } } - const int result = AddFile(hArchive, entry.path(), archiveFilePath, locale, gameRules, + const int result = AddFile(archive, entry.path(), archive_file_path, locale, game_rules, overrides, overwrite); if (result == 0) { - filesAdded++; + files_added++; } else { - filesFailed++; + files_failed++; } } if (update) { - std::cout << "[*] For " << inputPath << ": " << filesAdded << " files added, " - << filesSkipped << " files skipped, " << filesFailed << " files failed." + std::cout << "[*] For " << input_path << ": " << files_added << " files added, " + << files_skipped << " files skipped, " << files_failed << " files failed." << std::endl; } - return filesFailed; + return files_failed; } -int AddFile(HANDLE hArchive, const fs::path &localFile, const std::string &archiveFilePath, - const LCID locale, const GameRules &gameRules, +int AddFile(HANDLE archive, const fs::path &local_file, const std::string &archive_file_path, + const LCID locale, const GameRules &game_rules, const CompressionSettingsOverrides &overrides, bool overwrite) { // Return if file doesn't exist on disk - if (!fs::exists(localFile)) { - std::cerr << "[!] File doesn't exist on disk: " << localFile << std::endl; + if (!fs::exists(local_file)) { + std::cerr << "[!] File doesn't exist on disk: " << local_file << std::endl; return 1; } // Check if file exists in MPQ archive SFileSetLocale(locale); - HANDLE hFile; - if (SFileOpenFileEx(hArchive, archiveFilePath.c_str(), SFILE_OPEN_FROM_MPQ, &hFile)) { - int32_t fileLocale = GetFileInfo(hFile, SFileInfoLocale); - SFileCloseFile(hFile); - if (fileLocale == locale && !overwrite) { + HANDLE file; + if (SFileOpenFileEx(archive, archive_file_path.c_str(), SFILE_OPEN_FROM_MPQ, &file)) { + int32_t file_locale = GetFileInfo(file, SFileInfoLocale); + SFileCloseFile(file); + if (file_locale == locale && !overwrite) { std::cerr << "[!] File" << PrettyPrintLocale(locale, " for locale ") - << " already exists in MPQ archive: " << archiveFilePath << " - Skipping..." + << " already exists in MPQ archive: " << archive_file_path << " - Skipping..." << std::endl; return 1; - } else if (fileLocale == locale) { + } else if (file_locale == locale) { std::cout << "[+] File" << PrettyPrintLocale(locale, " for locale ") - << " already exists in MPQ archive: " << archiveFilePath + << " already exists in MPQ archive: " << archive_file_path << " - Overwriting..." << std::endl; } } std::cout << "[+] Adding file" << PrettyPrintLocale(locale, " for locale ") << ": " - << archiveFilePath << std::endl; + << archive_file_path << std::endl; // Verify that we are not exceeding maxFile size of the archive, and if we do, increase it - int32_t numberOfFiles = GetFileInfo(hArchive, SFileMpqNumberOfFiles); - int32_t maxFiles = GetFileInfo(hArchive, SFileMpqMaxFileCount); + int32_t number_of_files = GetFileInfo(archive, SFileMpqNumberOfFiles); + int32_t max_files = GetFileInfo(archive, SFileMpqMaxFileCount); - if (numberOfFiles + 1 > maxFiles) { - uint32_t newMaxFiles = NextPowerOfTwo(static_cast(numberOfFiles + 1)); - bool setMaxFileCount = SFileSetMaxFileCount(hArchive, newMaxFiles); - if (!setMaxFileCount) { + if (number_of_files + 1 > max_files) { + uint32_t new_max_files = NextPowerOfTwo(static_cast(number_of_files + 1)); + bool set_max_file_count = SFileSetMaxFileCount(archive, new_max_files); + if (!set_max_file_count) { const auto error = SErrGetLastError(); - std::cerr << "[!] Failed to increase new max file count to " << newMaxFiles << ": (" + std::cerr << "[!] Failed to increase new max file count to " << new_max_files << ": (" << error << ") " << StormErrorString(error) << std::endl; return 1; } } // Get file size for rule matching - const std::uintmax_t rawFileSize = fs::file_size(localFile); - if (rawFileSize > std::numeric_limits::max()) { + const std::uintmax_t raw_file_size = fs::file_size(local_file); + if (raw_file_size > std::numeric_limits::max()) { std::cerr << "[!] Warning: file exceeds 4GB, size-based compression rules may not apply " "correctly: " - << localFile << std::endl; + << local_file << std::endl; } - const DWORD fileSize = static_cast( - std::min(rawFileSize, static_cast(std::numeric_limits::max()))); + const DWORD file_size = static_cast( + std::min(raw_file_size, static_cast(std::numeric_limits::max()))); // Get game-specific rules - auto [flags, compressionFirst, compressionNext] = - gameRules.GetCompressionSettings(archiveFilePath, fileSize); + const auto settings = game_rules.GetCompressionSettings(archive_file_path, file_size); // Apply overrides where specified, otherwise use game rules - DWORD dwFlags = overrides.dwFlags.value_or(flags); - DWORD dwCompression = overrides.dwCompression.value_or(compressionFirst); - DWORD dwCompressionNext = overrides.dwCompressionNext.value_or(compressionNext); + DWORD flags = overrides.flags.value_or(settings.mpq_flags); + DWORD compression = overrides.compression.value_or(settings.compression_first); + DWORD compression_next = overrides.compression_next.value_or(settings.compression_next); if (overwrite) { - dwFlags += MPQ_FILE_REPLACEEXISTING; + flags += MPQ_FILE_REPLACEEXISTING; } - bool addedFile = SFileAddFileEx(hArchive, localFile.u8string().c_str(), archiveFilePath.c_str(), - dwFlags, dwCompression, dwCompressionNext); + bool added_file = + SFileAddFileEx(archive, local_file.u8string().c_str(), archive_file_path.c_str(), flags, + compression, compression_next); - if (!addedFile) { + if (!added_file) { const auto error = SErrGetLastError(); - std::cerr << "[!] Failed to add: " << archiveFilePath << ": (" << error << ") " + std::cerr << "[!] Failed to add: " << archive_file_path << ": (" << error << ") " << StormErrorString(error) << std::endl; return 1; } @@ -340,21 +342,21 @@ int AddFile(HANDLE hArchive, const fs::path &localFile, const std::string &archi return 0; } -int RemoveFile(HANDLE hArchive, const std::string &archiveFilePath, LCID locale) { +int RemoveFile(HANDLE archive, const std::string &archive_file_path, LCID locale) { SFileSetLocale(locale); std::cout << "[-] Removing file" << PrettyPrintLocale(locale, " for locale ") << ": " - << archiveFilePath << std::endl; + << archive_file_path << std::endl; - if (!FileExistsInArchiveForLocale(hArchive, archiveFilePath, locale)) { + if (!FileExistsInArchiveForLocale(archive, archive_file_path, locale)) { std::cerr << "[!] Failed: File doesn't exist" - << PrettyPrintLocale(locale, " for locale ", true) << ": " << archiveFilePath + << PrettyPrintLocale(locale, " for locale ", true) << ": " << archive_file_path << std::endl; return 1; } - if (!SFileRemoveFile(hArchive, archiveFilePath.c_str(), 0)) { + if (!SFileRemoveFile(archive, archive_file_path.c_str(), 0)) { std::cerr << "[!] Failed: File cannot be removed" - << PrettyPrintLocale(locale, " for locale ", true) << ": " << archiveFilePath + << PrettyPrintLocale(locale, " for locale ", true) << ": " << archive_file_path << std::endl; return 1; } @@ -365,7 +367,8 @@ int RemoveFile(HANDLE hArchive, const std::string &archiveFilePath, LCID locale) std::string GetFlagString(uint32_t flags) { std::string result; - // clang-format off: preserve column-aligned flag-to-char mappings + // Preserve column-aligned flag-to-char mappings + // clang-format off if (flags & MPQ_FILE_IMPLODE) result += 'i'; if (flags & MPQ_FILE_COMPRESS) result += 'c'; if (flags & MPQ_FILE_ENCRYPTED) result += 'e'; @@ -384,28 +387,28 @@ std::string GetFlagString(uint32_t flags) { return result; } -int ListFiles(HANDLE hArchive, const std::optional &listfileName, bool listAll, - bool listDetailed, const std::vector &properties) { +int ListFiles(HANDLE archive, const std::optional &listfile_name, bool list_all, + bool list_detailed, const std::vector &properties) { // Check if the user provided a listfile input - const char *listfile = listfileName.has_value() ? listfileName->c_str() : nullptr; + const char *listfile = listfile_name.has_value() ? listfile_name->c_str() : nullptr; - SFILE_FIND_DATA findData; - HANDLE findHandle = SFileFindFirstFile(hArchive, "*", &findData, listfile); - if (findHandle == nullptr) { + SFILE_FIND_DATA find_data; + HANDLE find_handle = SFileFindFirstFile(archive, "*", &find_data, listfile); + if (find_handle == nullptr) { std::cerr << "[!] Failed to find first file in MPQ archive." << std::endl; return -1; } - std::vector propertiesToPrint = + std::vector properties_to_print = 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 + list_detailed = + 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 = { + static const std::map property_info_class = { {"hash-index", SFileInfoHashIndex}, {"name-hash1", SFileInfoNameHash1}, {"name-hash2", SFileInfoNameHash2}, @@ -422,241 +425,242 @@ int ListFiles(HANDLE hArchive, const std::optional &listfileName, b }; std::set - seenFileNames; // Used to prevent printing the same file name multiple times + seen_file_names; // Used to prevent printing the same file name multiple times // Loop through all files in the MPQ archive do { // Skip special files unless user wants to list all (like ls -a) - if (!listAll && std::find(kSpecialMpqFiles.begin(), kSpecialMpqFiles.end(), - findData.cFileName) != kSpecialMpqFiles.end()) { + if (!list_all && std::find(special_mpq_files.begin(), special_mpq_files.end(), + find_data.cFileName) != special_mpq_files.end()) { continue; } // Print the detailed (long) file listing (like ls -l) - if (listDetailed) { - if (seenFileNames.find(findData.cFileName) != seenFileNames.end()) { + if (list_detailed) { + if (seen_file_names.find(find_data.cFileName) != seen_file_names.end()) { // Filename has been seen before, and thus printed before. Skip over it. continue; } - seenFileNames.insert(findData.cFileName); + seen_file_names.insert(find_data.cFileName); // Multiple files can be stored with identical filenames under different locales. // Loop over all locales and print the file details for each locale. - DWORD maxLocales = 32; // This will be updated in the call to SFileEnumLocales - std::vector fileLocaleVec(maxLocales); - LCID *fileLocales = fileLocaleVec.data(); + DWORD max_locales = 32; // This will be updated in the call to SFileEnumLocales + std::vector file_locale_vec(max_locales); + LCID *file_locales = file_locale_vec.data(); DWORD result = - SFileEnumLocales(hArchive, findData.cFileName, fileLocales, &maxLocales, 0); + SFileEnumLocales(archive, find_data.cFileName, file_locales, &max_locales, 0); if (result == ERROR_INVALID_PARAMETER) { // This ought to mean that the file name is unknown, whereupon `SFileEnumLocales` // exits early since its check for `IsPseudoFileName` returns true. If that is the // case, it will not have populated `fileLocales` or have updated `maxLocales`. Just // set the maxLocales to 1 and list the file with the unknown name once. - maxLocales = 1; - fileLocales[0] = defaultLocale; + max_locales = 1; + file_locales[0] = default_locale; } else if (result == ERROR_INVALID_HANDLE || result == ERROR_NOT_SUPPORTED) { - std::cerr << "[!] Internal error for file: " << findData.cFileName << std::endl; + std::cerr << "[!] Internal error for file: " << find_data.cFileName << std::endl; continue; } else if (result == ERROR_INSUFFICIENT_BUFFER) { - std::cerr << "[!] There are more than " << maxLocales - << " locales for the file: " << findData.cFileName - << ". Will only list the " << maxLocales << " first files." << std::endl; + std::cerr << "[!] There are more than " << max_locales + << " locales for the file: " << find_data.cFileName + << ". Will only list the " << max_locales << " first files." << std::endl; } // Loop through all found locales - for (DWORD i = 0; i < maxLocales; i++) { - LCID locale = fileLocales[i]; + for (DWORD i = 0; i < max_locales; i++) { + LCID locale = file_locales[i]; SFileSetLocale(locale); - HANDLE hFile; + HANDLE file; // We need to open the file to get detailed information // Use our custom GetFileInfo function - if (!SFileOpenFileEx(hArchive, findData.cFileName, SFILE_OPEN_FROM_MPQ, &hFile)) { - std::cerr << "[!] Failed to open file: " << findData.cFileName << std::endl; - continue; // Skip to the next file + if (!SFileOpenFileEx(archive, find_data.cFileName, SFILE_OPEN_FROM_MPQ, &file)) { + std::cerr << "[!] Failed to open file: " << find_data.cFileName << std::endl; + continue; // Skip to the next file } - for (const auto &prop : propertiesToPrint) { - auto it = kPropertyInfoClass.find(prop); - if (it == kPropertyInfoClass.end()) continue; + for (const auto &prop : properties_to_print) { + auto it = property_info_class.find(prop); + if (it == property_info_class.end()) + continue; if (prop == "hash-index" || prop == "file-index") { - std::cout << std::setw(5) << GetFileInfo(hFile, it->second) << " "; + std::cout << std::setw(5) << GetFileInfo(file, 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(' ') + << GetFileInfo(file, 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(' ') + << GetFileInfo(file, it->second) << std::setfill(' ') << std::dec << " "; } else if (prop == "locale") { std::cout << std::setw(4) - << LocaleToLang(GetFileInfo(hFile, it->second)) << " "; + << LocaleToLang(GetFileInfo(file, it->second)) << " "; } else if (prop == "byte-offset") { std::cout << std::hex << std::setw(8) - << GetFileInfo(hFile, it->second) << std::dec << " "; + << GetFileInfo(file, it->second) << std::dec << " "; } else if (prop == "file-time") { std::cout << std::setw(19) - << FileTimeToLsTime(GetFileInfo(hFile, it->second)) + << FileTimeToLsTime(GetFileInfo(file, it->second)) << " "; } else if (prop == "file-size" || prop == "compressed-size") { - std::cout << std::setw(8) << GetFileInfo(hFile, it->second) << " "; + std::cout << std::setw(8) << GetFileInfo(file, it->second) << " "; } else if (prop == "flags") { std::cout << std::setw(8) - << GetFlagString(GetFileInfo(hFile, it->second)) << " "; + << GetFlagString(GetFileInfo(file, 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(' ') + << GetFileInfo(file, it->second) << std::setfill(' ') << std::dec << " "; } } - std::cout << " " << findData.cFileName << std::endl; - SFileCloseFile(hFile); + std::cout << " " << find_data.cFileName << std::endl; + SFileCloseFile(file); } - SFileSetLocale(defaultLocale); // Reset locale to default after changing it + SFileSetLocale(default_locale); // Reset locale to default after changing it } else { // Print just the filename (like default ls command output) - std::cout << findData.cFileName << std::endl; + std::cout << find_data.cFileName << std::endl; } - } while (SFileFindNextFile(findHandle, &findData)); + } while (SFileFindNextFile(find_handle, &find_data)); - SFileFindClose(findHandle); + SFileFindClose(find_handle); return 0; } -std::unique_ptr ReadFile(HANDLE hArchive, const char *szFileName, unsigned int *fileSize, - LCID preferredLocale) { - SFileSetLocale(preferredLocale); - if (!FileExistsInArchiveForLocale(hArchive, szFileName, preferredLocale) && - !FileExistsInArchiveForLocale(hArchive, szFileName, defaultLocale)) { +std::unique_ptr ReadFile(HANDLE archive, const char *file_name, unsigned int *file_size, + LCID preferred_locale) { + SFileSetLocale(preferred_locale); + if (!FileExistsInArchiveForLocale(archive, file_name, preferred_locale) && + !FileExistsInArchiveForLocale(archive, file_name, default_locale)) { std::cerr << "[!] Failed: File doesn't exist" - << PrettyPrintLocale(preferredLocale, " for locale ", true) << ": " << szFileName + << PrettyPrintLocale(preferred_locale, " for locale ", true) << ": " << file_name << std::endl; return nullptr; } - HANDLE hFile; - if (!SFileOpenFileEx(hArchive, szFileName, SFILE_OPEN_FROM_MPQ, &hFile)) { - std::cerr << "[!] Failed: File cannot be opened: " << szFileName << std::endl; + HANDLE file; + if (!SFileOpenFileEx(archive, file_name, SFILE_OPEN_FROM_MPQ, &file)) { + std::cerr << "[!] Failed: File cannot be opened: " << file_name << std::endl; return nullptr; } - *fileSize = SFileGetFileSize(hFile, nullptr); - if (*fileSize == SFILE_INVALID_SIZE) { - std::cerr << "[!] Failed: Invalid file size for: " << szFileName << std::endl; - SFileCloseFile(hFile); + *file_size = SFileGetFileSize(file, nullptr); + if (*file_size == SFILE_INVALID_SIZE) { + std::cerr << "[!] Failed: Invalid file size for: " << file_name << std::endl; + SFileCloseFile(file); return nullptr; } - auto fileContent = std::make_unique(*fileSize); - DWORD dwBytesRead; - if (!SFileReadFile(hFile, fileContent.get(), *fileSize, &dwBytesRead, nullptr)) { - std::cerr << "[!] Failed: Cannot read file contents for: " << szFileName << std::endl; - SFileCloseFile(hFile); + auto file_content = std::make_unique(*file_size); + DWORD bytes_read; + if (!SFileReadFile(file, file_content.get(), *file_size, &bytes_read, nullptr)) { + std::cerr << "[!] Failed: Cannot read file contents for: " << file_name << std::endl; + SFileCloseFile(file); return nullptr; } - SFileCloseFile(hFile); - return fileContent; + SFileCloseFile(file); + return file_content; } -void PrintMpqInfo(HANDLE hArchive, const std::optional &infoProperty) { +void PrintMpqInfo(HANDLE archive, const std::optional &info_property) { // Map of property names to their corresponding actions - std::map> propertyActions = { + std::map> property_actions = { {"format-version", - [&](bool printName) { - TMPQHeader header = GetFileInfo(hArchive, SFileMpqHeader); - uint16_t formatVersion = - header.wFormatVersion + 1; // Add +1 because StormLib starts at 0 - if (printName) { + [&](bool print_name) { + TMPQHeader header = GetFileInfo(archive, SFileMpqHeader); + uint16_t format_version = + header.wFormatVersion + 1; // Add +1 because StormLib starts at 0 + if (print_name) { std::cout << "Format version: "; } - std::cout << formatVersion << std::endl; + std::cout << format_version << std::endl; }}, {"header-offset", - [&](bool printName) { - int64_t headerOffset = GetFileInfo(hArchive, SFileMpqHeaderOffset); - if (printName) { + [&](bool print_name) { + int64_t header_offset = GetFileInfo(archive, SFileMpqHeaderOffset); + if (print_name) { std::cout << "Header offset: "; } - std::cout << headerOffset << std::endl; + std::cout << header_offset << std::endl; }}, {"header-size", - [&](bool printName) { - int64_t headerSize = GetFileInfo(hArchive, SFileMpqHeaderSize); - if (printName) { + [&](bool print_name) { + int64_t header_size = GetFileInfo(archive, SFileMpqHeaderSize); + if (print_name) { std::cout << "Header size: "; } - std::cout << headerSize << std::endl; + std::cout << header_size << std::endl; }}, {"archive-size", - [&](bool printName) { - int32_t archiveSize = GetFileInfo(hArchive, SFileMpqArchiveSize); - if (printName) { + [&](bool print_name) { + int32_t archive_size = GetFileInfo(archive, SFileMpqArchiveSize); + if (print_name) { std::cout << "Archive size: "; } - std::cout << archiveSize << std::endl; + std::cout << archive_size << std::endl; }}, {"file-count", - [&](bool printName) { - int32_t numberOfFiles = GetFileInfo(hArchive, SFileMpqNumberOfFiles); - if (printName) { + [&](bool print_name) { + int32_t number_of_files = GetFileInfo(archive, SFileMpqNumberOfFiles); + if (print_name) { std::cout << "File count: "; } - std::cout << numberOfFiles << std::endl; + std::cout << number_of_files << std::endl; }}, {"max-files", - [&](bool printName) { - int32_t maxFiles = GetFileInfo(hArchive, SFileMpqMaxFileCount); - if (printName) { + [&](bool print_name) { + int32_t max_files = GetFileInfo(archive, SFileMpqMaxFileCount); + if (print_name) { std::cout << "Max files: "; } - std::cout << maxFiles << std::endl; + std::cout << max_files << std::endl; }}, - {"signature-type", [&](bool printName) { - int32_t signatureType = GetFileInfo(hArchive, SFileMpqSignatures); - if (printName) { + {"signature-type", [&](bool print_name) { + int32_t signature_type = GetFileInfo(archive, SFileMpqSignatures); + if (print_name) { std::cout << "Signature type: "; } - if (signatureType == SIGNATURE_TYPE_NONE) { + if (signature_type == SIGNATURE_TYPE_NONE) { std::cout << "None" << std::endl; - } else if (signatureType == SIGNATURE_TYPE_WEAK) { + } else if (signature_type == SIGNATURE_TYPE_WEAK) { std::cout << "Weak" << std::endl; - } else if (signatureType == SIGNATURE_TYPE_STRONG) { + } else if (signature_type == SIGNATURE_TYPE_STRONG) { std::cout << "Strong" << std::endl; } }}}; // 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 + if (!info_property.has_value()) { + for (const auto &[key, action] : property_actions) { + action(true); // Print property name and value } } else { - auto it = propertyActions.find(infoProperty.value()); - if (it != propertyActions.end()) { - it->second(false); // Print only the value + auto it = property_actions.find(info_property.value()); + if (it != property_actions.end()) { + it->second(false); // Print only the value } } } -uint32_t VerifyMpqArchive(HANDLE hArchive) { - return SFileVerifyArchive(hArchive); +uint32_t VerifyMpqArchive(HANDLE archive) { + return SFileVerifyArchive(archive); } -int CompactMpqArchive(HANDLE hArchive, const std::optional &listfileName) { +int CompactMpqArchive(HANDLE archive, const std::optional &listfile_name) { std::cout << "[*] Compacting archive. This may take some time..." << std::endl; // Check if the user provided a listfile input - const char *listfile = listfileName.has_value() ? listfileName->c_str() : nullptr; + const char *listfile = listfile_name.has_value() ? listfile_name->c_str() : nullptr; - if (!SFileCompactArchive(hArchive, listfile, false)) { + if (!SFileCompactArchive(archive, listfile, false)) { const auto error = SErrGetLastError(); std::cerr << "[!] Failed to compact archive: (" << error << ") " << StormErrorString(error) << std::endl; @@ -665,50 +669,51 @@ int CompactMpqArchive(HANDLE hArchive, const std::optional &listfil return 0; } -int32_t PrintMpqSignature(HANDLE hArchive, const std::string &target) { +int32_t PrintMpqSignature(HANDLE archive, const std::string &target) { // Determine if we have a strong or weak digital signature - int32_t signatureType = GetFileInfo(hArchive, SFileMpqSignatures); + int32_t signature_type = GetFileInfo(archive, SFileMpqSignatures); - std::vector signatureContent; + std::vector signature_content; - if (signatureType == SIGNATURE_TYPE_NONE) { + if (signature_type == SIGNATURE_TYPE_NONE) { return 1; - } else if (signatureType == SIGNATURE_TYPE_WEAK) { - const char *szFileName = "(signature)"; - uint32_t fileSize; - auto fileContent = ReadFile(hArchive, szFileName, &fileSize, defaultLocale); + } else if (signature_type == SIGNATURE_TYPE_WEAK) { + const char *file_name = "(signature)"; + uint32_t file_size; + auto file_content = ReadFile(archive, file_name, &file_size, default_locale); - if (!fileContent) { + if (!file_content) { std::cerr << "[!] Failed to read weak signature file." << std::endl; return -1; } - signatureContent.resize(fileSize); - std::copy(fileContent.get(), fileContent.get() + fileSize, signatureContent.begin()); + signature_content.resize(file_size); + std::copy(file_content.get(), file_content.get() + file_size, signature_content.begin()); - PrintAsBinary(fileContent.get(), fileSize); + PrintAsBinary(file_content.get(), file_size); - } else if (signatureType == SIGNATURE_TYPE_STRONG) { - signatureContent = GetFileInfo>(hArchive, SFileMpqStrongSignature); - if (signatureContent.empty()) { - int64_t archiveSize = GetFileInfo(hArchive, SFileMpqArchiveSize64); - int64_t archiveOffset = GetFileInfo(hArchive, SFileMpqHeaderOffset); + } else if (signature_type == SIGNATURE_TYPE_STRONG) { + signature_content = GetFileInfo>(archive, SFileMpqStrongSignature); + if (signature_content.empty()) { + int64_t archive_size = GetFileInfo(archive, SFileMpqArchiveSize64); + int64_t archive_offset = GetFileInfo(archive, SFileMpqHeaderOffset); - const fs::path archivePath = fs::canonical(target); - std::uintmax_t fileSize = fs::file_size(archivePath); - int64_t signatureLength = fileSize - archiveOffset - archiveSize; + const fs::path archive_path = fs::canonical(target); + std::uintmax_t file_size = fs::file_size(archive_path); + int64_t signature_length = file_size - archive_offset - archive_size; - if (signatureLength <= 0) { - std::cerr << "[!] Invalid signature length: " << signatureLength << std::endl; + if (signature_length <= 0) { + std::cerr << "[!] Invalid signature length: " << signature_length << std::endl; return -1; } - std::ifstream file_mpq(archivePath, std::ios::binary); - file_mpq.seekg(archiveOffset + archiveSize, std::ios::beg); - signatureContent.resize(static_cast(signatureLength)); - file_mpq.read(signatureContent.data(), signatureContent.size()); + std::ifstream file_mpq(archive_path, std::ios::binary); + file_mpq.seekg(archive_offset + archive_size, std::ios::beg); + signature_content.resize(static_cast(signature_length)); + file_mpq.read(signature_content.data(), signature_content.size()); file_mpq.close(); - PrintAsBinary(signatureContent.data(), static_cast(signatureContent.size())); + PrintAsBinary(signature_content.data(), + static_cast(signature_content.size())); } } diff --git a/src/mpq.h b/src/mpq.h index 27d7f92..767d623 100644 --- a/src/mpq.h +++ b/src/mpq.h @@ -12,37 +12,36 @@ namespace fs = std::filesystem; -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, uint32_t fileCount, - const GameRules &gameRules); -int AddFiles(HANDLE hArchive, const std::string &inputPath, const std::string &pathPrefix, - LCID locale, const GameRules &gameRules, +bool OpenMpqArchive(const std::string &filename, HANDLE *archive, int32_t flags); +bool CloseMpqArchive(HANDLE archive); +bool SignMpqArchive(HANDLE archive); +int ExtractFiles(HANDLE archive, const std::string &output, + const std::optional &listfile_name, LCID preferred_locale); +int ExtractFile(HANDLE archive, const std::string &output, const std::string &file_name, + bool keep_folder_structure, LCID preferred_locale); +HANDLE CreateMpqArchive(const std::string &output_archive_name, uint32_t file_count, + const GameRules &game_rules); +int AddFiles(HANDLE archive, const std::string &input_path, const std::string &path_prefix, + LCID locale, const GameRules &game_rules, const CompressionSettingsOverrides &overrides = CompressionSettingsOverrides(), bool overwrite = false, bool update = false); -int AddFile(HANDLE hArchive, const fs::path &localFile, const std::string &archiveFilePath, - LCID locale, const GameRules &gameRules, +int AddFile(HANDLE archive, const fs::path &local_file, const std::string &archive_file_path, + LCID locale, const GameRules &game_rules, const CompressionSettingsOverrides &overrides = CompressionSettingsOverrides(), bool overwrite = false); -int RemoveFile(HANDLE hArchive, const std::string &archiveFilePath, LCID locale); -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::optional &infoProperty); -uint32_t VerifyMpqArchive(HANDLE hArchive); -int CompactMpqArchive(HANDLE hArchive, const std::optional &listfileName); -int32_t PrintMpqSignature(HANDLE hArchive, const std::string &target); +int RemoveFile(HANDLE archive, const std::string &archive_file_path, LCID locale); +int ListFiles(HANDLE archive, const std::optional &listfile_name, bool list_all, + bool list_detailed, const std::vector &properties); +std::unique_ptr ReadFile(HANDLE archive, const char *file_name, unsigned int *file_size, + LCID preferred_locale); +void PrintMpqInfo(HANDLE archive, const std::optional &info_property); +uint32_t VerifyMpqArchive(HANDLE archive); +int CompactMpqArchive(HANDLE archive, const std::optional &listfile_name); +int32_t PrintMpqSignature(HANDLE archive, const std::string &target); -template -T GetFileInfo(HANDLE hFile, SFileInfoClass infoClass) { +template T GetFileInfo(HANDLE file, SFileInfoClass info_class) { T value{}; - if (!SFileGetFileInfo(hFile, infoClass, &value, sizeof(T), nullptr)) { + if (!SFileGetFileInfo(file, info_class, &value, sizeof(T), nullptr)) { return T{}; } return value; diff --git a/src/validators.h b/src/validators.h index 28009b4..dbfbec5 100644 --- a/src/validators.h +++ b/src/validators.h @@ -7,27 +7,28 @@ #include "locales.h" // Defined in gamerules.cpp -extern const CLI::Validator GameProfileValid; +extern const CLI::Validator game_profile_valid; // Inline locale validator -inline const auto LocaleValid = CLI::Validator( +inline const auto locale_valid = CLI::Validator( [](const std::string &str) { - if (str == "default") return std::string(); + if (str == "default") + return std::string(); - if (ParseHexLocale(str) != defaultLocale) { + if (ParseHexLocale(str) != default_locale) { return std::string(); } const LCID locale = LangToLocale(str); if (locale == 0) { - std::string validLocales = "Locale must be nothing, or one of:"; + std::string valid_locales = "Locale must be nothing, or one of:"; for (const auto &l : GetAllLocales()) { - validLocales += " " + l; + valid_locales += " " + l; } - return validLocales; + return valid_locales; } return std::string(); }, "", "LocaleValidator"); -#endif // VALIDATORS_H +#endif // VALIDATORS_H