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 const auto absFilePath = std::filesystem::absolute(outputDir / std::filesystem::path{filepath}).lexically_normal();
113 return !absFilePath.string().starts_with(absOutDir.string());
124 auto extension = std::filesystem::path{path}.extension().string();
127 if (registry.contains(extension)) {
128 for (
const auto& func : registry.at(extension)) {
129 if (
auto packFile = func(path, callback, requestProperty)) {
138 std::vector<std::string> out;
140 if (std::ranges::find(out, extension) == out.end()) {
141 out.push_back(extension);
144 std::ranges::sort(out);
169 return static_cast<bool>(this->
findEntry(path, includeUnbaked));
174 if (
const auto it = this->
entries.find(path); it != this->entries.end()) {
177 if (includeUnbaked) {
178 if (
const auto it = this->
unbakedEntries.find(path); it != this->unbakedEntries.end()) {
190 const auto bytes = this->
readEntry(path);
195 for (
auto byte : *bytes) {
196 if (
byte ==
static_cast<std::byte
>(0))
198 out +=
static_cast<char>(byte);
216 entry.unbakedUsingByteBuffer =
false;
217 entry.unbakedData = filepath;
233 entry.unbakedUsingByteBuffer =
true;
237 entry.unbakedData = std::move(buffer);
243 this->
addEntry(path, std::vector<std::byte>{buffer.begin(), buffer.end()}, options);
248 return this->
addDirectory(entryBaseDir, dir, [options](
const std::string&) {
254 if (this->
isReadOnly() || !std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) {
259 if (!entryBaseDir.empty()) {
262 const auto dirLen = std::filesystem::absolute(dir).string().length() + 1;
263 int64_t filesAdded = 0;
264 for (
const auto& file : std::filesystem::recursive_directory_iterator(dir, std::filesystem::directory_options::skip_permission_denied)) {
265 if (!file.is_regular_file()) {
269 std::string entryPath;
271 absPath = std::filesystem::absolute(file.path()).string();
273 entryPath = this->
cleanEntryPath(entryBaseDir + absPath.substr(dirLen));
274 }
catch (
const std::exception&) {
277 if (entryPath.empty()) {
280 filesAdded += this->
addEntry(entryPath, absPath, creation ? creation(entryPath) :
EntryOptions{});
288 if (this->
entries.count(oldPath)) {
291 auto entry = this->
entries.at(oldPath);
293 this->
entries.emplace(newPath, entry);
309 std::vector<std::string> entryPaths;
310 std::vector<std::string> unbakedEntryPaths;
311 this->
runForAllEntries([&oldDir, &entryPaths, &unbakedEntryPaths](
const std::string& path,
const Entry& entry) {
312 if (path.starts_with(oldDir)) {
314 unbakedEntryPaths.push_back(path);
316 entryPaths.push_back(path);
321 for (
const auto& entryPath : entryPaths) {
322 auto entry = this->
entries.at(entryPath);
323 this->
entries.erase(entryPath);
324 this->
entries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
326 for (
const auto& entryPath : unbakedEntryPaths) {
329 this->
unbakedEntries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
331 return !entryPaths.empty() || !unbakedEntryPaths.empty();
340 if (this->
entries.find(path) != this->entries.end()) {
344 if (this->
unbakedEntries.find(path) != this->unbakedEntries.end()) {
358 if (dirName ==
"/") {
364 std::size_t count = this->
entries.erase_prefix(dirName);
370 return this->
bake(
"", {},
nullptr);
374 return this->
bake(outputDir_, {},
nullptr);
378 return this->
bake(outputDir_, options,
nullptr);
382 if (filepath.empty()) {
386 const auto data = this->
readEntry(entryPath);
391 FileStream stream{filepath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
407 auto outputDirPath = std::filesystem::path{outputDir} / std::filesystem::path{dir}.filename();
408 bool noneFailed =
true;
409 this->
runForAllEntries([
this, &dir, &outputDirPath, &noneFailed](
const std::string& path,
const Entry&) {
410 if (!path.starts_with(dir)) {
414 std::string outputPath = path.substr(dir.length());
416 ::fixFilePathForWindows(outputPath);
418 if (::checkForExtractionEscapeAttempt(outputDirPath, outputPath) || !this->
extractEntry(path, (outputDirPath / outputPath).
string())) {
426 if (outputDir.empty()) {
430 std::filesystem::path outputDirPath{outputDir};
431 if (createUnderPackFileDir) {
434 bool noneFailed =
true;
436 std::string entryPath = path;
438 ::fixFilePathForWindows(entryPath);
440 if (::checkForExtractionEscapeAttempt(outputDirPath, entryPath) || !this->
extractEntry(path, (outputDirPath / entryPath).
string())) {
448 if (outputDir.empty() || !predicate) {
453 std::vector<std::string> saveEntryPaths;
455 if (predicate(path, entry)) {
456 saveEntryPaths.push_back(path);
459 if (saveEntryPaths.empty()) {
463 std::size_t rootDirLen = 0;
464 if (stripSharedDirs) {
466 std::vector<std::string> rootDirList;
468 std::vector<std::vector<std::string>> pathSplits;
469 pathSplits.reserve(saveEntryPaths.size());
470 for (
const auto& path : saveEntryPaths) {
471 pathSplits.push_back(::splitPath(path));
474 bool allTheSame =
true;
475 const std::string& first = pathSplits[0][0];
476 for (
const auto& path : pathSplits) {
477 if (path.size() == 1) {
481 if (path[0] != first) {
489 rootDirList.push_back(first);
490 for (
auto& path : pathSplits) {
491 path.erase(path.begin());
494 rootDirLen = ::joinPath(rootDirList).length() + 1;
498 const std::filesystem::path outputDirPath{outputDir};
499 bool noneFailed =
true;
500 for (
const auto& path : saveEntryPaths) {
501 auto savePath = path;
503 ::fixFilePathForWindows(savePath);
505 if (::checkForExtractionEscapeAttempt(outputDirPath, savePath.substr(rootDirLen)) || !this->extractEntry(path, (outputDirPath / savePath.substr(rootDirLen)).string())) {
521 std::size_t count = 0;
523 if (includeUnbaked) {
531 for (
auto entry = this->
entries.cbegin(); entry != this->entries.cend(); ++entry) {
533 operation(key, entry.value());
535 if (includeUnbaked) {
536 for (
auto entry = this->
unbakedEntries.cbegin(); entry != this->unbakedEntries.cend(); ++entry) {
538 operation(key, entry.value());
547 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
550 auto keyView = std::string_view{key}.substr(dir.length());
551 if (std::ranges::find(keyView,
'/') != keyView.end()) {
555 operation(key, entry.value());
557 if (includeUnbaked) {
558 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
561 auto keyView = std::string_view{key}.substr(dir.length());
562 if (std::ranges::find(keyView,
'/') != keyView.end()) {
566 operation(key, entry.value());
573 for (
auto entry = this->
entries.begin(); entry != this->entries.end(); ++entry) {
575 operation(key, entry.value());
577 if (includeUnbaked) {
578 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
580 operation(key, entry.value());
589 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
592 auto keyView = std::string_view{key}.substr(dir.length());
593 if (std::ranges::find(keyView,
'/') != keyView.end()) {
597 operation(key, entry.value());
599 if (includeUnbaked) {
600 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
603 auto keyView = std::string_view{key}.substr(dir.length());
604 if (std::ranges::find(keyView,
'/') != keyView.end()) {
608 operation(key, entry.value());
622 return std::filesystem::path{this->
fullFilePath}.filename().string();
631 return std::filesystem::path{this->
fullFilePath}.stem().string();
642PackFile::operator std::string()
const {
649 ::fixFilePathForWindows(copy);
657 std::vector<std::string> out;
671 std::string out = outputDir;
676 if (
const auto lastSlash = out.rfind(
'/'); lastSlash != std::string::npos) {
687 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
690 entry->unbaked =
false;
693 entry->unbakedUsingByteBuffer =
false;
694 entry->unbakedData =
"";
696 this->
entries.insert(key, *entry);
725 std::vector<std::byte> unbakedData;
726 if (entry.unbakedUsingByteBuffer) {
727 unbakedData = std::get<std::vector<std::byte>>(entry.unbakedData);
735 static std::unordered_map<std::string, std::vector<PackFile::OpenFactoryFunction>> extensionRegistry;
736 return extensionRegistry;
741 return factory(path, callback);
746 const std::string extensionStr{extension};
748 if (!registry.contains(extensionStr)) {
749 registry[extensionStr] = {};
751 registry[extensionStr].push_back(factory);
758PackFileReadOnly::operator std::string()
const {
759 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)