diff --git a/include/nod/IDiscIO.hpp b/include/nod/IDiscIO.hpp index 2999597..f9eb12e 100644 --- a/include/nod/IDiscIO.hpp +++ b/include/nod/IDiscIO.hpp @@ -28,6 +28,7 @@ public: virtual ~IDiscIO() = default; virtual std::unique_ptr beginReadStream(uint64_t offset = 0) const = 0; virtual std::unique_ptr beginWriteStream(uint64_t offset = 0) const = 0; + virtual bool hasWiiCrypto() const { return true; } /* NFS overrides this to false */ }; struct IPartReadStream : IReadStream { diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 802027a..1a04043 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(nod DiscBase.cpp DiscGCN.cpp DiscIOISO.cpp + DiscIONFS.cpp DiscIOWBFS.cpp DiscWii.cpp nod.cpp diff --git a/lib/DiscIOISO.cpp b/lib/DiscIOISO.cpp index 3054c04..61479b4 100644 --- a/lib/DiscIOISO.cpp +++ b/lib/DiscIOISO.cpp @@ -28,9 +28,8 @@ public: bool err = false; auto ret = std::unique_ptr(new ReadStream(m_fio->beginReadStream(offset), err)); - if (err) { - return nullptr; - } + if (err) + return {}; return ret; } @@ -51,9 +50,8 @@ public: bool err = false; auto ret = std::unique_ptr(new WriteStream(m_fio->beginWriteStream(offset), err)); - if (err) { - return nullptr; - } + if (err) + return {}; return ret; } diff --git a/lib/DiscIONFS.cpp b/lib/DiscIONFS.cpp new file mode 100644 index 0000000..bea6bb4 --- /dev/null +++ b/lib/DiscIONFS.cpp @@ -0,0 +1,282 @@ +#include "nod/IDiscIO.hpp" +#include "nod/IFileIO.hpp" +#include "nod/Util.hpp" +#include "nod/aes.hpp" + +#include + +#include + +namespace nod { + +/* + * NFS is the image format used to distribute Wii VC games for the Wii U. + * It is an LBA format similar to WBFS but adds its own encryption layer. + * It logically stores a standard Wii disc image with partitions. + */ + +class DiscIONFS : public IDiscIO { + struct DiscIONFSShared { + std::vector> files; + + struct NFSHead { + uint32_t magic; // EGGS + uint32_t version; + uint32_t unknown[2]; // Signature, UUID? + uint32_t lbaRangeCount; + struct { + uint32_t startBlock; + uint32_t numBlocks; + } lbaRanges[61]; + uint32_t endMagic; // SGGE + } nfsHead; + + uint8_t key[16]; + + uint32_t calculateNumFiles() const { + uint32_t totalSectorCount = 0; + for (uint32_t i = 0; i < nfsHead.lbaRangeCount; ++i) { + const auto& range = nfsHead.lbaRanges[i]; + totalSectorCount += range.numBlocks; + } + return (uint64_t(totalSectorCount) * uint64_t(0x8000) + + (uint64_t(0x200) + uint64_t(0xF9FFFFF))) / uint64_t(0xFA00000); + } + + struct FBO { + uint32_t file, block, offset; + }; + + FBO logicalToFBO(uint64_t offset) const { + auto sectorAndRemBytes = std::lldiv(offset, 0x8000); /* 32768 bytes per block */ + uint32_t i, physicalBlock; + for (i = 0, physicalBlock = 0; i < nfsHead.lbaRangeCount; ++i) { + const auto& range = nfsHead.lbaRanges[i]; + if (sectorAndRemBytes.quot >= range.startBlock && + sectorAndRemBytes.quot - range.startBlock < range.numBlocks) { + sectorAndRemBytes.quot = physicalBlock + (sectorAndRemBytes.quot - range.startBlock); + break; + } + physicalBlock += range.numBlocks; + } + /* This offset has no physical mapping, read zeroes */ + if (i == nfsHead.lbaRangeCount) + return {UINT32_MAX, UINT32_MAX, UINT32_MAX}; + auto fileAndRemBlocks = std::lldiv(sectorAndRemBytes.quot, 8000); /* 8000 blocks per file */ + return {uint32_t(fileAndRemBlocks.quot), uint32_t(fileAndRemBlocks.rem), uint32_t(sectorAndRemBytes.rem)}; + } + + DiscIONFSShared(SystemStringView fpin, bool& err) { + /* Validate file path format */ + using SignedSize = std::make_signed::type; + const auto dotPos = SignedSize(fpin.rfind('.')); + const auto slashPos = SignedSize(fpin.rfind("/\\")); + if (fpin.size() <= 4 || dotPos == -1 || dotPos <= slashPos || + fpin.compare(slashPos + 1, 4, "hif_") || + fpin.compare(dotPos, fpin.size() - dotPos, ".nfs")) { + LogModule.report(logvisor::Error, + fmt("'{}' must begin with 'hif_' and end with '.nfs' to be accepted as an NFS image"), fpin); + err = true; + return; + } + + /* Load key file */ + const SystemString dir(fpin.begin(), fpin.begin() + slashPos + 1); + auto keyFile = NewFileIO(dir + "../code/htk.bin")->beginReadStream(); + if (!keyFile) + keyFile = NewFileIO(dir + "htk.bin")->beginReadStream(); + if (!keyFile) { + LogModule.report(logvisor::Error, fmt("Unable to open '{}../code/htk.bin' or '{}htk.bin'"), dir, dir); + err = true; + return; + } + if (keyFile->read(key, 16) != 16) { + LogModule.report(logvisor::Error, fmt("Unable to read from '{}../code/htk.bin' or '{}htk.bin'"), dir, dir); + err = true; + return; + } + + /* Load header from first file */ + const SystemString firstPath = fmt::format(fmt("{}hif_{:06}.nfs"), dir, 0); + files.push_back(NewFileIO(firstPath)); + auto rs = files.back()->beginReadStream(); + if (!rs) { + LogModule.report(logvisor::Error, fmt("'{}' does not exist"), firstPath); + err = true; + return; + } + if (rs->read(&nfsHead, 0x200) != 0x200) { + LogModule.report(logvisor::Error, fmt("Unable to read header from '{}'"), firstPath); + err = true; + return; + } + if (std::memcmp(&nfsHead.magic, "EGGS", 4)) { + LogModule.report(logvisor::Error, fmt("Invalid magic in '{}'"), firstPath); + err = true; + return; + } + nfsHead.lbaRangeCount = SBig(nfsHead.lbaRangeCount); + for (uint32_t i = 0; i < nfsHead.lbaRangeCount; ++i) { + auto& range = nfsHead.lbaRanges[i]; + range.startBlock = SBig(range.startBlock); + range.numBlocks = SBig(range.numBlocks); + } + + /* Ensure remaining files exist */ + const uint32_t numFiles = calculateNumFiles(); + files.reserve(numFiles); + for (uint32_t i = 1; i < numFiles; ++i) { + SystemString path = fmt::format(fmt("{}hif_{:06}.nfs"), dir, i); + files.push_back(NewFileIO(path)); + if (!files.back()->exists()) { + LogModule.report(logvisor::Error, fmt("'{}' does not exist"), path); + err = true; + return; + } + } + } + }; + std::shared_ptr m_shared; + +public: + DiscIONFS(SystemStringView fpin, bool& err) : m_shared(std::make_shared(fpin, err)) {} + + class ReadStream : public IReadStream { + friend class DiscIONFS; + std::shared_ptr m_shared; + std::unique_ptr m_rs; + std::unique_ptr m_aes; + + /* Physical address - all UINT32_MAX indicates logical zero block */ + DiscIONFSShared::FBO m_physAddr; + + /* Logical address */ + uint64_t m_offset; + + /* Active file stream and its offset as set in the system. + * Block is typically one ahead of the presently decrypted block. */ + uint32_t m_curFile = UINT32_MAX; + uint32_t m_curBlock = UINT32_MAX; + + ReadStream(std::shared_ptr shared, uint64_t offset, bool& err) + : m_shared(std::move(shared)), m_aes(NewAES()), + m_physAddr({UINT32_MAX, UINT32_MAX, UINT32_MAX}), m_offset(offset) { + m_aes->setKey(m_shared->key); + setNewLogicalAddr(offset); + } + + uint8_t m_encBuf[0x8000] = {}; + uint8_t m_decBuf[0x8000] = {}; + + void setCurFile(uint32_t curFile) { + if (curFile >= m_shared->files.size()) { + LogModule.report(logvisor::Error, fmt("Out of bounds NFS file access")); + return; + } + m_curFile = curFile; + m_curBlock = UINT32_MAX; + m_rs = m_shared->files[m_curFile]->beginReadStream(); + } + + void setCurBlock(uint32_t curBlock) { + m_curBlock = curBlock; + m_rs->seek(m_curBlock * 0x8000 + 0x200); + } + + void setNewPhysicalAddr(DiscIONFSShared::FBO physAddr) { + /* If we're just changing the offset, nothing else needs to be done */ + if (m_physAddr.file == physAddr.file && m_physAddr.block == physAddr.block) { + m_physAddr.offset = physAddr.offset; + return; + } + m_physAddr = physAddr; + + /* Set logical zero block */ + if (m_physAddr.file == UINT32_MAX) { + memset(m_decBuf, 0, 0x8000); + return; + } + + /* Make necessary file and block current with system */ + if (m_physAddr.file != m_curFile) + setCurFile(m_physAddr.file); + if (m_physAddr.block != m_curBlock) + setCurBlock(m_physAddr.block); + + /* Read block, handling 0x200 overlap case */ + if (m_physAddr.block == 7999) { + m_rs->read(m_encBuf, 0x7E00); + setCurFile(m_curFile + 1); + m_rs->read(m_encBuf + 0x7E00, 0x200); + m_curBlock = 0; + } else { + m_rs->read(m_encBuf, 0x8000); + ++m_curBlock; + } + + /* Decrypt */ + const uint32_t ivBuf[] = {0, 0, 0, SBig(m_physAddr.block)}; + m_aes->decrypt((const uint8_t*)ivBuf, m_encBuf, m_decBuf, 0x8000); + } + + void setNewLogicalAddr(uint64_t addr) { + setNewPhysicalAddr(m_shared->logicalToFBO(m_offset)); + } + + public: + uint64_t read(void* buf, uint64_t length) override { + uint64_t rem = length; + uint8_t* dst = (uint8_t*)buf; + + /* Perform reads on block boundaries */ + while (rem) { + uint64_t readSize = rem; + uint32_t blockOffset = (m_physAddr.offset == UINT32_MAX) ? 0 : m_physAddr.offset; + if (readSize + blockOffset > 0x8000) + readSize = 0x8000 - blockOffset; + + memmove(dst, m_decBuf + blockOffset, readSize); + dst += readSize; + rem -= readSize; + m_offset += readSize; + setNewLogicalAddr(m_offset); + } + + return dst - (uint8_t*)buf; + } + uint64_t position() const override { return m_offset; } + void seek(int64_t offset, int whence) override { + if (whence == SEEK_SET) + m_offset = offset; + else if (whence == SEEK_CUR) + m_offset += offset; + else + return; + setNewLogicalAddr(m_offset); + } + }; + + std::unique_ptr beginReadStream(uint64_t offset) const override { + bool err = false; + auto ret = std::unique_ptr(new ReadStream(m_shared, offset, err)); + + if (err) + return {}; + + return ret; + } + + std::unique_ptr beginWriteStream(uint64_t offset) const override { return {}; } + + bool hasWiiCrypto() const override { return false; } +}; + +std::unique_ptr NewDiscIONFS(SystemStringView path) { + bool err = false; + auto ret = std::make_unique(path, err); + if (err) + return {}; + return ret; +} + +} \ No newline at end of file diff --git a/lib/DiscIOWBFS.cpp b/lib/DiscIOWBFS.cpp index a5e9261..304f08a 100644 --- a/lib/DiscIOWBFS.cpp +++ b/lib/DiscIOWBFS.cpp @@ -23,7 +23,7 @@ static uint8_t size_to_shift(uint32_t size) { } class DiscIOWBFS : public IDiscIO { - SystemString filepath; + std::unique_ptr m_fio; struct WBFSHead { uint32_t magic; @@ -83,10 +83,9 @@ class DiscIOWBFS : public IDiscIO { } public: - DiscIOWBFS(SystemStringView fpin) : filepath(fpin) { + DiscIOWBFS(SystemStringView fpin) : m_fio(NewFileIO(fpin)) { /* Temporary file handle to read LBA table */ - std::unique_ptr fio = NewFileIO(filepath); - std::unique_ptr rs = fio->beginReadStream(); + std::unique_ptr rs = m_fio->beginReadStream(); if (!rs) return; @@ -265,16 +264,15 @@ public: std::unique_ptr beginReadStream(uint64_t offset) const override { bool err = false; - auto ret = std::unique_ptr(new ReadStream(*this, NewFileIO(filepath)->beginReadStream(), offset, err)); + auto ret = std::unique_ptr(new ReadStream(*this, m_fio->beginReadStream(), offset, err)); - if (err) { - return nullptr; - } + if (err) + return {}; return ret; } - std::unique_ptr beginWriteStream(uint64_t offset) const override { return nullptr; } + std::unique_ptr beginWriteStream(uint64_t offset) const override { return {}; } }; std::unique_ptr NewDiscIOWBFS(SystemStringView path) { return std::make_unique(path); } diff --git a/lib/DiscWii.cpp b/lib/DiscWii.cpp index 0b525b8..1347990 100644 --- a/lib/DiscWii.cpp +++ b/lib/DiscWii.cpp @@ -332,14 +332,22 @@ public: uint8_t m_decBuf[0x7c00]; void decryptBlock() { - m_dio->read(m_encBuf, 0x8000); - m_aes->decrypt(&m_encBuf[0x3d0], &m_encBuf[0x400], m_decBuf, 0x7c00); + if (m_aes) { + m_dio->read(m_encBuf, 0x8000); + m_aes->decrypt(&m_encBuf[0x3d0], &m_encBuf[0x400], m_decBuf, 0x7c00); + } else { + m_dio->seek(0x400, SEEK_CUR); + m_dio->read(m_decBuf, 0x7c00); + } } public: PartReadStream(const PartitionWii& parent, uint64_t baseOffset, uint64_t offset, bool& err) - : m_aes(NewAES()), m_parent(parent), m_baseOffset(baseOffset), m_offset(offset) { - m_aes->setKey(parent.m_decKey); + : m_parent(parent), m_baseOffset(baseOffset), m_offset(offset) { + if (m_parent.m_parent.getDiscIO().hasWiiCrypto()) { + m_aes = NewAES(); + m_aes->setKey(parent.m_decKey); + } size_t block = m_offset / 0x7c00; m_dio = m_parent.m_parent.getDiscIO().beginReadStream(m_baseOffset + block * 0x8000); if (!m_dio) { @@ -365,27 +373,25 @@ public: } uint64_t position() const override { return m_offset; } uint64_t read(void* buf, uint64_t length) override { - size_t block = m_offset / 0x7c00; - size_t cacheOffset = m_offset % 0x7c00; - uint64_t cacheSize; + auto blockAndRemOff = std::lldiv(m_offset, 0x7c00); uint64_t rem = length; uint8_t* dst = (uint8_t*)buf; while (rem) { - if (block != m_curBlock) { + if (blockAndRemOff.quot != m_curBlock) { decryptBlock(); - m_curBlock = block; + m_curBlock = blockAndRemOff.quot; } - cacheSize = rem; - if (cacheSize + cacheOffset > 0x7c00) - cacheSize = 0x7c00 - cacheOffset; + uint64_t cacheSize = rem; + if (cacheSize + blockAndRemOff.rem > 0x7c00) + cacheSize = 0x7c00 - blockAndRemOff.rem; - memmove(dst, m_decBuf + cacheOffset, cacheSize); + memmove(dst, m_decBuf + blockAndRemOff.rem, cacheSize); dst += cacheSize; rem -= cacheSize; - cacheOffset = 0; - ++block; + blockAndRemOff.rem = 0; + ++blockAndRemOff.quot; } m_offset += length; diff --git a/lib/FileIOFILE.cpp b/lib/FileIOFILE.cpp index b265eb8..f46e6bd 100644 --- a/lib/FileIOFILE.cpp +++ b/lib/FileIOFILE.cpp @@ -77,9 +77,8 @@ public: bool err = false; auto ret = std::unique_ptr(new WriteStream(m_path, m_maxWriteSize, err)); - if (err) { - return nullptr; - } + if (err) + return {}; return ret; } @@ -88,9 +87,8 @@ public: bool err = false; auto ret = std::unique_ptr(new WriteStream(m_path, offset, m_maxWriteSize, err)); - if (err) { - return nullptr; - } + if (err) + return {}; return ret; } @@ -137,9 +135,8 @@ public: bool err = false; auto ret = std::unique_ptr(new ReadStream(m_path, err)); - if (err) { - return nullptr; - } + if (err) + return {}; return ret; } @@ -148,9 +145,8 @@ public: bool err = false; auto ret = std::unique_ptr(new ReadStream(m_path, offset, err)); - if (err) { - return nullptr; - } + if (err) + return {}; return ret; } diff --git a/lib/nod.cpp b/lib/nod.cpp index 846da56..abe7461 100644 --- a/lib/nod.cpp +++ b/lib/nod.cpp @@ -12,6 +12,7 @@ logvisor::Module LogModule("nod"); std::unique_ptr NewDiscIOISO(SystemStringView path); std::unique_ptr NewDiscIOWBFS(SystemStringView path); +std::unique_ptr NewDiscIONFS(SystemStringView path); std::unique_ptr OpenDiscFromImage(SystemStringView path, bool& isWii) { /* Temporary file handle to determine image type */ @@ -32,9 +33,17 @@ std::unique_ptr OpenDiscFromImage(SystemStringView path, bool& isWii) return {}; } + using SignedSize = std::make_signed::type; + const auto dotPos = SignedSize(path.rfind('.')); + const auto slashPos = SignedSize(path.rfind("/\\")); if (magic == nod::SBig((uint32_t)'WBFS')) { discIO = NewDiscIOWBFS(path); isWii = true; + } else if (path.size() > 4 && dotPos != -1 && dotPos > slashPos && + !path.compare(slashPos + 1, 4, "hif_") && + !path.compare(dotPos, path.size() - dotPos, ".nfs")) { + discIO = NewDiscIONFS(path); + isWii = true; } else { rs->seek(0x18, SEEK_SET); rs->read(&magic, 4);