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();
110bool checkForExtractionEscapeAttempt(
const std::filesystem::path& outputDir,
const std::string& filepath) {
111 const auto absOutDir = std::filesystem::absolute(outputDir).lexically_normal();
112 auto absOutDirString = absOutDir.string();
114 absOutDirString +=
'/';
116 const auto absFilePath = std::filesystem::absolute(outputDir / std::filesystem::path{filepath}).lexically_normal();
117 auto absFilePathString = absFilePath.string();
120 return !absFilePathString.starts_with(absOutDirString);
131 auto extension = std::filesystem::path{path}.extension().string();
134 if (registry.contains(extension)) {
135 for (
const auto& func : registry.at(extension)) {
136 if (
auto packFile = func(path, callback, requestProperty)) {
145 std::vector<std::string> out;
147 if (std::ranges::find(out, extension) == out.end()) {
148 out.push_back(extension);
151 std::ranges::sort(out);
176 return static_cast<bool>(this->
findEntry(path, includeUnbaked));
181 if (
const auto it = this->
entries.find(path); it != this->entries.end()) {
184 if (includeUnbaked) {
185 if (
const auto it = this->
unbakedEntries.find(path); it != this->unbakedEntries.end()) {
197 const auto bytes = this->
readEntry(path);
202 for (
auto byte : *bytes) {
203 if (
byte ==
static_cast<std::byte
>(0))
205 out +=
static_cast<char>(byte);
223 entry.unbakedUsingByteBuffer =
false;
224 entry.unbakedData = filepath;
240 entry.unbakedUsingByteBuffer =
true;
244 entry.unbakedData = std::move(buffer);
250 this->
addEntry(path, std::vector<std::byte>{buffer.begin(), buffer.end()}, options);
255 return this->
addDirectory(entryBaseDir, dir, [options](
const std::string&) {
261 if (this->
isReadOnly() || !std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) {
266 if (!entryBaseDir.empty()) {
269 const auto dirLen = std::filesystem::absolute(dir).string().length() + 1;
270 int64_t filesAdded = 0;
271 for (
const auto& file : std::filesystem::recursive_directory_iterator(dir, std::filesystem::directory_options::skip_permission_denied)) {
272 if (!file.is_regular_file()) {
276 std::string entryPath;
278 absPath = std::filesystem::absolute(file.path()).string();
280 entryPath = this->
cleanEntryPath(entryBaseDir + absPath.substr(dirLen));
281 }
catch (
const std::exception&) {
284 if (entryPath.empty()) {
287 filesAdded += this->
addEntry(entryPath, absPath, creation ? creation(entryPath) :
EntryOptions{});
295 if (this->
entries.count(oldPath)) {
298 auto entry = this->
entries.at(oldPath);
300 this->
entries.emplace(newPath, entry);
316 std::vector<std::string> entryPaths;
317 std::vector<std::string> unbakedEntryPaths;
318 this->
runForAllEntries([&oldDir, &entryPaths, &unbakedEntryPaths](
const std::string& path,
const Entry& entry) {
319 if (path.starts_with(oldDir)) {
321 unbakedEntryPaths.push_back(path);
323 entryPaths.push_back(path);
328 for (
const auto& entryPath : entryPaths) {
329 auto entry = this->
entries.at(entryPath);
330 this->
entries.erase(entryPath);
331 this->
entries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
333 for (
const auto& entryPath : unbakedEntryPaths) {
336 this->
unbakedEntries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
338 return !entryPaths.empty() || !unbakedEntryPaths.empty();
347 if (this->
entries.find(path) != this->entries.end()) {
351 if (this->
unbakedEntries.find(path) != this->unbakedEntries.end()) {
365 if (dirName ==
"/") {
371 std::size_t count = this->
entries.erase_prefix(dirName);
377 return this->
bake(
"", {},
nullptr);
381 return this->
bake(outputDir_, {},
nullptr);
385 return this->
bake(outputDir_, options,
nullptr);
389 if (filepath.empty()) {
393 const auto data = this->
readEntry(entryPath);
398 FileStream stream{filepath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
414 auto outputDirPath = std::filesystem::path{outputDir} / std::filesystem::path{dir}.filename();
415 bool noneFailed =
true;
416 this->
runForAllEntries([
this, &dir, &outputDirPath, &noneFailed](
const std::string& path,
const Entry&) {
417 if (!path.starts_with(dir)) {
421 std::string outputPath = path.substr(dir.length());
423 ::fixFilePathForWindows(outputPath);
425 if (::checkForExtractionEscapeAttempt(outputDirPath, outputPath) || !this->
extractEntry(path, (outputDirPath / outputPath).
string())) {
433 if (outputDir.empty()) {
437 std::filesystem::path outputDirPath{outputDir};
438 if (createUnderPackFileDir) {
441 bool noneFailed =
true;
443 std::string entryPath = path;
445 ::fixFilePathForWindows(entryPath);
447 if (::checkForExtractionEscapeAttempt(outputDirPath, entryPath) || !this->
extractEntry(path, (outputDirPath / entryPath).
string())) {
455 if (outputDir.empty() || !predicate) {
460 std::vector<std::string> saveEntryPaths;
462 if (predicate(path, entry)) {
463 saveEntryPaths.push_back(path);
466 if (saveEntryPaths.empty()) {
470 std::size_t rootDirLen = 0;
471 if (stripSharedDirs) {
473 std::vector<std::string> rootDirList;
475 std::vector<std::vector<std::string>> pathSplits;
476 pathSplits.reserve(saveEntryPaths.size());
477 for (
const auto& path : saveEntryPaths) {
478 pathSplits.push_back(::splitPath(path));
481 bool allTheSame =
true;
482 const std::string& first = pathSplits[0][0];
483 for (
const auto& path : pathSplits) {
484 if (path.size() == 1) {
488 if (path[0] != first) {
496 rootDirList.push_back(first);
497 for (
auto& path : pathSplits) {
498 path.erase(path.begin());
501 rootDirLen = ::joinPath(rootDirList).length() + 1;
505 const std::filesystem::path outputDirPath{outputDir};
506 bool noneFailed =
true;
507 for (
const auto& path : saveEntryPaths) {
508 auto savePath = path;
510 ::fixFilePathForWindows(savePath);
512 if (::checkForExtractionEscapeAttempt(outputDirPath, savePath.substr(rootDirLen)) || !this->extractEntry(path, (outputDirPath / savePath.substr(rootDirLen)).string())) {
528 std::size_t count = 0;
530 if (includeUnbaked) {
538 for (
auto entry = this->
entries.cbegin(); entry != this->entries.cend(); ++entry) {
540 operation(key, entry.value());
542 if (includeUnbaked) {
543 for (
auto entry = this->
unbakedEntries.cbegin(); entry != this->unbakedEntries.cend(); ++entry) {
545 operation(key, entry.value());
554 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
557 auto keyView = std::string_view{key}.substr(dir.length());
558 if (std::ranges::find(keyView,
'/') != keyView.end()) {
562 operation(key, entry.value());
564 if (includeUnbaked) {
565 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
568 auto keyView = std::string_view{key}.substr(dir.length());
569 if (std::ranges::find(keyView,
'/') != keyView.end()) {
573 operation(key, entry.value());
580 for (
auto entry = this->
entries.begin(); entry != this->entries.end(); ++entry) {
582 operation(key, entry.value());
584 if (includeUnbaked) {
585 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
587 operation(key, entry.value());
596 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
599 auto keyView = std::string_view{key}.substr(dir.length());
600 if (std::ranges::find(keyView,
'/') != keyView.end()) {
604 operation(key, entry.value());
606 if (includeUnbaked) {
607 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
610 auto keyView = std::string_view{key}.substr(dir.length());
611 if (std::ranges::find(keyView,
'/') != keyView.end()) {
615 operation(key, entry.value());
629 return std::filesystem::path{this->
fullFilePath}.filename().string();
638 return std::filesystem::path{this->
fullFilePath}.stem().string();
649PackFile::operator std::string()
const {
656 ::fixFilePathForWindows(copy);
664 std::vector<std::string> out;
678 std::string out = outputDir;
683 if (
const auto lastSlash = out.rfind(
'/'); lastSlash != std::string::npos) {
694 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
697 entry->unbaked =
false;
700 entry->unbakedUsingByteBuffer =
false;
701 entry->unbakedData =
"";
703 this->
entries.insert(key, *entry);
732 std::vector<std::byte> unbakedData;
733 if (entry.unbakedUsingByteBuffer) {
734 unbakedData = std::get<std::vector<std::byte>>(entry.unbakedData);
742 static std::unordered_map<std::string, std::vector<PackFile::OpenFactoryFunction>> extensionRegistry;
743 return extensionRegistry;
748 return factory(path, callback);
753 const std::string extensionStr{extension};
755 if (!registry.contains(extensionStr)) {
756 registry[extensionStr] = {};
758 registry[extensionStr].push_back(factory);
765PackFileReadOnly::operator std::string()
const {
766 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.
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.
int64_t addDirectory(const std::string &entryBaseDir, const std::string &dir, EntryOptions options={})
Adds new entries using the contents of a given directory.
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.
bool 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,...
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.
bool bake()
If output folder is an empty string, it will overwrite the original.
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
std::function< EntryOptions(const std::string &path)> EntryCreation
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...
std::optional< std::vector< std::byte > > operator[](const std::string &path_) const
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::filesystem::path &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)