From 1587e137e5e0b2a046b08e8ebb3b984f60a8a3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Sj=C3=B6blom?= Date: Sun, 26 Oct 2025 11:32:17 +0100 Subject: [PATCH] Adding locale support for the extract subcommands --- README.md | 9 ++ src/main.cpp | 10 +- src/mpq.cpp | 9 +- src/mpq.h | 4 +- test/test_extract.py | 233 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4e8aa69..9c09abc 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,15 @@ Extract a single file using the `-f` option. If the target file in the MPQ archi mpqcli extract -f "Documentation\Layout\Greeting.html" "World of Warcraft_1.12.1.5875/Data/base.MPQ" ``` +### Extract one specific file with locale + +Use the `--locale` argument to specify the locale of the file to extract. If there is no file with the requested name and locale, the default locale will be used instead. + +``` +mpqcli extract -f "rez\gluBNRes.res" Patch_rt.mpq --locale deDE +``` + + ### Read a specific file from an MPQ archive Read the `patch.cmd` file from an MPQ archive and print the file contents to stdout. Even though the subcommand always outputs bytes, plaintext files will be human-readable. diff --git a/src/main.cpp b/src/main.cpp index cf67d4b..5ae4f44 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -137,6 +137,7 @@ int main(int argc, char **argv) { extract->add_flag("-k,--keep", extractKeepFolderStructure, "Keep folder structure (default false)"); extract->add_option("-l,--listfile", baseListfileName, "File listing content of an MPQ archive") ->check(CLI::ExistingFile); + extract->add_option("--locale", baseLocale, "Preferred locale for extracted file"); // Subcommand: Read CLI::App* read = app.add_subcommand("read", "Read a file from an MPQ archive"); @@ -296,10 +297,15 @@ int main(int argc, char **argv) { return 1; } + LCID locale = LangToLocale(baseLocale); + if (baseLocale != "default" && locale == defaultLocale) { + std::cout << "[!] Warning: The locale '" << baseLocale << "' is unknown. Will use default locale instead." << std::endl; + } + if (baseFile != "default") { - ExtractFile(hArchive, baseOutput, baseFile, extractKeepFolderStructure); + ExtractFile(hArchive, baseOutput, baseFile, extractKeepFolderStructure, locale); } else { - ExtractFiles(hArchive, baseOutput, baseListfileName); + ExtractFiles(hArchive, baseOutput, baseListfileName, locale); } } diff --git a/src/mpq.cpp b/src/mpq.cpp index f1d577a..d0351d1 100644 --- a/src/mpq.cpp +++ b/src/mpq.cpp @@ -38,7 +38,8 @@ int SignMpqArchive(HANDLE hArchive) { return 1; } -int ExtractFiles(HANDLE hArchive, const std::string& output, const std::string& listfileName) { +int ExtractFiles(HANDLE hArchive, const std::string& output, const std::string& listfileName, LCID preferredLocale) { + SFileSetLocale(preferredLocale); // Check if the user provided a listfile input const char *listfile = (listfileName == "default") ? NULL : listfileName.c_str(); @@ -55,7 +56,8 @@ int ExtractFiles(HANDLE hArchive, const std::string& output, const std::string& hArchive, output, findData.cFileName, - true // Keep folder structure + true, // Keep folder structure + preferredLocale ); if (result != 0) { return result; @@ -67,7 +69,8 @@ int ExtractFiles(HANDLE hArchive, const std::string& output, const std::string& return 0; } -int ExtractFile(HANDLE hArchive, const std::string& output, const std::string& fileName, bool keepFolderStructure) { +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 (!SFileHasFile(hArchive, szFileName)) { std::cerr << "[!] Failed: File doesn't exist: " << szFileName << std::endl; diff --git a/src/mpq.h b/src/mpq.h index 3acd9a7..17a2b34 100644 --- a/src/mpq.h +++ b/src/mpq.h @@ -11,8 +11,8 @@ namespace fs = std::filesystem; int OpenMpqArchive(const std::string &filename, HANDLE *hArchive, int32_t flags); int CloseMpqArchive(HANDLE hArchive); int SignMpqArchive(HANDLE hArchive); -int ExtractFiles(HANDLE hArchive, const std::string& output, const std::string &listfileName); -int ExtractFile(HANDLE hArchive, const std::string& output, const std::string& fileName, bool keepFolderStructure); +int ExtractFiles(HANDLE hArchive, const std::string& output, const std::string &listfileName, LCID preferredLocale); +int ExtractFile(HANDLE hArchive, const std::string& output, const std::string& fileName, bool keepFolderStructure, LCID preferredLocale); HANDLE CreateMpqArchive(std::string outputArchiveName, int32_t fileCount, int32_t mpqVersion); int AddFiles(HANDLE hArchive, const std::string& inputPath, LCID locale); int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archiveFilePath, LCID locale); diff --git a/test/test_extract.py b/test/test_extract.py index 6390223..afd9087 100644 --- a/test/test_extract.py +++ b/test/test_extract.py @@ -127,6 +127,8 @@ def test_extract_file_from_mpq_output_directory_specified(binary_path, generate_ script_dir = Path(__file__).parent test_file = script_dir / "data" / "mpq_with_output_v1.mpq" output_dir = script_dir / "data" / "extracted_file" + for fi in output_dir.glob("*"): + fi.unlink(missing_ok=True) expected_output = { @@ -155,3 +157,234 @@ def test_extract_file_from_mpq_output_directory_specified(binary_path, generate_ assert output_lines == expected_lines, f"Unexpected output: {output_lines}" assert output_file.exists(), "Output directory was not created" assert output_files == expected_output, f"Unexpected files: {output_files}" + + +def test_extract_file_from_mpq_with_locale(binary_path, generate_locales_mpq_test_files): + """ + Test MPQ archive file extraction with a specified locale. + + This test checks: + - If the file with the given locale is extracted correctly. + - If the output files match the expected files. + """ + _ = generate_locales_mpq_test_files + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_many_locales.mpq" + output_dir = script_dir / "data" / "extracted_file" + file_to_extract = "cats.txt" + locale = "esES" + for fi in output_dir.glob("*"): + fi.unlink(missing_ok=True) + + result = subprocess.run( + [str(binary_path), "extract", "-o", str(output_dir), "-f", file_to_extract, str(test_file), "--locale", locale], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + expected_stdout = { + "[*] Extracted: " + file_to_extract + } + output_file = output_dir / file_to_extract + expected_content = "Este es un archivo sobre gatos." + + output_lines = set(result.stdout.splitlines()) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_stdout, f"Unexpected output: {output_lines}" + assert output_file.exists(), "Output directory was not created" + assert output_file.read_text(encoding="utf-8") == expected_content, "Unexpected file content" + + +def test_extract_file_from_mpq_with_default_locale(binary_path, generate_locales_mpq_test_files): + """ + Test MPQ archive file extraction with no specified locale but many matching file names. + + This test checks: + - If the file with the default locale is extracted correctly. + - If the output files match the expected files. + """ + _ = generate_locales_mpq_test_files + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_many_locales.mpq" + output_dir = script_dir / "data" / "extracted_file" + file_to_extract = "cats.txt" + for fi in output_dir.glob("*"): + fi.unlink(missing_ok=True) + + + result = subprocess.run( + [str(binary_path), "extract", "-o", str(output_dir), "-f", file_to_extract, str(test_file)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + expected_stdout = { + "[*] Extracted: " + file_to_extract + } + output_file = output_dir / file_to_extract + expected_content = "This is a file about cats." + + output_lines = set(result.stdout.splitlines()) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_stdout, f"Unexpected output: {output_lines}" + assert output_file.exists(), "Output directory was not created" + assert output_file.read_text(encoding="utf-8") == expected_content, "Unexpected file content" + + +def test_extract_file_from_mpq_with_illegal_locale(binary_path, generate_locales_mpq_test_files): + """ + Test MPQ archive file extraction with an illegal locale. + + This test checks: + - When a locale is given that does not exist in the file, the file with the default locale is extracted. + - If the output files match the expected files. + """ + _ = generate_locales_mpq_test_files + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_many_locales.mpq" + output_dir = script_dir / "data" / "extracted_file" + file_to_extract = "cats.txt" + locale = "nosuchlocale" + for fi in output_dir.glob("*"): + fi.unlink(missing_ok=True) + + + result = subprocess.run( + [str(binary_path), "extract", "-o", str(output_dir), "-f", file_to_extract, str(test_file), "--locale", locale], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + expected_stdout = { + "[!] Warning: The locale 'nosuchlocale' is unknown. Will use default locale instead.", + "[*] Extracted: " + file_to_extract + } + output_file = output_dir / file_to_extract + expected_content = "This is a file about cats." + + output_lines = set(result.stdout.splitlines()) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_stdout, f"Unexpected output: {output_lines}" + assert output_file.exists(), "Output directory was not created" + assert output_file.read_text(encoding="utf-8") == expected_content, "Unexpected file content" + + +def test_extract_file_from_mpq_with_locale_not_in_file(binary_path, generate_locales_mpq_test_files): + """ + Test MPQ archive file extraction with a specified locale that is not in the file. + + This test checks: + - When a locale is given that does not exist in the file, the file with the default locale is extracted. + - If the output files match the expected files. + """ + _ = generate_locales_mpq_test_files + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_many_locales.mpq" + output_dir = script_dir / "data" / "extracted_file" + file_to_extract = "cats.txt" + locale = "ptPT" # There is no file for this locale + for fi in output_dir.glob("*"): + fi.unlink(missing_ok=True) + + result = subprocess.run( + [str(binary_path), "extract", "-o", str(output_dir), "-f", file_to_extract, str(test_file), "--locale", locale], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + expected_stdout = { + "[*] Extracted: " + file_to_extract + } + output_file = output_dir / file_to_extract + expected_content = "This is a file about cats." + + output_lines = set(result.stdout.splitlines()) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_stdout, f"Unexpected output: {output_lines}" + assert output_file.exists(), "Output directory was not created" + assert output_file.read_text(encoding="utf-8") == expected_content, "Unexpected file content" + + +def test_extract_file_from_mpq_with_no_locale_argument_and_no_default_locale(binary_path, generate_locales_mpq_test_files): + """ + Test MPQ archive file extraction without a specified locale, and no file with the default locale. + + This test checks: + - When no locale is given, and no file by that name exists for the default locale, + but one does for a different one, no file is extracted. + """ + _ = generate_locales_mpq_test_files + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_one_locale.mpq" + output_dir = script_dir / "data" / "extracted_file" + file_to_extract = "cats.txt" # There is a file by this name, but for locale esES + for fi in output_dir.glob("*"): + fi.unlink(missing_ok=True) + + + result = subprocess.run( + [str(binary_path), "extract", "-o", str(output_dir), "-f", file_to_extract, str(test_file)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + expected_stdout_output = set() + expected_stderr_output = { + "[!] Failed: File doesn't exist: " + file_to_extract, + } + + stdout_output_lines = set(result.stdout.splitlines()) + stderr_output_lines = set(result.stderr.splitlines()) + + output_file = output_dir / file_to_extract + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert stdout_output_lines == expected_stdout_output, f"Unexpected output: {stdout_output_lines}" + assert stderr_output_lines == expected_stderr_output, f"Unexpected output: {stderr_output_lines}" + assert not output_file.exists(), "Output directory was not created" + + +def test_extract_file_from_mpq_with_wrong_locale_argument_and_no_default_locale(binary_path, generate_locales_mpq_test_files): + """ + Test MPQ archive file extraction with a specified locale, and no file with the default locale. + + This test checks: + - When no locale is given, and no file by that name exists for the default locale, + but one does for a different one, no file is extracted. + """ + _ = generate_locales_mpq_test_files + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_with_one_locale.mpq" + output_dir = script_dir / "data" / "extracted_file" + file_to_extract = "cats.txt" # There is a file by this name, but for locale esES + locale = "deDE" + for fi in output_dir.glob("*"): + fi.unlink(missing_ok=True) + + + result = subprocess.run( + [str(binary_path), "extract", "-o", str(output_dir), "-f", file_to_extract, str(test_file), "--locale", locale], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + expected_stdout_output = set() + expected_stderr_output = { + "[!] Failed: File doesn't exist: " + file_to_extract, + } + + stdout_output_lines = set(result.stdout.splitlines()) + stderr_output_lines = set(result.stderr.splitlines()) + + output_file = output_dir / file_to_extract + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert stdout_output_lines == expected_stdout_output, f"Unexpected output: {stdout_output_lines}" + assert stderr_output_lines == expected_stderr_output, f"Unexpected output: {stderr_output_lines}" + assert not output_file.exists(), "Output directory was not created"