24std::string joinPath(
const std::vector<std::string>& list) {
28 std::string result = list.front();
29 for (
int i = 1; i < list.size(); ++i) {
30 result +=
'/' + list[i];
35std::vector<std::string> splitPath(
const std::string&
string) {
36 std::vector<std::string> result;
37 std::stringstream stream{
string};
39 while (std::getline(stream, segment,
'/')) {
40 result.push_back(segment);
47void replace(std::string& line,
const std::string& oldString,
const std::string& newString) {
48 const auto oldSize = oldString.length();
49 if (oldSize > line.length()) {
53 const auto newSize = newString.length();
56 pos = line.find(oldString, pos);
57 if (pos == std::string::npos) {
60 if (oldSize == newSize) {
61 line.replace(pos, oldSize, newString);
63 line.erase(pos, oldSize);
64 line.insert(pos, newString);
70void fixFilePathForWindows(std::string& path) {
72 ::replace(path,
"<",
"_");
73 ::replace(path,
"<",
"_");
74 ::replace(path,
">",
"_");
75 ::replace(path,
":",
"_");
76 ::replace(path,
"\"",
"_");
77 ::replace(path,
"|",
"_");
78 ::replace(path,
"?",
"_");
79 ::replace(path,
"*",
"_");
81 std::filesystem::path filePath{path};
82 auto filename = filePath.filename().string();
83 auto extension = filePath.extension().string();
84 auto stem = filePath.stem().string();
88 if (stem ==
"CON" || stem ==
"PRN" || stem ==
"AUX" || stem ==
"NUL") {
89 filename =
"___" + extension;
90 }
else if (stem.length() == 4 && stem[3] !=
'0' && ((stem.starts_with(
"COM") || stem.starts_with(
"LPT")))) {
93 filename += extension;
97 if (extension ==
".") {
102 path = (filePath.parent_path() / filename).
string();
110 : fullFilePath(std::move(fullFilePath_)) {
115 auto extension = std::filesystem::path{path}.extension().string();
118 if (registry.contains(extension)) {
119 for (
const auto& func : registry.at(extension)) {
120 if (
auto packFile = func(path, callback)) {
129 std::vector<std::string> out;
131 if (std::find(out.begin(), out.end(), extension) == out.end()) {
132 out.push_back(extension);
135 std::sort(out.begin(), out.end());
160 return static_cast<bool>(this->
findEntry(path, includeUnbaked));
165 if (
auto it = this->
entries.find(path); it != this->entries.end()) {
168 if (includeUnbaked) {
169 if (
auto it = this->
unbakedEntries.find(path); it != this->unbakedEntries.end()) {
182 for (
auto byte : *bytes) {
183 if (
byte ==
static_cast<std::byte
>(0))
185 out +=
static_cast<char>(byte);
199 entry.unbakedUsingByteBuffer =
false;
200 entry.unbakedData = filepath;
215 entry.unbakedUsingByteBuffer =
true;
219 entry.unbakedData = std::move(buffer);
224 std::vector<std::byte> data;
225 if (buffer && bufferLen > 0) {
226 data.resize(bufferLen);
227 std::memcpy(data.data(), buffer, bufferLen);
229 this->
addEntry(path, std::move(data), options);
233 this->
addDirectory(entryBaseDir, dir, [options](
const std::string& path) {
239 if (!std::filesystem::exists(dir) || std::filesystem::status(dir).type() != std::filesystem::file_type::directory) {
244 if (!entryBaseDir.empty()) {
247 const auto dirLen = std::filesystem::absolute(dir).string().length() + 1;
248 for (
const auto& file : std::filesystem::recursive_directory_iterator(dir, std::filesystem::directory_options::skip_permission_denied)) {
249 if (!file.is_regular_file()) {
253 std::string entryPath;
255 absPath = std::filesystem::absolute(file.path()).string();
257 entryPath = this->
cleanEntryPath(entryBaseDir + absPath.substr(dirLen));
258 }
catch (
const std::exception&) {
261 if (entryPath.empty()) {
271 if (this->
entries.count(oldPath)) {
274 auto entry = this->
entries.at(oldPath);
276 this->
entries.emplace(newPath, entry);
291 std::vector<std::string> entryPaths;
292 std::vector<std::string> unbakedEntryPaths;
293 this->
runForAllEntries([&oldDir, &entryPaths, &unbakedEntryPaths](
const std::string& path,
const Entry& entry) {
294 if (path.starts_with(oldDir)) {
296 unbakedEntryPaths.push_back(path);
298 entryPaths.push_back(path);
303 for (
const auto& entryPath : entryPaths) {
304 auto entry = this->entries.at(entryPath);
305 this->entries.erase(entryPath);
306 this->entries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
308 for (
const auto& entryPath : unbakedEntryPaths) {
309 auto entry = this->unbakedEntries.at(entryPath);
310 this->unbakedEntries.erase(entryPath);
311 this->unbakedEntries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
313 return !entryPaths.empty() || !unbakedEntryPaths.empty();
322 if (this->
entries.find(path) != this->entries.end()) {
326 if (this->
unbakedEntries.find(path) != this->unbakedEntries.end()) {
340 if (dirName ==
"/") {
346 std::size_t count = this->
entries.erase_prefix(dirName);
352 if (filepath.empty()) {
361 FileStream stream{filepath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
377 auto outputDirPath = std::filesystem::path{outputDir} / std::filesystem::path{dir}.filename();
378 bool noneFailed =
true;
379 this->
runForAllEntries([
this, &dir, &outputDirPath, &noneFailed](
const std::string& path,
const Entry& entry) {
380 if (!path.starts_with(dir)) {
384 std::string outputPath = path.substr(dir.length());
386 ::fixFilePathForWindows(outputPath);
388 if (!this->
extractEntry(path, (outputDirPath / outputPath).
string())) {
396 if (outputDir.empty()) {
400 std::filesystem::path outputDirPath{outputDir};
401 if (createUnderPackFileDir) {
404 bool noneFailed =
true;
405 this->
runForAllEntries([
this, &outputDirPath, &noneFailed](
const std::string& path,
const Entry& entry) {
406 std::string entryPath = path;
408 ::fixFilePathForWindows(entryPath);
410 if (!this->
extractEntry(path, (outputDirPath / entryPath).
string())) {
418 if (outputDir.empty() || !predicate) {
423 std::vector<std::string> saveEntryPaths;
425 if (predicate(path, entry)) {
426 saveEntryPaths.push_back(path);
429 if (saveEntryPaths.empty()) {
433 std::size_t rootDirLen = 0;
434 if (stripSharedDirs) {
436 std::vector<std::string> rootDirList;
438 std::vector<std::vector<std::string>> pathSplits;
439 pathSplits.reserve(saveEntryPaths.size());
440 for (
const auto& path : saveEntryPaths) {
441 pathSplits.push_back(::splitPath(path));
444 bool allTheSame =
true;
445 const std::string& first = pathSplits[0][0];
446 for (
const auto& path : pathSplits) {
447 if (path.size() == 1) {
451 if (path[0] != first) {
459 rootDirList.push_back(first);
460 for (
auto& path : pathSplits) {
461 path.erase(path.begin());
464 rootDirLen = ::joinPath(rootDirList).length() + 1;
468 std::filesystem::path outputDirPath{outputDir};
469 bool noneFailed =
true;
470 for (
const auto& path : saveEntryPaths) {
471 auto savePath = path;
473 ::fixFilePathForWindows(savePath);
475 if (!this->
extractEntry(path, (outputDirPath / savePath.substr(rootDirLen)).string())) {
491 std::size_t count = 0;
493 if (includeUnbaked) {
501 for (
auto entry = this->
entries.cbegin(); entry != this->entries.cend(); ++entry) {
503 operation(key, entry.value());
505 if (includeUnbaked) {
506 for (
auto entry = this->
unbakedEntries.cbegin(); entry != this->unbakedEntries.cend(); ++entry) {
508 operation(key, entry.value());
517 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
520 auto keyView = std::string_view{key}.substr(dir.length());
521 if (std::find(keyView.begin(), keyView.end(),
'/') != keyView.end()) {
525 operation(key, entry.value());
527 if (includeUnbaked) {
528 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
531 auto keyView = std::string_view{key}.substr(dir.length());
532 if (std::find(keyView.begin(), keyView.end(),
'/') != keyView.end()) {
536 operation(key, entry.value());
543 for (
auto entry = this->
entries.begin(); entry != this->entries.end(); ++entry) {
545 operation(key, entry.value());
547 if (includeUnbaked) {
548 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
550 operation(key, entry.value());
559 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
562 auto keyView = std::string_view{key}.substr(dir.length());
563 if (std::find(keyView.begin(), keyView.end(),
'/') != keyView.end()) {
567 operation(key, entry.value());
569 if (includeUnbaked) {
570 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
573 auto keyView = std::string_view{key}.substr(dir.length());
574 if (std::find(keyView.begin(), keyView.end(),
'/') != keyView.end()) {
578 operation(key, entry.value());
592 return std::filesystem::path{this->
fullFilePath}.filename().string();
601 return std::filesystem::path{this->
fullFilePath}.stem().string();
612PackFile::operator std::string()
const {
613 return this->getTruncatedFilename();
619 ::fixFilePathForWindows(copy);
627 std::vector<std::string> out;
632 auto data = this->readEntry(path);
641 std::string out = outputDir;
646 auto lastSlash = out.rfind(
'/');
647 if (lastSlash != std::string::npos) {
658 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
661 entry->unbaked =
false;
664 entry->unbakedUsingByteBuffer =
false;
665 entry->unbakedData =
"";
667 this->
entries.insert(key, *entry);
696 std::vector<std::byte> unbakedData;
697 if (entry.unbakedUsingByteBuffer) {
698 unbakedData = std::get<std::vector<std::byte>>(entry.unbakedData);
706 static std::unordered_map<std::string, std::vector<PackFile::OpenFactoryFunction>> extensionRegistry;
707 return extensionRegistry;
711 std::string extensionStr{extension};
713 if (!registry.contains(extensionStr)) {
714 registry[extensionStr] = {};
716 registry[extensionStr].push_back(factory);
723PackFileReadOnly::operator std::string()
const {
724 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_)
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.
std::function< std::unique_ptr< PackFile >(const std::string &path, const EntryCallback &callback)> OpenFactoryFunction
static std::unordered_map< std::string, std::vector< OpenFactoryFunction > > & getOpenExtensionRegistry()
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
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)
static const OpenFactoryFunction & registerOpenExtensionForTypeFactory(std::string_view extension, const OpenFactoryFunction &factory)
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)
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.
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open a generic pack file. The parser is selected based on the file extension.
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)