mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 01:23:15 -04:00
Merge commit '28cbea46ab44105fde928653507add5d9d4873f4' into fix/pr168-save-safety
This commit is contained in:
@@ -277,6 +277,13 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
|
||||
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
|
||||
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||
|
||||
# The input expression engine is self-contained, so it can be exercised without
|
||||
# linking the runtime or SDL.
|
||||
add_executable(mkw_input_expr_tests
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "nand_path.h"
|
||||
#include "nand_settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace RuntimeConsoleIdentity {
|
||||
|
||||
struct Identity {
|
||||
std::string serial;
|
||||
std::string productCode;
|
||||
std::string area;
|
||||
std::string gameRegion;
|
||||
std::array<uint8_t, 6> mac;
|
||||
};
|
||||
|
||||
inline bool IsValidSerial(const std::string& serial) {
|
||||
return serial.size() == 9 &&
|
||||
serial != "000000000" &&
|
||||
std::all_of(serial.begin(), serial.end(),
|
||||
[](unsigned char value) { return std::isdigit(value) != 0; });
|
||||
}
|
||||
|
||||
inline Identity FromSerial(std::string serial) {
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
|
||||
// so every API exposes one coherent, stable virtual-console identity.
|
||||
uint32_t hash = 2166136261u;
|
||||
for (const unsigned char value : serial) {
|
||||
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
|
||||
|
||||
return {
|
||||
std::move(serial),
|
||||
{}, {}, {},
|
||||
{
|
||||
0x00,
|
||||
0x09,
|
||||
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
|
||||
};
|
||||
}
|
||||
|
||||
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
|
||||
std::ifstream input(path);
|
||||
std::string line;
|
||||
if (!input || !std::getline(input, line)) {
|
||||
return std::nullopt;
|
||||
inline Identity LoadFromNand() {
|
||||
const auto root = RuntimeNandPath::DiscoverNandRootPath();
|
||||
const auto settings = RuntimeNandSettings::Read(root);
|
||||
if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
|
||||
RuntimeNandPath::FailNandRoot(
|
||||
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
|
||||
root / "title/00000001/00000002/data/setting.txt");
|
||||
}
|
||||
constexpr std::string_view prefix = "serial=";
|
||||
if (line.rfind(prefix, 0) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string serial = line.substr(prefix.size());
|
||||
if (!IsValidSerial(serial)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path temporary = path;
|
||||
temporary += ".tmp";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
output << "serial=" << serial << '\n';
|
||||
output.close();
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::rename(temporary, path, ec);
|
||||
if (!ec) {
|
||||
return true;
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline std::string GenerateSerial() {
|
||||
std::random_device entropy;
|
||||
std::seed_seq seed{
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
};
|
||||
std::mt19937 generator(seed);
|
||||
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
|
||||
return std::to_string(distribution(generator));
|
||||
}
|
||||
|
||||
inline Identity LoadOrCreate(const std::filesystem::path& path) {
|
||||
if (const auto serial = ReadSerial(path)) {
|
||||
return FromSerial(*serial);
|
||||
}
|
||||
|
||||
const std::string generated = GenerateSerial();
|
||||
if (WriteSerial(path, generated)) {
|
||||
return FromSerial(generated);
|
||||
}
|
||||
|
||||
// Remain operational in a read-only environment. This fallback matches
|
||||
// Dolphin's deterministic serial while keeping the same valid identity shape.
|
||||
return FromSerial("123456789");
|
||||
Identity identity = FromSerial(settings->at("SERNO"));
|
||||
identity.productCode = settings->at("CODE");
|
||||
identity.area = settings->at("AREA");
|
||||
identity.gameRegion = settings->at("GAME");
|
||||
return identity;
|
||||
}
|
||||
|
||||
inline const Identity& Current() {
|
||||
static const Identity identity =
|
||||
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
|
||||
static const Identity identity = LoadFromNand();
|
||||
return identity;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "nand_settings.h"
|
||||
#include "runtime_log.h"
|
||||
#include "system_bridge.h"
|
||||
|
||||
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
inline std::filesystem::path DiscoverNandRootPath() {
|
||||
inline std::filesystem::path ResolveNandRootPath() {
|
||||
const std::string configPath = RuntimeConfigFile::NandRoot();
|
||||
if (!configPath.empty()) {
|
||||
const auto path = ResolveConfiguredPath(configPath);
|
||||
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
|
||||
return CreateManagedNandRoot();
|
||||
}
|
||||
|
||||
inline std::filesystem::path DiscoverNandRootPath() {
|
||||
static const auto root = [] {
|
||||
const auto resolved = ResolveNandRootPath();
|
||||
std::string error;
|
||||
if (!RuntimeNandSettings::Ensure(resolved, error)) {
|
||||
FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved));
|
||||
}
|
||||
return resolved;
|
||||
}();
|
||||
return root;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandPath
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace RuntimeNandSettings {
|
||||
|
||||
using Settings = std::map<std::string, std::string>;
|
||||
|
||||
inline std::filesystem::path FilePath(const std::filesystem::path& root) {
|
||||
return root / "title/00000001/00000002/data/setting.txt";
|
||||
}
|
||||
|
||||
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
|
||||
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
|
||||
std::ifstream input(FilePath(nandRoot), std::ios::binary);
|
||||
std::array<uint8_t, 256> bytes{};
|
||||
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
uint32_t key = 0x73B5DBFAu;
|
||||
std::string decoded;
|
||||
for (const uint8_t byte : bytes) {
|
||||
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
|
||||
key = (key << 1) | (key >> 31);
|
||||
if (value == '\0') {
|
||||
break;
|
||||
}
|
||||
if (value != '\r') {
|
||||
decoded += value;
|
||||
}
|
||||
}
|
||||
Settings settings;
|
||||
for (size_t start = 0; start < decoded.size();) {
|
||||
const size_t end = decoded.find('\n', start);
|
||||
const std::string line = decoded.substr(start, end - start);
|
||||
const size_t equals = line.find('=');
|
||||
if (equals != std::string::npos && equals != 0) {
|
||||
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
|
||||
}
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
inline bool HasIdentity(const Settings& settings) {
|
||||
const auto serial = settings.find("SERNO");
|
||||
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
|
||||
serial->second.find_first_not_of("0123456789") != std::string::npos ||
|
||||
serial->second.find_first_not_of('0') == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
|
||||
const auto value = settings.find(field.first);
|
||||
if (value == settings.end() || value->second.empty() ||
|
||||
value->second.size() > field.second) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dolphin's normal (non-deterministic) first-boot algorithm. It is independent
|
||||
// of the ES device ID. Matching another NAND requires that NAND's saved serial.
|
||||
inline std::string GenerateSerial(std::time_t now) {
|
||||
if (now < 0) {
|
||||
return {};
|
||||
}
|
||||
const auto digits = std::to_string(now % 1000000000);
|
||||
return std::string(9 - digits.size(), '0') + digits;
|
||||
}
|
||||
|
||||
// This recompilation targets the European disc. These are Dolphin's PAL boot
|
||||
// defaults; an existing setting.txt always takes precedence, in every region.
|
||||
inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& serial) {
|
||||
const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}};
|
||||
if (!HasIdentity(identity)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::array<uint8_t, 256> bytes{};
|
||||
size_t position = 0;
|
||||
uint32_t key = 0x73B5DBFAu;
|
||||
const auto writeByte = [&](char value) {
|
||||
bytes[position++] = static_cast<uint8_t>(value) ^ static_cast<uint8_t>(key);
|
||||
key = (key << 1) | (key >> 31);
|
||||
};
|
||||
for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"),
|
||||
std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"),
|
||||
"SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) {
|
||||
for (;;) {
|
||||
if (position + line.size() > bytes.size()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto start = position;
|
||||
const auto savedKey = key;
|
||||
bool hasNull = false;
|
||||
for (const char value : line) {
|
||||
writeByte(value);
|
||||
hasNull |= bytes[position - 1] == 0;
|
||||
}
|
||||
if (!hasNull) {
|
||||
break;
|
||||
}
|
||||
// Nintendo stops at an encoded NUL. Dolphin inserts an extra LF
|
||||
// before this line and retries with the shifted encryption key.
|
||||
position = start;
|
||||
key = savedKey;
|
||||
writeByte('\n');
|
||||
}
|
||||
}
|
||||
return bytes; // The unused tail stays raw zero, as in Dolphin.
|
||||
}
|
||||
|
||||
// Never replace an existing file, including an unreadable or damaged one.
|
||||
// Publish a complete file atomically so simultaneous launches use one identity.
|
||||
inline bool Ensure(const std::filesystem::path& root, std::string& error,
|
||||
std::time_t now = std::time(nullptr)) {
|
||||
const auto path = FilePath(root);
|
||||
std::error_code ec;
|
||||
const auto status = std::filesystem::symlink_status(path, ec);
|
||||
if (ec && ec != std::errc::no_such_file_or_directory) {
|
||||
error = "Cannot inspect NAND setting.txt: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
if (std::filesystem::exists(status)) {
|
||||
const auto existing = Read(root);
|
||||
if (existing && HasIdentity(*existing)) {
|
||||
return true;
|
||||
}
|
||||
error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bytes = EncodeNew(GenerateSerial(now));
|
||||
if (!bytes) {
|
||||
error = "Cannot initialize NAND settings: invalid system clock";
|
||||
return false;
|
||||
}
|
||||
ec.clear();
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
error = "Cannot create NAND settings directory: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
static std::atomic<unsigned> sequence{0};
|
||||
const auto scratch = path.parent_path() / (".setting-init-" + std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" + std::to_string(sequence++));
|
||||
if (!std::filesystem::create_directory(scratch, ec)) {
|
||||
error = "Cannot create temporary NAND settings directory: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
const auto temporary = scratch / "setting.txt";
|
||||
bool written = false;
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
|
||||
output.close();
|
||||
written = static_cast<bool>(output);
|
||||
}
|
||||
bool published = false;
|
||||
if (written) {
|
||||
#ifdef _WIN32
|
||||
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
|
||||
#else
|
||||
published = ::link(temporary.c_str(), path.c_str()) == 0;
|
||||
#endif
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
std::filesystem::remove(scratch, ec);
|
||||
// A competing launcher may have published its settings first. Always read
|
||||
// the winner from NAND rather than using our unpersisted candidate serial.
|
||||
const auto persisted = Read(root);
|
||||
if (persisted && HasIdentity(*persisted)) {
|
||||
return true;
|
||||
}
|
||||
error = published ? "Cannot read newly initialized NAND setting.txt" :
|
||||
"Cannot persist NAND setting.txt; check NAND directory permissions";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandSettings
|
||||
+29
-15
@@ -12,7 +12,25 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kPalProductRegion = 2;
|
||||
// Use the SDK's own value tables, including its unknown-region result.
|
||||
uint32_t LookupProductRegion(uint32_t table, uint32_t stride, uint32_t count,
|
||||
const std::string& value) {
|
||||
for (uint32_t index = 0; index < count; ++index) {
|
||||
const uint32_t entry = table + index * stride;
|
||||
if (!Memory::Contains(entry, stride)) {
|
||||
break;
|
||||
}
|
||||
const auto* bytes = static_cast<const uint8_t*>(Memory::GetPointer(entry, stride));
|
||||
if (bytes[0] == 0xFF) {
|
||||
break;
|
||||
}
|
||||
if (value.size() < stride - 1 &&
|
||||
std::memcmp(bytes + 1, value.c_str(), value.size() + 1) == 0) {
|
||||
return bytes[0];
|
||||
}
|
||||
}
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -50,16 +68,12 @@ extern "C" uint32_t SCGetEuRgb60Mode_HLE()
|
||||
|
||||
PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ());
|
||||
|
||||
// The managed NAND intentionally starts without a console-owned setting.txt.
|
||||
// DWC nevertheless requires the Wii product code and serial number so it can
|
||||
// include csnum in NAS authentication. Expose one stable virtual-console
|
||||
// identity without requiring or mutating a user's real NAND.
|
||||
// Expose the selected emulated NAND identity through the SDK SC APIs.
|
||||
|
||||
extern "C" uint32_t SCGetProductArea_HLE()
|
||||
{
|
||||
// The PAL setting.txt AREA value is "EUR". The SDK's lookup table at
|
||||
// 0x8029CEB0 maps JPN=0, USA=1, EUR=2.
|
||||
return kPalProductRegion;
|
||||
return LookupProductRegion(0x8029CEB0u, 5, 13,
|
||||
RuntimeConsoleIdentity::Current().area);
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ());
|
||||
@@ -68,12 +82,13 @@ extern "C" uint32_t SCGetProductCode_HLE()
|
||||
{
|
||||
// Original PAL SC storage for the six-byte CODE value.
|
||||
constexpr uint32_t kProductCodeAddress = 0x803869E0u;
|
||||
static constexpr char kProductCode[] = "LEH";
|
||||
if (!Memory::Contains(kProductCodeAddress, sizeof(kProductCode))) {
|
||||
const std::string& productCode = RuntimeConsoleIdentity::Current().productCode;
|
||||
const size_t size = productCode.size() + 1;
|
||||
if (!Memory::Contains(kProductCodeAddress, size)) {
|
||||
return 0;
|
||||
}
|
||||
std::memcpy(Memory::GetPointer(kProductCodeAddress, sizeof(kProductCode)),
|
||||
kProductCode, sizeof(kProductCode));
|
||||
std::memcpy(Memory::GetPointer(kProductCodeAddress, size),
|
||||
productCode.c_str(), size);
|
||||
return kProductCodeAddress;
|
||||
}
|
||||
|
||||
@@ -94,9 +109,8 @@ PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddr
|
||||
|
||||
extern "C" uint32_t SCGetProductGameRegion_HLE()
|
||||
{
|
||||
// The PAL setting.txt GAME value is "EU". The SDK's own lookup table at
|
||||
// 0x8029CEF8 maps JP=0, US=1, EU=2.
|
||||
return kPalProductRegion;
|
||||
return LookupProductRegion(0x8029CEF8u, 4, 4,
|
||||
RuntimeConsoleIdentity::Current().gameRegion);
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ());
|
||||
|
||||
@@ -483,11 +483,6 @@ void DrawRumbleSettings() {
|
||||
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD,
|
||||
};
|
||||
PADControlAllMotors(stopAll.data());
|
||||
#if defined(_WIN32)
|
||||
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
Wup028Adapter::SetRumble(port, false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "nand_settings.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
static void Require(bool condition, const char* message = "NAND settings check failed") {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
static std::string ReadBytes(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto root = std::filesystem::temp_directory_path() /
|
||||
("wiicomp-nand-settings-" + std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
const auto path = root / "title/00000001/00000002/data/setting.txt";
|
||||
try {
|
||||
using namespace RuntimeNandSettings;
|
||||
Require(GenerateSerial(1800000123) == "800000123", "Dolphin timestamp modulo");
|
||||
Require(GenerateSerial(1000000001) == "000000001", "Dolphin leading zero padding");
|
||||
Require(GenerateSerial(-1).empty(), "Invalid clock must not supply an identity");
|
||||
// Golden bytes generated by Dolphin's unmodified SettingsHandler.cpp
|
||||
// (upstream 2026-09-06), PAL boot fields and synthetic serial 000000001.
|
||||
// Everything after this prefix is raw zero padding to 256 bytes.
|
||||
const std::string goldenHex =
|
||||
"bba6ac929a0bc96b7eed83d27f33a1e7e73d9b836d8b47c59ee23df6b275baab"
|
||||
"bec9d9dead03cc7a3bdafee50c30ab9fb86194e119fe4ba19eff62d5ec3aacb3"
|
||||
"b5c9d9e3977eac0943d7ff903120a49ef024eafe1cf77be79cf6229a823aabf0f0";
|
||||
std::array<uint8_t, 256> golden{};
|
||||
for (size_t i = 0; i < goldenHex.size() / 2; ++i) {
|
||||
golden[i] = static_cast<uint8_t>(std::stoul(goldenHex.substr(i * 2, 2), nullptr, 16));
|
||||
}
|
||||
Require(EncodeNew("000000001") == golden, "Exact Dolphin writer golden fixture");
|
||||
std::string error;
|
||||
Require(!RuntimeNandSettings::Read(root));
|
||||
Require(!std::filesystem::exists(root));
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
|
||||
std::array<uint8_t, 256> fixture{};
|
||||
for (size_t i = 0; i < fixture.size(); ++i) {
|
||||
const unsigned shift = i % 32;
|
||||
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
|
||||
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
|
||||
fixture[i] = static_cast<uint8_t>(key) ^ (i < plain.size() ? plain[i] : 0);
|
||||
}
|
||||
{
|
||||
std::ofstream output(path, std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(fixture.data()), fixture.size());
|
||||
}
|
||||
auto settings = RuntimeNandSettings::Read(root);
|
||||
Require(settings && RuntimeNandSettings::HasIdentity(*settings));
|
||||
Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU");
|
||||
Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US");
|
||||
Require(Ensure(root, error, 1800000123), "Existing imported NAND must work");
|
||||
std::array<uint8_t, 256> after{};
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
input.read(reinterpret_cast<char*>(after.data()), after.size());
|
||||
}
|
||||
Require(after == fixture);
|
||||
for (const auto serial : {"", "000000000", "1234567890", "123ABC789"}) {
|
||||
(*settings)["SERNO"] = serial;
|
||||
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||
}
|
||||
(*settings)["SERNO"] = "012345678";
|
||||
Require(RuntimeNandSettings::HasIdentity(*settings));
|
||||
(*settings)["CODE"] = "TOOLONG";
|
||||
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||
(*settings)["CODE"] = "LEH";
|
||||
settings->erase("GAME");
|
||||
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||
std::filesystem::resize_file(path, 128);
|
||||
Require(!RuntimeNandSettings::Read(root));
|
||||
const auto damaged = ReadBytes(path);
|
||||
Require(!Ensure(root, error, 1800000123), "Do not replace a truncated identity");
|
||||
Require(ReadBytes(path) == damaged, "Damaged file must remain untouched");
|
||||
|
||||
const auto fresh = root / "fresh";
|
||||
Require(Ensure(fresh, error, 1800000123), "Missing setting.txt must initialize");
|
||||
const auto generated = Read(fresh);
|
||||
Require(generated && HasIdentity(*generated), "Generated file must be readable");
|
||||
Require(generated->at("SERNO") == "800000123", "Persist Dolphin-generated serial");
|
||||
Require(generated->at("CODE") == "LEH" && generated->at("AREA") == "EUR" &&
|
||||
generated->at("GAME") == "EU", "PAL first-boot fields");
|
||||
Require(generated->at("MODEL") == "RVL-001(EUR)" && generated->at("VIDEO") == "PAL" &&
|
||||
generated->at("DVD") == "0" && generated->at("MPCH") == "0x7FFE",
|
||||
"Complete Dolphin boot settings");
|
||||
const auto firstBoot = ReadBytes(FilePath(fresh));
|
||||
Require(firstBoot.size() == 256 && firstBoot.back() == 0, "Dolphin buffer size and raw zero padding");
|
||||
Require(Ensure(fresh, error, 1900000999), "Second boot");
|
||||
Require(ReadBytes(FilePath(fresh)) == firstBoot, "Second boot must not change any bytes");
|
||||
|
||||
const auto blocked = root / "blocked";
|
||||
{ std::ofstream output(blocked); output << "file obstructing NAND directory"; }
|
||||
Require(!Ensure(blocked, error, 1800000123), "Write failure must not return an ephemeral identity");
|
||||
Require(!Ensure(root / "bad-clock", error, -1), "Clock failure must not initialize");
|
||||
|
||||
const auto concurrent = root / "concurrent";
|
||||
std::array<bool, 16> results{};
|
||||
std::vector<std::thread> workers;
|
||||
for (size_t i = 0; i < results.size(); ++i) {
|
||||
workers.emplace_back([&, i] {
|
||||
std::string detail;
|
||||
results[i] = Ensure(concurrent, detail, 1800000001 + i);
|
||||
});
|
||||
}
|
||||
for (auto& worker : workers) worker.join();
|
||||
for (const bool result : results) Require(result, "Concurrent boot must read the persisted winner");
|
||||
const auto winner = ReadBytes(FilePath(concurrent));
|
||||
Require(Read(concurrent) && HasIdentity(*Read(concurrent)), "Concurrent boot must persist valid settings");
|
||||
Require(Ensure(concurrent, error, 1900000999), "Boot after concurrent initialization");
|
||||
Require(ReadBytes(FilePath(concurrent)) == winner, "Concurrent winner must remain stable");
|
||||
|
||||
// Independently decode as Nintendo does: stop at the first encoded NUL.
|
||||
// Exercise serials that force Dolphin's extra-LF escaping, not only
|
||||
// values that happen to work with a plain rotating-XOR encoder.
|
||||
bool sawExtraLf = false;
|
||||
for (int serial = 1; serial <= 10000; ++serial) {
|
||||
const auto number = GenerateSerial(1000000000 + serial);
|
||||
const auto encoded = EncodeNew(number);
|
||||
Require(encoded.has_value(), "Serial encoding must fit");
|
||||
std::string decoded;
|
||||
for (size_t i = 0; i < encoded->size() && (*encoded)[i] != 0; ++i) {
|
||||
const unsigned shift = i % 32;
|
||||
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
|
||||
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
|
||||
decoded += static_cast<char>((*encoded)[i] ^ static_cast<uint8_t>(key));
|
||||
}
|
||||
Require(decoded.find("SERNO=" + number + "\r\n") != std::string::npos &&
|
||||
decoded.find("GAME=EU\r\n") != std::string::npos,
|
||||
"Encoded NUL must not truncate settings");
|
||||
sawExtraLf |= decoded.find("\r\n\n") != std::string::npos;
|
||||
}
|
||||
Require(sawExtraLf, "Exercise Dolphin LF escape path");
|
||||
std::filesystem::remove_all(root);
|
||||
std::cout << "NAND settings checks passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::filesystem::remove_all(root);
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user