mirror of
https://github.com/fallout2-ce/fallout2-ce.git
synced 2026-07-27 16:47:11 -07:00
Grow ability for ce-dat-tool to extract a list of files (#506)
* Grow ability for `ce-dat-tool` to extract a list of files This is needed for Et Tu installation, which selectively extracts files from fo1's master.dat. (It doesn't work if everything is extracted) * cache entry list to avoid O(n) cost when extracting many files
This commit is contained in:
+3
-2
@@ -66,8 +66,9 @@ Available commands:
|
||||
1. `./<BUILD_DIR>/ce-dat-tool <archive.dat> list [pattern]`
|
||||
2. `ce-dat-tool <archive.dat> info [pattern]`
|
||||
3. `ce-dat-tool <archive.dat> extract [--lower] <output-dir> [pattern]`
|
||||
4. `ce-dat-tool <archive.dat> cat <entry>`
|
||||
5. `ce-dat-tool create <input-dir> <archive.dat>`
|
||||
4. `ce-dat-tool <archive.dat> extract [--lower] (--file-list|--files-from) <list-file> <output-dir>`
|
||||
5. `ce-dat-tool <archive.dat> cat <entry>`
|
||||
6. `ce-dat-tool create <input-dir> <archive.dat>`
|
||||
|
||||
Use `--lower` with `extract` when you want every extracted file and directory name forced to lowercase.
|
||||
For example:
|
||||
|
||||
+31
-7
@@ -47,9 +47,17 @@ std::string normalizeDatPath(std::string path)
|
||||
return path;
|
||||
}
|
||||
|
||||
bool normalizeAndComparePath(const std::string& a, const std::string& b)
|
||||
std::string normalizeDatPathKey(std::string path)
|
||||
{
|
||||
return compat_stricmp(normalizeDatPath(a).c_str(), normalizeDatPath(b).c_str()) == 0;
|
||||
for (char& ch : path) {
|
||||
if (ch == '/') {
|
||||
ch = '\\';
|
||||
} else {
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
bool readBe32(FILE* stream, int* value)
|
||||
@@ -539,16 +547,32 @@ private:
|
||||
|
||||
const DatArchiveEntry* DatArchive::findEntry(const std::string& path) const
|
||||
{
|
||||
std::string normalizedPath = normalizeDatPath(path);
|
||||
for (const DatArchiveEntry& entry : entries()) {
|
||||
if (normalizeAndComparePath(entry.path, normalizedPath)) {
|
||||
return &entry;
|
||||
}
|
||||
ensureEntryLookup();
|
||||
|
||||
auto it = entryLookup_.find(normalizeDatPathKey(path));
|
||||
if (it != entryLookup_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void DatArchive::ensureEntryLookup() const
|
||||
{
|
||||
if (entryLookupInitialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<DatArchiveEntry>& archiveEntries = entries();
|
||||
entryLookup_.reserve(archiveEntries.size());
|
||||
|
||||
for (const DatArchiveEntry& entry : entries()) {
|
||||
entryLookup_.emplace(normalizeDatPathKey(entry.path), &entry);
|
||||
}
|
||||
|
||||
entryLookupInitialized_ = true;
|
||||
}
|
||||
|
||||
std::vector<const DatArchiveEntry*> DatArchive::findEntries(const std::string& pattern) const
|
||||
{
|
||||
std::vector<const DatArchiveEntry*> matches;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace fallout {
|
||||
@@ -42,6 +43,12 @@ public:
|
||||
std::vector<const DatArchiveEntry*> findEntries(const std::string& pattern) const;
|
||||
|
||||
static std::unique_ptr<DatArchive> open(const std::string& path);
|
||||
|
||||
private:
|
||||
void ensureEntryLookup() const;
|
||||
|
||||
mutable bool entryLookupInitialized_ = false;
|
||||
mutable std::unordered_map<std::string, const DatArchiveEntry*> entryLookup_;
|
||||
};
|
||||
|
||||
} // namespace fallout
|
||||
|
||||
+95
-11
@@ -44,6 +44,7 @@ struct Options {
|
||||
std::string archivePath;
|
||||
std::string command;
|
||||
std::vector<std::string> args;
|
||||
std::string extractFileListPath;
|
||||
bool lowerExtractedPaths = false;
|
||||
};
|
||||
|
||||
@@ -105,6 +106,21 @@ std::string toLowerAscii(std::string value)
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string trimAsciiWhitespace(std::string_view value)
|
||||
{
|
||||
const auto isNotSpace = [](char ch) {
|
||||
return !std::isspace(static_cast<unsigned char>(ch));
|
||||
};
|
||||
|
||||
const auto start = std::find_if(value.begin(), value.end(), isNotSpace);
|
||||
if (start == value.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto end = std::find_if(value.rbegin(), value.rend(), isNotSpace).base();
|
||||
return std::string(start, end);
|
||||
}
|
||||
|
||||
bool isAbsoluteOutputPath(const std::string& path)
|
||||
{
|
||||
if (path.empty()) {
|
||||
@@ -789,12 +805,14 @@ void printUsage(std::ostream& stream)
|
||||
<< " ce-dat-tool <archive.dat> list [pattern]\n"
|
||||
<< " ce-dat-tool <archive.dat> info [pattern]\n"
|
||||
<< " ce-dat-tool <archive.dat> extract [--lower] <output-dir> [pattern]\n"
|
||||
<< " ce-dat-tool <archive.dat> extract [--lower] (--file-list|--files-from) <list-file> <output-dir>\n"
|
||||
<< " ce-dat-tool <archive.dat> cat <entry>\n"
|
||||
<< "\n"
|
||||
<< "Notes:\n"
|
||||
<< " - Create preserves input path casing and stores Windows-style archive paths.\n"
|
||||
<< " - Create compresses entries only when zlib output is smaller.\n"
|
||||
<< " - Patterns use the same Windows-style wildcard matching as the game.\n"
|
||||
<< " - File lists contain one archive path per line. Empty lines are ignored.\n"
|
||||
<< " - Archive paths are case-insensitive and should use backslashes internally.\n";
|
||||
}
|
||||
|
||||
@@ -819,11 +837,23 @@ bool parseOptions(int argc, char** argv, Options* options)
|
||||
}
|
||||
|
||||
if (options->command == "extract") {
|
||||
auto lowerIt = std::find(options->args.begin(), options->args.end(), "--lower");
|
||||
if (lowerIt != options->args.end()) {
|
||||
options->lowerExtractedPaths = true;
|
||||
options->args.erase(lowerIt);
|
||||
std::vector<std::string> args;
|
||||
for (size_t index = 0; index < options->args.size(); index++) {
|
||||
const std::string& arg = options->args[index];
|
||||
if (arg == "--lower") {
|
||||
options->lowerExtractedPaths = true;
|
||||
} else if (arg == "--file-list" || arg == "--files-from") {
|
||||
if (index + 1 >= options->args.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
options->extractFileListPath = options->args[index + 1];
|
||||
index++;
|
||||
} else {
|
||||
args.push_back(arg);
|
||||
}
|
||||
}
|
||||
options->args = std::move(args);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -897,6 +927,37 @@ int listCommand(const DatArchive& archive, const std::string& pattern)
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool readExtractFileList(const std::string& fileListPath, std::vector<std::string>* paths)
|
||||
{
|
||||
std::ifstream input(fileListPath);
|
||||
if (!input.is_open()) {
|
||||
std::cerr << "Failed to open file list: " << fileListPath << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
std::string path = trimAsciiWhitespace(line);
|
||||
if (path.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
paths->push_back(normalizeDatPath(std::move(path)));
|
||||
}
|
||||
|
||||
if (!input.eof()) {
|
||||
std::cerr << "Failed while reading file list: " << fileListPath << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (paths->empty()) {
|
||||
std::cerr << "File list is empty: " << fileListPath << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int infoCommand(const DatArchive& archive, const std::vector<std::string>& args)
|
||||
{
|
||||
if (args.empty()) {
|
||||
@@ -954,7 +1015,7 @@ int infoCommand(const DatArchive& archive, const std::vector<std::string>& args)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int extractCommand(const DatArchive& archive, const std::vector<std::string>& args, bool lowerExtractedPaths)
|
||||
int extractCommand(const DatArchive& archive, const std::vector<std::string>& args, bool lowerExtractedPaths, const std::string& fileListPath)
|
||||
{
|
||||
if (args.empty()) {
|
||||
std::cerr << "extract requires an output directory\n";
|
||||
@@ -963,7 +1024,12 @@ int extractCommand(const DatArchive& archive, const std::vector<std::string>& ar
|
||||
|
||||
std::string outputDir = args[0];
|
||||
std::string pattern = "*";
|
||||
if (args.size() >= 2) {
|
||||
if (!fileListPath.empty() && args.size() >= 2) {
|
||||
std::cerr << "extract cannot use both --file-list and a pattern\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (fileListPath.empty() && args.size() >= 2) {
|
||||
pattern = normalizeDatPath(args[1]);
|
||||
}
|
||||
|
||||
@@ -972,10 +1038,28 @@ int extractCommand(const DatArchive& archive, const std::vector<std::string>& ar
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::vector<const DatArchiveEntry*> matches = archive.findEntries(pattern);
|
||||
if (matches.empty()) {
|
||||
std::cerr << "No entries matched pattern: " << pattern << "\n";
|
||||
return 1;
|
||||
std::vector<const DatArchiveEntry*> matches;
|
||||
if (!fileListPath.empty()) {
|
||||
std::vector<std::string> fileListPaths;
|
||||
if (!readExtractFileList(fileListPath, &fileListPaths)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (const std::string& entryPath : fileListPaths) {
|
||||
const DatArchiveEntry* entry = archive.findEntry(entryPath);
|
||||
if (entry == nullptr) {
|
||||
std::cerr << "Entry not found: " << entryPath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
matches.push_back(entry);
|
||||
}
|
||||
} else {
|
||||
matches = archive.findEntries(pattern);
|
||||
if (matches.empty()) {
|
||||
std::cerr << "No entries matched pattern: " << pattern << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int extracted = 0;
|
||||
@@ -1065,7 +1149,7 @@ int run(const Options& options)
|
||||
} else if (options.command == "info") {
|
||||
rc = infoCommand(*archive, options.args);
|
||||
} else if (options.command == "extract") {
|
||||
rc = extractCommand(*archive, options.args, options.lowerExtractedPaths);
|
||||
rc = extractCommand(*archive, options.args, options.lowerExtractedPaths, options.extractFileListPath);
|
||||
} else if (options.command == "cat") {
|
||||
rc = catCommand(*archive, options.args);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user