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()) {
184 const auto bytes = this->
readEntry(path);
189 for (
auto byte : *bytes) {
190 if (
byte ==
static_cast<std::byte
>(0))
192 out +=
static_cast<char>(byte);
206 entry.unbakedUsingByteBuffer =
false;
207 entry.unbakedData = filepath;
222 entry.unbakedUsingByteBuffer =
true;
226 entry.unbakedData = std::move(buffer);
231 this->
addEntry(path, std::vector<std::byte>{buffer.begin(), buffer.end()}, options);
235 this->
addDirectory(entryBaseDir, dir, [options](
const std::string&) {
241 if (!std::filesystem::exists(dir) || std::filesystem::status(dir).type() != std::filesystem::file_type::directory) {
246 if (!entryBaseDir.empty()) {
249 const auto dirLen = std::filesystem::absolute(dir).string().length() + 1;
250 for (
const auto& file : std::filesystem::recursive_directory_iterator(dir, std::filesystem::directory_options::skip_permission_denied)) {
251 if (!file.is_regular_file()) {
255 std::string entryPath;
257 absPath = std::filesystem::absolute(file.path()).string();
259 entryPath = this->
cleanEntryPath(entryBaseDir + absPath.substr(dirLen));
260 }
catch (
const std::exception&) {
263 if (entryPath.empty()) {
273 if (this->
entries.count(oldPath)) {
276 auto entry = this->
entries.at(oldPath);
278 this->
entries.emplace(newPath, entry);
294 std::vector<std::string> entryPaths;
295 std::vector<std::string> unbakedEntryPaths;
296 this->
runForAllEntries([&oldDir, &entryPaths, &unbakedEntryPaths](
const std::string& path,
const Entry& entry) {
297 if (path.starts_with(oldDir)) {
299 unbakedEntryPaths.push_back(path);
301 entryPaths.push_back(path);
306 for (
const auto& entryPath : entryPaths) {
307 auto entry = this->entries.at(entryPath);
308 this->entries.erase(entryPath);
309 this->entries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
311 for (
const auto& entryPath : unbakedEntryPaths) {
312 auto entry = this->unbakedEntries.at(entryPath);
313 this->unbakedEntries.erase(entryPath);
314 this->unbakedEntries.emplace(newDir + entryPath.substr(oldDir.length()), entry);
316 return !entryPaths.empty() || !unbakedEntryPaths.empty();
325 if (this->
entries.find(path) != this->entries.end()) {
329 if (this->
unbakedEntries.find(path) != this->unbakedEntries.end()) {
343 if (dirName ==
"/") {
349 std::size_t count = this->
entries.erase_prefix(dirName);
355 if (filepath.empty()) {
359 const auto data = this->
readEntry(entryPath);
364 FileStream stream{filepath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
380 auto outputDirPath = std::filesystem::path{outputDir} / std::filesystem::path{dir}.filename();
381 bool noneFailed =
true;
382 this->
runForAllEntries([
this, &dir, &outputDirPath, &noneFailed](
const std::string& path,
const Entry&) {
383 if (!path.starts_with(dir)) {
387 std::string outputPath = path.substr(dir.length());
389 ::fixFilePathForWindows(outputPath);
391 if (!this->
extractEntry(path, (outputDirPath / outputPath).
string())) {
399 if (outputDir.empty()) {
403 std::filesystem::path outputDirPath{outputDir};
404 if (createUnderPackFileDir) {
407 bool noneFailed =
true;
409 std::string entryPath = path;
411 ::fixFilePathForWindows(entryPath);
413 if (!this->
extractEntry(path, (outputDirPath / entryPath).
string())) {
421 if (outputDir.empty() || !predicate) {
426 std::vector<std::string> saveEntryPaths;
428 if (predicate(path, entry)) {
429 saveEntryPaths.push_back(path);
432 if (saveEntryPaths.empty()) {
436 std::size_t rootDirLen = 0;
437 if (stripSharedDirs) {
439 std::vector<std::string> rootDirList;
441 std::vector<std::vector<std::string>> pathSplits;
442 pathSplits.reserve(saveEntryPaths.size());
443 for (
const auto& path : saveEntryPaths) {
444 pathSplits.push_back(::splitPath(path));
447 bool allTheSame =
true;
448 const std::string& first = pathSplits[0][0];
449 for (
const auto& path : pathSplits) {
450 if (path.size() == 1) {
454 if (path[0] != first) {
462 rootDirList.push_back(first);
463 for (
auto& path : pathSplits) {
464 path.erase(path.begin());
467 rootDirLen = ::joinPath(rootDirList).length() + 1;
471 const std::filesystem::path outputDirPath{outputDir};
472 bool noneFailed =
true;
473 for (
const auto& path : saveEntryPaths) {
474 auto savePath = path;
476 ::fixFilePathForWindows(savePath);
478 if (!this->
extractEntry(path, (outputDirPath / savePath.substr(rootDirLen)).string())) {
494 std::size_t count = 0;
496 if (includeUnbaked) {
504 for (
auto entry = this->
entries.cbegin(); entry != this->entries.cend(); ++entry) {
506 operation(key, entry.value());
508 if (includeUnbaked) {
509 for (
auto entry = this->
unbakedEntries.cbegin(); entry != this->unbakedEntries.cend(); ++entry) {
511 operation(key, entry.value());
520 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
523 auto keyView = std::string_view{key}.substr(dir.length());
524 if (std::ranges::find(keyView,
'/') != keyView.end()) {
528 operation(key, entry.value());
530 if (includeUnbaked) {
531 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
534 auto keyView = std::string_view{key}.substr(dir.length());
535 if (std::ranges::find(keyView,
'/') != keyView.end()) {
539 operation(key, entry.value());
546 for (
auto entry = this->
entries.begin(); entry != this->entries.end(); ++entry) {
548 operation(key, entry.value());
550 if (includeUnbaked) {
551 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
553 operation(key, entry.value());
562 for (
auto [entry, end] = this->
entries.equal_prefix_range(dir); entry != end; ++entry) {
565 auto keyView = std::string_view{key}.substr(dir.length());
566 if (std::ranges::find(keyView,
'/') != keyView.end()) {
570 operation(key, entry.value());
572 if (includeUnbaked) {
573 for (
auto [entry, end] = this->
unbakedEntries.equal_prefix_range(dir); entry != end; ++entry) {
576 auto keyView = std::string_view{key}.substr(dir.length());
577 if (std::ranges::find(keyView,
'/') != keyView.end()) {
581 operation(key, entry.value());
595 return std::filesystem::path{this->
fullFilePath}.filename().string();
604 return std::filesystem::path{this->
fullFilePath}.stem().string();
615PackFile::operator std::string()
const {
616 return this->getTruncatedFilename();
622 ::fixFilePathForWindows(copy);
630 std::vector<std::string> out;
635 auto data = this->readEntry(path);
644 std::string out = outputDir;
649 if (
const auto lastSlash = out.rfind(
'/'); lastSlash != std::string::npos) {
660 for (
auto entry = this->
unbakedEntries.begin(); entry != this->unbakedEntries.end(); ++entry) {
663 entry->unbaked =
false;
666 entry->unbakedUsingByteBuffer =
false;
667 entry->unbakedData =
"";
669 this->
entries.insert(key, *entry);
698 std::vector<std::byte> unbakedData;
699 if (entry.unbakedUsingByteBuffer) {
700 unbakedData = std::get<std::vector<std::byte>>(entry.unbakedData);
708 static std::unordered_map<std::string, std::vector<PackFile::OpenFactoryFunction>> extensionRegistry;
709 return extensionRegistry;
714 return factory(path, callback);
719 const std::string extensionStr{extension};
721 if (!registry.contains(extensionStr)) {
722 registry[extensionStr] = {};
724 registry[extensionStr].push_back(factory);
731PackFileReadOnly::operator std::string()
const {
732 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...
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::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)