Files
LibCommon/Source/Common/FileUtil.cpp
T
Lioncache 23e72d7859 TString: Remove operator* shorthand
Makes it explicit whenever the underlying data is fetched in a
meaningfully searchable way.
2026-02-10 15:12:28 -05:00

624 lines
16 KiB
C++

#include "FileUtil.h"
#include "Common/Flags.h"
#include "Common/Log.h"
#include "Common/FileIO/CFileInStream.h"
#include "Common/FileIO/CFileOutStream.h"
#include <chrono>
#include <filesystem>
#include <system_error>
#ifdef WIN32
#include <Windows.h>
// Windows why
#undef CopyFile
#undef DeleteFile
#undef MoveFile
#include <io.h>
#define check_dir_access(path) _access(path, 0x6)
#else
#include <unistd.h>
#define check_dir_access(path) access(path, R_OK | W_OK | X_OK)
#endif
// These are mostly just wrappers around std::filesystem functions.
using namespace std::filesystem;
namespace FileUtil
{
#define ToPath(Path) path(std::u8string((Path).cbegin(), (Path).cend()))
bool Exists(const TString &rkFilePath)
{
return exists(ToPath(rkFilePath));
}
bool IsRoot(const TString& rkPath)
{
// todo: is this actually a good/multiplatform way of checking for root?
TString AbsPath = MakeAbsolute(rkPath);
TStringList Split = AbsPath.Split("\\/");
return (Split.size() <= 1);
}
bool IsFile(const TString& rkFilePath)
{
return is_regular_file(ToPath(rkFilePath));
}
bool IsDirectory(const TString& rkDirPath)
{
return is_directory(ToPath(rkDirPath));
}
bool IsDirectoryWritable(const TString& rkDirPath)
{
return check_dir_access(rkDirPath.CString()) == 0;
}
bool IsAbsolute(const TString& rkDirPath)
{
return ToPath(rkDirPath).is_absolute();
}
bool IsRelative(const TString& rkDirPath)
{
return ToPath(rkDirPath).is_relative();
}
bool IsEmpty(const TString& rkDirPath)
{
if (!IsDirectory(rkDirPath))
{
NLog::Error("Non-directory path passed to IsEmpty(): {}", rkDirPath);
return false;
}
return is_empty(ToPath(rkDirPath));
}
bool MakeDirectory(const TString& rkNewDir)
{
if (!IsValidPath(rkNewDir, true))
{
NLog::Error("Unable to create directory because name contains illegal characters: {}", rkNewDir);
return false;
}
// Sometimes the MS implementation returns false with a zero-error for some reason
std::error_code err;
return create_directories(ToPath(rkNewDir), err) || !err;
}
bool CopyFile(const TString& rkOrigPath, const TString& rkNewPath)
{
if (!IsValidPath(rkNewPath, false))
{
NLog::Error("Unable to copy file because destination name contains illegal characters: {}", rkNewPath);
return false;
}
MakeDirectory(rkNewPath.GetFileDirectory());
std::error_code Error;
// call std::filesystem::copy, not std::copy
std::filesystem::copy(ToPath(rkOrigPath), ToPath(rkNewPath), Error);
return (Error.value() == 0);
}
bool CopyDirectory(const TString& rkOrigPath, const TString& rkNewPath)
{
if (!IsValidPath(rkNewPath, true))
{
NLog::Error("Unable to copy directory because destination name contains illegal characters: {}", rkNewPath);
return false;
}
MakeDirectory(rkNewPath.GetFileDirectory());
std::error_code Error;
// call std::filesystem::copy, not std::copy
std::filesystem::copy(ToPath(rkOrigPath), ToPath(rkNewPath), Error);
return (Error.value() == 0);
}
bool MoveFile(const TString& rkOldPath, const TString& rkNewPath)
{
if (!IsValidPath(rkNewPath, false))
{
NLog::Error("Unable to move file because destination name contains illegal characters: {}", rkNewPath);
return false;
}
if (Exists(rkNewPath))
{
NLog::Error("Unable to move file because there is an existing file at the destination path: {}", rkNewPath);
return false;
}
std::error_code Error;
rename(ToPath(rkOldPath), ToPath(rkNewPath), Error);
return Error.value() == 0;
}
bool MoveDirectory(const TString& rkOldPath, const TString& rkNewPath)
{
if (!IsValidPath(rkNewPath, true))
{
NLog::Error("Unable to move directory because destination name contains illegal characters: {}", rkNewPath);
return false;
}
if (Exists(rkNewPath))
{
NLog::Error("Unable to move directory because there is an existing directory at the destination path: {}", rkNewPath);
return false;
}
std::error_code Error;
rename(ToPath(rkOldPath), ToPath(rkNewPath), Error);
return Error.value() == 0;
}
bool DeleteFile(const TString& rkFilePath)
{
if (!IsFile(rkFilePath))
return false;
return remove(ToPath(rkFilePath)) == 1;
}
bool DeleteDirectory(const TString& rkDirPath, bool FailIfNotEmpty)
{
// This is an extremely destructive function, be careful using it!
if (!IsDirectory(rkDirPath))
return false;
// Sanity check - don't delete root
bool Root = IsRoot(rkDirPath);
if (Root)
{
ASSERT(false);
NLog::Fatal("Attempted to delete root directory!");
return false;
}
// Check if directory is empty
if (FailIfNotEmpty && !IsEmpty(rkDirPath))
return false;
// Delete directory
std::error_code Error;
remove_all(ToPath(rkDirPath), Error);
return (Error.value() == 0);
}
bool ClearDirectory(const TString& rkDirPath)
{
// This is an extremely destructive function, be careful using it!
if (!IsDirectory(rkDirPath)) return false;
// Sanity check - don't clear root
bool Root = IsRoot(rkDirPath);
if (Root)
{
ASSERT(false);
NLog::Fatal("Attempted to clear root directory!");
return false;
}
// Delete directory contents
TStringList DirContents;
GetDirectoryContents(rkDirPath, DirContents, false);
for (const auto& DirContent : DirContents)
{
bool Success = false;
if (IsFile(DirContent))
Success = DeleteFile(DirContent);
else if (IsDirectory(DirContent))
Success = DeleteDirectory(DirContent, false);
if (!Success)
NLog::Error("Failed to delete filesystem object: {}", DirContent);
}
return true;
}
void MarkHidden(const TString& rkFilePath, bool Hidden)
{
#ifdef WIN32
T16String FilePath16 = rkFilePath.ToUTF16();
DWORD Attrs = GetFileAttributesW( ToWChar(FilePath16) );
if (Hidden)
Attrs |= FILE_ATTRIBUTE_HIDDEN;
else
Attrs &= ~FILE_ATTRIBUTE_HIDDEN;
SetFileAttributesW( ToWChar(FilePath16), Attrs );
#else
NLog::Error("MarkHidden unimplemented: {}", rkFilePath);
#endif
}
void UpdateLastModifiedTime(const TString& rkFilePath)
{
last_write_time( ToPath(rkFilePath), file_time_type::clock::now() );
}
uint64_t FileSize(const TString &rkFilePath)
{
return (uint64_t) (Exists(rkFilePath) ? file_size(ToPath(rkFilePath)) : -1);
}
uint64_t LastModifiedTime(const TString& rkFilePath)
{
return (uint64_t) last_write_time(ToPath(rkFilePath)).time_since_epoch().count();
}
TString WorkingDirectory()
{
return current_path().string();
}
TString MakeAbsolute(TString Path)
{
if (!ToPath(Path).has_root_path())
Path = WorkingDirectory() + "/" + Path;
TStringList Components = Path.Split("/\\");
TStringList::iterator Prev;
for (auto Iter = Components.begin(); Iter != Components.end(); ++Iter)
{
if (*Iter == ".")
Iter = Components.erase(Iter);
else if (*Iter == "..")
Iter = std::prev(Components.erase(Prev, std::next(Iter)));
Prev = Iter;
}
TString Out;
for (auto I = Path.Begin(), E = Path.End(); I != E && (*I == '/' || *I == '\\'); ++I)
Out += *I;
for (const auto& Component : Components)
Out += Component + "/";
return Out;
}
TString MakeRelative(const TString& rkPath, const TString& rkRelativeTo /*= WorkingDirectory()*/)
{
TString AbsPath = MakeAbsolute(rkPath);
TString AbsRelTo = MakeAbsolute(rkRelativeTo);
TStringList PathComponents = AbsPath.Split("/\\");
TStringList RelToComponents = AbsRelTo.Split("/\\");
// Find furthest common parent
auto PathIter = PathComponents.begin();
auto RelToIter = RelToComponents.begin();
for (; PathIter != PathComponents.end() && RelToIter != RelToComponents.end(); ++PathIter, ++RelToIter)
{
if (*PathIter != *RelToIter)
break;
}
// If there's no common components, then we can't construct a relative path...
if (PathIter == PathComponents.begin())
return AbsPath;
// Construct output path
TString Out;
for (; RelToIter != RelToComponents.end(); ++RelToIter)
Out += "../";
for (; PathIter != PathComponents.end(); ++PathIter)
Out += *PathIter + "/";
// Attempt to detect if this path is a file as opposed to a directory; if so, remove trailing backslash
if (PathComponents.back().Contains('.') && !rkPath.EndsWith('/') && !rkPath.EndsWith('\\'))
Out = Out.ChopBack(1);
return Out;
}
TString SimplifyRelativePath(const TString& rkPath)
{
TStringList PathComponents = rkPath.Split("/\\");
auto PrevIter = PathComponents.begin();
for (auto Iter = PathComponents.begin(); Iter != PathComponents.end(); PrevIter = Iter, ++Iter)
{
if (*Iter == ".." && *PrevIter != "..")
{
PrevIter = PathComponents.erase(PrevIter);
PrevIter = PathComponents.erase(PrevIter);
Iter = PrevIter;
--Iter;
}
}
TString Out;
for (const auto& PathComponent : PathComponents)
Out += PathComponent + '/';
return Out;
}
uint32_t MaxFileNameLength()
{
return 255;
}
constexpr char gskIllegalNameChars[] = {
'<', '>', '\"', '/', '\\', '|', '?', '*', ':'
};
TString SanitizeName(TString Name, bool Directory, bool RootDir /*= false*/)
{
// Windows only atm
if (Directory && (Name == "." || Name == ".."))
return Name;
// Remove illegal characters from path
for (size_t iChr = 0; iChr < Name.Size(); iChr++)
{
char Chr = Name[iChr];
bool Remove = false;
if (Chr >= 0 && Chr <= 31)
Remove = true;
// Allow colon only as the last character of root
bool IsLegalColon = (Chr == ':' && RootDir && iChr == Name.Size() - 1);
if (!IsLegalColon && !IsValidFileNameCharacter(Chr))
Remove = true;
if (Remove)
{
Name.Remove(iChr, 1);
iChr--;
}
}
// For directories, space and dot are not allowed at the end of the path
if (Directory)
{
int64_t ChopNum = 0;
for (auto iChr = (int64_t) Name.Size() - 1; iChr >= 0; iChr--)
{
char Chr = Name[iChr];
if (Chr == ' ' || Chr == '.')
ChopNum++;
else
break;
}
if (ChopNum > 0)
Name = Name.ChopBack(ChopNum);
}
// Remove spaces from beginning of path
size_t NumLeadingSpaces = 0;
while (NumLeadingSpaces < Name.Size() && Name[NumLeadingSpaces] == ' ')
NumLeadingSpaces++;
if (NumLeadingSpaces > 0)
Name = Name.ChopFront(NumLeadingSpaces);
// Ensure the name is below the character limit
if (Name.Size() > MaxFileNameLength())
{
const int64_t ChopNum = Name.Size() - MaxFileNameLength();
Name = Name.ChopBack(ChopNum);
}
return Name;
}
TString SanitizePath(TString Path, bool Directory)
{
TStringList Components = Path.Split("\\/");
size_t CompIdx = 0;
Path = "";
for (auto It = Components.begin(); It != Components.end(); ++It)
{
TString Comp = *It;
bool IsDir = Directory || CompIdx < Components.size() - 1;
bool IsRoot = CompIdx == 0;
Comp = SanitizeName(Comp, IsDir, IsRoot);
Path += Comp;
if (IsDir)
Path += '/';
CompIdx++;
}
return Path;
}
bool IsValidFileNameCharacter(char Chr)
{
if (Chr >= 0 && Chr <= 31)
return false;
for (char gskIllegalNameChar : gskIllegalNameChars)
{
if (Chr == gskIllegalNameChar)
return false;
}
return true;
}
bool IsValidName(const TString& rkName, bool Directory, bool RootDir /*= false*/)
{
// Only accounting for Windows limitations right now. However, this function should
// ideally return the same output on all platforms to ensure projects are cross platform.
if (rkName.IsEmpty())
return false;
if (rkName.Size() > MaxFileNameLength())
return false;
if (Directory && (rkName == "." || rkName == ".."))
return true;
// Check for banned characters
for (size_t iChr = 0; iChr < rkName.Size(); iChr++)
{
char Chr = rkName[iChr];
// Allow colon only as the last character of root
bool IsLegalColon = (Chr == ':' && RootDir && iChr == rkName.Size() - 1);
if (!IsLegalColon && !IsValidFileNameCharacter(Chr))
return false;
}
if (Directory && (rkName.Back() == ' ' || rkName.Back() == '.'))
return false;
return true;
}
bool IsValidPath(const TString& rkPath, bool Directory)
{
// Only accounting for Windows limitations right now. However, this function should
// ideally return the same output on all platforms to ensure projects are cross platform.
TStringList Components = rkPath.Split("\\/");
// Validate other components
size_t CompIdx = 0;
for (auto It = Components.begin(); It != Components.end(); ++It)
{
bool IsRoot = CompIdx == 0;
bool IsDir = Directory || CompIdx < (Components.size() - 1);
if (!IsValidName(*It, IsDir, IsRoot))
return false;
CompIdx++;
}
return true;
}
void GetDirectoryContents(TString DirPath, TStringList& rOut, bool Recursive /*= true*/, bool IncludeFiles /*= true*/, bool IncludeDirs /*= true*/)
{
if (IsDirectory(DirPath))
{
DirPath.Replace("\\", "/");
bool IncludeAll = IncludeFiles && IncludeDirs;
auto AddFileLambda = [IncludeFiles, IncludeDirs, IncludeAll, &rOut](const TString& rkPath) -> void {
bool ShouldAddFile = IncludeAll || (IncludeFiles && IsFile(rkPath)) || (IncludeDirs && IsDirectory(rkPath));
if (ShouldAddFile)
rOut.push_back(rkPath);
};
if (Recursive)
{
for (recursive_directory_iterator It(ToPath(DirPath)); It != recursive_directory_iterator(); ++It)
{
AddFileLambda(It->path().string());
}
}
else
{
for (directory_iterator It(ToPath(DirPath)); It != directory_iterator(); ++It)
{
AddFileLambda(It->path().string());
}
}
}
}
TString FindFileExtension(const TString& rkDir, const TString& rkName)
{
for (directory_iterator It(ToPath(rkDir)); It != directory_iterator(); ++It)
{
TString Name = It->path().filename().string();
if (Name.GetFileName(false) == rkName)
return Name.GetFileExtension();
}
return "";
}
bool LoadFileToString(const TString& rkFilePath, TString& rOut)
{
CFileInStream File(rkFilePath);
if (File.IsValid())
{
rOut = TString(File.Size());
File.ReadBytes(&rOut[0], rOut.Size());
return true;
}
return false;
}
bool LoadFileToBuffer(const TString& rkFilePath, std::vector<uint8_t>& Out)
{
CFileInStream File(rkFilePath);
if (File.IsValid())
{
Out.resize(File.Size());
File.ReadBytes(Out.data(), Out.size());
return true;
}
return false;
}
bool SaveStringToFile(const TString& rkFilePath, const TString& kString)
{
CFileOutStream File(rkFilePath);
if (File.IsValid())
{
File.WriteBytes(&kString[0], kString.Size());
return true;
}
return false;
}
bool SaveBufferToFile(const TString& rkFilePath, const std::vector<uint8_t>& kBuffer)
{
CFileOutStream File(rkFilePath);
if (File.IsValid())
{
File.WriteBytes(kBuffer.data(), kBuffer.size());
return true;
}
return false;
}
}