AuroraRuntime/Source/IO/FS/FS.cpp

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;
}
}
}