diff --git a/src/core/file_sys/archive_nand.cpp b/src/core/file_sys/archive_nand.cpp index 8d203bd19..b7b0fbf01 100644 --- a/src/core/file_sys/archive_nand.cpp +++ b/src/core/file_sys/archive_nand.cpp @@ -3,17 +3,28 @@ // Refer to the license.txt file included. #include +#include #include +#include +#include +#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 "common/string_util.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/layered_fs.h" +#include "core/file_sys/ncch_container.h" #include "core/file_sys/path_parser.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/loader/loader.h" SERIALIZE_EXPORT_IMPL(FileSys::VirtualFile) SERIALIZE_EXPORT_IMPL(FileSys::NANDArchive) @@ -27,19 +38,165 @@ namespace FileSys { namespace { -// Virtual TWL NAND files served from memory; keyed by ASCII path as stored on the TWL partition. -// These files reside on actual TWL NAND hardware and are never written by normal operation, so -// Azahar serves fixed content without creating any host-filesystem files. -struct TwlVirtualEntry { - const char* path; - const char* content; +constexpr u64 SYSTEM_DATA_ARCHIVE_TITLE_ID_HIGH = 0x000400DBULL; +constexpr std::array NVER_OLD_3DS_TITLE_ID_LOWS = { + 0x00016202, 0x00016302, 0x00016102, 0x00016102, 0x00016402, 0x00016502, 0x00016602}; +constexpr std::array NVER_NEW_3DS_TITLE_ID_LOWS = { + 0x20016202, 0x20016302, 0x20016102, 0x20016102, 0, 0x20016502, 0}; +constexpr std::array CVER_TITLE_ID_LOWS = { + 0x00017202, 0x00017302, 0x00017102, 0x00017102, 0x00017402, 0x00017502, 0x00017602}; + +constexpr std::size_t VERSION_BIN_SIZE = 8; +constexpr u32 INVALID_ROMFS_OFFSET = 0xFFFFFFFF; + +struct RomFSDirectoryMetadata { + u32_le parent_directory_offset; + u32_le next_sibling_offset; + u32_le first_child_directory_offset; + u32_le first_file_offset; + u32_le hash_bucket_next; + u32_le name_length; }; -constexpr TwlVirtualEntry TWL_VIRTUAL_FILES[] = { - // 3DSident reads this via Kernel_GetInitalVersion(): extracts between "cup:" and " preInstall:" - // then appends "-" and the value between "nup:" and " cup:". Display format is "cup-nup". - // Azahar has no distinct initial vs current firmware, so both report the current system version. - {"/sys/log/product.log", "nup:11.17.0-50U cup:11.17.0-50U preInstall:\n"}, +static_assert(sizeof(RomFSDirectoryMetadata) == 0x18, + "Size of RomFSDirectoryMetadata is not correct"); + +struct RomFSFileMetadata { + u32_le parent_directory_offset; + u32_le next_sibling_offset; + u64_le file_data_offset; + u64_le file_data_length; + u32_le hash_bucket_next; + u32_le name_length; }; +static_assert(sizeof(RomFSFileMetadata) == 0x20, "Size of RomFSFileMetadata is not correct"); + +template +bool ReadRomFSStruct(RomFSReader& romfs, std::size_t offset, T& out) { + return romfs.ReadFile(offset, sizeof(T), reinterpret_cast(&out)) == sizeof(T); +} + +std::string ReadRomFSName(RomFSReader& romfs, std::size_t offset, u32 name_length) { + std::vector buffer(name_length / sizeof(u16_le)); + if (romfs.ReadFile(offset, name_length, reinterpret_cast(buffer.data())) != name_length) { + return {}; + } + + std::u16string name(buffer.size(), 0); + std::transform(buffer.begin(), buffer.end(), name.begin(), [](u16_le character) { + return static_cast(static_cast(character)); + }); + return Common::UTF16ToUTF8(name); +} + +std::optional> ReadRootRomFSFile(RomFSReader& romfs, + std::string_view target_name) { + RomFSHeader header{}; + if (!ReadRomFSStruct(romfs, 0, header)) { + return std::nullopt; + } + + RomFSDirectoryMetadata root_directory{}; + if (!ReadRomFSStruct(romfs, header.directory_metadata_table.offset, root_directory)) { + return std::nullopt; + } + + u32 file_offset = root_directory.first_file_offset; + while (file_offset != INVALID_ROMFS_OFFSET) { + RomFSFileMetadata file{}; + const auto metadata_offset = header.file_metadata_table.offset + file_offset; + if (!ReadRomFSStruct(romfs, metadata_offset, file)) { + return std::nullopt; + } + + const std::string name = + ReadRomFSName(romfs, metadata_offset + sizeof(file), file.name_length); + if (name == target_name && file.file_data_length == VERSION_BIN_SIZE) { + std::array data{}; + const std::size_t data_offset = header.file_data_offset + file.file_data_offset; + if (romfs.ReadFile(data_offset, data.size(), data.data()) != data.size()) { + return std::nullopt; + } + return data; + } + + file_offset = file.next_sibling_offset; + } + + return std::nullopt; +} + +std::vector GetRegionSearchOrder() { + std::vector order; + const auto configured_region = Settings::values.region_value.GetValue(); + if (configured_region >= 0 && configured_region < static_cast(CVER_TITLE_ID_LOWS.size())) { + order.push_back(static_cast(configured_region)); + } + + for (std::size_t region = 0; region < CVER_TITLE_ID_LOWS.size(); ++region) { + if (std::find(order.begin(), order.end(), region) == order.end()) { + order.push_back(region); + } + } + return order; +} + +std::optional> ReadVersionBin(u64 title_id) { + const std::string content_path = + Service::AM::GetTitleContentPath(Service::FS::MediaType::NAND, title_id); + if (!FileUtil::Exists(content_path)) { + return std::nullopt; + } + + NCCHContainer container(content_path); + std::shared_ptr romfs; + if (container.ReadRomFS(romfs, false) != Loader::ResultStatus::Success || !romfs) { + return std::nullopt; + } + + return ReadRootRomFSFile(*romfs, "version.bin"); +} + +std::optional> FindInstalledVersionBin( + const std::array& title_id_lows) { + for (const std::size_t region : GetRegionSearchOrder()) { + const u32 title_id_low = title_id_lows[region]; + if (title_id_low == 0) { + continue; + } + + const u64 title_id = (SYSTEM_DATA_ARCHIVE_TITLE_ID_HIGH << 32) | title_id_low; + if (auto version = ReadVersionBin(title_id)) { + return version; + } + } + + return std::nullopt; +} + +std::optional> FindInstalledNVer() { + const bool is_new_3ds = Settings::values.is_new_3ds.GetValue(); + if (auto version = + FindInstalledVersionBin(is_new_3ds ? NVER_NEW_3DS_TITLE_ID_LOWS + : NVER_OLD_3DS_TITLE_ID_LOWS)) { + return version; + } + + return FindInstalledVersionBin(is_new_3ds ? NVER_OLD_3DS_TITLE_ID_LOWS + : NVER_NEW_3DS_TITLE_ID_LOWS); +} + +std::optional BuildProductLog() { + const auto cver = FindInstalledVersionBin(CVER_TITLE_ID_LOWS); + const auto nver = FindInstalledNVer(); + if (!cver || !nver) { + return std::nullopt; + } + + const std::string cup = std::to_string((*cver)[2]) + "." + std::to_string((*cver)[1]) + "." + + std::to_string((*cver)[0]); + const std::string nup = std::to_string((*nver)[2]) + static_cast((*nver)[4]); + return "nup:" + nup + " cup:" + cup + " preInstall:\n"; +} } // namespace @@ -53,9 +210,9 @@ ResultVal> NANDArchive::OpenFile(const Path& path, return ResultInvalidOpenFlags; } const std::string path_str = path.AsString(); - for (const auto& entry : TWL_VIRTUAL_FILES) { - if (path_str == entry.path) { - return std::make_unique(entry.content); + if (path_str == "/sys/log/product.log") { + if (auto product_log = BuildProductLog()) { + return std::make_unique(*product_log); } } return ResultNotFound; diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index 78867c8f4..60d77531a 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -651,6 +651,21 @@ void Module::Interface::UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx) rb.Push(cfg->UpdateConfigNANDSavegame()); } +void Module::Interface::ClearParentalControls(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + // Clear the parental restriction email (0x000C0002); PIN and secret answer in + // 0x00100001 are intentionally preserved per hardware behaviour. + static const std::array empty_email{}; + Result res = cfg->SetConfigBlock(static_cast(ParentalRestrictionEmailBlockID), + static_cast(empty_email.size()), + AccessFlag::SystemWrite, empty_email.data()); + if (res.IsSuccess()) { + res = cfg->UpdateConfigNANDSavegame(); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res); +} + void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); [[maybe_unused]] u32 out_size = rp.Pop(); diff --git a/src/core/hle/service/cfg/cfg.h b/src/core/hle/service/cfg/cfg.h index 8b02d6f8e..5c6caf837 100644 --- a/src/core/hle/service/cfg/cfg.h +++ b/src/core/hle/service/cfg/cfg.h @@ -365,6 +365,7 @@ public: * 1 : Result of function, 0 on success, otherwise error code */ void UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx); + void ClearParentalControls(Kernel::HLERequestContext& ctx); /** * CFG::GetLocalFriendCodeSeedData service function diff --git a/src/core/hle/service/cfg/cfg_defaults.cpp b/src/core/hle/service/cfg/cfg_defaults.cpp index dc36bf033..4440e7670 100644 --- a/src/core/hle/service/cfg/cfg_defaults.cpp +++ b/src/core/hle/service/cfg/cfg_defaults.cpp @@ -61,7 +61,6 @@ constexpr u32_le DEFAULT_CLOCK_SEQUENCE = 0; constexpr const char DEFAULT_SERVER_TYPE[4] = {'L', '1', '\0', '\0'}; constexpr u32_le DEFAULT_0x00160000_DATA = 0; constexpr u32_le DEFAULT_MIIVERSE_ACCESS_KEY = 0; -constexpr std::array DEFAULT_TWL_PARENTAL_RESTRICTIONS = {}; static const std::unordered_map DEFAULT_CONFIG_BLOCKS = { {UserTimeOffsetBlockID, @@ -73,9 +72,6 @@ static const std::unordered_map DEFAULT_CONF {BacklightControlNew3dsBlockID, {AccessFlag::System, &DEFAULT_NEW_3DS_BACKLIGHT_CONTROLS, sizeof(DEFAULT_NEW_3DS_BACKLIGHT_CONTROLS)}}, - {TwlParentalRestrictionsBlockID, - {AccessFlag::System, &DEFAULT_TWL_PARENTAL_RESTRICTIONS, - sizeof(DEFAULT_TWL_PARENTAL_RESTRICTIONS)}}, {SoundOutputModeBlockID, {AccessFlag::Global, &DEFAULT_SOUND_OUTPUT_MODE, sizeof(DEFAULT_SOUND_OUTPUT_MODE)}}, {ConsoleUniqueID1BlockID, diff --git a/src/core/hle/service/cfg/cfg_i.cpp b/src/core/hle/service/cfg/cfg_i.cpp index aa7c17c3b..d3ba03e6b 100644 --- a/src/core/hle/service/cfg/cfg_i.cpp +++ b/src/core/hle/service/cfg/cfg_i.cpp @@ -34,6 +34,7 @@ CFG_I::CFG_I(std::shared_ptr cfg) : Module::Interface(std::move(cfg), "c {0x0407, &CFG_I::SecureInfoGetByte101, "SecureInfoGetByte101"}, {0x0408, &CFG_I::SecureInfoGetSerialNo, "SecureInfoGetSerialNo"}, {0x0409, nullptr, "UpdateConfigBlk00040003"}, + {0x040F, &CFG_I::ClearParentalControls, "ClearParentalControls"}, // cfg:i {0x0801, &CFG_I::GetSystemConfig, "GetSystemConfig"}, {0x0802, &CFG_I::SetSystemConfig, "SetSystemConfig"}, diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index 1e2225a24..e5f60dc7b 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -970,6 +971,37 @@ void FS_USER::GetNandArchiveResource(Kernel::HLERequestContext& ctx) { rb.PushRaw(*resource); } +void FS_USER::GetSdmcCid(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 size = rp.Pop(); + auto& output_buffer = rp.PopMappedBuffer(); + + // All-zeroes dummy SD CID. Azahar uses all-zeroes for ID0/ID1 in the SDMC path + // (Nintendo 3DS/000…/000…), so returning a non-zero CID would produce a SHA-256- + // derived ID0 that disagrees with those paths. All-zeroes is the honest answer + // for an emulated device with no real SD card. + static constexpr std::array sdmc_cid = {}; + output_buffer.Write(sdmc_cid.data(), 0, std::min(size, sdmc_cid.size())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(output_buffer); +} + +void FS_USER::GetNandCid(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 size = rp.Pop(); + auto& output_buffer = rp.PopMappedBuffer(); + + // All-zeroes dummy NAND CID, consistent with Azahar's all-zeroes ID0/ID1. + static constexpr std::array nand_cid = {}; + output_buffer.Write(nand_cid.data(), 0, std::min(size, nand_cid.size())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(output_buffer); +} + void FS_USER::CreateExtSaveData(Kernel::HLERequestContext& ctx) { // TODO(Subv): Figure out the other parameters. IPC::RequestParser rp(ctx); @@ -2026,8 +2058,8 @@ FS_USER::FS_USER(Core::System& system) {0x0816, nullptr, "GetSdmcFatfsError"}, {0x0817, &FS_USER::IsSdmcDetected, "IsSdmcDetected"}, {0x0818, &FS_USER::IsSdmcWriteable, "IsSdmcWritable"}, - {0x0819, nullptr, "GetSdmcCid"}, - {0x081A, nullptr, "GetNandCid"}, + {0x0819, &FS_USER::GetSdmcCid, "GetSdmcCid"}, + {0x081A, &FS_USER::GetNandCid, "GetNandCid"}, {0x081B, nullptr, "GetSdmcSpeedInfo"}, {0x081C, nullptr, "GetNandSpeedInfo"}, {0x081D, nullptr, "GetSdmcLog"}, diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index 3ecbe6768..dee13d0e3 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -396,6 +396,28 @@ private: */ void GetNandArchiveResource(Kernel::HLERequestContext& ctx); + /** + * FS_User::GetSdmcCid service function + * Inputs: + * 1 : Output buffer size (must be 0x10) + * 2 : (size << 4) | 0xC (mapped write buffer descriptor) + * 3 : Output buffer pointer + * Outputs: + * 1 : Result code + */ + void GetSdmcCid(Kernel::HLERequestContext& ctx); + + /** + * FS_User::GetNandCid service function + * Inputs: + * 1 : Output buffer size (must be 0x10) + * 2 : (size << 4) | 0xC (mapped write buffer descriptor) + * 3 : Output buffer pointer + * Outputs: + * 1 : Result code + */ + void GetNandCid(Kernel::HLERequestContext& ctx); + /** * FS_User::CreateExtSaveData service function * Inputs: diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 7a3baf26a..7f611058b 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -456,6 +456,18 @@ void Module::Interface::IsShutdownByBatteryEmpty(Kernel::HLERequestContext& ctx) rb.Push(false); } +void Module::Interface::ConfigureNew3DSCPU(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + // Bit 0: 804 MHz clock (0 = 268 MHz, 1 = 804 MHz) + // Bit 1: New 3DS L2 cache (0 = disabled, 1 = enabled) + // Azahar always runs as New 3DS; no actual clock/cache switching is needed. + const u32 config = rp.Pop(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + LOG_DEBUG(Service_PTM, "called, config={:02X}", config); +} + void Module::Interface::GetSystemTime(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); diff --git a/src/core/hle/service/ptm/ptm.h b/src/core/hle/service/ptm/ptm.h index cad29697e..913dc050f 100644 --- a/src/core/hle/service/ptm/ptm.h +++ b/src/core/hle/service/ptm/ptm.h @@ -168,6 +168,17 @@ public: void IsShutdownByBatteryEmpty(Kernel::HLERequestContext& ctx); + /** + * PTM::ConfigureNew3DSCPU service function + * Inputs: + * 1 : u32 config bitfield + * bit 0 = clock rate (0 = 268 MHz, 1 = 804 MHz) + * bit 1 = L2 cache (0 = disabled, 1 = enabled) + * Outputs: + * 1 : Result code + */ + void ConfigureNew3DSCPU(Kernel::HLERequestContext& ctx); + /** * PTM::GetSystemTime service function * Outputs: diff --git a/src/core/hle/service/ptm/ptm_sysm.cpp b/src/core/hle/service/ptm/ptm_sysm.cpp index af53345da..10f0823b9 100644 --- a/src/core/hle/service/ptm/ptm_sysm.cpp +++ b/src/core/hle/service/ptm/ptm_sysm.cpp @@ -61,7 +61,7 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr ptm, const char* name) {0x0812, &PTM_S_Common::IsShutdownByBatteryEmpty, "IsShutdownByBatteryEmpty"}, {0x0813, nullptr, "FormatSavedata"}, {0x0814, nullptr, "GetLegacyJumpProhibitedFlag"}, - {0x0818, nullptr, "ConfigureNew3DSCPU"}, + {0x0818, &PTM_S_Common::ConfigureNew3DSCPU, "ConfigureNew3DSCPU"}, // clang-format on }; RegisterHandlers(functions);