mirror of
https://github.com/azahar-emu/azahar.git
synced 2026-06-06 02:33:44 -04:00
hle: implement remaining 3DSident-facing service commands
Implement several previously-stubbed service commands and refine the system version reporting from the prior hardware-info work: - ptm:sysm ConfigureNew3DSCPU (0x0818): accept the clock/L2 config bitfield. Azahar always runs as New 3DS, so no switching is needed. - fs:USER GetSdmcCid (0x0819) / GetNandCid (0x081A): return all-zero CIDs, consistent with the all-zero ID0/ID1 used in the SDMC path. - cfg:i ClearParentalControls (0x040F): clear the parental restriction email block (0x000C0002) and persist. The PIN and secret answer in 0x00100001 are intentionally preserved, matching hardware behaviour. Synthesize the virtual TWL /sys/log/product.log dynamically from the installed CVer/NVer titles' RomFS version.bin instead of a hardcoded string, so the reported system version reflects the actual installed firmware. If those titles are absent, the file is reported as not found rather than returning misleading data. Drop the dummy all-zero TwlParentalRestrictions config block default, which caused a fake parental PIN to be reported when the block was never set up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
db11399785
commit
255bfd7be6
10 changed files with 268 additions and 21 deletions
|
|
@ -3,17 +3,28 @@
|
|||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#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<u32, 7> NVER_OLD_3DS_TITLE_ID_LOWS = {
|
||||
0x00016202, 0x00016302, 0x00016102, 0x00016102, 0x00016402, 0x00016502, 0x00016602};
|
||||
constexpr std::array<u32, 7> NVER_NEW_3DS_TITLE_ID_LOWS = {
|
||||
0x20016202, 0x20016302, 0x20016102, 0x20016102, 0, 0x20016502, 0};
|
||||
constexpr std::array<u32, 7> 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 <typename T>
|
||||
bool ReadRomFSStruct(RomFSReader& romfs, std::size_t offset, T& out) {
|
||||
return romfs.ReadFile(offset, sizeof(T), reinterpret_cast<u8*>(&out)) == sizeof(T);
|
||||
}
|
||||
|
||||
std::string ReadRomFSName(RomFSReader& romfs, std::size_t offset, u32 name_length) {
|
||||
std::vector<u16_le> buffer(name_length / sizeof(u16_le));
|
||||
if (romfs.ReadFile(offset, name_length, reinterpret_cast<u8*>(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<char16_t>(static_cast<u16>(character));
|
||||
});
|
||||
return Common::UTF16ToUTF8(name);
|
||||
}
|
||||
|
||||
std::optional<std::array<u8, VERSION_BIN_SIZE>> 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<u8, VERSION_BIN_SIZE> 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<std::size_t> GetRegionSearchOrder() {
|
||||
std::vector<std::size_t> order;
|
||||
const auto configured_region = Settings::values.region_value.GetValue();
|
||||
if (configured_region >= 0 && configured_region < static_cast<int>(CVER_TITLE_ID_LOWS.size())) {
|
||||
order.push_back(static_cast<std::size_t>(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<std::array<u8, VERSION_BIN_SIZE>> 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<RomFSReader> romfs;
|
||||
if (container.ReadRomFS(romfs, false) != Loader::ResultStatus::Success || !romfs) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return ReadRootRomFSFile(*romfs, "version.bin");
|
||||
}
|
||||
|
||||
std::optional<std::array<u8, VERSION_BIN_SIZE>> FindInstalledVersionBin(
|
||||
const std::array<u32, 7>& 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<std::array<u8, VERSION_BIN_SIZE>> 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<std::string> 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<char>((*nver)[4]);
|
||||
return "nup:" + nup + " cup:" + cup + " preInstall:\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
|
@ -53,9 +210,9 @@ ResultVal<std::unique_ptr<FileBackend>> 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<VirtualFile>(entry.content);
|
||||
if (path_str == "/sys/log/product.log") {
|
||||
if (auto product_log = BuildProductLog()) {
|
||||
return std::make_unique<VirtualFile>(*product_log);
|
||||
}
|
||||
}
|
||||
return ResultNotFound;
|
||||
|
|
|
|||
|
|
@ -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<u8, 0x200> empty_email{};
|
||||
Result res = cfg->SetConfigBlock(static_cast<u32>(ParentalRestrictionEmailBlockID),
|
||||
static_cast<u32>(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<u32>();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<u8, 0x94> DEFAULT_TWL_PARENTAL_RESTRICTIONS = {};
|
||||
|
||||
static const std::unordered_map<ConfigBlockID, ConfigBlockDefaults> DEFAULT_CONFIG_BLOCKS = {
|
||||
{UserTimeOffsetBlockID,
|
||||
|
|
@ -73,9 +72,6 @@ static const std::unordered_map<ConfigBlockID, ConfigBlockDefaults> 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,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ CFG_I::CFG_I(std::shared_ptr<Module> 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"},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cryptopp/aes.h>
|
||||
#include <cryptopp/cmac.h>
|
||||
#include <cryptopp/modes.h>
|
||||
|
|
@ -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<u32>();
|
||||
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<u8, 0x10> sdmc_cid = {};
|
||||
output_buffer.Write(sdmc_cid.data(), 0, std::min<std::size_t>(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<u32>();
|
||||
auto& output_buffer = rp.PopMappedBuffer();
|
||||
|
||||
// All-zeroes dummy NAND CID, consistent with Azahar's all-zeroes ID0/ID1.
|
||||
static constexpr std::array<u8, 0x10> nand_cid = {};
|
||||
output_buffer.Write(nand_cid.data(), 0, std::min<std::size_t>(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"},
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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<u32>();
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr<Module> 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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue