diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index da988cc0f..01d92ef20 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -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({'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 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); } diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index 694b11011..1e38877a1 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -121,6 +121,7 @@ Z3DSMetadata::Z3DSMetadata(const std::span& source_data) { } // Only binary type supported for now if (item.type != Item::TYPE_BINARY) { + in.ignore(static_cast(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& underlying_magic, size_t frame_size, - std::function&& update_callback) { + std::function&& update_callback, + std::unordered_map> 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 buffer(next_chunk); size_t in_size = in_file.GetSize(); diff --git a/src/common/zstd_compression.h b/src/common/zstd_compression.h index 590a40fcd..75aceab2e 100644 --- a/src/common/zstd_compression.h +++ b/src/common/zstd_compression.h @@ -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& underlying_magic, size_t frame_size, - std::function&& update_callback = nullptr); + std::function&& update_callback = nullptr, + std::unordered_map> metadata = {}); bool DeCompressZ3DSFile(const std::string& src_file, const std::string& dst_file, std::function&& update_callback = nullptr); diff --git a/src/core/file_sys/cia_container.cpp b/src/core/file_sys/cia_container.cpp index 2c9f39853..570cd9b45 100644 --- a/src/core/file_sys/cia_container.cpp +++ b/src/core/file_sys/cia_container.cpp @@ -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 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 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 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 meta_data, s return Loader::ResultStatus::Success; } +Loader::ResultStatus CIAContainer::LoadSMDH(std::span smdh_data, std::size_t offset) { + if (smdh_data.size() - offset < sizeof(Loader::SMDH)) { + return Loader::ResultStatus::Error; + } + + cia_smdh = std::make_unique(); + + 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& CIAContainer::GetSMDH() const { + return cia_smdh; +} + u64 CIAContainer::GetCertificateOffset() const { return Common::AlignUp(cia_header.header_size, CIA_SECTION_ALIGNMENT); } diff --git a/src/core/file_sys/cia_container.h b/src/core/file_sys/cia_container.h index a2d210a77..ec4108b7d 100644 --- a/src/core/file_sys/cia_container.h +++ b/src/core/file_sys/cia_container.h @@ -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 tmd_data, std::size_t offset = 0); Loader::ResultStatus LoadTitleMetadata(const TitleMetadata& tmd); Loader::ResultStatus LoadMetadata(std::span meta_data, std::size_t offset = 0); + Loader::ResultStatus LoadSMDH(std::span smdh_data, std::size_t offset = 0); const CIAHeader* GetHeader(); Ticket& GetTicket(); const TitleMetadata& GetTitleMetadata() const; std::array& GetDependencies(); u32 GetCoreVersion() const; + const std::unique_ptr& 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 cia_smdh; Ticket cia_ticket; TitleMetadata cia_tmd; }; diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h index 675228499..c7984d338 100644 --- a/src/core/file_sys/ncch_container.h +++ b/src/core/file_sys/ncch_container.h @@ -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"); diff --git a/src/core/file_sys/title_metadata.cpp b/src/core/file_sys/title_metadata.cpp index d0cc2838e..0defd61c6 100644 --- a/src/core/file_sys/title_metadata.cpp +++ b/src/core/file_sys/title_metadata.cpp @@ -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(chunk.index))) { + continue; + } + total_size += chunk.size; + } + return total_size; +} + bool TitleMetadata::GetContentOptional(std::size_t index) const { return (static_cast(tmd_chunks[index].type) & FileSys::TMDContentTypeFlag::Optional) != 0; } diff --git a/src/core/file_sys/title_metadata.h b/src/core/file_sys/title_metadata.h index d1ca730a6..f74037b01 100644 --- a/src/core/file_sys/title_metadata.h +++ b/src/core/file_sys/title_metadata.h @@ -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 GetContentCTRByIndex(std::size_t index) const; bool HasEncryptedContent(const CIAHeader* header = nullptr) const; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 211855899..7dd42cecc 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -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>> GetCIAInfos( + const std::string& path) { + if (!FileUtil::Exists(path)) { + LOG_ERROR(Service_AM, "File {} does not exist!", path); + return ResultUnknown; + } + + std::unique_ptr in_file = std::make_unique(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 smdh{}; + if (cia_smdh) { + smdh = std::make_unique(*cia_smdh); + } + + return std::pair>(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(TID_HIGH_UPDATE) << 32); diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 5fd52811a..5f92d7adc 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -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>> GetCIAInfos(const std::string& path); + /** * Get the update title ID for a title * @param titleId the title ID diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 2f2d6f3c3..a98536d1c 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -91,6 +91,7 @@ public: std::array underlying_magic{}; std::string recommended_compressed_extension; std::string recommended_uncompressed_extension; + std::unordered_map> default_metadata; }; explicit AppLoader(Core::System& system_, FileUtil::IOFile&& file) diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 9e4defaf6..05fe13dee 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -424,6 +424,16 @@ AppLoader::CompressFileInfo AppLoader_NCCH::GetCompressFileInfo() { info.recommended_compressed_extension = "zcxi"; info.recommended_uncompressed_extension = "cxi"; } + std::vector title_info_vec(sizeof(Service::AM::TitleInfo)); + Service::AM::TitleInfo* title_info = + reinterpret_cast(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; } diff --git a/src/core/loader/smdh.cpp b/src/core/loader/smdh.cpp index 68eb2f1ee..c0f0d6aeb 100644 --- a/src/core/loader/smdh.cpp +++ b/src/core/loader/smdh.cpp @@ -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 smdh_data) { return Loader::MakeMagic('S', 'M', 'D', 'H') == magic; } +bool SMDH::IsValid() const { + return Loader::MakeMagic('S', 'M', 'D', 'H') == magic; +} + std::vector SMDH::GetIcon(bool large) const { u32 size; const u8* icon_data; diff --git a/src/core/loader/smdh.h b/src/core/loader/smdh.h index 9f157a68a..2913b195b 100644 --- a/src/core/loader/smdh.h +++ b/src/core/loader/smdh.h @@ -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)