mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
2d9dc4e0f2
* import setting.txt * coderabbit ugh
184 lines
9.9 KiB
C++
184 lines
9.9 KiB
C++
#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 auto scratchParent = root / "scratch-collisions";
|
|
std::filesystem::create_directories(scratchParent / ".setting-init-fixed-0");
|
|
const auto sentinel = scratchParent / ".setting-init-fixed-0" / "setting.txt";
|
|
{ std::ofstream output(sentinel); output << "another launch owns this"; }
|
|
const auto occupiedFile = scratchParent / ".setting-init-fixed-1";
|
|
{ std::ofstream output(occupiedFile); output << "leave this file alone"; }
|
|
std::error_code scratchError;
|
|
const auto claimed = CreateScratchDirectory(scratchParent, "fixed", scratchError);
|
|
Require(claimed && *claimed == scratchParent / ".setting-init-fixed-2" && !scratchError,
|
|
"Retry collisions with both existing directories and files");
|
|
Require(ReadBytes(sentinel) == "another launch owns this" &&
|
|
ReadBytes(occupiedFile) == "leave this file alone", "Never modify another launch's scratch data");
|
|
Require(!CreateScratchDirectory(occupiedFile / "not-a-directory", "fixed", scratchError) && scratchError,
|
|
"Real filesystem errors must fail rather than retry indefinitely");
|
|
|
|
// Force all claimants to use the same token; this deterministically
|
|
// exercises the collision path even when host clock precision is high.
|
|
std::array<std::optional<std::filesystem::path>, 16> claims;
|
|
std::vector<std::thread> claimants;
|
|
for (size_t i = 0; i < claims.size(); ++i) {
|
|
claimants.emplace_back([&, i] {
|
|
std::error_code ec;
|
|
claims[i] = CreateScratchDirectory(scratchParent, "shared", ec);
|
|
});
|
|
}
|
|
for (auto& claimant : claimants) claimant.join();
|
|
for (size_t i = 0; i < claims.size(); ++i) {
|
|
Require(claims[i].has_value(), "Every concurrent claimant must acquire a scratch directory");
|
|
for (size_t j = 0; j < i; ++j) {
|
|
Require(claims[i] != claims[j], "Concurrent claimants must own different scratch directories");
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|