Files
sphaira/sphaira/source/fs.cpp

816 lines
22 KiB
C++

#include "fs.hpp"
#include "defines.hpp"
#include "ui/nvg_util.hpp"
#include "log.hpp"
#include <switch.h>
#include <cstdio>
#include <cstring>
#include <vector>
#include <string_view>
#include <algorithm>
#include <ranges>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <ftw.h>
namespace fs {
namespace {
// these folders and internals cannot be modified
constexpr std::string_view READONLY_ROOT_FOLDERS[]{
"/atmosphere/automatic_backups",
"/bootloader/res",
"/bootloader/sys",
"/backup", // some people never back this up...
"/Nintendo", // Nintendo private folder
"/Nintendo/Contents",
"/Nintendo/save",
"/emuMMC", // emunand
"/warmboot_mariko",
};
// these files and folders cannot be modified
constexpr std::string_view READONLY_FILES[]{
"/", // don't allow deleting root
"/atmosphere", // don't allow deleting all of /atmosphere
"/atmosphere/hbl.nsp",
"/atmosphere/package3",
"/atmosphere/reboot_payload.bin",
"/atmosphere/stratosphere.romfs",
"/bootloader", // don't allow deleting all of /bootloader
"/bootloader/hekate_ipl.ini",
"/switch", // don't allow deleting all of /switch
"/hbmenu.nro", // breaks hbl
"/payload.bin", // some modchips need this
"/boot.dat", // sxos
"/license.dat", // sxos
"/switch/prod.keys",
"/switch/title.keys",
"/switch/reboot_to_payload.nro",
};
bool is_read_only_root(std::string_view path) {
for (auto p : READONLY_ROOT_FOLDERS) {
if (path.starts_with(p)) {
return true;
}
}
return false;
}
bool is_read_only_file(std::string_view path) {
for (auto p : READONLY_FILES) {
if (path == p) {
return true;
}
}
return false;
}
bool is_read_only(std::string_view path) {
if (is_read_only_root(path)) {
return true;
}
if (is_read_only_file(path)) {
return true;
}
return false;
}
} // namespace
FsPath AppendPath(const FsPath& root_path, const FsPath& file_path) {
FsPath path;
if (root_path[std::strlen(root_path) - 1] != '/') {
std::snprintf(path, sizeof(path), "%s/%s", root_path.s, file_path.s);
} else {
std::snprintf(path, sizeof(path), "%s%s", root_path.s, file_path.s);
}
return path;
}
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return fsFsCreateFile(fs, path, size, option);
}
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return fsFsCreateDirectory(fs, path);
}
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
// try and create the directory / see if it already exists before the loop.
Result rc;
if (fs) {
rc = CreateDirectory(fs, _path, ignore_read_only);
} else {
rc = CreateDirectory(_path, ignore_read_only);
}
if (R_SUCCEEDED(rc) || rc == FsError_PathAlreadyExists) {
R_SUCCEED();
}
auto path_view = std::string_view{_path};
// todo: fix this for sdmc: and ums0:
FsPath path{"/"};
if (auto s = std::strchr(_path.s, ':')) {
const int len = (s - _path.s) + 1;
std::snprintf(path, sizeof(path), "%.*s/", len, _path.s);
path_view = path_view.substr(len);
}
for (const auto dir : std::views::split(path_view, '/')) {
if (dir.empty()) {
continue;
}
std::strncat(path, dir.data(), dir.size());
log_write("[FS] dir creation path is now: %s\n", path.s);
if (fs) {
rc = CreateDirectory(fs, path, ignore_read_only);
} else {
rc = CreateDirectory(path, ignore_read_only);
}
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
log_write("failed to create folder: %s\n", path.s);
return rc;
}
// log_write("created_directory: %s\n", path);
std::strcat(path, "/");
}
R_SUCCEED();
}
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
// strip file name form path.
const auto last_slash = std::strrchr(_path, '/');
if (!last_slash) {
R_SUCCEED();
}
FsPath new_path{};
std::snprintf(new_path, sizeof(new_path), "%.*s", (int)(last_slash - _path.s), _path.s);
return CreateDirectoryRecursively(fs, new_path, ignore_read_only);
}
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return fsFsDeleteFile(fs, path);
}
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return fsFsDeleteDirectory(fs, path);
}
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return fsFsDeleteDirectoryRecursively(fs, path);
}
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
return fsFsRenameFile(fs, src, dst);
}
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
return fsFsRenameDirectory(fs, src, dst);
}
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out) {
return fsFsGetEntryType(fs, path, out);
}
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out) {
return fsFsGetFileTimeStampRaw(fs, path, out);
}
Result SetTimestamp(FsFileSystem* fs, const FsPath& path, const FsTimeStampRaw* ts) {
// unsuported.
R_SUCCEED();
}
bool FileExists(FsFileSystem* fs, const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(fs, path, &type), false);
return type == FsDirEntryType_File;
}
bool DirExists(FsFileSystem* fs, const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(fs, path, &type), false);
return type == FsDirEntryType_Dir;
}
Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>& out) {
FsNative fs{_fs, false};
R_TRY(fs.GetFsOpenResult());
File f;
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(f.Close());
s64 size;
R_TRY(f.GetSize(&size));
out.resize(size);
u64 bytes_read;
R_TRY(f.Read(0, out.data(), out.size(), FsReadOption_None, &bytes_read));
R_UNLESS(bytes_read == out.size(), 1);
R_SUCCEED();
}
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
FsNative fs{_fs, false, ignore_read_only};
R_TRY(fs.GetFsOpenResult());
if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
return rc;
}
File f;
R_TRY(fs.OpenFile(path, FsOpenMode_Write, &f));
ON_SCOPE_EXIT(f.Close());
R_TRY(f.SetSize(in.size()));
R_TRY(f.Write(0, in.data(), in.size(), FsWriteOption_None));
R_SUCCEED();
}
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
std::vector<u8> data;
R_TRY(read_entire_file(fs, src, data));
return write_entire_file(fs, dst, data, ignore_read_only);
}
Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
auto fd = open(path, O_WRONLY | O_CREAT, DEFFILEMODE);
if (fd == -1) {
if (errno == EEXIST) {
return FsError_PathAlreadyExists;
}
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
ON_SCOPE_EXIT(close(fd));
if (size) {
R_UNLESS(!ftruncate(fd, size), Fs::ResultUnknownStdioError);
}
R_SUCCEED();
}
Result CreateDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
if (mkdir(path, ACCESSPERMS)) {
if (errno == EEXIST) {
return FsError_PathAlreadyExists;
}
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return CreateDirectoryRecursively(nullptr, path, ignore_read_only);
}
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return CreateDirectoryRecursivelyWithPath(nullptr, path, ignore_read_only);
}
Result DeleteFile(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
if (unlink(path)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
Result DeleteDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
if (rmdir(path)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
// ftw / ntfw isn't found by linker...
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
#if 0
// const auto unlink_cb = [](const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) -> int {
const auto unlink_cb = [](const char *fpath, const struct stat *sb, int typeflag) -> int {
return remove(fpath);
};
// todo: check for reasonable max fd limit
// if (nftw(path, unlink_cb, 16, FTW_DEPTH)) {
if (ftw(path, unlink_cb, 16)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
#else
R_THROW(0xFFFF);
#endif
}
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
if (rename(src, dst)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
return RenameFile(src, dst, ignore_read_only);
}
Result GetEntryType(const FsPath& path, FsDirEntryType* out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
*out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir;
R_SUCCEED();
}
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
out->is_valid = true;
out->created = st.st_ctim.tv_sec;
out->modified = st.st_mtim.tv_sec;
out->accessed = st.st_atim.tv_sec;
R_SUCCEED();
}
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw* ts) {
if (ts->is_valid) {
timeval val[2]{};
val[0].tv_sec = ts->accessed;
val[1].tv_sec = ts->modified;
if (utimes(path, val)) {
log_write("utimes() failed: %d %s\n", errno, strerror(errno));
}
}
R_SUCCEED();
}
bool FileExists(const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(path, &type), false);
return type == FsDirEntryType_File;
}
bool DirExists(const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(path, &type), false);
return type == FsDirEntryType_Dir;
}
Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
auto f = std::fopen(path, "rb");
if (!f) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
ON_SCOPE_EXIT(std::fclose(f));
std::fseek(f, 0, SEEK_END);
const auto size = std::ftell(f);
std::rewind(f);
out.resize(size);
std::fread(out.data(), 1, out.size(), f);
R_SUCCEED();
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
auto f = std::fopen(path, "wb");
if (!f) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
ON_SCOPE_EXIT(std::fclose(f));
std::fwrite(in.data(), 1, in.size(), f);
R_SUCCEED();
}
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
std::vector<u8> data;
R_TRY(read_entire_file(src, data));
return write_entire_file(dst, data, ignore_read_only);
}
Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) {
f->m_fs = fs;
if (f->m_fs->IsNative()) {
auto fs = (fs::FsNative*)f->m_fs;
R_TRY(fsFsOpenFile(&fs->m_fs, path, mode, &f->m_native));
} else {
if ((mode & FsOpenMode_Read) && (mode & FsOpenMode_Write)) {
f->m_stdio = std::fopen(path, "rb+");
} else if (mode & FsOpenMode_Read) {
f->m_stdio = std::fopen(path, "rb");
} else if (mode & FsOpenMode_Write) {
// not possible to open file with just write and not append
// or create or truncate. So rw it is!
f->m_stdio = std::fopen(path, "rb+");
}
R_UNLESS(f->m_stdio, Fs::ResultUnknownStdioError);
std::strcpy(f->m_path, path);
}
R_SUCCEED();
}
File::~File() {
Close();
}
Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read) {
*bytes_read = 0;
R_UNLESS(m_fs, 0x1);
if (m_fs->IsNative()) {
R_TRY(fsFileRead(&m_native, off, buf, read_size, option, bytes_read));
} else {
if (m_stdio_off != off) {
m_stdio_off = off;
std::fseek(m_stdio, off, SEEK_SET);
}
*bytes_read = std::fread(buf, 1, read_size, m_stdio);
// if we read less bytes than expected, check if there was an error (ignoring eof).
if (*bytes_read < read_size) {
if (!std::feof(m_stdio) && std::ferror(m_stdio)) {
R_THROW(Fs::ResultUnknownStdioError);
}
}
m_stdio_off += *bytes_read;
}
R_SUCCEED();
}
Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
R_UNLESS(m_fs, 0x1);
if (m_fs->IsNative()) {
R_TRY(fsFileWrite(&m_native, off, buf, write_size, option));
} else {
if (m_stdio_off != off) {
log_write("[FS] diff seek\n");
m_stdio_off = off;
std::fseek(m_stdio, off, SEEK_SET);
}
const auto result = std::fwrite(buf, 1, write_size, m_stdio);
// log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size);
R_UNLESS(result == write_size, Fs::ResultUnknownStdioError);
m_stdio_off += write_size;
}
R_SUCCEED();
}
Result File::SetSize(s64 sz) {
R_UNLESS(m_fs, 0x1);
if (m_fs->IsNative()) {
R_TRY(fsFileSetSize(&m_native, sz));
} else {
const auto fd = fileno(m_stdio);
R_UNLESS(fd > 0, Fs::ResultUnknownStdioError);
R_UNLESS(!ftruncate(fd, sz), Fs::ResultUnknownStdioError);
}
R_SUCCEED();
}
Result File::GetSize(s64* out) {
R_UNLESS(m_fs, 0x1);
if (m_fs->IsNative()) {
R_TRY(fsFileGetSize(&m_native, out));
} else {
struct stat st;
const auto fd = fileno(m_stdio);
bool did_stat{};
if (fd && !fstat(fd, &st)) {
did_stat = true;
}
if (!did_stat) {
R_UNLESS(!lstat(m_path, &st), Fs::ResultUnknownStdioError);
}
*out = st.st_size;
}
R_SUCCEED();
}
void File::Close() {
if (!m_fs) {
return;
}
if (m_fs->IsNative()) {
if (serviceIsActive(&m_native.s)) {
fsFileClose(&m_native);
m_native = {};
}
} else {
if (m_stdio) {
std::fclose(m_stdio);
m_stdio = {};
}
}
}
Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d) {
d->m_fs = fs;
d->m_mode = mode;
if (d->m_fs->IsNative()) {
auto fs = (fs::FsNative*)d->m_fs;
R_TRY(fsFsOpenDirectory(&fs->m_fs, path, mode, &d->m_native));
} else {
d->m_stdio = opendir(path);
R_UNLESS(d->m_stdio, Fs::ResultUnknownStdioError);
}
R_SUCCEED();
}
Result DirGetEntryCount(fs::Fs* m_fs, const fs::FsPath& path, s64* count, u32 mode) {
s64 file_count, dir_count;
R_TRY(DirGetEntryCount(m_fs, path, &file_count, &dir_count, mode));
*count = file_count + dir_count;
R_SUCCEED();
}
Result DirGetEntryCount(fs::Fs* m_fs, const fs::FsPath& path, s64* file_count, s64* dir_count, u32 mode) {
*file_count = *dir_count = 0;
if (m_fs->IsNative()) {
if (mode & FsDirOpenMode_ReadDirs){
fs::Dir dir;
R_TRY(m_fs->OpenDirectory(path, FsDirOpenMode_ReadDirs|FsDirOpenMode_NoFileSize, &dir));
R_TRY(dir.GetEntryCount(dir_count));
}
if (mode & FsDirOpenMode_ReadFiles){
fs::Dir dir;
R_TRY(m_fs->OpenDirectory(path, FsDirOpenMode_ReadFiles|FsDirOpenMode_NoFileSize, &dir));
R_TRY(dir.GetEntryCount(file_count));
}
} else {
fs::Dir dir;
R_TRY(m_fs->OpenDirectory(path, mode, &dir));
while (auto d = readdir(dir.m_stdio)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
if (d->d_type == DT_DIR) {
if (!(mode & FsDirOpenMode_ReadDirs)) {
continue;
}
(*dir_count)++;
} else if (d->d_type == DT_REG) {
if (!(mode & FsDirOpenMode_ReadFiles)) {
continue;
}
(*file_count)++;
}
}
}
R_SUCCEED();
}
Dir::~Dir() {
Close();
}
Result Dir::GetEntryCount(s64* out) {
*out = 0;
R_UNLESS(m_fs, 0x1);
if (m_fs->IsNative()) {
R_TRY(fsDirGetEntryCount(&m_native, out));
} else {
while (auto d = readdir(m_stdio)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
(*out)++;
}
// NOTE: this will *not* work for native mounted folders!!!
rewinddir(m_stdio);
}
R_SUCCEED();
}
Result Dir::ReadAll(std::vector<FsDirectoryEntry>& buf) {
buf.clear();
R_UNLESS(m_fs, 0x1);
if (m_fs->IsNative()) {
s64 count;
R_TRY(GetEntryCount(&count));
buf.resize(count);
R_TRY(fsDirRead(&m_native, &count, buf.size(), buf.data()));
buf.resize(count);
} else {
buf.reserve(1000);
while (auto d = readdir(m_stdio)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
FsDirectoryEntry entry{};
if (d->d_type == DT_DIR) {
if (!(m_mode & FsDirOpenMode_ReadDirs)) {
continue;
}
entry.type = FsDirEntryType_Dir;
} else if (d->d_type == DT_REG) {
if (!(m_mode & FsDirOpenMode_ReadFiles)) {
continue;
}
entry.type = FsDirEntryType_File;
} else {
log_write("[FS] WARNING: unknown type when reading dir: %u\n", d->d_type);
continue;
}
std::strcpy(entry.name, d->d_name);
buf.emplace_back(entry);
}
}
R_SUCCEED();
}
void Dir::Close() {
if (!m_fs) {
return;
}
if (m_fs->IsNative()) {
if (serviceIsActive(&m_native.s)) {
fsDirClose(&m_native);
m_native = {};
}
} else {
if (m_stdio) {
closedir(m_stdio);
m_stdio = {};
}
}
}
Result FileGetSizeAndTimestamp(fs::Fs* m_fs, const FsPath& path, FsTimeStampRaw* ts, s64* size) {
*ts = {};
*size = {};
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs;
R_TRY(fs->GetFileTimeStampRaw(path, ts));
File f;
R_TRY(m_fs->OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(f.Close());
R_TRY(f.GetSize(size));
} else {
struct stat st;
R_UNLESS(!lstat(path, &st), 0x1);
ts->is_valid = true;
ts->created = st.st_ctim.tv_sec;
ts->modified = st.st_mtim.tv_sec;
ts->accessed = st.st_atim.tv_sec;
*size = st.st_size;
}
R_SUCCEED();
}
Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out) {
*out = true;
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs;
s64 count;
R_TRY(fs->DirGetEntryCount(path, &count, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles));
*out = !count;
} else {
auto dir = opendir(path);
R_UNLESS(dir, 0x1);
ON_SCOPE_EXIT(closedir(dir));
while (auto d = readdir(dir)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
*out = false;
break;
}
}
R_SUCCEED();
}
} // namespace fs