Support downloading owned DLCs (#950)

This commit is contained in:
PabloMK7 2025-04-23 17:08:48 +02:00 committed by GitHub
parent bac344d059
commit 391f91f735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 876 additions and 184 deletions

View file

@ -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;
} }

View file

@ -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;

View file

@ -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

View file

@ -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;
}; };

View file

@ -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));

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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:

View file

@ -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"},