/*** Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: FS.cpp Date: 2021-6-16 Author: Reece ***/ #include #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 (auto pProcPath = Process::GetProcessDirectory()) { buffer = *pProcPath; } else { result.clear(); return; } } // Local Aurora profile else if (path[iOffset] == '~') { if (auto optPath = GetProfileDomain()) { buffer = *optPath; } else { result.clear(); return; } } // Global Aurora profile else if (path[iOffset] == '!') { if (auto optPath = GetSystemDomain()) { buffer = *optPath; } else { 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] == '^') { if (auto pProcPath = Process::GetProcessDirectory()) { str = *pProcPath; } return; } if (str[0] == '~') { if (auto optPath = FS::GetProfileDomain()) { str = *optPath; } return; } if (str[0] == '!') { if (auto optPath = FS::GetSystemDomain()) { str = *optPath; } return; } } // best case -> O(n) wherein we merely check each BYTE with a handful of branch conditions int doubleDots = 0; int doubleSlash = 0; char prevC = '\0'; //for (auto &character : str) for (auto itr = str.begin(); itr != str.end(); itr++) { auto holding = *itr; auto character = holding; if ((character == '\\') || (character == '/')) { // AuFS::Devices will break if this is not correct // Turns out POSIX files can have valid NT separators #if defined(AURORA_IS_POSIX_DERIVED) if (character == '\\' && prevC == '\\') { *(itr - 1) = '\\'; itr = str.erase(itr); doubleSlash--; continue; } else { *itr = kPathSplitter; doubleSlash++; } #else *itr = kPathSplitter; doubleSlash++; #endif } else { doubleSlash = 0; } if (character == '.') { doubleDots++; } else { doubleDots = 0; } requiresExpanding |= doubleDots >= 2; requiresExpanding |= doubleSlash >= 2; prevC = holding; } // 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 AuROString &path, AuString &buffer) { AuByteBuffer fileBuffer; if (!ReadFile(path, fileBuffer)) { return false; } if (fileBuffer.IsEmpty()) { buffer.clear(); return true; } return Locale::Encoding::DecodeUTF8(fileBuffer.data(), fileBuffer.size(), buffer, Locale::ECodePage::eUTF8).first != 0; } AUKN_SYM bool ReadFileHeader(const AuROString &path, AuUInt16 uLength, Memory::ByteBuffer &buffer) { AuUInt uSize {}; auto memView = buffer.GetOrAllocateLinearWriteable(uLength); SysCheckNotNullMemory(memView, false); auto pStream = FS::OpenReadUnique(path); SysCheckNotNullNested(pStream, false); SysCheckNotNull(pStream->Read({ memView, uSize }), false); buffer.writePtr += uSize; return true; } AUKN_SYM bool WriteString(const AuROString &path, const AuROString &str) { char bom[3] { '\xEF', '\xBB', '\xBF' }; auto pStream = FS::OpenWriteUnique(path); if (!pStream) { return false; } bool bOk {}; bOk = pStream->Write(AuMemoryViewStreamRead { bom }); if (auto uLength = str.length()) { AuUInt uOutLength {}; bOk &= pStream->Write(AuMemoryViewStreamRead { str, uOutLength }); bOk &= uOutLength == uLength; } else { bOk = true; } pStream->Flush(); pStream->WriteEoS(); return bOk; } AUKN_SYM bool WriteNewString(const AuROString &path, const AuROString &str) { static const char bom[3] { '\xEF', '\xBB', '\xBF' }; bool bOk {}; { AuIO::IOHandle handle; auto createRequest = AuIO::IIOHandle::HandleCreate::ReadWrite(path); createRequest.bAlwaysCreateDirTree = true; createRequest.bFailIfNonEmptyFile = true; if (!handle->InitFromPath(createRequest)) { return false; } auto pStream = OpenBlockingFileStreamFromHandleShared(AuUnsafeRaiiToShared(handle.AsPointer())); if (!pStream) { return false; } bOk = pStream->Write(AuMemoryViewStreamRead { bom }); if (auto uLength = str.length()) { AuUInt uOutLength {}; bOk &= pStream->Write(AuMemoryViewStreamRead { str, uOutLength }); bOk &= uOutLength == uLength; } else { bOk = true; } pStream->WriteEoS(); pStream->Flush(); } RuntimeWaitForSecondaryTick(); return bOk; } AUKN_SYM bool WriteNewFile(const AuROString &path, const Memory::MemoryViewRead &blob) { bool bOk {}; { AuIO::IOHandle handle; auto createRequest = AuIO::IIOHandle::HandleCreate::ReadWrite(path); createRequest.bAlwaysCreateDirTree = true; createRequest.bFailIfNonEmptyFile = true; if (!handle->InitFromPath(createRequest)) { return false; } auto pStream = OpenBlockingFileStreamFromHandleShared(AuUnsafeRaiiToShared(handle.AsPointer())); if (!pStream) { return false; } if (blob.length) { AuUInt uLength {}; bOk = pStream->Write(AuMemoryViewStreamRead { blob, uLength }); bOk &= uLength == blob.length; } else { bOk = true; } pStream->WriteEoS(); pStream->Flush(); } RuntimeWaitForSecondaryTick(); return bOk; } AUKN_SYM bool WriteFile(const AuROString &path, const Memory::MemoryViewRead &blob) { bool bOk {}; 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::OpenBlockingFileStreamFromHandleShared(AuUnsafeRaiiToShared(handle.AsPointer())); if (!pStream) { return false; } if (blob.length) { AuUInt uLength {}; bOk = pStream->Write(AuMemoryViewStreamRead { blob, uLength }); bOk &= uLength == blob.length; } else { bOk = true; } pStream->WriteEoS(); pStream->SetFlushOnClose(true); return bOk; } AUKN_SYM void CopyDirectory(const AuList> &pendingWork, bool bUseResult, CopyDirResult *out) { AU_DEBUG_MEMCRUNCH; SysCheckArgNotNull(out, ); AuList> copyWork = pendingWork; while (copyWork.size()) { auto now = AuExchange(copyWork, {}); for (const auto &[rawPath, rawPathDest] : now) { AuList 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); } } } else { if (bUseResult) { out->errorTraversePaths.push_back(rawPath); } else { SysPushErrorIO("failed to copy dir: {}", rawPath); } continue; } AuList 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); } else { SysPushErrorIO("failed to copy dir: {}", rawPath); } } } } } AUKN_SYM void MoveDirectory(const AuList> &pendingWork, bool bUseResult, CopyDirResult *out) { AU_DEBUG_MEMCRUNCH; SysCheckArgNotNull(out, ); AuList> copyWork = pendingWork; AuList 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 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 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); } else { SysPushErrorIO("failed to move dir: {}", 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) && !AuExists(out->errorTraversePaths, dir)) { out->errorTraversePaths.push_back(dir); } } } AUKN_SYM bool NormalizePath(AuString &out, const AuROString &in) { try { out = NormalizePathRet(in); return out.size(); } catch (...) { return false; } } static AuUInt GetLastSplitterIndex(const AuROString &path) { AuUInt indexA {}, indexB {}; auto a = path.find_last_of('\\'); if (a != AuString::npos) { indexA = a; #if defined(AURORA_IS_POSIX_DERIVED) if (a) { if (path[(a - 1)] == '\\') { indexA = 0; } } #endif } auto b = path.find_last_of('/'); if (b != AuString::npos) indexB = b; return AuMax(indexA, indexB); } AUKN_SYM bool GetFileFromPath(AuROString &out, const AuROString &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(AuROString &out, const AuROString &path) { 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; } AUKN_SYM bool GoUpToSeparator(AuROString &out, const AuROString &path) { 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; } }