/*** Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: Resources.cpp Date: 2021-6-16 Author: Reece ***/ #include #include "FS.hpp" #include "Resources.hpp" #if defined(AURORA_PLATFORM_LINUX) #include #include #include #elif defined(AURORA_PLATFORM_WIN32) #include #include #include #endif namespace Aurora::IO::FS { #if defined(AURORA_PLATFORM_WIN32) static void Win32FixGlobalAppDataAcl(const AuString &path); #endif static AuString gHomeDirectory; static AuString gUserHomeDirectory; static AuString gUserWritableAppData; static AuString gGlobalWritableAppDirectory; static AuString gAdminWritableAppDirectory; static AuString gProgramsFolder; static AuString gApplicationData; static AuString gUserShellDocuments; static AuString gUserShellDownloads; static AuString gUserShellPhotos; static AuString gUserShellVideos; static AuString gUserShellMusic; static AuString gUserShellDesktop; static AuOptional gSystemLibPath; static AuOptional gSystemLibPath2; static AuOptional gUserLibPath; static AuOptional gUserLibPath2; static AuString gTempDir; // Should the following be /opt? Probably, if it were a direct replacement for Windows' appdata on Linux for global software packages outside of our ecosystem, sure; however, this is strictly a fallback for when there is no home // We don't support initially-undefined global application configurations across users on Unix targets. We can therefore conclude the application running is a service whose user is without a home, and should be subject to the same rules as daemon deployed by a real package manager // For internal packages, in our own ecosystem of tools, I think this follows the UNIX spec, not that I care what arcahic C-with-vendor-packages-as-an-OS specification says. // The only way you can break this assumption is if you argue for users who will be outside of our deployment pipeline, wanting global configs, and don't have write permission on a relevant global directory. // They can shove it. Superuser should install software for all users. // We should use: // 2> XDG envvars (with a fallback to a home relative path) for non-root installs; for special installs via package manager, use /var; // 2> for root installs of an application whose system configs should be shared amongst all users, unsupported, idc, it's sandboxed per user. // we don't have any good examples of home family computer-esc posix machines static const char * kUnixAppData {"/var"}; AUKN_SYM AuOptional GetSystemDomain() { return gApplicationData.size() ? gApplicationData : AuOptional {}; } AUKN_SYM AuOptional GetProfileDomain() { return gHomeDirectory.size() ? gHomeDirectory : AuOptional {}; } AUKN_SYM bool GetSystemResourcePath(const AuROString &fileName, AuString &path) { path.clear(); if (fileName.find("..") != AuString::npos) { AuLogWarn("Exploit Attempt? A system resource path may not contain relative directory move tokens: {}", fileName); return false; } try { #if defined(AU_CFG_ID_INTERNAL) || defined(AU_CFG_ID_DEBUG) { AuString tempPath; if (Process::GetWorkingDirectory(tempPath)) { tempPath += "/" + AuString(fileName); if (FileExists(tempPath)) { path = tempPath; return true; } } } #endif { if (auto pProcPath = Process::GetProcessDirectory()) { auto tempPath = AuString(*pProcPath) + "/" + AuString(fileName); if (FileExists(tempPath)) { path = tempPath; return true; } } } { if (auto optPackagePath = GetPackagePath()) { auto tempPath = AuString(*optPackagePath) + "/" + AuString(fileName); if (auto pProcPath = Process::GetProcessDirectory()) { auto tempPath2 = AuString(*pProcPath) + "/" + AuString(fileName); if (tempPath2 == tempPath) { // nop } else if (FileExists(tempPath)) { path = tempPath; return true; } } else if (FileExists(tempPath)) { path = tempPath; return true; } } } #if defined(AU_CFG_ID_SHIP) { AuString tempPath; if (Process::GetWorkingDirectory(tempPath)) { tempPath += "/" + AuString(fileName); if (FileExists(tempPath)) { path = tempPath; return true; } } } #endif { auto systemPath = gHomeDirectory + AuString(fileName); if (FileExists(systemPath)) { path = systemPath; return true; } } { auto systemPath = gApplicationData + AuString(fileName); if (FileExists(systemPath)) { path = systemPath; return true; } } } catch (...) { SysPushErrorCatch(); } return false; } #if defined(AURORA_PLATFORM_WIN32) static AuOptional GUIDTOCISL(REFKNOWNFOLDERID rfid) { if (rfid == FOLDERID_RoamingAppData) { return CSIDL_APPDATA; } if (rfid == FOLDERID_ProgramData) { return CSIDL_COMMON_APPDATA; } if (rfid == FOLDERID_Documents || rfid == FOLDERID_Downloads) { return CSIDL_MYDOCUMENTS; } if (rfid == FOLDERID_Music) { return CSIDL_MYMUSIC; } if (rfid == FOLDERID_Pictures) { return CSIDL_MYPICTURES; } if (rfid == FOLDERID_Desktop) { return CSIDL_DESKTOP; } if (rfid == FOLDERID_Videos) { return CSIDL_MYVIDEO; } if (rfid == FOLDERID_System || rfid == FOLDERID_SystemX86) { return CSIDL_SYSTEM; } if (rfid == FOLDERID_Profile) { return CSIDL_PROFILE; } return {}; } static AuString GetSpecialDirOldOS(REFKNOWNFOLDERID rfid) { if (!pSHGetFolderPathA) { return ""; } if (auto opt = GUIDTOCISL(rfid)) { AuString temp(MAX_PATH, '\x00'); if (pSHGetFolderPathA(0, *opt, 0, 0, temp.data()) == S_OK) { return temp; } } return ""; } static AuString GetSpecialDir(REFKNOWNFOLDERID rfid, bool bNoThrow = false) { PWSTR directory; if (!pSHGetKnownFolderPath) { return GetSpecialDirOldOS(rfid); } if (pSHGetKnownFolderPath(rfid, KF_FLAG_DEFAULT, NULL, &directory) != S_OK) { AuString str; if ((str = GetSpecialDirOldOS(rfid)).size()) { return str; } if (rfid == FOLDERID_UserProgramFiles || bNoThrow) { return ""; } SysPanic("Couldn't get known special directory path of [MS:{}-{}-{}-{}{}{}{}{}{}{}{}] with a NULL access token", rfid.Data1, rfid.Data2, rfid.Data3, rfid.Data4[0], rfid.Data4[1], rfid.Data4[2], rfid.Data4[3], rfid.Data4[4], rfid.Data4[5], rfid.Data4[6], rfid.Data4[7]); } auto ret = Locale::ConvertFromWChar(directory); if (pCoTaskMemFree) { pCoTaskMemFree(directory); } return ret; } static void SetNamespaceDirectories() { gHomeDirectory = GetSpecialDir(FOLDERID_RoamingAppData); gApplicationData = GetSpecialDir(FOLDERID_ProgramData); if constexpr (AuBuild::IsPlatformX32()) { gSystemLibPath = GetSpecialDir(FOLDERID_SystemX86); } else { gSystemLibPath = GetSpecialDir(FOLDERID_System); } gUserHomeDirectory = GetSpecialDir(FOLDERID_Profile); gAdminWritableAppDirectory = gApplicationData; gUserWritableAppData = gHomeDirectory; gProgramsFolder = AuSwInfo::IsWindows7OrGreater() ? GetSpecialDir(FOLDERID_UserProgramFiles) : ""; if (gProgramsFolder.empty()) { gProgramsFolder = GetSpecialDir(FOLDERID_Documents); if (gProgramsFolder.size()) { gProgramsFolder += "\\Programs"; if (!AuFS::DirExists(gProgramsFolder)) { AuFS::DirMk(gProgramsFolder); } } } gUserShellDocuments = GetSpecialDir(FOLDERID_Documents, true); gUserShellDownloads = GetSpecialDir(FOLDERID_Downloads, true); gUserShellPhotos = GetSpecialDir(FOLDERID_Pictures, true); gUserShellVideos = GetSpecialDir(FOLDERID_Videos, true); gUserShellMusic = GetSpecialDir(FOLDERID_Music, true); gUserShellDesktop = GetSpecialDir(FOLDERID_Desktop, true); if (pGetTempPathW) { wchar_t tempPath[2048]; if (auto uLength = pGetTempPathW(AuArraySize(tempPath), tempPath)) { gTempDir = AuLocale::ConvertFromWChar(tempPath, uLength); } } if (gTempDir.empty()) { gTempDir = GetSpecialDir(FOLDERID_Documents) + "/Temp/"; } } #elif defined(AURORA_PLATFORM_LINUX) || defined(AURORA_PLATFORM_BSD) static void SetUnixPaths(AuOptional &primary, AuOptional &secondary, const AuString &base) { primary = base; if (Aurora::Build::IsPlatformX32()) { secondary = base + "32"; } else { secondary = base + "64"; } if (DirExists(*secondary)) { AuSwap(secondary, primary); } else { secondary.reset(); } if (!DirExists(*primary)) { primary.reset(); } } static void SetXdg(AuString &out, const char *envvar, const char *home, const char *defaultHomeExt, const char *fallback) { auto value = getenv(envvar); if (value) { out = value; } else if (home) { out = AuString(home) + defaultHomeExt; } else { out = fallback; } } static void SetPosixOptionalUserShellDir(AuString &str, const AuROString &suffix) { AuString strA = gUserHomeDirectory + "/" + AuString({ AuToUpper(suffix[0]) }) + AuString(suffix.SubStr(1)); AuString strB = gUserHomeDirectory + "/" + AuString({ AuToLower(suffix[0]) }) + AuString(suffix.SubStr(1)); if (AuFS::DirExists(strA)) { str = strA; } else if (AuFS::DirExists(strB)) { str = strB; } } static void SetNamespaceDirectories() { const char *homedir; homedir = getenv("HOME"); if (!homedir) { homedir = getpwuid(getuid())->pw_dir; } gUserHomeDirectory = homedir; // XDG Base Directory Specification // $XDG_CONFIG_HOME defines the base directory relative to which user-specific configuration files should be stored. If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used. // $XDG_DATA_HOME defines the base directory relative to which user-specific data files should be stored SetXdg(gApplicationData, "XDG_CONFIG_HOME", homedir, "/.config", kUnixAppData); SetXdg(gHomeDirectory, "XDG_DATA_HOME", homedir, "/.local/share", "."); // https://www.freedesktop.org/wiki/Software/systemd/TheCaseForTheUsrMerge/ // Arch 2012: https://archlinux.org/news/the-lib-directory-becomes-a-symlink/ // Ubuntu 2018 (mandatory 2021+): https://lists.ubuntu.com/archives/ubuntu-devel-announce/2018-November/001253.html https://wiki.debian.org/UsrMerge // Fedora (2011?): https://fedoraproject.org/wiki/Features/UsrMove SetUnixPaths(gUserLibPath, gUserLibPath2, "/usr/local/lib"); SetUnixPaths(gSystemLibPath, gSystemLibPath2, "/usr/lib"); gProgramsFolder = "/opt"; gGlobalWritableAppDirectory = "/opt"; gAdminWritableAppDirectory = kUnixAppData; gUserWritableAppData = gApplicationData; SetPosixOptionalUserShellDir(gUserShellDocuments, "Documents"); SetPosixOptionalUserShellDir(gUserShellDownloads, "Downloads"); SetPosixOptionalUserShellDir(gUserShellPhotos, "Pictures"); SetPosixOptionalUserShellDir(gUserShellVideos, "Videos"); SetPosixOptionalUserShellDir(gUserShellMusic, "Music"); SetPosixOptionalUserShellDir(gUserShellDesktop, "Desktop"); // Android: gApplicationData = ANativeActivity::internalDataPath // Android: gHomeDirectory = ANativeActivity::externalDataPath (?) // Android: SetPosixOptionalUserShellDir maybe overload the home path with emulated 0 DCIM if we have external file permissions? externalDataPath? // iOS Package Dir: CFBundleCopyResourcesDirectoryURL // iOS/MacOS : NSTemporaryDirectory #if defined(AURORA_PLATFORM_ANDROID) gTempDir = "/data/local/tmp/"; #else gTempDir = "/tmp/"; #endif } #else static void SetNamespaceDirectories() { gHomeDirectory = "."; gApplicationData = "."; } #endif AuOptional GetSystemLibPath() { return gSystemLibPath; } AuOptional GetUserLibPath() { return gUserLibPath; } AuOptional GetSystemLibPath2() { return gSystemLibPath2; } AuOptional GetUserLibPath2() { return gUserLibPath2; } static void ChangeDir() { #if !defined(AU_NO_AU_HOME_BRANDING) if (gRuntimeConfig.fio.optDefaultBrand) { #if !defined(AURORA_PLATFORM_WIN32) gApplicationData += "/" + gRuntimeConfig.fio.optDefaultBrand.value(); #endif gHomeDirectory += "/" + gRuntimeConfig.fio.optDefaultBrand.value(); gProgramsFolder += "/" + gRuntimeConfig.fio.optDefaultBrand.value(); } #endif #if defined(AURORA_PLATFORM_WIN32) gApplicationData += "\\AllUsers"; if (!FS::DirExists(gApplicationData)) { if (FS::DirMk(gApplicationData)) { Win32FixGlobalAppDataAcl(gApplicationData); } } #if defined(AURORA_PLATFORM_WIN32) if (gRuntimeConfig.fio.optDefaultBrand) { gApplicationData += "\\" + gRuntimeConfig.fio.optDefaultBrand.value(); } #endif FS::DirMk(gApplicationData); #endif NormalizePath(gProgramsFolder); NormalizePath(gHomeDirectory); if ((gApplicationData == gHomeDirectory) || (gApplicationData == gProgramsFolder)) { gApplicationData += kPathSplitter; gHomeDirectory += kPathSplitter; gApplicationData += "System"; gHomeDirectory += "Profile"; } // Noting we append a path splitter to prevent hair pulling over missing path delimiters // Eg: GetHome() + "myAwesomeApp/Config" = %HOME%/Aurora/ProfilemyAwsomeApp/Config gApplicationData += kPathSplitter; gHomeDirectory += kPathSplitter; gProgramsFolder += kPathSplitter; static const auto Fixup = [](auto &str) { if (str.empty()) { return; } if (AuEndsWith(str, '\\')) { return; } if (AuEndsWith(str, '/')) { return; } str += kPathSplitter; }; Fixup(gUserHomeDirectory); Fixup(gAdminWritableAppDirectory); Fixup(gUserWritableAppData); Fixup(gGlobalWritableAppDirectory); } void InitResources() { DeinitResources(); SetNamespaceDirectories(); ChangeDir(); } void DeinitResources() { gHomeDirectory.clear(); gUserHomeDirectory.clear(); gUserWritableAppData.clear(); gGlobalWritableAppDirectory.clear(); gAdminWritableAppDirectory.clear(); gProgramsFolder.clear(); gApplicationData.clear(); } AUKN_SYM AuOptional GetAppData() { return gUserWritableAppData.size() ? gUserWritableAppData : AuOptional {}; } AUKN_SYM AuOptional GetUserHome() { return gUserHomeDirectory.size() ? gUserHomeDirectory : AuOptional {}; } AUKN_SYM AuOptional GetPackagePath() { // TODO: iOS/mac OS -> CFBundleCopyResourcesDirectoryURL if (auto optProcessDirectory = Process::GetProcessDirectory()) { return optProcessDirectory; } return AuOptional{}; } AUKN_SYM AuOptional GetWritableAppdata() { return gGlobalWritableAppDirectory.size() ? gGlobalWritableAppDirectory : AuOptional {}; } AUKN_SYM AuOptional GetRootAppdata() { return gAdminWritableAppDirectory.size() ? gAdminWritableAppDirectory : AuOptional {}; } AUKN_SYM AuOptional GetUserProgramsFolder() { return gProgramsFolder.size() ? gProgramsFolder : AuOptional {}; } AUKN_SYM AuOptional GetUserDocuments() { return gUserShellDocuments.size() ? gUserShellDocuments : AuOptional {}; } AUKN_SYM AuOptional GetUserDesktop() { return gUserShellDesktop.size() ? gUserShellDesktop : AuOptional {}; } AUKN_SYM AuOptional GetUserPhotos() { return gUserShellPhotos.size() ? gUserShellPhotos : AuOptional {}; } AUKN_SYM AuOptional GetUserVideos() { return gUserShellVideos.size() ? gUserShellVideos : AuOptional {}; } AUKN_SYM AuOptional GetUserMusic() { return gUserShellMusic.size() ? gUserShellMusic : AuOptional {}; } AUKN_SYM AuOptional GetUserDownloads() { return gUserShellDownloads.size() ? gUserShellDownloads : GetUserDocuments(); } AUKN_SYM AuOptional NewTempFile() { try { AuString path; path = gTempDir; if (path.empty()) { SysPushErrorUninitialized(); return {}; } path += fmt::format("TempFile_{}", AuRNG::ReadString(64, AuRNG::ERngStringCharacters::eAlphaNumericCharacters)); if (!AuFS::WriteNewFile(path, {})) { SysPushErrorIO(); return {}; } auto normalizedPath = AuFS::NormalizePathRet(path); if (normalizedPath.empty() && path.size()) { SysPushErrorMemory(); return {}; } #if defined(AURORA_PLATFORM_WIN32) auto widePath = AuLocale::ConvertFromUTF8(normalizedPath); MoveFileExW(widePath.c_str(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT); #endif return normalizedPath; } catch (...) { // I hate C++ strings SysPushErrorCatch(); return {}; } } AUKN_SYM AuOptional NewTempDirectory() { try { AuString path; path = gTempDir; if (path.empty()) { SysPushErrorUninitialized(); return {}; } path += fmt::format("TempDirectory_{}", AuRNG::ReadString(32, AuRNG::ERngStringCharacters::eAlphaNumericCharacters)); if (!_MkDir(path)) { SysPushErrorIO(); return {}; } auto normalizedPath = AuFS::NormalizePathRet(path); if (normalizedPath.empty() && path.size()) { SysPushErrorMemory(); return {}; } #if defined(AURORA_PLATFORM_WIN32) auto widePath = AuLocale::ConvertFromUTF8(normalizedPath); MoveFileExW(widePath.c_str(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT); #endif return normalizedPath + AuString(1, kPathSplitter); } catch (...) { // I hate C++ strings SysPushErrorCatch(); return {}; } } #if defined(AURORA_PLATFORM_WIN32) static void Win32FixGlobalAppDataAcl(const AuString &path) { BOOL bRetval = FALSE; HANDLE hToken = NULL; PSID pSIDEveryone = NULL; PACL pACL = NULL; SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY; const int NUM_ACES = 1; EXPLICIT_ACCESS_A ea[NUM_ACES]; DWORD dwRes; if (!pAllocateAndInitializeSid) { SysPushErrorFIO("AllocateAndInitializeSid (Everyone) error"); goto Cleanup; } // Specify the DACL to use. // Create a SID for the Everyone group. if (!pAllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &pSIDEveryone)) { SysPushErrorFIO("AllocateAndInitializeSid (Everyone) error"); goto Cleanup; } ZeroMemory(&ea, NUM_ACES * sizeof(EXPLICIT_ACCESS_A)); // Set read access for Everyone. ea[0].grfAccessPermissions = GENERIC_ALL; ea[0].grfAccessMode = SET_ACCESS; ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ea[0].Trustee.ptstrName = (LPTSTR) pSIDEveryone; if (!pSetEntriesInAclA) { SysPushErrorFIO("Failed SetEntriesInAcl"); goto Cleanup; } if (ERROR_SUCCESS != pSetEntriesInAclA(NUM_ACES, ea, NULL, &pACL)) { SysPushErrorFIO("Failed SetEntriesInAcl"); goto Cleanup; } if (!pSetNamedSecurityInfoW) { SysPushErrorFIO("Failed SetNamedSecurityInfoW"); goto Cleanup; } // Try to modify the object's DACL. dwRes = pSetNamedSecurityInfoW( Locale::ConvertFromUTF8(FS::NormalizePathRet(path)).data(), // name of the object SE_FILE_OBJECT, // type of object DACL_SECURITY_INFORMATION, // change only the object's DACL NULL, NULL, // do not change owner or group pACL, // DACL specified NULL); // do not change SACL if (ERROR_SUCCESS == dwRes) { bRetval = TRUE; // No more processing needed. goto Cleanup; } if (dwRes != ERROR_ACCESS_DENIED) { SysPushErrorFIO("First SetNamedSecurityInfo call failed: {}", dwRes); goto Cleanup; } Cleanup: if (pSIDEveryone) { if (pFreeSid) { pFreeSid(pSIDEveryone); } } if (pACL) { LocalFree(pACL); } if (hToken) { AuWin32CloseHandle(hToken); } if (!bRetval) { AuLogError("Couldn't grant ownership to EVERYONE; System wide configuration directory {} will be inaccessible to other users", path); } } #endif }