diff --git a/README.md b/README.md index b7bdac5..0852d85 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ mpqcli remove fth.txt wow-patch.mpq Pretty simple, list files in an MPQ archive. Useful to "pipe" to other tools, such as `grep` (see below for examples). ``` -mpqcli list wow-patch.mpq +$ mpqcli list wow-patch.mpq BM_COKETENT01.BLP Blizzard_CraftUI.xml CreatureSoundData.dbc @@ -188,22 +188,67 @@ realmlist.wtf Similar to the `ls` command with the `-l` and `-a` options, additional detailed information can be included with the `list` subcommand. The `-a` option includes printing "special" files used in MPQ archives including: `(listfile)`, `(attributes)` and `(signature)`. ``` -mpqcli list -d -a wow-patch.mpq - 88604 enUS 2006-03-29 02:02:37 BM_COKETENT01.BLP - 243 enUS 2006-04-04 21:28:14 Blizzard_CraftUI.xml - 388 enUS 2006-03-29 19:32:46 CreatureSoundData.dbc - ... - 184 enUS 2006-04-04 21:28:14 Blizzard_CraftUI.lua - 44900 enUS 2006-03-29 02:01:02 30ee7bd3959906e358eff01332cf045e.blp - 68 enUS 2006-04-07 00:58:44 realmlist.wtf +$ mpqcli list -d -a wow-patch.mpq + 88604 enUS 2006-03-29 02:02:37 BM_COKETENT01.BLP + 243 enUS 2006-04-04 21:28:14 Blizzard_CraftUI.xml + 388 enUS 2006-03-29 19:32:46 CreatureSoundData.dbc + ... + 184 enUS 2006-04-04 21:28:14 Blizzard_CraftUI.lua + 44900 enUS 2006-03-29 02:01:02 30ee7bd3959906e358eff01332cf045e.blp + 68 enUS 2006-04-07 00:58:44 realmlist.wtf +``` + +### List specific properties + +The `list` subcommand supports listing the following properties: + +- `hash-index` - Index in the hash table where the file entry is. +- `name-hash1` - The first hash of the file name. +- `name-hash2` - The second hash of the file name. +- `name-hash3` - 64-bit Jenkins hash of the file name, used for searching in the HET table. +- `locale` - Locale info of the file. +- `file-index` - Index in the file table of the file. +- `byte-offset` - Offset of the file in the MPQ, relative to the MPQ header. +- `file-time` - Timestamp of the file. +- `file-size` - Uncompressed file size of the file, in bytes. +- `compressed-size` - Compressed file size of the file, in bytes. +- `encryption-key` - Encryption key for the file. +- `encryption-key-raw` - Encryption key for the file. +- `flags` - File flags for the file within MPQ: + * `i`: File is Imploded (By PKWARE Data Compression Library). + * `c`: File is Compressed (By any of multiple methods). + * `e`: File is Encrypted. + * `2`: File is Encrypted with key v2. + * `p`: File is a Patch file. + * `u`: File is stored as a single Unit, rather than split into sectors. + * `d`: File is a Deletion marker. Used in MPQ patches, indicating that the file no longer exists. + * `r`: File has Sector CRC checksums for each sector. This is ignored if the file is not compressed or imploded. + * `s`: Present on STANDARD.SNP\(signature). + * `x`: File exists; this is reset if the file is deleted. + * `m`: Mask for a file being compressed. + * `n`: Use default flags for internal files. + * `f`: Fix key; This is obsolete. + +You can use the `-p` or `--property` argument with the `list` subcommand to print the given properties. Many properties can be given, and they will be printed in the order given. + +``` +$ mpqcli list -d -a War2Patch.mpq -p hash-index -p locale -p flags + 3028 enUS iexmn Rez\gluchat.bin + 8926 enUS iexmn Rez\Gateways.txt + 9078 enUS iexmn Rez\mltiplay.bin +10329 enUS ce2xmnf (listfile) +14213 enUS iexmn Rez\mltiplay_ita.bin +14472 enUS iexmn Rez\mltiplay_esp.bin +15278 enUS iexmn Rez\mltiplay_fra.bin +15731 enUS iexmn Rez\mltiplay_deu.bin ``` ### List all files with an external listfile -Older MPQ archives do not contain (complete) file paths of their content. By providing an external listfile that lists the content of the MPQ archive, the listed files will have the correct paths. Listfiles can be downloaded on [Ladislav Zezula's site](http://www.zezula.net/en/mpq/download.html). +Older MPQ archives do not contain (complete) file paths of their content. By using the `-l` or `--listfile` argument, one can provide an external listfile that lists the content of the MPQ archive, so that the listed files will have the correct paths. Listfiles can be downloaded on [Ladislav Zezula's site](http://www.zezula.net/en/mpq/download.html). ``` -mpqcli list -l /path/to/listfile StarDat.mpq +$ mpqcli list -l /path/to/listfile StarDat.mpq ``` ### Extract all files from an MPQ archive diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aa6e436..05c88c7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(mpqcli main.cpp mpq.cpp helpers.cpp + locales.cpp ) # Add dependencies diff --git a/src/helpers.cpp b/src/helpers.cpp index aca852d..2a52bb4 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -1,8 +1,6 @@ #include #include -#include #include -#include #include #ifdef _WIN32 @@ -33,24 +31,6 @@ std::string FileTimeToLsTime(int64_t fileTime) { return std::string(buf); } -std::string LocaleToLang(uint16_t locale) { - switch (locale) { - case 0: return "enUS"; // English (US/GB) - case 1: return "koKR"; // Korean - case 2: return "frFR"; // French - case 3: return "deDE"; // German - case 4: return "zhCN"; // Chinese (Simplified) - case 5: return "zhTW"; // Chinese (Taiwan) - case 6: return "esES"; // Spanish (Spain) - case 7: return "esMX"; // Spanish (Mexico) - case 8: return "ruRU"; // Russian - case 9: return "jaJP"; // Japanese - case 10: return "ptPT"; // Portuguese (Portugal) - case 11: return "itIT"; // Italian - default: return ""; // Unknown locale - } -} - std::string NormalizeFilePath(const fs::path &path) { std::string filePath = path.u8string(); #ifndef _WIN32 diff --git a/src/helpers.h b/src/helpers.h index b38b35a..951fc02 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -7,7 +7,6 @@ namespace fs = std::filesystem; std::string FileTimeToLsTime(int64_t fileTime); -std::string LocaleToLang(uint16_t locale); std::string NormalizeFilePath(const fs::path &path); std::string WindowsifyFilePath(const fs::path &path); int32_t CalculateMpqMaxFileValue(const std::string &directory); diff --git a/src/locales.cpp b/src/locales.cpp new file mode 100644 index 0000000..ba4895e --- /dev/null +++ b/src/locales.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include + +#include "locales.h" + +namespace { + // Files in MPQs have locales with which they are associated. + // Multiple files can have the same file name if they have different locales. + // This function maps locales to language names. + // + // The mappings are from the Windows Language Code Identifier (LCID). + // They can be found, for example, here: + // 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, "ptPT"}, // Portuguese (Portugal) + {0x419, "ruRU"}, // Russian + {0x804, "zhCN"}, // Chinese (Simplified) + {0x809, "enGB"}, // English (UK) + {0x80A, "esMX"} // Spanish (Mexico) + }; + + // 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; + } + } + return reverseMap; + }(); +} + +std::string LocaleToLang(uint16_t locale) { + auto it = localeToLangMap.find(locale); + return it != localeToLangMap.end() ? it->second : ""; +} + +LCID LangToLocale(const std::string& lang) { + auto it = langToLocaleMap.find(lang); + return it != langToLocaleMap.end() ? it->second : defaultLocale; +} + + +std::vector GetAllLocales() { + std::vector locales; + for (const auto& [locale, lang] : localeToLangMap) { + if (locale != defaultLocale) { // Skip the default locale to avoid duplication + locales.push_back(lang); + } + } + // Sort the locales for consistent output + std::sort(locales.begin(), locales.end()); + return locales; +} diff --git a/src/locales.h b/src/locales.h new file mode 100644 index 0000000..393c320 --- /dev/null +++ b/src/locales.h @@ -0,0 +1,35 @@ +#ifndef LOCALES_H +#define LOCALES_H + +#include +#include + +#include +#include + +const LCID defaultLocale = 0; + +std::string LocaleToLang(uint16_t locale); +LCID LangToLocale(const std::string &lang); +std::vector GetAllLocales(); + +// Validator for CLI11 +const inline auto LocaleValid = CLI::Validator( + [](const std::string &str) { + if (str == "default") return std::string(); + + LCID locale = LangToLocale(str); + if (locale == 0) { + std::string validLocales = "Locale must be nothing, or one of:"; + for (const auto& l : GetAllLocales()) { + validLocales += " " + l; + } + return validLocales; + } + return std::string(); + }, + "Validates locales and outputs valid locales", + "LocaleValidator" +); + +#endif //LOCALES_H diff --git a/src/main.cpp b/src/main.cpp index d6ec78e..53e7acc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include "mpq.h" #include "helpers.h" +#include "locales.h" #include "mpqcli.h" namespace fs = std::filesystem; @@ -16,7 +17,7 @@ int main(int argc, char **argv) { "A command line tool to create, add, remove, list, extract, read, and verify MPQ archives " "using the StormLib library" }; - + // Require at least one subcommand app.require_subcommand(1); @@ -25,10 +26,13 @@ int main(int argc, char **argv) { std::string baseTarget = "default"; // all subcommands std::string baseFile = "default"; // add, remove, extract, read std::string basePath = "default"; // add + std::string baseLocale = "default"; // create, add, remove, extract, read std::string baseOutput = "default"; // create, extract std::string baseListfileName = "default"; // list, extract // CLI: info - std::string infoProperty = "default"; + std::string infoProperty = "default"; + // CLI: list + std::vector listProperties; // CLI: extract bool extractKeepFolderStructure = false; // CLI: create @@ -41,13 +45,28 @@ int main(int argc, char **argv) { bool verifyPrintSignature = false; std::set validInfoProperties = { - "format-version", - "header-offset", - "header-size", - "archive-size", - "file-count", - "max-files", - "signature-type" + "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", }; // Subcommand: Version @@ -101,6 +120,8 @@ int main(int argc, char **argv) { ->check(CLI::ExistingFile); list->add_flag("-d,--detailed", listDetailed, "File listing with additional columns (default false)"); list->add_flag("-a,--all", listAll, "File listing including hidden files (default true)"); + list->add_option("-p,--property", listProperties, "Prints only specific property values") + ->check(CLI::IsMember(validFileListProperties)); // Subcommand: Extract CLI::App *extract = app.add_subcommand("extract", "Extract files from the MPQ archive"); @@ -147,7 +168,7 @@ int main(int argc, char **argv) { } // Handle subcommand: About - if (app.got_subcommand(about)){ + if (app.got_subcommand(about)) { std::cout << "Name: mpqcli" << std::endl; std::cout << "Version: " << MPQCLI_VERSION << "-" << GIT_COMMIT_HASH << std::endl; std::cout << "Author: Thomas Laurenson" << std::endl; @@ -156,10 +177,10 @@ int main(int argc, char **argv) { std::cout << "Dependencies:" << std::endl; std::cout << " - StormLib (https://github.com/ladislav-zezula/StormLib)" << std::endl; std::cout << " - CLI11 (https://github.com/CLIUtils/CLI11)" << std::endl; - }; + } // Handle subcommand: Info - if (app.got_subcommand(info)){ + if (app.got_subcommand(info)) { HANDLE hArchive; if (!OpenMpqArchive(baseTarget, &hArchive, MPQ_OPEN_READ_ONLY)) { std::cerr << "[!] Failed to open MPQ archive." << std::endl; @@ -178,12 +199,12 @@ int main(int argc, char **argv) { outputFilePath.replace_extension(".mpq"); } std::string outputFile = outputFilePath.u8string(); - + std::cout << "[*] Output file: " << outputFile << std::endl; - // Determine number of files we are going to add + // Determine the number of files we are going to add int32_t fileCount = CalculateMpqMaxFileValue(baseTarget); - + // Create the MPQ archive and add files HANDLE hArchive = CreateMpqArchive(outputFile, fileCount, createMpqVersion); if (hArchive) { @@ -245,7 +266,7 @@ int main(int argc, char **argv) { std::cerr << "[!] Failed to open MPQ archive." << std::endl; return 1; } - ListFiles(hArchive, baseListfileName, listAll, listDetailed); + ListFiles(hArchive, baseListfileName, listAll, listDetailed, listProperties); } // Handle subcommand: Extract diff --git a/src/mpq.cpp b/src/mpq.cpp index 2253275..b470862 100644 --- a/src/mpq.cpp +++ b/src/mpq.cpp @@ -10,6 +10,7 @@ #include "mpq.h" #include "helpers.h" +#include "locales.h" namespace fs = std::filesystem; @@ -171,7 +172,7 @@ int AddFiles(HANDLE hArchive, const std::string& target) { return 0; } -int AddFile(HANDLE hArchive, fs::path localFile, const std::string& archiveFilePath) { +int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archiveFilePath) { std::cout << "[+] Adding file: " << archiveFilePath << std::endl; // Return if file doesn't exist on disk @@ -191,12 +192,10 @@ int AddFile(HANDLE hArchive, fs::path localFile, const std::string& archiveFileP int32_t numberOfFiles = GetFileInfo(hArchive, SFileMpqNumberOfFiles); int32_t maxFiles = GetFileInfo(hArchive, SFileMpqMaxFileCount); - if (numberOfFiles + 1 > maxFiles) - { + if (numberOfFiles + 1 > maxFiles) { int32_t newMaxFiles = NextPowerOfTwo(numberOfFiles + 1); bool setMaxFileCount = SFileSetMaxFileCount(hArchive, newMaxFiles); - if (!setMaxFileCount) - { + if (!setMaxFileCount) { int32_t error = SErrGetLastError(); std::cerr << "[!] Error: " << error << " Failed to increase new max file count to: " << newMaxFiles << std::endl; return -1; @@ -241,7 +240,27 @@ int RemoveFile(HANDLE hArchive, const std::string& archiveFilePath) { return 0; } -int ListFiles(HANDLE hArchive, const std::string& listfileName, bool listAll, bool listDetailed) { +std::string GetFlagString(uint32_t flags) { + std::string result; + + if (flags & MPQ_FILE_IMPLODE) result += 'i'; + if (flags & MPQ_FILE_COMPRESS) result += 'c'; + if (flags & MPQ_FILE_ENCRYPTED) result += 'e'; + if (flags & MPQ_FILE_KEY_V2) result += '2'; + if (flags & MPQ_FILE_PATCH_FILE) result += 'p'; + if (flags & MPQ_FILE_SINGLE_UNIT) result += 'u'; + if (flags & MPQ_FILE_DELETE_MARKER) result += 'd'; + if (flags & MPQ_FILE_SECTOR_CRC) result += 'r'; + if (flags & MPQ_FILE_SIGNATURE) result += 's'; + if (flags & MPQ_FILE_EXISTS) result += 'x'; + if (flags & MPQ_FILE_COMPRESS_MASK) result += 'm'; + if (flags & MPQ_FILE_DEFAULT_INTERNAL) result += 'n'; + if (flags & MPQ_FILE_FIX_KEY) result += 'f'; + + return result; +} + +int ListFiles(HANDLE hArchive, const std::string& listfileName, bool listAll, bool listDetailed, std::vector& propertiesToPrint) { // Check if the user provided a listfile input const char *listfile = (listfileName == "default") ? NULL : listfileName.c_str(); @@ -253,6 +272,13 @@ int ListFiles(HANDLE hArchive, const std::string& listfileName, bool listAll, bo return -1; } + if (propertiesToPrint.empty()) { + propertiesToPrint = { + // Default properties, if the user didn't specify any + "file-size", "locale", "file-time", + }; + } + // "Special" files are base files used by MPQ file format // These are skipped, unless "-a" or "--all" are specified std::vector specialFiles = { @@ -261,7 +287,8 @@ int ListFiles(HANDLE hArchive, const std::string& listfileName, bool listAll, bo "(attributes)" }; - // Loop through all files in MPQ archive + std::set seenFileNames; // 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(specialFiles.begin(), specialFiles.end(), findData.cFileName) != specialFiles.end()) { @@ -270,27 +297,114 @@ int ListFiles(HANDLE hArchive, const std::string& listfileName, bool listAll, bo // Print the detailed (long) file listing (like ls -l) if (listDetailed) { - // We need to open the file to get detailed information - // Use our custom GetFileInfo function - HANDLE hFile; - 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 (seenFileNames.find(findData.cFileName) != seenFileNames.end()) { + // Filename has been seen before, and thus printed before. Skip over it. + continue; } + seenFileNames.insert(findData.cFileName); - int32_t fileSize = GetFileInfo(hFile, SFileInfoFileSize); - int32_t fileLocale = GetFileInfo(hFile, SFileInfoLocale); - std::string fileLocaleStr = LocaleToLang(fileLocale); - int64_t fileTime = GetFileInfo(hFile, SFileInfoFileTime); - std::string fileTimeStr = FileTimeToLsTime(fileTime); - // Print the file details in a formatted way - std::cout << std::setw(11) << fileSize << " " // 4GB max size is 10 characters - << std::setw(5) << fileLocaleStr << " " // Locale is max 4 characters - << std::setw(19) << fileTimeStr << " " // File time is formatted as "YYYY-MM-DD HH:MM:SS" - << findData.cFileName << std::endl; + // 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 + LCID * fileLocales = (LCID *)malloc(maxLocales * sizeof(LCID)); - SFileCloseFile(hFile); + if (fileLocales == NULL) { + std::cerr << "[!] Unable to allocate memory for locales for file: " << findData.cFileName << std::endl; + continue; + } + DWORD result = SFileEnumLocales(hArchive, findData.cFileName, fileLocales, &maxLocales, 0); + + 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; + } + + // Loop through all found locales + for (DWORD i = 0; i < maxLocales; i++) { + LCID locale = fileLocales[i]; + SFileSetLocale(locale); + HANDLE hFile; + + // 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 + } + + std::vector>> propertyActions = { + {"hash-index", [&]() { + std::cout << std::setw(5) << GetFileInfo(hFile, SFileInfoHashIndex) << " " ; + }}, + {"name-hash1", [&]() { + std::cout << std::setfill('0') << std::hex << std::setw(8) << + GetFileInfo(hFile, SFileInfoNameHash1) << + std::setfill(' ') << std::dec << " "; + }}, + {"name-hash2", [&]() { + std::cout << std::setfill('0') << std::hex << std::setw(8) << + GetFileInfo(hFile, SFileInfoNameHash2) << + std::setfill(' ') << std::dec << " "; + }}, + {"name-hash3", [&]() { + std::cout << std::setfill('0') << std::hex << std::setw(16) << + GetFileInfo(hFile, SFileInfoNameHash3) << + std::setfill(' ') << std::dec << " "; + }}, + {"locale", [&]() { + int32_t fileLocale = GetFileInfo(hFile, SFileInfoLocale); + std::string fileLocaleStr = LocaleToLang(fileLocale); + std::cout << std::setw(4) << fileLocaleStr << " "; + }}, + {"file-index", [&]() { + std::cout << std::setw(5) << GetFileInfo(hFile, SFileInfoFileIndex) << " "; + }}, + {"byte-offset", [&]() { + std::cout << std::hex << std::setw(8) << + GetFileInfo(hFile, SFileInfoByteOffset) << + std::dec << " "; + }}, + {"file-time", [&]() { + int64_t fileTime = GetFileInfo(hFile, SFileInfoFileTime); + std::string fileTimeStr = FileTimeToLsTime(fileTime); + std::cout << std::setw(19) << fileTimeStr << " "; + }}, + {"file-size", [&]() { + std::cout << std::setw(8) << GetFileInfo(hFile, SFileInfoFileSize) << " "; + }}, + {"compressed-size", [&]() { + std::cout << std::setw(8) << GetFileInfo(hFile, SFileInfoCompressedSize) << " "; + }}, + {"flags", [&]() { + int32_t flags = GetFileInfo(hFile, SFileInfoFlags); + std::cout << std::setw(8) << GetFlagString(flags) << " "; + }}, + {"encryption-key", [&]() { + std::cout << std::setfill('0') << std::hex << std::setw(8) << + GetFileInfo(hFile, SFileInfoEncryptionKey) << + std::setfill(' ') << std::dec << " "; + }}, + {"encryption-key-raw", [&]() { + std::cout << std::setfill('0') << std::hex << std::setw(8) << + GetFileInfo(hFile, SFileInfoEncryptionKeyRaw) << + std::setfill(' ') << std::dec << " "; + }}, + }; + + for (const auto& prop : propertiesToPrint) { + for (const auto &[key, action]: propertyActions) { + if (prop == key) { + action(); // Print property + } + } + } + + std::cout << " " << findData.cFileName << std::endl; + SFileCloseFile(hFile); + } + SFileSetLocale(defaultLocale); // Reset locale to default after changing it + free(fileLocales); } else { // Print just the filename (like default ls command output) std::cout << findData.cFileName << std::endl; diff --git a/src/mpq.h b/src/mpq.h index ee3226b..c86f302 100644 --- a/src/mpq.h +++ b/src/mpq.h @@ -15,9 +15,9 @@ int ExtractFiles(HANDLE hArchive, const std::string& output, const std::string & int ExtractFile(HANDLE hArchive, const std::string& output, const std::string& fileName, bool keepFolderStructure); HANDLE CreateMpqArchive(std::string outputArchiveName, int32_t fileCount, int32_t mpqVersion); int AddFiles(HANDLE hArchive, const std::string& inputPath); -int AddFile(HANDLE hArchive, fs::path localFile, const std::string& archiveFilePath); +int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archiveFilePath); int RemoveFile(HANDLE hArchive, const std::string& archiveFilePath); -int ListFiles(HANDLE hHandle, const std::string &listfileName, bool listAll, bool listDetailed); +int ListFiles(HANDLE hHandle, const std::string &listfileName, bool listAll, bool listDetailed, std::vector& propertiesToPrint); char* ReadFile(HANDLE hArchive, const char *szFileName, unsigned int *fileSize); void PrintMpqInfo(HANDLE hArchive, const std::string& infoProperty); uint32_t VerifyMpqArchive(HANDLE hArchive); diff --git a/test/conftest.py b/test/conftest.py index d2b4e9c..f4d996a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -3,6 +3,7 @@ import platform from pathlib import Path import urllib.request +import subprocess import pytest @@ -60,6 +61,65 @@ def generate_test_files(): yield created_files +@pytest.fixture(scope="session") +def generate_locales_mpq_test_files(binary_path): + script_dir = Path(__file__).parent + + data_dir = script_dir / "data" + data_dir.mkdir(parents=True, exist_ok=True) + + locales_files_dir = data_dir / "locale_files" + locales_files_dir.mkdir(parents=True, exist_ok=True) + + mpq_many_locales_file_name = data_dir / "mpq_with_many_locales.mpq" + mpq_one_locale_file_name = data_dir / "mpq_with_one_locale.mpq" + text_file_name = "cats.txt" + mpq_many_locales_file_name.unlink(missing_ok=True) + mpq_one_locale_file_name.unlink(missing_ok=True) + + locale_files = { + "": "This is a file about cats.", # Default locale + "deDE": "Dies ist eine Datei über Katzen.", + "esES": "Este es un archivo sobre gatos.", + } + + # Put all items into mpq_many_locales_file_name with their locale + for locale, content in locale_files.items(): + file_path = locales_files_dir / text_file_name + file_path.write_text(content, newline="\n") + + if locale == "": # Default locale - create a new MPQ file + result = subprocess.run( + [str(binary_path), "create", "-v", "1", "-o", str(mpq_many_locales_file_name), str(locales_files_dir)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + else: # Explicit locale - add to existing MPQ file + result = subprocess.run( + [str(binary_path), "add", str(file_path), str(mpq_many_locales_file_name), "--locale", locale], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + # Put the last item into mpq_one_locale_file_name with its locale + locale, content = list(locale_files.items())[-1] + file_path = locales_files_dir / text_file_name + file_path.write_text(content, newline="\n") + + result = subprocess.run( + [str(binary_path), "create", "-v", "1", "-o", str(mpq_one_locale_file_name), str(locales_files_dir), "--locale", locale], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + @pytest.fixture(scope="session") def download_test_files(): script_dir = Path(__file__).parent diff --git a/test/test_list.py b/test/test_list.py index 6ce3141..6483e19 100644 --- a/test/test_list.py +++ b/test/test_list.py @@ -4,6 +4,12 @@ def test_list_mpq_with_output_v1(binary_path): + """ + Test MPQ file listing with no parameters. + + This test checks: + - That running the list command with no parameters renders a list of the files inside, with no details. + """ script_dir = Path(__file__).parent test_file = script_dir / "data" / "mpq_with_output_v1.mpq" @@ -26,25 +32,30 @@ def test_list_mpq_with_output_v1(binary_path): assert output_lines == expected_output, f"Unexpected output: {output_lines}" -def test_list_mpq_with_detailed(binary_path): +def test_list_mpq_with_standard_details(binary_path): + """ + Test MPQ file listing with the standard details. + + This test checks: + - That the standard long listing parameters work as expected. + """ script_dir = Path(__file__).parent test_file = script_dir / "data" / "mpq_with_output_v1.mpq" - # Update expected_out to match long listing format expected_output = { - " 27 enUS (listfile)", - " 148 enUS (attributes)", - " 27 enUS 2025-07-29 14:31:00 dogs.txt", - " 8 enUS 2025-07-29 14:31:00 bytes", - " 27 enUS 2025-07-29 14:31:00 cats.txt", + " 27 enUS (listfile)", + " 148 enUS (attributes)", + " 27 enUS 2025-07-29 14:31:00 dogs.txt", + " 8 enUS 2025-07-29 14:31:00 bytes", + " 27 enUS 2025-07-29 14:31:00 cats.txt", } # Adjust filesize for Windows if platform.system() == "Windows": - expected_output.remove(" 27 enUS 2025-07-29 14:31:00 dogs.txt") - expected_output.add(" 28 enUS 2025-07-29 14:31:00 dogs.txt") - expected_output.remove(" 27 enUS 2025-07-29 14:31:00 cats.txt") - expected_output.add(" 28 enUS 2025-07-29 14:31:00 cats.txt") + expected_output.remove(" 27 enUS 2025-07-29 14:31:00 dogs.txt") + expected_output.add(" 28 enUS 2025-07-29 14:31:00 dogs.txt") + expected_output.remove(" 27 enUS 2025-07-29 14:31:00 cats.txt") + expected_output.add(" 28 enUS 2025-07-29 14:31:00 cats.txt") result = subprocess.run( [str(binary_path), "list", "-a", "-d", str(test_file)], @@ -59,7 +70,84 @@ def test_list_mpq_with_detailed(binary_path): assert output_lines == expected_output, f"Unexpected output: {output_lines}" +def test_list_mpq_with_specified_details(binary_path): + """ + Test MPQ file listing with specified details. + + This test checks: + - That providing parameters to be listed works as expected. + """ + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_output_v1.mpq" + + expected_output = { + " 44 0fd58937 70ab788e 0000000000000000 35 cexmn 935a7772 cats.txt", + " 0 eb30456b 48345fbb 0000000000000000 35 cexmn a073c614 dogs.txt", + " 35 147178ed c99b9ee2 0000000000000000 16 cexmn eaa753f9 bytes", + " 25 fd657910 4e9b98a7 0000000000000000 35 ce2xmnf 2d2f0a94 (listfile)", + " 14 d38437cb 07dfeaec 0000000000000000 123 ce2xmnf 50e314af (attributes)", + } + # Adjust filesize for Windows + if platform.system() == "Windows": + expected_output.remove(" 44 0fd58937 70ab788e 0000000000000000 35 cexmn 935a7772 cats.txt") + expected_output.add(" 44 0fd58937 70ab788e 0000000000000000 36 cexmn 935a7772 cats.txt") + expected_output.remove(" 0 eb30456b 48345fbb 0000000000000000 35 cexmn a073c614 dogs.txt") + expected_output.add(" 0 eb30456b 48345fbb 0000000000000000 36 cexmn a073c614 dogs.txt") + + result = subprocess.run( + [str(binary_path), "list", "-a", "-d", str(test_file), + "-p", "hash-index", "-p", "name-hash1", "-p", "name-hash2", "-p", "name-hash3", + "-p", "compressed-size", "-p", "flags", "-p", "encryption-key-raw"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + output_lines = set(result.stdout.splitlines()) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_output, f"Unexpected output: {output_lines}" + + +def test_list_mpq_with_specified_details_but_no_detailed_flag(binary_path): + """ + Test MPQ file listing. + + This test checks: + - That providing parameters to be listed does nothing if the -d/--detailed flag is not provided. + """ + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_output_v1.mpq" + + expected_output = { + "cats.txt", + "dogs.txt", + "bytes", + "(listfile)", + "(attributes)", + } + + result = subprocess.run( + [str(binary_path), "list", "-a", str(test_file), + "-p", "hash-index", "-p", "flags", "-p", "encryption-key-raw", "-p", "encryption-key"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + output_lines = set(result.stdout.splitlines()) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_output, f"Unexpected output: {output_lines}" + + def test_list_mpq_with_weak_signature(binary_path): + """ + Test MPQ file listing of MPQ with weak signature. + + This test checks: + - That handling MPQs with weak signatures generates the expected output. + """ script_dir = Path(__file__).parent test_file = script_dir / "data" / "mpq_with_weak_signature.mpq"