Introduction
A command-line tool to create, add, remove, list, extract, read, rename, verify, and compact MPQ archives using the StormLib library.
Warning: This project is under active development and will change functionality between released versions until version 1.0.0.
Overview
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 or a single file
- Run one command to list all files in an MPQ archive
- Pipe the output to
grepor other tools to search, filter, or process files - Redirect output to files or other commands for further automation
If you require an MPQ tool with a graphical interface (GUI), I would recommend using Ladik’s MPQ Editor.
Installation
Precompiled binaries
Pre-built binaries are available for Linux and Windows.
Linux/WSL:
$ curl -fsSL https://github.com/thegraydot/mpqcli/releases/latest/download/install.sh | bash
Microsoft Windows:
PS> Invoke-WebRequest -Uri https://github.com/thegraydot/mpqcli/releases/latest/download/install.ps1 -OutFile install.ps1
PS> .\install.ps1
Both installers put mpqcli on the system path and may ask for elevation; pass --user on Linux or -User on Windows to install for the current user instead. Each release also ships checksums.txt, signed with cosign: the installer verifies the SHA-256 of the binary always, and the signature when cosign is installed.
Check the latest release with binaries.
Docker image
The Docker image for mpqcli is hosted on GitHub Container Registry (GHCR). It provides a lightweight and portable way to use mpqcli without needing to build or download a binary.
To download the latest version of the mpqcli Docker image, run:
$ docker pull ghcr.io/thegraydot/mpqcli:latest
You can run mpqcli commands directly using the Docker container. For example:
$ docker run --rm ghcr.io/thegraydot/mpqcli:latest version
To use local files, mount a directory from your host system at /data. That is the container’s working directory, so paths are relative to the mounted directory. The following example lists example.mpq from the present working directory:
$ docker run --rm -v "$PWD":/data ghcr.io/thegraydot/mpqcli:latest list example.mpq
The container runs as root, so a file it creates or extracts is owned by root on the host. To have the output owned by you, run the container as your own user:
$ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/data ghcr.io/thegraydot/mpqcli:latest create example.mpq files/
A shell function makes that the default for everyday use:
mpqcli() { docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/data ghcr.io/thegraydot/mpqcli:latest "$@"; }
Under rootless Docker or Podman the container’s root already maps to your own user, so run without --user there; on Podman, --userns=keep-id is the equivalent of the flag above.
Commands
Many of the command examples use the MPQ archive file, named wow-patch.mpq, from a original (Vanilla) World of Warcraft patch file, named WoW-1.10.0-to-1.10.1-enGB-patch.zip. If you want to replicate these examples, you can download the wow-patch.mpq file from the Internet Archive.
The mpqcli program has the following subcommands:
| Command | Description |
|---|---|
version | Print the tool version number |
about | Print information about the tool |
completion | Print shell completion script to stdout |
info | Print information about MPQ archive properties |
create | Create an MPQ archive from files and directories |
add | Add a file to an existing MPQ archive |
remove | Remove a file from an existing MPQ archive |
rename | Rename a file in an existing MPQ archive |
list | List files in an MPQ archive |
extract | Extract files from an MPQ archive |
read | Read a specific file to stdout |
verify | Verify an MPQ archive signature |
compact | Compact (defragment) an MPQ archive |
version
Print the tool version number.
$ mpqcli version
0.10.0-30601b2cdf0c1a1c918709bca2e7416ab88bfa18
about
Print information about the tool.
$ mpqcli about
Name: mpqcli
Version: 0.10.0-30601b2cdf0c1a1c918709bca2e7416ab88bfa18
Author: Thomas Laurenson
License: MIT
GitHub: https://github.com/thegraydot/mpqcli
Dependencies:
- StormLib (https://github.com/ladislav-zezula/StormLib)
- CLI11 (https://github.com/CLIUtils/CLI11)
completion
Print a shell completion script to stdout.
Supported shells
- bash
- zsh
- fish
- PowerShell
Bash
Write the completion script to a file and source it from your shell profile.
$ mpqcli completion bash > ~/.bash_completion.d/mpqcli
$ source ~/.bash_completion.d/mpqcli
Alternatively, write the script to a system-wide completions directory (requires root):
$ mpqcli completion bash | sudo tee /etc/bash_completion.d/mpqcli > /dev/null
Zsh
Write the completion script to a directory that is on your $fpath.
$ mpqcli completion zsh > "${fpath[1]}/_mpqcli"
Fish
Write the completion script to the fish completions directory.
$ mpqcli completion fish > ~/.config/fish/completions/mpqcli.fish
PowerShell
Append the completion script to your PowerShell profile so it loads automatically.
PS> mpqcli completion powershell >> $PROFILE
info
Print information about MPQ archive properties.
Print information about an MPQ archive
The info subcommand prints a list of useful information (property keys and values) of an MPQ archive.
$ mpqcli info wow-patch.mpq
Archive size: 1798918
File count: 65
Format version: 1
Header offset: 0
Header size: 32
Max files: 128
Signature type: Weak
Print one specific MPQ archive property
The info subcommand supports the following properties:
archive-sizefile-countformat-versionheader-offsetheader-sizemax-filessignature-type
You can use the -q or --property argument with the info subcommand to print just the value of a specific property. This can be useful for automation, for example, to determine the signature type of a directory of MPQ archives.
$ mpqcli info --property file-count wow-patch.mpq
65
create
Create an MPQ archive from files and directories.
Create an MPQ archive from a directory
Name the new archive first, then the directory to put in it. Automatically adds (listfile) to the archive, and will skip this file if it exists in the directory.
$ mpqcli create archive.mpq <directory>
The default mode of operation for the create subcommand is to take everything from the directory (and below) and recursively add it to the archive. The directory’s contents go at the root of the archive, and the structure below it is retained. Windows-style backslash path separators are used (\), as per the observed behaviour in most MPQ archives.
The archive must not already exist; create never overwrites a file. If an input is missing or any file cannot be added, no archive is created.
Create an MPQ archive from several files and directories
Name as many files and directories as needed after the archive. Each file is stored under its own name, and each directory’s contents go at the root of the archive. If two inputs hold a file with the same name, no archive is created.
$ mpqcli create archive.mpq readme.txt textures/ sounds/
Pass - in place of the files to read their paths from stdin, one per line:
$ find . -maxdepth 1 -name '*.txt' | mpqcli create texts.mpq -
As with files named on the command line, each file is stored under its own name, without the folders in its path. To keep the folder structure, name the directory instead.
Create an MPQ archive for a specific game
Target a specific game version by using the -g or --game argument. This will automatically set the correct archive format version and settings, although they can be overridden.
$ mpqcli create -g starcraft archive.mpq <directory>
$ mpqcli create --game wow-wotlk \
--sector-size 16384 \
--format-version 3 archive.mpq <directory>
Create an MPQ archive from a single file
Create an MPQ file from a single file.
$ mpqcli create --game diablo2 archive.mpq <file>
Control where files are stored
For a single file, one can specify both directory and filename in one step using -p or --path:
$ mpqcli create archive.mpq fts.txt --path "texts\swarm.txt"
[+] Adding file: texts\swarm.txt
For directories, or several inputs, -p or --path is a prefix that every file is stored under:
$ mpqcli create archive.mpq textures/ --path textures
[+] Adding file: textures\Creature\Bear\Bear.blp
[+] Adding file: textures\Creature\Wolf\Wolf.blp
Create and sign an MPQ archive
Use the -s or --sign argument to cryptographically sign an MPQ archive with the Blizzard weak signature.
$ mpqcli create --format-version 1 --sign archive.mpq <directory>
Create an MPQ archive with a given locale
Use the -e or --locale argument to specify the locale that all added files will have in the archive. Note that subsequent added files will have the default locale unless the --locale argument is specified again.
$ mpqcli create archive.mpq <directory> --locale koKR
add
Add one or more files to an existing MPQ archive.
The archive is always the first positional argument, followed by one or more files or directories. All inputs are processed in a single open/close cycle, which is significantly faster than calling add once per file.
Add a single file
$ echo "For The Horde" > fth.txt
$ mpqcli add wow-patch.mpq fth.txt
[+] Adding file: fth.txt
Add multiple files at once
Pass more than one file path after the archive. The archive is opened once for all files.
$ mpqcli add wow-patch.mpq fth.txt fta.txt fts.txt
[+] Adding file: fth.txt
[+] Adding file: fta.txt
[+] Adding file: fts.txt
Add files from stdin
Pass - as the file argument to read paths from standard input. This works with find, ls, or any other tool that produces file paths.
$ find . -name "*.blp" | mpqcli add wow-patch.mpq -
[+] Adding file: textures\Creature\Bear\Bear.blp
[+] Adding file: textures\Creature\Wolf\Wolf.blp
...
Add a directory
Pass a directory path to recursively add all files within it. The directory structure is preserved relative to the directory root.
$ mpqcli add wow-patch.mpq textures/
[+] Adding file: Creature\Bear\Bear.blp
[+] Adding file: Creature\Wolf\Wolf.blp
Use --path to add a prefix to every archived path:
$ mpqcli add wow-patch.mpq textures/ --path textures
[+] Adding file: textures\Creature\Bear\Bear.blp
[+] Adding file: textures\Creature\Wolf\Wolf.blp
Control where files are stored
For single files, one can specify both directory and filename in one step using -p or --path:
$ mpqcli add wow-patch.mpq fts.txt --path "texts\swarm.txt"
[+] Adding file: texts\swarm.txt
For directories, one can specify the base directory using -p or --path:
$ 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.
Replacing files that already exist
add never replaces an archived file unless you ask it to. Two flags control what happens when a file is already present, and they are mutually exclusive, passing both is an error, because “replace everything” and “replace only what changed” are contradictory requests.
| Flags | Behaviour when the file already exists |
|---|---|
| (neither) | Skip it and leave the archived copy alone |
-w, --overwrite | Replace it unconditionally |
-u, --update | Replace it only if the local file differs |
Default: skip
Without either flag, an existing file is skipped. This is a normal outcome, not an error, so the exit code is still 0:
$ mpqcli add wow-patch.mpq allegiance.txt
[!] File already exists in MPQ archive: allegiance.txt - Skipping...
[*] 1 file(s) already in the archive were skipped. Use --overwrite to replace them, or --update to replace only the ones that changed.
--overwrite: replace unconditionally
$ mpqcli add wow-patch.mpq allegiance.txt --overwrite
[~] File already exists in MPQ archive: allegiance.txt - Overwriting...
[+] Adding file: allegiance.txt
Add with a locale
Use -e or --locale to store the file under a specific locale. Files added without --locale use the default locale.
$ mpqcli add wow-patch.mpq allianz.txt --locale deDE
[+] Adding file for locale deDE: allianz.txt
Add with game-specific compression
Use -g or --game to apply the compression and encryption rules for a specific game.
$ mpqcli add archive.mpq khwhat1.wav --game warcraft2
[+] Adding file: khwhat1.wav
Replace only what changed with –update
The --update flag replaces a file only when the local copy differs from the archived one, and skips it otherwise. This is useful for incremental updates where only changed files need to be re-added. It applies to single files and directories alike, the comparison is always made per file.
The skip decision follows this chain:
- File size must match. If the sizes differ the file is always re-added.
- If the sizes match, the archive’s
(attributes)file is consulted:- Timestamp: if the archive stores file timestamps, the local file’s last-modification time is compared at one-second resolution. A match skips the file.
- MD5: if the timestamp did not match or is unavailable, and the archive stores MD5 checksums, the MD5 of the local file is computed and compared. A match skips the file.
- CRC32: if neither timestamp nor MD5 produced a match or was available, and the archive stores CRC32 checksums, those are compared. A match skips the file.
- No attributes: if the archive has no
(attributes)file, the file is always re-added even when sizes match, because no reliable content check is possible.
Note: a timestamp match alone skips the file, without comparing checksums. A file whose content changed but whose size and modification time were both preserved (for example by cp -p or tools that restore timestamps) will therefore not be detected as changed. This is the same trade-off tools like rsync make by default. If exact change detection matters, use --overwrite instead of --update to unconditionally replace every file.
$ mpqcli add wow-patch.mpq textures/ --update
[*] Skipping unchanged file: Creature\Bear\Bear.blp (MD5 matches)
[~] File already exists in MPQ archive: Creature\Wolf\Wolf.blp - Overwriting...
[+] Adding file: Creature\Wolf\Wolf.blp
[*] For textures: 1 files added, 1 files skipped, 0 files failed.
The same comparison applies when the target is a single file:
$ mpqcli add wow-patch.mpq allegiance.txt --update
[*] Skipping unchanged file: allegiance.txt (MD5 matches)
Flow chart of update method:
flowchart TD
A[Start] --> SM{File size matches}
SM -->|Yes| TM{Timestamps matches}
SM -->|No| U[Update file]
TM -->|Yes| S[Skip]
TM -->|No| MX{MD5 exists}
MX -->|Yes| MM{MD5 matches}
MX -->|No| CX{CRC32 exists}
MM -->|Yes| S
MM -->|No| U
CX -->|Yes| CM{CRC32 matches}
CX -->|No| U
CM -->|Yes| S
CM -->|No| U
%% Layout hint
MM ~~~ U
CM ~~~ S
remove
Remove one or more files from an existing MPQ archive.
The archive is always the first positional argument, followed by one or more archive paths to remove. All removals happen in a single open/close cycle.
Remove a single file
$ mpqcli remove wow-patch.mpq fth.txt
[-] Removing file: fth.txt
Remove multiple files at once
Pass more than one archive path after the archive argument:
$ mpqcli remove wow-patch.mpq fth.txt fta.txt fts.txt
[-] Removing file: fth.txt
[-] Removing file: fta.txt
[-] Removing file: fts.txt
Remove files from stdin
Pass - to read archive paths from standard input:
$ echo -e "fth.txt\nfta.txt" | mpqcli remove wow-patch.mpq -
[-] Removing file: fth.txt
[-] Removing file: fta.txt
Remove a file with a given locale
Use -e or --locale to remove only the copy stored under a specific locale. Without --locale,
the file stored under the default locale is removed.
$ mpqcli remove wow-patch.mpq alianza.txt --locale esES
[-] Removing file for locale esES: alianza.txt
rename
Rename a file in an existing MPQ archive without extracting and re-adding it.
The archive is the first positional argument, followed by the current and new archive paths.
This operation does not cause MPQ fragmentation, and thus it is not necessary to
compact the archive.
$ mpqcli rename wow-patch.mpq old-name.txt new-name.txt
[~] Renaming file: old-name.txt -> new-name.txt
Use -e or --locale to rename only the copy stored under a specific locale. Without --locale,
the file stored under the default locale is renamed.
$ mpqcli rename wow-patch.mpq old-name.txt new-name.txt --locale esES
[~] Renaming file for locale esES: old-name.txt -> new-name.txt
list
List files in an MPQ archive.
List all files in an MPQ archive
Pretty simple, list files in an MPQ archive. Useful to “pipe” to other tools, such as grep (see the Advanced Examples page).
$ mpqcli list wow-patch.mpq
BM_COKETENT01.BLP
Blizzard_CraftUI.xml
CreatureSoundData.dbc
...
Blizzard_CraftUI.lua
30ee7bd3959906e358eff01332cf045e.blp
realmlist.wtf
List all files with detailed output
Similar to the ls command with the -l and -a options, additional detailed information can be included with the list subcommand. The -a option includes printing “special” files used in MPQ archives including: (listfile), (attributes) and (signature).
$ mpqcli list -d -a wow-patch.mpq
88604 enUS 2006-03-29 02:02:37 BM_COKETENT01.BLP
243 enUS 2006-04-04 21:28:14 Blizzard_CraftUI.xml
388 enUS 2006-03-29 19:32:46 CreatureSoundData.dbc
...
184 enUS 2006-04-04 21:28:14 Blizzard_CraftUI.lua
44900 enUS 2006-03-29 02:01:02 30ee7bd3959906e358eff01332cf045e.blp
68 enUS 2006-04-07 00:58:44 realmlist.wtf
List specific properties
The list subcommand supports listing the following properties:
hash-index- Index in the hash table where the file entry is.name-hash1- The first hash of the file name.name-hash2- The second hash of the file name.name-hash3- 64-bit Jenkins hash of the file name, used for searching in the HET table.locale- Locale info of the file.file-index- Index in the file table of the file.byte-offset- Offset of the file in the MPQ, relative to the MPQ header.file-time- Timestamp of the file.file-size- Uncompressed file size of the file, in bytes.compressed-size- Compressed file size of the file, in bytes.encryption-key- Encryption key for the file.encryption-key-raw- Encryption key for the file.flags- File flags for the file within MPQ:i: File is Imploded (By PKWARE Data Compression Library).c: File is Compressed (By any of multiple methods).e: File is Encrypted.2: File is Encrypted with key v2.p: File is a Patch file.u: File is stored as a single Unit, rather than split into sectors.d: File is a Deletion marker. Used in MPQ patches, indicating that the file no longer exists.r: File has Sector CRC checksums for each sector. This is ignored if the file is not compressed or imploded.s: Present on STANDARD.SNP(signature).x: File exists; this is reset if the file is deleted.
You can use the -q or --property argument with the list subcommand to print the given properties. Many properties can be given, and they will be printed in the order given.
$ mpqcli list -d -a Patch_rt.mpq --property hash-index --property locale --property flags
...
10893 enUS ix glue\ScorePv\pMain.pcx
11173 enUS ix rez\gluAll.tbl
11174 deDE ix rez\gluAll.tbl
11175 esES ix rez\gluAll.tbl
11176 frFR ix rez\gluAll.tbl
11177 itIT ix rez\gluAll.tbl
11178 ptBR ix rez\gluAll.tbl
11726 enUS cx (attributes)
11884 enUS ix glue\ScoreTd\pMain.pcx
...
List all files with an external listfile
Older MPQ archives do not contain (complete) file paths of their content. By using the -l or --listfile argument, one can provide an external listfile that lists the content of the MPQ archive, so that the listed files will have the correct paths. Listfiles can be downloaded on Ladislav Zezula’s site.
$ mpqcli list -l /path/to/listfile StarDat.mpq
extract
Extract every file, or only the named files, from an MPQ archive.
Extract all files from an MPQ archive
The output will be saved in a folder with the same name as the MPQ archive, without the extension. Files keep the folder structure they have in the archive.
$ mpqcli extract wow-patch.mpq
[*] Extracted: BM_COKETENT01.BLP
[*] Extracted: Blizzard_CraftUI.xml
[*] Extracted: CreatureSoundData.dbc
...
[*] Extracted: Interface/FrameXML/BlizzardFrameXML.lua
[*] Extracted: 30ee7bd3959906e358eff01332cf045e.blp
[*] Extracted: realmlist.wtf
Extract all files to an output directory
Extract files to a specific output directory, which will be created if it doesn’t already exist. In this example, patch-1.10 is the user-specified output directory.
$ mpqcli extract -o patch-1.10 wow-patch.mpq
Extract all files without the folder structure
Use the -j or --flat flag to write every file straight into the output directory, keeping only the filename. If two files would end up with the same name, nothing is extracted.
$ mpqcli extract --flat wow-patch.mpq
[*] Extracted: BlizzardFrameXML.lua
[*] Extracted: BlizzardFrameXML.xml
Extract all files with an external listfile
Older MPQ archives do not contain (complete) file paths of their content. By providing an external listfile that lists the content of the MPQ archive, the extracted files will have the correct names and paths. Listfiles can be downloaded on Ladislav Zezula’s site.
$ mpqcli extract -l path/to/listfile War2Dat.mpq
The listfile only applies when extracting every file, so it cannot be combined with named files.
Extract specific files
Name one or more files after the archive to extract only those. A file nested in a directory needs its full path in the archive, and is extracted with its folders; add --flat to drop them. Similar to the examples above, you can use the -o argument to specify the output directory.
$ mpqcli extract "World of Warcraft_1.12.1.5875/Data/base.MPQ" \
"Documentation\Layout\Greeting.html"
[*] Extracted: Documentation/Layout/Greeting.html
Extract files named on stdin
Pass - in place of the file names to read them from stdin, one per line. If no names arrive, nothing is extracted. See the advanced examples for searching an archive and extracting the matches.
$ mpqcli list wow-patch.mpq | grep -i '\.exe$' | mpqcli extract wow-patch.mpq -
Extract one specific file with locale
Use the -e or --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 Patch_rt.mpq "rez\gluBNRes.res" --locale deDE
read
Read a specific file to stdout.
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.
$ mpqcli read wow-patch.mpq patch.cmd
* set the product patch name
PatchVersion This patch upgrades World of Warcraft from version 1.10.0.5195 to version 1.10.1.5230.
* make sure that we don't patch version 1.10.1.5230 or greater
FileVersionEqualTo "$(InstallPath)\WoW.exe" 1.10.0.5195 0xffff.0xffff.0xffff.0xffff
* ProductVersionLessThan "$(InstallPath)\WoW.exe" 0.0.0.256 0.0.0.0xffff
Language enGB
SetLauncher "$(InstallPath)\WoW.exe"
* SetUninstall $(WinDir)\WoWUnin.dat
Self "World of Warcraft"
WoWPatchIndex 2
PatchSize 1992294400
The tool will always print output in bytes and is designed to be “piped” or redirected. For example, redirect the binary WoW.exe file from a WoW patch to a local file:
$ mpqcli read wow-patch.mpq "WoW.exe" > WoW.exe
Another example, piping the bytes to the xxd tool.
$ mpqcli read wow-patch.mpq "WoW.exe" | xxd
00000000: 1800 0404 de2a a5da 3240 4500 3250 4500 .....*..2@E.2PE.
00000010: 69ac 2703 0859 c601 0a7a 4500 8942 5344 i.'..Y...zE..BSD
00000020: 4946 4634 30a0 2905 8203 f244 0482 3250 IFF40.)....D..2P
00000030: 4504 8080 0280 9002 8098 0281 d801 0585 E...............
Read one specific file with locale
Use the -e or --locale argument to specify the locale of the file to read. If there is no file with the requested name and locale, the default locale will be used instead.
$ mpqcli read Patch_rt.mpq "rez\stat_txt.tbl" --locale ptPT
verify
Verify an MPQ archive signature.
Verify an MPQ archive
Check the digital signature of an MPQ archive by verifying the signature in the archive with a collection of known Blizzard public keys (bundled in StormLib library). The tool will print if verification succeeds or fails, as well as return 0 for success and any other value for failure.
$ mpqcli verify wow-patch.mpq
[*] Verify success
If verification passes, a zero (0) exit status will be returned. This can be helpful to verify a large number of MPQ archives without the need to review the status message that is printed out.
$ echo $?
0
Verify an MPQ archive and print the digital signature
Check the digital signature of an MPQ archive, and also write the signature’s raw bytes to stdout, using the -x or --print argument. The verification result still goes to stderr, so the bytes can be redirected to a file on their own:
$ mpqcli verify --print wow-patch.mpq > signature
To view the signature as hexadecimal, pipe it through a tool such as xxd:
$ mpqcli verify --print wow-patch.mpq | xxd
compact
Compacts an MPQ archive. It performs a complete archive rebuild, effectively defragmenting the MPQ archive, removing all gaps that have been created by adding, replacing, renaming or deleting files within the archive. To succeed, the function requires all files in MPQ archive to be accessible and their filenames to be known.
This may take several minutes to complete for large archives.
$ mpqcli compact wow-patch.mpq
[*] Compacting archive. This may take some time...
Use an external listfile
The compact command requires all files inside the archive to be known. Older MPQ archives do not contain (complete) file paths of their content. By using the -l or --listfile argument, one can provide an external listfile that lists the content of the MPQ archive. Listfiles can be downloaded on Ladislav Zezula’s site.
$ mpqcli compact -l /path/to/listfile DIABDAT.MPQ
[*] Compacting archive. This may take some time...
Advanced examples
Search and extract files on Linux
The mpqcli tool has no native search feature - instead, it is designed to be integrated with other, external operating system tools. For example, mpqcli list can be “piped” to grep in Linux or Select-String in Windows PowerShell to perform searching.
The following command lists all files in an MPQ archive, and each filename is filtered using grep - selecting files ending in .exe (note: . is a regex wildcard matching any character; use \.exe for a strictly literal match) - which is then passed back to mpqcli extract. The result: search and extract all exe files.
$ mpqcli list wow-patch.mpq | grep -i \.exe | mpqcli extract wow-patch.mpq -
[*] Extracted: Launcher.exe
[*] Extracted: BackgroundDownloader.exe
[*] Extracted: WoW.exe
[*] Extracted: BNUpdate.exe
[*] Extracted: Repair.exe
[*] Extracted: WowError.exe
Note that directories are specified with backslashes. The names reach mpqcli extract on stdin unchanged, so only the backslash in the grep pattern needs escaping. The following example extracts all dat files in the arr directory from the StarDat.mpq archive, keeping the folder.
$ mpqcli list -l scbw.txt StarDat.mpq | grep -i 'arr\\.*dat$' | sort | mpqcli extract StarDat.mpq -
[*] Extracted: arr/flingy.dat
[*] Extracted: arr/images.dat
[*] Extracted: arr/mapdata.dat
[*] Extracted: arr/orders.dat
[*] Extracted: arr/portdata.dat
[*] Extracted: arr/sfxdata.dat
[*] Extracted: arr/sprites.dat
[*] Extracted: arr/techdata.dat
[*] Extracted: arr/units.dat
[*] Extracted: arr/upgrades.dat
[*] Extracted: arr/weapons.dat
Search and extract files on Windows
The following command lists all files in an MPQ archive, and each filename is filtered using Select-String - selecting files ending in .exe (note: . is a regex wildcard matching any character; use \.exe for a strictly literal match) - which is then passed back to mpqcli extract on stdin. ForEach-Object Line passes on the text of each match rather than the match object. The result: search and extract all exe files.
PS> mpqcli.exe list wow-patch.mpq | Select-String -Pattern \.exe | `
ForEach-Object Line | mpqcli.exe extract wow-patch.mpq -
[*] Extracted: Launcher.exe
[*] Extracted: BackgroundDownloader.exe
[*] Extracted: WoW.exe
[*] Extracted: BNUpdate.exe
[*] Extracted: Repair.exe
[*] Extracted: WowError.exe
Building
Requirements
- cmake 3.25 or later
- C++ 17 compiler
- StormLib (provided as Git Submodule)
- CLI11 (provided as Git Submodule)
- hash-library (provided as Git Submodule)
Linux
$ git clone --recursive https://github.com/thegraydot/mpqcli.git
$ cd mpqcli
$ cmake -B build/release -DCMAKE_BUILD_TYPE=Release
$ cmake --build build/release
The mpqcli binary will be available in: ./build/release/bin/mpqcli
Windows
$ git clone --recursive https://github.com/thegraydot/mpqcli.git
$ cd mpqcli
$ cmake -B build/release
$ cmake --build build/release --config Release
The mpqcli.exe binary will be available in: .\build\release\bin\mpqcli.exe
Dependencies
StormLib
This project requires the StormLib library. Many thanks to Ladislav Zezula for authoring such a good library and releasing the code under an open-source licence. The StormLib library has a number of requirements. However, the build method specifies using the libraries bundled with StormLib.
CLI11
This project also uses the CLI11 command line parser for C++11 and beyond. It provides simple and easy-to-use CLI arguments.
Tests
The tests are written with the Catch2 framework. They come in two layers, each built as its own binary in build/dev/bin:
- Unit tests (
test/unit/, built asmpqcli_unit_tests) call the core library directly, with one test file per source file insrc/. - Functional tests (
test/functional/, built asmpqcli_functional_tests) run the builtmpqclibinary as a user would, using subprocess.h. There is one test file per subcommand, checking its exit code and output.
Catch2 and subprocess.h are submodules like the other dependencies, and the tests are built by default. Build, then run each layer:
$ cmake -B build/dev
$ cmake --build build/dev
$ ctest --test-dir build/dev -L unit
$ ctest --test-dir build/dev -L functional
On Windows, add -C Release (or whichever configuration you built) to each ctest command. To build without the tests, configure with -DMPQCLI_BUILD_TESTING=OFF.
With the Makefile, make test builds and runs the unit tests and make test_functional the functional tests; make test_all runs both.
Real archives
A few functional tests read two genuine World of Warcraft patch archives, the only archives in the suite with a real Blizzard signature. They are not committed to the repository, and those tests are skipped until the archives are present. Fetch them with:
$ make fetch_integration_data
This needs filenget on your PATH, and checks each download against the checksums in test/integration_data.sha256. The archives go in test/data/integration/. To keep them somewhere else, set MPQCLI_INTEGRATION_DATA to that directory, either in the environment or as a CMake cache variable.
Contributing
Contributions are welcome. Please read the guidelines below before opening a pull request.
Before you start
If you are unsure whether a feature fits the project, or whether an existing tool could already be combined with mpqcli to achieve the same result, open an issue first. This avoids wasted effort and keeps the project focused.
mpqcli follows the Unix philosophy. The tool is designed to do one thing well and to compose with other tools via pipes and redirection. If you find yourself wanting to add functionality that could be handled by a separate tool - for example, sorting the output of list - the right answer is usually to pipe the output to that tool rather than adding it here.
Prerequisites and setup
Clone the repository and initialise submodules:
git clone https://github.com/thegraydot/mpqcli.git
cd mpqcli
git submodule update --init --recursive
Install the clang lint tools, pinned to the major version the checks are formatted against:
sudo apt-get install -y clang-18 clang-format-18 clang-tidy-18
Makefile reference
Run make help to list all available targets. Common ones:
| Target | Description |
|---|---|
make configure | Configure the cmake build (uses the default compiler) |
make build | Build the project into build/dev |
make test | Run the unit tests |
make test_functional | Run the functional tests against the built binary |
make test_all | Run every test layer in build/dev |
make fetch_integration_data | Download the real archives a few functional tests read (needs filenget) |
make check_all | Run every static check (clang-format, clang-tidy, completion scripts) |
make check_format | Check formatting only (dry run) |
make format | Auto-fix formatting in-place |
make configure_lint | Configure build/lint with clang++ for clang-tidy |
make check_lint | Run clang-tidy static analysis |
make check_embed | Syntax-check the embedded completion scripts |
make clean | Remove all build and docs artefacts |
Requirements for a pull request
1. Builds on your platform
Make sure the project builds cleanly on your development machine before opening a PR:
make build
On Windows without a POSIX shell, use the cmake commands from the building page instead.
A PR automatically triggers the CI build workflow, which compiles and tests across all supported Linux targets (AMD64 and ARM64). You are not expected to reproduce all of those locally.
2. Tests pass
Run the test suite before submitting:
make test
make test_functional
All tests must pass without errors. A few functional tests need two real Blizzard patch archives and are skipped until you fetch them with make fetch_integration_data, which needs filenget. The building page explains more.
3. New features should include tests
If your change adds or modifies user-facing functionality - such as a new subcommand flag or a change in output format - please include a corresponding test under test/. The tests use Catch2 and come in two layers:
test/unit/mirrorssrc/, with onetest_<source>.cppper source file, and calls the core library directly. Prefer it for logic: it is faster, and a failure points at the code.test/functional/has one file per subcommand and runs the builtmpqclibinary, asserting on its exit code, stdout and stderr. Use it for what only the command line shows: argument parsing, exit codes and output.
Shared fixtures live in test/fixtures/, one header each. The existing files, such as test/functional/test_list.cpp and test/unit/util/test_path.cpp, are good references for the style.
4. Linting must pass
All C++ code is formatted with clang-format and analysed with clang-tidy, and the completion scripts are parsed by their shells. make check_all runs all three checks. It configures a clang build tree of its own for clang-tidy, so it needs no prior make configure:
make check_all
If there are formatting violations, auto-fix them with:
make format
Then re-run make check_all to confirm everything passes.
5. Match the existing code style
C++ formatting is enforced by .clang-format (LLVM style base). Static analysis is enforced by .clang-tidy. Both configs live in the repo root. The tests are held to the same formatting, but clang-tidy does not analyse them.
Suppression policy
Suppressions are occasionally necessary for third-party code or intentional patterns. When suppressing a clang-tidy warning:
- Use
// NOLINT(check-name)with the specific check name - bare// NOLINTis not acceptable - Every suppression must have a comment explaining why it is justified
// NOLINT(bugprone-easily-swappable-parameters): parameters validated by CLI11
Disabling clang-format locally
Use // clang-format off / // clang-format on only when the default formatting genuinely hurts readability (e.g. column-aligned tables). Add a brief comment explaining the intent:
// Preserve column-aligned flag-to-char mappings for readability
// clang-format off
if (flags & MPQ_FILE_IMPLODE) result += 'i';
if (flags & MPQ_FILE_COMPRESS) result += 'c';
// clang-format on
Known design constraints
StormLib locale state is global and not thread-safe
SFileSetLocale sets a process-wide locale variable (g_lcFileLocale) inside StormLib. All locale-sensitive operations in src/mpq/ - file open, add, remove, read, extract, and list - call SFileSetLocale immediately before the relevant StormLib call. There is no locale-explicit alternative in StormLib’s public API (SFileOpenFileEx, SFileAddFileEx, etc. all read g_lcFileLocale internally).
This means:
- The
SFileSetLocale+ StormLib-call sequence is not atomic and would be unsafe under concurrency - mpqcli is intentionally single-threaded; do not introduce threads or async I/O without auditing every locale-sensitive call site in
src/mpq/
If you add a new StormLib call that is locale-sensitive, follow the existing pattern: call SFileSetLocale immediately before it, with no intervening calls between the two.
Workflow summary
- Fork the repository and create a branch for your change
- Run
git submodule update --init --recursiveafter cloning - Install the clang tools as shown above
- Make your changes and verify they build:
make build - Run
make check_all, fixing any issues - Run
make testandmake test_functionaland confirm all tests pass - Open a pull request with a clear description of what was changed and why
Changelog
0.13.0 - 2026-10-16
Added
- Accept several files, or a list read from stdin, in create and extract
- Add a flat option to extract
- Add short flags -e, -j, -q and -x for the locale, flat, property and print options
Changed
- Take the archive as the first argument in every subcommand, including create and read
- Keep folders when extracting named files
- Rename the create version option to format-version
- Reject an unknown locale in extract and read instead of using the default locale
Fixed
- Show the encryption keys of encrypted files listed without their names
- Accept Windows line endings in paths read from stdin
- Stop add and create from storing an archive inside itself
- Leave no archive behind when create cannot add every file
Removed
- Remove create -o, extract -f and -k, and the -p flag from info, list and verify
Thanks
- Thanks to @sjoblomj for the contributions in this release
- Thanks to @MikaMika for reporting the inconsistent subcommand arguments
0.12.0 - 2026-10-04
Added
- Stop cleanly on Ctrl-C, leaving no partial archive behind
Changed
- Ship one static musl Linux binary per architecture, with the release assets renamed by OS and architecture
- Publish install scripts and a signed checksums file with each release
- Run the Docker image in /data so mounted paths need no prefix
- Print progress and warnings on stderr, leaving stdout for results alone
- Mark an overwritten file as changed rather than added when adding
Fixed
- Fail with a non-zero exit code on an unknown subcommand, a missing argument or an internal error
- Fail with a non-zero exit code when closing, signing, listing or printing a signature fails
- Complete every locale, game profile and archive path in the zsh, fish and PowerShell scripts
Removed
- Remove the install scripts from the repository; each release now carries its own
0.11.0 - 2026-09-21
Added
- Add rename subcommand to rename a file inside an archive
Changed
- Compare timestamps and checksums when updating files, not size alone
- Accept the update flag for single files as well as directories
- Reject the update and overwrite flags on add when both are given
Fixed
- Report filesystem errors instead of aborting when a path cannot be resolved
- Add to and create archives on volumes that cannot report real paths
- Block path traversal in extract before anything is written to disk
Updated
- Update StormLib for a variety of fixes
- Add hash-library as a submodule dependency
- Build with StormLib’s bundled compression libraries instead of system packages
- Require CMake 3.25 or later
Thanks
- Thanks to @sjoblomj for the contributions in this release
0.10.2 - 2026-07-03
Fixed
verifyincorrectly reporting success for a tampered archiveverifyfailure messages now clarify whether a signature is missing or invalidaddoverwrite flag not always working as expected- Archive size incorrectly reported for large archives
- File sizes shown as negative for large files
Updated
- Simplified how strong archive signatures are read internally
0.10.1 - 2026-07-03
Added
- Command completion for common shells (bash, zsh, fish and powershell)
Fixed
- Memory leak in read subcommand
- Inconsistencies in documentation
Updated
- Code style to conform to Google CPP style with slight variations
- StormLib dependency for a variety of fixes
Thanks
- Thanks to @sjoblomj for the contributions in this release
0.10.0 - 2026-06-07
Added
- Compact subcommand to compress MPQ archive after creation/editing
- Support passing multiple files to the
addandremovesubcommands - Support reading file paths from stdin in the
addandremovesubcommands - Support adding entire directories with the
addsubcommand - Add an update flag to the
addsubcommand to skip files whose archived size matches the on-disk size
Fixed
extractcommand now reports an error when the output directory cannot be created- Path traversal guard in
extractuses fully resolved paths, closing a potential bypass - Crash when reading strong signatures from malformed or truncated archives
- Docker glibc image updated to ubuntu:24.04
- Adding files is now ordered and operating system agnostic
addsubcommand now returns non-zero exit codes on failure- Error messages now include StormLib error codes
- Duplicate error messages removed
- Makefile build targets corrected in
CONTRIBUTING.md
Changed
--filename-in-archiveand--directory-in-archivereplaced by a unified--pathflag- The path flag on add now acts as an archive path prefix when a directory is given
Thanks
- Thanks to @sjoblomj for the contributions in this release
0.9.9 - 2026-04-05
Fixed
- Potential path traversal attack in extract subcommand
- Empty MPQ archive name when directory supplied with trailing slash
- Bug where MPQ internal files were added with create command
- Signature being added automatically when not requested
- Potential DWORD overflow in
AddFiles
0.9.8 - 2026-03-22
Added
- The
createsubcommand now supports a single file - The
addandcreatesubcommands have additional arguments for specifiying the file location in the MPQ archive (name-in-archiveanddir-in-archive) - The
addsubcommand can now overwrite existing files if requested (overwrite)
Fixed
- Several memory leaks
Thanks
- Thanks to @sjoblomj for adding more features and fixes in this release
0.9.7 - 2026-02-23
Added
- Game profile support for
createandaddsubcommands via-g/--gameoption - Game profiles for Diablo, StarCraft, Warcraft, WoW, and more
- Game setting override options for
createsubcommand - Compression override options for
addsubcommand - Support for unfamiliar/unknown locales via raw hex LCID
- Additional built-in locales:
ptBR,esMX,ptPT
Fixed
- Subcommands now properly verify locale before operating on files
- Error messages now include locale context in
remove,extract, andread - MPQ archives are now properly closed across all subcommands
- Non-zero exit codes returned on failure in
extractandremove listsubcommand now shows long output when properties are requested
Changed
- Improved locale printing and display throughout
Thanks
- A big thank you to @sjoblomj for adding so many features in this release
0.9.6 - 2025-12-23
Added
- Proper support for locales in MPQ files
Fixed
- Fixed bug where MPQ signature only printed on success
0.9.5 - 2025-10-23
Changed
- Long (detailed) list format to output ISO 8601 timestamps
Fixed
- Message prefix format consistency
0.9.4 - 2025-10-12
Added
- Filename added to program output
- Added simple wrapper around verify command
Fixed
- Show help menu when no arguments provided
0.9.3 - 2025-10-03
Added
- Support for
addsubcommand to have path inside MPQ archive
Fixed
- Bug in
addsubcommand when exceeding max file count
0.9.2 - 2025-08-21
Fixed
- Bug in
createsubcommand resulting in incorrect max file count
0.9.1 - 2025-08-19
Fixed
- Bug in Dockerfiles resulting in missing commit hash from version
0.9.0 - 2025-08-19
Added
- Linux binaries for amd64 and arm64
- Linux binaries built with musl and glibc
Updated
- Install script for Linux to provided better binary download options
Changed
- Linux binary release naming convention
0.8.1 - 2025-08-15
Added
- Added print raw bytes for
readandverifysubcommands
Fixed
- Tests for
readandverifysubcommands
Removed
- Ability to print hex or plaintext in
readandverifysubcommands - Removed messages when printing signature value
0.8.0 - 2025-08-11
Added
- Added
add/removesubcommands working - Added tests for
add/removesubcommands - Added
max-filesproperty toinfosubcommand
Changed
- Change for default maximum file count when creating MPQ archives
0.7.0 - 2025-08-09
Added
- Added detailed (long) listing
Fixed
- Updated create subcommand to always include attributes
- Updated Info function to support archive/file property fetching
0.6.2 - 2025-07-11
Added
- Added tests for
extractsubcommand - Added dependabot configuration for Python and Docker
Fixed
- Fixed deprecated Windows Action running to Server 2022
0.6.1 - 2025-06-27
Fixed
- Updated readme and help menu to match application functionality
0.6.0 - 2025-06-02
Added
- Added the
readsubcommand - Added tests for
readsubcommand - Added new helper functions to print ASCII or hexidecimal output
Fixed
- Fixed line endings in Windows pytest cases
0.5.0 - 2025-05-05
Added
- Added the
verifysubcommand - Included argument to print digital signature
- Added ability to sign archive when creating MPQ
- Added tests for
verifysubcommand
0.4.4 - 2025-05-05
Fixed
- Refactored
createsubcommand tests
0.4.3 - 2025-05-01
Added
- Refactored
infosubcommand - Added test for
infosubcommand
Fixed
- Updated tests to be multi-platform (Windows)
Removed
- Removed exe patch split functionality
0.4.2 - 2025-04-22
Added
- Added Dockerfile
- Added Docker build and release workflows
- Added
addandremovesubcommand placeholders - Added
aboutsubcommand
0.4.1 - 2025-04-22
Added
- Added Python testing environment
- Added tests for
listsubcommand - Added test workflow
0.4.0 - 2025-04-12
Added
- Added Windows binary build to releases
0.3.2 - 2025-03-29
Added
- External listfile support for
listandextractby @sjoblomj - Added functions for add and remove files
Fixed
- Fixed path extraction on Linux to create nested folder structure
0.3.1 - 2025-03-03
Fixed
- Fixed path extraction on Linux to create nested folder structure
0.3.0 - 2025-02-24
Added
- Added
createsubcommand - Added download information to readme
Fixed
- Improved Linux binary compatibility
0.2.3 - 2025-02-01
Fixed
- Improved Linux binary compatibility
0.2.2 - 2025-02-01
Added
- Added the
extract,verifyandlistsubcommands - Added
release.ymlworkflow for Linux binary