Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 8 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}
}

Expand Down
9 changes: 6 additions & 3 deletions src/mpq.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/mpq.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
233 changes: 233 additions & 0 deletions test/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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"