* Almost all strings for translation * Remove nonexistent strings. --------- Co-authored-by: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com>
1177 lines
39 KiB
C++
1177 lines
39 KiB
C++
// creates and installs nca's on the fly
|
|
// based on hacbrewpack (romfs creation) and yati (installation)
|
|
#include <switch.h>
|
|
#include <cstring>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <span>
|
|
|
|
#include "owo.hpp"
|
|
#include "defines.hpp"
|
|
#include "app.hpp"
|
|
#include "ui/progress_box.hpp"
|
|
#include "i18n.hpp"
|
|
|
|
namespace sphaira {
|
|
namespace {
|
|
|
|
constexpr u32 IVFC_MAX_LEVEL = 6;
|
|
constexpr u32 IVFC_HASH_BLOCK_SIZE = 0x4000;
|
|
constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000;
|
|
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
|
|
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
|
|
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
|
constexpr u32 NCA_SECTION_TOTAL = 0x4;
|
|
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
|
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
|
|
|
enum NcaDistributionType {
|
|
NcaDistributionType_System = 0x0,
|
|
NcaDistributionType_GameCard = 0x1
|
|
};
|
|
|
|
enum NcaContentType {
|
|
NcaContentType_Program = 0x0,
|
|
NcaContentType_Meta = 0x1,
|
|
NcaContentType_Control = 0x2,
|
|
NcaContentType_Manual = 0x3,
|
|
NcaContentType_Data = 0x4,
|
|
NcaContentType_PublicData = 0x5,
|
|
};
|
|
|
|
enum NcaFileSystemType {
|
|
NcaFileSystemType_RomFS = 0x0,
|
|
NcaFileSystemType_PFS0 = 0x1
|
|
};
|
|
|
|
enum NcaHashType {
|
|
NcaHashType_Auto = 0x0,
|
|
NcaHashType_HierarchicalSha256 = 0x2,
|
|
NcaHashType_HierarchicalIntegrity = 0x3
|
|
};
|
|
|
|
enum NcaEncryptionType {
|
|
NcaEncryptionType_Auto = 0x0,
|
|
NcaEncryptionType_None = 0x1,
|
|
NcaEncryptionType_AesCtrOld = 0x2,
|
|
NcaEncryptionType_AesCtr = 0x3,
|
|
NcaEncryptionType_AesCtrEx = 0x4
|
|
};
|
|
|
|
enum NsApplicationRecordType {
|
|
// installed
|
|
NsApplicationRecordType_Installed = 0x3,
|
|
// application is gamecard, but gamecard isn't insterted
|
|
NsApplicationRecordType_GamecardMissing = 0x5,
|
|
// archived
|
|
NsApplicationRecordType_Archived = 0xB,
|
|
};
|
|
|
|
// 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{};
|
|
};
|
|
|
|
struct NcaEntry {
|
|
NcaEntry(const BufHelper& buf, NcmContentType _type) : data{buf.buf}, type{_type} {
|
|
sha256CalculateHash(hash, data.data(), data.size());
|
|
}
|
|
|
|
const std::vector<u8> data;
|
|
const u8 type;
|
|
u8 hash[SHA256_HASH_SIZE];
|
|
};
|
|
|
|
struct CnmtHeader {
|
|
u64 title_id;
|
|
u32 title_version;
|
|
u8 meta_type; // NcmContentMetaType
|
|
u8 _0xD;
|
|
NcmContentMetaHeader meta_header;
|
|
u8 install_type; // NcmContentInstallType
|
|
u8 _0x17;
|
|
u32 required_sys_version;
|
|
u8 _0x1C[0x4];
|
|
};
|
|
static_assert(sizeof(CnmtHeader) == 0x20);
|
|
|
|
struct NcmContentStorageRecord {
|
|
NcmContentMetaKey key;
|
|
u8 storage_id; //
|
|
u8 padding[0x7];
|
|
};
|
|
|
|
struct NcmContentMetaData {
|
|
NcmContentMetaHeader header;
|
|
NcmApplicationMetaExtendedHeader extended;
|
|
NcmContentInfo infos[3];
|
|
};
|
|
|
|
struct NcaMetaEntry {
|
|
NcaMetaEntry(const BufHelper& buf, NcmContentType type) : nca_entry{buf, type} { }
|
|
|
|
NcaEntry nca_entry;
|
|
NcmContentMetaHeader content_meta_header{};
|
|
NcmContentMetaKey content_meta_key{};
|
|
NcmContentStorageRecord content_storage_record{};
|
|
NcmContentMetaData content_meta_data{};
|
|
};
|
|
|
|
struct Pfs0Header {
|
|
u32 magic;
|
|
u32 total_files;
|
|
u32 string_table_size;
|
|
u32 padding;
|
|
};
|
|
|
|
struct Pfs0FileTable {
|
|
u64 data_offset;
|
|
u64 data_size;
|
|
u32 name_offset;
|
|
u32 padding;
|
|
};
|
|
|
|
struct Pfs0StringTable {
|
|
char name[256];
|
|
};
|
|
|
|
struct FileEntry {
|
|
std::string name;
|
|
std::vector<u8> data;
|
|
};
|
|
|
|
using FileEntries = std::vector<FileEntry>;
|
|
|
|
struct NpdmMeta {
|
|
u32 magic; // "META"
|
|
u32 signature_key_generation; // +9.0.0
|
|
u32 _0x8;
|
|
u8 flags;
|
|
u8 _0xD;
|
|
u8 main_thread_priority;
|
|
u8 main_thread_core_num;
|
|
u32 _0x10;
|
|
u32 sys_resource_size; // +3.0.0
|
|
u32 version;
|
|
u32 main_thread_stack_size;
|
|
char title_name[0x10];
|
|
char product_code[0x10];
|
|
u8 _0x40[0x30];
|
|
u32 aci0_offset;
|
|
u32 aci0_size;
|
|
u32 acid_offset;
|
|
u32 acid_size;
|
|
};
|
|
|
|
struct NpdmPatch {
|
|
char title_name[0x10]{"Application"};
|
|
char product_code[0x10]{};
|
|
u64 tid;
|
|
};
|
|
|
|
struct NcapPatch {
|
|
std::string name;
|
|
std::string author;
|
|
u64 tid;
|
|
};
|
|
|
|
struct NcaSectionTableEntry {
|
|
u32 media_start_offset; // divided by 0x200.
|
|
u32 media_end_offset; // divided by 0x200.
|
|
u8 _0x8[0x4]; // unknown.
|
|
u8 _0xC[0x4]; // unknown.
|
|
};
|
|
|
|
struct LayerRegion {
|
|
u64 offset;
|
|
u64 size;
|
|
};
|
|
|
|
struct HierarchicalSha256Data {
|
|
u8 master_hash[0x20];
|
|
u32 block_size;
|
|
u32 layer_count;
|
|
LayerRegion hash_layer;
|
|
LayerRegion pfs0_layer;
|
|
LayerRegion unused_layers[3];
|
|
u8 _0x78[0x80];
|
|
};
|
|
|
|
#pragma pack(push, 1)
|
|
struct HierarchicalIntegrityVerificationLevelInformation {
|
|
u64 logical_offset;
|
|
u64 hash_data_size;
|
|
u32 block_size; // log2
|
|
u32 _0x14; // reserved
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
struct InfoLevelHash {
|
|
u32 max_layers;
|
|
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
|
u8 signature_salt[0x20];
|
|
};
|
|
|
|
struct IntegrityMetaInfo {
|
|
u32 magic; // IVFC
|
|
u32 version;
|
|
u32 master_hash_size;
|
|
InfoLevelHash info_level_hash;
|
|
u8 master_hash[0x20];
|
|
u8 _0xE0[0x18];
|
|
};
|
|
|
|
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
|
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
|
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
|
|
|
typedef struct romfs_dirent_ctx {
|
|
u32 entry_offset;
|
|
struct romfs_dirent_ctx *parent; /* Parent node */
|
|
struct romfs_dirent_ctx *child; /* Child node */
|
|
struct romfs_dirent_ctx *sibling; /* Sibling node */
|
|
struct romfs_fent_ctx *file; /* File node */
|
|
struct romfs_dirent_ctx *next; /* Next node */
|
|
} romfs_dirent_ctx_t;
|
|
|
|
typedef struct romfs_fent_ctx {
|
|
u32 entry_offset;
|
|
u64 offset;
|
|
u64 size;
|
|
romfs_dirent_ctx_t *parent; /* Parent dir */
|
|
struct romfs_fent_ctx *sibling; /* Sibling file */
|
|
struct romfs_fent_ctx *next; /* Logical next file */
|
|
} romfs_fent_ctx_t;
|
|
|
|
typedef struct {
|
|
romfs_fent_ctx_t *files;
|
|
u64 num_dirs;
|
|
u64 num_files;
|
|
u64 dir_table_size;
|
|
u64 file_table_size;
|
|
u64 dir_hash_table_size;
|
|
u64 file_hash_table_size;
|
|
u64 file_partition_size;
|
|
} romfs_ctx_t;
|
|
|
|
struct NcaFsHeader {
|
|
u16 version; // always 2.
|
|
u8 fs_type; // see NcaFileSystemType.
|
|
u8 hash_type; // see NcaHashType.
|
|
u8 encryption_type; // see NcaEncryptionType.
|
|
u8 metadata_hash_type;
|
|
u8 _0x6[0x2]; // empty.
|
|
|
|
union {
|
|
HierarchicalSha256Data hierarchical_sha256_data;
|
|
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
|
} hash_data;
|
|
|
|
u8 patch_info[0x40];
|
|
u64 section_ctr;
|
|
u8 spares_info[0x30];
|
|
u8 compression_info[0x28];
|
|
u8 meta_data_hash_data_info[0x30];
|
|
u8 reserved[0x30];
|
|
};
|
|
|
|
struct NcaSectionHeaderHash {
|
|
u8 sha256[0x20];
|
|
};
|
|
|
|
struct NcaKeyArea {
|
|
u8 area[0x10];
|
|
};
|
|
|
|
struct NcaHeader {
|
|
u8 rsa_fixed_key[0x100];
|
|
u8 rsa_npdm[0x100]; // key from npdm.
|
|
u32 magic;
|
|
u8 distribution_type; // see NcaDistributionType.
|
|
u8 content_type; // see NcaContentType.
|
|
u8 old_key_gen; // see NcaOldKeyGeneration.
|
|
u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex.
|
|
u64 size;
|
|
u64 title_id;
|
|
u32 context_id;
|
|
u32 sdk_version;
|
|
u8 key_gen; // see NcaKeyGeneration.
|
|
u8 header_1_sig_key_gen;
|
|
u8 _0x222[0xE]; // empty.
|
|
FsRightsId rights_id;
|
|
|
|
NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
|
NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
|
NcaKeyArea key_area[NCA_SECTION_TOTAL];
|
|
|
|
u8 _0x340[0xC0]; // empty.
|
|
|
|
NcaFsHeader fs_header[NCA_SECTION_TOTAL];
|
|
};
|
|
|
|
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
|
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
|
};
|
|
|
|
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
|
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
|
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
|
};
|
|
|
|
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
|
|
const u64 size = block - (off % block);
|
|
if (size) {
|
|
std::vector<u8> padding(size);
|
|
buf.write(padding.data(), padding.size());
|
|
}
|
|
return size;
|
|
}
|
|
|
|
auto romfs_get_direntry(romfs_dir *directories, u32 offset) -> romfs_dir* {
|
|
return (romfs_dir*)((u8*)directories + offset);
|
|
}
|
|
|
|
auto romfs_get_fentry(romfs_file *files, u32 offset) -> romfs_file* {
|
|
return (romfs_file*)((u8*)files + offset);
|
|
}
|
|
|
|
auto calc_path_hash(u32 parent, const u8 *path, u32 start, u32 path_len) -> u32 {
|
|
u32 hash = parent ^ 123456789;
|
|
for (u32 i = 0; i < path_len; i++) {
|
|
hash = (hash >> 5) | (hash << 27);
|
|
hash ^= path[start + i];
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
auto align(u32 offset, u32 alignment) -> u32 {
|
|
const u32 mask = ~(alignment - 1);
|
|
return (offset + (alignment - 1)) & mask;
|
|
}
|
|
|
|
auto align64(u64 offset, u64 alignment) -> u64 {
|
|
const u64 mask = ~(u64)(alignment - 1);
|
|
return (offset + (alignment - 1)) & mask;
|
|
}
|
|
|
|
auto romfs_get_hash_table_count(u32 num_entries) -> u32 {
|
|
if (num_entries < 3) {
|
|
return 3;
|
|
} else if (num_entries < 19) {
|
|
return num_entries | 1;
|
|
}
|
|
|
|
u32 count = num_entries;
|
|
while (count % 2 == 0 || count % 3 == 0 || count % 5 == 0 || count % 7 == 0 || count % 11 == 0 || count % 13 == 0 || count % 17 == 0) {
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void romfs_visit_dir(const FileEntries& entries, romfs_dirent_ctx_t *parent, romfs_ctx_t *romfs_ctx) {
|
|
romfs_dirent_ctx_t *child_dir_tree = NULL;
|
|
romfs_fent_ctx_t *child_file_tree = NULL;
|
|
romfs_fent_ctx_t *cur_file = NULL;
|
|
|
|
for (auto& e : entries) {
|
|
/* File */
|
|
cur_file = (romfs_fent_ctx_t*)calloc(1, sizeof(romfs_fent_ctx_t));
|
|
|
|
romfs_ctx->num_files++;
|
|
|
|
cur_file->parent = parent;
|
|
cur_file->size = e.data.size();
|
|
|
|
romfs_ctx->file_table_size += sizeof(romfs_file) + align(e.name.length() - 1, 4);
|
|
|
|
/* Ordered insertion on sibling */
|
|
if (child_file_tree == NULL) {
|
|
cur_file->sibling = child_file_tree;
|
|
child_file_tree = cur_file;
|
|
} else {
|
|
romfs_fent_ctx_t *child, *prev;
|
|
prev = child_file_tree;
|
|
child = child_file_tree->sibling;
|
|
prev->sibling = cur_file;
|
|
cur_file->sibling = child;
|
|
}
|
|
|
|
/* Ordered insertion on next */
|
|
if (romfs_ctx->files == NULL) {
|
|
cur_file->next = romfs_ctx->files;
|
|
romfs_ctx->files = cur_file;
|
|
} else {
|
|
romfs_fent_ctx_t *child, *prev;
|
|
prev = romfs_ctx->files;
|
|
child = romfs_ctx->files->next;
|
|
prev->next = cur_file;
|
|
cur_file->next = child;
|
|
}
|
|
|
|
cur_file = NULL;
|
|
}
|
|
|
|
parent->child = child_dir_tree;
|
|
parent->file = child_file_tree;
|
|
}
|
|
|
|
void build_romfs_into_file(const FileEntries& entries, BufHelper& buf) {
|
|
auto root_ctx = (romfs_dirent_ctx_t*)calloc(1, sizeof(romfs_dirent_ctx_t));
|
|
root_ctx->parent = root_ctx;
|
|
|
|
romfs_ctx_t romfs_ctx{};
|
|
|
|
romfs_ctx.dir_table_size = sizeof(romfs_dir); /* Root directory. */
|
|
romfs_ctx.num_dirs = 1;
|
|
|
|
/* Visit all directories. */
|
|
romfs_visit_dir(entries, root_ctx, &romfs_ctx);
|
|
const u32 dir_hash_table_entry_count = romfs_get_hash_table_count(romfs_ctx.num_dirs);
|
|
const u32 file_hash_table_entry_count = romfs_get_hash_table_count(romfs_ctx.num_files);
|
|
romfs_ctx.dir_hash_table_size = 4 * dir_hash_table_entry_count;
|
|
romfs_ctx.file_hash_table_size = 4 * file_hash_table_entry_count;
|
|
|
|
romfs_header header{};
|
|
romfs_fent_ctx_t *cur_file{};
|
|
romfs_dirent_ctx_t *cur_dir{};
|
|
u32 entry_offset{};
|
|
|
|
std::vector<u32> dir_hash_table(dir_hash_table_entry_count, ROMFS_ENTRY_EMPTY);
|
|
std::vector<u32> file_hash_table(file_hash_table_entry_count, ROMFS_ENTRY_EMPTY);
|
|
|
|
auto dir_table = (romfs_dir*)calloc(1, romfs_ctx.dir_table_size);
|
|
auto file_table = (romfs_file *)calloc(1, romfs_ctx.file_table_size);
|
|
|
|
/* Determine file offsets. */
|
|
cur_file = romfs_ctx.files;
|
|
entry_offset = 0;
|
|
for (auto& e : entries) {
|
|
romfs_ctx.file_partition_size = align64(romfs_ctx.file_partition_size, 0x10);
|
|
cur_file->offset = romfs_ctx.file_partition_size;
|
|
romfs_ctx.file_partition_size += cur_file->size;
|
|
cur_file->entry_offset = entry_offset;
|
|
entry_offset += sizeof(romfs_file) + align(e.name.length() - 1, 4);
|
|
cur_file = cur_file->next;
|
|
}
|
|
|
|
/* Determine dir offsets. */
|
|
root_ctx->entry_offset = 0x0;
|
|
|
|
/* Populate file tables. */
|
|
cur_file = romfs_ctx.files;
|
|
for (auto& e : entries) {
|
|
auto cur_entry = romfs_get_fentry(file_table, cur_file->entry_offset);
|
|
cur_entry->parent = (cur_file->parent->entry_offset);
|
|
cur_entry->sibling = (cur_file->sibling == NULL ? ROMFS_ENTRY_EMPTY : cur_file->sibling->entry_offset);
|
|
cur_entry->dataOff = (cur_file->offset);
|
|
cur_entry->dataSize = (cur_file->size);
|
|
|
|
const u32 name_size = e.name.length() - 1;
|
|
const u32 hash = calc_path_hash(cur_file->parent->entry_offset, (const u8 *)e.name.c_str(), 1, name_size);
|
|
cur_entry->nextHash = file_hash_table[hash % file_hash_table_entry_count];
|
|
file_hash_table[hash % file_hash_table_entry_count] = (cur_file->entry_offset);
|
|
|
|
cur_entry->nameLen = name_size;
|
|
std::memcpy(cur_entry->name, e.name.c_str() + 1, name_size);
|
|
|
|
cur_file = cur_file->next;
|
|
}
|
|
|
|
/* Populate dir tables. */
|
|
cur_dir = root_ctx;
|
|
|
|
while (cur_dir != NULL) {
|
|
auto cur_entry = romfs_get_direntry(dir_table, cur_dir->entry_offset);
|
|
cur_entry->parent = cur_dir->parent->entry_offset;
|
|
cur_entry->sibling = cur_dir->sibling == NULL ? ROMFS_ENTRY_EMPTY : cur_dir->sibling->entry_offset;
|
|
cur_entry->childDir = cur_dir->child == NULL ? ROMFS_ENTRY_EMPTY : cur_dir->child->entry_offset;
|
|
cur_entry->childFile = cur_dir->file == NULL ? ROMFS_ENTRY_EMPTY : cur_dir->file->entry_offset;
|
|
|
|
const auto hash = calc_path_hash(0, 0, 0, 0);
|
|
cur_entry->nextHash = dir_hash_table[hash % dir_hash_table_entry_count];
|
|
dir_hash_table[hash % dir_hash_table_entry_count] = (cur_dir->entry_offset);
|
|
|
|
cur_entry->nameLen = 0;
|
|
|
|
auto temp = cur_dir;
|
|
cur_dir = cur_dir->next;
|
|
free(temp);
|
|
}
|
|
|
|
header.headerSize = sizeof(header);
|
|
header.fileHashTableSize = romfs_ctx.file_hash_table_size;
|
|
header.fileTableSize = romfs_ctx.file_table_size;
|
|
header.dirHashTableSize = romfs_ctx.dir_hash_table_size;
|
|
header.dirTableSize = romfs_ctx.dir_table_size;
|
|
header.fileDataOff = ROMFS_FILEPARTITION_OFS;
|
|
|
|
header.dirHashTableOff = align64(romfs_ctx.file_partition_size + ROMFS_FILEPARTITION_OFS, 4);
|
|
header.dirTableOff = header.dirHashTableOff + romfs_ctx.dir_hash_table_size;
|
|
header.fileHashTableOff = header.dirTableOff + romfs_ctx.dir_table_size;
|
|
header.fileTableOff = header.fileHashTableOff + romfs_ctx.file_hash_table_size;
|
|
|
|
buf.write(&header, sizeof(header));
|
|
|
|
/* Write files. */
|
|
cur_file = romfs_ctx.files;
|
|
for (auto&e : entries) {
|
|
buf.seek(cur_file->offset + ROMFS_FILEPARTITION_OFS);
|
|
buf.write(e.data.data(), e.data.size());
|
|
|
|
auto temp = cur_file;
|
|
cur_file = cur_file->next;
|
|
free(temp);
|
|
}
|
|
|
|
buf.seek(header.dirHashTableOff);
|
|
buf.write(dir_hash_table.data(), romfs_ctx.dir_hash_table_size);
|
|
|
|
buf.write(dir_table, romfs_ctx.dir_table_size);
|
|
free(dir_table);
|
|
|
|
buf.write(file_hash_table.data(), romfs_ctx.file_hash_table_size);
|
|
|
|
buf.write(file_table, romfs_ctx.file_table_size);
|
|
free(file_table);
|
|
}
|
|
|
|
auto romfs_build(const FileEntries& entries, u64 *out_size) -> std::vector<u8> {
|
|
BufHelper buf;
|
|
|
|
build_romfs_into_file(entries, buf);
|
|
|
|
// Write Padding
|
|
buf.seek(buf.buf.size());
|
|
*out_size = buf.tell();
|
|
write_padding(buf, buf.tell(), IVFC_HASH_BLOCK_SIZE);
|
|
|
|
return buf.buf;
|
|
}
|
|
|
|
// todo: manually build npdm
|
|
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
|
NpdmMeta meta{};
|
|
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
|
|
|
// apply patch
|
|
std::memcpy(npdm.data() + 0x20, &patch.title_name, sizeof(patch.title_name));
|
|
std::memcpy(npdm.data() + 0x30, &patch.product_code, sizeof(patch.product_code));
|
|
std::memcpy(npdm.data() + meta.aci0_offset + 0x10, &patch.tid, sizeof(patch.tid));
|
|
std::memcpy(npdm.data() + meta.acid_offset + 0x210, &patch.tid, sizeof(patch.tid));
|
|
std::memcpy(npdm.data() + meta.acid_offset + 0x218, &patch.tid, sizeof(patch.tid));
|
|
}
|
|
|
|
void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) {
|
|
// patch title
|
|
if (!patch.name.empty()) {
|
|
for (auto& lang : nacp.lang) {
|
|
std::strncpy(lang.name, patch.name.c_str(), sizeof(lang.name)-1);
|
|
}
|
|
}
|
|
|
|
// patch author
|
|
if (!patch.name.empty()) {
|
|
for (auto& lang : nacp.lang) {
|
|
std::strncpy(lang.author, patch.author.c_str(), sizeof(lang.author)-1);
|
|
}
|
|
}
|
|
|
|
// misc
|
|
nacp.startup_user_account = 0x00; // skip user select prompt
|
|
nacp.user_account_switch_lock = 0x00; // allow account switch
|
|
nacp.add_on_content_registration_type = 0x01; // on demand
|
|
nacp.screenshot = 0; // 0x0 = true
|
|
nacp.video_capture = 0x2; // auto
|
|
nacp.logo_type = 0x2; // Nintendo
|
|
nacp.logo_handling = 0x0; // auto
|
|
nacp.data_loss_confirmation = 0x0; // disable as we don't use saves
|
|
nacp.required_network_service_license_on_launch = 0x0; // don't require linked account
|
|
const char error_code[] = "sphaira";
|
|
static_assert(sizeof(error_code) <= 9);
|
|
nacp.application_error_code_category = 0; // this is actually a char[8], not a u64 :)
|
|
std::memcpy(&nacp.application_error_code_category, error_code, sizeof(error_code)-1);
|
|
|
|
// update tid
|
|
nacp.presence_group_id = patch.tid;
|
|
nacp.save_data_owner_id = patch.tid;
|
|
nacp.pseudo_device_id_seed = patch.tid;
|
|
nacp.add_on_content_base_id = patch.tid ^ 0x1000;
|
|
for (auto& id : nacp.local_communication_id) {
|
|
id = patch.tid;
|
|
}
|
|
|
|
// enable play logging
|
|
nacp.play_log_policy = 0x0; // open
|
|
nacp.play_log_query_capability = 0x0;
|
|
|
|
// disable save creation
|
|
nacp.user_account_save_data_size = 0x0;
|
|
nacp.user_account_save_data_journal_size = 0x0;
|
|
nacp.device_save_data_size = 0x0;
|
|
nacp.device_save_data_journal_size = 0x0;
|
|
nacp.user_account_save_data_size_max = 0x0;
|
|
nacp.user_account_save_data_journal_size_max = 0x0;
|
|
nacp.device_save_data_size_max = 0x0;
|
|
nacp.device_save_data_journal_size_max = 0x0;
|
|
}
|
|
|
|
void add_file_entry(FileEntries& entries, const char* name, const void* data, u64 size) {
|
|
FileEntry entry;
|
|
entry.name = name;
|
|
entry.data.resize(size);
|
|
std::memcpy(entry.data.data(), data, size);
|
|
entries.emplace_back(entry);
|
|
}
|
|
|
|
void add_file_entry(FileEntries& entries, const char* name, std::span<const u8> data) {
|
|
add_file_entry(entries, name, data.data(), data.size());
|
|
}
|
|
|
|
auto build_ivfc_master_hash(std::span<const u8> level1) -> std::vector<u8> {
|
|
std::vector<u8> hash(SHA256_HASH_SIZE);
|
|
sha256CalculateHash(hash.data(), level1.data(), level1.size());
|
|
return hash;
|
|
}
|
|
|
|
auto build_pfs0(const FileEntries& entries) -> std::vector<u8> {
|
|
BufHelper buf;
|
|
|
|
Pfs0Header header{};
|
|
std::vector<Pfs0FileTable> file_table(entries.size());
|
|
std::vector<char> string_table;
|
|
|
|
u64 string_offset{};
|
|
u64 data_offset{};
|
|
|
|
for (u32 i = 0; i < entries.size(); i++) {
|
|
file_table[i].data_offset = data_offset;
|
|
file_table[i].data_size = entries[i].data.size();
|
|
file_table[i].name_offset = string_offset;
|
|
file_table[i].padding = 0;
|
|
|
|
string_table.resize(string_offset + entries[i].name.length() + 1);
|
|
std::memcpy(string_table.data() + string_offset, entries[i].name.c_str(), entries[i].name.length() + 1);
|
|
|
|
data_offset += entries[i].data.size();
|
|
string_offset += entries[i].name.length() + 1;
|
|
}
|
|
|
|
// align table
|
|
string_table.resize((string_table.size() + 0x1F) & ~0x1F);
|
|
|
|
header.magic = 0x30534650;
|
|
header.total_files = entries.size();
|
|
header.string_table_size = string_table.size();
|
|
header.padding = 0;
|
|
|
|
buf.write(&header, sizeof(header));
|
|
buf.write(file_table.data(), sizeof(Pfs0FileTable) * file_table.size());
|
|
buf.write(string_table.data(), string_table.size());
|
|
|
|
for (const auto&e : entries) {
|
|
buf.write(e.data.data(), e.data.size());
|
|
}
|
|
|
|
return buf.buf;
|
|
}
|
|
|
|
auto build_pfs0_hash_table(const std::vector<u8>& pfs0, u32 block_size) -> std::vector<u8> {
|
|
BufHelper buf;
|
|
u8 hash[SHA256_HASH_SIZE];
|
|
u32 read_size = block_size;
|
|
|
|
for (u32 i = 0; i < pfs0.size(); i += read_size) {
|
|
if (i + read_size >= pfs0.size()) {
|
|
read_size = pfs0.size() - i;
|
|
}
|
|
sha256CalculateHash(hash, pfs0.data() + i, read_size);
|
|
buf.write(hash, sizeof(hash));
|
|
}
|
|
|
|
return buf.buf;
|
|
}
|
|
|
|
auto build_pfs0_master_hash(const std::vector<u8>& pfs0_hash_table) -> std::vector<u8> {
|
|
std::vector<u8> hash(SHA256_HASH_SIZE);
|
|
sha256CalculateHash(hash.data(), pfs0_hash_table.data(), pfs0_hash_table.size());
|
|
return hash;
|
|
}
|
|
|
|
void write_nca_padding(BufHelper& buf) {
|
|
write_padding(buf, buf.tell(), 0x200);
|
|
}
|
|
|
|
void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
|
Aes128XtsContext ctx{};
|
|
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
|
|
|
|
u8 sector{};
|
|
for (u64 pos = 0; pos < 0xC00; pos += 0x200) {
|
|
aes128XtsContextResetSector(&ctx, sector++, true);
|
|
aes128XtsEncrypt(&ctx, (u8*)header + pos, (const u8*)header + pos, 0x200);
|
|
}
|
|
}
|
|
|
|
void write_nca_section(NcaHeader& nca_header, u8 index, u64 start, u64 end) {
|
|
auto& section = nca_header.fs_table[index];
|
|
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
|
section.media_end_offset = end / 0x200; // Section end offset / 200
|
|
section._0x8[0] = 0x1; // Always 1
|
|
}
|
|
|
|
void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
|
auto& fs_header = nca_header.fs_header[index];
|
|
fs_header.hash_type = NcaHashType_HierarchicalSha256;
|
|
fs_header.fs_type = NcaFileSystemType_PFS0;
|
|
fs_header.version = 0x2; // Always 2
|
|
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
|
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
|
|
fs_header.encryption_type = NcaEncryptionType_None;
|
|
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
|
|
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
|
|
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
|
}
|
|
|
|
void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) {
|
|
auto& fs_header = nca_header.fs_header[index];
|
|
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
|
|
fs_header.fs_type = NcaFileSystemType_RomFS;
|
|
fs_header.version = 0x2; // Always 2
|
|
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
|
|
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
|
|
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
|
|
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
|
|
fs_header.encryption_type = NcaEncryptionType_None;
|
|
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
|
|
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
|
}
|
|
|
|
void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
|
const auto pfs0 = build_pfs0(entries);
|
|
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
|
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
|
|
|
|
buf.write(pfs0_hash_table.data(), pfs0_hash_table.size());
|
|
const auto padding_size = write_padding(buf, pfs0_hash_table.size(), PFS0_PADDING_SIZE);
|
|
|
|
nca_header.fs_header[index].hash_data.hierarchical_sha256_data.pfs0_layer.offset = pfs0_hash_table.size() + padding_size;
|
|
nca_header.fs_header[index].hash_data.hierarchical_sha256_data.pfs0_layer.size = pfs0.size();
|
|
|
|
buf.write(pfs0.data(), pfs0.size());
|
|
write_nca_padding(buf);
|
|
|
|
const auto section_start = index == 0 ? sizeof(nca_header) : nca_header.fs_table[index-1].media_end_offset * 0x200;
|
|
write_nca_section(nca_header, index, section_start, buf.tell());
|
|
write_nca_fs_header_pfs0(nca_header, index, pfs0_master_hash, pfs0_hash_table.size(), block_size);
|
|
}
|
|
|
|
auto ivfc_create_level(const std::vector<u8>& src) -> std::vector<u8> {
|
|
BufHelper buf;
|
|
u8 hash[SHA256_HASH_SIZE];
|
|
u64 read_size = IVFC_HASH_BLOCK_SIZE;
|
|
|
|
for (u32 i = 0; i < src.size(); i += read_size) {
|
|
if (i + read_size >= src.size()) {
|
|
read_size = src.size() - i;
|
|
}
|
|
sha256CalculateHash(hash, src.data() + i, read_size);
|
|
buf.write(hash, sizeof(hash));
|
|
}
|
|
|
|
write_padding(buf, buf.tell(), IVFC_HASH_BLOCK_SIZE);
|
|
|
|
return buf.buf;
|
|
}
|
|
|
|
void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
|
auto& fs_header = nca_header.fs_header[index];
|
|
auto& meta_info = fs_header.hash_data.integrity_meta_info;
|
|
auto& info_level_hash = meta_info.info_level_hash;
|
|
|
|
std::vector<u8> ivfc[IVFC_MAX_LEVEL];
|
|
|
|
ivfc[5] = romfs_build(entries, &info_level_hash.levels[5].hash_data_size);
|
|
|
|
for (int b = 4; b >= 0; b--) {
|
|
ivfc[b] = ivfc_create_level(ivfc[b + 1]);
|
|
info_level_hash.levels[b].hash_data_size = ivfc[b].size();
|
|
info_level_hash.levels[b].block_size = 0x0E; // 0x4000
|
|
}
|
|
|
|
info_level_hash.levels[0].logical_offset = 0;
|
|
for (int i = 1; i <= 5; i++) {
|
|
info_level_hash.levels[i].logical_offset = info_level_hash.levels[i - 1].logical_offset + info_level_hash.levels[i - 1].hash_data_size;
|
|
}
|
|
|
|
for (const auto& iv : ivfc) {
|
|
buf.write(iv.data(), iv.size());
|
|
}
|
|
|
|
write_nca_padding(buf);
|
|
|
|
const auto ivfc_master_hash = build_ivfc_master_hash(ivfc[0]);
|
|
std::memcpy(meta_info.master_hash, ivfc_master_hash.data(), sizeof(meta_info.master_hash));
|
|
|
|
const auto section_start = index == 0 ? sizeof(nca_header) : nca_header.fs_table[index-1].media_end_offset * 0x200;
|
|
write_nca_section(nca_header, index, section_start, buf.tell());
|
|
write_nca_fs_header_romfs(nca_header, index);
|
|
}
|
|
|
|
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
|
|
nca_header.magic = 0x3341434E;
|
|
nca_header.distribution_type = NcaDistributionType_System;
|
|
nca_header.content_type = type;
|
|
nca_header.title_id = tid;
|
|
nca_header.sdk_version = 0x000C1100;
|
|
nca_header.size = buf.tell();
|
|
|
|
nca_encrypt_header(&nca_header, key);
|
|
buf.seek(0);
|
|
buf.write(&nca_header, sizeof(nca_header));
|
|
}
|
|
|
|
auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
|
BufHelper buf;
|
|
NcaHeader nca_header{};
|
|
buf.write(&nca_header, sizeof(nca_header));
|
|
|
|
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
|
|
write_nca_romfs(nca_header, 1, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
|
// only write logo if set (can only 1 file be added?)
|
|
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
|
|
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
|
|
}
|
|
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf);
|
|
|
|
return {buf, NcmContentType_Program};
|
|
}
|
|
|
|
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
|
|
NcaHeader nca_header{};
|
|
BufHelper buf;
|
|
buf.write(&nca_header, sizeof(nca_header));
|
|
|
|
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
|
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf);
|
|
|
|
return {buf, NcmContentType_Control};
|
|
}
|
|
|
|
auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
|
CnmtHeader cnmt_header{};
|
|
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
|
NcmPackagedContentInfo packaged_content_info[2]{};
|
|
u8 digest[0x20]{};
|
|
BufHelper buf;
|
|
|
|
cnmt_header.title_id = tid;
|
|
cnmt_header.title_version = 0; // todo: parse nacp.disaply_version
|
|
cnmt_header.meta_type = NcmContentMetaType_Application;
|
|
cnmt_header.meta_header.extended_header_size = sizeof(cnmt_extended);
|
|
cnmt_header.meta_header.content_count = 0x2; // program + control
|
|
cnmt_header.meta_header.content_meta_count = 0x1; // only 1 meta
|
|
cnmt_header.meta_header.attributes = 0x0;
|
|
cnmt_header.meta_header.storage_id = storage_id;
|
|
cnmt_extended.patch_id = cnmt_header.title_id | 0x800;
|
|
|
|
for (u32 i = 0; i < ncas.size(); i++) {
|
|
std::memcpy(packaged_content_info[i].hash, ncas[i].hash, sizeof(packaged_content_info[i].hash));
|
|
std::memcpy(&packaged_content_info[i].info.content_id, ncas[i].hash, sizeof(packaged_content_info[i].info.content_id));
|
|
packaged_content_info[i].info.content_type = ncas[i].type;
|
|
ncmU64ToContentInfoSize(ncas[i].data.size(), &packaged_content_info[i].info);
|
|
}
|
|
|
|
// create control
|
|
BufHelper cnmt_buf;
|
|
cnmt_buf.write(&cnmt_header, sizeof(cnmt_header));
|
|
cnmt_buf.write(&cnmt_extended, sizeof(cnmt_extended));
|
|
cnmt_buf.write(&packaged_content_info, sizeof(packaged_content_info));
|
|
cnmt_buf.write(digest, sizeof(digest));
|
|
|
|
FileEntries cnmt;
|
|
char cnmt_name[34];
|
|
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
|
|
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
|
|
|
|
NcaHeader nca_header{};
|
|
buf.write(&nca_header, sizeof(nca_header));
|
|
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
|
|
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf);
|
|
|
|
// entry
|
|
NcaMetaEntry entry{buf, NcmContentType_Meta};
|
|
|
|
// header
|
|
entry.content_meta_header = cnmt_header.meta_header;
|
|
entry.content_meta_header.content_count++;
|
|
entry.content_meta_header.storage_id = 0;
|
|
|
|
// key
|
|
entry.content_meta_key.id = cnmt_header.title_id;
|
|
entry.content_meta_key.version = cnmt_header.title_version;
|
|
entry.content_meta_key.type = cnmt_header.meta_type;
|
|
entry.content_meta_key.install_type = NcmContentInstallType_Full;
|
|
std::memset(entry.content_meta_key.padding, 0, sizeof(entry.content_meta_key.padding));
|
|
|
|
// record
|
|
entry.content_storage_record.key = entry.content_meta_key;
|
|
entry.content_storage_record.storage_id = storage_id;
|
|
std::memset(entry.content_storage_record.padding, 0, sizeof(entry.content_storage_record.padding));
|
|
|
|
// data
|
|
entry.content_meta_data.header = entry.content_meta_header;
|
|
entry.content_meta_data.extended = cnmt_extended;
|
|
|
|
// meta content info
|
|
std::memcpy(&entry.content_meta_data.infos[0].content_id, entry.nca_entry.hash, sizeof(entry.content_meta_data.infos[0].content_id));
|
|
entry.content_meta_data.infos[0].content_type = entry.nca_entry.type;
|
|
entry.content_meta_data.infos[0].attr = 0;
|
|
ncmU64ToContentInfoSize(cnmt_buf.buf.size(), &entry.content_meta_data.infos[0]);
|
|
entry.content_meta_data.infos[0].id_offset = 0;
|
|
|
|
// program + control content info
|
|
entry.content_meta_data.infos[1] = packaged_content_info[0].info;
|
|
entry.content_meta_data.infos[2] = packaged_content_info[1].info;
|
|
|
|
return entry;
|
|
}
|
|
|
|
Result nsDeleteApplicationRecord(Service* srv, u64 tid) {
|
|
return serviceDispatchIn(srv, 27, tid);
|
|
}
|
|
|
|
Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) {
|
|
const struct {
|
|
u8 last_modified_event;
|
|
u8 padding[0x7];
|
|
u64 tid;
|
|
} in = { NsApplicationRecordType_Installed, {0}, tid };
|
|
|
|
return serviceDispatchIn(srv, 16, in,
|
|
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
|
.buffers = { { records, sizeof(NcmContentStorageRecord) * count } });
|
|
}
|
|
|
|
Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) {
|
|
return serviceDispatchIn(srv, 404, tid);
|
|
}
|
|
|
|
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
|
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
|
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
|
|
|
R_TRY(splCryptoInitialize());
|
|
ON_SCOPE_EXIT(splCryptoExit());
|
|
|
|
R_TRY(ncmInitialize());
|
|
ON_SCOPE_EXIT(ncmExit());
|
|
|
|
R_TRY(nsInitialize());
|
|
ON_SCOPE_EXIT(nsExit());
|
|
|
|
// generate header kek
|
|
u8 header_kek[0x20];
|
|
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
|
// gen header key 0
|
|
u8 key[0x20];
|
|
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key));
|
|
// gen header key 1
|
|
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10));
|
|
|
|
// fix args to include nro path
|
|
if (config.args.empty()) {
|
|
config.args = config.nro_path;
|
|
} else {
|
|
config.args = config.nro_path + ' ' + config.args;
|
|
}
|
|
|
|
// create tid by using a hash over path + args
|
|
u64 hash_data[SHA256_HASH_SIZE / sizeof(u64)];
|
|
const auto hash_path = config.nro_path + config.args;
|
|
sha256CalculateHash(hash_data, hash_path.data(), hash_path.length());
|
|
const u64 tid = 0x0100000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
|
|
|
|
std::vector<NcaEntry> nca_entries;
|
|
|
|
// create program
|
|
if (config.program_nca.empty()) {
|
|
R_UNLESS(!config.main.empty(), OwoError_BadArgs);
|
|
R_UNLESS(!config.npdm.empty(), OwoError_BadArgs);
|
|
|
|
pbox->NewTransfer("Creating Program"_i18n).UpdateTransfer(0, 8);
|
|
FileEntries exefs;
|
|
add_file_entry(exefs, "main", config.main);
|
|
add_file_entry(exefs, "main.npdm", config.npdm);
|
|
|
|
FileEntries romfs;
|
|
add_file_entry(romfs, "/nextArgv", config.args.data(), config.args.length());
|
|
add_file_entry(romfs, "/nextNroPath", config.nro_path.data(), config.nro_path.length());
|
|
|
|
FileEntries logo;
|
|
if (!config.logo.empty()) {
|
|
add_file_entry(logo, "NintendoLogo.png", config.logo);
|
|
}
|
|
if (!config.gif.empty()) {
|
|
add_file_entry(logo, "StartupMovie.gif", config.gif);
|
|
}
|
|
|
|
NpdmPatch npdm_patch;
|
|
npdm_patch.tid = tid;
|
|
patch_npdm(exefs[1].data, npdm_patch);
|
|
|
|
nca_entries.emplace_back(
|
|
create_program_nca(tid, key, exefs, romfs, logo)
|
|
);
|
|
} else {
|
|
nca_entries.emplace_back(
|
|
BufHelper{config.program_nca}, NcmContentType_Program
|
|
);
|
|
}
|
|
|
|
// create control
|
|
{
|
|
pbox->NewTransfer("Creating Control"_i18n).UpdateTransfer(1, 8);
|
|
// patch nacp
|
|
NcapPatch nacp_patch{};
|
|
nacp_patch.tid = tid;
|
|
nacp_patch.name = config.name;
|
|
nacp_patch.author = config.author;
|
|
patch_nacp(config.nacp, nacp_patch);
|
|
|
|
FileEntries romfs;
|
|
add_file_entry(romfs, "/control.nacp", &config.nacp, sizeof(config.nacp));
|
|
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
|
|
|
|
nca_entries.emplace_back(
|
|
create_control_nca(tid, key, romfs)
|
|
);
|
|
}
|
|
|
|
// create meta
|
|
NcmContentMetaHeader content_meta_header;
|
|
NcmContentMetaKey content_meta_key;
|
|
NcmContentStorageRecord content_storage_record;
|
|
NcmContentMetaData content_meta_data;
|
|
{
|
|
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
|
|
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
|
|
|
|
nca_entries.emplace_back(meta_entry.nca_entry);
|
|
content_meta_header = meta_entry.content_meta_header;
|
|
content_meta_key = meta_entry.content_meta_key;
|
|
content_storage_record = meta_entry.content_storage_record;
|
|
content_meta_data = meta_entry.content_meta_data;
|
|
}
|
|
|
|
// write ncas
|
|
{
|
|
NcmContentStorage cs;
|
|
R_TRY(ncmOpenContentStorage(&cs, storage_id));
|
|
ON_SCOPE_EXIT(ncmContentStorageClose(&cs));
|
|
|
|
for (const auto& nca : nca_entries) {
|
|
pbox->NewTransfer("Writing Nca"_i18n).UpdateTransfer(3, 8);
|
|
NcmContentId content_id;
|
|
NcmPlaceHolderId placeholder_id;
|
|
std::memcpy(&content_id, nca.hash, sizeof(content_id));
|
|
R_TRY(ncmContentStorageGeneratePlaceHolderId(&cs, &placeholder_id));
|
|
ncmContentStorageDeletePlaceHolder(&cs, &placeholder_id);
|
|
R_TRY(ncmContentStorageCreatePlaceHolder(&cs, &content_id, &placeholder_id, nca.data.size()));
|
|
R_TRY(ncmContentStorageWritePlaceHolder(&cs, &placeholder_id, 0, nca.data.data(), nca.data.size()));
|
|
ncmContentStorageDelete(&cs, &content_id);
|
|
R_TRY(ncmContentStorageRegister(&cs, &content_id, &placeholder_id));
|
|
}
|
|
}
|
|
|
|
// setup database
|
|
{
|
|
pbox->NewTransfer("Updating ncm databse"_i18n).UpdateTransfer(4, 8);
|
|
NcmContentMetaDatabase db;
|
|
R_TRY(ncmOpenContentMetaDatabase(&db, storage_id));
|
|
ON_SCOPE_EXIT(ncmContentMetaDatabaseClose(&db));
|
|
|
|
R_TRY(ncmContentMetaDatabaseSet(&db, &content_meta_key, &content_meta_data, sizeof(content_meta_data)));
|
|
R_TRY(ncmContentMetaDatabaseCommit(&db));
|
|
}
|
|
|
|
// push record
|
|
{
|
|
pbox->NewTransfer("Pushing application record"_i18n).UpdateTransfer(5, 8);
|
|
Service srv{}, *srv_ptr = &srv;
|
|
bool already_installed{};
|
|
|
|
if (hosversionAtLeast(3,0,0)) {
|
|
R_TRY(nsGetApplicationManagerInterface(&srv));
|
|
} else {
|
|
srv_ptr = nsGetServiceSession_ApplicationManagerInterface();
|
|
}
|
|
ON_SCOPE_EXIT(serviceClose(&srv));
|
|
|
|
|
|
if (hosversionAtLeast(2,0,0)) {
|
|
R_TRY(nsIsAnyApplicationEntityInstalled(tid, &already_installed));
|
|
}
|
|
|
|
// remove previous application record
|
|
if (already_installed || hosversionBefore(2,0,0)) {
|
|
const auto rc = nsDeleteApplicationRecord(srv_ptr, tid);
|
|
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
|
}
|
|
|
|
R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
|
|
|
// force flush
|
|
if (already_installed || hosversionBefore(2,0,0)) {
|
|
const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid);
|
|
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
|
}
|
|
}
|
|
|
|
R_SUCCEED();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
|
return install_forwader_internal(pbox, config, storage_id);
|
|
}
|
|
|
|
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
|
|
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
|
|
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
|
|
}));
|
|
R_SUCCEED();
|
|
}
|
|
|
|
} // namespace sphaira
|