12#include <FileStream.h>
27std::string joinPath(
const std::vector<std::string>& list) {
31 std::string result = list.front();
32 for (
int i = 1; i < list.size(); ++i) {
33 result +=
'/' + list[i];
38std::vector<std::string> splitPath(
const std::string&
string) {
39 std::vector<std::string> result;
40 std::stringstream stream{
string};
42 while (std::getline(stream, segment,
'/')) {
43 result.push_back(segment);
50void replace(std::string& line,
const std::string& oldString,
const std::string& newString) {
51 const auto oldSize = oldString.length();
52 if (oldSize > line.length()) {
56 const auto newSize = newString.length();
59 pos = line.find(oldString, pos);
60 if (pos == std::string::npos) {
63 if (oldSize == newSize) {
64 line.replace(pos, oldSize, newString);
66 line.erase(pos, oldSize);
67 line.insert(pos, newString);
73void fixFilePathForWindows(std::string& path) {
75 ::replace(path,
"<",
"_");
76 ::replace(path,
"<",
"_");
77 ::replace(path,
">",
"_");
78 ::replace(path,
":",
"_");
79 ::replace(path,
"\"",
"_");
80 ::replace(path,
"|",
"_");
81 ::replace(path,
"?",
"_");
82 ::replace(path,
"*",
"_");
84 const std::filesystem::path filePath{path};
85 auto filename = filePath.filename().string();
86 const auto extension = filePath.extension().string();
87 auto stem = filePath.stem().string();
91 if (stem ==
"CON" || stem ==
"PRN" || stem ==
"AUX" || stem ==
"NUL") {
92 filename =
"___" + extension;
93 }
else if (stem.length() == 4 && stem[3] !=
'0' && (stem.starts_with(
"COM") || stem.starts_with(
"LPT"))) {
96 filename += extension;
100 if (extension ==
".") {
105 path = (filePath.parent_path() / filename).
string();
113 : fullFilePath(std::move(fullFilePath_)) {
118 auto extension = std::filesystem::path{path}.extension().string();
121 if (registry.contains(extension)) {
122 for (
const auto& func : registry.at(extension)) {
123 if (
auto packFile = func(path, callback, requestProperty)) {
132 std::vector<std::string> out;
134 if (std::ranges::find(out, extension) == out.end()) {
135 out.push_back(extension);
138 std::ranges::sort(out);
163 return static_cast<bool>(this->
findEntry(path, includeUnbaked));
168 if (
const auto it = this->
entries.find(path); it != this->entries.end()) {
171 if (includeUnbaked) {
172 if (
const auto it = this->
unbakedEntries.find(path); it != this->unbakedEntries.end()) {
180 const auto bytes = this->
readEntry(path);
185 for (
auto byte : *bytes) {
186 if (
byte ==
static_cast<std::byte
>(0))
188 out +=
static_cast<char>(byte);
202 entry.unbakedUsingByteBuffer =
false;
203 entry.unbakedData = filepath;
218 entry.unbakedUsingByteBuffer =
true;
222 entry.unbakedData = std::move(buffer);
227 std::vector<std::byte> data;
228 if (buffer && bufferLen > 0) {
229 data.resize(bufferLen);
230 std::memcpy(data.data(), buffer, bufferLen);
232 this->
addEntry(path, std::move(data), options);
236 this->
addDirectory(entryBaseDir, dir, [options](
const std::string&) {
242 if (!std::filesystem::exists(dir) || std::filesystem::status(dir).type() != std::filesystem::file_type::directory) {
247 if (!entryBaseDir.empty()) {
250 const auto dirLen = std::filesystem::absolute(dir).string().length() + 1;
251 for (
const auto& file : std::filesystem::recursive_directory_iterator(dir, std::filesystem::directory_options::skip_permission_denied)) {
252 if (!file.is_regular_file()) {
256 std::string entryPath;
258 absPath = std::filesystem::absolute(file.path()).string();
260 entryPath = this->
cleanEntryPath(entryBaseDir + absPath.substr(dirLen));
261 }
catch (
const std::exception&) {
264 if (entryPath.empty()) {
274 if (this->
entries.count(oldPath)) {
277 auto entry = this->
entries.at(oldPath);
279 this->
entries.emplace(newPath, entry);
295 std::vector<std::string> entryPaths;
296 std::vector<std::string> unbakedEntryPaths;
297 this->
runForAllEntries([&oldDir, &entryPaths, &unbakedEntryPaths](
const std::string& path,
const Entry& entry) {
298 if (path.starts_with(oldDir)) {
300 unbakedEntryPaths.push_back(path);
302 entryPaths.push_back(path);
307 for (
const auto& entryPath : entryPaths) {
308 auto entry = this->entries.at(entryPath);
309 this->entries.erase(entryPath);
310 this->entries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
312 for (
const auto& entryPath : unbakedEntryPaths) {
313 auto entry = this->unbakedEntries.at(entryPath);
314 this->unbakedEntries.erase(entryPath);
315 this->unbakedEntries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
317 return !entryPaths.empty() || !unbakedEntryPaths.empty();
326 if (this->
entries.find(path) != this->entries.end()) {
330 if (this->
unbakedEntries.find(path) != this->unbakedEntries.end()) {
344 if (dirName ==
"/") {
350 std::size_t count = this->
entries.erase_prefix(dirName);
356 if (filepath.empty()) {
360 const auto data = this->
readEntry(entryPath);
365 FileStream stream{filepath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
381 auto outputDirPath = std::filesystem::path{outputDir} / std::filesystem::path{dir}.filename();
382 bool noneFailed =
true;
383 this->
runForAllEntries([
this, &dir, &outputDirPath, &noneFailed](
const std::string& path,
const Entry&) {
384 if (!path.starts_with(dir)) {
388 std::string outputPath = path.substr(dir.length());
390 ::fixFilePathForWindows(outputPath);
392 if (!this->
extractEntry(path, (outputDirPath / outputPath).
string())) {
400 if (outputDir.empty()) {
404 std::filesystem::path outputDirPath{outputDir};
405 if (createUnderPackFileDir) {
408 bool noneFailed =
true;
410 std::string entryPath = path;
412 ::fixFilePathForWindows(entryPath);
414 if (!this->
extractEntry(path, (outputDirPath / entryPath).
string())) {
422 if (outputDir.empty() || !predicate) {
427 std::vector<std::string> saveEntryPaths;
429 if (predicate(path, entry)) {
430 saveEntryPaths.push_back(path);
433 if (saveEntryPaths.empty()) {
437 std::size_t rootDirLen = 0;
438 if (stripSharedDirs) {
440 std::vector<std::string> rootDirList;
442 std::vector<std::vector<std::string>> pathSplits;
443 pathSplits.reserve(saveEntryPaths.size());
444 for (
const auto& path : saveEntryPaths) {
445 pathSplits.push_back(::splitPath(path));
448 bool allTheSame =
true;
449 const std::string& first = pathSplits[0][0];
450 for (
const auto& path : pathSplits) {
451 if (path.size() == 1) {
455 if (path[0] != first) {
463 rootDirList.push_back(first);
464 for (
auto& path : pathSplits) {
465 path.erase(path.begin());
468 rootDirLen = ::joinPath(rootDirList).length() + 1;
472 const std::filesystem::path outputDirPath{outputDir};
473 bool noneFailed =
true;
474 for (
const auto& path : saveEntryPaths) {
475 auto savePath = path;
477 ::fixFilePathForWindows(savePath);
479 if (!this->
extractEntry(path, (outputDirPath / savePath.substr(rootDirLen)).string())) {
495 std::size_t count = 0;
497 if (includeUnbaked) {
505 for (
auto entry = this->
entries.cbegin(); entry != this->entries.cend(); ++entry) {
507 operation(key, entry.value());
509 if (includeUnbaked) {
510 for (
auto entry = this->
unbakedEntries.cbegin(); entry != this->unbakedEntries.cend(); ++entry) {
512 operation(key, entry.value());
521 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
524 auto keyView = std::string_view{key}.substr(dir.length());
525 if (std::ranges::find(keyView,
'/') != keyView.end()) {
529 operation(key, entry.value());
531 if (includeUnbaked) {
532 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
535 auto keyView = std::string_view{key}.substr(dir.length());
536 if (std::ranges::find(keyView,
'/') != keyView.end()) {
540 operation(key, entry.value());
547 for (
auto entry = this->
entries.begin(); entry != this->entries.end(); ++entry) {
549 operation(key, entry.value());
551 if (includeUnbaked) {
552 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
554 operation(key, entry.value());
563 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
566 auto keyView = std::string_view{key}.substr(dir.length());
567 if (std::ranges::find(keyView,
'/') != keyView.end()) {
571 operation(key, entry.value());
573 if (includeUnbaked) {
574 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
577 auto keyView = std::string_view{key}.substr(dir.length());
578 if (std::ranges::find(keyView,
'/') != keyView.end()) {
582 operation(key, entry.value());
596 return std::filesystem::path{this->
fullFilePath}.filename().string();
605 return std::filesystem::path{this->
fullFilePath}.stem().string();
616PackFile::operator std::string()
const {
617 return this->getTruncatedFilename();
623 ::fixFilePathForWindows(copy);
631 std::vector<std::string> out;
636 auto data = this->readEntry(path);
645 std::string out = outputDir;
650 if (
const auto lastSlash = out.rfind(
'/'); lastSlash != std::string::npos) {
661 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
664 entry->unbaked =
false;
667 entry->unbakedUsingByteBuffer =
false;
668 entry->unbakedData =
"";
670 this->
entries.insert(key, *entry);
699 std::vector<std::byte> unbakedData;
700 if (entry.unbakedUsingByteBuffer) {
701 unbakedData = std::get<std::vector<std::byte>>(entry.unbakedData);
709 static std::unordered_map<std::string, std::vector<PackFile::OpenFactoryFunction>> extensionRegistry;
710 return extensionRegistry;
715 return factory(path, callback);
720 const std::string extensionStr{extension};
722 if (!registry.contains(extensionStr)) {
723 registry[extensionStr] = {};
725 registry[extensionStr].push_back(factory);
732PackFileReadOnly::operator std::string()
const {
733 return PackFile::operator std::string() +
" (Read-Only)";
This class represents the metadata that a file has inside a PackFile.
bool unbaked
Used to check if entry is saved to disk.
uint32_t crc32
CRC32 checksum - 0 if unused.
bool bake(const std::string &outputDir_, BakeOptions options, const EntryCallback &callback) final
If output folder is an empty string, it will overwrite the original.
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) final
PackFileReadOnly(const std::string &fullFilePath_)
std::function< std::unique_ptr< PackFile >(const std::string &path, const EntryCallback &callback)> OpenFactoryFunctionBasic
tsl::htrie_map< char, Entry > EntryTrie
std::optional< std::string > readEntryText(const std::string &path) const
Try to read the entry's data to a string.
bool extractAll(const std::string &outputDir, bool createUnderPackFileDir=true) const
Extract the contents of the pack file to disk at the given directory.
virtual bool hasPackFileSignature() const
Returns true if the file is signed.
EntryCallbackBase< void > EntryCallback
virtual std::size_t removeDirectory(const std::string &dirName_)
Remove a directory.
static std::unordered_map< std::string, std::vector< OpenFactoryFunction > > & getOpenExtensionRegistry()
static const OpenFactoryFunction & registerOpenExtensionForTypeFactory(std::string_view extension, const OpenFactoryFunctionBasic &factory)
virtual bool renameDirectory(const std::string &oldDir_, const std::string &newDir_)
Rename an existing directory.
void mergeUnbakedEntries()
std::optional< Entry > findEntry(const std::string &path_, bool includeUnbaked=true) const
Try to find an entry given the file path.
virtual bool verifyPackFileChecksum() const
Verify the checksum of the entire file, returns true on success Will return true if there is no check...
bool extractDirectory(const std::string &dir_, const std::string &outputDir) const
Extract the given directory to disk under the given output directory.
virtual bool verifyPackFileSignature() const
Verify the file signature, returns true on success Will return true if there is no signature ability ...
virtual void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options)=0
bool extractEntry(const std::string &entryPath, const std::string &filepath) const
Extract the given entry to disk at the given file path.
virtual std::string getTruncatedFilestem() const
/home/user/pak01_dir.vpk -> pak01
virtual std::vector< std::string > verifyEntryChecksums() const
Verify the checksums of each file, if a file fails the check its path will be added to the vector If ...
virtual bool hasPackFileChecksum() const
Returns true if the entire file has a checksum.
std::vector< std::string > verifyEntryChecksumsUsingCRC32() const
virtual constexpr bool isReadOnly() const noexcept
EntryCallbackBase< bool > EntryPredicate
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr, const OpenPropertyRequest &requestProperty=nullptr)
Open a generic pack file. The parser is selected based on the file extension.
virtual constexpr bool isCaseSensitive() const
Does the format support case-sensitive file names?
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
std::function< std::vector< std::byte >(PackFile *packFile, OpenProperty property)> OpenPropertyRequest
std::string getFilestem() const
/home/user/pak01_dir.vpk -> pak01_dir
bool hasEntry(const std::string &path, bool includeUnbaked=true) const
Check if an entry exists given the file path.
virtual bool renameEntry(const std::string &oldPath_, const std::string &newPath_)
Rename an existing entry.
std::string getFilename() const
/home/user/pak01_dir.vpk -> pak01_dir.vpk
std::string getBakeOutputDir(const std::string &outputDir) const
static std::string escapeEntryPathForWrite(const std::string &path)
On Windows, some characters and file names are invalid - this escapes the given entry path.
std::string getTruncatedFilepath() const
/home/user/pak01_dir.vpk -> /home/user/pak01
void runForAllEntries(const EntryCallback &operation, bool includeUnbaked=true) const
Run a callback for each entry in the pack file.
void setFullFilePath(const std::string &outputDir)
std::function< std::unique_ptr< PackFile >(const std::string &path, const EntryCallback &callback, const OpenPropertyRequest &requestProperty)> OpenFactoryFunction
void addDirectory(const std::string &entryBaseDir, const std::string &dir, EntryOptions options={})
Adds new entries using the contents of a given directory.
std::function< EntryOptions(const std::string &path)> EntryCreation
void addEntry(const std::string &entryPath, const std::string &filepath, EntryOptions options={})
Add a new entry from a file path - the first parameter is the path in the PackFile,...
std::string cleanEntryPath(const std::string &path) const
virtual Attribute getSupportedEntryAttributes() const
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
virtual std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const =0
Try to read the entry's data to a bytebuffer.
PackFile(const PackFile &other)=delete
static std::vector< std::string > getOpenableExtensions()
Returns a sorted list of supported extensions for opening, e.g. {".bsp", ".vpk"}.
const EntryTrie & getBakedEntries() const
Get entries saved to disk.
static Entry createNewEntry()
std::string getTruncatedFilename() const
/home/user/pak01_dir.vpk -> pak01.vpk
std::size_t getEntryCount(bool includeUnbaked=true) const
Get the number of entries in the pack file.
virtual bool removeEntry(const std::string &path_)
Remove an entry.
std::string_view getFilepath() const
/home/user/pak01_dir.vpk
const EntryTrie & getUnbakedEntries() const
Get entries that have been added but not yet baked.
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
uint32_t computeCRC32(std::span< const std::byte > buffer)
std::vector< std::byte > readFileBuffer(const std::string &filepath, std::size_t startOffset=0)
void normalizeSlashes(std::string &path, bool stripSlashPrefix=false, bool stripSlashSuffix=true)
void toUpper(std::string &input)
void toLower(std::string &input)