mirror of
https://github.com/azahar-emu/azahar
synced 2025-11-07 07:29:58 +01:00
Support downloading owned DLCs (#950)
This commit is contained in:
parent
bac344d059
commit
391f91f735
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2017 Citra Emulator Project
|
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
@ -157,6 +157,11 @@ Loader::ResultStatus CIAContainer::LoadTitleMetadata(std::span<const u8> tmd_dat
|
|||||||
return cia_tmd.Load(tmd_data, offset);
|
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<const u8> meta_data, std::size_t offset) {
|
Loader::ResultStatus CIAContainer::LoadMetadata(std::span<const u8> meta_data, std::size_t offset) {
|
||||||
if (meta_data.size() - offset < sizeof(Metadata)) {
|
if (meta_data.size() - offset < sizeof(Metadata)) {
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
@ -167,7 +172,7 @@ Loader::ResultStatus CIAContainer::LoadMetadata(std::span<const u8> meta_data, s
|
|||||||
return Loader::ResultStatus::Success;
|
return Loader::ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Ticket& CIAContainer::GetTicket() const {
|
Ticket& CIAContainer::GetTicket() {
|
||||||
return cia_ticket;
|
return cia_ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2017 Citra Emulator Project
|
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
@ -46,9 +46,10 @@ public:
|
|||||||
Loader::ResultStatus LoadTicket(std::span<const u8> ticket_data, std::size_t offset = 0);
|
Loader::ResultStatus LoadTicket(std::span<const u8> ticket_data, std::size_t offset = 0);
|
||||||
Loader::ResultStatus LoadTicket(const Ticket& ticket);
|
Loader::ResultStatus LoadTicket(const Ticket& ticket);
|
||||||
Loader::ResultStatus LoadTitleMetadata(std::span<const u8> tmd_data, std::size_t offset = 0);
|
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 LoadMetadata(std::span<const u8> meta_data, std::size_t offset = 0);
|
||||||
|
|
||||||
const Ticket& GetTicket() const;
|
Ticket& GetTicket();
|
||||||
const TitleMetadata& GetTitleMetadata() const;
|
const TitleMetadata& GetTitleMetadata() const;
|
||||||
std::array<u64, 0x30>& GetDependencies();
|
std::array<u64, 0x30>& GetDependencies();
|
||||||
u32 GetCoreVersion() const;
|
u32 GetCoreVersion() const;
|
||||||
|
|||||||
@ -100,8 +100,11 @@ Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t off
|
|||||||
|
|
||||||
if (total_size < content_index_end)
|
if (total_size < content_index_end)
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
content_index.resize(content_index_size);
|
std::vector<u8> content_index_vec;
|
||||||
std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size);
|
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;
|
return Loader::ResultStatus::Success;
|
||||||
}
|
}
|
||||||
@ -132,7 +135,7 @@ std::vector<u8> Ticket::Serialize() const {
|
|||||||
reinterpret_cast<const u8*>(&ticket_body) + sizeof(ticket_body)};
|
reinterpret_cast<const u8*>(&ticket_body) + sizeof(ticket_body)};
|
||||||
ret.insert(ret.end(), body_span.begin(), body_span.end());
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -167,7 +170,7 @@ std::optional<std::array<u8, 16>> Ticket::GetTitleKey() const {
|
|||||||
return title_key;
|
return title_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Ticket::IsPersonal() {
|
bool Ticket::IsPersonal() const {
|
||||||
if (ticket_body.console_id == 0u) {
|
if (ticket_body.console_id == 0u) {
|
||||||
// Common ticket
|
// Common ticket
|
||||||
return false;
|
return false;
|
||||||
@ -182,4 +185,88 @@ bool Ticket::IsPersonal() {
|
|||||||
return ticket_body.console_id == otp.GetDeviceID();
|
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<MainHeader*>(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<IndexHeader*>(
|
||||||
|
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<RightsField*>(
|
||||||
|
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
|
} // namespace FileSys
|
||||||
|
|||||||
@ -19,6 +19,12 @@ enum class ResultStatus;
|
|||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
class Ticket {
|
class Ticket {
|
||||||
|
struct LimitEntry {
|
||||||
|
u32_be type; // 4 -> Play times?
|
||||||
|
u32_be value;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(LimitEntry) == 0x8, "LimitEntry structure size is wrong");
|
||||||
|
|
||||||
public:
|
public:
|
||||||
#pragma pack(push, 1)
|
#pragma pack(push, 1)
|
||||||
struct Body {
|
struct Body {
|
||||||
@ -42,7 +48,7 @@ public:
|
|||||||
INSERT_PADDING_BYTES(1);
|
INSERT_PADDING_BYTES(1);
|
||||||
u8 audit;
|
u8 audit;
|
||||||
INSERT_PADDING_BYTES(0x42);
|
INSERT_PADDING_BYTES(0x42);
|
||||||
std::array<u8, 0x40> limits;
|
std::array<LimitEntry, 0x8> limits;
|
||||||
};
|
};
|
||||||
static_assert(sizeof(Body) == 0x164, "Ticket body structure size is wrong");
|
static_assert(sizeof(Body) == 0x164, "Ticket body structure size is wrong");
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
@ -67,13 +73,66 @@ public:
|
|||||||
return serialized_size;
|
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<u8, 0x80> rights;
|
||||||
|
};
|
||||||
|
|
||||||
|
ContentIndex() {}
|
||||||
|
|
||||||
|
void Load(Ticket* p, const std::vector<u8>& data) {
|
||||||
|
parent = p;
|
||||||
|
content_index = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<u8>& GetRaw() const {
|
||||||
|
return content_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasRights(u16 content_index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Initialize();
|
||||||
|
|
||||||
|
bool initialized = false;
|
||||||
|
std::vector<u8> content_index;
|
||||||
|
std::vector<RightsField> rights;
|
||||||
|
Ticket* parent = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Body ticket_body;
|
Body ticket_body;
|
||||||
u32_be signature_type;
|
u32_be signature_type;
|
||||||
std::vector<u8> ticket_signature;
|
std::vector<u8> ticket_signature;
|
||||||
std::vector<u8> content_index;
|
ContentIndex content_index;
|
||||||
|
|
||||||
size_t serialized_size = 0;
|
size_t serialized_size = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2017 Citra Emulator Project
|
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
@ -175,6 +175,10 @@ u64 TitleMetadata::GetContentSizeByIndex(std::size_t index) const {
|
|||||||
return tmd_chunks[index].size;
|
return tmd_chunks[index].size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TitleMetadata::GetContentOptional(std::size_t index) const {
|
||||||
|
return (static_cast<u16>(tmd_chunks[index].type) & FileSys::TMDContentTypeFlag::Optional) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
std::array<u8, 16> TitleMetadata::GetContentCTRByIndex(std::size_t index) const {
|
std::array<u8, 16> TitleMetadata::GetContentCTRByIndex(std::size_t index) const {
|
||||||
std::array<u8, 16> ctr{};
|
std::array<u8, 16> ctr{};
|
||||||
std::memcpy(ctr.data(), &tmd_chunks[index].index, sizeof(u16));
|
std::memcpy(ctr.data(), &tmd_chunks[index].index, sizeof(u16));
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2017 Citra Emulator Project
|
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
@ -97,6 +97,7 @@ public:
|
|||||||
u32 GetContentIDByIndex(std::size_t index) const;
|
u32 GetContentIDByIndex(std::size_t index) const;
|
||||||
u16 GetContentTypeByIndex(std::size_t index) const;
|
u16 GetContentTypeByIndex(std::size_t index) const;
|
||||||
u64 GetContentSizeByIndex(std::size_t index) const;
|
u64 GetContentSizeByIndex(std::size_t index) const;
|
||||||
|
bool GetContentOptional(std::size_t index) const;
|
||||||
std::array<u8, 16> GetContentCTRByIndex(std::size_t index) const;
|
std::array<u8, 16> GetContentCTRByIndex(std::size_t index) const;
|
||||||
bool HasEncryptedContent() const;
|
bool HasEncryptedContent() const;
|
||||||
|
|
||||||
|
|||||||
@ -509,49 +509,7 @@ Result CIAFile::WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t off
|
|||||||
return FileSys::ResultFileNotFound;
|
return FileSys::ResultFileNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create any other .app folders which may not exist yet
|
PrepareToImportContent(tmd);
|
||||||
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;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
@ -687,6 +645,55 @@ ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush
|
|||||||
return length;
|
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) {
|
Result CIAFile::ProvideTicket(const FileSys::Ticket& ticket) {
|
||||||
// There is no need to write the ticket to nand, as that will
|
// 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");
|
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;
|
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() {
|
const FileSys::TitleMetadata& CIAFile::GetTMD() {
|
||||||
return container.GetTitleMetadata();
|
return container.GetTitleMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileSys::Ticket& CIAFile::GetTicket() {
|
||||||
|
return container.GetTicket();
|
||||||
|
}
|
||||||
|
|
||||||
ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 offset,
|
ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 offset,
|
||||||
std::size_t length, const u8* buffer) {
|
std::size_t length, const u8* buffer) {
|
||||||
|
|
||||||
@ -771,8 +807,10 @@ bool CIAFile::Close() {
|
|||||||
// Install aborted
|
// Install aborted
|
||||||
if (!complete) {
|
if (!complete) {
|
||||||
LOG_ERROR(Service_AM, "CIAFile closed prematurely, aborting install...");
|
LOG_ERROR(Service_AM, "CIAFile closed prematurely, aborting install...");
|
||||||
|
if (!is_additional_content) {
|
||||||
FileUtil::DeleteDirRecursively(
|
FileUtil::DeleteDirRecursively(
|
||||||
GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID()));
|
GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID()));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1427,57 +1465,111 @@ void Module::Interface::FindDLCContentInfos(Kernel::HLERequestContext& ctx) {
|
|||||||
true);
|
true);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
auto& content_info_out = rp.PopMappedBuffer();
|
struct AsyncData {
|
||||||
|
Service::FS::MediaType media_type;
|
||||||
|
u64 title_id;
|
||||||
|
std::vector<u16> content_requested;
|
||||||
|
|
||||||
|
Result res{0};
|
||||||
|
std::vector<ContentInfo> out_vec;
|
||||||
|
Kernel::MappedBuffer* content_info_out;
|
||||||
|
};
|
||||||
|
auto async_data = std::make_shared<AsyncData>();
|
||||||
|
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();
|
||||||
|
|
||||||
|
ctx.RunAsync(
|
||||||
|
[this, async_data](Kernel::HLERequestContext& ctx) {
|
||||||
// Validate that only DLC TIDs are passed in
|
// Validate that only DLC TIDs are passed in
|
||||||
u32 tid_high = static_cast<u32>(title_id >> 32);
|
u32 tid_high = static_cast<u32>(async_data->title_id >> 32);
|
||||||
if (tid_high != TID_HIGH_DLC) {
|
if (tid_high != TID_HIGH_DLC) {
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
async_data->res = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM,
|
||||||
rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM,
|
ErrorSummary::InvalidArgument, ErrorLevel::Usage);
|
||||||
ErrorSummary::InvalidArgument, ErrorLevel::Usage));
|
return 0;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<u16_le> content_requested(content_count);
|
std::string tmd_path =
|
||||||
content_requested_in.Read(content_requested.data(), 0, content_count * sizeof(u16));
|
GetTitleMetadataPath(async_data->media_type, async_data->title_id);
|
||||||
|
|
||||||
std::string tmd_path = GetTitleMetadataPath(media_type, title_id);
|
// 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;
|
FileSys::TitleMetadata tmd;
|
||||||
if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) {
|
if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) {
|
||||||
std::size_t write_offset = 0;
|
|
||||||
// Get info for each content index requested
|
// Get info for each content index requested
|
||||||
for (std::size_t i = 0; i < content_count; i++) {
|
for (std::size_t i = 0; i < async_data->content_requested.size(); i++) {
|
||||||
if (content_requested[i] >= tmd.GetContentCount()) {
|
u16_le index = async_data->content_requested[i];
|
||||||
LOG_ERROR(Service_AM,
|
if (index >= tmd.GetContentCount()) {
|
||||||
|
LOG_ERROR(
|
||||||
|
Service_AM,
|
||||||
"Attempted to get info for non-existent content index {:04x}.",
|
"Attempted to get info for non-existent content index {:04x}.",
|
||||||
content_requested[i]);
|
index);
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
async_data->res = Result(0xFFFFFFFF);
|
||||||
rb.Push<u32>(-1); // TODO(Steveice10): Find the right error code
|
return 0;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentInfo content_info = {};
|
ContentInfo content_info = {};
|
||||||
content_info.index = content_requested[i];
|
content_info.index = index;
|
||||||
content_info.type = tmd.GetContentTypeByIndex(content_requested[i]);
|
content_info.type = tmd.GetContentTypeByIndex(index);
|
||||||
content_info.content_id = tmd.GetContentIDByIndex(content_requested[i]);
|
content_info.content_id = tmd.GetContentIDByIndex(index);
|
||||||
content_info.size = tmd.GetContentSizeByIndex(content_requested[i]);
|
content_info.size = tmd.GetContentSizeByIndex(index);
|
||||||
content_info.ownership =
|
content_info.ownership =
|
||||||
OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket.
|
(!has_ticket || ticket.HasRights(index)) ? OWNERSHIP_OWNED : 0;
|
||||||
|
|
||||||
if (FileUtil::Exists(
|
if (FileUtil::Exists(GetTitleContentPath(async_data->media_type,
|
||||||
GetTitleContentPath(media_type, title_id, content_requested[i]))) {
|
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;
|
content_info.ownership |= OWNERSHIP_DOWNLOADED;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo));
|
async_data->out_vec.push_back(content_info);
|
||||||
write_offset += sizeof(ContentInfo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
return 0;
|
||||||
rb.Push(ResultSuccess);
|
},
|
||||||
|
[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);
|
true);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
auto& content_info_out = rp.PopMappedBuffer();
|
struct AsyncData {
|
||||||
|
Service::FS::MediaType media_type;
|
||||||
|
u64 title_id;
|
||||||
|
u32 content_count;
|
||||||
|
u32 start_index;
|
||||||
|
|
||||||
|
Result res{0};
|
||||||
|
std::vector<ContentInfo> out_vec;
|
||||||
|
Kernel::MappedBuffer* content_info_out;
|
||||||
|
};
|
||||||
|
auto async_data = std::make_shared<AsyncData>();
|
||||||
|
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();
|
||||||
|
|
||||||
|
ctx.RunAsync(
|
||||||
|
[this, async_data](Kernel::HLERequestContext& ctx) {
|
||||||
// Validate that only DLC TIDs are passed in
|
// Validate that only DLC TIDs are passed in
|
||||||
u32 tid_high = static_cast<u32>(title_id >> 32);
|
u32 tid_high = static_cast<u32>(async_data->title_id >> 32);
|
||||||
if (tid_high != TID_HIGH_DLC) {
|
if (tid_high != TID_HIGH_DLC) {
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
async_data->res = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM,
|
||||||
rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM,
|
ErrorSummary::InvalidArgument, ErrorLevel::Usage);
|
||||||
ErrorSummary::InvalidArgument, ErrorLevel::Usage));
|
return 0;
|
||||||
rb.Push<u32>(0);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string tmd_path = GetTitleMetadataPath(media_type, title_id);
|
std::string tmd_path =
|
||||||
|
GetTitleMetadataPath(async_data->media_type, async_data->title_id);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
u32 copied = 0;
|
|
||||||
FileSys::TitleMetadata tmd;
|
FileSys::TitleMetadata tmd;
|
||||||
if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) {
|
if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) {
|
||||||
u32 end_index =
|
u32 end_index = std::min(async_data->start_index + async_data->content_count,
|
||||||
std::min(start_index + content_count, static_cast<u32>(tmd.GetContentCount()));
|
static_cast<u32>(tmd.GetContentCount()));
|
||||||
std::size_t write_offset = 0;
|
for (u32 i = async_data->start_index; i < end_index; i++) {
|
||||||
for (u32 i = start_index; i < end_index; i++) {
|
|
||||||
ContentInfo content_info = {};
|
ContentInfo content_info = {};
|
||||||
content_info.index = static_cast<u16>(i);
|
content_info.index = static_cast<u16>(i);
|
||||||
content_info.type = tmd.GetContentTypeByIndex(i);
|
content_info.type = tmd.GetContentTypeByIndex(i);
|
||||||
content_info.content_id = tmd.GetContentIDByIndex(i);
|
content_info.content_id = tmd.GetContentIDByIndex(i);
|
||||||
content_info.size = tmd.GetContentSizeByIndex(i);
|
content_info.size = tmd.GetContentSizeByIndex(i);
|
||||||
content_info.ownership =
|
content_info.ownership =
|
||||||
OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket.
|
(!has_ticket || ticket.HasRights(static_cast<u16>(i))) ? OWNERSHIP_OWNED
|
||||||
|
: 0;
|
||||||
|
|
||||||
if (FileUtil::Exists(GetTitleContentPath(media_type, title_id, i))) {
|
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;
|
content_info.ownership |= OWNERSHIP_DOWNLOADED;
|
||||||
}
|
}
|
||||||
|
|
||||||
content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo));
|
|
||||||
write_offset += sizeof(ContentInfo);
|
|
||||||
copied++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
async_data->out_vec.push_back(content_info);
|
||||||
rb.Push(ResultSuccess);
|
}
|
||||||
rb.Push(copied);
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
[async_data](Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestBuilder rb(ctx, 2, 0);
|
||||||
|
rb.Push(async_data->res);
|
||||||
|
rb.Push(static_cast<u32>(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);
|
true);
|
||||||
} else {
|
} else {
|
||||||
auto& ticket_info_out = rp.PopMappedBuffer();
|
LOG_DEBUG(Service_AM, "(STUBBED) called, ticket_count={}", ticket_count);
|
||||||
|
|
||||||
std::size_t write_offset = 0;
|
u32 tid_high = static_cast<u32>(title_id >> 32);
|
||||||
for (u32 i = 0; i < ticket_count; i++) {
|
if (tid_high != 0x0004008C && tid_high != 0x0004000D) {
|
||||||
TicketInfo ticket_info = {};
|
LOG_ERROR(Service_AM, "Tried to get infos for non-data title title_id={:016X}",
|
||||||
ticket_info.title_id = title_id;
|
title_id);
|
||||||
ticket_info.version = 0; // TODO
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
ticket_info.size = 0; // TODO
|
rb.Push(Result(60, ErrorModule::AM, ErrorSummary::InvalidArgument, ErrorLevel::Usage));
|
||||||
|
|
||||||
ticket_info_out.Write(&ticket_info, write_offset, sizeof(TicketInfo));
|
|
||||||
write_offset += sizeof(TicketInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
|
auto& out_buffer = rp.PopMappedBuffer();
|
||||||
rb.Push(ResultSuccess);
|
|
||||||
rb.Push(ticket_count);
|
|
||||||
rb.PushMappedBuffer(ticket_info_out);
|
|
||||||
|
|
||||||
LOG_WARNING(Service_AM,
|
std::scoped_lock lock(am->am_lists_mutex);
|
||||||
"(STUBBED) ticket_count=0x{:08X}, title_id=0x{:016x}, start_index=0x{:08X}",
|
auto range = am->am_ticket_list.equal_range(title_id);
|
||||||
ticket_count, title_id, start_index);
|
auto it = range.first;
|
||||||
|
std::advance(it, std::min(static_cast<size_t>(start_index),
|
||||||
|
static_cast<size_t>(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<u32>(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;
|
bool needs_cleanup = false;
|
||||||
for (auto& import_ctx : am->import_title_contexts) {
|
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;
|
needs_cleanup = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2727,7 +2890,8 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
if (!needs_cleanup) {
|
if (!needs_cleanup) {
|
||||||
for (auto& import_ctx : am->import_content_contexts) {
|
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;
|
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);
|
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();) {
|
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);
|
it = am->import_content_contexts.erase(it);
|
||||||
} else {
|
} else {
|
||||||
it++;
|
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();) {
|
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<Service::FS::MediaType>(media_type)) {
|
||||||
|
am->importing_title.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
it = am->import_title_contexts.erase(it);
|
it = am->import_title_contexts.erase(it);
|
||||||
} else {
|
} else {
|
||||||
it++;
|
it++;
|
||||||
@ -2865,16 +3040,22 @@ void Module::Interface::CheckContentRights(Kernel::HLERequestContext& ctx) {
|
|||||||
u64 tid = rp.Pop<u64>();
|
u64 tid = rp.Pop<u64>();
|
||||||
u16 content_index = rp.Pop<u16>();
|
u16 content_index = rp.Pop<u16>();
|
||||||
|
|
||||||
// TODO(shinyquagsire23): Read tickets for this instead?
|
bool has_ticket = false;
|
||||||
bool has_rights =
|
FileSys::Ticket ticket;
|
||||||
FileUtil::Exists(GetTitleContentPath(Service::FS::MediaType::NAND, tid, content_index)) ||
|
std::scoped_lock lock(am->am_lists_mutex);
|
||||||
FileUtil::Exists(GetTitleContentPath(Service::FS::MediaType::SDMC, tid, content_index));
|
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);
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
rb.Push(ResultSuccess); // No error
|
rb.Push(ResultSuccess); // No error
|
||||||
rb.Push(has_rights);
|
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) {
|
void Module::Interface::CheckContentRightsIgnorePlatform(Kernel::HLERequestContext& ctx) {
|
||||||
@ -2882,15 +3063,22 @@ void Module::Interface::CheckContentRightsIgnorePlatform(Kernel::HLERequestConte
|
|||||||
u64 tid = rp.Pop<u64>();
|
u64 tid = rp.Pop<u64>();
|
||||||
u16 content_index = rp.Pop<u16>();
|
u16 content_index = rp.Pop<u16>();
|
||||||
|
|
||||||
// TODO(shinyquagsire23): Read tickets for this instead?
|
bool has_ticket = false;
|
||||||
bool has_rights =
|
FileSys::Ticket ticket;
|
||||||
FileUtil::Exists(GetTitleContentPath(Service::FS::MediaType::SDMC, tid, content_index));
|
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);
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
rb.Push(ResultSuccess); // No error
|
rb.Push(ResultSuccess); // No error
|
||||||
rb.Push(has_rights);
|
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) {
|
void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) {
|
||||||
@ -3298,10 +3486,10 @@ void Module::Interface::CommitImportTitlesImpl(Kernel::HLERequestContext& ctx,
|
|||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
const auto media_type = static_cast<FS::MediaType>(rp.Pop<u8>());
|
const auto media_type = static_cast<FS::MediaType>(rp.Pop<u8>());
|
||||||
[[maybe_unused]] u32 count = rp.Pop<u32>();
|
[[maybe_unused]] u32 count = rp.Pop<u32>();
|
||||||
[[maybe_unused]] u8 database = rp.Pop<u8>();
|
bool cleanup = rp.Pop<bool>();
|
||||||
|
|
||||||
LOG_WARNING(Service_AM, "(STUBBED) update_firm_auto={} is_titles={}", is_update_firm_auto,
|
LOG_WARNING(Service_AM, "(STUBBED) update_firm_auto={} is_titles={} cleanup={}",
|
||||||
is_titles);
|
is_update_firm_auto, is_titles, cleanup);
|
||||||
|
|
||||||
auto& title_id_buf = rp.PopMappedBuffer();
|
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);
|
am->ScanForTitles(media_type);
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
@ -3670,7 +3881,7 @@ void Module::Interface::EndImportTmd(Kernel::HLERequestContext& ctx) {
|
|||||||
if (tmd_file.Succeeded()) {
|
if (tmd_file.Succeeded()) {
|
||||||
struct AsyncData {
|
struct AsyncData {
|
||||||
Service::AM::TMDFile* tmd_file;
|
Service::AM::TMDFile* tmd_file;
|
||||||
bool create_context;
|
[[maybe_unused]] bool create_context;
|
||||||
|
|
||||||
Result res{0};
|
Result res{0};
|
||||||
};
|
};
|
||||||
@ -3687,7 +3898,8 @@ void Module::Interface::EndImportTmd(Kernel::HLERequestContext& ctx) {
|
|||||||
IPC::RequestBuilder rb(ctx, 1, 0);
|
IPC::RequestBuilder rb(ctx, 1, 0);
|
||||||
rb.Push(async_data->res);
|
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();
|
const FileSys::TitleMetadata& tmd_info = am->importing_title->cia_file.GetTMD();
|
||||||
|
|
||||||
ImportTitleContext& context = am->import_title_contexts[tmd_info.GetTitleID()];
|
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.state = ImportTitleContextState::WAITING_FOR_IMPORT;
|
||||||
context.size = 0;
|
context.size = 0;
|
||||||
for (size_t i = 0; i < tmd_info.GetContentCount(); i++) {
|
for (size_t i = 0; i < tmd_info.GetContentCount(); i++) {
|
||||||
|
if (tmd_info.GetContentOptional(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ImportContentContext content_context;
|
ImportContentContext content_context;
|
||||||
content_context.content_id = tmd_info.GetContentIDByIndex(i);
|
content_context.content_id = tmd_info.GetContentIDByIndex(i);
|
||||||
content_context.index = static_cast<u16>(i);
|
content_context.index = static_cast<u16>(i);
|
||||||
@ -3724,13 +3939,68 @@ void Module::Interface::CreateImportContentContexts(Kernel::HLERequestContext& c
|
|||||||
const u32 content_count = rp.Pop<u32>();
|
const u32 content_count = rp.Pop<u32>();
|
||||||
auto content_buf = rp.PopMappedBuffer();
|
auto content_buf = rp.PopMappedBuffer();
|
||||||
|
|
||||||
std::vector<u16> content_indices(content_count);
|
LOG_DEBUG(Service_AM, "");
|
||||||
content_buf.Read(content_indices.data(), 0, content_buf.GetSize());
|
|
||||||
|
|
||||||
|
if (!am->importing_title) {
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||||
|
ErrorLevel::Permanent));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_WARNING(Service_AM, "(STUBBED)");
|
struct AsyncData {
|
||||||
|
std::vector<u16> content_indices;
|
||||||
|
|
||||||
|
Result res{0};
|
||||||
|
};
|
||||||
|
std::shared_ptr<AsyncData> async_data = std::make_shared<AsyncData>();
|
||||||
|
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<u16>(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) {
|
void Module::Interface::BeginImportContent(Kernel::HLERequestContext& ctx) {
|
||||||
@ -4145,6 +4415,255 @@ void Module::Interface::ListTicketInfos(Kernel::HLERequestContext& ctx) {
|
|||||||
rb.Push(written);
|
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<u32>(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<u16_le> content_requested;
|
||||||
|
|
||||||
|
std::vector<ContentInfo> out_vec;
|
||||||
|
Kernel::MappedBuffer* content_info_out;
|
||||||
|
Result res{0};
|
||||||
|
};
|
||||||
|
auto async_data = std::make_shared<AsyncData>();
|
||||||
|
async_data->content_count = rp.Pop<u32>();
|
||||||
|
|
||||||
|
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<ContentInfo> out_vec;
|
||||||
|
Kernel::MappedBuffer* content_info_out;
|
||||||
|
Result res{0};
|
||||||
|
};
|
||||||
|
auto async_data = std::make_shared<AsyncData>();
|
||||||
|
async_data->content_count = rp.Pop<u32>();
|
||||||
|
async_data->start_index = rp.Pop<u32>();
|
||||||
|
|
||||||
|
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<u32>(tmd.GetContentCount()));
|
||||||
|
for (u32 i = async_data->start_index; i < end_index; i++) {
|
||||||
|
ContentInfo content_info = {};
|
||||||
|
content_info.index = static_cast<u16>(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<u16>(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<u32>(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<Service::FS::MediaType>(rp.Pop<u8>());
|
||||||
|
u64 title_id = rp.Pop<u64>();
|
||||||
|
u32 content_count = rp.Pop<u32>();
|
||||||
|
auto& content_requested_in = rp.PopMappedBuffer();
|
||||||
|
|
||||||
|
std::vector<u16_le> 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<u32>(-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<u32>(-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<u32>(-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<u64>(size_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Module::Interface::UpdateImportContentContexts(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
const u32 content_count = rp.Pop<u32>();
|
||||||
|
auto content_buf = rp.PopMappedBuffer();
|
||||||
|
|
||||||
|
std::vector<u16> 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) {
|
void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
|
|
||||||
|
|||||||
@ -181,8 +181,11 @@ public:
|
|||||||
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
|
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
|
||||||
const u8* buffer) override;
|
const u8* buffer) override;
|
||||||
|
|
||||||
|
Result PrepareToImportContent(const FileSys::TitleMetadata& tmd);
|
||||||
Result ProvideTicket(const FileSys::Ticket& ticket);
|
Result ProvideTicket(const FileSys::Ticket& ticket);
|
||||||
|
Result ProvideTMDForAdditionalContent(const FileSys::TitleMetadata& tmd);
|
||||||
const FileSys::TitleMetadata& GetTMD();
|
const FileSys::TitleMetadata& GetTMD();
|
||||||
|
FileSys::Ticket& GetTicket();
|
||||||
CIAInstallState GetCiaInstallState() {
|
CIAInstallState GetCiaInstallState() {
|
||||||
return install_state;
|
return install_state;
|
||||||
}
|
}
|
||||||
@ -208,6 +211,7 @@ private:
|
|||||||
bool decryption_authorized;
|
bool decryption_authorized;
|
||||||
bool is_done = false;
|
bool is_done = false;
|
||||||
bool is_closed = false;
|
bool is_closed = false;
|
||||||
|
bool is_additional_content = false;
|
||||||
|
|
||||||
// Whether it's installing an update, and what step of installation it is at
|
// Whether it's installing an update, and what step of installation it is at
|
||||||
bool is_update = false;
|
bool is_update = false;
|
||||||
@ -233,11 +237,13 @@ class CurrentImportingTitle {
|
|||||||
public:
|
public:
|
||||||
explicit CurrentImportingTitle(Core::System& system_, u64 title_id_,
|
explicit CurrentImportingTitle(Core::System& system_, u64 title_id_,
|
||||||
Service::FS::MediaType media_type_)
|
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;
|
CIAFile cia_file;
|
||||||
u64 title_id;
|
u64 title_id;
|
||||||
Service::FS::MediaType media_type;
|
Service::FS::MediaType media_type;
|
||||||
|
bool tmd_provided;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A file handled returned for Tickets to be written into and subsequently installed.
|
// A file handled returned for Tickets to be written into and subsequently installed.
|
||||||
@ -1005,6 +1011,16 @@ public:
|
|||||||
|
|
||||||
void ListTicketInfos(Kernel::HLERequestContext& ctx);
|
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);
|
void ExportTicketWrapped(Kernel::HLERequestContext& ctx);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@ -113,11 +113,11 @@ AM_NET::AM_NET(std::shared_ptr<Module> am) : Module::Interface(std::move(am), "a
|
|||||||
{0x081F, &AM_NET::GetNumTicketsOfProgram, "GetNumTicketsOfProgram"},
|
{0x081F, &AM_NET::GetNumTicketsOfProgram, "GetNumTicketsOfProgram"},
|
||||||
{0x0820, &AM_NET::ListTicketInfos, "ListTicketInfos"},
|
{0x0820, &AM_NET::ListTicketInfos, "ListTicketInfos"},
|
||||||
{0x0821, nullptr, "GetRightsOnlyTicketData"},
|
{0x0821, nullptr, "GetRightsOnlyTicketData"},
|
||||||
{0x0822, nullptr, "GetNumCurrentContentInfos"},
|
{0x0822, &AM_NET::GetNumCurrentContentInfos, "GetNumCurrentContentInfos"},
|
||||||
{0x0823, nullptr, "FindCurrentContentInfos"},
|
{0x0823, &AM_NET::FindCurrentContentInfos, "FindCurrentContentInfos"},
|
||||||
{0x0824, nullptr, "ListCurrentContentInfos"},
|
{0x0824, &AM_NET::ListCurrentContentInfos, "ListCurrentContentInfos"},
|
||||||
{0x0825, nullptr, "CalculateContextRequiredSize"},
|
{0x0825, &AM_NET::CalculateContextRequiredSize, "CalculateContextRequiredSize"},
|
||||||
{0x0826, nullptr, "UpdateImportContentContexts"},
|
{0x0826, &AM_NET::UpdateImportContentContexts, "UpdateImportContentContexts"},
|
||||||
{0x0827, nullptr, "DeleteAllDemoLaunchInfos"},
|
{0x0827, nullptr, "DeleteAllDemoLaunchInfos"},
|
||||||
{0x0828, nullptr, "BeginImportTitleForOverWrite"},
|
{0x0828, nullptr, "BeginImportTitleForOverWrite"},
|
||||||
{0x0829, &AM_NET::ExportTicketWrapped, "ExportTicketWrapped"},
|
{0x0829, &AM_NET::ExportTicketWrapped, "ExportTicketWrapped"},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user