733 lines
21 KiB
C++
733 lines
21 KiB
C++
/***
|
|
Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved.
|
|
|
|
File: FS.cpp
|
|
Date: 2021-6-16
|
|
Author: Reece
|
|
***/
|
|
#include <Source/RuntimeInternal.hpp>
|
|
#include "FS.hpp"
|
|
#include "Source/Locale/Locale.hpp"
|
|
|
|
namespace Aurora::IO::FS
|
|
{
|
|
static bool IsMagicCharacter(char c)
|
|
{
|
|
return (c == '.') ||
|
|
(c == '!') ||
|
|
(c == '~') ||
|
|
(c == '?') ||
|
|
(c == '^');
|
|
}
|
|
|
|
static void ResolveAbsolutePath(bool requireMountExpand, bool requireSanitization, const AuString &path, AuString &result)
|
|
{
|
|
AuString buffer;
|
|
bool isResource {};
|
|
buffer.reserve(4096);
|
|
result.reserve(4096);
|
|
|
|
/**
|
|
Resolve special character prefixes
|
|
*/
|
|
if (requireMountExpand)
|
|
{
|
|
int iOffset { 0 };
|
|
|
|
/**
|
|
* Strip protocol prefix
|
|
*/
|
|
if (AuStartsWith(path, "file://"))
|
|
{
|
|
iOffset = 7;
|
|
}
|
|
|
|
if ((path.size() > iOffset + 1) &&
|
|
(path[iOffset + 1] == kPathSplitter))
|
|
{
|
|
// Working directory
|
|
if (path[iOffset] == '.')
|
|
{
|
|
if (!Process::GetWorkingDirectory(buffer))
|
|
{
|
|
result.clear();
|
|
return;
|
|
}
|
|
}
|
|
// Binary directory
|
|
else if (path[iOffset] == '^')
|
|
{
|
|
if (!Process::GetProcDirectory(buffer))
|
|
{
|
|
result.clear();
|
|
return;
|
|
}
|
|
}
|
|
// Local Aurora profile
|
|
else if (path[iOffset] == '~')
|
|
{
|
|
if (!FS::GetProfileDomain(buffer))
|
|
{
|
|
result.clear();
|
|
return;
|
|
}
|
|
}
|
|
// Global Aurora profile
|
|
else if (path[iOffset] == '!')
|
|
{
|
|
if (!FS::GetSystemDomain(buffer))
|
|
{
|
|
result.clear();
|
|
return;
|
|
}
|
|
}
|
|
else if (path[iOffset] == '?')
|
|
{
|
|
// ...except for this one
|
|
isResource = true;
|
|
}
|
|
else
|
|
{
|
|
buffer.insert(buffer.begin(), path.begin() + iOffset, path.begin() + iOffset + 2); // ???
|
|
}
|
|
|
|
buffer.insert(buffer.end(), path.begin() + iOffset + 2, path.end());
|
|
}
|
|
else if (iOffset)
|
|
{
|
|
buffer.insert(buffer.end(), path.begin() + iOffset, path.end());
|
|
}
|
|
else
|
|
{
|
|
buffer += path;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buffer += path;
|
|
}
|
|
|
|
if (requireSanitization)
|
|
{
|
|
/**
|
|
Quickly handle the edge case in some modern UNIX derived systems, and in
|
|
Windows UNC paths, where paths may start with '//' or '\\' respectively
|
|
*/
|
|
if (AuStartsWith(buffer, kDoublePathSplitter))
|
|
{
|
|
result += kDoublePathSplitter;
|
|
}
|
|
else if (buffer.size() && buffer[0] == kPathSplitter)
|
|
{
|
|
result += kPathSplitter;
|
|
}
|
|
|
|
/**
|
|
Technically, UTF-8 strings may contain the "NUL" byte.
|
|
|
|
o Character numbers from U+0000 to U+007F (US-ASCII repertoire)
|
|
correspond to octets 00 to 7F (7 bit US-ASCII paths). A direct
|
|
consequence is that a plain ASCII string is also a valid UTF-8
|
|
string.
|
|
|
|
You know what would suck? If we were targeting an esoteric platform,
|
|
say a kernel, and we we're trying to avoid string exploits by potential
|
|
attackers. I'm sure that would never happen to poor anticheat devs.
|
|
*/
|
|
const auto parts = AuSplitString(buffer, AuString(1, kPathSplitter), true /*ignore empty tokens*/); /// zzzz im going to SLEEP
|
|
for (const auto &ch : parts) // can you tell why FIO shouldn't be in hot paths yet?
|
|
{
|
|
if (ch == "..")
|
|
{
|
|
if (!result.size()) continue;
|
|
|
|
auto i = result.size() - 1;
|
|
|
|
if (i != 0)
|
|
{
|
|
i -= (result[result.size() - 1] == kPathSplitter);
|
|
}
|
|
|
|
while (i > 0 && result[i] != kPathSplitter)
|
|
{
|
|
--i;
|
|
}
|
|
|
|
if (i >= 0)
|
|
{
|
|
result.resize(i);
|
|
result += kPathSplitter;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ((ch.size() == 1) && (IsMagicCharacter(ch[0])))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result += ch;
|
|
result += kPathSplitter;
|
|
}
|
|
|
|
auto i = result.size() - 1;
|
|
if (result[i] == kPathSplitter)
|
|
{
|
|
result.resize(i);
|
|
}
|
|
}
|
|
else // !requireSanitization
|
|
{
|
|
result = buffer;
|
|
}
|
|
|
|
if (isResource)
|
|
{
|
|
AuString path;
|
|
if (!FS::GetSystemResourcePath(result, path))
|
|
{
|
|
result.clear();
|
|
}
|
|
else
|
|
{
|
|
result = path;
|
|
}
|
|
}
|
|
|
|
#if defined(AURORA_IS_MODERNNT_DERIVED)
|
|
if (result.size() >= MAX_PATH)
|
|
{
|
|
if (!AuStartsWith(result, "\\\\") &&
|
|
result[1] == ':')
|
|
{
|
|
result = "\\\\?\\" + result;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void /* internal, local export */ _NormalizePath(AuString &str)
|
|
{
|
|
bool requiresExpanding = false;
|
|
bool requiresMountUpdate = false;
|
|
|
|
requiresExpanding = str.size() && IsMagicCharacter(str[0]);
|
|
|
|
if (str.size() == 1)
|
|
{
|
|
if (str[0] == '.'
|
|
// Win32 has no concept of a rootfs
|
|
// However, all users expect paths in userspace programs to assume CWD
|
|
// Since we are not on a NIX operating system, we can assume these mean CWD
|
|
#if defined(AURORA_IS_MODERNNT_DERIVED)
|
|
||
|
|
str[0] == '/' ||
|
|
str[1] == '\\'
|
|
#endif
|
|
)
|
|
{
|
|
AuProcess::GetWorkingDirectory(str);
|
|
return;
|
|
}
|
|
|
|
if (str[0] == '^')
|
|
{
|
|
AuProcess::GetProcDirectory(str);
|
|
return;
|
|
}
|
|
|
|
if (str[0] == '~')
|
|
{
|
|
AuFS::GetProfileDomain(str);
|
|
return;
|
|
}
|
|
|
|
if (str[0] == '!')
|
|
{
|
|
AuFS::GetSystemDomain(str);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// best case -> O(n) wherein we merely check each BYTE with a handful of branch conditions
|
|
int doubleDots = 0;
|
|
int doubleSlash = 0;
|
|
for (auto &character : str)
|
|
{
|
|
if ((character == '\\') || (character == '/'))
|
|
{
|
|
character = kPathSplitter;
|
|
doubleSlash++;
|
|
}
|
|
else
|
|
{
|
|
doubleSlash = 0;
|
|
}
|
|
|
|
if (character == '.')
|
|
{
|
|
doubleDots++;
|
|
}
|
|
else
|
|
{
|
|
doubleDots = 0;
|
|
}
|
|
|
|
requiresExpanding |= doubleDots >= 2;
|
|
requiresExpanding |= doubleSlash >= 2;
|
|
}
|
|
|
|
// plus this, i guess
|
|
if (str.size() >= 2)
|
|
{
|
|
if ((str[1] == '\\') || (str[1] == '/'))
|
|
{
|
|
auto c = str[0];
|
|
if ((c == '.') || (c == '~') || (c == '!') || (c == '?' || (c == '^')))
|
|
{
|
|
requiresMountUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!requiresMountUpdate)
|
|
{
|
|
requiresMountUpdate = AuStartsWith(str, "file://");
|
|
}
|
|
|
|
// worst case -> yea have fun
|
|
if (requiresExpanding || requiresMountUpdate)
|
|
{
|
|
AuString temp;
|
|
ResolveAbsolutePath(requiresMountUpdate, requiresExpanding, str, temp);
|
|
str = temp;
|
|
}
|
|
}
|
|
|
|
AUKN_SYM bool ReadString(const AuString &path, AuString &buffer)
|
|
{
|
|
AuByteBuffer fileBuffer;
|
|
|
|
if (!ReadFile(path, fileBuffer))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Locale::Encoding::DecodeUTF8(fileBuffer.data(), fileBuffer.size(), buffer, Locale::ECodePage::eUTF8).first != 0;
|
|
}
|
|
|
|
AUKN_SYM bool WriteString(const AuString &path, const AuString &str)
|
|
{
|
|
char bom[3]
|
|
{
|
|
'\xEF', '\xBB', '\xBF'
|
|
};
|
|
|
|
auto pStream = FS::OpenWriteUnique(path);
|
|
if (!pStream)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool ok {};
|
|
ok = pStream->Write(AuMemoryViewStreamRead { bom });
|
|
ok &= pStream->Write(AuMemoryViewStreamRead { str });
|
|
pStream->Flush();
|
|
pStream->WriteEoS();
|
|
|
|
return ok;
|
|
}
|
|
|
|
AUKN_SYM bool WriteNewString(const AuString &path, const AuString &str)
|
|
{
|
|
AuIO::IOHandle handle;
|
|
bool bOk {};
|
|
AuUInt uLength {};
|
|
|
|
static const char bom[3]
|
|
{
|
|
'\xEF', '\xBB', '\xBF'
|
|
};
|
|
|
|
auto createRequest = AuIO::IIOHandle::HandleCreate::ReadWrite(path);
|
|
createRequest.bAlwaysCreateDirTree = true;
|
|
createRequest.bFailIfNonEmptyFile = true;
|
|
if (!handle->InitFromPath(createRequest))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto pStream = FS::OpenBlockingFileStreamFromHandle(AuUnsafeRaiiToShared(handle.AsPointer()));
|
|
if (!pStream)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bOk = pStream->Write(AuMemoryViewStreamRead { bom });
|
|
bOk &= pStream->Write(AuMemoryViewStreamRead { str, uLength });
|
|
|
|
bOk &= uLength == str.length();
|
|
|
|
pStream->WriteEoS();
|
|
pStream->Flush();
|
|
|
|
return bOk;
|
|
}
|
|
|
|
AUKN_SYM bool WriteNewFile(const AuString &path, const Memory::MemoryViewRead &blob)
|
|
{
|
|
bool bOk {};
|
|
AuUInt uLength {};
|
|
AuIO::IOHandle handle;
|
|
|
|
auto createRequest = AuIO::IIOHandle::HandleCreate::ReadWrite(path);
|
|
createRequest.bAlwaysCreateDirTree = true;
|
|
createRequest.bFailIfNonEmptyFile = true;
|
|
|
|
if (!handle->InitFromPath(createRequest))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto pStream = FS::OpenBlockingFileStreamFromHandle(AuUnsafeRaiiToShared(handle.AsPointer()));
|
|
if (!pStream)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bOk = pStream->Write(AuMemoryViewStreamRead { blob, uLength });
|
|
pStream->WriteEoS();
|
|
pStream->Flush();
|
|
|
|
bOk &= uLength == blob.length;
|
|
|
|
return bOk;
|
|
}
|
|
|
|
AUKN_SYM bool WriteFile(const AuString &path, const Memory::MemoryViewRead &blob)
|
|
{
|
|
bool bOk {};
|
|
AuUInt uLength {};
|
|
AuIO::IOHandle handle;
|
|
auto createRequest = AuIO::IIOHandle::HandleCreate::ReadWrite(path);
|
|
createRequest.bAlwaysCreateDirTree = true;
|
|
createRequest.bFailIfNonEmptyFile = false;
|
|
|
|
if (!handle->InitFromPath(createRequest))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto pStream = FS::OpenBlockingFileStreamFromHandle(AuUnsafeRaiiToShared(handle.AsPointer()));
|
|
if (!pStream)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bOk = pStream->Write(AuMemoryViewStreamRead { blob, uLength });
|
|
pStream->WriteEoS();
|
|
pStream->Flush();
|
|
|
|
bOk &= uLength == blob.length;
|
|
return bOk;
|
|
}
|
|
|
|
AUKN_SYM void CopyDirectory(const AuList<AuPair<AuString, AuString>> &pendingWork,
|
|
bool bUseResult,
|
|
CopyDirResult *out)
|
|
{
|
|
AU_DEBUG_MEMCRUNCH;
|
|
SysCheckArgNotNull(out, );
|
|
|
|
AuList<AuPair<AuString, AuString>> copyWork = pendingWork;
|
|
|
|
while (copyWork.size())
|
|
{
|
|
auto now = AuExchange(copyWork, {});
|
|
|
|
for (const auto &[rawPath, rawPathDest] : now)
|
|
{
|
|
AuList<AuString> files;
|
|
if (AuFS::FilesInDirectory(rawPath, files))
|
|
{
|
|
for (const auto &file : files)
|
|
{
|
|
if (!AuFS::Copy(rawPath + AuString({ AuFS::kPathSplitter }) + file, rawPathDest + AuString({ AuFS::kPathSplitter }) + file))
|
|
{
|
|
if (!bUseResult)
|
|
{
|
|
SysPushErrorIO("failed to copy: {}", file);
|
|
}
|
|
else
|
|
{
|
|
out->errorCopyPaths.push_back(rawPath + AuString({ AuFS::kPathSplitter }) + file);
|
|
}
|
|
}
|
|
else if (bUseResult)
|
|
{
|
|
out->copyPathsSuccess.push_back(rawPathDest + AuString({ AuFS::kPathSplitter }) + file);
|
|
}
|
|
}
|
|
}
|
|
|
|
AuList<AuString> dirs;
|
|
if (AuFS::DirsInDirectory(rawPath, dirs))
|
|
{
|
|
for (const auto &dir : dirs)
|
|
{
|
|
copyWork.push_back(AuMakePair(rawPath + AuString({ AuFS::kPathSplitter }) + dir, rawPathDest + AuString({ AuFS::kPathSplitter }) + dir));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bUseResult)
|
|
{
|
|
out->errorTraversePaths.push_back(rawPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AUKN_SYM void MoveDirectory(const AuList<AuPair<AuString, AuString>> &pendingWork,
|
|
bool bUseResult,
|
|
CopyDirResult *out)
|
|
{
|
|
AU_DEBUG_MEMCRUNCH;
|
|
SysCheckArgNotNull(out, );
|
|
|
|
AuList<AuPair<AuString, AuString>> copyWork = pendingWork;
|
|
AuList<AuString> dirsAll;
|
|
|
|
while (copyWork.size())
|
|
{
|
|
auto now = AuExchange(copyWork, {});
|
|
|
|
for (const auto &[rawPath, rawPathDest] : now)
|
|
{
|
|
auto rawPathA = NormalizePathRet(rawPath);
|
|
|
|
if (rawPathA.empty())
|
|
{
|
|
out->errorCopyPaths.push_back(rawPath);
|
|
continue;
|
|
}
|
|
|
|
#if defined(AURORA_PLATFORM_WIN32)
|
|
{
|
|
auto winLinkA = AuLocale::ConvertFromUTF8(rawPathA);
|
|
auto winLinkB = AuLocale::ConvertFromUTF8(NormalizePathRet(rawPathDest));
|
|
|
|
if (winLinkA.empty() ||
|
|
winLinkB.empty())
|
|
{
|
|
out->errorCopyPaths.push_back(rawPath);
|
|
continue;
|
|
}
|
|
|
|
if (::MoveFileExW(winLinkA.c_str(), winLinkB.c_str(), MOVEFILE_WRITE_THROUGH))
|
|
{
|
|
out->copyPathsSuccess.push_back(rawPath);
|
|
continue;
|
|
}
|
|
}
|
|
#endif
|
|
AuList<AuString> files;
|
|
if (AuFS::FilesInDirectory(rawPath, files))
|
|
{
|
|
for (const auto &file : files)
|
|
{
|
|
if (!AuFS::Relink(rawPath + AuString({ AuFS::kPathSplitter }) + file, rawPathDest + AuString({ AuFS::kPathSplitter }) + file))
|
|
{
|
|
if (!bUseResult)
|
|
{
|
|
SysPushErrorIO("failed to move: {}", file);
|
|
}
|
|
else
|
|
{
|
|
out->errorCopyPaths.push_back(rawPath + AuString({ AuFS::kPathSplitter }) + file);
|
|
}
|
|
}
|
|
else if (bUseResult)
|
|
{
|
|
out->copyPathsSuccess.push_back(rawPathDest + AuString({ AuFS::kPathSplitter }) + file);
|
|
}
|
|
}
|
|
}
|
|
|
|
AuList<AuString> dirs;
|
|
if (AuFS::DirsInDirectory(rawPath, dirs))
|
|
{
|
|
for (const auto &dir : dirs)
|
|
{
|
|
auto fullDirPath = rawPath + AuString({ AuFS::kPathSplitter }) + dir;
|
|
copyWork.push_back(AuMakePair(fullDirPath, rawPathDest + AuString({ AuFS::kPathSplitter }) + dir));
|
|
dirsAll.push_back(fullDirPath);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bUseResult)
|
|
{
|
|
out->errorTraversePaths.push_back(rawPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool bSuccess {};
|
|
do
|
|
{
|
|
bSuccess = false;
|
|
|
|
for (auto itr = dirsAll.begin();
|
|
itr != dirsAll.end();
|
|
)
|
|
{
|
|
if (AuFS::Remove(*itr))
|
|
{
|
|
bSuccess |= true;
|
|
itr = dirsAll.erase(itr);
|
|
}
|
|
else
|
|
{
|
|
itr++;
|
|
}
|
|
}
|
|
}
|
|
while (bSuccess &&
|
|
dirsAll.size());
|
|
|
|
for (const auto &dir : dirsAll)
|
|
{
|
|
if (AuFS::DirExists(dir))
|
|
{
|
|
out->errorTraversePaths.push_back(dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
AUKN_SYM bool NormalizePath(AuString &out, const AuString &in)
|
|
{
|
|
try
|
|
{
|
|
out = NormalizePathRet(in);
|
|
return out.size();
|
|
}
|
|
catch (...)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static AuUInt GetLastSplitterIndex(const AuString &path)
|
|
{
|
|
AuUInt indexA {}, indexB {};
|
|
|
|
auto a = path.find_last_of('\\');
|
|
if (a != AuString::npos) indexA = a;
|
|
|
|
auto b = path.find_last_of('/');
|
|
if (b != AuString::npos) indexB = b;
|
|
|
|
return AuMax(indexA, indexB);
|
|
}
|
|
|
|
AUKN_SYM bool GetFileFromPath(AuString &out, const AuString &path)
|
|
{
|
|
try
|
|
{
|
|
if (path.empty()) return false;
|
|
if (path[path.size() - 1] == '.') return false;
|
|
|
|
AuUInt max = GetLastSplitterIndex(path);
|
|
if (max == path.size()) return false;
|
|
|
|
out = path.substr(max + 1);
|
|
return true;
|
|
}
|
|
catch (...)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AUKN_SYM bool GetDirectoryFromPath(AuString &out, const AuString &path)
|
|
{
|
|
try
|
|
{
|
|
if (path.empty()) return false;
|
|
if (path[path.size() - 1] == '.')
|
|
{
|
|
if (path.size() > 2 && (path[path.size() - 2] == '\\' || path[path.size() - 2] == '/'))
|
|
{
|
|
out = path.substr(0, path.size() - 1);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AuUInt max = GetLastSplitterIndex(path);
|
|
if (!max)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (max == path.size())
|
|
{
|
|
out = path;
|
|
return true;
|
|
}
|
|
|
|
out = path.substr(0, max + 1);
|
|
return true;
|
|
}
|
|
catch (...)
|
|
{
|
|
out.clear();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AUKN_SYM bool GoUpToSeparator(AuString &out, const AuString &path)
|
|
{
|
|
try
|
|
{
|
|
if (path.empty()) return false;
|
|
if (path[path.size() - 1] == '.')
|
|
{
|
|
if (path.size() > 2 && (path[path.size() - 2] == '\\' || path[path.size() - 2] == '/'))
|
|
{
|
|
out = path.substr(0, path.size() - 2);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AuUInt max = GetLastSplitterIndex(path);
|
|
if (!max)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (max == path.size())
|
|
{
|
|
out = path.substr(0, max - 1);
|
|
return true;
|
|
}
|
|
|
|
out = path.substr(0, max);
|
|
return true;
|
|
}
|
|
catch (...)
|
|
{
|
|
out.clear();
|
|
return false;
|
|
}
|
|
}
|
|
} |