mirror of
https://github.com/GerbilSoft/rvthtool.git
synced 2025-06-18 11:35:33 -04:00

Option is -N or --ndev. NOTE: Only seems to work with Wii images at the moment. NDEV displays the disc information from GameCube images, but then hangs. Tested with SL and DL discs on the apploader. doc/sdk_header.md: Initial SDK header documentation.
986 lines
25 KiB
C
986 lines
25 KiB
C
/***************************************************************************
|
|
* RVT-H Tool (librvth) *
|
|
* rvth.c: RVT-H image handler. *
|
|
* *
|
|
* Copyright (c) 2018 by David Korth. *
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify it *
|
|
* under the terms of the GNU General Public License as published by the *
|
|
* Free Software Foundation; either version 2 of the License, or (at your *
|
|
* option) any later version. *
|
|
* *
|
|
* This program is distributed in the hope that it will be useful, but *
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
***************************************************************************/
|
|
|
|
#include "rvth.h"
|
|
#include "rvth_p.h"
|
|
#include "rvth_time.h"
|
|
|
|
#include "byteswap.h"
|
|
#include "nhcd_structs.h"
|
|
#include "gcn_structs.h"
|
|
#include "cert_store.h"
|
|
#include "cert.h"
|
|
#include "disc_header.h"
|
|
#include "ptbl.h"
|
|
|
|
// Disc image reader.
|
|
#include "reader.h"
|
|
|
|
// C includes.
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/**
|
|
* Set the region field in an RvtH_BankEntry.
|
|
* The reader field must have already been set.
|
|
* @param entry [in,out] RvtH_BankEntry
|
|
* @return Error code. (If negative, POSIX error; otherwise, see RvtH_Errors.)
|
|
*/
|
|
static int rvth_init_BankEntry_region(RvtH_BankEntry *entry)
|
|
{
|
|
uint32_t lba_size;
|
|
uint32_t lba_region;
|
|
bool is_wii = false;
|
|
|
|
// Sector buffer.
|
|
union {
|
|
uint8_t u8[RVTH_BLOCK_SIZE];
|
|
struct {
|
|
uint8_t bi2_pad[0x40];
|
|
GCN_Boot_Info bi2;
|
|
};
|
|
RVL_RegionSetting rvl_region;
|
|
} sector_buf;
|
|
|
|
assert(entry->reader != NULL);
|
|
|
|
switch (entry->type) {
|
|
case RVTH_BankType_Empty:
|
|
// Empty bank.
|
|
entry->region_code = 0xFF;
|
|
return RVTH_ERROR_BANK_EMPTY;
|
|
case RVTH_BankType_Unknown:
|
|
default:
|
|
// Unknown bank.
|
|
entry->region_code = 0xFF;
|
|
return RVTH_ERROR_BANK_UNKNOWN;
|
|
case RVTH_BankType_Wii_DL_Bank2:
|
|
// Second bank of a dual-layer Wii disc image.
|
|
// TODO: Automatically select the first bank?
|
|
entry->region_code = 0xFF;
|
|
return RVTH_ERROR_BANK_DL_2;
|
|
|
|
case RVTH_BankType_GCN:
|
|
// GameCube image. Read bi2.bin.
|
|
lba_region = BYTES_TO_LBA(GCN_Boot_Info_ADDRESS);
|
|
is_wii = false;
|
|
break;
|
|
|
|
case RVTH_BankType_Wii_SL:
|
|
case RVTH_BankType_Wii_DL:
|
|
// Wii disc image. Read the region settings.
|
|
lba_region = BYTES_TO_LBA(RVL_RegionSetting_ADDRESS);
|
|
is_wii = true;
|
|
break;
|
|
}
|
|
|
|
// Read the LBA containing the region code.
|
|
lba_size = reader_read(entry->reader, sector_buf.u8, lba_region, 1);
|
|
if (lba_size != 1) {
|
|
// Error reading the region code.
|
|
return -EIO;
|
|
}
|
|
|
|
// FIXME: region_code is a 32-bit value,
|
|
// but only the first few bits are used...
|
|
entry->region_code = (is_wii
|
|
? (uint8_t)be32_to_cpu(sector_buf.rvl_region.region_code)
|
|
: (uint8_t)be32_to_cpu(sector_buf.bi2.region_code)
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Set the crypto_type and sig_type fields in an RvtH_BankEntry.
|
|
* The reader and discHeader fields must have already been set.
|
|
* @param entry [in,out] RvtH_BankEntry
|
|
* @return Error code. (If negative, POSIX error; otherwise, see RvtH_Errors.)
|
|
*/
|
|
static int rvth_init_BankEntry_crypto(RvtH_BankEntry *entry)
|
|
{
|
|
const pt_entry_t *game_pte; // Game partition entry.
|
|
uint32_t lba_size;
|
|
uint32_t tmd_size;
|
|
int ret;
|
|
|
|
// Partition header.
|
|
RVL_PartitionHeader header;
|
|
const RVL_TMD_Header *tmdHeader;
|
|
|
|
assert(entry->reader != NULL);
|
|
|
|
// Clear the ticket/TMD crypto information initially.
|
|
entry->ios_version = 0;
|
|
memset(&entry->ticket, 0, sizeof(entry->ticket));
|
|
memset(&entry->tmd, 0, sizeof(entry->tmd));
|
|
|
|
switch (entry->type) {
|
|
case RVTH_BankType_Empty:
|
|
// Empty bank.
|
|
return RVTH_ERROR_BANK_EMPTY;
|
|
case RVTH_BankType_Unknown:
|
|
default:
|
|
// Unknown bank.
|
|
return RVTH_ERROR_BANK_UNKNOWN;
|
|
case RVTH_BankType_GCN:
|
|
// GameCube image. No encryption.
|
|
// TODO: Technically no signature either...
|
|
entry->crypto_type = RVTH_CryptoType_None;
|
|
return 0;
|
|
case RVTH_BankType_Wii_DL_Bank2:
|
|
// Second bank of a dual-layer Wii disc image.
|
|
// TODO: Automatically select the first bank?
|
|
return RVTH_ERROR_BANK_DL_2;
|
|
|
|
case RVTH_BankType_Wii_SL:
|
|
case RVTH_BankType_Wii_DL:
|
|
// Wii disc image.
|
|
break;
|
|
}
|
|
|
|
// Check for an unencrypted RVT-H image.
|
|
if (entry->discHeader.hash_verify != 0 && entry->discHeader.disc_noCrypt != 0) {
|
|
// Unencrypted.
|
|
// TODO: I haven't seen any images where only one of these
|
|
// is zero or non-zero, but that may show up...
|
|
entry->crypto_type = RVTH_CryptoType_None;
|
|
}
|
|
|
|
// Find the game partition.
|
|
// TODO: Error checking.
|
|
game_pte = rvth_ptbl_find_game(entry);
|
|
if (!game_pte) {
|
|
// No game partition...
|
|
return RVTH_ERROR_NO_GAME_PARTITION;
|
|
}
|
|
|
|
// Found the game partition.
|
|
// Read the partition header.
|
|
lba_size = reader_read(entry->reader, &header, game_pte->lba_start, BYTES_TO_LBA(sizeof(header)));
|
|
if (lba_size != BYTES_TO_LBA(sizeof(header))) {
|
|
// Error reading the partition header.
|
|
return -EIO;
|
|
}
|
|
|
|
// Check the ticket signature issuer.
|
|
switch (cert_get_issuer_from_name(header.ticket.issuer)) {
|
|
case RVL_CERT_ISSUER_RETAIL_TICKET:
|
|
// Retail certificate.
|
|
entry->ticket.sig_type = RVTH_SigType_Retail;
|
|
break;
|
|
case RVL_CERT_ISSUER_DEBUG_TICKET:
|
|
// Debug certificate.
|
|
entry->ticket.sig_type = RVTH_SigType_Debug;
|
|
break;
|
|
default:
|
|
// Unknown issuer, or not valid for ticket.
|
|
break;
|
|
}
|
|
|
|
// Validate the signature.
|
|
ret = cert_verify((const uint8_t*)&header.ticket, sizeof(header.ticket));
|
|
if (ret != 0) {
|
|
// Signature verification error.
|
|
if (ret > 0 && ((ret & SIG_ERROR_MASK) == SIG_ERROR_INVALID)) {
|
|
// Invalid signature.
|
|
entry->ticket.sig_status = ((ret & SIG_FAIL_HASH_FAKE)
|
|
? RVTH_SigStatus_Fake
|
|
: RVTH_SigStatus_Invalid);
|
|
} else {
|
|
// Other error.
|
|
entry->ticket.sig_status = RVTH_SigStatus_Unknown;
|
|
}
|
|
} else {
|
|
// Signature is valid.
|
|
entry->ticket.sig_status = RVTH_SigStatus_OK;
|
|
}
|
|
|
|
// Check the TMD signature issuer.
|
|
// TODO: Verify header.tmd_offset?
|
|
tmdHeader = (const RVL_TMD_Header*)header.data;
|
|
switch (cert_get_issuer_from_name(tmdHeader->issuer)) {
|
|
case RVL_CERT_ISSUER_RETAIL_TMD:
|
|
// Retail certificate.
|
|
entry->tmd.sig_type = RVTH_SigType_Retail;
|
|
break;
|
|
case RVL_CERT_ISSUER_DEBUG_TMD:
|
|
// Debug certificate.
|
|
entry->tmd.sig_type = RVTH_SigType_Debug;
|
|
break;
|
|
default:
|
|
// Unknown issuer, or not valid for TMD.
|
|
break;
|
|
}
|
|
|
|
// Check the TMD size.
|
|
tmd_size = be32_to_cpu(header.tmd_size);
|
|
if (tmd_size <= sizeof(header.data)) {
|
|
// TMD is not too big. We can validate the signature.
|
|
ret = cert_verify(header.data, tmd_size);
|
|
if (ret != 0) {
|
|
// Signature verification error.
|
|
if (ret > 0 && ((ret & SIG_ERROR_MASK) == SIG_ERROR_INVALID)) {
|
|
// Invalid signature.
|
|
entry->tmd.sig_status = ((ret & SIG_FAIL_HASH_FAKE)
|
|
? RVTH_SigStatus_Fake
|
|
: RVTH_SigStatus_Invalid);
|
|
} else {
|
|
// Other error.
|
|
entry->tmd.sig_status = RVTH_SigStatus_Unknown;
|
|
}
|
|
} else {
|
|
// Signature is valid.
|
|
entry->tmd.sig_status = RVTH_SigStatus_OK;
|
|
}
|
|
}
|
|
|
|
// Get the required IOS version.
|
|
if (be32_to_cpu(tmdHeader->sys_version.hi) == 1) {
|
|
uint32_t ios_tid_lo = be32_to_cpu(tmdHeader->sys_version.lo);
|
|
if (ios_tid_lo < 256) {
|
|
entry->ios_version = (uint8_t)ios_tid_lo;
|
|
}
|
|
}
|
|
|
|
// If encrypted, check the crypto type.
|
|
// NOTE: Retail + unencrypted is usually an error.
|
|
// FIXME: Common key index seems to have wacky values in homebrew WADs.
|
|
// - USB Loader GX UNEO Forwarder v5.1: 0x06
|
|
// - Nintendont forwarder: 0xB7
|
|
if (entry->crypto_type != RVTH_CryptoType_None) {
|
|
switch (entry->ticket.sig_type) {
|
|
case RVTH_SigType_Retail:
|
|
// Retail may be either Common Key or Korean Key.
|
|
switch (header.ticket.common_key_index) {
|
|
case 0:
|
|
entry->crypto_type = RVTH_CryptoType_Retail;
|
|
break;
|
|
case 1:
|
|
entry->crypto_type = RVTH_CryptoType_Korean;
|
|
break;
|
|
default:
|
|
entry->crypto_type = RVTH_CryptoType_Unknown;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case RVTH_SigType_Debug:
|
|
// There's only one debug key.
|
|
if (header.ticket.common_key_index == 0) {
|
|
entry->crypto_type = RVTH_CryptoType_Debug;
|
|
} else {
|
|
entry->crypto_type = RVTH_CryptoType_Unknown;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Should not happen...
|
|
entry->crypto_type = RVTH_CryptoType_Unknown;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Initialize an RVT-H bank entry from an opened HDD image.
|
|
* @param entry [out] RvtH_BankEntry
|
|
* @param f_img [in] RefFile*
|
|
* @param type [in] Bank type. (See RvtH_BankType_e.)
|
|
* @param lba_start [in] Starting LBA.
|
|
* @param lba_len [in] Length, in LBAs.
|
|
* @param nhcd_timestamp [in] Timestamp string pointer from the bank table.
|
|
* @return 0 on success; negative POSIX error code on error.
|
|
*/
|
|
static int rvth_init_BankEntry(RvtH_BankEntry *entry, RefFile *f_img,
|
|
uint8_t type, uint32_t lba_start, uint32_t lba_len,
|
|
const char *nhcd_timestamp)
|
|
{
|
|
uint32_t reader_lba_len;
|
|
bool isDeleted;
|
|
|
|
int ret; // errno or RvtH_Errors
|
|
|
|
// Initialize the standard properties.
|
|
memset(entry, 0, sizeof(*entry));
|
|
entry->lba_start = lba_start;
|
|
entry->lba_len = lba_len;
|
|
entry->type = type;
|
|
entry->timestamp = -1; // Default to "no timestamp".
|
|
|
|
assert(entry->type < RVTH_BankType_MAX);
|
|
if (type == RVTH_BankType_Unknown ||
|
|
type >= RVTH_BankType_MAX)
|
|
{
|
|
// Unknown entry type.
|
|
// Don't bother reading anything else.
|
|
return 0;
|
|
}
|
|
|
|
// Read the GCN disc header.
|
|
// TODO: For non-deleted banks, verify the magic number?
|
|
ret = rvth_disc_header_get(f_img, lba_start, &entry->discHeader, &isDeleted);
|
|
if (ret < 0) {
|
|
// Error...
|
|
// TODO: Mark the bank as invalid?
|
|
memset(&entry->discHeader, 0, sizeof(entry->discHeader));
|
|
return ret;
|
|
}
|
|
|
|
// If the original bank type is empty, or the disc header is zeroed,
|
|
// this bank is deleted.
|
|
entry->is_deleted = (isDeleted | (type == RVTH_BankType_Empty && ret >= RVTH_BankType_GCN));
|
|
if (entry->is_deleted) {
|
|
// Bank type was determined by rvth_disc_header_get().
|
|
type = (uint8_t)ret;
|
|
entry->type = type;
|
|
}
|
|
|
|
// Determine the maximum LBA length for the Reader:
|
|
// - GCN or Wii SL: Full bank size.
|
|
// - Wii DL: Dual-layer bank size.
|
|
// - First bank in extended bank table: Smaller bank size.
|
|
if (lba_start < NHCD_BANKTABLE_ADDRESS_LBA) {
|
|
// Bank starts before the bank table.
|
|
// This is a relocated Bank 1 on a device with
|
|
// an extended bank table, so it can only support
|
|
// GCN disc images.
|
|
reader_lba_len = NHCD_EXTBANKTABLE_BANK_1_SIZE_LBA;
|
|
} else {
|
|
// Use the default LBA length based on bank type.
|
|
switch (type) {
|
|
default:
|
|
case RVTH_BankType_Empty:
|
|
case RVTH_BankType_Unknown:
|
|
case RVTH_BankType_GCN:
|
|
case RVTH_BankType_Wii_SL:
|
|
case RVTH_BankType_Wii_DL_Bank2:
|
|
// Full bank.
|
|
reader_lba_len = NHCD_BANK_WII_SL_SIZE_RVTR_LBA;
|
|
break;
|
|
|
|
case RVTH_BankType_Wii_DL:
|
|
// Dual-layer bank.
|
|
reader_lba_len = NHCD_BANK_WII_DL_SIZE_RVTR_LBA;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lba_len == 0) {
|
|
// Empty bank. Assume the length matches the bank,
|
|
// except for GameCube.
|
|
lba_len = (type == RVTH_BankType_GCN)
|
|
? NHCD_BANK_GCN_SIZE_NR_LBA
|
|
: reader_lba_len;
|
|
}
|
|
|
|
// Set the bank entry's LBA length.
|
|
entry->lba_len = lba_len;
|
|
|
|
// Initialize the disc image reader.
|
|
entry->reader = reader_open(f_img, lba_start, reader_lba_len);
|
|
|
|
if (type == RVTH_BankType_Empty) {
|
|
// We're done here.
|
|
return 0;
|
|
}
|
|
|
|
// Parse the timestamp.
|
|
if (nhcd_timestamp) {
|
|
entry->timestamp = rvth_timestamp_parse(nhcd_timestamp);
|
|
}
|
|
|
|
// TODO: Error handling.
|
|
// Initialize the region code.
|
|
rvth_init_BankEntry_region(entry);
|
|
// Initialize the encryption status.
|
|
rvth_init_BankEntry_crypto(entry);
|
|
|
|
// We're done here.
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get a string description of an error number.
|
|
* - Negative: POSIX error. (strerror())
|
|
* - Positive; RVT-H error. (RvtH_Errors)
|
|
* @param err Error number.
|
|
* @return String description.
|
|
*/
|
|
const char *rvth_error(int err)
|
|
{
|
|
// TODO: Update functions to only return POSIX error codes
|
|
// for system-level issues. For anything weird encountered
|
|
// within an RVT-H HDD or GCN/Wii disc image, an
|
|
// RvtH_Errors code should be retured instead.
|
|
static const char *const errtbl[] = {
|
|
// tr: RVTH_ERROR_SUCCESS
|
|
"Success",
|
|
// tr: RVTH_ERROR_UNRECOGNIZED_FILE
|
|
"Unrecognized file format",
|
|
// tr: RVTH_ERROR_NHCD_TABLE_MAGIC
|
|
"Bank table magic is incorrect",
|
|
// tr: RVTH_ERROR_NO_BANKS
|
|
"No banks found",
|
|
// tr: RVTH_ERROR_BANK_UNKNOWN
|
|
"Bank status is unknown",
|
|
// tr: RVTH_ERROR_BANK_EMPTY
|
|
"Bank is empty",
|
|
// tr: RVTH_ERROR_BANK_DL_2
|
|
"Bank is second bank of a dual-layer image",
|
|
// tr: RVTH_ERROR_NOT_A_DEVICE
|
|
"Operation can only be performed on a device, not an image file",
|
|
// tr: RVTH_ERROR_BANK_IS_DELETED
|
|
"Bank is deleted",
|
|
// tr: RVTH_ERROR_BANK_NOT_DELETED
|
|
"Bank is not deleted",
|
|
// tr: RVTH_ERROR_NOT_HDD_IMAGE
|
|
"RVT-H object is not an HDD image",
|
|
// tr: RVTH_ERROR_NO_GAME_PARTITION
|
|
"Wii game partition not found",
|
|
// tr: RVTH_ERROR_INVALID_BANK_COUNT
|
|
"RVT-H bank count field is invalid",
|
|
// tr: RVTH_ERROR_IS_HDD_IMAGE
|
|
"Operation cannot be performed on devices or HDD images",
|
|
// tr: RVTH_ERROR_IS_RETAIL_CRYPTO
|
|
"Cannot import a retail-encrypted Wii game",
|
|
// tr: RVTH_ERROR_IMAGE_TOO_BIG
|
|
"Source image does not fit in an RVT-H bank",
|
|
// tr: RVTH_ERROR_BANK_NOT_EMPTY_OR_DELETED
|
|
"Destination bank is not empty or deleted",
|
|
// tr: RVTH_ERROR_NOT_WII_IMAGE
|
|
"Wii-specific operation was requested on a non-Wii image",
|
|
// tr: RVTH_ERROR_IS_UNENCRYPTED
|
|
"Image is unencrypted",
|
|
// tr: RVTH_ERROR_IS_ENCRYPTED
|
|
"Image is encrypted",
|
|
// tr: RVTH_ERROR_PARTITION_TABLE_CORRUPTED
|
|
"Wii partition table is corrupted",
|
|
// tr: RVTH_ERROR_PARTITION_HEADER_CORRUPTED
|
|
"At least one Wii partition header is corrupted",
|
|
// tr: RVTH_ERROR_ISSUER_UNKNOWN
|
|
"Certificate has an unknown issuer",
|
|
|
|
// 'import' command: Dual-Layer errors.
|
|
|
|
// tr: RVTH_ERROR_IMPORT_DL_EXT_NO_BANK1
|
|
"Extended Bank Table: Cannot use Bank 1 for a Dual-Layer image.",
|
|
// tr: RVTH_ERROR_IMPORT_DL_LAST_BANK
|
|
"Cannot use the last bank for a Dual-Layer image",
|
|
// tr: RVTH_ERROR_BANK2DL_NOT_EMPTY_OR_DELETED
|
|
"The second bank for the Dual-Layer image is not empty or deleted",
|
|
// tr: RVTH_ERROR_IMPORT_DL_NOT_CONTIGUOUS
|
|
"The two banks are not contiguous",
|
|
|
|
// NDEV option.
|
|
|
|
// tr: RVTH_ERROR_NDEV_GCN_NOT_SUPPORTED
|
|
"NDEV headers for GCN are currently unsupported.",
|
|
};
|
|
static_assert(ARRAY_SIZE(errtbl) == RVTH_ERROR_MAX, "Missing error descriptions!");
|
|
|
|
if (err < 0) {
|
|
return strerror(-err);
|
|
} else if (err >= ARRAY_SIZE(errtbl)) {
|
|
return "(unknown)";
|
|
}
|
|
|
|
return errtbl[err];
|
|
}
|
|
|
|
/**
|
|
* Open a Wii or GameCube disc image.
|
|
* @param f_img [in] RefFile*
|
|
* @param pErr [out,opt] Error code. (If negative, POSIX error; otherwise, see RvtH_Errors.)
|
|
* @return RvtH struct pointer if the file is a supported image; NULL on error. (check errno)
|
|
*/
|
|
static RvtH *rvth_open_gcm(RefFile *f_img, int *pErr)
|
|
{
|
|
RvtH *rvth = NULL;
|
|
RvtH_BankEntry *entry;
|
|
int ret = 0; // errno or RvtH_Errors
|
|
int err = 0; // errno setting
|
|
|
|
Reader *reader = NULL;
|
|
int64_t len;
|
|
uint8_t type;
|
|
|
|
// Disc header.
|
|
union {
|
|
GCN_DiscHeader gcn;
|
|
uint8_t sbuf[LBA_SIZE];
|
|
} discHeader;
|
|
|
|
// TODO: Detect CISO and WBFS.
|
|
|
|
// Get the file length.
|
|
// FIXME: This is obtained in rvth_open().
|
|
// Pass it as a parameter?
|
|
ret = ref_seeko(f_img, 0, SEEK_END);
|
|
if (ret != 0) {
|
|
// Seek error.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = EIO;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
len = ref_tello(f_img);
|
|
|
|
// Rewind back to the beginning of the file.
|
|
ret = ref_seeko(f_img, 0, SEEK_SET);
|
|
if (ret != 0) {
|
|
// Seek error.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = EIO;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
|
|
// Initialize the disc image reader.
|
|
// We need to do this before anything else in order to
|
|
// handle CISO and WBFS images.
|
|
reader = reader_open(f_img, 0, BYTES_TO_LBA(len));
|
|
if (!reader) {
|
|
// Unable to open the reader.
|
|
goto fail;
|
|
}
|
|
|
|
// Read the GCN disc header.
|
|
// NOTE: Since this is a standalone disc image, we'll just
|
|
// read the header directly.
|
|
ret = reader_read(reader, discHeader.sbuf, 0, 1);
|
|
if (ret < 0) {
|
|
// Error...
|
|
err = -ret;
|
|
goto fail;
|
|
}
|
|
|
|
// Identify the disc type.
|
|
type = rvth_disc_header_identify(&discHeader.gcn);
|
|
if (type == RVTH_BankType_Wii_SL &&
|
|
reader->lba_len > NHCD_BANK_WII_SL_SIZE_RVTR_LBA)
|
|
{
|
|
// Dual-layer image.
|
|
type = RVTH_BankType_Wii_DL;
|
|
}
|
|
|
|
// Allocate memory for the RvtH object
|
|
rvth = calloc(1, sizeof(RvtH));
|
|
if (!rvth) {
|
|
// Error allocating memory.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = ENOMEM; // NOTE: Standalone
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
|
|
// Allocate memory for a single RvtH_BankEntry object.
|
|
rvth->bank_count = 1;
|
|
rvth->is_hdd = false;
|
|
rvth->entries = calloc(1, sizeof(RvtH_BankEntry));
|
|
if (!rvth->entries) {
|
|
// Error allocating memory.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = ENOMEM;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
};
|
|
|
|
// Initialize the bank entry.
|
|
// NOTE: Not using rvth_init_BankEntry() here.
|
|
rvth->f_img = ref_dup(f_img);
|
|
entry = rvth->entries;
|
|
entry->lba_start = reader->lba_start;
|
|
entry->lba_len = reader->lba_len;
|
|
entry->type = type;
|
|
entry->is_deleted = false;
|
|
entry->reader = reader;
|
|
|
|
// Timestamp.
|
|
// TODO: Get the timestamp from the file.
|
|
entry->timestamp = -1;
|
|
|
|
if (type != RVTH_BankType_Empty) {
|
|
// Copy the disc header.
|
|
memcpy(&entry->discHeader, &discHeader.gcn, sizeof(entry->discHeader));
|
|
|
|
// TODO: Error handling.
|
|
// Initialize the region code.
|
|
rvth_init_BankEntry_region(entry);
|
|
// Initialize the encryption status.
|
|
rvth_init_BankEntry_crypto(entry);
|
|
}
|
|
|
|
// Disc image loaded.
|
|
return rvth;
|
|
|
|
fail:
|
|
// Failed to open the disc image.
|
|
if (reader) {
|
|
reader_close(reader);
|
|
}
|
|
|
|
rvth_close(rvth);
|
|
if (pErr) {
|
|
*pErr = ret;
|
|
}
|
|
if (err != 0) {
|
|
errno = err;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Open an RVT-H disk image.
|
|
* @param f_img [in] RefFile*
|
|
* @param pErr [out,opt] Error code. (If negative, POSIX error; otherwise, see RvtH_Errors.)
|
|
* @return RvtH struct pointer if the file is a supported image; NULL on error. (check errno)
|
|
*/
|
|
static RvtH *rvth_open_hdd(RefFile *f_img, int *pErr)
|
|
{
|
|
NHCD_BankTable_Header nhcd_header;
|
|
RvtH *rvth = NULL;
|
|
RvtH_BankEntry *rvth_entry;
|
|
int ret = 0; // errno or RvtH_Errors
|
|
int err = 0; // errno setting
|
|
|
|
unsigned int i;
|
|
int64_t addr;
|
|
size_t size;
|
|
|
|
// Check the bank table header.
|
|
ret = ref_seeko(f_img, LBA_TO_BYTES(NHCD_BANKTABLE_ADDRESS_LBA), SEEK_SET);
|
|
if (ret != 0) {
|
|
// Seek error.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = EIO;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
size = ref_read(&nhcd_header, 1, sizeof(nhcd_header), f_img);
|
|
if (size != sizeof(nhcd_header)) {
|
|
// Short read.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = EIO;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
|
|
// Check the magic number.
|
|
if (nhcd_header.magic != be32_to_cpu(NHCD_BANKTABLE_MAGIC)) {
|
|
// Incorrect magic number.
|
|
err = EIO;
|
|
ret = RVTH_ERROR_NHCD_TABLE_MAGIC;
|
|
goto fail;
|
|
}
|
|
|
|
// Allocate memory for the RvtH object
|
|
rvth = calloc(1, sizeof(RvtH));
|
|
if (!rvth) {
|
|
// Error allocating memory.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = ENOMEM;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
|
|
// Get the bank count.
|
|
rvth->bank_count = be32_to_cpu(nhcd_header.bank_count);
|
|
if (rvth->bank_count < 8 || rvth->bank_count > 32) {
|
|
// Bank count is either too small or too large.
|
|
// RVT-H systems are set to 8 banks at the factory,
|
|
// but we're supporting up to 32 in case the user
|
|
// has modified it.
|
|
// TODO: More extensive "extra bank" testing.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = ENOMEM;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
|
|
// Allocate memory for the 8 RvtH_BankEntry objects.
|
|
rvth->is_hdd = true;
|
|
rvth->entries = calloc(rvth->bank_count, sizeof(RvtH_BankEntry));
|
|
if (!rvth->entries) {
|
|
// Error allocating memory.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = ENOMEM;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
};
|
|
|
|
rvth->f_img = ref_dup(f_img);
|
|
rvth_entry = rvth->entries;
|
|
addr = (uint32_t)(LBA_TO_BYTES(NHCD_BANKTABLE_ADDRESS_LBA) + NHCD_BLOCK_SIZE);
|
|
for (i = 0; i < rvth->bank_count; i++, rvth_entry++, addr += 512) {
|
|
NHCD_BankEntry nhcd_entry;
|
|
uint32_t lba_start = 0, lba_len = 0;
|
|
uint8_t type = RVTH_BankType_Unknown;
|
|
|
|
if (i > 0 && (rvth_entry-1)->type == RVTH_BankType_Wii_DL) {
|
|
// Second bank for a dual-layer Wii image.
|
|
rvth_entry->type = RVTH_BankType_Wii_DL_Bank2;
|
|
rvth_entry->timestamp = -1;
|
|
continue;
|
|
}
|
|
|
|
ret = ref_seeko(f_img, addr, SEEK_SET);
|
|
if (ret != 0) {
|
|
// Seek error.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = EIO;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
size = ref_read(&nhcd_entry, 1, sizeof(nhcd_entry), f_img);
|
|
if (size != sizeof(nhcd_entry)) {
|
|
// Short read.
|
|
err = errno;
|
|
if (err == 0) {
|
|
err = EIO;
|
|
}
|
|
ret = -err;
|
|
goto fail;
|
|
}
|
|
|
|
// Check the type.
|
|
switch (be32_to_cpu(nhcd_entry.type)) {
|
|
default:
|
|
// Unknown bank type...
|
|
type = RVTH_BankType_Unknown;
|
|
break;
|
|
case NHCD_BankType_Empty:
|
|
// "Empty" bank. May have a deleted image.
|
|
type = RVTH_BankType_Empty;
|
|
break;
|
|
case NHCD_BankType_GCN:
|
|
// GameCube
|
|
type = RVTH_BankType_GCN;
|
|
break;
|
|
case NHCD_BankType_Wii_SL:
|
|
// Wii (single-layer)
|
|
type = RVTH_BankType_Wii_SL;
|
|
break;
|
|
case NHCD_BankType_Wii_DL:
|
|
// Wii (dual-layer)
|
|
// TODO: Cannot start in Bank 8.
|
|
type = RVTH_BankType_Wii_DL;
|
|
break;
|
|
}
|
|
|
|
// For valid types, use the listed LBAs if they're non-zero.
|
|
if (type >= RVTH_BankType_GCN) {
|
|
lba_start = be32_to_cpu(nhcd_entry.lba_start);
|
|
lba_len = be32_to_cpu(nhcd_entry.lba_len);
|
|
}
|
|
|
|
if (lba_start == 0 || lba_len == 0) {
|
|
// Invalid LBAs. Use the default starting offset.
|
|
// Bank size will be determined by rvth_init_BankEntry().
|
|
lba_start = NHCD_BANK_START_LBA(i, rvth->bank_count);
|
|
lba_len = 0;
|
|
}
|
|
|
|
// Initialize the bank entry.
|
|
rvth_init_BankEntry(rvth_entry, f_img, type,
|
|
lba_start, lba_len, nhcd_entry.timestamp);
|
|
}
|
|
|
|
// RVT-H image loaded.
|
|
return rvth;
|
|
|
|
fail:
|
|
// Failed to open the HDD image.
|
|
rvth_close(rvth);
|
|
if (pErr) {
|
|
*pErr = ret;
|
|
}
|
|
if (err != 0) {
|
|
errno = err;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Open an RVT-H disk image, GameCube disc image, or Wii disc image.
|
|
* @param filename [in] Filename.
|
|
* @param pErr [out,opt] Error code. (If negative, POSIX error; otherwise, see RvtH_Errors.)
|
|
* @return RvtH struct pointer if the file is a supported image; NULL on error. (check errno)
|
|
*/
|
|
RvtH *rvth_open(const TCHAR *filename, int *pErr)
|
|
{
|
|
RefFile *f_img;
|
|
RvtH *rvth = NULL;
|
|
int ret = 0;
|
|
int64_t len;
|
|
|
|
// Open the disk image.
|
|
f_img = ref_open(filename);
|
|
if (!f_img) {
|
|
// Could not open the file.
|
|
if (pErr) {
|
|
*pErr = -errno;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Determine if this is an HDD image or a disc image.
|
|
ret = ref_seeko(f_img, 0, SEEK_END);
|
|
if (ret != 0) {
|
|
// Seek error.
|
|
if (errno == 0) {
|
|
errno = EIO;
|
|
}
|
|
goto end;
|
|
}
|
|
len = ref_tello(f_img);
|
|
if (len == 0) {
|
|
// File is empty.
|
|
errno = EIO;
|
|
} else if (len <= 2*LBA_TO_BYTES(NHCD_BANK_SIZE_LBA)) {
|
|
// Two banks or less.
|
|
// This is most likely a standalone disc image.
|
|
rvth = rvth_open_gcm(f_img, pErr);
|
|
} else {
|
|
// More than two banks.
|
|
// This is most likely an RVT-H HDD image.
|
|
rvth = rvth_open_hdd(f_img, pErr);
|
|
}
|
|
|
|
end:
|
|
if (pErr) {
|
|
*pErr = -errno;
|
|
}
|
|
// If the RvtH object was opened, it will have
|
|
// called ref_dup() to increment the reference count.
|
|
ref_close(f_img);
|
|
return rvth;
|
|
}
|
|
|
|
/**
|
|
* Close an opened RVT-H disk image.
|
|
* @param rvth RVT-H disk image.
|
|
*/
|
|
void rvth_close(RvtH *rvth)
|
|
{
|
|
unsigned int i;
|
|
|
|
if (!rvth)
|
|
return;
|
|
|
|
// Close all bank entry files.
|
|
// RefFile has a reference count, so we have to clear the count.
|
|
for (i = 0; i < rvth->bank_count; i++) {
|
|
if (rvth->entries[i].reader) {
|
|
reader_close(rvth->entries[i].reader);
|
|
}
|
|
free(rvth->entries[i].ptbl);
|
|
}
|
|
|
|
// Free the bank entries array.
|
|
free(rvth->entries);
|
|
|
|
// Clear the main reference.
|
|
if (rvth->f_img) {
|
|
ref_close(rvth->f_img);
|
|
}
|
|
|
|
free(rvth);
|
|
}
|
|
|
|
/**
|
|
* Is this RVT-H object an HDD image or a standalone disc image?
|
|
* @param rvth RVT-H object.
|
|
* @return True if the RVT-H object is an HDD image; false if it's a standalone disc image.
|
|
*/
|
|
bool rvth_is_hdd(const RvtH *rvth)
|
|
{
|
|
if (!rvth) {
|
|
errno = EINVAL;
|
|
return false;
|
|
}
|
|
return rvth->is_hdd;
|
|
}
|
|
|
|
/**
|
|
* Get the number of banks in an opened RVT-H disk image.
|
|
* @param rvth RVT-H disk image.
|
|
* @return Number of banks.
|
|
*/
|
|
unsigned int rvth_get_BankCount(const RvtH *rvth)
|
|
{
|
|
if (!rvth) {
|
|
errno = EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
return rvth->bank_count;
|
|
}
|
|
|
|
/**
|
|
* Get a bank table entry.
|
|
* @param rvth [in] RVT-H disk image.
|
|
* @param bank [in] Bank number. (0-7)
|
|
* @param pErr [out,opt] Error code. (If negative, POSIX error; otherwise, see RvtH_Errors.)
|
|
* @return Bank table entry, or NULL if out of range.
|
|
*/
|
|
const RvtH_BankEntry *rvth_get_BankEntry(const RvtH *rvth, unsigned int bank, int *pErr)
|
|
{
|
|
if (!rvth) {
|
|
errno = EINVAL;
|
|
if (pErr) {
|
|
*pErr = -EINVAL;
|
|
}
|
|
return NULL;
|
|
} else if (bank >= rvth->bank_count) {
|
|
errno = ERANGE;
|
|
if (pErr) {
|
|
*pErr = -EINVAL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
return &rvth->entries[bank];
|
|
}
|