From e92272ce31da06c9accc705d76e1c433c8d4286d Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Wed, 11 Mar 2026 15:06:28 +0100 Subject: [PATCH] core: fs: Implement NAND archives (#1861) --- src/core/CMakeLists.txt | 2 + src/core/file_sys/archive_backend.h | 14 +- src/core/file_sys/archive_nand.cpp | 419 ++++++++++++++++++++++++++++ src/core/file_sys/archive_nand.h | 115 ++++++++ src/core/file_sys/archive_sdmc.cpp | 4 +- src/core/hle/service/fs/archive.cpp | 23 ++ src/core/hle/service/fs/archive.h | 3 + 7 files changed, 577 insertions(+), 3 deletions(-) create mode 100644 src/core/file_sys/archive_nand.cpp create mode 100644 src/core/file_sys/archive_nand.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a5abbc585..fd8212dcf 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -46,6 +46,8 @@ add_library(citra_core STATIC file_sys/archive_backend.h file_sys/archive_extsavedata.cpp file_sys/archive_extsavedata.h + file_sys/archive_nand.cpp + file_sys/archive_nand.h file_sys/archive_ncch.cpp file_sys/archive_ncch.h file_sys/archive_other_savedata.cpp diff --git a/src/core/file_sys/archive_backend.h b/src/core/file_sys/archive_backend.h index 32981ba8e..deed31cd1 100644 --- a/src/core/file_sys/archive_backend.h +++ b/src/core/file_sys/archive_backend.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -35,6 +35,18 @@ union Mode { BitField<0, 1, u32> read_flag; BitField<1, 1, u32> write_flag; BitField<2, 1, u32> create_flag; + + bool operator==(const Mode& other) const { + return hex == other.hex; + } + + bool operator!=(const Mode& other) const { + return !(*this == other); + } + + static constexpr Mode ReadOnly() { + return Mode{.hex = 1}; + } }; class Path { diff --git a/src/core/file_sys/archive_nand.cpp b/src/core/file_sys/archive_nand.cpp new file mode 100644 index 000000000..a77e4905c --- /dev/null +++ b/src/core/file_sys/archive_nand.cpp @@ -0,0 +1,419 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/archives.h" +#include "common/common_paths.h" +#include "common/error.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/file_sys/archive_nand.h" +#include "core/file_sys/disk_archive.h" +#include "core/file_sys/errors.h" +#include "core/file_sys/path_parser.h" + +SERIALIZE_EXPORT_IMPL(FileSys::NANDArchive) +SERIALIZE_EXPORT_IMPL(FileSys::ArchiveFactory_NAND) + +namespace FileSys { + +// TODO(PabloMK7): This code is very similar to the SMDC archive code. Maybe we should look +// into unifying everything in a FAT-like archive, as both the SMDC and NAND archives +// seem to behave the same way. + +ResultVal> NANDArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { + LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex); + + if (!AllowsWrite() && mode != Mode::ReadOnly()) { + return ResultInvalidOpenFlags; + } + + const PathParser path_parser(path); + + if (!path_parser.IsValid()) { + LOG_ERROR(Service_FS, "Invalid path {}", path.DebugStr()); + return ResultInvalidPath; + } + + if (mode.hex == 0) { + LOG_ERROR(Service_FS, "Empty open mode"); + return ResultInvalidOpenFlags; + } + + if (mode.create_flag && !mode.write_flag) { + LOG_ERROR(Service_FS, "Create flag set but write flag not set"); + return ResultInvalidOpenFlags; + } + + const auto full_path = path_parser.BuildHostPath(mount_point); + + switch (path_parser.GetHostStatus(mount_point)) { + case PathParser::InvalidMountPoint: + LOG_CRITICAL(Service_FS, "(unreachable) Invalid mount point {}", mount_point); + return ResultNotFound; + case PathParser::PathNotFound: + case PathParser::FileInPath: + LOG_DEBUG(Service_FS, "Path not found {}", full_path); + return ResultNotFound; + case PathParser::DirectoryFound: + LOG_DEBUG(Service_FS, "{} is not a file", full_path); + return ResultUnexpectedFileOrDirectorySdmc; + case PathParser::NotFound: + if (!mode.create_flag) { + LOG_DEBUG(Service_FS, "Non-existing file {} can't be open without mode create.", + full_path); + return ResultNotFound; + } else { + // Create the file + FileUtil::CreateEmptyFile(full_path); + } + break; + case PathParser::FileFound: + break; // Expected 'success' case + } + + FileUtil::IOFile file(full_path, mode.write_flag ? "r+b" : "rb"); + if (!file.IsOpen()) { + LOG_CRITICAL(Service_FS, "Error opening {}: {}", full_path, Common::GetLastErrorMsg()); + return ResultNotFound; + } + + return std::make_unique(std::move(file), mode, nullptr); +} + +Result NANDArchive::DeleteFile(const Path& path) const { + + if (!AllowsWrite()) { + return ResultInvalidOpenFlags; + } + + const PathParser path_parser(path); + + if (!path_parser.IsValid()) { + LOG_ERROR(Service_FS, "Invalid path {}", path.DebugStr()); + return ResultInvalidPath; + } + + const auto full_path = path_parser.BuildHostPath(mount_point); + + switch (path_parser.GetHostStatus(mount_point)) { + case PathParser::InvalidMountPoint: + LOG_CRITICAL(Service_FS, "(unreachable) Invalid mount point {}", mount_point); + return ResultNotFound; + case PathParser::PathNotFound: + case PathParser::FileInPath: + case PathParser::NotFound: + LOG_DEBUG(Service_FS, "{} not found", full_path); + return ResultNotFound; + case PathParser::DirectoryFound: + LOG_ERROR(Service_FS, "{} is not a file", full_path); + return ResultUnexpectedFileOrDirectorySdmc; + case PathParser::FileFound: + break; // Expected 'success' case + } + + if (FileUtil::Delete(full_path)) { + return ResultSuccess; + } + + LOG_CRITICAL(Service_FS, "(unreachable) Unknown error deleting {}", full_path); + return ResultNotFound; +} + +Result NANDArchive::RenameFile(const Path& src_path, const Path& dest_path) const { + + if (!AllowsWrite()) { + return ResultInvalidOpenFlags; + } + + const PathParser path_parser_src(src_path); + + // TODO: Verify these return codes with HW + if (!path_parser_src.IsValid()) { + LOG_ERROR(Service_FS, "Invalid src path {}", src_path.DebugStr()); + return ResultInvalidPath; + } + + const PathParser path_parser_dest(dest_path); + + if (!path_parser_dest.IsValid()) { + LOG_ERROR(Service_FS, "Invalid dest path {}", dest_path.DebugStr()); + return ResultInvalidPath; + } + + const auto src_path_full = path_parser_src.BuildHostPath(mount_point); + const auto dest_path_full = path_parser_dest.BuildHostPath(mount_point); + + if (FileUtil::Rename(src_path_full, dest_path_full)) { + return ResultSuccess; + } + + // TODO(yuriks): This code probably isn't right, it'll return a Status even if the file didn't + // exist or similar. Verify. + return Result(ErrorDescription::NoData, ErrorModule::FS, // TODO: verify description + ErrorSummary::NothingHappened, ErrorLevel::Status); +} + +template +static Result DeleteDirectoryHelper(const Path& path, const std::string& mount_point, T deleter) { + const PathParser path_parser(path); + + if (!path_parser.IsValid()) { + LOG_ERROR(Service_FS, "Invalid path {}", path.DebugStr()); + return ResultInvalidPath; + } + + if (path_parser.IsRootDirectory()) + return ResultInvalidOpenFlags; + + const auto full_path = path_parser.BuildHostPath(mount_point); + + switch (path_parser.GetHostStatus(mount_point)) { + case PathParser::InvalidMountPoint: + LOG_CRITICAL(Service_FS, "(unreachable) Invalid mount point {}", mount_point); + return ResultNotFound; + case PathParser::PathNotFound: + case PathParser::NotFound: + LOG_ERROR(Service_FS, "Path not found {}", full_path); + return ResultNotFound; + case PathParser::FileInPath: + case PathParser::FileFound: + LOG_ERROR(Service_FS, "Unexpected file in path {}", full_path); + return ResultUnexpectedFileOrDirectorySdmc; + case PathParser::DirectoryFound: + break; // Expected 'success' case + } + + if (deleter(full_path)) { + return ResultSuccess; + } + + LOG_ERROR(Service_FS, "Directory not empty {}", full_path); + return ResultUnexpectedFileOrDirectorySdmc; +} + +Result NANDArchive::DeleteDirectory(const Path& path) const { + if (!AllowsWrite()) { + return ResultInvalidOpenFlags; + } + + return DeleteDirectoryHelper(path, mount_point, FileUtil::DeleteDir); +} + +Result NANDArchive::DeleteDirectoryRecursively(const Path& path) const { + if (!AllowsWrite()) { + return ResultInvalidOpenFlags; + } + + return DeleteDirectoryHelper( + path, mount_point, [](const std::string& p) { return FileUtil::DeleteDirRecursively(p); }); +} + +Result NANDArchive::CreateFile(const FileSys::Path& path, u64 size, u32 attributes) const { + if (!AllowsWrite()) { + return ResultInvalidOpenFlags; + } + + const PathParser path_parser(path); + + if (!path_parser.IsValid()) { + LOG_ERROR(Service_FS, "Invalid path {}", path.DebugStr()); + return ResultInvalidPath; + } + + const auto full_path = path_parser.BuildHostPath(mount_point); + + switch (path_parser.GetHostStatus(mount_point)) { + case PathParser::InvalidMountPoint: + LOG_CRITICAL(Service_FS, "(unreachable) Invalid mount point {}", mount_point); + return ResultNotFound; + case PathParser::PathNotFound: + case PathParser::FileInPath: + LOG_ERROR(Service_FS, "Path not found {}", full_path); + return ResultNotFound; + case PathParser::DirectoryFound: + LOG_ERROR(Service_FS, "{} already exists", full_path); + return ResultUnexpectedFileOrDirectorySdmc; + case PathParser::FileFound: + LOG_ERROR(Service_FS, "{} already exists", full_path); + return ResultAlreadyExists; + case PathParser::NotFound: + break; // Expected 'success' case + } + + if (size == 0) { + FileUtil::CreateEmptyFile(full_path); + return ResultSuccess; + } + + FileUtil::IOFile file(full_path, "wb"); + // Creates a sparse file (or a normal file on filesystems without the concept of sparse files) + // We do this by seeking to the right size, then writing a single null byte. + if (file.Seek(size - 1, SEEK_SET) && file.WriteBytes("", 1) == 1) { + return ResultSuccess; + } + + LOG_ERROR(Service_FS, "Too large file"); + return Result(ErrorDescription::TooLarge, ErrorModule::FS, ErrorSummary::OutOfResource, + ErrorLevel::Info); +} + +Result NANDArchive::CreateDirectory(const Path& path, u32 attributes) const { + if (!AllowsWrite()) { + return ResultInvalidOpenFlags; + } + + const PathParser path_parser(path); + + if (!path_parser.IsValid()) { + LOG_ERROR(Service_FS, "Invalid path {}", path.DebugStr()); + return ResultInvalidPath; + } + + const auto full_path = path_parser.BuildHostPath(mount_point); + + switch (path_parser.GetHostStatus(mount_point)) { + case PathParser::InvalidMountPoint: + LOG_CRITICAL(Service_FS, "(unreachable) Invalid mount point {}", mount_point); + return ResultNotFound; + case PathParser::PathNotFound: + case PathParser::FileInPath: + LOG_ERROR(Service_FS, "Path not found {}", full_path); + return ResultNotFound; + case PathParser::DirectoryFound: + case PathParser::FileFound: + LOG_DEBUG(Service_FS, "{} already exists", full_path); + return ResultAlreadyExists; + case PathParser::NotFound: + break; // Expected 'success' case + } + + if (FileUtil::CreateDir(mount_point + path.AsString())) { + return ResultSuccess; + } + + LOG_CRITICAL(Service_FS, "(unreachable) Unknown error creating {}", mount_point); + return Result(ErrorDescription::NoData, ErrorModule::FS, ErrorSummary::Canceled, + ErrorLevel::Status); +} + +Result NANDArchive::RenameDirectory(const Path& src_path, const Path& dest_path) const { + if (!AllowsWrite()) { + return ResultInvalidOpenFlags; + } + + const PathParser path_parser_src(src_path); + + // TODO: Verify these return codes with HW + if (!path_parser_src.IsValid()) { + LOG_ERROR(Service_FS, "Invalid src path {}", src_path.DebugStr()); + return ResultInvalidPath; + } + + const PathParser path_parser_dest(dest_path); + + if (!path_parser_dest.IsValid()) { + LOG_ERROR(Service_FS, "Invalid dest path {}", dest_path.DebugStr()); + return ResultInvalidPath; + } + + const auto src_path_full = path_parser_src.BuildHostPath(mount_point); + const auto dest_path_full = path_parser_dest.BuildHostPath(mount_point); + + if (FileUtil::Rename(src_path_full, dest_path_full)) { + return ResultSuccess; + } + + // TODO(yuriks): This code probably isn't right, it'll return a Status even if the file didn't + // exist or similar. Verify. + return Result(ErrorDescription::NoData, ErrorModule::FS, // TODO: verify description + ErrorSummary::NothingHappened, ErrorLevel::Status); +} + +ResultVal> NANDArchive::OpenDirectory(const Path& path) { + const PathParser path_parser(path); + + if (!path_parser.IsValid()) { + LOG_ERROR(Service_FS, "Invalid path {}", path.DebugStr()); + return ResultInvalidPath; + } + + const auto full_path = path_parser.BuildHostPath(mount_point); + + switch (path_parser.GetHostStatus(mount_point)) { + case PathParser::InvalidMountPoint: + LOG_CRITICAL(Service_FS, "(unreachable) Invalid mount point {}", mount_point); + return ResultNotFound; + case PathParser::PathNotFound: + case PathParser::NotFound: + case PathParser::FileFound: + LOG_DEBUG(Service_FS, "{} not found", full_path); + return ResultNotFound; + case PathParser::FileInPath: + LOG_DEBUG(Service_FS, "Unexpected file in path {}", full_path); + return ResultUnexpectedFileOrDirectorySdmc; + case PathParser::DirectoryFound: + break; // Expected 'success' case + } + + return std::make_unique(full_path); +} + +u64 NANDArchive::GetFreeBytes() const { + // TODO: Stubbed to return 1GiB + return 1024 * 1024 * 1024; +} + +ArchiveFactory_NAND::ArchiveFactory_NAND(const std::string& nand_directory, NANDArchiveType type) + : nand_directory(nand_directory), archive_type(type) { + + LOG_DEBUG(Service_FS, "Directory {} set as NAND.", nand_directory); +} + +bool ArchiveFactory_NAND::Initialize() { + if (!FileUtil::CreateFullPath(GetPath())) { + LOG_ERROR(Service_FS, "Unable to create NAND path."); + return false; + } + + return true; +} + +std::string ArchiveFactory_NAND::GetPath() { + switch (archive_type) { + case NANDArchiveType::RW: + return PathParser("/rw").BuildHostPath(nand_directory) + DIR_SEP; + case NANDArchiveType::RO: + case NANDArchiveType::RO_W: + return PathParser("/ro").BuildHostPath(nand_directory) + DIR_SEP; + default: + break; + } + + UNREACHABLE(); + return ""; +} + +ResultVal> ArchiveFactory_NAND::Open(const Path& path, + u64 program_id) { + return std::make_unique(GetPath(), archive_type); +} + +Result ArchiveFactory_NAND::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, + u64 program_id, u32 directory_buckets, u32 file_buckets) { + // TODO(PabloMK7): Find proper error code + LOG_ERROR(Service_FS, "Unimplemented Format archive {}", GetName()); + return UnimplementedFunction(ErrorModule::FS); +} + +ResultVal ArchiveFactory_NAND::GetFormatInfo(const Path& path, + u64 program_id) const { + // TODO(PabloMK7): Implement + LOG_ERROR(Service_FS, "Unimplemented GetFormatInfo archive {}", GetName()); + return UnimplementedFunction(ErrorModule::FS); +} +} // namespace FileSys diff --git a/src/core/file_sys/archive_nand.h b/src/core/file_sys/archive_nand.h new file mode 100644 index 000000000..b354a72cb --- /dev/null +++ b/src/core/file_sys/archive_nand.h @@ -0,0 +1,115 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "core/file_sys/archive_backend.h" +#include "core/hle/result.h" + +namespace FileSys { + +enum class NANDArchiveType : u32 { + RW, ///< Access to Read Write (rw) directory + RO, ///< Access to Read Only (ro) directory + RO_W, ///< Access to Read Only (ro) directory with write permissions +}; + +/// Archive backend for SDMC archive +class NANDArchive : public ArchiveBackend { +public: + explicit NANDArchive(const std::string& mount_point_, NANDArchiveType archive_type) + : mount_point(mount_point_) {} + + std::string GetName() const override { + return "NANDArchive: " + mount_point; + } + + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; + Result DeleteFile(const Path& path) const override; + Result RenameFile(const Path& src_path, const Path& dest_path) const override; + Result DeleteDirectory(const Path& path) const override; + Result DeleteDirectoryRecursively(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; + Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; + ResultVal> OpenDirectory(const Path& path) override; + u64 GetFreeBytes() const override; + +protected: + std::string mount_point{}; + NANDArchiveType archive_type{}; + + NANDArchive() = default; + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar & mount_point; + ar & archive_type; + } + friend class boost::serialization::access; + +private: + bool AllowsWrite() const { + return archive_type != NANDArchiveType::RO; + } +}; + +/// File system interface to the NAND archive +class ArchiveFactory_NAND final : public ArchiveFactory { +public: + explicit ArchiveFactory_NAND(const std::string& mount_point, NANDArchiveType type); + + /** + * Initialize the archive. + * @return true if it initialized successfully + */ + bool Initialize(); + + std::string GetPath(); + + std::string GetName() const override { + switch (archive_type) { + case NANDArchiveType::RW: + return "NAND RW"; + case NANDArchiveType::RO: + return "NAND RO"; + case NANDArchiveType::RO_W: + return "NAND RO W"; + default: + break; + } + + UNIMPLEMENTED(); + return ""; + } + + ResultVal> Open(const Path& path, u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; + ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + +private: + std::string nand_directory; + NANDArchiveType archive_type; + + ArchiveFactory_NAND() = default; + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar & nand_directory; + ar & archive_type; + } + friend class boost::serialization::access; +}; + +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::NANDArchive) +BOOST_CLASS_EXPORT_KEY(FileSys::ArchiveFactory_NAND) diff --git a/src/core/file_sys/archive_sdmc.cpp b/src/core/file_sys/archive_sdmc.cpp index 91b1fa680..6d036529d 100644 --- a/src/core/file_sys/archive_sdmc.cpp +++ b/src/core/file_sys/archive_sdmc.cpp @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -185,7 +185,7 @@ static Result DeleteDirectoryHelper(const Path& path, const std::string& mount_p } if (path_parser.IsRootDirectory()) - return ResultNotFound; + return ResultInvalidOpenFlags; const auto full_path = path_parser.BuildHostPath(mount_point); diff --git a/src/core/hle/service/fs/archive.cpp b/src/core/hle/service/fs/archive.cpp index 131cb58c0..211bf39d6 100644 --- a/src/core/hle/service/fs/archive.cpp +++ b/src/core/hle/service/fs/archive.cpp @@ -15,6 +15,7 @@ #include "core/core.h" #include "core/file_sys/archive_backend.h" #include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_nand.h" #include "core/file_sys/archive_ncch.h" #include "core/file_sys/archive_other_savedata.h" #include "core/file_sys/archive_savedata.h" @@ -393,6 +394,28 @@ void ArchiveManager::RegisterArchiveTypes() { sdmc_directory, FileSys::ExtSaveDataType::Boss); RegisterArchiveType(std::move(bossextsavedata_factory), ArchiveIdCode::BossExtSaveData); + auto nand_rw = std::make_unique(nand_directory, + FileSys::NANDArchiveType::RW); + if (nand_rw->Initialize()) + RegisterArchiveType(std::move(nand_rw), ArchiveIdCode::NANDRW); + else + LOG_ERROR(Service_FS, "Can't instantiate NAND RW archive with path {}", nand_rw->GetPath()); + + auto nand_ro = std::make_unique(nand_directory, + FileSys::NANDArchiveType::RO); + if (nand_ro->Initialize()) + RegisterArchiveType(std::move(nand_ro), ArchiveIdCode::NANDRO); + else + LOG_ERROR(Service_FS, "Can't instantiate NAND RO archive with path {}", nand_ro->GetPath()); + + auto nand_ro_w = std::make_unique(nand_directory, + FileSys::NANDArchiveType::RO_W); + if (nand_ro_w->Initialize()) + RegisterArchiveType(std::move(nand_ro_w), ArchiveIdCode::NANDROW); + else + LOG_ERROR(Service_FS, "Can't instantiate NAND RO_W archive with path {}", + nand_ro_w->GetPath()); + // Create the NCCH archive, basically a small variation of the RomFS archive auto savedatacheck_factory = std::make_unique(); RegisterArchiveType(std::move(savedatacheck_factory), ArchiveIdCode::NCCH); diff --git a/src/core/hle/service/fs/archive.h b/src/core/hle/service/fs/archive.h index 2e017f3e0..2f31acbea 100644 --- a/src/core/hle/service/fs/archive.h +++ b/src/core/hle/service/fs/archive.h @@ -43,6 +43,9 @@ enum class ArchiveIdCode : u32 { SDMC = 0x00000009, SDMCWriteOnly = 0x0000000A, BossExtSaveData = 0x12345678, + NANDRW = 0x1234567D, + NANDRO = 0x1234567E, + NANDROW = 0x1234567F, NCCH = 0x2345678A, OtherSaveDataGeneral = 0x567890B2, OtherSaveDataPermitted = 0x567890B4,