Z3DS: Add title info and smdh to metadata

This commit is contained in:
PabloMK7 2025-07-16 20:38:59 +02:00
parent db8aaacb35
commit 00c0f01e73
14 changed files with 155 additions and 16 deletions

View File

@ -3076,12 +3076,24 @@ void GMainWindow::OnCompressFile() {
bool is_compressed = false;
if (Service::AM::CheckCIAToInstall(in_path, is_compressed, true) ==
Service::AM::InstallStatus::Success) {
auto meta_info = Service::AM::GetCIAInfos(in_path);
compress_info.is_supported = true;
compress_info.is_compressed = is_compressed;
compress_info.recommended_compressed_extension = "zcia";
compress_info.recommended_uncompressed_extension = "cia";
compress_info.underlying_magic = std::array<u8, 4>({'C', 'I', 'A', '\0'});
frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_CIA_FRAME_SIZE;
if (meta_info.Succeeded()) {
const auto& meta_info_val = meta_info.Unwrap();
std::vector<u8> value(sizeof(Service::AM::TitleInfo));
memcpy(value.data(), &meta_info_val.first, sizeof(Service::AM::TitleInfo));
compress_info.default_metadata.emplace("titleinfo", value);
if (meta_info_val.second) {
value.resize(sizeof(Loader::SMDH));
memcpy(value.data(), meta_info_val.second.get(), sizeof(Loader::SMDH));
compress_info.default_metadata.emplace("smdh", value);
}
}
}
}
}
@ -3121,8 +3133,9 @@ void GMainWindow::OnCompressFile() {
const auto progress = [&](std::size_t written, std::size_t total) {
emit UpdateProgress(written, total);
};
bool success = FileUtil::CompressZ3DSFile(in_path, out_path, compress_info.underlying_magic,
frame_size, progress);
bool success =
FileUtil::CompressZ3DSFile(in_path, out_path, compress_info.underlying_magic,
frame_size, progress, compress_info.default_metadata);
if (!success) {
FileUtil::Delete(out_path);
}

View File

@ -121,6 +121,7 @@ Z3DSMetadata::Z3DSMetadata(const std::span<u8>& source_data) {
}
// Only binary type supported for now
if (item.type != Item::TYPE_BINARY) {
in.ignore(static_cast<std::streamsize>(item.name_len) + item.data_len);
continue;
}
std::string name(item.name_len, '\0');
@ -665,7 +666,8 @@ void Z3DSReadIOFile::serialize(Archive& ar, const unsigned int) {
bool CompressZ3DSFile(const std::string& src_file_name, const std::string& dst_file_name,
const std::array<u8, 4>& underlying_magic, size_t frame_size,
std::function<ProgressCallback>&& update_callback) {
std::function<ProgressCallback>&& update_callback,
std::unordered_map<std::string, std::vector<u8>> metadata) {
IOFile in_file(src_file_name, "rb");
if (!in_file.IsOpen()) {
@ -687,6 +689,12 @@ bool CompressZ3DSFile(const std::string& src_file_name, const std::string& dst_f
Z3DSWriteIOFile out_compress_file(std::move(out_file), underlying_magic, frame_size);
for (auto& it : metadata) {
std::string val_str(it.second.size(), '\0');
memcpy(val_str.data(), it.second.data(), val_str.size());
out_compress_file.Metadata().Add(it.first, val_str);
}
size_t next_chunk = out_compress_file.GetNextWriteHint();
std::vector<u8> buffer(next_chunk);
size_t in_size = in_file.GetSize();

View File

@ -261,7 +261,8 @@ using ProgressCallback = void(std::size_t, std::size_t);
bool CompressZ3DSFile(const std::string& src_file, const std::string& dst_file,
const std::array<u8, 4>& underlying_magic, size_t frame_size,
std::function<ProgressCallback>&& update_callback = nullptr);
std::function<ProgressCallback>&& update_callback = nullptr,
std::unordered_map<std::string, std::vector<u8>> metadata = {});
bool DeCompressZ3DSFile(const std::string& src_file, const std::string& dst_file,
std::function<ProgressCallback>&& update_callback = nullptr);

View File

@ -54,6 +54,17 @@ Loader::ResultStatus CIAContainer::Load(const FileBackend& backend) {
result = LoadMetadata(meta_data);
if (result != Loader::ResultStatus::Success)
return result;
if (cia_header.meta_size >= sizeof(Metadata) + sizeof(Loader::SMDH)) {
std::vector<u8> smdh_data(sizeof(Loader::SMDH));
read_result = backend.Read(GetMetadataOffset() + CIA_METADATA_SIZE,
sizeof(Loader::SMDH), smdh_data.data());
if (read_result.Failed() || *read_result != sizeof(Loader::SMDH))
return Loader::ResultStatus::Error;
result = LoadSMDH(smdh_data);
if (result != Loader::ResultStatus::Success)
return result;
}
}
return Loader::ResultStatus::Success;
@ -102,6 +113,16 @@ Loader::ResultStatus CIAContainer::Load(FileUtil::IOFile* file) {
result = LoadMetadata(meta_data);
if (result != Loader::ResultStatus::Success)
return result;
if (cia_header.meta_size >= sizeof(Metadata) + sizeof(Loader::SMDH)) {
std::vector<u8> smdh_data(sizeof(Loader::SMDH));
file->Seek(GetMetadataOffset() + CIA_METADATA_SIZE, SEEK_SET);
if (file->ReadBytes(smdh_data.data(), sizeof(Loader::SMDH)) != sizeof(Loader::SMDH))
return Loader::ResultStatus::Error;
result = LoadSMDH(smdh_data);
if (result != Loader::ResultStatus::Success)
return result;
}
}
return Loader::ResultStatus::Success;
@ -127,6 +148,11 @@ Loader::ResultStatus CIAContainer::Load(std::span<const u8> file_data) {
result = LoadMetadata(file_data, GetMetadataOffset());
if (result != Loader::ResultStatus::Success)
return result;
if (cia_header.meta_size >= sizeof(Metadata) + sizeof(Loader::SMDH)) {
result = LoadSMDH(file_data, GetMetadataOffset() + CIA_METADATA_SIZE);
if (result != Loader::ResultStatus::Success)
return result;
}
}
return Loader::ResultStatus::Success;
@ -172,6 +198,18 @@ Loader::ResultStatus CIAContainer::LoadMetadata(std::span<const u8> meta_data, s
return Loader::ResultStatus::Success;
}
Loader::ResultStatus CIAContainer::LoadSMDH(std::span<const u8> smdh_data, std::size_t offset) {
if (smdh_data.size() - offset < sizeof(Loader::SMDH)) {
return Loader::ResultStatus::Error;
}
cia_smdh = std::make_unique<Loader::SMDH>();
std::memcpy(cia_smdh.get(), smdh_data.data(), sizeof(Loader::SMDH));
return Loader::ResultStatus::Success;
}
Ticket& CIAContainer::GetTicket() {
return cia_ticket;
}
@ -188,6 +226,10 @@ u32 CIAContainer::GetCoreVersion() const {
return cia_metadata.core_version;
}
const std::unique_ptr<Loader::SMDH>& CIAContainer::GetSMDH() const {
return cia_smdh;
}
u64 CIAContainer::GetCertificateOffset() const {
return Common::AlignUp(cia_header.header_size, CIA_SECTION_ALIGNMENT);
}

View File

@ -13,6 +13,7 @@
#include "common/swap.h"
#include "core/file_sys/ticket.h"
#include "core/file_sys/title_metadata.h"
#include "core/loader/smdh.h"
namespace Loader {
enum class ResultStatus;
@ -73,12 +74,14 @@ public:
Loader::ResultStatus LoadTitleMetadata(std::span<const u8> tmd_data, std::size_t offset = 0);
Loader::ResultStatus LoadTitleMetadata(const TitleMetadata& tmd);
Loader::ResultStatus LoadMetadata(std::span<const u8> meta_data, std::size_t offset = 0);
Loader::ResultStatus LoadSMDH(std::span<const u8> smdh_data, std::size_t offset = 0);
const CIAHeader* GetHeader();
Ticket& GetTicket();
const TitleMetadata& GetTitleMetadata() const;
std::array<u64, 0x30>& GetDependencies();
u32 GetCoreVersion() const;
const std::unique_ptr<Loader::SMDH>& GetSMDH() const;
u64 GetCertificateOffset() const;
u64 GetTicketOffset() const;
@ -108,6 +111,7 @@ private:
bool has_header = false;
CIAHeader cia_header;
Metadata cia_metadata;
std::unique_ptr<Loader::SMDH> cia_smdh;
Ticket cia_ticket;
TitleMetadata cia_tmd;
};

View File

@ -91,6 +91,10 @@ struct NCCH_Header {
u8 reserved_4[4];
u8 exefs_super_block_hash[0x20];
u8 romfs_super_block_hash[0x20];
u32 GetContentUnitSize() {
return 0x200u * (1u << content_unit_size);
}
};
static_assert(sizeof(NCCH_Header) == 0x200, "NCCH header structure size is wrong");

View File

@ -176,6 +176,17 @@ u64 TitleMetadata::GetContentSizeByIndex(std::size_t index) const {
return tmd_chunks[index].size;
}
u64 TitleMetadata::GetCombinedContentSize(const CIAHeader* header) const {
u64 total_size = 0;
for (auto& chunk : tmd_chunks) {
if (header && !header->IsContentPresent(static_cast<u16>(chunk.index))) {
continue;
}
total_size += chunk.size;
}
return total_size;
}
bool TitleMetadata::GetContentOptional(std::size_t index) const {
return (static_cast<u16>(tmd_chunks[index].type) & FileSys::TMDContentTypeFlag::Optional) != 0;
}

View File

@ -99,6 +99,7 @@ public:
u32 GetContentIDByIndex(std::size_t index) const;
u16 GetContentTypeByIndex(std::size_t index) const;
u64 GetContentSizeByIndex(std::size_t index) const;
u64 GetCombinedContentSize(const CIAHeader* header) const;
bool GetContentOptional(std::size_t index) const;
std::array<u8, 16> GetContentCTRByIndex(std::size_t index) const;
bool HasEncryptedContent(const CIAHeader* header = nullptr) const;

View File

@ -56,16 +56,6 @@ constexpr u8 VARIATION_SYSTEM = 0x02;
constexpr u32 TID_HIGH_UPDATE = 0x0004000E;
constexpr u32 TID_HIGH_DLC = 0x0004008C;
struct TitleInfo {
u64_le tid;
u64_le size;
u16_le version;
u16_le unused;
u32_le type;
};
static_assert(sizeof(TitleInfo) == 0x18, "Title info structure size is wrong");
constexpr u8 OWNERSHIP_DOWNLOADED = 0x01;
constexpr u8 OWNERSHIP_OWNED = 0x02;
@ -1183,6 +1173,37 @@ InstallStatus CheckCIAToInstall(const std::string& path, bool& is_compressed,
return InstallStatus::ErrorInvalid;
}
ResultVal<std::pair<TitleInfo, std::unique_ptr<Loader::SMDH>>> GetCIAInfos(
const std::string& path) {
if (!FileUtil::Exists(path)) {
LOG_ERROR(Service_AM, "File {} does not exist!", path);
return ResultUnknown;
}
std::unique_ptr<FileUtil::IOFile> in_file = std::make_unique<FileUtil::IOFile>(path, "rb");
FileSys::CIAContainer container;
if (container.Load(in_file.get()) == Loader::ResultStatus::Success) {
in_file->Seek(0, SEEK_SET);
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
TitleInfo info{};
info.tid = tmd.GetTitleID();
info.version = tmd.GetTitleVersion();
info.size = tmd.GetCombinedContentSize(container.GetHeader());
info.type = tmd.GetTitleType();
const auto& cia_smdh = container.GetSMDH();
std::unique_ptr<Loader::SMDH> smdh{};
if (cia_smdh) {
smdh = std::make_unique<Loader::SMDH>(*cia_smdh);
}
return std::pair<TitleInfo, std::unique_ptr<Loader::SMDH>>(info, std::move(smdh));
}
return ResultUnknown;
}
u64 GetTitleUpdateId(u64 title_id) {
// Real services seem to just discard and replace the whole high word.
return (title_id & 0xFFFFFFFF) | (static_cast<u64>(TID_HIGH_UPDATE) << 32);

View File

@ -105,6 +105,15 @@ struct ImportContentContext {
};
static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size");
struct TitleInfo {
u64_le tid;
u64_le size;
u16_le version;
u16_le unused;
u32_le type;
};
static_assert(sizeof(TitleInfo) == 0x18, "Title info structure size is wrong");
// Title ID valid length
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
@ -367,6 +376,11 @@ InstallStatus InstallCIA(const std::string& path,
InstallStatus CheckCIAToInstall(const std::string& path, bool& is_compressed,
bool check_encryption);
/**
* Get CIA metadata information from file.
*/
ResultVal<std::pair<TitleInfo, std::unique_ptr<Loader::SMDH>>> GetCIAInfos(const std::string& path);
/**
* Get the update title ID for a title
* @param titleId the title ID

View File

@ -91,6 +91,7 @@ public:
std::array<u8, 4> underlying_magic{};
std::string recommended_compressed_extension;
std::string recommended_uncompressed_extension;
std::unordered_map<std::string, std::vector<u8>> default_metadata;
};
explicit AppLoader(Core::System& system_, FileUtil::IOFile&& file)

View File

@ -424,6 +424,16 @@ AppLoader::CompressFileInfo AppLoader_NCCH::GetCompressFileInfo() {
info.recommended_compressed_extension = "zcxi";
info.recommended_uncompressed_extension = "cxi";
}
std::vector<u8> title_info_vec(sizeof(Service::AM::TitleInfo));
Service::AM::TitleInfo* title_info =
reinterpret_cast<Service::AM::TitleInfo*>(title_info_vec.data());
title_info->tid = base_ncch.ncch_header.program_id;
title_info->version = base_ncch.ncch_header.version;
title_info->size =
base_ncch.ncch_header.content_size * base_ncch.ncch_header.GetContentUnitSize();
title_info->unused = title_info->type = 0;
info.default_metadata.emplace("titleinfo", title_info_vec);
return info;
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -22,6 +22,10 @@ bool IsValidSMDH(std::span<const u8> smdh_data) {
return Loader::MakeMagic('S', 'M', 'D', 'H') == magic;
}
bool SMDH::IsValid() const {
return Loader::MakeMagic('S', 'M', 'D', 'H') == magic;
}
std::vector<u16> SMDH::GetIcon(bool large) const {
u32 size;
const u8* icon_data;

View File

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -77,6 +77,11 @@ struct SMDH {
Visible = 1 << 0,
};
/**
* Checks if SMDH is valid.
*/
bool IsValid() const;
/**
* Gets game icon from SMDH
* @param large If true, returns large icon (48x48), otherwise returns small icon (24x24)