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
27 changes: 10 additions & 17 deletions docs/commands/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,36 +66,29 @@ changed files need to be re-added.
$ mpqcli add wow-patch.mpq textures/ --update --overwrite
[~] Skipping unchanged file: Creature\Bear\Bear.blp
[+] Adding file: Creature\Wolf\Wolf.blp
[*] 1 files added, 1 files skipped
[*] For textures: 1 files added, 1 files skipped, 0 files failed.
```

Note: the skip check is size-based only. Files with the same size but different content
are not detected as changed. If precise change detection matters, pass `--overwrite`
without `--update` to unconditionally replace every file.

## Control where a single file is stored
## Control where files are stored

These options apply to single-file adds only.

Add a file under a specific name using `-f` or `--filename-in-archive`:

```bash
$ mpqcli add wow-patch.mpq fta.txt --filename-in-archive "alliance.txt"
[+] Adding file: alliance.txt
```

Add a file into a subdirectory using `-d` or `--directory-in-archive`:
For single files, one can specify both directory and filename in one step using `-p` or `--path`:

```bash
$ mpqcli add wow-patch.mpq fts.txt --directory-in-archive texts
[+] Adding file: texts\fts.txt
$ mpqcli add wow-patch.mpq fts.txt --path "texts\swarm.txt"
[+] Adding file: texts\swarm.txt
```

Specify both directory and filename in one step using `-p` or `--path`:
For directories, one can specify the base directory using `-p` or `--path`:

```bash
$ mpqcli add wow-patch.mpq fts.txt --path "texts\swarm.txt"
[+] Adding file: texts\swarm.txt
$ mpqcli add wow-patch.mpq textures/ --path textures
[+] Adding file: textures\Creature\Bear\Bear.blp
[+] Adding file: textures\Creature\Wolf\Wolf.blp
[*] For textures: 2 files added, 0 files skipped, 0 files failed.
```

## Overwrite existing files
Expand Down
17 changes: 16 additions & 1 deletion docs/commands/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,22 @@ Create an MPQ file from a single file.
$ mpqcli create --game diablo2 <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.
## Control where files are stored

For single files, one can specify both directory and filename in one step using `-p` or `--path`:

```bash
$ mpqcli create fts.txt --output archive.mpq --path "texts\swarm.txt"
[+] Adding file: texts\swarm.txt
```

For directories, one can specify the base directory using `-p` or `--path`:

```bash
$ mpqcli create textures/ --output archive.mpq --path textures
[+] Adding file: textures\Creature\Bear\Bear.blp
[+] Adding file: textures\Creature\Wolf\Wolf.blp
```

## Create and sign an MPQ archive

Expand Down
95 changes: 32 additions & 63 deletions src/commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@

namespace fs = std::filesystem;

std::string ResolveArchiveName(const std::string &f, const std::optional<std::string> &path,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might move this to helpers.cpp later. Will have a closer look after merging - but not a big deal either way

const bool treatAsDirectory = false) {
fs::path filePath = path.value_or(fs::path(f).filename().u8string());
if (treatAsDirectory) {
const std::string filename = fs::path(f).filename().u8string();
filePath = path.value_or("") / fs::path(filename);
}
return WindowsifyFilePath(filePath);
}

int HandleVersion() {
std::cout << MPQCLI_VERSION << "-" << GIT_COMMIT_HASH << std::endl;
return 0;
Expand Down Expand Up @@ -42,18 +52,13 @@ int HandleInfo(const std::string &target, const std::optional<std::string> &prop
return 0;
}

int HandleCreate(const std::string &target, const std::optional<std::string> &nameInArchive,
int HandleCreate(const std::string &target, const std::optional<std::string> &path,
const std::optional<std::string> &output, bool signArchive,
const std::optional<std::string> &locale,
const std::optional<std::string> &gameProfile, int32_t mpqVersion,
int64_t streamFlags, int64_t sectorSize, int64_t rawChunkSize, int64_t fileFlags1,
int64_t fileFlags2, int64_t fileFlags3, int64_t attrFlags, int64_t fileDwFlags,
int64_t fileDwCompression, int64_t fileDwCompressionNext) {
if (!fs::is_regular_file(target) && nameInArchive.has_value()) {
std::cerr << "[!] Cannot specify --name-in-archive when adding a directory." << std::endl;
return 1;
}

fs::path outputFilePath;
if (output.has_value()) {
outputFilePath = fs::absolute(output.value());
Expand Down Expand Up @@ -112,18 +117,19 @@ int HandleCreate(const std::string &target, const std::optional<std::string> &na
if (fileDwCompressionNext >= 0)
addOverrides.dwCompressionNext = static_cast<DWORD>(fileDwCompressionNext);

if (fs::is_regular_file(target)) {
// Default: use the filename as path, saves file to root of MPQ
fs::path filePath = fs::path(target);
std::string archivePath = filePath.filename().u8string();
if (nameInArchive.has_value()) { // Optional: specified filename inside archive
filePath = fs::path(nameInArchive.value());
archivePath = WindowsifyFilePath(filePath); // Normalise path for MPQ
}
if (fs::is_directory(target)) {
const std::string prefix = path.value_or("");
result |= AddFiles(hArchive, target, prefix, lcid, gameRules, addOverrides);

} else if (fs::is_regular_file(target)) {
std::string archivePath = ResolveArchiveName(target, path);
result |= AddFile(hArchive, target, archivePath, lcid, gameRules, addOverrides);

} else {
result |= AddFiles(hArchive, target, "", lcid, gameRules, addOverrides);
std::cerr << "[!] Not a file or directory: " << target << std::endl;
result |= 1;
}

if (signArchive) {
SignMpqArchive(hArchive);
}
Expand All @@ -137,9 +143,7 @@ int HandleCreate(const std::string &target, const std::optional<std::string> &na
}

int HandleAdd(const std::vector<std::string> &files, const std::string &target,
const std::optional<std::string> &path,
const std::optional<std::string> &dirInArchive,
const std::optional<std::string> &nameInArchive, bool overwrite, bool update,
const std::optional<std::string> &path, bool overwrite, bool update,
const std::optional<std::string> &locale,
const std::optional<std::string> &gameProfile, int64_t fileDwFlags,
int64_t fileDwCompression, int64_t fileDwCompressionNext) {
Expand All @@ -149,37 +153,6 @@ int HandleAdd(const std::vector<std::string> &files, const std::string &target,
return 1;
}

bool hasDirectory = false;
for (const auto &f : files) {
if (fs::is_directory(f)) {
hasDirectory = true;
break;
}
}
bool multipleInputs = files.size() > 1;

if ((hasDirectory || multipleInputs) &&
(dirInArchive.has_value() || nameInArchive.has_value())) {
std::cerr << "[!] --directory-in-archive and --filename-in-archive are only valid when "
"adding a single file."
<< std::endl;
CloseMpqArchive(hArchive);
return 1;
}
if (multipleInputs && path.has_value()) {
std::cerr << "[!] --path is only valid when adding a single file or directory."
<< std::endl;
CloseMpqArchive(hArchive);
return 1;
}
if (path.has_value() && (dirInArchive.has_value() || nameInArchive.has_value())) {
std::cerr << "[!] Cannot specify --path together with --name-in-archive or "
"--directory-in-archive."
<< std::endl;
CloseMpqArchive(hArchive);
return 1;
}

LCID lcid = locale.has_value() ? LangToLocale(locale.value()) : defaultLocale;

GameProfile profile;
Expand All @@ -197,6 +170,14 @@ int HandleAdd(const std::vector<std::string> &files, const std::string &target,
if (fileDwCompressionNext >= 0)
addOverrides.dwCompressionNext = static_cast<DWORD>(fileDwCompressionNext);

bool hasDirectory = false;
for (const auto &f : files) {
if (fs::is_directory(f)) {
hasDirectory = true;
break;
}
}

if (update && !hasDirectory) {
std::cerr << "[!] Warning: --update is only meaningful when adding a directory"
<< std::endl;
Expand All @@ -215,20 +196,8 @@ int HandleAdd(const std::vector<std::string> &files, const std::string &target,
AddFiles(hArchive, f, prefix, lcid, gameRules, addOverrides, overwrite, update);

} else if (fs::is_regular_file(f)) {
fs::path filePath = fs::path(f);
std::string archivePath = filePath.filename().u8string();

if (path.has_value()) {
filePath = fs::path(path.value());
archivePath = WindowsifyFilePath(filePath);
} else if (dirInArchive.has_value() || nameInArchive.has_value()) {
std::string effectiveDir =
dirInArchive.value_or(fs::path(f).parent_path().u8string());
std::string effectiveName = nameInArchive.value_or(archivePath);
filePath = fs::path(effectiveDir) / fs::path(effectiveName);
archivePath = WindowsifyFilePath(filePath);
}

const bool treatAsDirectory = hasDirectory || files.size() > 1;
std::string archivePath = ResolveArchiveName(f, path, treatAsDirectory);
result |= AddFile(hArchive, f, archivePath, lcid, gameRules, addOverrides, overwrite);

} else {
Expand Down
6 changes: 2 additions & 4 deletions src/commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@
int HandleVersion();
int HandleAbout();
int HandleInfo(const std::string &target, const std::optional<std::string> &property);
int HandleCreate(const std::string &target, const std::optional<std::string> &nameInArchive,
int HandleCreate(const std::string &target, const std::optional<std::string> &path,
const std::optional<std::string> &output, bool signArchive,
const std::optional<std::string> &locale,
const std::optional<std::string> &gameProfile, int32_t mpqVersion,
int64_t streamFlags, int64_t sectorSize, int64_t rawChunkSize, int64_t fileFlags1,
int64_t fileFlags2, int64_t fileFlags3, int64_t attrFlags, int64_t fileDwFlags,
int64_t fileDwCompression, int64_t fileDwCompressionNext);
int HandleAdd(const std::vector<std::string> &files, const std::string &target,
const std::optional<std::string> &path,
const std::optional<std::string> &dirInArchive,
const std::optional<std::string> &nameInArchive, bool overwrite, bool update,
const std::optional<std::string> &path, bool overwrite, bool update,
const std::optional<std::string> &locale,
const std::optional<std::string> &gameProfile, int64_t fileDwFlags,
int64_t fileDwCompression, int64_t fileDwCompressionNext);
Expand Down
33 changes: 15 additions & 18 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@ int main(int argc, char **argv) {

// CLI: base
// These are reused in multiple subcommands
// clang-format off: preserve column-aligned flag-to-char mappings
std::string baseTarget; // all subcommands
std::string baseFile; // extract, read
std::optional<std::string> baseLocale; // add, create, extract, read, remove
std::optional<std::string> baseNameInArchive; // add, create
std::optional<std::string> basePath; // add, create
std::optional<std::string> baseOutput; // create, extract
std::optional<std::string> baseListfileName; // extract, list
std::optional<std::string> baseGameProfile; // add, create
int64_t fileDwFlags = -1; // add, create
int64_t fileDwCompression = -1; // add, create
int64_t fileDwCompressionNext = -1; // add, create
// clang-format on
// CLI: info
std::optional<std::string> infoProperty;
// CLI: add
std::optional<std::string> addBasePath;
std::optional<std::string> addBaseDirInArchive;
bool addOverwrite = false;
bool addUpdate = false;
std::vector<std::string> addFiles;
Expand Down Expand Up @@ -90,7 +90,8 @@ int main(int argc, char **argv) {
create->add_option("target", baseTarget, "Directory or file to put in MPQ archive")
->required()
->check(CLI::ExistingPath);
create->add_option("-n,--name-in-archive", baseNameInArchive, "Filename inside MPQ archive");
create->add_option("-p,--path", basePath,
"Archive path for a single file, or prefix for a directory");
create->add_option("-o,--output", baseOutput, "Output MPQ archive");
create->add_flag("-s,--sign", createSignArchive, "Sign the MPQ archive (default false)");
create->add_option("--locale", baseLocale, "Locale to use for added files")->check(LocaleValid);
Expand Down Expand Up @@ -140,12 +141,8 @@ int main(int argc, char **argv) {
"Files or directories to add; pass - to read paths from stdin")
->required()
->expected(-1);
add->add_option("-p,--path", addBasePath,
"Archive path for a single file, or prefix for a directory add");
add->add_option("-d,--directory-in-archive", addBaseDirInArchive,
"Directory to put file inside within MPQ archive (single file only)");
add->add_option("-f,--filename-in-archive", baseNameInArchive,
"Filename inside MPQ archive (single file only)");
add->add_option("-p,--path", basePath,
"Archive path for a single file, or prefix for a directory");
add->add_flag("-w,--overwrite", addOverwrite, "Overwrite file if it already is in MPQ archive");
add->add_flag("-u,--update", addUpdate,
"Skip files whose archived size matches the on-disk size (directory add only)");
Expand Down Expand Up @@ -240,11 +237,11 @@ int main(int argc, char **argv) {
}

if (app.got_subcommand(create)) {
return HandleCreate(baseTarget, baseNameInArchive, baseOutput, createSignArchive,
baseLocale, baseGameProfile, createMpqVersion, createStreamFlags,
createSectorSize, createRawChunkSize, createFileFlags1,
createFileFlags2, createFileFlags3, createAttrFlags, fileDwFlags,
fileDwCompression, fileDwCompressionNext);
return HandleCreate(baseTarget, basePath, baseOutput, createSignArchive, baseLocale,
baseGameProfile, createMpqVersion, createStreamFlags, createSectorSize,
createRawChunkSize, createFileFlags1, createFileFlags2,
createFileFlags3, createAttrFlags, fileDwFlags, fileDwCompression,
fileDwCompressionNext);
}

if (app.got_subcommand(add)) {
Expand All @@ -259,9 +256,9 @@ int main(int argc, char **argv) {
resolvedAddFiles.push_back(f);
}
}
return HandleAdd(resolvedAddFiles, baseTarget, addBasePath, addBaseDirInArchive,
baseNameInArchive, addOverwrite, addUpdate, baseLocale, baseGameProfile,
fileDwFlags, fileDwCompression, fileDwCompressionNext);
return HandleAdd(resolvedAddFiles, baseTarget, basePath, addOverwrite, addUpdate,
baseLocale, baseGameProfile, fileDwFlags, fileDwCompression,
fileDwCompressionNext);
}

if (app.got_subcommand(remove)) {
Expand Down
Loading