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
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A command line tool to create, add, remove, list, extract, read, and verify MPQ

**This is a command-line tool, designed for automation and built with the Unix philosophy in mind.** It is designed to work seamlessly with other command-line tools, supporting piping, redirection, and integration into shell scripts and workflows. For example:

- Run one command to create an MPQ archive from a directory of files
- Run one command to create an MPQ archive from a directory of files or a single file
- Run one command to list all files in an MPQ archive
- Pipe the output to `grep` or other tools to search, filter, or process files
- Redirect output to files or other commands for further automation
Expand Down Expand Up @@ -72,7 +72,7 @@ The `mpqcli` program has the following subcommands:
- `version`: Print the tool version number
- `about`: Print information about the tool
- `info`: Print information about MPQ archive properties
- `create`: Create an MPQ archive from a target directory
- `create`: Create an MPQ archive from a target directory or a single file
- `add`: Add a file to an existing MPQ archive
- `remove`: Remove a file from an existing MPQ archive
- `list`: List files in a target MPQ archive
Expand Down Expand Up @@ -127,6 +127,16 @@ mpqcli create <target_directory>

The default mode of operation for the `create` subcommand is to take everything from the "target" directory (and below) and recursively add it to the archive. The directory structure is retained. Windows-style backslash path separators are used (`\`), as per the observed behavior in most MPQ archives.

### Create an MPQ archive from a single file

Create an MPQ file from a single file. Automatically adds `(listfile)` to the archive.

```
mpqcli create <target_file>
```

This will put the given file in the root of the MPQ archive. By optionally providing a path in the `--name-in-archive` parameter, the name that the file has in the MPQ archive can be changed, and it can be put in a directory.

### Create an MPQ archive using a specific version

Support for creating an MPQ archive version 1 or version 2 by using the `-v` or `--version` argument.
Expand Down
14 changes: 8 additions & 6 deletions src/helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,22 @@ std::string WindowsifyFilePath(const fs::path &path) {
return filePath;
}

int32_t CalculateMpqMaxFileValue(const std::string &directory) {
int32_t CalculateMpqMaxFileValue(const std::string &path) {
int32_t fileCount = 0;

// Determine number of files in target directory, recusively
for (const auto &entry : fs::recursive_directory_iterator(directory)) {
if (fs::is_regular_file(entry.path())) {
++fileCount;
// Determine the number of files in the target directory, recusively
if (!fs::is_regular_file(path)) {
for (const auto &entry: fs::recursive_directory_iterator(path)) {
if (fs::is_regular_file(entry.path())) {
++fileCount;
}
}
}

// Always add 3 for "special" files
fileCount += 3;

// Based on file count, determine max number of files an MPQ archive can hold
// Based on file count, determine the max number of files an MPQ archive can hold
// We always have a minimum of 32
// Anything over is rounded up to the closest power of 2
// For example: 64, 128, 256
Expand Down
2 changes: 1 addition & 1 deletion src/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace fs = std::filesystem;
std::string FileTimeToLsTime(int64_t fileTime);
std::string NormalizeFilePath(const fs::path &path);
std::string WindowsifyFilePath(const fs::path &path);
int32_t CalculateMpqMaxFileValue(const std::string &directory);
int32_t CalculateMpqMaxFileValue(const std::string &path);
int32_t NextPowerOfTwo(int32_t n);
void PrintAsBinary(const char* buffer, uint32_t size);

Expand Down
29 changes: 23 additions & 6 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ int main(int argc, char **argv) {
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
Expand Down Expand Up @@ -84,16 +85,17 @@ int main(int argc, char **argv) {
->check(CLI::IsMember(validInfoProperties));

// Subcommand: Create
CLI::App *create = app.add_subcommand("create", "Create an MPQ archive from target directory");
create->add_option("target", baseTarget, "Target directory")
CLI::App *create = app.add_subcommand("create", "Create an MPQ archive from target file or directory");
create->add_option("target", baseTarget, "Directory or file to put in MPQ archive")
->required()
->check(CLI::ExistingDirectory);
->check(CLI::ExistingPath);
create->add_option("-o,--output", baseOutput, "Output MPQ archive");
create->add_flag("-s,--sign", createSignArchive, "Sign the MPQ archive (default false)");
create->add_option("-v,--version", createMpqVersion, "Set the MPQ archive version (default 1)")
->check(CLI::Range(1, 2));
create->add_option("--locale", baseLocale, "Locale to use for added files")
->check(LocaleValid);
create->add_option("-n,--name-in-archive", baseNameInArchive, "Filename inside MPQ archive");

// Subcommand: Add
CLI::App *add = app.add_subcommand("add", "Add a file to an existing MPQ archive");
Expand Down Expand Up @@ -199,6 +201,10 @@ int main(int argc, char **argv) {

// Handle subcommand: Create
if (app.got_subcommand(create)) {
if (!fs::is_regular_file(baseTarget) && baseNameInArchive != "default") {
std::cerr << "[!] Cannot specify --name-in-archive when adding a directory." << std::endl;
return 1;
}
fs::path outputFilePath;
if (baseOutput != "default") {
outputFilePath = fs::absolute(baseOutput);
Expand All @@ -217,8 +223,19 @@ int main(int argc, char **argv) {
HANDLE hArchive = CreateMpqArchive(outputFile, fileCount, createMpqVersion);
if (hArchive) {
LCID locale = LangToLocale(baseLocale);
AddFiles(hArchive, baseTarget, locale);

if (fs::is_regular_file(baseTarget)) {
// Default: use the filename as path, saves file to root of MPQ
fs::path filePath = fs::path(baseTarget);
std::string archivePath = filePath.filename().u8string();
if (baseNameInArchive != "default") { // Optional: specified filename inside archive
filePath = fs::path(baseNameInArchive);
archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ
}

AddFile(hArchive, baseTarget, archivePath, locale);
} else {
AddFiles(hArchive, baseTarget, locale);
}
if (createSignArchive) {
SignMpqArchive(hArchive);
}
Expand Down Expand Up @@ -327,7 +344,7 @@ int main(int argc, char **argv) {

uint32_t fileSize;
char* fileContent = ReadFile(hArchive, baseFile.c_str(), &fileSize, locale);
if (fileContent == NULL) {
if (fileContent == nullptr) {
return 1;
}

Expand Down
92 changes: 92 additions & 0 deletions test/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,98 @@ def test_create_mpq_already_exists(binary_path, generate_test_files):
assert result.returncode == 1, f"mpqcli failed with error: {result.stderr}"


def test_create_mpq_from_file(binary_path, generate_test_files):
"""
Test MPQ archive creation from a file rather than a directory.

This test checks:
- MPQ archive creation from a file.
"""
_ = generate_test_files
script_dir = Path(__file__).parent

# Create a new test file on the fly
test_file = script_dir / "data" / "test.txt"
test_file.write_text("This is a test file for MPQ addition.")

target_file = test_file.with_suffix(".mpq")
# Remove the target file if it exists
# Testing creation when file exists is handled:
# test_create_mpq_already_exists
target_file.unlink(missing_ok=True)
result = subprocess.run(
[str(binary_path), "create", str(test_file), "--output", str(target_file)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)

assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}"
assert target_file.exists(), f"MPQ file was not created)"
assert target_file.stat().st_size > 0, f"MPQ file is empty)"

verify_archive_file_content(binary_path, target_file, {"enUS test.txt"})


def test_create_mpq_from_file_with_nameinarchive_parameter(binary_path, generate_test_files):
"""
Test MPQ archive creation from a file, with the --name-in-archive parameter.
This test checks:
- MPQ archive creation from a file.
- That the --name-in-archive parameter is correctly handled.
"""
_ = generate_test_files
script_dir = Path(__file__).parent

# Create a new test file on the fly
test_file = script_dir / "data" / "test.txt"
test_file.write_text("This is a test file for MPQ addition.")

target_file = test_file.with_suffix(".mpq")
# Remove the target file if it exists
# Testing creation when file exists is handled:
# test_create_mpq_already_exists
target_file.unlink(missing_ok=True)
result = subprocess.run(
[str(binary_path), "create", str(test_file), "--output", str(target_file), "--name-in-archive", "messages\\important.txt"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)

assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}"
assert target_file.exists(), f"MPQ file was not created)"
assert target_file.stat().st_size > 0, f"MPQ file is empty)"

verify_archive_file_content(binary_path, target_file, {"enUS messages\\important.txt"})


def test_create_mpq_from_directory_with_nameinarchive_parameter(binary_path, generate_test_files):
"""
Test MPQ archive creation from a directory, with the --name-in-archive parameter.
This test checks:
- No MPQ archive is created.
"""
_ = generate_test_files
script_dir = Path(__file__).parent
target_dir = script_dir / "data" / "files"

target_file = target_dir.with_name("files_test").with_suffix(".mpq")
# Remove the target file if it exists
# Testing creation when file exists is handled:
# test_create_mpq_already_exists
target_file.unlink(missing_ok=True)
result = subprocess.run(
[str(binary_path), "create", str(target_dir), "--output", str(target_file), "--name-in-archive", "messages\\important.txt"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)

assert result.returncode == 1, f"mpqcli failed with error: {result.stderr}"
assert not target_file.exists(), f"MPQ file was created)"


def test_create_mpq_with_illegal_locale(binary_path, generate_test_files):
"""
Test MPQ file creation with illegal locale.
Expand Down