From d6c8f120c655bf34536cd44ba854aecbf010368f Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:01:53 +0100 Subject: [PATCH] fs: add zip mount support. hash: fix not checking open result for file. fs: fix stdio not checking nullptr access. --- sphaira/CMakeLists.txt | 3 +- sphaira/include/yati/source/file.hpp | 1 + sphaira/source/fatfs.cpp | 26 +- sphaira/source/fs.cpp | 7 + sphaira/source/hasher.cpp | 2 + sphaira/source/ui/menus/filebrowser.cpp | 14 +- sphaira/source/utils/devoptab_zip.cpp | 766 ++++++++++++++++++++++++ sphaira/source/yati/source/file.cpp | 4 + 8 files changed, 807 insertions(+), 16 deletions(-) create mode 100644 sphaira/source/utils/devoptab_zip.cpp diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index dbc76ee..077b67c 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -93,8 +93,7 @@ add_executable(sphaira source/utils/devoptab_save.cpp source/utils/devoptab_nsp.cpp source/utils/devoptab_xci.cpp - # todo: - # source/utils/devoptab_zip.cpp + source/utils/devoptab_zip.cpp source/usb/base.cpp source/usb/usbds.cpp diff --git a/sphaira/include/yati/source/file.hpp b/sphaira/include/yati/source/file.hpp index bfdd875..2fadace 100644 --- a/sphaira/include/yati/source/file.hpp +++ b/sphaira/include/yati/source/file.hpp @@ -10,6 +10,7 @@ namespace sphaira::yati::source { struct File final : Base { File(fs::Fs* fs, const fs::FsPath& path); Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override; + Result GetSize(s64* out); private: fs::Fs* m_fs{}; diff --git a/sphaira/source/fatfs.cpp b/sphaira/source/fatfs.cpp index e3c5bc2..19b16d0 100644 --- a/sphaira/source/fatfs.cpp +++ b/sphaira/source/fatfs.cpp @@ -192,20 +192,20 @@ Result ReadStorage(FsStorage* storage, std::span lru_cache, voi // if the dst is big enough, read data in place. if (read_size > alloc_size) { - if (R_SUCCEEDED(fsStorageRead(storage, file_off, dst, read_size))) { - const auto bytes_read = read_size; - read_size -= bytes_read; - file_off += bytes_read; - amount += bytes_read; - dst += bytes_read; + R_TRY(fsStorageRead(storage, file_off, dst, read_size)); + const auto bytes_read = read_size; + read_size -= bytes_read; + file_off += bytes_read; + amount += bytes_read; + dst += bytes_read; - // save the last chunk of data to the m_buffered io. - const auto max_advance = std::min(amount, alloc_size); - m_buffered->off = file_off - max_advance; - m_buffered->size = max_advance; - std::memcpy(m_buffered->data, dst - max_advance, max_advance); - } - } else if (R_SUCCEEDED(fsStorageRead(storage, file_off, m_buffered->data, alloc_size))) { + // save the last chunk of data to the m_buffered io. + const auto max_advance = std::min(amount, alloc_size); + m_buffered->off = file_off - max_advance; + m_buffered->size = max_advance; + std::memcpy(m_buffered->data, dst - max_advance, max_advance); + } else { + R_TRY(fsStorageRead(storage, file_off, m_buffered->data, alloc_size)); const auto bytes_read = alloc_size; const auto max_advance = std::min(read_size, bytes_read); std::memcpy(dst, m_buffered->data, max_advance); diff --git a/sphaira/source/fs.cpp b/sphaira/source/fs.cpp index 68a089e..2d7bbd1 100644 --- a/sphaira/source/fs.cpp +++ b/sphaira/source/fs.cpp @@ -498,6 +498,8 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea if (m_fs->IsNative()) { R_TRY(fsFileRead(&m_native, off, buf, read_size, option, bytes_read)); } else { + R_UNLESS(m_stdio, Result_FsUnknownStdioError); + if (m_stdio_off != off) { m_stdio_off = off; std::fseek(m_stdio, off, SEEK_SET); @@ -524,6 +526,8 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) { if (m_fs->IsNative()) { R_TRY(fsFileWrite(&m_native, off, buf, write_size, option)); } else { + R_UNLESS(m_stdio, Result_FsUnknownStdioError); + if (m_stdio_off != off) { log_write("[FS] diff seek\n"); m_stdio_off = off; @@ -546,6 +550,7 @@ Result File::SetSize(s64 sz) { if (m_fs->IsNative()) { R_TRY(fsFileSetSize(&m_native, sz)); } else { + R_UNLESS(m_stdio, Result_FsUnknownStdioError); const auto fd = fileno(m_stdio); R_UNLESS(fd > 0, Result_FsUnknownStdioError); R_UNLESS(!ftruncate(fd, sz), Result_FsUnknownStdioError); @@ -560,6 +565,8 @@ Result File::GetSize(s64* out) { if (m_fs->IsNative()) { R_TRY(fsFileGetSize(&m_native, out)); } else { + R_UNLESS(m_stdio, Result_FsUnknownStdioError); + struct stat st; R_UNLESS(!fstat(fileno(m_stdio), &st), Result_FsUnknownStdioError); *out = st.st_size; diff --git a/sphaira/source/hasher.cpp b/sphaira/source/hasher.cpp index 80b8f56..2703b0e 100644 --- a/sphaira/source/hasher.cpp +++ b/sphaira/source/hasher.cpp @@ -18,10 +18,12 @@ struct FileSource final : BaseSource { } Result Size(s64* out) override { + R_TRY(m_open_result); return m_file.GetSize(out); } Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override { + R_TRY(m_open_result); const auto rc = m_file.Read(off, buf, size, 0, bytes_read); if (m_fs->IsNative() && m_is_file_based_emummc) { svcSleepThread(2e+6); // 2ms diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 631275c..a744cdf 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -690,6 +690,8 @@ void FsView::OnClick() { MountNspFs(); } else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) { MountXciFs(); + } else if (IsExtension(entry.GetExtension(), "zip")) { + MountZipFs(); } else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { InstallFiles(); } else if (IsSd()) { @@ -1991,7 +1993,17 @@ void FsView::MountXciFs() { } void FsView::MountZipFs() { - //todo: + fs::FsPath mount; + const auto rc = devoptab::MountZip(GetFs(), GetNewPathCurrent(), mount); + App::PushErrorBox(rc, "Failed to mount zip."_i18n); + + if (R_SUCCEEDED(rc)) { + auto fs = std::make_shared(mount, [mount](){ + devoptab::UmountZip(mount); + }); + + MountFsHelper(fs, GetEntry().GetName()); + } } Base::Base(u32 flags, u32 options) diff --git a/sphaira/source/utils/devoptab_zip.cpp b/sphaira/source/utils/devoptab_zip.cpp new file mode 100644 index 0000000..dbbd99d --- /dev/null +++ b/sphaira/source/utils/devoptab_zip.cpp @@ -0,0 +1,766 @@ + +#include "utils/devoptab.hpp" +#include "defines.hpp" +#include "log.hpp" + +#include "yati/source/file.hpp" + +#include +#include +#include +#include +#include +#include + +namespace sphaira::devoptab { +namespace { + +struct BufferedFileData { + BufferedFileData(std::unique_ptr&& _source, u64 _size) + : source{std::forward(_source)} + , capacity{_size} { + + } + + Result Read(void *_buffer, u64 file_off, u64 read_size); + +private: + std::unique_ptr source; + const u64 capacity; + + u64 m_off{}; + u64 m_size{}; + u8 m_data[1024 * 64]{}; +}; + +Result BufferedFileData::Read(void *_buffer, u64 file_off, u64 read_size) { + auto dst = static_cast(_buffer); + size_t amount = 0; + + R_UNLESS(file_off < capacity, FsError_UnsupportedOperateRangeForFileStorage); + read_size = std::min(read_size, capacity - file_off); + + if (m_size) { + // check if we can read this data into the beginning of dst. + if (file_off < m_off + m_size && file_off >= m_off) { + const auto off = file_off - m_off; + const auto size = std::min(read_size, m_size - off); + if (size) { + std::memcpy(dst, m_data + off, size); + + read_size -= size; + file_off += size; + amount += size; + dst += size; + } + } + } + + if (read_size) { + const auto alloc_size = sizeof(m_data); + m_off = 0; + m_size = 0; + u64 bytes_read; + + // if the dst is big enough, read data in place. + if (read_size > alloc_size) { + R_TRY(source->Read(dst, file_off, read_size, &bytes_read)); + + read_size -= bytes_read; + file_off += bytes_read; + amount += bytes_read; + dst += bytes_read; + + // save the last chunk of data to the m_buffered io. + const auto max_advance = std::min(amount, alloc_size); + m_off = file_off - max_advance; + m_size = max_advance; + std::memcpy(m_data, dst - max_advance, max_advance); + } else { + R_TRY(source->Read(m_data, file_off, alloc_size, &bytes_read)); + const auto bytes_read = alloc_size; + const auto max_advance = std::min(read_size, bytes_read); + std::memcpy(dst, m_data, max_advance); + + m_off = file_off; + m_size = bytes_read; + + read_size -= max_advance; + file_off += max_advance; + amount += max_advance; + dst += max_advance; + } + } + + R_SUCCEED(); +} + +#define LOCAL_HEADER_SIG 0x4034B50 +#define FILE_HEADER_SIG 0x2014B50 +#define DATA_DESCRIPTOR_SIG 0x8074B50 +#define END_RECORD_SIG 0x6054B50 + +enum mmz_Flag { + mmz_Flag_Encrypted = 1 << 0, + mmz_Flag_DataDescriptor = 1 << 3, + mmz_Flag_StrongEncrypted = 1 << 6, +}; + +enum mmz_Compression { + mmz_Compression_None = 0, + mmz_Compression_Deflate = 8, +}; + +// 30 bytes (0x1E) +#pragma pack(push,1) +typedef struct mmz_LocalHeader { + uint32_t sig; + uint16_t version; + uint16_t flags; + uint16_t compression; + uint16_t modtime; + uint16_t moddate; + uint32_t crc32; + uint32_t compressed_size; + uint32_t uncompressed_size; + uint16_t filename_len; + uint16_t extrafield_len; +} mmz_LocalHeader; +#pragma pack(pop) + +#pragma pack(push,1) +typedef struct mmz_DataDescriptor { + uint32_t sig; + uint32_t crc32; + uint32_t compressed_size; + uint32_t uncompressed_size; +} mmz_DataDescriptor; +#pragma pack(pop) + +// 46 bytes (0x2E) +#pragma pack(push,1) +typedef struct mmz_FileHeader { + uint32_t sig; + uint16_t version; + uint16_t version_needed; + uint16_t flags; + uint16_t compression; + uint16_t modtime; + uint16_t moddate; + uint32_t crc32; + uint32_t compressed_size; + uint32_t uncompressed_size; + uint16_t filename_len; + uint16_t extrafield_len; + uint16_t filecomment_len; + uint16_t disk_start; // wat + uint16_t internal_attr; // wat + uint32_t external_attr; // wat + uint32_t local_hdr_off; +} mmz_FileHeader; +#pragma pack(pop) + +#pragma pack(push,1) +typedef struct mmz_EndRecord { + uint32_t sig; + uint16_t disk_number; + uint16_t disk_wcd; + uint16_t disk_entries; + uint16_t total_entries; + uint32_t central_directory_size; + uint32_t file_hdr_off; + uint16_t comment_len; +} mmz_EndRecord; +#pragma pack(pop) + +struct FileEntry { + std::string path; + u16 flags; + u16 compression_type; + u16 modtime; + u16 moddate; + u32 compressed_size; // may be zero. + u32 uncompressed_size; // may be zero. + u32 local_file_header_off; +}; + +struct DirectoryEntry { + std::string path; + std::vector dir_child; + std::vector file_child; +}; + +using FileTableEntries = std::vector; + +struct Device { + std::unique_ptr source; + DirectoryEntry root; +}; + +struct Zfile { + z_stream z; // zlib stream. + Bytef* buffer; // buffer that compressed data is read into. + size_t buffer_size; // size of the above buffer. + size_t compressed_off; // offset of the compressed file. +}; + +struct File { + Device* device; + const FileEntry* entry; + Zfile zfile; // only used if the file is compressed. + size_t data_off; // offset of the file data. + size_t off; +}; + +struct Dir { + Device* device; + const DirectoryEntry* entry; + u32 index; +}; + +const FileEntry* find_file_entry(const DirectoryEntry& dir, std::string_view path) { + if (path.starts_with(dir.path)) { + // todo: check if / comes after file name in order to only check dirs. + for (const auto& e : dir.file_child) { + if (e.path == path) { + return &e; + } + } + + for (const auto& e : dir.dir_child) { + if (auto entry = find_file_entry(e, path)) { + return entry; + } + } + } + + return nullptr; +} + +const DirectoryEntry* find_dir_entry(const DirectoryEntry& dir, std::string_view path) { + if (dir.path == path) { + return &dir; + } + + if (path.starts_with(dir.path)) { + for (const auto& e : dir.dir_child) { + if (auto entry = find_dir_entry(e, path)) { + return entry; + } + } + } + + return nullptr; +} + +void set_stat_file(const FileEntry* entry, struct stat *st) { + std::memset(st, 0, sizeof(*st)); + + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + st->st_size = entry->uncompressed_size; + st->st_nlink = 1; + + struct tm tm{}; + tm.tm_sec = (entry->modtime & 0x1F) << 1; + tm.tm_min = (entry->modtime >> 5) & 0x3F; + tm.tm_hour = (entry->modtime >> 11); + tm.tm_mday = (entry->moddate & 0x1F); + tm.tm_mon = ((entry->moddate >> 5) & 0xF) - 1; + tm.tm_year = (entry->moddate >> 9) + 80; + + st->st_atime = mktime(&tm); + st->st_mtime = st->st_atime; + st->st_ctime = st->st_atime; +} + +int set_errno(struct _reent *r, int err) { + r->_errno = err; + return -1; +} + +int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) { + auto device = (Device*)r->deviceData; + auto file = static_cast(fileStruct); + std::memset(file, 0, sizeof(*file)); + + char path[FS_MAX_PATH]{}; + if (!fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + const auto entry = find_file_entry(device->root, path); + if (!entry) { + return set_errno(r, ENOENT); + } + + if ((entry->flags & mmz_Flag_Encrypted) || (entry->flags & mmz_Flag_StrongEncrypted)) { + log_write("[ZIP] encrypted zip not supported\n"); + return set_errno(r, ENOENT); + } + + if (entry->compression_type != mmz_Compression_None && entry->compression_type != mmz_Compression_Deflate) { + log_write("[ZIP] unsuported compression type: %u\n", entry->compression_type); + return set_errno(r, ENOENT); + } + + mmz_LocalHeader local_hdr{}; + auto offset = entry->local_file_header_off; + if (R_FAILED(device->source->Read(&local_hdr, offset, sizeof(local_hdr)))) { + return set_errno(r, ENOENT); + } + + if (local_hdr.sig != LOCAL_HEADER_SIG) { + return set_errno(r, ENOENT); + } + + offset += sizeof(local_hdr) + local_hdr.filename_len + local_hdr.extrafield_len; + + // todo: does a decs take prio over file header? + if (local_hdr.flags & mmz_Flag_DataDescriptor) { + mmz_DataDescriptor data_desc{}; + if (R_FAILED(device->source->Read(&data_desc, offset, sizeof(data_desc)))) { + return set_errno(r, ENOENT); + } + + if (data_desc.sig != DATA_DESCRIPTOR_SIG) { + return set_errno(r, ENOENT); + } + + offset += sizeof(data_desc); + } + + if (entry->compression_type == mmz_Compression_Deflate) { + auto& zfile = file->zfile; + zfile.buffer_size = 1024 * 64; + zfile.buffer = (Bytef*)calloc(1, zfile.buffer_size); + if (!zfile.buffer) { + return set_errno(r, ENOENT); + } + + // skip zlib header. + if (Z_OK != inflateInit2(&zfile.z, -MAX_WBITS)) { + free(zfile.buffer); + zfile.buffer = nullptr; + return set_errno(r, ENOENT); + } + } + + file->device = device; + file->entry = entry; + file->data_off = offset; + return r->_errno = 0; +} + +int devoptab_close(struct _reent *r, void *fd) { + auto file = static_cast(fd); + + if (file->entry->compression_type == mmz_Compression_Deflate) { + inflateEnd(&file->zfile.z); + + if (file->zfile.buffer) { + free(file->zfile.buffer); + } + } + + std::memset(file, 0, sizeof(*file)); + return r->_errno = 0; +} + +ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { + auto file = static_cast(fd); + len = std::min(len, file->entry->uncompressed_size - file->off); + + if (!len) { + return 0; + } + + if (file->entry->compression_type == mmz_Compression_None) { + if (R_FAILED(file->device->source->Read(ptr, file->data_off + file->off, len))) { + return set_errno(r, ENOENT); + } + } else if (file->entry->compression_type == mmz_Compression_Deflate) { + auto& zfile = file->zfile; + zfile.z.next_out = (Bytef*)ptr; + zfile.z.avail_out = len; + + // run until we have inflated enough data. + while (zfile.z.avail_out) { + // check if we need to fetch more data. + if (!zfile.z.next_in || !zfile.z.avail_in) { + const auto clen = std::min(zfile.buffer_size, file->entry->compressed_size - zfile.compressed_off); + if (R_FAILED(file->device->source->Read(zfile.buffer, file->data_off + zfile.compressed_off, clen))) { + return set_errno(r, ENOENT); + } + + zfile.compressed_off += clen; + zfile.z.next_in = zfile.buffer; + zfile.z.avail_in = clen; + } + + const auto rc = inflate(&zfile.z, Z_SYNC_FLUSH); + if (Z_OK != rc) { + if (Z_STREAM_END == rc) { + len -= zfile.z.avail_out; + } else { + log_write("[ZLIB] failed to inflate: %d %s\n", rc, zfile.z.msg); + return set_errno(r, ENOENT); + } + } + } + } + + file->off += len; + return len; +} + +off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { + auto file = static_cast(fd); + + // seek like normal. + if (file->entry->compression_type == mmz_Compression_None) { + if (dir == SEEK_CUR) { + pos += file->off; + } else if (dir == SEEK_END) { + pos = file->entry->uncompressed_size; + } + } else if (file->entry->compression_type == mmz_Compression_Deflate) { + // limited seek options. + // todo: support seeking to end and then back to orig position. + if (dir == SEEK_SET) { + if (pos == 0) { + inflateReset(&file->zfile.z); + } else if (pos != file->off) { + // seeking to the end is fine as it may be used to calc size. + if (pos != file->entry->uncompressed_size) { + // random access seek is not supported. + pos = file->off; + } + } + } else if (dir == SEEK_CUR) { + // random access seek is not supported. + pos = file->off; + } else if (dir == SEEK_END) { + pos = file->entry->uncompressed_size; + } + } + + r->_errno = 0; + return file->off = std::clamp(pos, 0, file->entry->uncompressed_size); +} + +int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { + auto file = static_cast(fd); + + std::memset(st, 0, sizeof(*st)); + set_stat_file(file->entry, st); + return r->_errno = 0; +} + +DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) { + auto device = (Device*)r->deviceData; + auto dir = static_cast(dirState->dirStruct); + std::memset(dir, 0, sizeof(*dir)); + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + set_errno(r, ENOENT); + return NULL; + } + + const auto entry = find_dir_entry(device->root, path); + if (!entry) { + set_errno(r, ENOENT); + return NULL; + } + + dir->device = device; + dir->entry = entry; + r->_errno = 0; + return dirState; +} + +int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + + dir->index = 0; + + return r->_errno = 0; +} + +int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { + auto dir = static_cast(dirState->dirStruct); + std::memset(filestat, 0, sizeof(*filestat)); + + u32 index = dir->index; + if (index >= dir->entry->dir_child.size()) { + index -= dir->entry->dir_child.size(); + if (index >= dir->entry->file_child.size()) { + return set_errno(r, ENOENT); + } else { + const auto& entry = dir->entry->file_child[index]; + const auto rel_path = entry.path.substr(entry.path.find_last_of('/') + 1); + + set_stat_file(&entry, filestat); + std::strcpy(filename, rel_path.c_str()); + } + + } else { + const auto& entry = dir->entry->dir_child[index]; + const auto rel_path = entry.path.substr(entry.path.find_last_of('/') + 1); + + filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + std::strcpy(filename, rel_path.c_str()); + } + + dir->index++; + return r->_errno = 0; +} + +int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + std::memset(dir, 0, sizeof(*dir)); + + return r->_errno = 0; +} + +int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { + auto device = (Device*)r->deviceData; + + if (!device) { + return set_errno(r, ENOENT); + } + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + std::memset(st, 0, sizeof(*st)); + st->st_nlink = 1; + + if (find_dir_entry(device->root, path)) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + } else if (auto entry = find_file_entry(device->root, path)) { + set_stat_file(entry, st); + } else { + log_write("[ZIP] didn't find in lstat\n"); + return set_errno(r, ENOENT); + } + + return r->_errno = 0; +} + +constexpr devoptab_t DEVOPTAB = { + .structSize = sizeof(File), + .open_r = devoptab_open, + .close_r = devoptab_close, + .read_r = devoptab_read, + .seek_r = devoptab_seek, + .fstat_r = devoptab_fstat, + .stat_r = devoptab_lstat, + .dirStateSize = sizeof(Dir), + .diropen_r = devoptab_diropen, + .dirreset_r = devoptab_dirreset, + .dirnext_r = devoptab_dirnext, + .dirclose_r = devoptab_dirclose, + .lstat_r = devoptab_lstat, +}; + +auto BuildPath(const std::string& path) -> std::string { + if (path.starts_with('/')) { + return path; + } + return "/" + path; +} + +void Parse(const FileTableEntries& entries, u32& index, DirectoryEntry& out) { + for (; index < entries.size(); index++) { + const auto path = BuildPath(entries[index].path); + + // check if this path belongs to this dir. + if (!path.starts_with(out.path)) { + return; + } + + if (path.ends_with('/')) { + auto& new_entry = out.dir_child.emplace_back(); + new_entry.path = path.substr(0, path.length() - 1); + + u32 new_index = index + 1; + Parse(entries, new_index, new_entry); + index = new_index - 1; + } else { + // check if this file actually belongs to this folder. + const auto idx = path.find_first_of('/', out.path.length() + 1); + const auto sub = path.substr(0, idx); + + if (idx != path.npos && out.path != sub) { + auto& new_entry = out.dir_child.emplace_back(); + new_entry.path = sub; + Parse(entries, index, new_entry); + } else { + auto& new_entry = out.file_child.emplace_back(entries[index]); + new_entry.path = path; + } + } + } +} + +void Parse(const FileTableEntries& entries, DirectoryEntry& out) { + u32 index = 0; + out.path = "/"; // add root folder. + Parse(entries, index, out); +} + +Result find_central_dir_offset(BufferedFileData* source, s64 size, mmz_EndRecord* record) { + // check if the record is at the end (no extra header). + auto offset = size - sizeof(*record); + R_TRY(source->Read(record, offset, sizeof(*record))); + + if (record->sig == END_RECORD_SIG) { + R_SUCCEED(); + } + + // failed, find the sig by reading the last 64k and loop across it. + const auto rsize = std::min(UINT16_MAX, size); + offset = size - rsize; + std::vector data(rsize); + R_TRY(source->Read(data.data(), offset, data.size())); + + // check in reverse order as it's more likely at the end. + for (s64 i = data.size() - sizeof(*record); i >= 0; i--) { + u32 sig; + std::memcpy(&sig, data.data() + i, sizeof(sig)); + if (sig == END_RECORD_SIG) { + std::memcpy(record, data.data() + i, sizeof(*record)); + R_SUCCEED(); + } + } + + R_THROW(0x1); +} + +Result ParseZip(BufferedFileData* source, s64 size, FileTableEntries& out) { + mmz_EndRecord end_rec; + R_TRY(find_central_dir_offset(source, size, &end_rec)); + + out.reserve(end_rec.total_entries); + auto file_header_off = end_rec.file_hdr_off; + + for (u16 i = 0; i < end_rec.total_entries; i++) { + // read the file header. + mmz_FileHeader file_hdr{}; + R_TRY(source->Read(&file_hdr, file_header_off, sizeof(file_hdr))); + + if (file_hdr.sig != FILE_HEADER_SIG) { + log_write("[ZIP] invalid file record\n"); + R_THROW(0x1); + } + + // save all the data hat we care about. + auto& new_entry = out.emplace_back(); + new_entry.flags = file_hdr.flags; + new_entry.compression_type = file_hdr.compression; + new_entry.modtime = file_hdr.modtime; + new_entry.moddate = file_hdr.moddate; + new_entry.compressed_size = file_hdr.compressed_size; + new_entry.uncompressed_size = file_hdr.uncompressed_size; + new_entry.local_file_header_off = file_hdr.local_hdr_off; + + // read the file name. + const auto filename_off = file_header_off + sizeof(file_hdr); + new_entry.path.resize(file_hdr.filename_len); + R_TRY(source->Read(new_entry.path.data(), filename_off, new_entry.path.size())); + + // advance the offset. + file_header_off += sizeof(file_hdr) + file_hdr.filename_len + file_hdr.extrafield_len + file_hdr.filecomment_len; + } + + R_SUCCEED(); +} + +struct Entry { + Device device; + devoptab_t devoptab; + fs::FsPath path; + fs::FsPath mount; + char name[32]; + s32 ref_count; +}; + +Mutex g_mutex; +std::vector g_entries; +u32 g_mount_idx; + +} // namespace + +Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { + SCOPED_MUTEX(&g_mutex); + + // check if we already have the save mounted. + for (auto& e : g_entries) { + if (e.path == path) { + e.ref_count++; + out_path = e.mount; + R_SUCCEED(); + } + } + + // create new entry. + auto& entry = g_entries.emplace_back(); + auto source = std::make_unique(fs, path); + + s64 size; + R_TRY(source->GetSize(&size)); + log_write("[ZIP] got size\n"); + + auto buffered = std::make_unique(std::move(source), size); + + FileTableEntries table_entries; + R_TRY(ParseZip(buffered.get(), size, table_entries)); + log_write("[ZIP] parsed zip\n"); + + DirectoryEntry root; + Parse(table_entries, root); + + entry.path = path; + entry.devoptab = DEVOPTAB; + entry.devoptab.name = entry.name; + entry.devoptab.deviceData = &entry.device; + entry.device.source = std::move(buffered); + entry.device.root = root; + std::snprintf(entry.name, sizeof(entry.name), "zip_%u", g_mount_idx); + std::snprintf(entry.mount, sizeof(entry.mount), "zip_%u:/", g_mount_idx); + + R_UNLESS(AddDevice(&entry.devoptab) >= 0, 0x1); + log_write("[ZIP] DEVICE SUCCESS %s %s\n", path.s, entry.name); + + out_path = entry.mount; + entry.ref_count++; + g_mount_idx++; + + R_SUCCEED(); +} + +void UmountZip(const fs::FsPath& mount) { + SCOPED_MUTEX(&g_mutex); + + auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){ + return mount == e.mount; + }); + + if (itr == g_entries.end()) { + return; + } + + if (itr->ref_count) { + itr->ref_count--; + } + + if (!itr->ref_count) { + RemoveDevice(mount); + g_entries.erase(itr); + } +} + +} // namespace sphaira::devoptab diff --git a/sphaira/source/yati/source/file.cpp b/sphaira/source/yati/source/file.cpp index a5e5e29..66fb16e 100644 --- a/sphaira/source/yati/source/file.cpp +++ b/sphaira/source/yati/source/file.cpp @@ -11,4 +11,8 @@ Result File::Read(void* buf, s64 off, s64 size, u64* bytes_read) { return m_file.Read(off, buf, size, 0, bytes_read); } +Result File::GetSize(s64* out) { + return m_file.GetSize(out); +} + } // namespace sphaira::yati::source