diff --git a/src/core/file_sys/cia_container.cpp b/src/core/file_sys/cia_container.cpp index 3297b96b0..98ec7f3df 100644 --- a/src/core/file_sys/cia_container.cpp +++ b/src/core/file_sys/cia_container.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -157,6 +157,11 @@ Loader::ResultStatus CIAContainer::LoadTitleMetadata(std::span tmd_dat return cia_tmd.Load(tmd_data, offset); } +Loader::ResultStatus CIAContainer::LoadTitleMetadata(const TitleMetadata& tmd) { + cia_tmd = tmd; + return Loader::ResultStatus::Success; +} + Loader::ResultStatus CIAContainer::LoadMetadata(std::span meta_data, std::size_t offset) { if (meta_data.size() - offset < sizeof(Metadata)) { return Loader::ResultStatus::Error; @@ -167,7 +172,7 @@ Loader::ResultStatus CIAContainer::LoadMetadata(std::span meta_data, s return Loader::ResultStatus::Success; } -const Ticket& CIAContainer::GetTicket() const { +Ticket& CIAContainer::GetTicket() { return cia_ticket; } diff --git a/src/core/file_sys/cia_container.h b/src/core/file_sys/cia_container.h index d7fa46dad..e36a12dba 100644 --- a/src/core/file_sys/cia_container.h +++ b/src/core/file_sys/cia_container.h @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -46,9 +46,10 @@ public: Loader::ResultStatus LoadTicket(std::span ticket_data, std::size_t offset = 0); Loader::ResultStatus LoadTicket(const Ticket& ticket); 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); - const Ticket& GetTicket() const; + Ticket& GetTicket(); const TitleMetadata& GetTitleMetadata() const; std::array& GetDependencies(); u32 GetCoreVersion() const; diff --git a/src/core/file_sys/ticket.cpp b/src/core/file_sys/ticket.cpp index 1979755a7..76c4cbae5 100644 --- a/src/core/file_sys/ticket.cpp +++ b/src/core/file_sys/ticket.cpp @@ -100,8 +100,11 @@ Loader::ResultStatus Ticket::Load(std::span file_data, std::size_t off if (total_size < content_index_end) return Loader::ResultStatus::Error; - content_index.resize(content_index_size); - std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size); + std::vector content_index_vec; + content_index_vec.resize(content_index_size); + std::memcpy(content_index_vec.data(), &file_data[offset + content_index_start], + content_index_size); + content_index.Load(this, content_index_vec); return Loader::ResultStatus::Success; } @@ -132,7 +135,7 @@ std::vector Ticket::Serialize() const { reinterpret_cast(&ticket_body) + sizeof(ticket_body)}; ret.insert(ret.end(), body_span.begin(), body_span.end()); - ret.insert(ret.end(), content_index.begin(), content_index.end()); + ret.insert(ret.end(), content_index.GetRaw().cbegin(), content_index.GetRaw().cend()); return ret; } @@ -167,7 +170,7 @@ std::optional> Ticket::GetTitleKey() const { return title_key; } -bool Ticket::IsPersonal() { +bool Ticket::IsPersonal() const { if (ticket_body.console_id == 0u) { // Common ticket return false; @@ -182,4 +185,88 @@ bool Ticket::IsPersonal() { return ticket_body.console_id == otp.GetDeviceID(); } +void Ticket::ContentIndex::Initialize() { + if (!parent || initialized) { + return; + } + + if (content_index.size() < sizeof(MainHeader)) { + LOG_ERROR(Service_FS, "Ticket content index is too small"); + return; + } + MainHeader* main_header = reinterpret_cast(content_index.data()); + if (main_header->always1 != 1 || main_header->header_size != sizeof(MainHeader) || + main_header->context_index_size != content_index.size() || + main_header->index_header_size != sizeof(IndexHeader)) { + u16 always1 = main_header->always1; + u16 header_size = main_header->header_size; + u32 context_index_size = main_header->context_index_size; + u16 index_header_size = main_header->index_header_size; + LOG_ERROR(Service_FS, + "Ticket content index has unexpected parameters title_id={}, ticket_id={}, " + "always1={}, header_size={}, " + "size={}, index_header_size={}", + parent->GetTitleID(), parent->GetTicketID(), always1, header_size, + context_index_size, index_header_size); + return; + } + for (u32 i = 0; i < main_header->index_headers_count; i++) { + IndexHeader* curr_header = reinterpret_cast( + content_index.data() + main_header->index_headers_offset + + main_header->index_header_size * i); + if (curr_header->type != 3 || curr_header->entry_size != sizeof(RightsField)) { + u16 type = curr_header->type; + LOG_WARNING(Service_FS, + "Found unsupported index header type, skiping... title_id={}, " + "ticket_id={}, type={}", + parent->GetTitleID(), parent->GetTicketID(), type); + continue; + } + for (u32 j = 0; j < curr_header->entry_count; j++) { + RightsField* field = reinterpret_cast( + content_index.data() + curr_header->data_offset + curr_header->entry_size * j); + rights.push_back(*field); + } + } + initialized = true; +} + +bool Ticket::ContentIndex::HasRights(u16 content_index) { + if (!initialized) { + Initialize(); + if (!initialized) + return false; + } + // From: + // https://github.com/d0k3/GodMode9/blob/4424c37a89337ffb074c80807da1e80f358779b7/arm9/source/game/ticket.c#L198 + if (rights.empty()) { + return content_index < 256; // when no fields, true if below 256 + } + + bool has_right = false; + + // it loops until one of these happens: + // - we run out of bit fields + // - at the first encounter of an index offset field that's bigger than index + // - at the first encounter of a positive indicator of content rights + for (u32 i = 0; i < rights.size(); i++) { + u16 start_index = rights[i].start_index; + if (content_index < start_index) { + break; + } + + u16 bit_pos = content_index - start_index; + if (bit_pos >= 1024) { + continue; // not in this field + } + + if (rights[i].rights[bit_pos / 8] & (1 << (bit_pos % 8))) { + has_right = true; + break; + } + } + + return has_right; +} + } // namespace FileSys diff --git a/src/core/file_sys/ticket.h b/src/core/file_sys/ticket.h index ed6d379c0..db97cf09f 100644 --- a/src/core/file_sys/ticket.h +++ b/src/core/file_sys/ticket.h @@ -19,6 +19,12 @@ enum class ResultStatus; namespace FileSys { class Ticket { + struct LimitEntry { + u32_be type; // 4 -> Play times? + u32_be value; + }; + static_assert(sizeof(LimitEntry) == 0x8, "LimitEntry structure size is wrong"); + public: #pragma pack(push, 1) struct Body { @@ -42,7 +48,7 @@ public: INSERT_PADDING_BYTES(1); u8 audit; INSERT_PADDING_BYTES(0x42); - std::array limits; + std::array limits; }; static_assert(sizeof(Body) == 0x164, "Ticket body structure size is wrong"); #pragma pack(pop) @@ -67,13 +73,66 @@ public: return serialized_size; } - bool IsPersonal(); + bool IsPersonal() const; + + bool HasRights(u16 index) { + return content_index.HasRights(index); + } + + class ContentIndex { + public: + struct MainHeader { + u16_be always1; + u16_be header_size; + u32_be context_index_size; + u32_be index_headers_offset; + u16_be index_headers_count; + u16_be index_header_size; + u32_be padding; + }; + + struct IndexHeader { + u32_be data_offset; + u32_be entry_count; + u32_be entry_size; + u32_be total_size; + u16_be type; + u16_be padding; + }; + + struct RightsField { + u16_be unknown; + u16_be start_index; + std::array rights; + }; + + ContentIndex() {} + + void Load(Ticket* p, const std::vector& data) { + parent = p; + content_index = data; + } + + const std::vector& GetRaw() const { + return content_index; + } + + bool HasRights(u16 content_index); + + private: + void Initialize(); + + bool initialized = false; + std::vector content_index; + std::vector rights; + Ticket* parent = nullptr; + }; private: Body ticket_body; u32_be signature_type; std::vector ticket_signature; - std::vector content_index; + ContentIndex content_index; size_t serialized_size = 0; }; diff --git a/src/core/file_sys/title_metadata.cpp b/src/core/file_sys/title_metadata.cpp index 794a14398..09857078f 100644 --- a/src/core/file_sys/title_metadata.cpp +++ b/src/core/file_sys/title_metadata.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -175,6 +175,10 @@ u64 TitleMetadata::GetContentSizeByIndex(std::size_t index) const { return tmd_chunks[index].size; } +bool TitleMetadata::GetContentOptional(std::size_t index) const { + return (static_cast(tmd_chunks[index].type) & FileSys::TMDContentTypeFlag::Optional) != 0; +} + std::array TitleMetadata::GetContentCTRByIndex(std::size_t index) const { std::array ctr{}; std::memcpy(ctr.data(), &tmd_chunks[index].index, sizeof(u16)); diff --git a/src/core/file_sys/title_metadata.h b/src/core/file_sys/title_metadata.h index 620988568..379a2389b 100644 --- a/src/core/file_sys/title_metadata.h +++ b/src/core/file_sys/title_metadata.h @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -97,6 +97,7 @@ public: u32 GetContentIDByIndex(std::size_t index) const; u16 GetContentTypeByIndex(std::size_t index) const; u64 GetContentSizeByIndex(std::size_t index) const; + bool GetContentOptional(std::size_t index) const; std::array GetContentCTRByIndex(std::size_t index) const; bool HasEncryptedContent() const; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 3dc76067c..556b2cde3 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -509,49 +509,7 @@ Result CIAFile::WriteTitleMetadata(std::span tmd_data, std::size_t off return FileSys::ResultFileNotFound; } - // Create any other .app folders which may not exist yet - std::string app_folder; - auto main_content_path = GetTitleContentPath(media_type, tmd.GetTitleID(), - FileSys::TMDContentIndex::Main, is_update); - Common::SplitPath(main_content_path, &app_folder, nullptr, nullptr); - FileUtil::CreateFullPath(app_folder); - - auto content_count = container.GetTitleMetadata().GetContentCount(); - content_written.resize(content_count); - - current_content_file.reset(); - current_content_index = -1; - content_file_paths.clear(); - for (std::size_t i = 0; i < content_count; i++) { - auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update); - content_file_paths.emplace_back(path); - } - - if (container.GetTitleMetadata().HasEncryptedContent()) { - if (!decryption_authorized) { - LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); - return {ErrorDescription::NotAuthorized, ErrorModule::AM, ErrorSummary::InvalidState, - ErrorLevel::Permanent}; - } else { - if (auto title_key = container.GetTicket().GetTitleKey()) { - decryption_state->content.resize(content_count); - for (std::size_t i = 0; i < content_count; ++i) { - auto ctr = tmd.GetContentCTRByIndex(i); - decryption_state->content[i].SetKeyWithIV(title_key->data(), title_key->size(), - ctr.data()); - } - } else { - LOG_ERROR(Service_AM, "Could not read title key from ticket for encrypted CIA."); - // TODO: Correct error code. - return FileSys::ResultFileNotFound; - } - } - } else { - LOG_INFO(Service_AM, - "Title has no encrypted content, skipping initializing decryption state."); - } - - install_state = CIAInstallState::TMDLoaded; + PrepareToImportContent(tmd); return ResultSuccess; } @@ -687,6 +645,55 @@ ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush return length; } +Result CIAFile::PrepareToImportContent(const FileSys::TitleMetadata& tmd) { + + // Create any other .app folders which may not exist yet + std::string app_folder; + auto main_content_path = GetTitleContentPath(media_type, tmd.GetTitleID(), + FileSys::TMDContentIndex::Main, is_update); + Common::SplitPath(main_content_path, &app_folder, nullptr, nullptr); + FileUtil::CreateFullPath(app_folder); + + auto content_count = container.GetTitleMetadata().GetContentCount(); + content_written.resize(content_count); + + current_content_file.reset(); + current_content_index = -1; + content_file_paths.clear(); + for (std::size_t i = 0; i < content_count; i++) { + auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update); + content_file_paths.emplace_back(path); + } + + if (container.GetTitleMetadata().HasEncryptedContent()) { + if (!decryption_authorized) { + LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); + return {ErrorDescription::NotAuthorized, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent}; + } else { + if (auto title_key = container.GetTicket().GetTitleKey()) { + decryption_state->content.resize(content_count); + for (std::size_t i = 0; i < content_count; ++i) { + auto ctr = tmd.GetContentCTRByIndex(i); + decryption_state->content[i].SetKeyWithIV(title_key->data(), title_key->size(), + ctr.data()); + } + } else { + LOG_ERROR(Service_AM, "Could not read title key from ticket for encrypted CIA."); + // TODO: Correct error code. + return FileSys::ResultFileNotFound; + } + } + } else { + LOG_INFO(Service_AM, + "Title has no encrypted content, skipping initializing decryption state."); + } + + install_state = CIAInstallState::TMDLoaded; + + return ResultSuccess; +} + Result CIAFile::ProvideTicket(const FileSys::Ticket& ticket) { // There is no need to write the ticket to nand, as that will ASSERT_MSG(from_cdn, "This method should only be used when installing from CDN"); @@ -703,10 +710,39 @@ Result CIAFile::ProvideTicket(const FileSys::Ticket& ticket) { return ResultSuccess; } +Result CIAFile::ProvideTMDForAdditionalContent(const FileSys::TitleMetadata& tmd) { + ASSERT_MSG(from_cdn, "This method should only be used when installing from CDN"); + + if (install_state != CIAInstallState::TicketLoaded) { + LOG_ERROR(Service_AM, "Ticket not provided yet"); + // TODO: Correct result code. + return {ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidArgument, + ErrorLevel::Permanent}; + } + + auto load_result = container.LoadTitleMetadata(tmd); + if (load_result != Loader::ResultStatus::Success) { + LOG_ERROR(Service_AM, "Could not read ticket from CIA."); + // TODO: Correct result code. + return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, + ErrorLevel::Permanent}; + } + + is_additional_content = true; + + PrepareToImportContent(container.GetTitleMetadata()); + + return ResultSuccess; +} + const FileSys::TitleMetadata& CIAFile::GetTMD() { return container.GetTitleMetadata(); } +FileSys::Ticket& CIAFile::GetTicket() { + return container.GetTicket(); +} + ResultVal CIAFile::WriteContentDataIndexed(u16 content_index, u64 offset, std::size_t length, const u8* buffer) { @@ -771,8 +807,10 @@ bool CIAFile::Close() { // Install aborted if (!complete) { LOG_ERROR(Service_AM, "CIAFile closed prematurely, aborting install..."); - FileUtil::DeleteDirRecursively( - GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID())); + if (!is_additional_content) { + FileUtil::DeleteDirRecursively( + GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID())); + } return true; } @@ -1427,57 +1465,111 @@ void Module::Interface::FindDLCContentInfos(Kernel::HLERequestContext& ctx) { true); } else { - auto& content_info_out = rp.PopMappedBuffer(); + struct AsyncData { + Service::FS::MediaType media_type; + u64 title_id; + std::vector content_requested; - // Validate that only DLC TIDs are passed in - u32 tid_high = static_cast(title_id >> 32); - if (tid_high != TID_HIGH_DLC) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, - ErrorSummary::InvalidArgument, ErrorLevel::Usage)); - return; - } + Result res{0}; + std::vector out_vec; + Kernel::MappedBuffer* content_info_out; + }; + auto async_data = std::make_shared(); + async_data->media_type = media_type; + async_data->title_id = title_id; + async_data->content_requested.resize(content_count); + content_requested_in.Read(async_data->content_requested.data(), 0, + content_count * sizeof(u16)); + async_data->content_info_out = &rp.PopMappedBuffer(); - std::vector content_requested(content_count); - content_requested_in.Read(content_requested.data(), 0, content_count * sizeof(u16)); - - std::string tmd_path = GetTitleMetadataPath(media_type, title_id); - - FileSys::TitleMetadata tmd; - if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { - std::size_t write_offset = 0; - // Get info for each content index requested - for (std::size_t i = 0; i < content_count; i++) { - if (content_requested[i] >= tmd.GetContentCount()) { - LOG_ERROR(Service_AM, - "Attempted to get info for non-existent content index {:04x}.", - content_requested[i]); - - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(-1); // TODO(Steveice10): Find the right error code - return; + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + // Validate that only DLC TIDs are passed in + u32 tid_high = static_cast(async_data->title_id >> 32); + if (tid_high != TID_HIGH_DLC) { + async_data->res = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, + ErrorSummary::InvalidArgument, ErrorLevel::Usage); + return 0; } - ContentInfo content_info = {}; - content_info.index = content_requested[i]; - content_info.type = tmd.GetContentTypeByIndex(content_requested[i]); - content_info.content_id = tmd.GetContentIDByIndex(content_requested[i]); - content_info.size = tmd.GetContentSizeByIndex(content_requested[i]); - content_info.ownership = - OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket. + std::string tmd_path = + GetTitleMetadataPath(async_data->media_type, async_data->title_id); - if (FileUtil::Exists( - GetTitleContentPath(media_type, title_id, content_requested[i]))) { - content_info.ownership |= OWNERSHIP_DOWNLOADED; + // In normal circumstances, if there is no ticket we shouldn't be able to have + // any contents either. However to keep compatibility with older emulator builds, + // we give rights anyway if the ticket is not installed. + bool has_ticket = false; + FileSys::Ticket ticket; + std::scoped_lock lock(am->am_lists_mutex); + auto entries = am->am_ticket_list.find(async_data->title_id); + if (entries != am->am_ticket_list.end() && + ticket.Load(async_data->title_id, (*entries).second) == + Loader::ResultStatus::Success) { + has_ticket = true; } - content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo)); - write_offset += sizeof(ContentInfo); - } - } + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { + // Get info for each content index requested + for (std::size_t i = 0; i < async_data->content_requested.size(); i++) { + u16_le index = async_data->content_requested[i]; + if (index >= tmd.GetContentCount()) { + LOG_ERROR( + Service_AM, + "Attempted to get info for non-existent content index {:04x}.", + index); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + async_data->res = Result(0xFFFFFFFF); + return 0; + } + + ContentInfo content_info = {}; + content_info.index = index; + content_info.type = tmd.GetContentTypeByIndex(index); + content_info.content_id = tmd.GetContentIDByIndex(index); + content_info.size = tmd.GetContentSizeByIndex(index); + content_info.ownership = + (!has_ticket || ticket.HasRights(index)) ? OWNERSHIP_OWNED : 0; + + if (FileUtil::Exists(GetTitleContentPath(async_data->media_type, + async_data->title_id, index))) { + bool pending = false; + for (auto& import_ctx : am->import_content_contexts) { + if (import_ctx.first == async_data->title_id && + import_ctx.second.index == index && + (import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_IMPORT || + import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_COMMIT || + import_ctx.second.state == + ImportTitleContextState::RESUMABLE)) { + LOG_DEBUG(Service_AM, "content pending commit index={:016X}", + i); + pending = true; + break; + } + } + if (!pending) { + content_info.ownership |= OWNERSHIP_DOWNLOADED; + } + } + + async_data->out_vec.push_back(content_info); + } + } + + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 0); + rb.Push(async_data->res); + if (async_data->res.IsSuccess()) { + async_data->content_info_out->Write(async_data->out_vec.data(), 0, + async_data->out_vec.size() * + sizeof(ContentInfo)); + } + }, + true); } } @@ -1553,48 +1645,102 @@ void Module::Interface::ListDLCContentInfos(Kernel::HLERequestContext& ctx) { true); } else { - auto& content_info_out = rp.PopMappedBuffer(); + struct AsyncData { + Service::FS::MediaType media_type; + u64 title_id; + u32 content_count; + u32 start_index; - // Validate that only DLC TIDs are passed in - u32 tid_high = static_cast(title_id >> 32); - if (tid_high != TID_HIGH_DLC) { - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, - ErrorSummary::InvalidArgument, ErrorLevel::Usage)); - rb.Push(0); - return; - } + Result res{0}; + std::vector out_vec; + Kernel::MappedBuffer* content_info_out; + }; + auto async_data = std::make_shared(); + async_data->media_type = media_type; + async_data->title_id = title_id; + async_data->content_count = content_count; + async_data->start_index = start_index; + async_data->content_info_out = &rp.PopMappedBuffer(); - std::string tmd_path = GetTitleMetadataPath(media_type, title_id); - - u32 copied = 0; - FileSys::TitleMetadata tmd; - if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { - u32 end_index = - std::min(start_index + content_count, static_cast(tmd.GetContentCount())); - std::size_t write_offset = 0; - for (u32 i = start_index; i < end_index; i++) { - ContentInfo content_info = {}; - content_info.index = static_cast(i); - content_info.type = tmd.GetContentTypeByIndex(i); - content_info.content_id = tmd.GetContentIDByIndex(i); - content_info.size = tmd.GetContentSizeByIndex(i); - content_info.ownership = - OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket. - - if (FileUtil::Exists(GetTitleContentPath(media_type, title_id, i))) { - content_info.ownership |= OWNERSHIP_DOWNLOADED; + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + // Validate that only DLC TIDs are passed in + u32 tid_high = static_cast(async_data->title_id >> 32); + if (tid_high != TID_HIGH_DLC) { + async_data->res = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, + ErrorSummary::InvalidArgument, ErrorLevel::Usage); + return 0; } - content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo)); - write_offset += sizeof(ContentInfo); - copied++; - } - } + std::string tmd_path = + GetTitleMetadataPath(async_data->media_type, async_data->title_id); - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(ResultSuccess); - rb.Push(copied); + // In normal circumstances, if there is no ticket we shouldn't be able to have + // any contents either. However to keep compatibility with older emulator builds, + // we give rights anyway if the ticket is not installed. + bool has_ticket = false; + FileSys::Ticket ticket; + std::scoped_lock lock(am->am_lists_mutex); + auto entries = am->am_ticket_list.find(async_data->title_id); + if (entries != am->am_ticket_list.end() && + ticket.Load(async_data->title_id, (*entries).second) == + Loader::ResultStatus::Success) { + has_ticket = true; + } + + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { + u32 end_index = std::min(async_data->start_index + async_data->content_count, + static_cast(tmd.GetContentCount())); + for (u32 i = async_data->start_index; i < end_index; i++) { + ContentInfo content_info = {}; + content_info.index = static_cast(i); + content_info.type = tmd.GetContentTypeByIndex(i); + content_info.content_id = tmd.GetContentIDByIndex(i); + content_info.size = tmd.GetContentSizeByIndex(i); + content_info.ownership = + (!has_ticket || ticket.HasRights(static_cast(i))) ? OWNERSHIP_OWNED + : 0; + + if (FileUtil::Exists(GetTitleContentPath(async_data->media_type, + async_data->title_id, i))) { + bool pending = false; + for (auto& import_ctx : am->import_content_contexts) { + if (import_ctx.first == async_data->title_id && + import_ctx.second.index == i && + (import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_IMPORT || + import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_COMMIT || + import_ctx.second.state == + ImportTitleContextState::RESUMABLE)) { + LOG_DEBUG(Service_AM, "content pending commit index={:016X}", + i); + pending = true; + break; + } + } + if (!pending) { + content_info.ownership |= OWNERSHIP_DOWNLOADED; + } + } + + async_data->out_vec.push_back(content_info); + } + } + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 0); + rb.Push(async_data->res); + rb.Push(static_cast(async_data->out_vec.size())); + if (async_data->res.IsSuccess()) { + async_data->content_info_out->Write(async_data->out_vec.data(), 0, + async_data->out_vec.size() * + sizeof(ContentInfo)); + } + }, + true); } } @@ -2301,27 +2447,43 @@ void Module::Interface::ListDataTitleTicketInfos(Kernel::HLERequestContext& ctx) }, true); } else { - auto& ticket_info_out = rp.PopMappedBuffer(); + LOG_DEBUG(Service_AM, "(STUBBED) called, ticket_count={}", ticket_count); - std::size_t write_offset = 0; - for (u32 i = 0; i < ticket_count; i++) { - TicketInfo ticket_info = {}; - ticket_info.title_id = title_id; - ticket_info.version = 0; // TODO - ticket_info.size = 0; // TODO - - ticket_info_out.Write(&ticket_info, write_offset, sizeof(TicketInfo)); - write_offset += sizeof(TicketInfo); + u32 tid_high = static_cast(title_id >> 32); + if (tid_high != 0x0004008C && tid_high != 0x0004000D) { + LOG_ERROR(Service_AM, "Tried to get infos for non-data title title_id={:016X}", + title_id); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(60, ErrorModule::AM, ErrorSummary::InvalidArgument, ErrorLevel::Usage)); } - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(ResultSuccess); - rb.Push(ticket_count); - rb.PushMappedBuffer(ticket_info_out); + auto& out_buffer = rp.PopMappedBuffer(); - LOG_WARNING(Service_AM, - "(STUBBED) ticket_count=0x{:08X}, title_id=0x{:016x}, start_index=0x{:08X}", - ticket_count, title_id, start_index); + std::scoped_lock lock(am->am_lists_mutex); + auto range = am->am_ticket_list.equal_range(title_id); + auto it = range.first; + std::advance(it, std::min(static_cast(start_index), + static_cast(std::distance(range.first, range.second)))); + + u32 written = 0; + for (; it != range.second && written < ticket_count; it++) { + FileSys::Ticket ticket; + if (ticket.Load(title_id, it->second) != Loader::ResultStatus::Success) + continue; + + TicketInfo info = {}; + info.title_id = ticket.GetTitleID(); + info.ticket_id = ticket.GetTicketID(); + info.version = ticket.GetVersion(); + info.size = static_cast(ticket.GetSerializedSize()); + + out_buffer.Write(&info, written * sizeof(TicketInfo), sizeof(TicketInfo)); + written++; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); // No error + rb.Push(written); } } @@ -2719,7 +2881,8 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) { bool needs_cleanup = false; for (auto& import_ctx : am->import_title_contexts) { - if (import_ctx.second.state == ImportTitleContextState::NEEDS_CLEANUP) { + if (import_ctx.second.state == ImportTitleContextState::RESUMABLE || + import_ctx.second.state == ImportTitleContextState::WAITING_FOR_IMPORT) { needs_cleanup = true; break; } @@ -2727,7 +2890,8 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) { if (!needs_cleanup) { for (auto& import_ctx : am->import_content_contexts) { - if (import_ctx.second.state == ImportTitleContextState::NEEDS_CLEANUP) { + if (import_ctx.second.state == ImportTitleContextState::RESUMABLE || + import_ctx.second.state == ImportTitleContextState::WAITING_FOR_IMPORT) { needs_cleanup = true; } } @@ -2745,7 +2909,9 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_AM, "(STUBBED) called, media_type={:#02x}", media_type); for (auto it = am->import_content_contexts.begin(); it != am->import_content_contexts.end();) { - if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) { + if (it->second.state == ImportTitleContextState::RESUMABLE || + it->second.state == ImportTitleContextState::WAITING_FOR_IMPORT || + it->second.state == ImportTitleContextState::NEEDS_CLEANUP) { it = am->import_content_contexts.erase(it); } else { it++; @@ -2753,7 +2919,16 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) { } for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end();) { - if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) { + if (it->second.state == ImportTitleContextState::RESUMABLE || + it->second.state == ImportTitleContextState::WAITING_FOR_IMPORT || + it->second.state == ImportTitleContextState::NEEDS_CLEANUP) { + if (am->importing_title) { + if (am->importing_title->title_id == it->second.title_id && + am->importing_title->media_type == + static_cast(media_type)) { + am->importing_title.reset(); + } + } it = am->import_title_contexts.erase(it); } else { it++; @@ -2865,16 +3040,22 @@ void Module::Interface::CheckContentRights(Kernel::HLERequestContext& ctx) { u64 tid = rp.Pop(); u16 content_index = rp.Pop(); - // TODO(shinyquagsire23): Read tickets for this instead? - bool has_rights = - FileUtil::Exists(GetTitleContentPath(Service::FS::MediaType::NAND, tid, content_index)) || - FileUtil::Exists(GetTitleContentPath(Service::FS::MediaType::SDMC, tid, content_index)); + bool has_ticket = false; + FileSys::Ticket ticket; + std::scoped_lock lock(am->am_lists_mutex); + auto entries = am->am_ticket_list.find(tid); + if (entries != am->am_ticket_list.end() && + ticket.Load(tid, (*entries).second) == Loader::ResultStatus::Success) { + has_ticket = true; + } + + bool has_rights = (!has_ticket || ticket.HasRights(content_index)); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); // No error rb.Push(has_rights); - LOG_WARNING(Service_AM, "(STUBBED) tid={:016x}, content_index={}", tid, content_index); + LOG_DEBUG(Service_AM, "tid={:016x}, content_index={}", tid, content_index); } void Module::Interface::CheckContentRightsIgnorePlatform(Kernel::HLERequestContext& ctx) { @@ -2882,15 +3063,22 @@ void Module::Interface::CheckContentRightsIgnorePlatform(Kernel::HLERequestConte u64 tid = rp.Pop(); u16 content_index = rp.Pop(); - // TODO(shinyquagsire23): Read tickets for this instead? - bool has_rights = - FileUtil::Exists(GetTitleContentPath(Service::FS::MediaType::SDMC, tid, content_index)); + bool has_ticket = false; + FileSys::Ticket ticket; + std::scoped_lock lock(am->am_lists_mutex); + auto entries = am->am_ticket_list.find(tid); + if (entries != am->am_ticket_list.end() && + ticket.Load(tid, (*entries).second) == Loader::ResultStatus::Success) { + has_ticket = true; + } + + bool has_rights = (!has_ticket || ticket.HasRights(content_index)); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); // No error rb.Push(has_rights); - LOG_WARNING(Service_AM, "(STUBBED) tid={:016x}, content_index={}", tid, content_index); + LOG_DEBUG(Service_AM, "tid={:016x}, content_index={}", tid, content_index); } void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) { @@ -3298,10 +3486,10 @@ void Module::Interface::CommitImportTitlesImpl(Kernel::HLERequestContext& ctx, IPC::RequestParser rp(ctx); const auto media_type = static_cast(rp.Pop()); [[maybe_unused]] u32 count = rp.Pop(); - [[maybe_unused]] u8 database = rp.Pop(); + bool cleanup = rp.Pop(); - LOG_WARNING(Service_AM, "(STUBBED) update_firm_auto={} is_titles={}", is_update_firm_auto, - is_titles); + LOG_WARNING(Service_AM, "(STUBBED) update_firm_auto={} is_titles={} cleanup={}", + is_update_firm_auto, is_titles, cleanup); auto& title_id_buf = rp.PopMappedBuffer(); @@ -3323,6 +3511,29 @@ void Module::Interface::CommitImportTitlesImpl(Kernel::HLERequestContext& ctx, } } + if (cleanup) { + for (auto it = am->import_content_contexts.begin(); + it != am->import_content_contexts.end();) { + if (it->second.state == ImportTitleContextState::RESUMABLE || + it->second.state == ImportTitleContextState::WAITING_FOR_IMPORT || + it->second.state == ImportTitleContextState::NEEDS_CLEANUP) { + it = am->import_content_contexts.erase(it); + } else { + it++; + } + } + + for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end();) { + if (it->second.state == ImportTitleContextState::RESUMABLE || + it->second.state == ImportTitleContextState::WAITING_FOR_IMPORT || + it->second.state == ImportTitleContextState::NEEDS_CLEANUP) { + it = am->import_title_contexts.erase(it); + } else { + it++; + } + } + } + am->ScanForTitles(media_type); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); @@ -3670,7 +3881,7 @@ void Module::Interface::EndImportTmd(Kernel::HLERequestContext& ctx) { if (tmd_file.Succeeded()) { struct AsyncData { Service::AM::TMDFile* tmd_file; - bool create_context; + [[maybe_unused]] bool create_context; Result res{0}; }; @@ -3687,7 +3898,8 @@ void Module::Interface::EndImportTmd(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb(ctx, 1, 0); rb.Push(async_data->res); - if (async_data->create_context) { + if (async_data->res.IsSuccess()) { + am->importing_title->tmd_provided = true; const FileSys::TitleMetadata& tmd_info = am->importing_title->cia_file.GetTMD(); ImportTitleContext& context = am->import_title_contexts[tmd_info.GetTitleID()]; @@ -3697,6 +3909,9 @@ void Module::Interface::EndImportTmd(Kernel::HLERequestContext& ctx) { context.state = ImportTitleContextState::WAITING_FOR_IMPORT; context.size = 0; for (size_t i = 0; i < tmd_info.GetContentCount(); i++) { + if (tmd_info.GetContentOptional(i)) { + continue; + } ImportContentContext content_context; content_context.content_id = tmd_info.GetContentIDByIndex(i); content_context.index = static_cast(i); @@ -3724,13 +3939,68 @@ void Module::Interface::CreateImportContentContexts(Kernel::HLERequestContext& c const u32 content_count = rp.Pop(); auto content_buf = rp.PopMappedBuffer(); - std::vector content_indices(content_count); - content_buf.Read(content_indices.data(), 0, content_buf.GetSize()); + LOG_DEBUG(Service_AM, ""); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + if (!am->importing_title) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } - LOG_WARNING(Service_AM, "(STUBBED)"); + struct AsyncData { + std::vector content_indices; + + Result res{0}; + }; + std::shared_ptr async_data = std::make_shared(); + async_data->content_indices.resize(content_count); + content_buf.Read(async_data->content_indices.data(), 0, content_buf.GetSize()); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + if (!am->importing_title->tmd_provided) { + std::string tmd_path = GetTitleMetadataPath(am->importing_title->media_type, + am->importing_title->title_id); + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) != Loader::ResultStatus::Success) { + LOG_ERROR(Service_AM, "Couldn't load TMD for title_id={:016X}, mediatype={}", + am->importing_title->title_id, am->importing_title->media_type); + + async_data->res = + Result(0xFFFFFFFF); // TODO(PabloMK7): Find the right error code + return 0; + } + am->importing_title->cia_file.ProvideTMDForAdditionalContent(tmd); + am->importing_title->tmd_provided = true; + } + const FileSys::TitleMetadata& tmd = am->importing_title->cia_file.GetTMD(); + for (size_t i = 0; i < async_data->content_indices.size(); i++) { + u16 index = async_data->content_indices[i]; + if (index > tmd.GetContentCount()) { + LOG_ERROR(Service_AM, + "Tried to create context for invalid index title_id={:016x} index={}", + am->importing_title->title_id, index); + async_data->res = + Result(0xFFFFFFFF); // TODO(PabloMK7): Find the right error code + return 0; + } + ImportContentContext content_context; + content_context.content_id = tmd.GetContentIDByIndex(index); + content_context.index = static_cast(index); + content_context.state = ImportTitleContextState::WAITING_FOR_IMPORT; + content_context.size = tmd.GetContentSizeByIndex(index); + content_context.current_size = 0; + am->import_content_contexts.insert( + std::make_pair(am->importing_title->title_id, content_context)); + } + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void Module::Interface::BeginImportContent(Kernel::HLERequestContext& ctx) { @@ -4145,6 +4415,255 @@ void Module::Interface::ListTicketInfos(Kernel::HLERequestContext& ctx) { rb.Push(written); } +void Module::Interface::GetNumCurrentContentInfos(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); // No error + rb.Push(static_cast(am->importing_title->cia_file.GetTMD().GetContentCount())); +} + +void Module::Interface::FindCurrentContentInfos(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + struct AsyncData { + u32 content_count; + std::vector content_requested; + + std::vector out_vec; + Kernel::MappedBuffer* content_info_out; + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->content_count = rp.Pop(); + + auto& content_requested_in = rp.PopMappedBuffer(); + async_data->content_requested.resize(async_data->content_count); + content_requested_in.Read(async_data->content_requested.data(), 0, + async_data->content_count * sizeof(u16)); + async_data->content_info_out = &rp.PopMappedBuffer(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + const FileSys::TitleMetadata& tmd = am->importing_title->cia_file.GetTMD(); + FileSys::Ticket& ticket = am->importing_title->cia_file.GetTicket(); + // Get info for each content index requested + for (std::size_t i = 0; i < async_data->content_count; i++) { + u16_le index = async_data->content_requested[i]; + if (index >= tmd.GetContentCount()) { + LOG_ERROR(Service_AM, + "Attempted to get info for non-existent content index {:04x}.", + index); + + async_data->res = Result(0xFFFFFFFF); + return 0; + } + + ContentInfo content_info = {}; + content_info.index = index; + content_info.type = tmd.GetContentTypeByIndex(index); + content_info.content_id = tmd.GetContentIDByIndex(index); + content_info.size = tmd.GetContentSizeByIndex(index); + content_info.ownership = ticket.HasRights(index) ? OWNERSHIP_OWNED : 0; + + if (FileUtil::Exists(GetTitleContentPath(am->importing_title->media_type, + am->importing_title->title_id, index))) { + bool pending = false; + for (auto& import_ctx : am->import_content_contexts) { + if (import_ctx.first == am->importing_title->title_id && + import_ctx.second.index == index && + (import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_IMPORT || + import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_COMMIT || + import_ctx.second.state == ImportTitleContextState::RESUMABLE)) { + LOG_DEBUG(Service_AM, "content pending commit index={:016X}", index); + pending = true; + break; + } + } + if (!pending) { + content_info.ownership |= OWNERSHIP_DOWNLOADED; + } + } + async_data->out_vec.push_back(content_info); + } + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + if (async_data->res.IsSuccess()) { + async_data->content_info_out->Write(async_data->out_vec.data(), 0, + async_data->out_vec.size() * + sizeof(ContentInfo)); + } + }, + true); +} + +void Module::Interface::ListCurrentContentInfos(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + struct AsyncData { + u32 content_count; + u32 start_index; + + std::vector out_vec; + Kernel::MappedBuffer* content_info_out; + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->content_count = rp.Pop(); + async_data->start_index = rp.Pop(); + + async_data->content_info_out = &rp.PopMappedBuffer(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + const FileSys::TitleMetadata& tmd = am->importing_title->cia_file.GetTMD(); + FileSys::Ticket& ticket = am->importing_title->cia_file.GetTicket(); + u32 end_index = std::min(async_data->start_index + async_data->content_count, + static_cast(tmd.GetContentCount())); + for (u32 i = async_data->start_index; i < end_index; i++) { + ContentInfo content_info = {}; + content_info.index = static_cast(i); + content_info.type = tmd.GetContentTypeByIndex(i); + content_info.content_id = tmd.GetContentIDByIndex(i); + content_info.size = tmd.GetContentSizeByIndex(i); + content_info.ownership = + ticket.HasRights(static_cast(i)) ? OWNERSHIP_OWNED : 0; + + if (FileUtil::Exists(GetTitleContentPath(am->importing_title->media_type, + am->importing_title->title_id, i))) { + bool pending = false; + for (auto& import_ctx : am->import_content_contexts) { + if (import_ctx.first == am->importing_title->title_id && + import_ctx.second.index == i && + (import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_IMPORT || + import_ctx.second.state == + ImportTitleContextState::WAITING_FOR_COMMIT || + import_ctx.second.state == ImportTitleContextState::RESUMABLE)) { + LOG_DEBUG(Service_AM, "content pending commit index={:016X}", i); + pending = true; + break; + } + } + if (!pending) { + content_info.ownership |= OWNERSHIP_DOWNLOADED; + } + } + + async_data->out_vec.push_back(content_info); + } + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 0); + rb.Push(async_data->res); + rb.Push(static_cast(async_data->out_vec.size())); + if (async_data->res.IsSuccess()) { + async_data->content_info_out->Write(async_data->out_vec.data(), 0, + async_data->out_vec.size() * + sizeof(ContentInfo)); + } + }, + true); +} + +void Module::Interface::CalculateContextRequiredSize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_DEBUG(Service_AM, ""); + + auto media_type = static_cast(rp.Pop()); + u64 title_id = rp.Pop(); + u32 content_count = rp.Pop(); + auto& content_requested_in = rp.PopMappedBuffer(); + + std::vector content_requested(content_count); + content_requested_in.Read(content_requested.data(), 0, content_count * sizeof(u16)); + + std::string tmd_path = GetTitleMetadataPath(media_type, title_id); + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) != Loader::ResultStatus::Success) { + LOG_ERROR(Service_AM, "Couldn't load TMD for title_id={:016X}, mediatype={}", title_id, + media_type); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(-1); // TODO(PabloMK7): Find the right error code + return; + } + u64 size_out = 0; + // Get info for each content index requested + for (std::size_t i = 0; i < content_count; i++) { + if (content_requested[i] >= tmd.GetContentCount()) { + LOG_ERROR(Service_AM, "Attempted to get info for non-existent content index {:04x}.", + content_requested[i]); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(-1); // TODO(PabloMK7): Find the right error code + return; + } + if (!tmd.GetContentOptional(content_requested[i])) { + LOG_ERROR(Service_AM, "Attempted to get info for non-optional content index {:04x}.", + content_requested[i]); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(-1); // TODO(PabloMK7): Find the right error code + return; + } + + size_out += tmd.GetContentSizeByIndex(content_requested[i]); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + rb.Push(ResultSuccess); + rb.Push(size_out); +} + +void Module::Interface::UpdateImportContentContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 content_count = rp.Pop(); + auto content_buf = rp.PopMappedBuffer(); + + std::vector content_indices(content_count); + content_buf.Read(content_indices.data(), 0, content_buf.GetSize()); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + + LOG_WARNING(Service_AM, "(STUBBED)"); +} + void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index afac4dae5..9585f96e3 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -181,8 +181,11 @@ public: ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; + Result PrepareToImportContent(const FileSys::TitleMetadata& tmd); Result ProvideTicket(const FileSys::Ticket& ticket); + Result ProvideTMDForAdditionalContent(const FileSys::TitleMetadata& tmd); const FileSys::TitleMetadata& GetTMD(); + FileSys::Ticket& GetTicket(); CIAInstallState GetCiaInstallState() { return install_state; } @@ -208,6 +211,7 @@ private: bool decryption_authorized; bool is_done = false; bool is_closed = false; + bool is_additional_content = false; // Whether it's installing an update, and what step of installation it is at bool is_update = false; @@ -233,11 +237,13 @@ class CurrentImportingTitle { public: explicit CurrentImportingTitle(Core::System& system_, u64 title_id_, Service::FS::MediaType media_type_) - : cia_file(system_, media_type_, true), title_id(title_id_), media_type(media_type_) {} + : cia_file(system_, media_type_, true), title_id(title_id_), media_type(media_type_), + tmd_provided(false) {} CIAFile cia_file; u64 title_id; Service::FS::MediaType media_type; + bool tmd_provided; }; // A file handled returned for Tickets to be written into and subsequently installed. @@ -1005,6 +1011,16 @@ public: void ListTicketInfos(Kernel::HLERequestContext& ctx); + void GetNumCurrentContentInfos(Kernel::HLERequestContext& ctx); + + void FindCurrentContentInfos(Kernel::HLERequestContext& ctx); + + void ListCurrentContentInfos(Kernel::HLERequestContext& ctx); + + void CalculateContextRequiredSize(Kernel::HLERequestContext& ctx); + + void UpdateImportContentContexts(Kernel::HLERequestContext& ctx); + void ExportTicketWrapped(Kernel::HLERequestContext& ctx); protected: diff --git a/src/core/hle/service/am/am_net.cpp b/src/core/hle/service/am/am_net.cpp index 4c39cf62b..1ed88ea8b 100644 --- a/src/core/hle/service/am/am_net.cpp +++ b/src/core/hle/service/am/am_net.cpp @@ -113,11 +113,11 @@ AM_NET::AM_NET(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x081F, &AM_NET::GetNumTicketsOfProgram, "GetNumTicketsOfProgram"}, {0x0820, &AM_NET::ListTicketInfos, "ListTicketInfos"}, {0x0821, nullptr, "GetRightsOnlyTicketData"}, - {0x0822, nullptr, "GetNumCurrentContentInfos"}, - {0x0823, nullptr, "FindCurrentContentInfos"}, - {0x0824, nullptr, "ListCurrentContentInfos"}, - {0x0825, nullptr, "CalculateContextRequiredSize"}, - {0x0826, nullptr, "UpdateImportContentContexts"}, + {0x0822, &AM_NET::GetNumCurrentContentInfos, "GetNumCurrentContentInfos"}, + {0x0823, &AM_NET::FindCurrentContentInfos, "FindCurrentContentInfos"}, + {0x0824, &AM_NET::ListCurrentContentInfos, "ListCurrentContentInfos"}, + {0x0825, &AM_NET::CalculateContextRequiredSize, "CalculateContextRequiredSize"}, + {0x0826, &AM_NET::UpdateImportContentContexts, "UpdateImportContentContexts"}, {0x0827, nullptr, "DeleteAllDemoLaunchInfos"}, {0x0828, nullptr, "BeginImportTitleForOverWrite"}, {0x0829, &AM_NET::ExportTicketWrapped, "ExportTicketWrapped"},