From 91dbc30a41345955f42120f4fb97a1b6bb089808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Sj=C3=B6blom?= Date: Thu, 27 Nov 2025 18:46:58 +0100 Subject: [PATCH] Adding 'overwrite', 'name-in-archive' and 'dir-in-archive' arguments to the add command --- README.md | 29 ++++++- src/main.cpp | 32 ++++--- src/mpq.cpp | 13 ++- src/mpq.h | 2 +- test/test_add.py | 216 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 01920ae..b493ed3 100644 --- a/README.md +++ b/README.md @@ -162,14 +162,37 @@ $ mpqcli add fth.txt wow-patch.mpq [+] Adding file for locale 0: fth.txt ``` -Alternatively, you can add a file to a specific subdirectory using the `-p` or `--path` argument. +Alternatively, you can add a file under a specific file name using the `-n` or `--name-in-archive` argument. ``` $ echo "For The Alliance" > fta.txt -$ mpqcli add fta.txt wow-patch.mpq --path texts -[+] Adding file for locale 0: texts\fta.txt +$ mpqcli add fta.txt wow-patch.mpq --name-in-archive "texts\\alliance.txt" +[+] Adding file for locale 0: texts\alliance.txt ``` +Alternatively, you can add a file to a specific subdirectory using the `--dir-in-archive` argument. + +``` +$ echo "For The Swarm" > fts.txt +$ mpqcli add fts.txt wow-patch.mpq --dir-in-archive texts +[+] Adding file for locale 0: texts\fts.txt +``` + +To overwrite a file in an MPQ archive, set the `--overwrite` flag: + +``` +$ echo "For The Horde" > allegiance.txt +$ mpqcli add allegiance.txt wow-patch.mpq +[+] Adding file for locale 0: allegiance.txt +$ echo "For The Alliance" > allegiance.txt +$ mpqcli add allegiance.txt wow-patch.mpq +[!] File for locale 0 already exists in MPQ archive: allegiance.txt - Skipping... +$ mpqcli add allegiance.txt wow-patch.mpq --overwrite +[+] File for locale 0 already exists in MPQ archive: allegiance.txt - Overwriting... +[+] Adding file for locale 0: allegiance.txt +``` + + ### Add files to an MPQ archive with a given locale Use the `--locale` argument to specify the locale that the added file will have in the archive. Note that subsequent added files will have the default locale unless the `--locale` argument is specified again. diff --git a/src/main.cpp b/src/main.cpp index c6a2aa3..0916a5b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,14 +25,15 @@ int main(int argc, char **argv) { // These are reused in multiple subcommands std::string baseTarget = "default"; // all subcommands std::string baseFile = "default"; // add, remove, extract, read - std::string basePath = "default"; // add std::string baseLocale = "default"; // create, add, remove, extract, read + std::string baseNameInArchive = "default"; // add, create std::string baseOutput = "default"; // create, extract std::string baseListfileName = "default"; // list, extract // CLI: info std::string infoProperty = "default"; - // CLI: list - std::vector listProperties; + // CLI: add + std::string baseDirInArchive = "default"; // add + bool addOverwrite = false; // CLI: extract bool extractKeepFolderStructure = false; // CLI: create @@ -41,6 +42,7 @@ int main(int argc, char **argv) { // CLI: list bool listDetailed = false; bool listAll = false; + std::vector listProperties; // CLI: verify bool verifyPrintSignature = false; @@ -103,7 +105,9 @@ int main(int argc, char **argv) { add->add_option("target", baseTarget, "Target MPQ archive") ->required() ->check(CLI::ExistingFile); - add->add_option("-p,--path", basePath, "Path within MPQ archive"); + add->add_option("--dir-in-archive", baseDirInArchive, "Directory to put file inside within MPQ archive"); + add->add_option("-n,--name-in-archive", baseNameInArchive, "Filename inside MPQ archive"); + add->add_flag("-w,--overwrite", addOverwrite, "Overwrite file if it already is in MPQ archive"); add->add_option("--locale", baseLocale, "Locale to use for added file") ->check(LocaleValid); @@ -241,20 +245,24 @@ int main(int argc, char **argv) { // Path to file on disk fs::path filePath = fs::path(baseFile); - // Default: use the filename as path, saves file to root of MPQ - std::string archivePath = filePath.filename().u8string(); + std::string archivePath = filePath.filename().u8string(); // Default: use the filename as path, saves file to root of MPQ + if (baseNameInArchive != "default" && baseDirInArchive != "default") { + // Return error since providing both arguments makes no sense and is a user error + std::cerr << "[!] Cannot specify both --name-in-archive and --dir-in-archive." << std::endl; + return 1; - // Optional: specified path inside archive - if (basePath != "default") { - fs::path archiveFullPath = fs::path(basePath) / filePath.filename(); + } else if (baseNameInArchive != "default") { // Optional: specified filename inside archive + filePath = fs::path(baseNameInArchive); + archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ - // Normalise path for MPQ - archivePath = WindowsifyFilePath(archiveFullPath.u8string()); + } else if (baseDirInArchive != "default") { // Optional: specified directory inside archive + filePath = fs::path(baseDirInArchive) / archivePath; + archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ } LCID locale = LangToLocale(baseLocale); - AddFile(hArchive, baseFile, archivePath, locale); + AddFile(hArchive, baseFile, archivePath, locale, addOverwrite); CloseMpqArchive(hArchive); } diff --git a/src/mpq.cpp b/src/mpq.cpp index 4841fbe..ae4477a 100644 --- a/src/mpq.cpp +++ b/src/mpq.cpp @@ -169,13 +169,13 @@ int AddFiles(HANDLE hArchive, const std::string& target, LCID locale) { // Normalise path for MPQ std::string archiveFilePath = WindowsifyFilePath(inputFilePath.u8string()); - AddFile(hArchive, entry.path().u8string(), archiveFilePath, locale); + AddFile(hArchive, entry.path().u8string(), archiveFilePath, locale, false); } } return 0; } -int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archiveFilePath, LCID locale) { +int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archiveFilePath, LCID locale, bool overwrite) { // Return if file doesn't exist on disk if (!fs::exists(localFile)) { @@ -188,10 +188,13 @@ int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archi HANDLE hFile; if (SFileOpenFileEx(hArchive, archiveFilePath.c_str(), SFILE_OPEN_FROM_MPQ, &hFile)) { int32_t fileLocale = GetFileInfo(hFile, SFileInfoLocale); - if (fileLocale == locale) { + if (fileLocale == locale && !overwrite) { std::cerr << "[!] File for locale " << locale << " already exists in MPQ archive: " << archiveFilePath << " - Skipping..." << std::endl; return -1; + } else if (fileLocale == locale) { + std::cout << "[+] File for locale " << locale << " already exists in MPQ archive: " << archiveFilePath + << " - Overwriting..." << std::endl; } } SFileCloseFile(hFile); @@ -215,6 +218,10 @@ int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archi DWORD dwFlags = MPQ_FILE_COMPRESS | MPQ_FILE_ENCRYPTED; DWORD dwCompression = MPQ_COMPRESSION_ZLIB; + if (overwrite) { + dwFlags += MPQ_FILE_REPLACEEXISTING; + } + bool addedFile = SFileAddFileEx( hArchive, localFile.u8string().c_str(), diff --git a/src/mpq.h b/src/mpq.h index 02bb1b3..87e56fb 100644 --- a/src/mpq.h +++ b/src/mpq.h @@ -15,7 +15,7 @@ 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, 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); +int AddFile(HANDLE hArchive, const fs::path& localFile, const std::string& archiveFilePath, LCID locale, bool overwrite); int RemoveFile(HANDLE hArchive, const std::string& archiveFilePath, LCID locale); 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, LCID preferredLocale); diff --git a/test/test_add.py b/test/test_add.py index 20ad165..2b2a7ee 100644 --- a/test/test_add.py +++ b/test/test_add.py @@ -93,6 +93,209 @@ def test_add_file_to_mpq_archive(binary_path, generate_test_files): verify_archive_file_content(binary_path, target_file, expected_content) +def test_add_file_with_nameinarchive_and_dirinarchive_to_mpq_archive(binary_path, generate_test_files): + """ + Test MPQ file addition with name-in-archive and dir-in-archive arguments. + This test checks: + - If the application correctly gives an error when using both name-in-archive and dir-in-archive arguments. + - If the application correctly handles adding a file to an MPQ archive with the name-in-archive argument. + - If the application correctly handles adding a file to an MPQ archive with the dir-in-archive argument. + """ + _ = generate_test_files + script_dir = Path(__file__).parent + target_file = script_dir / "data" / "files.mpq" + + # Start by creating an MPQ archive for this test + create_mpq_archive_for_test(binary_path, script_dir) + + # Create new test files on the fly + test_file0 = script_dir / "data" / "test0.txt" + test_file0.write_text("This is a test file for MPQ addition.") + test_file1 = script_dir / "data" / "test1.txt" + test_file1.write_text("This is a another test file for MPQ addition.") + + + result = subprocess.run( + [str(binary_path), "add", str(test_file0), str(target_file), "--dir-in-archive", "directory", "--name-in-archive", "important.txt"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + assert result.returncode == 1, f"mpqcli failed with error: {result.stderr}" + + result = subprocess.run( + [str(binary_path), "add", str(test_file0), str(target_file), "--dir-in-archive", "directory"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + result = subprocess.run( + [str(binary_path), "add", str(test_file1), str(target_file), "--name-in-archive", "important\\message.txt"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + result = subprocess.run( + [str(binary_path), "list", str(target_file)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + output_lines = set(result.stdout.splitlines()) + + expected_output = { + # Files from before: + "cats.txt", + "dogs.txt", + "bytes", + # Files added in this test: + "directory\\test0.txt", + "important\\message.txt", + } + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_output, f"Unexpected output: {output_lines}" + + +def test_add_existing_file_without_overwrite_should_fail(binary_path, generate_test_files): + """ + Test adding existing files to MPQ archive without the overwrite flag. + This test checks: + - If the application correctly prints an error when adding an existing file to an MPQ archive without the overwrite flag. + """ + _ = generate_test_files + script_dir = Path(__file__).parent + target_file = script_dir / "data" / "files.mpq" + + # Start by creating an MPQ archive for this test + create_mpq_archive_for_test(binary_path, script_dir) + + + # Verify that existing file has the expected content before attempting to add a new file with different content + expected_content = {"This is a file about cats."} + verify_file_in_mpq_has_content(binary_path, target_file, "cats.txt", expected_content) + + + # Create new test files on the fly + test_file = script_dir / "data" / "cats.txt" + test_file.write_text("Attempting to make this file about dogs.") + + + result = subprocess.run( + [str(binary_path), "add", str(test_file), str(target_file)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + output_lines = set(result.stdout.splitlines()) + assert output_lines == set(), f"Unexpected output: {output_lines}" + + output_lines = set(result.stderr.splitlines()) + expected_stderr_output = { + "[!] File for locale 0 already exists in MPQ archive: cats.txt - Skipping...", + } + assert output_lines == expected_stderr_output, f"Unexpected output: {output_lines}" + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + # Verify that the file content is unchanged + verify_file_in_mpq_has_content(binary_path, target_file, "cats.txt", expected_content) + + +def test_add_existing_file_with_overwrite_should_succeed(binary_path, generate_test_files): + """ + Test adding existing files to MPQ archive with the overwrite flag. + This test checks: + - If the application correctly overwrites an existing file to an MPQ archive with the overwrite flag set. + """ + _ = generate_test_files + script_dir = Path(__file__).parent + target_file = script_dir / "data" / "files.mpq" + + # Start by creating an MPQ archive for this test + create_mpq_archive_for_test(binary_path, script_dir) + + + # Verify that file has the expected content before overwriting + verify_file_in_mpq_has_content(binary_path, target_file, "cats.txt", { "This is a file about cats." }) + + + # Create new test files on the fly + test_file = script_dir / "data" / "cats.txt" + test_file.write_text("This file is suddenly about dogs.") + + result = subprocess.run( + [str(binary_path), "add", str(test_file), str(target_file), "--overwrite"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + output_lines = set(result.stdout.splitlines()) + expected_stdout_output = { + "[+] File for locale 0 already exists in MPQ archive: cats.txt - Overwriting...", + "[+] Adding file for locale 0: cats.txt", + } + assert output_lines == expected_stdout_output, f"Unexpected output: {output_lines}" + + output_lines = set(result.stderr.splitlines()) + expected_stderr_output = set() + assert output_lines == expected_stderr_output, f"Unexpected output: {output_lines}" + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + + # Verify that file has the expected content after overwriting + verify_file_in_mpq_has_content(binary_path, target_file, "cats.txt", { "This file is suddenly about dogs." }) + + +def test_add_nonexisting_file_with_overwrite_should_succeed(binary_path, generate_test_files): + """ + Test adding a non-existing file to MPQ archive with the overwrite flag. + This test checks: + - If the application correctly adds a non-existing file to an MPQ archive with the overwrite flag set. + """ + _ = generate_test_files + script_dir = Path(__file__).parent + target_file = script_dir / "data" / "files.mpq" + + # Start by creating an MPQ archive for this test + create_mpq_archive_for_test(binary_path, script_dir) + + + # Create new test files on the fly + test_file = script_dir / "data" / "test.txt" + test_file.write_text("This file is newly added.") + + result = subprocess.run( + [str(binary_path), "add", str(test_file), str(target_file), "--overwrite"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + output_lines = set(result.stdout.splitlines()) + expected_stdout_output = { + "[+] Adding file for locale 0: test.txt", + } + assert output_lines == expected_stdout_output, f"Unexpected output: {output_lines}" + + output_lines = set(result.stderr.splitlines()) + expected_stderr_output = set() + assert output_lines == expected_stderr_output, f"Unexpected output: {output_lines}" + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + + + # Verify that file has the expected content + verify_file_in_mpq_has_content(binary_path, target_file, "test.txt", { "This file is newly added." }) + + def test_create_mpq_with_illegal_locale(binary_path, generate_test_files): """ Test MPQ file addition with illegal locale. @@ -217,3 +420,16 @@ def verify_archive_file_content(binary_path, test_file, expected_output): assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" assert output_lines == expected_output, f"Unexpected output: {output_lines}" + +def verify_file_in_mpq_has_content(binary_path, mpq_archive, file_name, expected_content): + result = subprocess.run( + [str(binary_path), "read", file_name, str(mpq_archive)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Remove empty lines from output, Windows adds an extra empty line + output_lines = set(line for line in result.stdout.splitlines() if line.strip() != "") + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert output_lines == expected_content, f"Unexpected output: {output_lines}"