diff --git a/src/kernel/fileSystem.cpp b/src/kernel/fileSystem.cpp index 72edb1e82..d89176af4 100644 --- a/src/kernel/fileSystem.cpp +++ b/src/kernel/fileSystem.cpp @@ -28,7 +28,8 @@ namespace Libs::LibKernel::FileSystem { LIB_NAME("libkernel", "libkernel"); -constexpr int DESCRIPTOR_MIN = 3; +constexpr int DESCRIPTOR_MIN = 3; +constexpr uint64_t DIR_BLOCK_SIZE = 512; enum class SpecialFile { None, @@ -68,7 +69,7 @@ struct File { std::atomic_bool sync_writes; SpecialFile special; Common::Mutex mutex; - std::vector dents; + std::vector dirents; uint64_t dents_offset; }; @@ -119,6 +120,51 @@ static uint64_t AlignUp(uint64_t value, uint64_t alignment) { return (alignment != 0 ? (value + alignment - 1) & ~(alignment - 1) : value); } +static std::vector PackDirents(const std::vector& entries) { + std::vector dirents; + uint64_t offset = 0; + uint64_t last_reclen_offset = 0; + for (const auto& entry: entries) { + const auto& name = entry.name; + EXIT_NOT_IMPLEMENTED(name.size() > 255); + const auto reclen = AlignUp(8 + name.size() + 1, 4); + if (offset + reclen > dirents.size()) { + if (!dirents.empty()) { + auto* last_reclen = + reinterpret_cast(dirents.data() + last_reclen_offset); + *last_reclen += static_cast(dirents.size() - offset); + } + offset = dirents.size(); + dirents.resize(offset + DIR_BLOCK_SIZE); + } + *reinterpret_cast(dirents.data() + offset) = + Common::hash(name.data(), static_cast(name.size())); + *reinterpret_cast(dirents.data() + offset + 4) = static_cast(reclen); + dirents[offset + 6] = entry.is_file ? 8 : 4; + dirents[offset + 7] = static_cast(name.size()); + memcpy(dirents.data() + offset + 8, name.c_str(), name.size() + 1); + last_reclen_offset = offset + 4; + offset += reclen; + } + if (!dirents.empty()) { + auto* last_reclen = reinterpret_cast(dirents.data() + last_reclen_offset); + *last_reclen += static_cast(dirents.size() - offset); + } + return dirents; +} + +static uint64_t ReadDirectory(File* file, void* buf, uint64_t count) { + const auto remaining = + file->dents_offset < file->dirents.size() ? file->dirents.size() - file->dents_offset : 0; + const auto bytes = std::min(count, remaining); + if (bytes != 0) { + Memory::InvalidateMemory(reinterpret_cast(buf), bytes); + memcpy(buf, file->dirents.data() + file->dents_offset, bytes); + file->dents_offset += bytes; + } + return bytes; +} + static int PosixToKernel(int posix_errno) { return KERNEL_ERROR_UNKNOWN + posix_errno; } @@ -433,15 +479,16 @@ int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode) { EXIT_NOT_IMPLEMENTED(!directory && rw_mode != Common::File::Mode::Read); EXIT_NOT_IMPLEMENTED(!directory && (trunc || creat)); - file->dents = Common::File::GetDirEntries(file->real_name); + const auto entries = Common::File::GetDirEntries(file->real_name); + file->dirents = PackDirents(entries); file->dents_offset = 0; file->directory = true; LOGF_COLOR(Log::Color::Green, "\tOpen dir: %s, entries = %" PRIu32 ", [ok]\n", Common::PathToString(file->real_name).c_str(), - static_cast(file->dents.size())); + static_cast(entries.size())); - for (const auto& f: file->dents) { + for (const auto& f: entries) { LOGF("\t\t%s %s\n", f.is_file ? "[file]" : "[dir ]", f.name.c_str()); } } else { @@ -538,11 +585,18 @@ int64_t KYTY_SYSV_ABI KernelRead(int d, void* buf, size_t nbytes) { return KERNEL_ERROR_EBADF; } - EXIT_NOT_IMPLEMENTED(file->directory); - EXIT_IF(!file->opened); - EXIT_NOT_IMPLEMENTED(nbytes > UINT_MAX); + if (nbytes > INT_MAX) { + return KERNEL_ERROR_EINVAL; + } + if (file->directory) { + Common::LockGuard lock(file->mutex); + const auto bytes = ReadDirectory(file, buf, nbytes); + LOGF("\tRead %" PRIu64 " directory bytes from: %s\n", bytes, + Common::PathToString(file->real_name).c_str()); + return static_cast(bytes); + } if (file->special == SpecialFile::Random) { FillRandomBuffer(buf, nbytes); @@ -761,7 +815,7 @@ int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence) { PRINT_NAME(); if (d < DESCRIPTOR_MIN) { - return KERNEL_ERROR_EPERM; + return KERNEL_ERROR_EBADF; } auto* file = g_files->GetFile(d); @@ -770,8 +824,6 @@ int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence) { return KERNEL_ERROR_EBADF; } - EXIT_NOT_IMPLEMENTED(file->directory); - EXIT_IF(!file->opened); if (file->special != SpecialFile::None) { @@ -780,38 +832,47 @@ int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence) { Common::LockGuard lock(file->mutex); - bool is_invalid = file->f.IsInvalid(); - - if (whence == 1) { - offset = static_cast(file->f.Tell()) + offset; - whence = 0; - } - - if (whence == 2) { - offset = static_cast(file->f.Size()) + offset; - whence = 0; - } - - EXIT_NOT_IMPLEMENTED(whence != 0); - - if (offset < 0) { - return KERNEL_ERROR_EINVAL; - } - - file->f.Seek(offset); - auto pos = static_cast(file->f.Tell()); - - EXIT_IF(pos != offset); - - if (is_invalid) { + if (!file->directory && file->f.IsInvalid()) { LOGF("\tfile is invalid\n"); return KERNEL_ERROR_EIO; } - LOGF("\tLseek (pos = %" PRId64 ") to: %s\n", offset, + uint64_t base = 0; + switch (whence) { + case 0: break; + case 1: base = file->directory ? file->dents_offset : file->f.Tell(); break; + case 2: base = file->directory ? file->dirents.size() : file->f.Size(); break; + default: return KERNEL_ERROR_EINVAL; + } + + uint64_t position = 0; + if (offset >= 0) { + if (base > static_cast(INT64_MAX - offset)) { + return KERNEL_ERROR_EOVERFLOW; + } + position = base + static_cast(offset); + } else { + const auto distance = uint64_t {0} - static_cast(offset); + if (base < distance) { + return KERNEL_ERROR_EINVAL; + } + position = base - distance; + if (position > INT64_MAX) { + return KERNEL_ERROR_EOVERFLOW; + } + } + + if (file->directory) { + file->dents_offset = position; + } else { + file->f.Seek(position); + EXIT_IF(file->f.Tell() != position); + } + + LOGF("\tLseek (pos = %" PRIu64 ") to: %s\n", position, Common::PathToString(file->real_name).c_str()); - return pos; + return static_cast(position); } int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb) { @@ -842,9 +903,10 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb) { auto wt = at; if (is_dir) { - stat.st_size = 0; + stat.st_size = + static_cast(PackDirents(Common::File::GetDirEntries(real_file_name)).size()); stat.st_blksize = 512; - stat.st_blocks = 0; + stat.st_blocks = stat.st_size / 512; } else { stat.st_size = static_cast(Common::File::Size(real_file_name)); stat.st_blksize = 512; @@ -920,9 +982,9 @@ int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb) { stat.st_blksize = 512; stat.st_blocks = (stat.st_size + 511) / 512; } else { - stat.st_size = 0; + stat.st_size = static_cast(file->dirents.size()); stat.st_blksize = 512; - stat.st_blocks = 0; + stat.st_blocks = stat.st_size / 512; } SecToTimespec(&stat.st_atim, at.ToUnix()); @@ -1057,14 +1119,15 @@ int KYTY_SYSV_ABI KernelGetdirentries(int fd, char* buf, int nbytes, int64_t* ba return KERNEL_ERROR_EBADF; } - constexpr uint64_t DIR_BLOCK_SIZE = 512; - constexpr uint64_t DIRENT_META_SIZE = 8; - if (!file->directory || nbytes < static_cast(DIR_BLOCK_SIZE)) { return KERNEL_ERROR_EINVAL; } EXIT_IF(!file->opened); + Common::LockGuard lock(file->mutex); + if (file->dents_offset > file->dirents.size() || file->dents_offset % DIR_BLOCK_SIZE != 0) { + return KERNEL_ERROR_EINVAL; + } LOGF("\t dir = %s\n" "\t nbytes = %d\n" @@ -1074,94 +1137,8 @@ int KYTY_SYSV_ABI KernelGetdirentries(int fd, char* buf, int nbytes, int64_t* ba if (basep != nullptr) { *basep = static_cast(file->dents_offset); } - - std::vector dirents; - dirents.reserve( - std::max(static_cast(DIR_BLOCK_SIZE), file->dents.size() * 64u)); - - uint64_t next_ceiling = 0; - uint64_t dirent_offset = 0; - int64_t last_reclen_offset = -1; - - for (const auto& entry: file->dents) { - const auto& str = entry.name; - auto str_size = str.size(); - EXIT_NOT_IMPLEMENTED(str_size > 255); - - uint64_t reclen = AlignUp(DIRENT_META_SIZE + str_size + 1, 4); - if (dirent_offset + reclen > next_ceiling) { - if (last_reclen_offset >= 0) { - auto* last_reclen = - reinterpret_cast(dirents.data() + last_reclen_offset); - *last_reclen = static_cast(*last_reclen + (next_ceiling - dirent_offset)); - } - - dirent_offset = next_ceiling; - next_ceiling += DIR_BLOCK_SIZE; - dirents.resize(next_ceiling); - std::fill(dirents.begin() + static_cast(dirent_offset), dirents.end(), - 0); - } - - *reinterpret_cast(dirents.data() + dirent_offset + 0) = - Common::hash(str.data(), static_cast(str.size())); - *reinterpret_cast(dirents.data() + dirent_offset + 4) = - static_cast(reclen); - *reinterpret_cast(dirents.data() + dirent_offset + 6) = (entry.is_file ? 8 : 4); - *reinterpret_cast(dirents.data() + dirent_offset + 7) = - static_cast(str_size); - memcpy(dirents.data() + dirent_offset + 8, str.data(), str_size); - dirents[dirent_offset + 8 + str_size] = '\0'; - - last_reclen_offset = static_cast(dirent_offset + 4); - dirent_offset += reclen; - - LOGF("\t name = %s\n", str.data()); - } - - if (last_reclen_offset >= 0) { - auto* last_reclen = reinterpret_cast(dirents.data() + last_reclen_offset); - *last_reclen = static_cast(*last_reclen + (next_ceiling - dirent_offset)); - } - - const uint64_t directory_size = next_ceiling; - if (file->dents_offset >= directory_size) { - return 0; - } - - uint64_t bytes_written = 0; - uint64_t working_offset = file->dents_offset; - uint64_t dirent_buffer_offset = 0; - uint64_t aligned_count = AlignDown(static_cast(nbytes), DIR_BLOCK_SIZE); - - while (dirent_buffer_offset < dirents.size()) { - const auto* normal_dirent = dirents.data() + dirent_buffer_offset; - uint16_t reclen = *reinterpret_cast(normal_dirent + 4); - uint8_t namlen = *(normal_dirent + 7); - - if (namlen == 0 || reclen == 0) { - break; - } - - if (working_offset >= reclen) { - dirent_buffer_offset += reclen; - working_offset -= reclen; - continue; - } - - if (bytes_written + reclen > aligned_count) { - break; - } - - const auto bytes_to_copy = reclen - working_offset; - memcpy(buf + bytes_written, normal_dirent + working_offset, bytes_to_copy); - bytes_written += bytes_to_copy; - dirent_buffer_offset += reclen; - working_offset = 0; - } - - file->dents_offset += bytes_written; - return static_cast(bytes_written); + return static_cast( + ReadDirectory(file, buf, AlignDown(static_cast(nbytes), DIR_BLOCK_SIZE))); } int KYTY_SYSV_ABI KernelGetdents(int fd, char* buf, int nbytes) { diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index 400a64ec6..28b93ec5d 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -1,8 +1,10 @@ +#include "SDL.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/logging/log.h" #include "common/subsystems.h" #include "common/threads.h" +#include "graphics/presentation/window/windowInternal.h" #include "kernel/fileSystem.h" #include "libs/errno.h" #include "libs/network.h" @@ -15,8 +17,11 @@ #include #include #include +#include +#include #include #include +#include namespace Libs::LibKernelApr { void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols); @@ -123,6 +128,109 @@ void CheckMountRoot(const std::filesystem::path &root) { FileSystem::Umount("/app0"); } +void CheckDirectoryStream(const std::filesystem::path &root) { + const auto directory = root / "directory-stream"; + Check(std::filesystem::create_directory(directory), + "create seek fixture directory"); + for (int i = 0; i < 48; ++i) { + Common::File fixture; + Check(fixture.Create(directory / ("directory-entry-" + std::to_string(i))), + "create enough entries to cross directory blocks"); + fixture.Close(); + } + FileSystem::Mount(directory, "/app0"); + const int fd = FileSystem::KernelOpen("/app0/", 0, 0); + Check(fd >= 3, "open directory as read-only asset"); + const auto end = FileSystem::KernelLseek(fd, 0, 2); + Check(end > 512 && end % 512 == 0, + "directory SEEK_END uses padded stream size"); + FileSystem::FileStat stat{}; + Check(FileSystem::KernelFstat(fd, &stat) == OK && stat.st_size == end && + stat.st_blksize == 512, + "directory stat agrees with seek and enumeration"); + Check(FileSystem::KernelStat("/app0/", &stat) == OK && stat.st_size == end && + stat.st_blocks == end / 512, + "path and descriptor directory stat agree"); + Check(FileSystem::KernelLseek(fd, 0, 0) == 0, + "rewind directory for asset read"); + std::vector raw(static_cast(end)); + Check(FileSystem::KernelRead(fd, raw.data(), raw.size()) == end && + FileSystem::KernelRead(fd, raw.data(), 1) == 0, + "raw directory read reaches EOF"); + Check(FileSystem::KernelLseek(fd, -end, 1) == 0, + "directory SEEK_CUR uses the position advanced by read"); + + std::array block{}; + int64_t base = -1; + for (int64_t offset = 0; offset < end; offset += block.size()) { + Check(FileSystem::KernelGetdirentries(fd, block.data(), block.size(), + &base) == block.size() && + base == offset && + std::memcmp(block.data(), raw.data() + offset, block.size()) == 0, + "directory enumeration shares raw bytes and reports each block " + "position"); + for (size_t pos = 0; pos < block.size();) { + Check(block.size() - pos >= 8, "directory record header fits"); + uint16_t length = 0; + std::memcpy(&length, block.data() + pos + 4, sizeof(length)); + const auto name_length = static_cast(block[pos + 7]); + Check(length >= 9 + name_length && length <= block.size() - pos && + block[pos + 8 + name_length] == '\0', + "directory entries remain complete within every block"); + pos += length; + } + } + Check(FileSystem::KernelGetdirentries(fd, block.data(), block.size(), + &base) == 0 && + base == end, + "directory enumeration reports EOF position"); + Check(FileSystem::KernelLseek(fd, 512, 0) == 512 && + FileSystem::KernelGetdirentries(fd, block.data(), block.size(), + &base) == block.size() && + base == 512 && + std::memcmp(block.data(), raw.data() + 512, block.size()) == 0, + "restore and reread a directory enumeration position"); + + const auto position = FileSystem::KernelLseek(fd, 0, 1); + Check( + FileSystem::KernelLseek(fd, 0, 9) == + Libs::LibKernel::KERNEL_ERROR_EINVAL && + FileSystem::KernelLseek(fd, -1, 0) == + Libs::LibKernel::KERNEL_ERROR_EINVAL && + FileSystem::KernelLseek(fd, std::numeric_limits::min(), 1) == + Libs::LibKernel::KERNEL_ERROR_EINVAL && + FileSystem::KernelLseek(fd, std::numeric_limits::max(), 1) == + Libs::LibKernel::KERNEL_ERROR_EOVERFLOW && + FileSystem::KernelLseek(fd, 0, 1) == position, + "invalid and overflowing directory seeks preserve the position"); + Check(FileSystem::KernelLseek(fd, -19, 2) == end - 19 && + FileSystem::KernelRead(fd, block.data(), block.size()) == 19 && + std::memcmp(block.data(), raw.data() + end - 19, 19) == 0, + "raw directory reads support byte positions and stop at EOF"); + Check(FileSystem::KernelLseek(fd, 1, 0) == 1 && + FileSystem::KernelGetdents(fd, block.data(), block.size()) == + Libs::LibKernel::KERNEL_ERROR_EINVAL && + FileSystem::KernelLseek(fd, 0, 1) == 1, + "directory enumeration rejects an incomplete record position"); + Check(FileSystem::KernelLseek(fd, 512, 2) == end + 512 && + FileSystem::KernelGetdirentries(fd, block.data(), block.size(), + &base) == + Libs::LibKernel::KERNEL_ERROR_EINVAL && + FileSystem::KernelLseek(fd, 0, 1) == end + 512, + "directory enumeration rejects a position beyond EOF"); + Check(FileSystem::KernelRead( + fd, block.data(), + static_cast(std::numeric_limits::max()) + 1) == + Libs::LibKernel::KERNEL_ERROR_EINVAL && + FileSystem::KernelLseek(fd, 0, 1) == end + 512, + "oversized read fails without changing the directory position"); + Check(FileSystem::KernelClose(fd) == OK, "close directory stream"); + Check(FileSystem::KernelLseek(fd, 0, 0) == + Libs::LibKernel::KERNEL_ERROR_EBADF, + "seek rejects a closed descriptor"); + FileSystem::Umount("/app0"); +} + void CheckAprPaths(const std::filesystem::path &root) { Loader::SymbolDatabase symbols; Libs::LibKernelApr::InitLibKernel_1_Apr(&symbols); @@ -267,15 +375,26 @@ int main() { Config::Load(options); subsystems.Initialize(); + Check(SDL_InitSubSystem(SDL_INIT_VIDEO) == 0, "initialize Vulkan test video"); + auto graphics = std::make_unique(); + graphics->graphic_ctx.screen_width = 64; + graphics->graphic_ctx.screen_height = 64; + graphics->window = SDL_CreateWindow("KernelFileSystemTests", 0, 0, 64, 64, + SDL_WINDOW_VULKAN | SDL_WINDOW_HIDDEN); + Check(graphics->window != nullptr, "create hidden Vulkan test window"); + graphics->CreateVulkan(); + TempDirectory temporary; FileSystem::Initialize(); CheckMountRoot(temporary.Path()); + CheckDirectoryStream(temporary.Path()); CheckAprPaths(temporary.Path()); FileSystem::Mount(temporary.Path(), "/savedata0"); CheckSaveRename(temporary.Path(), "first-save"); CheckSaveRename(temporary.Path(), "replacement-save"); FileSystem::Shutdown(); CheckSocketWakeup(); + graphics.reset(); subsystems.Destroy(); std::printf("KernelFileSystemTests: all cases passed\n");