Skip to content
Open
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
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 20 additions & 12 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> listProperties;
// CLI: add
std::string baseDirInArchive = "default"; // add
bool addOverwrite = false;
// CLI: extract
bool extractKeepFolderStructure = false;
// CLI: create
Expand All @@ -41,6 +42,7 @@ int main(int argc, char **argv) {
// CLI: list
bool listDetailed = false;
bool listAll = false;
std::vector<std::string> listProperties;
// CLI: verify
bool verifyPrintSignature = false;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}

Expand Down
14 changes: 11 additions & 3 deletions src/mpq.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,14 @@ 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)) {
std::cerr << "[!] File doesn't exist on disk: " << localFile << std::endl;
Expand All @@ -187,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<int32_t>(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);
Expand All @@ -214,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(),
Expand Down
2 changes: 1 addition & 1 deletion src/mpq.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>& propertiesToPrint);
char* ReadFile(HANDLE hArchive, const char *szFileName, unsigned int *fileSize, LCID preferredLocale);
Expand Down
216 changes: 216 additions & 0 deletions test/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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}"