/* * Copyright (c) Atmosphère-NX * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #if defined(ATMOSPHERE_OS_WINDOWS) #include #include #include #elif defined(ATMOSPHERE_OS_LINUX) || defined(ATMOSPHERE_OS_MACOS) #include #include #include #include #include #include #endif #if defined(ATMOSPHERE_OS_LINUX) #include #elif defined(ATMOSPHERE_OS_MACOS) extern "C" ssize_t __getdirentries64(int fd, char *buffer, size_t buffer_size, uintptr_t *basep); #endif #if !defined(ATMOSPHERE_OS_HORIZON) namespace ams::fssystem { namespace { constexpr s64 NanoSecondsPerWindowsTick = 100; constexpr s64 WindowsTicksPerSecond = TimeSpan::FromSeconds(1).GetNanoSeconds() / TimeSpan::FromNanoSeconds(NanoSecondsPerWindowsTick).GetNanoSeconds(); constexpr s64 OffsetToConvertToPosixTime = 11644473600; [[maybe_unused]] constexpr ALWAYS_INLINE s64 ConvertWindowsTimeToPosixTime(s64 windows_ticks) { return (windows_ticks / WindowsTicksPerSecond) - OffsetToConvertToPosixTime; } [[maybe_unused]] constexpr ALWAYS_INLINE s64 ConvertPosixTimeToWindowsTime(s64 posix_sec, s64 posix_ns = 0) { return ((posix_sec + OffsetToConvertToPosixTime) * WindowsTicksPerSecond) + util::DivideUp(posix_ns, NanoSecondsPerWindowsTick); } #if defined(ATMOSPHERE_OS_WINDOWS) constexpr int MaxFilePathLength = MAX_PATH - 1; constexpr int MaxDirectoryPathLength = MaxFilePathLength - (8 + 1 + 3); static_assert(MaxFilePathLength == 259); static_assert(MaxDirectoryPathLength == 247); bool AreLongPathsEnabledImpl() { /* Get handle to ntdll. */ const HMODULE module = ::GetModuleHandleW(L"ntdll"); if (module == nullptr) { return true; } /* Get function pointer to long paths enabled. */ const auto enabled_funcptr = ::GetProcAddress(module, "RtlAreLongPathsEnabled"); if (enabled_funcptr == nullptr) { return true; } /* Get whether long paths are enabled. */ using FunctionType = BOOLEAN (NTAPI *)(); return reinterpret_cast(reinterpret_cast(enabled_funcptr))(); } Result ConvertLastErrorToResult() { switch (::GetLastError()) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_NO_MORE_FILES: case ERROR_BAD_NETPATH: case ERROR_BAD_NET_NAME: case ERROR_DIRECTORY: case ERROR_BAD_DEVICE: case ERROR_CONNECTION_UNAVAIL: case ERROR_NO_NET_OR_BAD_PATH: case ERROR_NOT_CONNECTED: R_THROW(fs::ResultPathNotFound()); case ERROR_ACCESS_DENIED: case ERROR_SHARING_VIOLATION: R_THROW(fs::ResultTargetLocked()); case ERROR_HANDLE_EOF: R_THROW(fs::ResultOutOfRange()); case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: R_THROW(fs::ResultPathAlreadyExists()); case ERROR_DISK_FULL: case ERROR_SPACES_NOT_ENOUGH_DRIVES: R_THROW(fs::ResultNotEnoughFreeSpace()); case ERROR_DIR_NOT_EMPTY: R_THROW(fs::ResultDirectoryNotEmpty()); case ERROR_BAD_PATHNAME: R_THROW(fs::ResultInvalidPathFormat()); case ERROR_FILENAME_EXCED_RANGE: R_THROW(fs::ResultTooLongPath()); default: //printf("Returning ConvertLastErrorToResult() -> ResultUnexpectedInLocalFileSystemE, last_error=0x%08x\n", static_cast(::GetLastError())); R_THROW(fs::ResultUnexpectedInLocalFileSystemE()); } } Result WaitDeletionCompletion(const wchar_t *native_path) { /* Wait for the path to be deleted. */ constexpr int MaxTryCount = 25; for (int i = 0; i < MaxTryCount; ++i) { /* Get the file attributes. */ const auto attr = ::GetFileAttributesW(native_path); /* If they're not invalid, we're done. */ R_SUCCEED_IF(attr != INVALID_FILE_ATTRIBUTES); /* Get last error. */ const auto err = ::GetLastError(); /* If error was file not found, the delete is complete. */ R_SUCCEED_IF(err == ERROR_FILE_NOT_FOUND); /* If the error was access denied, we want to try again. */ R_UNLESS(err == ERROR_ACCESS_DENIED, ConvertLastErrorToResult()); /* Sleep before checking again. */ ::Sleep(2); } /* We received access denied 25 times in a row. */ R_THROW(fs::ResultTargetLocked()); } Result GetEntryTypeImpl(fs::DirectoryEntryType *out, const wchar_t *native_path) { const auto res = ::GetFileAttributesW(native_path); if (res == INVALID_FILE_ATTRIBUTES) { switch (::GetLastError()) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_ACCESS_DENIED: case ERROR_BAD_NETPATH: case ERROR_BAD_NET_NAME: case ERROR_BAD_DEVICE: case ERROR_CONNECTION_UNAVAIL: case ERROR_NO_NET_OR_BAD_PATH: case ERROR_NOT_CONNECTED: R_THROW(fs::ResultPathNotFound()); default: //printf("Returning GetEntryTypeImpl() -> ResultUnexpectedInLocalFileSystemF, last_error=0x%08x\n", static_cast(::GetLastError())); R_THROW(fs::ResultUnexpectedInLocalFileSystemF()); } } *out = (res & FILE_ATTRIBUTE_DIRECTORY) ? fs::DirectoryEntryType_Directory : fs::DirectoryEntryType_File; R_SUCCEED(); } Result SetFileSizeImpl(HANDLE handle, s64 size) { /* Seek to the desired size. */ LARGE_INTEGER seek; seek.QuadPart = size; R_UNLESS(::SetFilePointerEx(handle, seek, nullptr, FILE_BEGIN) != 0, ConvertLastErrorToResult()); /* Try to set the file size. */ if (::SetEndOfFile(handle) == 0) { /* Check if the error resulted from too large size. */ R_UNLESS(::GetLastError() == ERROR_INVALID_PARAMETER, ConvertLastErrorToResult()); R_UNLESS(size <= INT64_C(0x00000FFFFFFF0000), ConvertLastErrorToResult()); /* The file size is too large. */ R_THROW(fs::ResultTooLargeSize()); } R_SUCCEED(); } class LocalFile : public ::ams::fs::fsa::IFile, public ::ams::fs::impl::Newable { private: const HANDLE m_handle; const fs::OpenMode m_open_mode; public: LocalFile(HANDLE h, fs::OpenMode m) : m_handle(h), m_open_mode(m) { /* ... */ } virtual ~LocalFile() { ::CloseHandle(m_handle); } public: virtual Result DoRead(size_t *out, s64 offset, void *buffer, size_t size, const fs::ReadOption &option) override { /* Check that read is possible. */ size_t dry_read_size; R_TRY(this->DryRead(std::addressof(dry_read_size), offset, size, option, m_open_mode)); /* If we have nothing to read, we don't need to do anything. */ if (dry_read_size == 0) { *out = 0; R_SUCCEED(); } /* Prepare to do asynchronous IO. */ OVERLAPPED overlapped = {}; overlapped.Offset = static_cast(offset); overlapped.OffsetHigh = static_cast(offset >> BITSIZEOF(DWORD)); overlapped.hEvent = ::CreateEvent(nullptr, true, false, nullptr); R_UNLESS(overlapped.hEvent != nullptr, fs::ResultUnexpectedInLocalFileSystemA()); ON_SCOPE_EXIT { ::CloseHandle(overlapped.hEvent); }; /* Read from the file. */ DWORD size_read; if (!::ReadFile(m_handle, buffer, static_cast(size), std::addressof(size_read), std::addressof(overlapped))) { /* If we fail for reason other than io pending, return the error result. */ const auto err = ::GetLastError(); R_UNLESS(err == ERROR_IO_PENDING, ConvertLastErrorToResult()); /* Get the wait result. */ if (!::GetOverlappedResult(m_handle, std::addressof(overlapped), std::addressof(size_read), true)) { /* We failed...check if it's because we're at the end of the file. */ R_UNLESS(::GetLastError() == ERROR_HANDLE_EOF, ConvertLastErrorToResult()); /* Get the file size. */ LARGE_INTEGER file_size; R_UNLESS(::GetFileSizeEx(m_handle, std::addressof(file_size)), ConvertLastErrorToResult()); /* Check the filesize matches offset. */ R_UNLESS(file_size.QuadPart == offset, ConvertLastErrorToResult()); } } /* Set the output read size. */ *out = size_read; R_SUCCEED(); } virtual Result DoGetSize(s64 *out) override { /* Get the file size. */ LARGE_INTEGER size; R_UNLESS(::GetFileSizeEx(m_handle, std::addressof(size)), fs::ResultUnexpectedInLocalFileSystemD()); /* Set the output. */ *out = size.QuadPart; R_SUCCEED(); } virtual Result DoFlush() override { /* If we're not writable, we have nothing to flush. */ R_SUCCEED_IF((m_open_mode & fs::OpenMode_Write) == 0); /* Flush our buffer. */ R_UNLESS(::FlushFileBuffers(m_handle), fs::ResultUnexpectedInLocalFileSystemC()); R_SUCCEED(); } virtual Result DoWrite(s64 offset, const void *buffer, size_t size, const fs::WriteOption &option) override { /* Verify that we can write. */ bool needs_append; R_TRY(this->DryWrite(std::addressof(needs_append), offset, size, option, m_open_mode)); /* If we need to, perform the write. */ if (size != 0) { /* Prepare to do asynchronous IO. */ OVERLAPPED overlapped = {}; overlapped.Offset = static_cast(offset); overlapped.OffsetHigh = static_cast(offset >> BITSIZEOF(DWORD)); overlapped.hEvent = ::CreateEvent(nullptr, true, false, nullptr); R_UNLESS(overlapped.hEvent != nullptr, fs::ResultUnexpectedInLocalFileSystemA()); ON_SCOPE_EXIT { ::CloseHandle(overlapped.hEvent); }; /* Write to the file. */ DWORD size_written; if (!::WriteFile(m_handle, buffer, static_cast(size), std::addressof(size_written), std::addressof(overlapped))) { /* If we fail for reason other than io pending, return the error result. */ const auto err = ::GetLastError(); R_UNLESS(err == ERROR_IO_PENDING, ConvertLastErrorToResult()); /* Get the wait result. */ R_UNLESS(::GetOverlappedResult(m_handle, std::addressof(overlapped), std::addressof(size_written), true), ConvertLastErrorToResult()); } /* Check that a correct amount of data was written. */ R_UNLESS(size_written >= size, fs::ResultNotEnoughFreeSpace()); /* Sanity check that we wrote the right amount. */ AMS_ASSERT(size_written == size); } /* If we need to, flush. */ if (option.HasFlushFlag()) { R_TRY(this->Flush()); } R_SUCCEED(); } virtual Result DoSetSize(s64 size) override { /* Verify we can set the size. */ R_TRY(this->DrySetSize(size, m_open_mode)); /* Try to set the file size. */ R_RETURN(SetFileSizeImpl(m_handle, size)); } virtual Result DoOperateRange(void *dst, size_t dst_size, fs::OperationId op_id, s64 offset, s64 size, const void *src, size_t src_size) override { AMS_UNUSED(offset, size, src, src_size); switch (op_id) { case fs::OperationId::Invalidate: R_SUCCEED(); case fs::OperationId::QueryRange: R_UNLESS(dst != nullptr, fs::ResultNullptrArgument()); R_UNLESS(dst_size == sizeof(fs::QueryRangeInfo), fs::ResultInvalidSize()); static_cast(dst)->Clear(); R_SUCCEED(); default: R_THROW(fs::ResultUnsupportedOperateRangeForTmFileSystemFile()); } } public: virtual sf::cmif::DomainObjectId GetDomainObjectId() const override { AMS_ABORT("GetDomainObjectId() should never be called on a LocalFile"); } }; bool IsDirectory(const WIN32_FIND_DATAW &fd) { return fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; } class LocalDirectory : public ::ams::fs::fsa::IDirectory, public ::ams::fs::impl::Newable { private: std::unique_ptr m_path; HANDLE m_dir_handle; HANDLE m_search_handle; fs::OpenDirectoryMode m_open_mode; public: LocalDirectory(HANDLE d, fs::OpenDirectoryMode m, std::unique_ptr &&p) : m_path(std::move(p)), m_dir_handle(d), m_search_handle(INVALID_HANDLE_VALUE) { m_open_mode = static_cast(util::ToUnderlying(m) & ~util::ToUnderlying(fs::OpenDirectoryMode_NotRequireFileSize)); } virtual ~LocalDirectory() { if (m_search_handle != INVALID_HANDLE_VALUE) { ::FindClose(m_search_handle); } ::CloseHandle(m_dir_handle); } public: virtual Result DoRead(s64 *out_count, fs::DirectoryEntry *out_entries, s64 max_entries) override { auto read_count = 0; while (read_count < max_entries) { /* Read the next file. */ WIN32_FIND_DATAW fd; std::memset(fd.cFileName, 0, sizeof(fd.cFileName)); if (m_search_handle == INVALID_HANDLE_VALUE) { /* Create our search handle. */ if (m_search_handle = ::FindFirstFileW(m_path.get(), std::addressof(fd)); m_search_handle == INVALID_HANDLE_VALUE) { /* Check that we failed because there are no files. */ R_UNLESS(::GetLastError() == ERROR_FILE_NOT_FOUND, ConvertLastErrorToResult()); break; } } else if (!::FindNextFileW(m_search_handle, std::addressof(fd))) { /* Check that we failed because we ran out of files. */ R_UNLESS(::GetLastError() == ERROR_NO_MORE_FILES, ConvertLastErrorToResult()); break; } /* If we shouldn't create an entry, continue. */ if (!this->IsReadTarget(fd)) { continue; } /* Create the entry. */ auto &entry = out_entries[read_count++]; std::memset(entry.name, 0, sizeof(entry.name)); const auto wide_res = ::WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, entry.name, sizeof(entry.name), nullptr, nullptr); R_UNLESS(wide_res != 0, fs::ResultInvalidPath()); entry.type = IsDirectory(fd) ? fs::DirectoryEntryType_Directory : fs::DirectoryEntryType_File; entry.file_size = static_cast(fd.nFileSizeLow) | static_cast(static_cast(fd.nFileSizeHigh) << BITSIZEOF(fd.nFileSizeLow)); } /* Set the output read count. */ *out_count = read_count; R_SUCCEED(); } virtual Result DoGetEntryCount(s64 *out) override { /* Open a new search handle. */ WIN32_FIND_DATAW fd; auto handle = ::FindFirstFileW(m_path.get(), std::addressof(fd)); R_UNLESS(handle != INVALID_HANDLE_VALUE, ConvertLastErrorToResult()); ON_SCOPE_EXIT { ::FindClose(handle); }; /* Iterate to get the total entry count. */ auto entry_count = 0; while (::FindNextFileW(handle, std::addressof(fd))) { if (this->IsReadTarget(fd)) { ++entry_count; } } /* Check that we stopped iterating because we ran out of files. */ R_UNLESS(::GetLastError() == ERROR_NO_MORE_FILES, ConvertLastErrorToResult()); /* Set the output. */ *out = entry_count; R_SUCCEED(); } private: bool IsReadTarget(const WIN32_FIND_DATAW &fd) const { /* If the file is "..", don't return it. */ if (::wcsncmp(fd.cFileName, L"..", 3) == 0 || ::wcsncmp(fd.cFileName, L".", 2) == 0) { return false; } /* Return whether our open mode supports the target. */ if (IsDirectory(fd)) { return m_open_mode != fs::OpenDirectoryMode_File; } else { return m_open_mode != fs::OpenDirectoryMode_Directory; } } public: virtual sf::cmif::DomainObjectId GetDomainObjectId() const override { AMS_ABORT("GetDomainObjectId() should never be called on a LocalDirectory"); } }; #else constexpr int MaxFilePathLength = PATH_MAX - 1; constexpr int MaxDirectoryPathLength = PATH_MAX - 1; #if defined (ATMOSPHERE_OS_LINUX) struct linux_dirent64 { ino64_t d_ino; off64_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[]; }; using NativeDirectoryEntryType = struct linux_dirent64; #else using NativeDirectoryEntryType = struct dirent; #endif bool AreLongPathsEnabledImpl() { /* TODO: How does this work on linux/macos? */ return true; } enum ErrnoSource { ErrnoSource_OpenFile, // 0 ErrnoSource_CreateFile, // 1 ErrnoSource_Unlink, // 2 ErrnoSource_Pread, // 3 ErrnoSource_Pwrite, // 4 ErrnoSource_Ftruncate, // 5 // ErrnoSource_OpenDirectory, // 6 ErrnoSource_Mkdir, // 7 ErrnoSource_Rmdir, // 8 ErrnoSource_GetDents, // 9 // ErrnoSource_RenameDirectory, // 10 ErrnoSource_RenameFile, // 11 // ErrnoSource_Stat, // 12 ErrnoSource_Statvfs, // 13 }; Result ConvertErrnoToResult(ErrnoSource source) { switch (errno) { case ENOENT: R_THROW(fs::ResultPathNotFound()); case EEXIST: switch (source) { case ErrnoSource_Rmdir: R_THROW(fs::ResultDirectoryNotEmpty()); default: R_THROW(fs::ResultPathAlreadyExists()); } case ENOTDIR: switch (source) { case ErrnoSource_Rmdir: R_THROW(fs::ResultPathNotFound()); default: R_THROW(fs::ResultPathNotFound()); } case EISDIR: switch (source) { case ErrnoSource_CreateFile: R_THROW(fs::ResultPathAlreadyExists()); case ErrnoSource_OpenFile: case ErrnoSource_Unlink: R_THROW(fs::ResultPathNotFound()); default: R_THROW(fs::ResultUnexpectedInLocalFileSystemE()); } case ENOTEMPTY: R_THROW(fs::ResultDirectoryNotEmpty()); case EACCES: case EINTR: R_THROW(fs::ResultTargetLocked()); default: //printf("Returning default errno -> result, errno=%d, source=%d\n", errno, static_cast(source)); R_THROW(fs::ResultUnexpectedInLocalFileSystemE()); } } Result WaitDeletionCompletion(const char *native_path) { /* TODO: Does linux need to wait for delete to complete? */ AMS_UNUSED(native_path); R_SUCCEED(); } Result GetEntryTypeImpl(fs::DirectoryEntryType *out, const char *native_path) { struct stat st; R_UNLESS(::stat(native_path, std::addressof(st)) == 0, ConvertErrnoToResult(ErrnoSource_Stat)); *out = (S_ISDIR(st.st_mode)) ? fs::DirectoryEntryType_Directory : fs::DirectoryEntryType_File; R_SUCCEED(); } auto RetryForEIntr(auto f) { decltype(f()) res; do { res = f(); } while (res < 0 && errno == EINTR); return res; }; void CloseFileDescriptor(int handle) { const int res = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::close(handle); }); AMS_ASSERT(res == 0); AMS_UNUSED(res); } Result SetFileSizeImpl(int handle, s64 size) { const auto res = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::ftruncate(handle, size); }); R_UNLESS(res == 0, ConvertErrnoToResult(ErrnoSource_Ftruncate)); R_SUCCEED(); } class LocalFile : public ::ams::fs::fsa::IFile, public ::ams::fs::impl::Newable { private: const int m_handle; const fs::OpenMode m_open_mode; public: LocalFile(int h, fs::OpenMode m) : m_handle(h), m_open_mode(m) { /* ... */ } virtual ~LocalFile() { CloseFileDescriptor(m_handle); } public: virtual Result DoRead(size_t *out, s64 offset, void *buffer, size_t size, const fs::ReadOption &option) override { /* Check that read is possible. */ size_t dry_read_size; R_TRY(this->DryRead(std::addressof(dry_read_size), offset, size, option, m_open_mode)); /* If we have nothing to read, we don't need to do anything. */ if (dry_read_size == 0) { *out = 0; R_SUCCEED(); } /* Read. */ const auto read_size = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA -> ssize_t { return ::pread(m_handle, buffer, size, offset); }); R_UNLESS(read_size >= 0, ConvertErrnoToResult(ErrnoSource_Pread)); /* Set output. */ *out = static_cast(read_size); R_SUCCEED(); } virtual Result DoGetSize(s64 *out) override { /* Get the file size. */ const auto size = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA -> s64 { return ::lseek(m_handle, 0, SEEK_END); }); R_UNLESS(size >= 0, fs::ResultUnexpectedInLocalFileSystemD()); /* Set the output. */ *out = size; R_SUCCEED(); } virtual Result DoFlush() override { /* If we're not writable, we have nothing to flush. */ R_SUCCEED_IF((m_open_mode & fs::OpenMode_Write) == 0); /* Flush our buffer. */ const auto res = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::fsync(m_handle); }); R_UNLESS(res == 0, fs::ResultUnexpectedInLocalFileSystemC()); R_SUCCEED(); } virtual Result DoWrite(s64 offset, const void *buffer, size_t size, const fs::WriteOption &option) override { /* Verify that we can write. */ bool needs_append; R_TRY(this->DryWrite(std::addressof(needs_append), offset, size, option, m_open_mode)); /* If we need to, perform the write. */ if (size != 0) { /* Read. */ const auto size_written = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA -> ssize_t { return ::pwrite(m_handle, buffer, size, offset); }); R_UNLESS(size_written >= 0, ConvertErrnoToResult(ErrnoSource_Pwrite)); /* Check that a correct amount of data was written. */ R_UNLESS(static_cast(size_written) >= size, fs::ResultNotEnoughFreeSpace()); /* Sanity check that we wrote the right amount. */ AMS_ASSERT(static_cast(size_written) == size); } /* If we need to, flush. */ if (option.HasFlushFlag()) { R_TRY(this->Flush()); } R_SUCCEED(); } virtual Result DoSetSize(s64 size) override { /* Verify we can set the size. */ R_TRY(this->DrySetSize(size, m_open_mode)); /* Try to set the file size. */ R_RETURN(SetFileSizeImpl(m_handle, size)); } virtual Result DoOperateRange(void *dst, size_t dst_size, fs::OperationId op_id, s64 offset, s64 size, const void *src, size_t src_size) override { AMS_UNUSED(offset, size, src, src_size); switch (op_id) { case fs::OperationId::Invalidate: R_SUCCEED(); case fs::OperationId::QueryRange: R_UNLESS(dst != nullptr, fs::ResultNullptrArgument()); R_UNLESS(dst_size == sizeof(fs::QueryRangeInfo), fs::ResultInvalidSize()); static_cast(dst)->Clear(); R_SUCCEED(); default: R_THROW(fs::ResultUnsupportedOperateRangeForTmFileSystemFile()); } } public: virtual sf::cmif::DomainObjectId GetDomainObjectId() const override { AMS_ABORT("GetDomainObjectId() should never be called on a LocalFile"); } }; class LocalDirectory : public ::ams::fs::fsa::IDirectory, public ::ams::fs::impl::Newable { private: std::unique_ptr m_path; int m_dir_handle; fs::OpenDirectoryMode m_open_mode; bool m_not_require_file_size; std::unique_ptr m_temp_entries; int m_temp_entries_count; int m_temp_entries_ofs; #if defined(ATMOSPHERE_OS_MACOS) uintptr_t m_basep = 0; #endif public: LocalDirectory(int d, fs::OpenDirectoryMode m, std::unique_ptr &&p) : m_path(std::move(p)), m_dir_handle(d), m_temp_entries(nullptr), m_temp_entries_count(0), m_temp_entries_ofs(0) { m_open_mode = static_cast(util::ToUnderlying(m) & ~util::ToUnderlying(fs::OpenDirectoryMode_NotRequireFileSize)); m_not_require_file_size = m & fs::OpenDirectoryMode_NotRequireFileSize; } virtual ~LocalDirectory() { CloseFileDescriptor(m_dir_handle); } public: virtual Result DoRead(s64 *out_count, fs::DirectoryEntry *out_entries, s64 max_entries) override { auto read_count = 0; /* Copy out any pending entries from a previous call. */ while (m_temp_entries_ofs < m_temp_entries_count && read_count < max_entries) { out_entries[read_count++] = m_temp_entries[m_temp_entries_ofs++]; } /* If we're done with our temporary entries, release them. */ if (m_temp_entries_ofs == m_temp_entries_count) { m_temp_entries.reset(); m_temp_entries_ofs = 0; m_temp_entries_count = 0; } if (read_count < max_entries) { /* Declare buffer to hold temporary path. */ char path_buf[PATH_MAX]; auto base_path_len = std::strlen(m_path.get()); std::memcpy(path_buf, m_path.get(), base_path_len); if (path_buf[base_path_len - 1] != '/') { path_buf[base_path_len++] = '/'; } #if defined(ATMOSPHERE_OS_LINUX) char buf[1_KB]; #else char buf[2_KB]; #endif NativeDirectoryEntryType *ent = nullptr; while (read_count < max_entries) { /* Read next entries. */ #if defined (ATMOSPHERE_OS_LINUX) const auto nread = ::syscall(SYS_getdents64, m_dir_handle, buf, sizeof(buf)); #elif defined(ATMOSPHERE_OS_MACOS) const auto nread = ::__getdirentries64(m_dir_handle, buf, sizeof(buf), std::addressof(m_basep)); #else #error "Unknown OS to read from directory FD" #endif R_UNLESS(nread >= 0, ConvertErrnoToResult(ErrnoSource_GetDents)); /* If we read nothing, we've hit the end of the directory. */ if (nread == 0) { break; } /* Determine the number of entries we read. */ auto cur_read_entries = 0; for (auto pos = 0; pos < nread; pos += ent->d_reclen) { /* Get the native entry. */ ent = reinterpret_cast(buf + pos); /* If the entry isn't a read target, ignore it. */ if (IsReadTarget(ent)) { ++cur_read_entries; } } /* If we'll end up reading more than we can fit, allocate a temporary buffer. */ if (read_count + cur_read_entries > max_entries) { /* Allocate temporary entries. */ m_temp_entries_count = (read_count + cur_read_entries) - max_entries; m_temp_entries_ofs = 0; /* TODO: Non-fatal? */ m_temp_entries = fs::impl::MakeUnique(m_temp_entries_count); AMS_ABORT_UNLESS(m_temp_entries != nullptr); } /* Iterate received entries. */ for (auto pos = 0; pos < nread; pos += ent->d_reclen) { /* Get the native entry. */ ent = reinterpret_cast(buf + pos); /* If the entry isn't a read target, ignore it. */ if (!IsReadTarget(ent)) { continue; } /* Decide on the output entry. */ fs::DirectoryEntry *out_entry; if (read_count < max_entries) { out_entry = std::addressof(out_entries[read_count++]); } else { out_entry = std::addressof(m_temp_entries[m_temp_entries_ofs++]); } /* Setup the output entry. */ { std::memset(out_entry->name, 0, sizeof(out_entry->name)); const auto name_len = std::strlen(ent->d_name); AMS_ABORT_UNLESS(name_len <= fs::EntryNameLengthMax); std::memcpy(out_entry->name, ent->d_name, name_len + 1); out_entry->type = (ent->d_type == DT_DIR) ? fs::DirectoryEntryType_Directory : fs::DirectoryEntryType_File; /* If we have to, get the filesize. This is (unfortunately) expensive on linux. */ if (out_entry->type == fs::DirectoryEntryType_File && !m_not_require_file_size) { /* Set up the temporary file path. */ AMS_ABORT_UNLESS(base_path_len + name_len + 1 <= PATH_MAX); std::memcpy(path_buf + base_path_len, ent->d_name, name_len + 1); /* Get the file stats. */ struct stat st; R_UNLESS(::stat(path_buf, std::addressof(st)) == 0, ConvertErrnoToResult(ErrnoSource_Stat)); out_entry->file_size = static_cast(st.st_size); } } } /* Ensure our temporary entries are correct. */ if (m_temp_entries != nullptr) { AMS_ASSERT(read_count == max_entries); AMS_ASSERT(m_temp_entries_ofs == m_temp_entries_count); m_temp_entries_ofs = 0; } } } /* Set the output read count. */ *out_count = read_count; R_SUCCEED(); } virtual Result DoGetEntryCount(s64 *out) override { /* Open the directory anew. */ const auto handle = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::open(m_path.get(), O_RDONLY | O_DIRECTORY); }); R_UNLESS(handle >= 0, ConvertErrnoToResult(ErrnoSource_OpenDirectory)); ON_SCOPE_EXIT { CloseFileDescriptor(handle); }; /* Iterate to get the total entry count. */ auto entry_count = 0; { #if defined(ATMOSPHERE_OS_LINUX) char buf[1_KB]; #else char buf[2_KB]; uintptr_t basep = 0; #endif NativeDirectoryEntryType *ent = nullptr; while (true) { /* Read next entries. */ #if defined (ATMOSPHERE_OS_LINUX) const auto nread = ::syscall(SYS_getdents64, handle, buf, sizeof(buf)); #elif defined(ATMOSPHERE_OS_MACOS) const auto nread = ::__getdirentries64(handle, buf, sizeof(buf), std::addressof(basep)); #else #error "Unknown OS to read from directory FD" #endif R_UNLESS(nread >= 0, ConvertErrnoToResult(ErrnoSource_GetDents)); /* If we read nothing, we've hit the end of the directory. */ if (nread == 0) { break; } /* Iterate received entries. */ for (auto pos = 0; pos < nread; pos += ent->d_reclen) { /* Get the entry. */ ent = reinterpret_cast(buf + pos); /* If the entry is a read target, increment our count. */ if (IsReadTarget(ent)) { ++entry_count; } } } } *out = entry_count; R_SUCCEED(); } private: bool IsReadTarget(const NativeDirectoryEntryType *ent) const { /* If the file is "..", don't return it. */ if (std::strcmp(ent->d_name, ".") == 0 || std::strcmp(ent->d_name, "..") == 0) { return false; } /* Return whether our open mode supports the target. */ if (ent->d_type == DT_DIR) { return m_open_mode != fs::OpenDirectoryMode_File; } else { return m_open_mode != fs::OpenDirectoryMode_Directory; } } public: virtual sf::cmif::DomainObjectId GetDomainObjectId() const override { AMS_ABORT("GetDomainObjectId() should never be called on a LocalDirectory"); } }; #endif bool AreLongPathsEnabled() { AMS_FUNCTION_LOCAL_STATIC(bool, s_enabled, AreLongPathsEnabledImpl()); return s_enabled; } } Result LocalFileSystem::Initialize(const fs::Path &root_path, fssystem::PathCaseSensitiveMode case_sensitive_mode) { /* Initialize our root path. */ R_TRY(m_root_path.Initialize(root_path)); /* If we're not empty, we'll need to convert to a native path. */ if (m_root_path.IsEmpty()) { /* Reset our native path, since we're acting without a root. */ m_native_path_buffer.reset(nullptr); m_native_path_length = 0; } else { /* Convert to native path. */ NativePathBuffer native_path = nullptr; int native_len = 0; #if defined(ATMOSPHERE_OS_WINDOWS) { /* Get path length. */ native_len = ::MultiByteToWideChar(CP_UTF8, 0, m_root_path.GetString(), -1, nullptr, 0); /* Allocate our native path buffer. */ native_path = fs::impl::MakeUnique(native_len + 1); R_UNLESS(native_path != nullptr, fs::ResultAllocationMemoryFailedMakeUnique()); /* Convert path. */ const auto res = ::MultiByteToWideChar(CP_UTF8, 0, m_root_path.GetString(), -1, native_path.get(), native_len); R_UNLESS(res != 0, fs::ResultTooLongPath()); R_UNLESS(res <= static_cast(fs::EntryNameLengthMax + 1), fs::ResultTooLongPath()); /* Fix up directory separators. */ for (NativeCharacterType *p = native_path.get(); *p != 0; ++p) { if (*p == '/') { *p = '\\'; } } } #else { /* Get path size. */ native_len = std::strlen(m_root_path.GetString()); /* Tentatively assume other operating systems do the sane thing and use utf-8 strings. */ native_path = fs::impl::MakeUnique(native_len + 1); R_UNLESS(native_path != nullptr, fs::ResultAllocationMemoryFailedMakeUnique()); /* Copy in path. */ std::memcpy(native_path.get(), m_root_path.GetString(), native_len + 1); } #endif /* Temporarily set case sensitive mode to insensitive, and verify we can get the root directory. */ m_case_sensitive_mode = fssystem::PathCaseSensitiveMode_CaseInsensitive; { constexpr fs::Path RequiredRootPath = fs::MakeConstantPath("/"); fs::DirectoryEntryType type; R_TRY(this->GetEntryType(std::addressof(type), RequiredRootPath)); R_UNLESS(type == fs::DirectoryEntryType_Directory, fs::ResultPathNotFound()); } /* Set our native path members. */ m_native_path_buffer = std::move(native_path); m_native_path_length = native_len; } /* Set our case sensitive mode. */ m_case_sensitive_mode = case_sensitive_mode; R_SUCCEED(); } Result LocalFileSystem::GetCaseSensitivePath(int *out_size, char *dst, size_t dst_size, const char *path, const char *work_path) { AMS_UNUSED(out_size, dst, dst_size, path, work_path); AMS_ABORT("TODO"); } Result LocalFileSystem::CheckPathCaseSensitively(const NativeCharacterType *path, const NativeCharacterType *root_path, NativeCharacterType *cs_buf, size_t cs_size, bool check_case_sensitivity) { AMS_UNUSED(path, root_path, cs_buf, cs_size, check_case_sensitivity); AMS_ABORT("TODO"); } Result LocalFileSystem::ResolveFullPath(NativePathBuffer *out, const fs::Path &path, int max_len, int min_len, bool check_case_sensitivity) { /* Create the full path. */ fs::Path full_path; R_TRY(full_path.Combine(m_root_path, path)); /* Check that the path is valid. */ fs::PathFlags flags; flags.AllowWindowsPath(); flags.AllowRelativePath(); flags.AllowEmptyPath(); R_TRY(fs::PathFormatter::CheckPathFormat(full_path.GetString(), flags)); /* Check the path's character count. */ #if defined(ATMOSPHERE_OS_WINDOWS) AreLongPathsEnabled(); // TODO: R_TRY(fs::CheckCharacterCountForWindows(full_path.GetString(), MaxBasePathLength, AreLongPathsEnabled() ? 0 : max_len)); AMS_UNUSED(max_len); #else AreLongPathsEnabled(); /* TODO: Check character count for linux/macos? */ AMS_UNUSED(max_len); #endif /* Convert to native path. */ NativePathBuffer native_path = nullptr; #if defined(ATMOSPHERE_OS_WINDOWS) { /* Get path length. */ const int native_len = ::MultiByteToWideChar(CP_UTF8, 0, full_path.GetString(), -1, nullptr, 0); /* Allocate our native path buffer. */ native_path = fs::impl::MakeUnique(native_len + min_len + 1); R_UNLESS(native_path != nullptr, fs::ResultAllocationMemoryFailedMakeUnique()); /* Convert path. */ const auto res = ::MultiByteToWideChar(CP_UTF8, 0, full_path.GetString(), -1, native_path.get(), native_len); R_UNLESS(res != 0, fs::ResultTooLongPath()); R_UNLESS(res <= native_len, fs::ResultTooLongPath()); /* Fix up directory separators. */ s32 len = 0; for (NativeCharacterType *p = native_path.get(); *p != 0; ++p) { if (*p == '/') { *p = '\\'; } ++len; } /* Fix up trailing : */ if (native_path[len - 1] == ':') { native_path[len] = '\\'; native_path[len + 1] = 0; } /* If case sensitivity is required, allocate case sensitive buffer. */ if (m_case_sensitive_mode == PathCaseSensitiveMode_CaseSensitive && native_path[0] != 0) { /* Allocate case sensitive buffer. */ auto case_sensitive_buffer_size = sizeof(NativeCharacterType) * (m_native_path_length + native_len + 1 + fs::EntryNameLengthMax); NativePathBuffer case_sensitive_path_buffer = fs::impl::MakeUnique(case_sensitive_buffer_size / sizeof(NativeCharacterType)); R_UNLESS(case_sensitive_path_buffer != nullptr, fs::ResultAllocationMemoryFailedMakeUnique()); /* Get root path. */ const NativeCharacterType *root_path = m_native_path_buffer.get() != nullptr ? m_native_path_buffer.get() : L""; /* Perform case sensitive path checking. */ R_TRY(this->CheckPathCaseSensitively(native_path.get(), root_path, case_sensitive_path_buffer.get(), case_sensitive_buffer_size, check_case_sensitivity)); } /* Set default path, if empty. */ if (native_path[0] == 0) { native_path[0] = '.'; native_path[1] = '\\'; native_path[2] = 0; } } #else { /* Get path size. */ const int native_len = std::strlen(full_path.GetString()); /* Tentatively assume other operating systems do the sane thing and use utf-8 strings. */ native_path = fs::impl::MakeUnique(native_len + min_len + 1); R_UNLESS(native_path != nullptr, fs::ResultAllocationMemoryFailedMakeUnique()); /* Copy in path. */ std::memcpy(native_path.get(), full_path.GetString(), native_len + 1); /* TODO: Is case sensitivity adjustment needed here? */ AMS_UNUSED(check_case_sensitivity); } #endif /* Set the output path. */ *out = std::move(native_path); R_SUCCEED(); } Result LocalFileSystem::DoGetDiskFreeSpace(s64 *out_free, s64 *out_total, s64 *out_total_free, const fs::Path &path) { /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, false)); /* Get the disk free space. */ #if defined(ATMOSPHERE_OS_WINDOWS) { ULARGE_INTEGER free, total, total_free; R_UNLESS(::GetDiskFreeSpaceExW(native_path.get(), std::addressof(free), std::addressof(total), std::addressof(total_free)), ConvertLastErrorToResult()); *out_free = static_cast(free.QuadPart); *out_total = static_cast(total.QuadPart); *out_total_free = static_cast(total_free.QuadPart); } #else { struct statvfs st; const auto res = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::statvfs(native_path.get(), std::addressof(st)); }); R_UNLESS(res >= 0, ConvertErrnoToResult(ErrnoSource_Statvfs)); *out_free = static_cast(st.f_bavail) * static_cast(st.f_frsize); *out_total = static_cast(st.f_blocks) * static_cast(st.f_frsize); *out_total_free = static_cast(st.f_bfree) * static_cast(st.f_frsize); } #endif R_SUCCEED(); } Result LocalFileSystem::DeleteDirectoryRecursivelyInternal(const NativeCharacterType *path, bool delete_top) { #if defined(ATMOSPHERE_OS_WINDOWS) { /* Get the path length. */ const auto path_len = ::wcslen(path); /* Allocate a new path buffer. */ NativePathBuffer cur_path_buf = fs::impl::MakeUnique(path_len + MAX_PATH); R_UNLESS(cur_path_buf.get() != nullptr, fs::ResultAllocationMemoryFailedMakeUnique()); /* Copy the path into the temporary buffer. */ ::wcscpy(cur_path_buf.get(), path); ::wcscat(cur_path_buf.get(), L"\\*"); /* Iterate the directory, deleting all contents. */ { /* Begin finding. */ WIN32_FIND_DATAW fd; const auto handle = ::FindFirstFileW(cur_path_buf.get(), std::addressof(fd)); R_UNLESS(handle != INVALID_HANDLE_VALUE, ConvertLastErrorToResult()); ON_SCOPE_EXIT { ::FindClose(handle); }; /* Clear the path from \\* to path\\ */ wchar_t * const dst = cur_path_buf.get() + path_len + 1; *dst = 0; /* Loop files. */ while (::FindNextFileW(handle, std::addressof(fd))) { /* Skip . and .. */ if (::wcsncmp(fd.cFileName, L"..", 3) == 0 || ::wcsncmp(fd.cFileName, L".", 2) == 0) { continue; } /* Copy the current filename to our working path. */ ::wcscpy(dst, fd.cFileName); if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { /* If a directory, delete it recursively. */ R_TRY(this->DeleteDirectoryRecursivelyInternal(cur_path_buf.get(), true)); } else { /* If a file, just delete it. */ auto delete_file = [&]() -> Result { R_UNLESS(::DeleteFileW(cur_path_buf.get()), ConvertLastErrorToResult()); R_SUCCEED(); }; R_TRY(fssystem::RetryToAvoidTargetLocked(delete_file)); R_TRY(WaitDeletionCompletion(cur_path_buf.get())); } } /* Check that we stopped iterating because we ran out of files. */ R_UNLESS(::GetLastError() == ERROR_NO_MORE_FILES, ConvertLastErrorToResult()); } /* If we should, delete the top level directory. */ if (delete_top) { auto delete_impl = [&] () -> Result { R_UNLESS(::RemoveDirectoryW(path), ConvertLastErrorToResult()); R_SUCCEED(); }; /* Perform the delete. */ R_TRY(fssystem::RetryToAvoidTargetLocked(delete_impl)); /* Wait for the deletion to complete. */ R_TRY(WaitDeletionCompletion(path)); } } #else { /* Get the path length. */ const auto path_len = std::strlen(path); /* Allocate a temporary buffer. */ NativePathBuffer cur_path_buf = fs::impl::MakeUnique(path_len + PATH_MAX); R_UNLESS(cur_path_buf.get() != nullptr, fs::ResultAllocationMemoryFailedMakeUnique()); /* Copy the path into the temporary buffer. */ std::memcpy(cur_path_buf.get(), path, path_len); auto ofs = path_len; if (cur_path_buf.get()[ofs - 1] != '/') { cur_path_buf.get()[ofs++] = '/'; } /* Iterate the directory, deleting all contents. */ { /* Open the directory. */ const auto handle = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::open(path, O_RDONLY | O_DIRECTORY); }); R_UNLESS(handle >= 0, ConvertErrnoToResult(ErrnoSource_OpenDirectory)); ON_SCOPE_EXIT { CloseFileDescriptor(handle); }; #if defined(ATMOSPHERE_OS_LINUX) char buf[1_KB]; #else char buf[2_KB]; uintptr_t basep = 0; #endif NativeDirectoryEntryType *ent = nullptr; static_assert(sizeof(*ent) <= sizeof(buf)); while (true) { /* Read next entries. */ #if defined (ATMOSPHERE_OS_LINUX) const auto nread = ::syscall(SYS_getdents64, handle, buf, sizeof(buf)); #elif defined(ATMOSPHERE_OS_MACOS) const auto nread = ::__getdirentries64(handle, buf, sizeof(buf), std::addressof(basep)); #else #error "Unknown OS to read from directory FD" #endif R_UNLESS(nread >= 0, ConvertErrnoToResult(ErrnoSource_GetDents)); /* If we read nothing, we've hit the end of the directory. */ if (nread == 0) { break; } /* Iterate received entries. */ for (auto pos = 0; pos < nread; pos += ent->d_reclen) { /* Get the entry. */ ent = reinterpret_cast(buf + pos); /* Skip . and .. */ if (std::strcmp(ent->d_name, ".") == 0 || std::strcmp(ent->d_name, "..") == 0) { continue; } /* Get the entry name length. */ const int e_len = std::strlen(ent->d_name); std::memcpy(cur_path_buf.get() + ofs, ent->d_name, e_len + 1); /* Get the dir type. */ const auto d_type = ent->d_type; if (d_type == DT_DIR) { /* If a directory, recursively delete it. */ R_TRY(this->DeleteDirectoryRecursivelyInternal(cur_path_buf.get(), true)); } else { /* If a file, just delete it. */ auto delete_file = [&]() -> Result { const auto res = ::unlink(cur_path_buf.get()); R_UNLESS(res >= 0, ConvertErrnoToResult(ErrnoSource_Unlink)); R_SUCCEED(); }; R_TRY(fssystem::RetryToAvoidTargetLocked(delete_file)); R_TRY(WaitDeletionCompletion(cur_path_buf.get())); } } } } /* If we should, delete the top level directory. */ if (delete_top) { auto delete_impl = [&] () -> Result { R_UNLESS(::rmdir(path) == 0, ConvertErrnoToResult(ErrnoSource_Rmdir)); R_SUCCEED(); }; /* Perform the delete. */ R_TRY(fssystem::RetryToAvoidTargetLocked(delete_impl)); /* Wait for the deletion to complete. */ R_TRY(WaitDeletionCompletion(path)); } } #endif R_SUCCEED(); } Result LocalFileSystem::DoCreateFile(const fs::Path &path, s64 size, int flags) { AMS_UNUSED(flags); /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, false)); /* Create the file. */ #if defined(ATMOSPHERE_OS_WINDOWS) { /* Get handle to created file. */ const auto handle = ::CreateFileW(native_path.get(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { /* If we failed because of target locked, it may be the case that the path already exists as a directory. */ R_TRY_CATCH(ConvertLastErrorToResult()) { R_CATCH(fs::ResultTargetLocked) { /* Get the file attributes. */ const auto attr = ::GetFileAttributesW(native_path.get()); /* Check they're valid. */ R_UNLESS(attr != INVALID_FILE_ATTRIBUTES, R_CURRENT_RESULT); /* Check that they specify a directory. */ R_UNLESS((attr & FILE_ATTRIBUTE_DIRECTORY) != 0, R_CURRENT_RESULT); /* The path is an existing directory. */ R_THROW(fs::ResultPathAlreadyExists()); } } R_END_TRY_CATCH; } ON_RESULT_FAILURE { ::DeleteFileW(native_path.get()); }; ON_SCOPE_EXIT { ::CloseHandle(handle); }; /* Set the file as sparse. */ { DWORD dummy; ::DeviceIoControl(handle, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, std::addressof(dummy), nullptr); } /* Set the file size. */ if (size > 0) { R_TRY(SetFileSizeImpl(handle, size)); } } #else { /* Create the file. */ const auto handle = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA -> int { return ::open(native_path.get(), O_WRONLY | O_CREAT | O_EXCL, 0666); }); R_UNLESS(handle >= 0, ConvertErrnoToResult(ErrnoSource_CreateFile)); ON_RESULT_FAILURE { ::unlink(native_path.get()); }; ON_SCOPE_EXIT { CloseFileDescriptor(handle); }; /* Set the file as sparse. */ /* TODO: How do you do this on macos/linux? */ /* Set the file size. */ if (size > 0) { R_TRY(SetFileSizeImpl(handle, size)); } } #endif R_SUCCEED(); } Result LocalFileSystem::DoDeleteFile(const fs::Path &path) { /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, true)); /* Delete the file, retrying on target locked. */ auto delete_impl = [&] () -> Result { #if defined(ATMOSPHERE_OS_WINDOWS) { /* Try to delete the file directly. */ R_SUCCEED_IF(::DeleteFileW(native_path.get())); /* Convert the last error to a result. */ const auto last_error_result = ConvertLastErrorToResult(); /* Check if access denied; it may indicate we tried to open a directory. */ R_UNLESS(::GetLastError() == ERROR_ACCESS_DENIED, last_error_result); /* Check if we tried to open a directory. */ fs::DirectoryEntryType type; R_TRY(GetEntryTypeImpl(std::addressof(type), native_path.get())); /* If the type is anything other than directory, perform generic result conversion. */ R_UNLESS(type == fs::DirectoryEntryType_Directory, last_error_result); /* Return path not found, for trying to open a file as a directory. */ R_THROW(fs::ResultPathNotFound()); } #else { /* If on macOS, we need to check if the path is a directory before trying to unlink it. */ /* This is because unlink succeeds on directories when executing as superuser. */ #if defined(ATMOSPHERE_OS_MACOS) { /* Check if we tried to open a directory. */ fs::DirectoryEntryType type; R_TRY(GetEntryTypeImpl(std::addressof(type), native_path.get())); R_UNLESS(type == fs::DirectoryEntryType_File, fs::ResultPathNotFound()); } #endif /* Delete the file. */ const auto res = ::unlink(native_path.get()); R_UNLESS(res >= 0, ConvertErrnoToResult(ErrnoSource_Unlink)); } #endif R_SUCCEED(); }; /* Perform the delete. */ R_TRY(fssystem::RetryToAvoidTargetLocked(delete_impl)); /* Wait for the deletion to complete. */ R_RETURN(WaitDeletionCompletion(native_path.get())); } Result LocalFileSystem::DoCreateDirectory(const fs::Path &path) { /* Check for path validity. */ R_UNLESS(path != "/", fs::ResultPathNotFound()); R_UNLESS(path != ".", fs::ResultPathNotFound()); /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxDirectoryPathLength, 0, false)); /* Create the directory. */ #if defined(ATMOSPHERE_OS_WINDOWS) R_UNLESS(::CreateDirectoryW(native_path.get(), nullptr), ConvertLastErrorToResult()); #else R_UNLESS(::mkdir(native_path.get(), 0777) == 0, ConvertErrnoToResult(ErrnoSource_Mkdir)); #endif R_SUCCEED(); } Result LocalFileSystem::DoDeleteDirectory(const fs::Path &path) { /* Guard against deletion of raw drive. */ #if defined(ATMOSPHERE_OS_WINDOWS) R_UNLESS(!fs::IsWindowsDriveRootPath(path), fs::ResultDirectoryNotDeletable()); #else /* TODO: Linux/macOS? */ #endif /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, true)); /* Delete the directory, retrying on target locked. */ auto delete_impl = [&] () -> Result { #if defined(ATMOSPHERE_OS_WINDOWS) R_UNLESS(::RemoveDirectoryW(native_path.get()), ConvertLastErrorToResult()); #else R_UNLESS(::rmdir(native_path.get()) == 0, ConvertErrnoToResult(ErrnoSource_Rmdir)); #endif R_SUCCEED(); }; /* Perform the delete. */ R_TRY(fssystem::RetryToAvoidTargetLocked(delete_impl)); /* Wait for the deletion to complete. */ R_RETURN(WaitDeletionCompletion(native_path.get())); } Result LocalFileSystem::DoDeleteDirectoryRecursively(const fs::Path &path) { /* Guard against deletion of raw drive. */ #if defined(ATMOSPHERE_OS_WINDOWS) R_UNLESS(!fs::IsWindowsDriveRootPath(path), fs::ResultDirectoryNotDeletable()); #else /* TODO: Linux/macOS? */ #endif /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, true)); /* Delete the directory. */ R_RETURN(this->DeleteDirectoryRecursivelyInternal(native_path.get(), true)); } Result LocalFileSystem::DoRenameFile(const fs::Path &old_path, const fs::Path &new_path) { /* Resolve the old path. */ NativePathBuffer native_old_path; R_TRY(this->ResolveFullPath(std::addressof(native_old_path), old_path, MaxFilePathLength, 0, true)); /* Resolve the new path. */ NativePathBuffer native_new_path; R_TRY(this->ResolveFullPath(std::addressof(native_new_path), new_path, MaxFilePathLength, 0, false)); /* Check that the old path is a file. */ fs::DirectoryEntryType type; R_TRY(GetEntryTypeImpl(std::addressof(type), native_old_path.get())); R_UNLESS(type == fs::DirectoryEntryType_File, fs::ResultPathNotFound()); /* Perform the rename. */ auto rename_impl = [&]() -> Result { #if defined(ATMOSPHERE_OS_WINDOWS) if (!::MoveFileW(native_old_path.get(), native_new_path.get())) { R_TRY_CATCH(ConvertLastErrorToResult()) { R_CATCH(fs::ResultTargetLocked) { /* If we're performing a self rename, succeed. */ R_SUCCEED_IF(::wcscmp(native_old_path.get(), native_new_path.get()) == 0); /* Otherwise, check if the new path already exists. */ const auto attr = ::GetFileAttributesW(native_new_path.get()); R_UNLESS(attr == INVALID_FILE_ATTRIBUTES, fs::ResultPathAlreadyExists()); /* Return the original result. */ R_THROW(R_CURRENT_RESULT); } } R_END_TRY_CATCH; } #else { /* ::rename() will destroy an existing file at new path...so check for that case ahead of time. */ { struct stat st; R_UNLESS(::stat(native_new_path.get(), std::addressof(st)) < 0, fs::ResultPathAlreadyExists()); } /* Rename the file. */ R_UNLESS(::rename(native_old_path.get(), native_new_path.get()) == 0, ConvertErrnoToResult(ErrnoSource_RenameFile)); } #endif R_SUCCEED(); }; R_RETURN(fssystem::RetryToAvoidTargetLocked(rename_impl)); } Result LocalFileSystem::DoRenameDirectory(const fs::Path &old_path, const fs::Path &new_path) { /* Resolve the old path. */ NativePathBuffer native_old_path; R_TRY(this->ResolveFullPath(std::addressof(native_old_path), old_path, MaxDirectoryPathLength, 0, true)); /* Resolve the new path. */ NativePathBuffer native_new_path; R_TRY(this->ResolveFullPath(std::addressof(native_new_path), new_path, MaxDirectoryPathLength, 0, false)); /* Check that the old path is a file. */ fs::DirectoryEntryType type; R_TRY(GetEntryTypeImpl(std::addressof(type), native_old_path.get())); R_UNLESS(type == fs::DirectoryEntryType_Directory, fs::ResultPathNotFound()); /* Perform the rename. */ auto rename_impl = [&]() -> Result { #if defined(ATMOSPHERE_OS_WINDOWS) if (!::MoveFileW(native_old_path.get(), native_new_path.get())) { R_TRY_CATCH(ConvertLastErrorToResult()) { R_CATCH(fs::ResultTargetLocked) { /* If we're performing a self rename, succeed. */ R_SUCCEED_IF(::wcscmp(native_old_path.get(), native_new_path.get()) == 0); /* Otherwise, check if the new path already exists. */ const auto attr = ::GetFileAttributesW(native_new_path.get()); R_UNLESS(attr == INVALID_FILE_ATTRIBUTES, fs::ResultPathAlreadyExists()); /* Return the original result. */ R_THROW(R_CURRENT_RESULT); } } R_END_TRY_CATCH; } #else { /* ::rename() will overwrite an existing empty directory at the target, so check for that ahead of time. */ { struct stat st; R_UNLESS(::stat(native_new_path.get(), std::addressof(st)) < 0, fs::ResultPathAlreadyExists()); } /* Rename the directory. */ R_UNLESS(::rename(native_old_path.get(), native_new_path.get()) == 0, ConvertErrnoToResult(ErrnoSource_RenameDirectory)); } #endif R_SUCCEED(); }; R_RETURN(fssystem::RetryToAvoidTargetLocked(rename_impl)); } Result LocalFileSystem::DoGetEntryType(fs::DirectoryEntryType *out, const fs::Path &path) { /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, true)); /* Get the entry type. */ R_RETURN(GetEntryTypeImpl(out, native_path.get())); } Result LocalFileSystem::DoOpenFile(std::unique_ptr *out_file, const fs::Path &path, fs::OpenMode mode) { /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, true)); /* Open the file, retrying on target locked. */ auto open_impl = [&] () -> Result { #if defined(ATMOSPHERE_OS_WINDOWS) /* Open a windows file handle. */ const DWORD desired_access = ((mode & fs::OpenMode_Read) ? GENERIC_READ : 0) | ((mode & fs::OpenMode_Write) ? GENERIC_WRITE : 0); const auto file_handle = ::CreateFileW(native_path.get(), desired_access, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr); if (file_handle == INVALID_HANDLE_VALUE) { /* Convert the last error to a result. */ const auto last_error_result = ConvertLastErrorToResult(); /* Check if access denied; it may indicate we tried to open a directory. */ R_UNLESS(::GetLastError() == ERROR_ACCESS_DENIED, last_error_result); /* Check if we tried to open a directory. */ fs::DirectoryEntryType type; R_TRY(GetEntryTypeImpl(std::addressof(type), native_path.get())); /* If the type isn't file, return path not found. */ R_UNLESS(type == fs::DirectoryEntryType_File, fs::ResultPathNotFound()); /* Return the error we encountered earlier. */ R_THROW(last_error_result); } ON_RESULT_FAILURE { ::CloseHandle(file_handle); }; #else const bool is_read = (mode & fs::OpenMode_Read); const bool is_write = (mode & fs::OpenMode_Write); int file_handle = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::open(native_path.get(), (is_read && is_write) ? (O_RDWR) : (is_write ? (O_WRONLY) : (is_read ? (O_RDONLY) : (0)))); }); R_UNLESS(file_handle >= 0, ConvertErrnoToResult(ErrnoSource_OpenFile)); ON_RESULT_FAILURE { CloseFileDescriptor(file_handle); }; #endif /* Create a new local file. */ auto file = std::make_unique(file_handle, mode); R_UNLESS(file != nullptr, fs::ResultAllocationMemoryFailedInLocalFileSystemA()); /* Set the output file. */ *out_file = std::move(file); R_SUCCEED(); }; R_RETURN(fssystem::RetryToAvoidTargetLocked(open_impl)); } Result LocalFileSystem::DoOpenDirectory(std::unique_ptr *out_dir, const fs::Path &path, fs::OpenDirectoryMode mode) { /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 3, true)); /* Open the directory, retrying on target locked. */ auto open_impl = [&] () -> Result { #if defined(ATMOSPHERE_OS_WINDOWS) /* Open a handle file handle. */ const auto dir_handle = ::CreateFileW(native_path.get(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); R_UNLESS(dir_handle != INVALID_HANDLE_VALUE, ConvertLastErrorToResult()); ON_RESULT_FAILURE { ::CloseHandle(dir_handle); }; /* Check that we tried to open a directory. */ fs::DirectoryEntryType type; R_TRY(GetEntryTypeImpl(std::addressof(type), native_path.get())); /* If the type isn't directory, return path not found. */ R_UNLESS(type == fs::DirectoryEntryType_Directory, fs::ResultPathNotFound()); /* Fix up the path for us to perform a windows search. */ const auto native_len = ::wcslen(native_path.get()); const bool has_sep = native_len > 0 && native_path[native_len - 1] == '\\'; if (has_sep) { native_path[native_len + 0] = '*'; native_path[native_len + 1] = 0; } else { native_path[native_len + 0] = '\\'; native_path[native_len + 1] = '*'; native_path[native_len + 2] = 0; } #else /* Open the directory. */ const auto dir_handle = RetryForEIntr([&] () ALWAYS_INLINE_LAMBDA { return ::open(native_path.get(), O_RDONLY | O_DIRECTORY); }); R_UNLESS(dir_handle >= 0, ConvertErrnoToResult(ErrnoSource_OpenDirectory)); ON_RESULT_FAILURE { CloseFileDescriptor(dir_handle); }; #endif /* Create a new local directory. */ auto dir = std::make_unique(dir_handle, mode, std::move(native_path)); R_UNLESS(dir != nullptr, fs::ResultAllocationMemoryFailedInLocalFileSystemB()); /* Set the output directory. */ *out_dir = std::move(dir); R_SUCCEED(); }; R_RETURN(fssystem::RetryToAvoidTargetLocked(open_impl)); } Result LocalFileSystem::DoCommit() { R_SUCCEED(); } Result LocalFileSystem::DoGetFreeSpaceSize(s64 *out, const fs::Path &path) { s64 dummy; R_RETURN(this->DoGetDiskFreeSpace(out, std::addressof(dummy), std::addressof(dummy), path)); } Result LocalFileSystem::DoGetTotalSpaceSize(s64 *out, const fs::Path &path) { s64 dummy; R_RETURN(this->DoGetDiskFreeSpace(std::addressof(dummy), out, std::addressof(dummy), path)); } Result LocalFileSystem::DoCleanDirectoryRecursively(const fs::Path &path) { /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, true)); /* Delete the directory. */ R_RETURN(this->DeleteDirectoryRecursivelyInternal(native_path.get(), false)); } Result LocalFileSystem::DoGetFileTimeStampRaw(fs::FileTimeStampRaw *out, const fs::Path &path) { /* Resolve the path. */ NativePathBuffer native_path; R_TRY(this->ResolveFullPath(std::addressof(native_path), path, MaxFilePathLength, 0, true)); /* Get the file timestamp. */ #if defined(ATMOSPHERE_OS_WINDOWS) { /* Get the file attributes. */ WIN32_FILE_ATTRIBUTE_DATA attr; R_UNLESS(::GetFileAttributesExW(native_path.get(), GetFileExInfoStandard, std::addressof(attr)), ConvertLastErrorToResult()); /* Check that the file isn't a directory. */ R_UNLESS((attr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0, fs::ResultPathNotFound()); /* Decode the FILETIME values. */ const s64 create = static_cast(static_cast(attr.ftCreationTime.dwLowDateTime ) | (static_cast(attr.ftCreationTime.dwHighDateTime ) << BITSIZEOF(attr.ftCreationTime.dwLowDateTime ))); const s64 access = static_cast(static_cast(attr.ftLastAccessTime.dwLowDateTime) | (static_cast(attr.ftLastAccessTime.dwHighDateTime) << BITSIZEOF(attr.ftLastAccessTime.dwLowDateTime))); const s64 modify = static_cast(static_cast(attr.ftLastWriteTime.dwLowDateTime ) | (static_cast(attr.ftLastWriteTime.dwHighDateTime ) << BITSIZEOF(attr.ftLastWriteTime.dwLowDateTime ))); /* Set the output timestamps. */ if (m_use_posix_time) { out->create = ConvertWindowsTimeToPosixTime(create); out->access = ConvertWindowsTimeToPosixTime(access); out->modify = ConvertWindowsTimeToPosixTime(modify); } else { out->create = create; out->access = access; out->modify = modify; } /* We're not using local timestamps. */ out->is_local_time = false; } #else { /* Get the file stats. */ struct stat st; R_UNLESS(::stat(native_path.get(), std::addressof(st)) == 0, ConvertErrnoToResult(ErrnoSource_Stat)); /* Check that the path isn't a directory. */ R_UNLESS(!(S_ISDIR(st.st_mode)), fs::ResultPathNotFound()); /* Set the output timestamps. */ #if defined(ATMOSPHERE_OS_LINUX) if (m_use_posix_time) { out->create = st.st_ctim.tv_sec; out->access = st.st_atim.tv_sec; out->modify = st.st_mtim.tv_sec; } else { out->create = ConvertPosixTimeToWindowsTime(st.st_ctim.tv_sec, st.st_ctim.tv_nsec); out->access = ConvertPosixTimeToWindowsTime(st.st_atim.tv_sec, st.st_atim.tv_nsec); out->modify = ConvertPosixTimeToWindowsTime(st.st_mtim.tv_sec, st.st_mtim.tv_nsec); } #else if (m_use_posix_time) { out->create = st.st_ctimespec.tv_sec; out->access = st.st_atimespec.tv_sec; out->modify = st.st_mtimespec.tv_sec; } else { out->create = ConvertPosixTimeToWindowsTime(st.st_ctimespec.tv_sec, st.st_ctimespec.tv_nsec); out->access = ConvertPosixTimeToWindowsTime(st.st_atimespec.tv_sec, st.st_atimespec.tv_nsec); out->modify = ConvertPosixTimeToWindowsTime(st.st_mtimespec.tv_sec, st.st_mtimespec.tv_nsec); } #endif /* We're not using local timestamps. */ out->is_local_time = false; } #endif R_SUCCEED(); } Result LocalFileSystem::DoQueryEntry(char *dst, size_t dst_size, const char *src, size_t src_size, fs::fsa::QueryId query, const fs::Path &path) { AMS_UNUSED(dst, dst_size, src, src_size, query, path); R_THROW(fs::ResultUnsupportedOperation()); } Result LocalFileSystem::DoCommitProvisionally(s64 counter) { AMS_UNUSED(counter); R_SUCCEED(); } Result LocalFileSystem::DoRollback() { R_SUCCEED(); } } #endif