Files
sphaira/sphaira/source/yati/yati.cpp
2025-05-11 03:00:04 +01:00

1489 lines
57 KiB
C++

#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include "yati/source/stream_file.hpp"
#include "yati/source/stdio.hpp"
#include "yati/container/nsp.hpp"
#include "yati/container/xci.hpp"
#include "yati/nx/ncz.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
#include "yati/nx/ns.hpp"
#include "yati/nx/es.hpp"
#include "yati/nx/keys.hpp"
#include "yati/nx/crypto.hpp"
#include "ui/progress_box.hpp"
#include "app.hpp"
#include "i18n.hpp"
#include "log.hpp"
#include <zstd.h>
#include <minIni.h>
#include <algorithm>
namespace sphaira::yati {
namespace {
constexpr NcmStorageId NCM_STORAGE_IDS[]{
NcmStorageId_BuiltInUser,
NcmStorageId_SdCard,
};
constexpr u32 KEYGEN_LIMIT = 0x20;
struct NcaCollection : container::CollectionEntry {
nca::Header header{};
// NcmContentType
u8 type{};
NcmContentId content_id{};
NcmPlaceHolderId placeholder_id{};
// new hash of the nca..
u8 hash[SHA256_HASH_SIZE]{};
// set true if nca has been modified.
bool modified{};
// set if the nca was not installed.
bool skipped{};
};
struct CnmtCollection : NcaCollection {
// list of all nca's the cnmt depends on
std::vector<NcaCollection> ncas{};
// only set if any of the nca's depend on a ticket / cert.
// if set, the ticket / cert will be installed once all nca's have installed.
std::vector<FsRightsId> rights_id{};
NcmContentMetaHeader meta_header{};
NcmContentMetaKey key{};
NcmContentInfo content_info{};
std::vector<u8> extended_header{};
std::vector<NcmPackagedContentInfo> infos{};
};
struct TikCollection {
// raw data of the ticket / cert.
std::vector<u8> ticket{};
std::vector<u8> cert{};
// set via the name of the ticket.
FsRightsId rights_id{};
// set if ticket is required by an nca.
bool required{};
};
struct Yati;
const u64 INFLATE_BUFFER_MAX = 1024*1024*4;
struct ThreadBuffer {
ThreadBuffer() {
buf.reserve(INFLATE_BUFFER_MAX);
}
std::vector<u8> buf;
s64 off;
};
template<std::size_t Size>
struct RingBuf {
private:
ThreadBuffer buf[Size]{};
unsigned r_index{};
unsigned w_index{};
static_assert((sizeof(RingBuf::buf) & (sizeof(RingBuf::buf) - 1)) == 0, "Must be power of 2!");
public:
void ringbuf_reset() {
this->r_index = this->w_index;
}
unsigned ringbuf_capacity() const {
return sizeof(this->buf) / sizeof(this->buf[0]);
}
unsigned ringbuf_size() const {
return (this->w_index - this->r_index) % (ringbuf_capacity() * 2U);
}
unsigned ringbuf_free() const {
return ringbuf_capacity() - ringbuf_size();
}
void ringbuf_push(std::vector<u8>& buf_in, s64 off_in) {
auto& value = this->buf[this->w_index % ringbuf_capacity()];
value.off = off_in;
std::swap(value.buf, buf_in);
this->w_index = (this->w_index + 1U) % (ringbuf_capacity() * 2U);
}
void ringbuf_pop(std::vector<u8>& buf_out, s64& off_out) {
auto& value = this->buf[this->r_index % ringbuf_capacity()];
off_out = value.off;
std::swap(value.buf, buf_out);
this->r_index = (this->r_index + 1U) % (ringbuf_capacity() * 2U);
}
};
struct ThreadData {
ThreadData(Yati* _yati, std::span<TikCollection> _tik, NcaCollection* _nca)
: yati{_yati}, tik{_tik}, nca{_nca} {
mutexInit(std::addressof(read_mutex));
mutexInit(std::addressof(write_mutex));
condvarInit(std::addressof(can_read));
condvarInit(std::addressof(can_decompress));
condvarInit(std::addressof(can_decompress_write));
condvarInit(std::addressof(can_write));
sha256ContextCreate(&sha256);
// this will be updated with the actual size from nca header.
write_size = nca->size;
read_buffer_size = 1024*1024*4;
max_buffer_size = std::max(read_buffer_size, INFLATE_BUFFER_MAX);
}
auto GetResults() -> Result;
void WakeAllThreads();
Result Read(void* buf, s64 size, u64* bytes_read);
Result SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size) {
buf.resize(size);
mutexLock(std::addressof(read_mutex));
if (!read_buffers.ringbuf_free()) {
R_TRY(condvarWait(std::addressof(can_read), std::addressof(read_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex)));
R_TRY(GetResults());
read_buffers.ringbuf_push(buf, off);
return condvarWakeOne(std::addressof(can_decompress));
}
Result GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out) {
mutexLock(std::addressof(read_mutex));
if (!read_buffers.ringbuf_size()) {
R_TRY(condvarWait(std::addressof(can_decompress), std::addressof(read_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex)));
R_TRY(GetResults());
read_buffers.ringbuf_pop(buf_out, off_out);
return condvarWakeOne(std::addressof(can_read));
}
Result SetWriteBuf(std::vector<u8>& buf, s64 size, bool skip_verify) {
buf.resize(size);
if (!skip_verify) {
sha256ContextUpdate(std::addressof(sha256), buf.data(), buf.size());
}
mutexLock(std::addressof(write_mutex));
if (!write_buffers.ringbuf_free()) {
R_TRY(condvarWait(std::addressof(can_decompress_write), std::addressof(write_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex)));
R_TRY(GetResults());
write_buffers.ringbuf_push(buf, 0);
return condvarWakeOne(std::addressof(can_write));
}
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) {
mutexLock(std::addressof(write_mutex));
if (!write_buffers.ringbuf_size()) {
R_TRY(condvarWait(std::addressof(can_write), std::addressof(write_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex)));
R_TRY(GetResults());
write_buffers.ringbuf_pop(buf_out, off_out);
return condvarWakeOne(std::addressof(can_decompress_write));
}
// these need to be copied
Yati* yati{};
std::span<TikCollection> tik{};
NcaCollection* nca{};
// these need to be created
Mutex read_mutex{};
Mutex write_mutex{};
CondVar can_read{};
CondVar can_decompress{};
CondVar can_decompress_write{};
CondVar can_write{};
RingBuf<4> read_buffers{};
RingBuf<4> write_buffers{};
ncz::BlockHeader ncz_block_header{};
std::vector<ncz::Section> ncz_sections{};
std::vector<ncz::BlockInfo> ncz_blocks{};
Sha256Context sha256{};
u64 read_buffer_size{};
u64 max_buffer_size{};
// these are shared between threads
volatile s64 read_offset{};
volatile s64 decompress_offset{};
volatile s64 write_offset{};
volatile s64 write_size{};
volatile Result read_result{};
volatile Result decompress_result{};
volatile Result write_result{};
};
struct Yati {
Yati(ui::ProgressBox*, std::shared_ptr<source::Base>);
~Yati();
Result Setup(const ConfigOverride& override);
Result InstallNca(std::span<TikCollection> tickets, NcaCollection& nca);
Result InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca);
Result InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections);
Result readFuncInternal(ThreadData* t);
Result decompressFuncInternal(ThreadData* t);
Result writeFuncInternal(ThreadData* t);
Result ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, const container::Collections& collections, bool read_data);
Result GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool& skip);
Result ShouldSkip(const CnmtCollection& cnmt, bool& skip);
Result ImportTickets(std::span<TikCollection> tickets);
Result RemoveInstalledNcas(const CnmtCollection& cnmt);
Result RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_version_num);
// private:
ui::ProgressBox* pbox{};
std::shared_ptr<source::Base> source{};
// for all content storages
NcmContentStorage ncm_cs[2]{};
NcmContentMetaDatabase ncm_db[2]{};
// these point to the above struct
NcmContentStorage cs{};
NcmContentMetaDatabase db{};
NcmStorageId storage_id{};
Service es{};
Service ns_app{};
std::unique_ptr<container::Base> container{};
Config config{};
keys::Keys keys{};
};
auto ThreadData::GetResults() -> Result {
R_UNLESS(!yati->pbox->ShouldExit(), Result_Cancelled);
R_TRY(read_result);
R_TRY(decompress_result);
R_TRY(write_result);
R_SUCCEED();
}
void ThreadData::WakeAllThreads() {
condvarWakeAll(std::addressof(can_read));
condvarWakeAll(std::addressof(can_decompress));
condvarWakeAll(std::addressof(can_decompress_write));
condvarWakeAll(std::addressof(can_write));
mutexUnlock(std::addressof(read_mutex));
mutexUnlock(std::addressof(write_mutex));
}
Result ThreadData::Read(void* buf, s64 size, u64* bytes_read) {
size = std::min<s64>(size, nca->size - read_offset);
const auto rc = yati->source->Read(buf, nca->offset + read_offset, size, bytes_read);
read_offset += *bytes_read;
R_UNLESS(size == *bytes_read, Result_InvalidNcaReadSize);
return rc;
}
auto isRightsIdValid(FsRightsId id) -> bool {
FsRightsId empty_id{};
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id));
}
auto getKeyGenFromRightsId(FsRightsId id) -> u8 {
return id.c[sizeof(id) - 1];
}
struct HashStr {
char str[0x21];
};
HashStr hexIdToStr(auto id) {
HashStr str{};
const auto id_lower = std::byteswap(*(u64*)id.c);
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
return str;
}
// read thread reads all data from the source, it also handles
// parsing ncz headers, sections and reading ncz blocks
Result Yati::readFuncInternal(ThreadData* t) {
// the main buffer which data is read into.
std::vector<u8> buf;
// workaround ncz block reading ahead. if block isn't found, we usually
// would seek back to the offset, however this is not possible in stream
// mode, so we instead store the data to the temp buffer and pre-pend it.
std::vector<u8> temp_buf;
buf.reserve(t->max_buffer_size);
temp_buf.reserve(t->max_buffer_size);
while (t->read_offset < t->nca->size && R_SUCCEEDED(t->GetResults())) {
const auto buffer_offset = t->read_offset;
// read more data
s64 read_size = t->read_buffer_size;
if (!t->read_offset) {
read_size = NCZ_SECTION_OFFSET;
}
s64 buf_offset = 0;
if (!temp_buf.empty()) {
buf = temp_buf;
read_size -= temp_buf.size();
buf_offset = temp_buf.size();
temp_buf.clear();
}
u64 bytes_read{};
buf.resize(buf_offset + read_size);
R_TRY(t->Read(buf.data() + buf_offset, read_size, std::addressof(bytes_read)));
auto buf_size = buf_offset + bytes_read;
// read enough bytes for ncz, check magic
if (t->read_offset == NCZ_SECTION_OFFSET) {
// check for ncz section header.
ncz::Header header{};
std::memcpy(std::addressof(header), buf.data() + 0x4000, sizeof(header));
if (header.magic == NCZ_SECTION_MAGIC) {
// validate section header.
R_UNLESS(header.total_sections, Result_InvalidNczSectionCount);
buf_size = 0x4000;
log_write("found ncz, total number of sections: %zu\n", header.total_sections);
t->ncz_sections.resize(header.total_sections);
R_TRY(t->Read(t->ncz_sections.data(), t->ncz_sections.size() * sizeof(ncz::Section), std::addressof(bytes_read)));
// check for ncz block header.
R_TRY(t->Read(std::addressof(t->ncz_block_header), sizeof(t->ncz_block_header), std::addressof(bytes_read)));
if (t->ncz_block_header.magic != NCZ_BLOCK_MAGIC) {
// didn't find block, keep the data we just read in the temp buffer.
temp_buf.resize(sizeof(t->ncz_block_header));
std::memcpy(temp_buf.data(), std::addressof(t->ncz_block_header), temp_buf.size());
log_write("storing temp data of size: %zu\n", temp_buf.size());
} else {
// validate block header.
R_UNLESS(t->ncz_block_header.version == 0x2, Result_InvalidNczBlockVersion);
R_UNLESS(t->ncz_block_header.type == 0x1, Result_InvalidNczBlockType);
R_UNLESS(t->ncz_block_header.total_blocks, Result_InvalidNczBlockTotal);
R_UNLESS(t->ncz_block_header.block_size_exponent >= 14 && t->ncz_block_header.block_size_exponent <= 32, Result_InvalidNczBlockSizeExponent);
// read blocks (array of block sizes).
std::vector<ncz::Block> blocks(t->ncz_block_header.total_blocks);
R_TRY(t->Read(blocks.data(), blocks.size() * sizeof(ncz::Block), std::addressof(bytes_read)));
// calculate offsets for each block.
auto block_offset = t->read_offset;
for (const auto& block : blocks) {
t->ncz_blocks.emplace_back(block_offset, block.size);
block_offset += block.size;
}
}
}
}
R_TRY(t->SetDecompressBuf(buf, buffer_offset, buf_size));
}
log_write("read success\n");
R_SUCCEED();
}
// decompress thread handles decrypting / modifying the nca header, decompressing ncz
// and calculating the running sha256.
Result Yati::decompressFuncInternal(ThreadData* t) {
// only used for ncz files.
auto dctx = ZSTD_createDCtx();
ON_SCOPE_EXIT(ZSTD_freeDCtx(dctx));
const auto chunk_size = ZSTD_DStreamOutSize();
const ncz::Section* ncz_section{};
const ncz::BlockInfo* ncz_block{};
bool is_ncz{};
s64 inflate_offset{};
Aes128CtrContext ctx{};
std::vector<u8> inflate_buf{};
inflate_buf.reserve(t->max_buffer_size);
s64 written{};
s64 decompress_buf_off{};
std::vector<u8> buf{};
buf.reserve(t->max_buffer_size);
// encrypts the nca and passes the buffer to the write thread.
const auto ncz_flush = [&](s64 size) -> Result {
if (!inflate_offset) {
R_SUCCEED();
}
// if we are not moving the whole vector, then we need to keep
// the remaining data.
// rather that copying the entire vector to the write thread,
// only copy (store) the remaining amount.
std::vector<u8> temp_vector{};
if (size < inflate_offset) {
temp_vector.resize(inflate_offset - size);
std::memcpy(temp_vector.data(), inflate_buf.data() + size, temp_vector.size());
}
for (s64 off = 0; off < size;) {
// log_write("looking for section\n");
if (!ncz_section || !ncz_section->InRange(written)) {
auto it = std::find_if(t->ncz_sections.cbegin(), t->ncz_sections.cend(), [written](auto& e){
return e.InRange(written);
});
R_UNLESS(it != t->ncz_sections.cend(), Result_NczSectionNotFound);
ncz_section = &(*it);
if (ncz_section->crypto_type >= nca::EncryptionType_AesCtr) {
const auto swp = std::byteswap(u64(written) >> 4);
u8 counter[0x16];
std::memcpy(counter + 0x0, ncz_section->counter, 0x8);
std::memcpy(counter + 0x8, &swp, 0x8);
aes128CtrContextCreate(&ctx, ncz_section->key, counter);
}
}
const auto total_size = ncz_section->offset + ncz_section->size;
const auto chunk_size = std::min<u64>(total_size - written, size - off);
if (ncz_section->crypto_type >= nca::EncryptionType_AesCtr) {
aes128CtrCrypt(&ctx, inflate_buf.data() + off, inflate_buf.data() + off, chunk_size);
}
written += chunk_size;
off += chunk_size;
}
R_TRY(t->SetWriteBuf(inflate_buf, size, config.skip_nca_hash_verify));
inflate_offset -= size;
// restore remaining data to the swapped buffer.
if (!temp_vector.empty()) {
log_write("storing data size: %zu\n", temp_vector.size());
inflate_buf = temp_vector;
}
R_SUCCEED();
};
while (t->decompress_offset < t->write_size && R_SUCCEEDED(t->GetResults())) {
R_TRY(t->GetDecompressBuf(buf, decompress_buf_off));
// do we have an nsz? if so, setup buffers.
if (!is_ncz && !t->ncz_sections.empty()) {
log_write("YES IT FOUND NCZ\n");
is_ncz = true;
}
// if we don't have a ncz or it's before the ncz header, pass buffer directly to write
if (!is_ncz || !decompress_buf_off) {
// check nca header
if (!decompress_buf_off) {
nca::Header header{};
crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false);
log_write("verifying nca header magic\n");
R_UNLESS(header.magic == 0x3341434E, Result_InvalidNcaMagic);
log_write("nca magic is ok! type: %u\n", header.content_type);
// store the unmodified header.
t->nca->header = header;
if (!config.skip_rsa_header_fixed_key_verify) {
log_write("verifying nca fixed key\n");
R_TRY(nca::VerifyFixedKey(header));
log_write("nca fixed key is ok! type: %u\n", header.content_type);
} else {
log_write("skipping nca verification\n");
}
t->write_size = header.size;
R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size));
if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) {
header.distribution_type = nca::DistributionType_System;
t->nca->modified = true;
}
TikCollection* ticket = nullptr;
if (isRightsIdValid(header.rights_id)) {
auto it = std::find_if(t->tik.begin(), t->tik.end(), [&header](auto& e){
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
});
R_UNLESS(it != t->tik.end(), Result_TicketNotFound);
it->required = true;
ticket = &(*it);
}
if ((config.convert_to_standard_crypto && isRightsIdValid(header.rights_id)) || config.lower_master_key) {
t->nca->modified = true;
u8 keak_generation;
if (isRightsIdValid(header.rights_id)) {
const auto key_gen = getKeyGenFromRightsId(header.rights_id);
log_write("converting to standard crypto: 0x%X 0x%X\n", key_gen, header.key_gen);
// fetch ticket data block.
es::TicketData ticket_data;
R_TRY(es::GetTicketData(ticket->ticket, std::addressof(ticket_data)));
// validate that this indeed the correct ticket.
R_UNLESS(!std::memcmp(std::addressof(header.rights_id), std::addressof(ticket_data.rights_id), sizeof(header.rights_id)), Result_InvalidTicketBadRightsId);
// some scene releases use buggy software which set the master key
// revision in the properties bitfield...lol, still happens in 2025.
// to fix this, get mkey rev from the rights id
// todo: verify this code.
if (ticket_data.title_key_type == es::TicketTitleKeyType_Common) {
if (!ticket_data.master_key_revision && ticket_data.master_key_revision != getKeyGenFromRightsId(ticket_data.rights_id) && ticket_data.properties_bitfield) {
// get the actual mkey
ticket_data.master_key_revision = getKeyGenFromRightsId(ticket_data.rights_id);
// unset the properties
ticket_data.properties_bitfield = 0;
}
}
// decrypt title key.
keys::KeyEntry title_key;
R_TRY(es::GetTitleKey(title_key, ticket_data, keys));
R_TRY(es::DecryptTitleKey(title_key, key_gen, keys));
std::memset(header.key_area, 0, sizeof(header.key_area));
std::memcpy(&header.key_area[0x2], &title_key, sizeof(title_key));
keak_generation = key_gen;
ticket->required = false;
} else if (config.lower_master_key) {
R_TRY(nca::DecryptKeak(keys, header));
}
if (config.lower_master_key) {
keak_generation = 0;
}
R_TRY(nca::EncryptKeak(keys, header, keak_generation));
std::memset(&header.rights_id, 0, sizeof(header.rights_id));
}
if (t->nca->modified) {
crypto::cryptoAes128Xts(std::addressof(header), buf.data(), keys.header_key, 0, 0x200, sizeof(header), true);
}
}
written += buf.size();
t->decompress_offset += buf.size();
R_TRY(t->SetWriteBuf(buf, buf.size(), config.skip_nca_hash_verify));
} else if (is_ncz) {
u64 buf_off{};
while (buf_off < buf.size()) {
std::span<const u8> buffer{buf.data() + buf_off, buf.size() - buf_off};
bool compressed = true;
// todo: blocks need to use read offset, as the offset + size is compressed range.
if (t->ncz_blocks.size()) {
if (!ncz_block || !ncz_block->InRange(decompress_buf_off)) {
auto it = std::find_if(t->ncz_blocks.cbegin(), t->ncz_blocks.cend(), [decompress_buf_off](auto& e){
return e.InRange(decompress_buf_off);
});
R_UNLESS(it != t->ncz_blocks.cend(), Result_NczBlockNotFound);
// log_write("looking found block\n");
ncz_block = &(*it);
}
// https://github.com/nicoboss/nsz/issues/79
auto decompressedBlockSize = 1 << t->ncz_block_header.block_size_exponent;
// special handling for the last block to check it's actually compressed
if (ncz_block->offset == t->ncz_blocks.back().offset) {
log_write("last block special handling\n");
decompressedBlockSize = t->ncz_block_header.decompressed_size % decompressedBlockSize;
}
// check if this block is compressed.
compressed = ncz_block->size < decompressedBlockSize;
// clip read size as blocks can be up to 32GB in size!
const auto size = std::min<u64>(buf.size() - buf_off, ncz_block->size);
buffer = {buf.data() + buf_off, size};
}
if (compressed) {
// log_write("COMPRESSED block\n");
ZSTD_inBuffer input = { buffer.data(), buffer.size(), 0 };
while (input.pos < input.size) {
R_TRY(t->GetResults());
inflate_buf.resize(inflate_offset + chunk_size);
ZSTD_outBuffer output = { inflate_buf.data() + inflate_offset, chunk_size, 0 };
const auto res = ZSTD_decompressStream(dctx, std::addressof(output), std::addressof(input));
R_UNLESS(!ZSTD_isError(res), Result_InvalidNczZstdError);
t->decompress_offset += output.pos;
inflate_offset += output.pos;
if (inflate_offset >= INFLATE_BUFFER_MAX) {
// log_write("flushing compressed data: %zd vs %zd diff: %zd\n", inflate_offset, INFLATE_BUFFER_MAX, inflate_offset - INFLATE_BUFFER_MAX);
R_TRY(ncz_flush(INFLATE_BUFFER_MAX));
}
}
} else {
inflate_buf.resize(inflate_offset + buffer.size());
std::memcpy(inflate_buf.data() + inflate_offset, buffer.data(), buffer.size());
t->decompress_offset += buffer.size();
inflate_offset += buffer.size();
if (inflate_offset >= INFLATE_BUFFER_MAX) {
// log_write("flushing copy data\n");
R_TRY(ncz_flush(INFLATE_BUFFER_MAX));
}
}
buf_off += buffer.size();
decompress_buf_off += buffer.size();
}
}
}
// flush remaining data.
if (is_ncz && inflate_offset) {
log_write("flushing remaining\n");
R_TRY(ncz_flush(inflate_offset));
}
log_write("decompress thread done!\n");
// get final hash output.
sha256ContextGetHash(std::addressof(t->sha256), t->nca->hash);
R_SUCCEED();
}
// write thread writes data to the nca placeholder.
Result Yati::writeFuncInternal(ThreadData* t) {
std::vector<u8> buf;
buf.reserve(t->max_buffer_size);
while (t->write_offset < t->write_size && R_SUCCEEDED(t->GetResults())) {
s64 dummy_off;
R_TRY(t->GetWriteBuf(buf, dummy_off));
R_TRY(ncmContentStorageWritePlaceHolder(std::addressof(cs), std::addressof(t->nca->placeholder_id), t->write_offset, buf.data(), buf.size()));
t->write_offset += buf.size();
}
log_write("finished write thread!\n");
R_SUCCEED();
}
void readFunc(void* d) {
auto t = static_cast<ThreadData*>(d);
t->read_result = t->yati->readFuncInternal(t);
log_write("read thread returned now\n");
}
void decompressFunc(void* d) {
log_write("hello decomp thread func\n");
auto t = static_cast<ThreadData*>(d);
t->decompress_result = t->yati->decompressFuncInternal(t);
log_write("decompress thread returned now\n");
}
void writeFunc(void* d) {
auto t = static_cast<ThreadData*>(d);
t->write_result = t->yati->writeFuncInternal(t);
log_write("write thread returned now\n");
}
// stdio-like wrapper for std::vector
struct BufHelper {
BufHelper() = default;
BufHelper(std::span<const u8> data) {
write(data);
}
void write(const void* data, u64 size) {
if (offset + size >= buf.size()) {
buf.resize(offset + size);
}
std::memcpy(buf.data() + offset, data, size);
offset += size;
}
void write(std::span<const u8> data) {
write(data.data(), data.size());
}
void seek(u64 where_to) {
offset = where_to;
}
[[nodiscard]]
auto tell() const {
return offset;
}
std::vector<u8> buf{};
u64 offset{};
};
Yati::Yati(ui::ProgressBox* _pbox, std::shared_ptr<source::Base> _source) : pbox{_pbox}, source{_source} {
appletSetMediaPlaybackState(true);
}
Yati::~Yati() {
splCryptoExit();
serviceClose(std::addressof(ns_app));
nsExit();
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
ncmContentMetaDatabaseClose(std::addressof(ncm_db[i]));
ncmContentStorageClose(std::addressof(ncm_cs[i]));
}
serviceClose(std::addressof(es));
appletSetMediaPlaybackState(false);
if (config.boost_mode) {
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
}
}
Result Yati::Setup(const ConfigOverride& override) {
config.sd_card_install = override.sd_card_install.value_or(App::GetApp()->m_install_sd.Get());
config.boost_mode = App::GetApp()->m_boost_mode.Get();
config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get();
config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get();
config.ticket_only = App::GetApp()->m_ticket_only.Get();
config.skip_base = App::GetApp()->m_skip_base.Get();
config.skip_patch = App::GetApp()->m_skip_patch.Get();
config.skip_addon = App::GetApp()->m_skip_addon.Get();
config.skip_data_patch = App::GetApp()->m_skip_data_patch.Get();
config.skip_ticket = App::GetApp()->m_skip_ticket.Get();
config.skip_nca_hash_verify = override.skip_nca_hash_verify.value_or(App::GetApp()->m_skip_nca_hash_verify.Get());
config.skip_rsa_header_fixed_key_verify = override.skip_rsa_header_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get());
config.skip_rsa_npdm_fixed_key_verify = override.skip_rsa_npdm_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get());
config.ignore_distribution_bit = override.ignore_distribution_bit.value_or(App::GetApp()->m_ignore_distribution_bit.Get());
config.convert_to_standard_crypto = override.convert_to_standard_crypto.value_or(App::GetApp()->m_convert_to_standard_crypto.Get());
config.lower_master_key = override.lower_master_key.value_or(App::GetApp()->m_lower_master_key.Get());
config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get());
storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser;
if (config.boost_mode) {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
}
R_TRY(source->GetOpenResult());
R_TRY(splCryptoInitialize());
R_TRY(nsInitialize());
R_TRY(nsGetApplicationManagerInterface(std::addressof(ns_app)));
R_TRY(smGetService(std::addressof(es), "es"));
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
R_TRY(ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i]));
R_TRY(ncmOpenContentStorage(std::addressof(ncm_cs[i]), NCM_STORAGE_IDS[i]));
}
cs = ncm_cs[config.sd_card_install];
db = ncm_db[config.sd_card_install];
R_TRY(parse_keys(keys, true));
R_SUCCEED();
}
Result Yati::InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca) {
if (config.skip_if_already_installed) {
R_TRY(ncmContentStorageHas(std::addressof(cs), std::addressof(nca.skipped), std::addressof(nca.content_id)));
if (nca.skipped) {
log_write("\tskipped nca as it's already installed ncmContentStorageHas()\n");
R_TRY(ncmContentStorageReadContentIdFile(std::addressof(cs), std::addressof(nca.header), sizeof(nca.header), std::addressof(nca.content_id), 0));
crypto::cryptoAes128Xts(std::addressof(nca.header), std::addressof(nca.header), keys.header_key, 0, 0x200, sizeof(nca.header), false);
R_SUCCEED();
}
}
log_write("generateing placeholder\n");
R_TRY(ncmContentStorageGeneratePlaceHolderId(std::addressof(cs), std::addressof(nca.placeholder_id)));
log_write("creating placeholder\n");
R_TRY(ncmContentStorageCreatePlaceHolder(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id), nca.size));
log_write("opening thread\n");
ThreadData t_data{this, tickets, std::addressof(nca)};
#define READ_THREAD_CORE 1
#define DECOMPRESS_THREAD_CORE 2
#define WRITE_THREAD_CORE 0
// #define READ_THREAD_CORE 2
// #define DECOMPRESS_THREAD_CORE 2
// #define WRITE_THREAD_CORE 2
Thread t_read{};
R_TRY(threadCreate(&t_read, readFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, READ_THREAD_CORE));
ON_SCOPE_EXIT(threadClose(&t_read));
Thread t_decompress{};
R_TRY(threadCreate(&t_decompress, decompressFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, DECOMPRESS_THREAD_CORE));
ON_SCOPE_EXIT(threadClose(&t_decompress));
Thread t_write{};
R_TRY(threadCreate(&t_write, writeFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, WRITE_THREAD_CORE));
ON_SCOPE_EXIT(threadClose(&t_write));
log_write("starting threads\n");
R_TRY(threadStart(std::addressof(t_read)));
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read)));
R_TRY(threadStart(std::addressof(t_decompress)));
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_decompress)));
R_TRY(threadStart(std::addressof(t_write)));
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write)));
while (t_data.write_offset != t_data.write_size && R_SUCCEEDED(t_data.GetResults())) {
pbox->UpdateTransfer(t_data.write_offset, t_data.write_size);
svcSleepThread(1e+6);
}
// wait for all threads to close.
log_write("waiting for threads to close\n");
for (;;) {
t_data.WakeAllThreads();
pbox->Yield();
if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) {
continue;
} else if (R_FAILED(waitSingleHandle(t_decompress.handle, 1000))) {
continue;
} else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) {
continue;
}
break;
}
log_write("threads closed\n");
// if any of the threads failed, wake up all threads so they can exit.
if (R_FAILED(t_data.GetResults())) {
log_write("some reads failed, waking threads: %s\n", nca.name.c_str());
log_write("returning due to fail: %s\n", nca.name.c_str());
return t_data.GetResults();
}
R_TRY(t_data.GetResults());
NcmContentId content_id{};
std::memcpy(std::addressof(content_id), nca.hash, sizeof(content_id));
log_write("old id: %s new id: %s\n", hexIdToStr(nca.content_id).str, hexIdToStr(content_id).str);
if (!config.skip_nca_hash_verify && !nca.modified) {
if (std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id))) {
log_write("nca hash is invalid!!!!\n");
R_UNLESS(!std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id)), Result_InvalidNcaSha256);
} else {
log_write("nca hash is valid!\n");
}
} else {
log_write("skipping nca sha256 verify\n");
}
R_SUCCEED();
}
Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
log_write("in install nca\n");
pbox->NewTransfer(nca.name);
keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str());
R_TRY(InstallNcaInternal(tickets, nca));
fs::FsPath path;
if (nca.skipped) {
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.content_id)));
} else {
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
}
if (nca.header.content_type == nca::ContentType_Program) {
// todo: verify npdm key.
} else if (nca.header.content_type == nca::ContentType_Control) {
NacpLanguageEntry entry;
std::vector<u8> icon;
R_TRY(yati::ParseControlNca(path, nca.header.program_id, &entry, sizeof(entry), &icon));
pbox->SetTitle(entry.name).SetImageData(icon);
}
R_SUCCEED();
}
Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections) {
R_TRY(InstallNca(tickets, cnmt));
fs::FsPath path;
if (cnmt.skipped) {
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.content_id)));
} else {
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
}
ncm::PackagedContentMeta header;
std::vector<NcmPackagedContentInfo> infos;
R_TRY(ParseCnmtNca(path, cnmt.header.program_id, header, cnmt.extended_header, infos));
for (const auto& packed_info : infos) {
const auto& info = packed_info.info;
if (info.content_type == NcmContentType_DeltaFragment) {
continue;
}
const auto str = hexIdToStr(info.content_id);
const auto it = std::find_if(collections.cbegin(), collections.cend(), [&str](auto& e){
return e.name.find(str.str) != e.name.npos;
});
R_UNLESS(it != collections.cend(), Result_NcaNotFound);
log_write("found: %s\n", str.str);
cnmt.infos.emplace_back(packed_info);
auto& nca = cnmt.ncas.emplace_back(*it);
nca.type = info.content_type;
}
// update header
cnmt.meta_header = header.meta_header;
cnmt.meta_header.content_count = cnmt.infos.size() + 1;
cnmt.meta_header.storage_id = 0;
cnmt.key.id = header.title_id;
cnmt.key.version = header.title_version;
cnmt.key.type = header.meta_type;
cnmt.key.install_type = NcmContentInstallType_Full;
std::memset(cnmt.key.padding, 0, sizeof(cnmt.key.padding));
cnmt.content_info.content_id = cnmt.content_id;
cnmt.content_info.content_type = NcmContentType_Meta;
cnmt.content_info.attr = 0;
ncmU64ToContentInfoSize(cnmt.size, &cnmt.content_info);
cnmt.content_info.id_offset = 0;
if (config.lower_system_version) {
auto extended_header = (ncm::ExtendedHeader*)cnmt.extended_header.data();
log_write("patching version\n");
if (cnmt.key.type == NcmContentMetaType_Application) {
extended_header->application.required_system_version = 0;
} else if (cnmt.key.type == NcmContentMetaType_Patch) {
extended_header->patch.required_system_version = 0;
}
}
// sort ncas
const auto sorter = [](NcaCollection& lhs, NcaCollection& rhs) -> bool {
return lhs.type > rhs.type;
};
std::sort(cnmt.ncas.begin(), cnmt.ncas.end(), sorter);
log_write("found all cnmts\n");
R_SUCCEED();
}
Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, const container::Collections& collections, bool read_data) {
for (const auto& collection : collections) {
if (collection.name.ends_with(".tik")) {
TikCollection entry{};
keys::parse_hex_key(entry.rights_id.c, collection.name.c_str());
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
const auto cert = std::find_if(collections.cbegin(), collections.cend(), [&str](auto& e){
return e.name.find(str) != e.name.npos;
});
R_UNLESS(cert != collections.cend(), Result_CertNotFound);
entry.ticket.resize(collection.size);
entry.cert.resize(cert->size);
// only supported on non-stream installs.
if (read_data) {
u64 bytes_read;
R_TRY(source->Read(entry.ticket.data(), collection.offset, entry.ticket.size(), &bytes_read));
R_TRY(source->Read(entry.cert.data(), cert->offset, entry.cert.size(), &bytes_read));
}
tickets.emplace_back(entry);
}
}
R_SUCCEED();
}
Result Yati::GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool& skip) {
const auto app_id = ncm::GetAppId(cnmt.key);
version_out = cnmt.key.version;
for (auto& db : ncm_db) {
s32 db_list_total;
s32 db_list_count;
std::vector<NcmContentMetaKey> keys(1);
if (R_SUCCEEDED(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), NcmContentMetaType_Unknown, app_id, 0, UINT64_MAX, NcmContentInstallType_Full))) {
if (db_list_total != keys.size()) {
keys.resize(db_list_total);
if (keys.size()) {
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), NcmContentMetaType_Unknown, app_id, 0, UINT64_MAX, NcmContentInstallType_Full));
}
}
for (auto& key : keys) {
log_write("found record: %016lX type: %u version: %u\n", key.id, key.type, key.version);
if (key.id == cnmt.key.id && cnmt.key.version == key.version && config.skip_if_already_installed) {
log_write("skipping as already installed\n");
skip = true;
}
// check if we are downgrading
if (cnmt.key.type == NcmContentMetaType_Patch) {
if (cnmt.key.type == key.type && cnmt.key.version < key.version && !config.allow_downgrade) {
log_write("skipping due to it being lower\n");
skip = true;
}
} else {
version_out = std::max(version_out, key.version);
}
}
}
}
R_SUCCEED();
}
Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
// skip invalid types
if (!(cnmt.key.type & 0x80)) {
log_write("\tskipping: invalid: %u\n", cnmt.key.type);
skip = true;
} else if (config.skip_base && cnmt.key.type == NcmContentMetaType_Application) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) {
log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n");
skip = true;
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
skip = true;
} else if (config.skip_if_already_installed) {
bool has;
R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key)));
if (has) {
log_write("\tskipping: [ncmContentMetaDatabaseHas()]\n");
skip = true;
}
}
R_SUCCEED();
}
Result Yati::ImportTickets(std::span<TikCollection> tickets) {
for (auto& ticket : tickets) {
if (ticket.required) {
if (config.skip_ticket) {
log_write("WARNING: skipping ticket install, but it's required!\n");
} else {
log_write("patching ticket\n");
R_TRY(es::PatchTicket(ticket.ticket, keys));
log_write("installing ticket\n");
R_TRY(es::ImportTicket(std::addressof(es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
ticket.required = false;
}
}
}
R_SUCCEED();
}
Result Yati::RemoveInstalledNcas(const CnmtCollection& cnmt) {
const auto app_id = ncm::GetAppId(cnmt.key);
// remove current entries (if any).
s32 db_list_total;
s32 db_list_count;
u64 id_min = cnmt.key.id;
u64 id_max = cnmt.key.id;
// if installing a patch, remove all previously installed patches.
if (cnmt.key.type == NcmContentMetaType_Patch) {
id_min = 0;
id_max = UINT64_MAX;
}
log_write("listing keys\n");
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
auto& cs = ncm_cs[i];
auto& db = ncm_db[i];
std::vector<NcmContentMetaKey> keys(1);
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast<NcmContentMetaType>(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full));
if (db_list_total != keys.size()) {
keys.resize(db_list_total);
if (keys.size()) {
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast<NcmContentMetaType>(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full));
}
}
for (auto& key : keys) {
log_write("found key: 0x%016lX type: %u version: %u\n", key.id, key.type, key.version);
NcmContentMetaHeader header;
u64 out_size;
log_write("trying to get from db\n");
R_TRY(ncmContentMetaDatabaseGet(std::addressof(db), std::addressof(key), std::addressof(out_size), std::addressof(header), sizeof(header)));
R_UNLESS(out_size == sizeof(header), Result_NcmDbCorruptHeader);
log_write("trying to list infos\n");
std::vector<NcmContentInfo> infos(header.content_count);
s32 content_info_out;
R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(content_info_out), infos.data(), infos.size(), std::addressof(key), 0));
R_UNLESS(content_info_out == infos.size(), Result_NcmDbCorruptInfos);
log_write("size matches\n");
for (auto& info : infos) {
R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id)));
}
log_write("trying to remove it\n");
R_TRY(ncmContentMetaDatabaseRemove(std::addressof(db), std::addressof(key)));
R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db)));
log_write("all done with this key\n\n");
}
}
log_write("done with keys\n");
R_SUCCEED();
}
Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_version_num) {
const auto app_id = ncm::GetAppId(cnmt.key);
// register all nca's
log_write("registering cnmt nca\n");
R_TRY(ncm::Register(std::addressof(cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id)));
log_write("registered cnmt nca\n");
for (auto& nca : cnmt.ncas) {
if (!nca.skipped && nca.type != NcmContentType_DeltaFragment) {
log_write("registering nca: %s\n", nca.name.c_str());
R_TRY(ncm::Register(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id)));
log_write("registered nca: %s\n", nca.name.c_str());
}
}
log_write("register'd all ncas\n");
// build ncm meta and push to the database.
BufHelper buf{};
buf.write(std::addressof(cnmt.meta_header), sizeof(cnmt.meta_header));
buf.write(cnmt.extended_header.data(), cnmt.extended_header.size());
buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info));
for (auto& info : cnmt.infos) {
buf.write(std::addressof(info.info), sizeof(info.info));
}
pbox->NewTransfer("Updating ncm databse"_i18n);
R_TRY(ncmContentMetaDatabaseSet(std::addressof(db), std::addressof(cnmt.key), buf.buf.data(), buf.tell()));
R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db)));
// push record.
ncm::ContentStorageRecord content_storage_record{};
content_storage_record.key = cnmt.key;
content_storage_record.storage_id = storage_id;
pbox->NewTransfer("Pushing application record"_i18n);
R_TRY(ns::PushApplicationRecord(std::addressof(ns_app), app_id, std::addressof(content_storage_record), 1));
if (hosversionAtLeast(6,0,0)) {
R_TRY(avmInitialize());
ON_SCOPE_EXIT(avmExit());
R_TRY(avmPushLaunchVersion(app_id, latest_version_num));
}
log_write("pushed\n");
R_SUCCEED();
}
Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override) {
auto yati = std::make_unique<Yati>(pbox, source);
R_TRY(yati->Setup(override));
std::vector<TikCollection> tickets{};
R_TRY(yati->ParseTicketsIntoCollection(tickets, collections, true));
std::vector<CnmtCollection> cnmts{};
for (const auto& collection : collections) {
log_write("found collection: %s\n", collection.name.c_str());
if (collection.name.ends_with(".cnmt.nca") || collection.name.ends_with(".cnmt.ncz")) {
auto& cnmt = cnmts.emplace_back(NcaCollection{collection});
cnmt.type = NcmContentType_Meta;
}
}
for (auto& cnmt : cnmts) {
ON_SCOPE_EXIT(
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(cnmt.placeholder_id));
for (auto& nca : cnmt.ncas) {
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(nca.placeholder_id));
}
);
R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections));
u32 latest_version_num;
bool skip = false;
R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip));
R_TRY(yati->ShouldSkip(cnmt, skip));
if (skip) {
log_write("skipping install!\n");
continue;
}
log_write("installing nca's\n");
for (auto& nca : cnmt.ncas) {
R_TRY(yati->InstallNca(tickets, nca));
}
R_TRY(yati->ImportTickets(tickets));
R_TRY(yati->RemoveInstalledNcas(cnmt));
R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num));
}
log_write("success!\n");
R_SUCCEED();
}
Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, container::Collections collections, const ConfigOverride& override) {
auto yati = std::make_unique<Yati>(pbox, source);
R_TRY(yati->Setup(override));
// not supported with stream installs (yet).
yati->config.skip_if_already_installed = false;
yati->config.convert_to_standard_crypto = false;
yati->config.lower_master_key = false;
std::vector<NcaCollection> ncas{};
std::vector<CnmtCollection> cnmts{};
std::vector<TikCollection> tickets{};
ON_SCOPE_EXIT(
for (const auto& cnmt : cnmts) {
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(cnmt.placeholder_id));
}
for (const auto& nca : ncas) {
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(nca.placeholder_id));
}
);
// fill ticket entries, the data will be filled later on.
R_TRY(yati->ParseTicketsIntoCollection(tickets, collections, false));
// sort based on lowest offset.
const auto sorter = [](const container::CollectionEntry& lhs, const container::CollectionEntry& rhs) -> bool {
return lhs.offset < rhs.offset;
};
std::sort(collections.begin(), collections.end(), sorter);
for (const auto& collection : collections) {
if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) {
auto& nca = ncas.emplace_back(NcaCollection{collection});
if (collection.name.ends_with(".cnmt.nca") || collection.name.ends_with(".cnmt.ncz")) {
auto& cnmt = cnmts.emplace_back(nca);
cnmt.type = NcmContentType_Meta;
R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections));
} else {
R_TRY(yati->InstallNca(tickets, nca));
}
} else if (collection.name.ends_with(".tik") || collection.name.ends_with(".cert")) {
FsRightsId rights_id{};
keys::parse_hex_key(rights_id.c, collection.name.c_str());
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
auto entry = std::find_if(tickets.begin(), tickets.end(), [&rights_id](auto& e){
return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id));
});
// this will never fail...but just in case.
R_UNLESS(entry != tickets.end(), Result_CertNotFound);
u64 bytes_read;
if (collection.name.ends_with(".tik")) {
R_TRY(source->Read(entry->ticket.data(), collection.offset, entry->ticket.size(), &bytes_read));
} else {
R_TRY(source->Read(entry->cert.data(), collection.offset, entry->cert.size(), &bytes_read));
}
}
}
for (auto& cnmt : cnmts) {
// copy nca structs into cnmt.
for (auto& cnmt_nca : cnmt.ncas) {
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [&cnmt_nca](auto& e){
return e.name == cnmt_nca.name;
});
R_UNLESS(it != ncas.cend(), Result_NczSectionNotFound);
const auto type = cnmt_nca.type;
cnmt_nca = *it;
cnmt_nca.type = type;
}
u32 latest_version_num;
bool skip = false;
R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip));
R_TRY(yati->ShouldSkip(cnmt, skip));
if (skip) {
log_write("skipping install!\n");
continue;
}
R_TRY(yati->ImportTickets(tickets));
R_TRY(yati->RemoveInstalledNcas(cnmt));
R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num));
}
log_write("success!\n");
R_SUCCEED();
}
} // namespace
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override) {
return InstallFromSource(pbox, std::make_shared<source::File>(fs, path), path, override);
// return InstallFromSource(pbox, std::make_shared<source::StreamFile>(fs, path), path, override);
}
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override) {
return InstallFromSource(pbox, std::make_shared<source::Stdio>(path), path, override);
}
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override) {
const auto ext = std::strrchr(path.s, '.');
R_UNLESS(ext, Result_ContainerNotFound);
if (!strcasecmp(ext, ".nsp") || !strcasecmp(ext, ".nsz")) {
return InstallFromContainer(pbox, std::make_unique<container::Nsp>(source), override);
} else if (!strcasecmp(ext, ".xci") || !strcasecmp(ext, ".xcz")) {
return InstallFromContainer(pbox, std::make_unique<container::Xci>(source), override);
}
R_THROW(Result_ContainerNotFound);
}
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override) {
container::Collections collections;
R_TRY(container->GetCollections(collections));
return InstallFromCollections(pbox, container->GetSource(), collections);
}
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override) {
if (source->IsStream()) {
return InstallInternalStream(pbox, source, collections, override);
} else {
return InstallInternal(pbox, source, collections, override);
}
}
Result ParseCnmtNca(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentMeta, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
FsDir dir;
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 total_entries;
FsDirectoryEntry buf;
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 offset{};
u64 bytes_read;
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read extended header
extended_header.resize(header.meta_header.extended_header_size);
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read infos.
infos.resize(header.meta_header.content_count);
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
offset += bytes_read;
R_SUCCEED();
}
Result ParseControlNca(const fs::FsPath& path, u64 program_id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
// read nacp.
if (nacp_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
}
// read icon.
if (icon_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 size;
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
icon_out->resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
}
R_SUCCEED();
}
} // namespace sphaira::yati