/* * Copyright (c) 2019 Adubbz * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ncm_contentmetadatabase.hpp" #include "ncm_utils.hpp" #include "debug.hpp" namespace sts::ncm { namespace { struct InstallContentMetaHeader { u16 extended_header_size; u16 content_count; u16 content_meta_count; ContentMetaAttribute attributes; u8 padding; }; static_assert(sizeof(InstallContentMetaHeader) == 0x8, "InstallContentMetaHeader definition!"); struct ApplicationMetaExtendedHeader { TitleId patch_id; u32 required_system_version; u32 padding; }; struct PatchMetaExtendedHeader { TitleId application_id; u32 required_system_version; u32 extended_data_size; u8 reserved[0x8]; }; struct AddOnContentMetaExtendedHeader { TitleId application_id; u32 required_application_version; u32 padding; }; struct SystemUpdateMetaExtendedHeader { u32 extended_data_size; }; inline const InstallContentMetaHeader* GetValueHeader(const void* value) { return reinterpret_cast(value); } template inline const ExtendedHeaderType* GetValueExtendedHeader(const void* value) { return reinterpret_cast(reinterpret_cast(value) + sizeof(InstallContentMetaHeader)); } inline const ContentInfo* GetValueContentInfos(const void* value) { return reinterpret_cast(reinterpret_cast(value) + sizeof(InstallContentMetaHeader) + GetValueHeader(value)->extended_header_size); } inline const ContentMetaInfo* GetValueContentMetaInfos(const void* value) { auto header = GetValueHeader(value); return reinterpret_cast(reinterpret_cast(GetValueContentInfos(value)) + sizeof(ContentInfo) * header->content_count); } Result GetContentMetaSize(size_t *out, const ContentMetaKey &key, const kvdb::MemoryKeyValueStore *kvs) { R_TRY_CATCH(kvs->GetValueSize(out, key)) { R_CATCH(ResultKvdbKeyNotFound) { return ResultNcmContentMetaNotFound; } } R_END_TRY_CATCH; return ResultSuccess; } Result GetContentMetaValuePointer(const void **out_value_ptr, size_t *out_size, const ContentMetaKey &key, const kvdb::MemoryKeyValueStore *kvs) { R_TRY(GetContentMetaSize(out_size, key, kvs)); R_TRY(kvs->GetValuePointer(out_value_ptr, key)); return ResultSuccess; } } Result ContentMetaDatabaseInterface::GetContentIdByTypeImpl(ContentId* out, const ContentMetaKey& key, ContentType type, std::optional id_offset) { R_TRY(this->EnsureEnabled()); const auto it = this->kvs->lower_bound(key); if (it == this->kvs->end() || it->GetKey().id != key.id) { return ResultNcmContentMetaNotFound; } const auto stored_key = it->GetKey(); const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, stored_key, this->kvs)); const auto header = GetValueHeader(value); if (header->content_count == 0) { return ResultNcmContentNotFound; } const ContentInfo* content_infos = GetValueContentInfos(value); const ContentInfo* found_content_info = nullptr; if (id_offset) { for (size_t i = 0; i < header->content_count; i++) { const ContentInfo* content_info = &content_infos[i]; if (content_info->content_type == type && content_info->id_offset == *id_offset) { found_content_info = content_info; break; } } } else { const ContentInfo* lowest_id_offset_info = nullptr; for (size_t i = 0; i < header->content_count; i++) { const ContentInfo* content_info = &content_infos[i]; if (content_info->content_type == type && (!lowest_id_offset_info || lowest_id_offset_info->id_offset > content_info->id_offset)) { lowest_id_offset_info = content_info; } } found_content_info = lowest_id_offset_info; } if (!found_content_info) { return ResultNcmContentNotFound; } *out = found_content_info->content_id; return ResultSuccess; } Result ContentMetaDatabaseInterface::GetLatestContentMetaKeyImpl(ContentMetaKey* out_key, TitleId tid) { R_TRY(this->EnsureEnabled()); ContentMetaKey key = {0}; key.id = tid; bool found_key = false; for (auto entry = this->kvs->lower_bound(key); entry != this->kvs->end(); entry++) { if (entry->GetKey().id != key.id) { break; } if (entry->GetKey().install_type == ContentInstallType::Full) { key = entry->GetKey(); found_key = true; } } if (!found_key) { return ResultNcmContentMetaNotFound; } *out_key = key; return ResultSuccess; } Result ContentMetaDatabaseInterface::EnsureEnabled() { if (this->disabled) { return ResultNcmInvalidContentMetaDatabase; } return ResultSuccess; } Result ContentMetaDatabaseInterface::Set(ContentMetaKey key, InBuffer value) { R_DEBUG_START R_TRY(this->EnsureEnabled()); D_LOG(" set title_id: 0x%lx\n", key.id); D_LOG(" set version: 0x%x\n", key.version); R_TRY(this->kvs->Set(key, value.buffer, value.num_elements)); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::Get(Out out_size, ContentMetaKey key, OutBuffer out_value) { R_DEBUG_START R_TRY(this->EnsureEnabled()); R_TRY(this->kvs->Get(out_size.GetPointer(), out_value.buffer, out_value.num_elements, key)); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::Remove(ContentMetaKey key) { R_DEBUG_START R_TRY(this->EnsureEnabled()); R_TRY(this->kvs->Remove(key)); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetContentIdByType(Out out_content_id, ContentMetaKey key, ContentType type) { ContentId content_id; R_TRY(this->GetContentIdByTypeImpl(&content_id, key, type, std::nullopt)); out_content_id.SetValue(content_id); return ResultSuccess; } Result ContentMetaDatabaseInterface::ListContentInfo(Out out_count, OutBuffer out_info, ContentMetaKey key, u32 offset) { R_DEBUG_START if (offset >> 0x1f != 0) { return ResultNcmInvalidOffset; } R_TRY(this->EnsureEnabled()); const void *value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); const auto header = GetValueHeader(value); const auto content_infos = GetValueContentInfos(value); size_t count; for (count = 0; offset + count < header->content_count && count < out_info.num_elements; count++) { out_info[count] = content_infos[offset + count]; } out_count.SetValue(count); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::List(Out out_entries_total, Out out_entries_written, OutBuffer out_info, ContentMetaType type, TitleId application_title_id, TitleId title_id_min, TitleId title_id_max, ContentInstallType install_type) { R_DEBUG_START R_TRY(this->EnsureEnabled()); size_t entries_total = 0; size_t entries_written = 0; /* If there are no entries then we've already successfully listed them all. */ if (this->kvs->GetCount() == 0) { out_entries_total.SetValue(entries_total); out_entries_written.SetValue(entries_written); return ResultSuccess; } for (auto entry = this->kvs->begin(); entry != this->kvs->end(); entry++) { ContentMetaKey key = entry->GetKey(); /* Check if this entry matches the given filters. */ if (!((static_cast(type) == 0 || key.type == type) && (title_id_min <= key.id && key.id <= title_id_max) && (key.install_type == ContentInstallType::Full || key.install_type == install_type))) { continue; } if (static_cast(application_title_id) != 0) { const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); /* Each of these types are owned by an application. We need to check if their owner application matches the filter. */ if (key.type == ContentMetaType::Application || key.type == ContentMetaType::Patch || key.type == ContentMetaType::AddOnContent || key.type == ContentMetaType::Delta) { TitleId entry_application_tid = key.id; switch (key.type) { case ContentMetaType::Application: break; default: /* The first u64 of all non-application extended headers is the application title id. */ entry_application_tid = *GetValueExtendedHeader(value); } /* Application tid doesn't match filter, skip this entry. */ if (entry_application_tid != application_title_id) { continue; } } } /* Write the entry to the output buffer. */ if (entries_written < out_info.num_elements) { out_info[entries_written] = key; entries_written++; } entries_total++; } out_entries_total.SetValue(entries_total); out_entries_written.SetValue(entries_written); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetLatestContentMetaKey(Out out_key, TitleId title_id) { R_DEBUG_START R_TRY(this->EnsureEnabled()); ContentMetaKey key; R_TRY(this->GetLatestContentMetaKeyImpl(&key, title_id)); out_key.SetValue(key); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::ListApplication(Out out_entries_total, Out out_entries_written, OutBuffer out_keys, ContentMetaType type) { R_DEBUG_START R_TRY(this->EnsureEnabled()); size_t entries_total = 0; size_t entries_written = 0; /* If there are no entries then we've already successfully listed them all. */ if (this->kvs->GetCount() == 0) { out_entries_total.SetValue(entries_total); out_entries_written.SetValue(entries_written); return ResultSuccess; } for (auto entry = this->kvs->begin(); entry != this->kvs->end(); entry++) { ContentMetaKey key = entry->GetKey(); /* Check if this entry matches the given filters. */ if (!((static_cast(type) == 0 || key.type == type))) { continue; } const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); if (key.type == ContentMetaType::Application || key.type == ContentMetaType::Patch || key.type == ContentMetaType::AddOnContent || key.type == ContentMetaType::Delta) { TitleId application_tid = key.id; switch (key.type) { case ContentMetaType::Application: break; default: /* The first u64 of all non-application extended headers is the application title id. */ application_tid = *GetValueExtendedHeader(value); } /* Write the entry to the output buffer. */ if (entries_written < out_keys.num_elements) { ApplicationContentMetaKey* out_app_key = &out_keys[entries_written]; out_app_key->application_title_id = application_tid; out_app_key->key = key; entries_written++; } entries_total++; } } out_entries_total.SetValue(entries_total); out_entries_written.SetValue(entries_written); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::Has(Out out, ContentMetaKey key) { R_DEBUG_START R_TRY(this->EnsureEnabled()); if (this->kvs->GetCount() == 0) { out.SetValue(false); return ResultSuccess; } bool has = false; const auto it = this->kvs->lower_bound(key); if (it != this->kvs->end()) { has = it->GetKey() == key; } out.SetValue(has); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::HasAll(Out out, InBuffer keys) { R_DEBUG_START R_TRY(this->EnsureEnabled()); bool has = true; for (size_t i = 0; i < keys.num_elements && has; i++) { R_TRY(this->Has(&has, keys[i])); } out.SetValue(has); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetSize(Out out_size, ContentMetaKey key) { R_DEBUG_START R_TRY(this->EnsureEnabled()); if (this->kvs->GetCount() == 0) { return ResultNcmContentMetaNotFound; } const auto it = this->kvs->lower_bound(key); if (it == this->kvs->end() || it->GetKey() != key) { return ResultNcmContentMetaNotFound; } out_size.SetValue(it->GetValueSize()); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetRequiredSystemVersion(Out out_version, ContentMetaKey key) { R_DEBUG_START R_TRY(this->EnsureEnabled()); if (key.type != ContentMetaType::Application && key.type != ContentMetaType::Patch) { return ResultNcmInvalidContentMetaKey; } const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); /* Required system version is at the same offset for the patch and application extended header. We use the application header for convenience. */ const auto ext_header = GetValueExtendedHeader(value); out_version.SetValue(ext_header->required_system_version); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetPatchId(Out out_patch_id, ContentMetaKey key) { R_DEBUG_START R_TRY(this->EnsureEnabled()); if (key.type != ContentMetaType::Application) { return ResultNcmInvalidContentMetaKey; } D_LOG(" key title_id: 0x%lx\n", key.id); D_LOG(" key version: 0x%x\n", key.version); const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); const auto ext_header = GetValueExtendedHeader(value); out_patch_id.SetValue(ext_header->patch_id); D_LOG(" patch title_id: 0x%lx\n", *out_patch_id); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::DisableForcibly() { R_DEBUG_START this->disabled = true; return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::LookupOrphanContent(OutBuffer out_orphaned, InBuffer content_ids) { R_DEBUG_START R_TRY(this->EnsureEnabled()); if (out_orphaned.num_elements < content_ids.num_elements) { return ResultNcmBufferInsufficient; } /* Default to orphaned for all content ids. */ if (out_orphaned.num_elements > 0) { std::fill_n(out_orphaned.buffer, out_orphaned.num_elements, true); } if (this->kvs->GetCount() == 0) { return ResultSuccess; } for (auto entry = this->kvs->begin(); entry != this->kvs->end(); entry++) { const auto value = entry->GetValuePointer(); const auto header = GetValueHeader(value); if (header->content_count == 0) { continue; } if (content_ids.num_elements == 0) { continue; } const ContentInfo* content_infos = GetValueContentInfos(value); for (size_t i = 0; i < header->content_count; i++) { const ContentInfo* content_info = &content_infos[i]; /* Check if any of this entry's content infos matches one of the provided content ids. If they do, then the content id isn't orphaned. */ for (size_t j = 0; j < content_ids.num_elements; j++) { const ContentId content_id = content_ids[j]; if (content_id == content_info->content_id) { out_orphaned[j] = false; break; } } } } return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::Commit() { R_DEBUG_START R_TRY(this->EnsureEnabled()); R_TRY(this->kvs->Save()); R_TRY(fsdevCommitDevice(this->mount_name)); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::HasContent(Out out, ContentMetaKey key, ContentId content_id) { R_DEBUG_START const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); const auto header = GetValueHeader(value); const ContentInfo* content_infos = GetValueContentInfos(value); if (header->content_count > 0) { for (size_t i = 0; i < header->content_count; i++) { const ContentInfo* content_info = &content_infos[i]; if (content_id == content_info->content_id) { out.SetValue(false); return ResultSuccess; } } } out.SetValue(false); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::ListContentMetaInfo(Out out_entries_written, OutBuffer out_meta_info, ContentMetaKey key, u32 start_index) { R_DEBUG_START if (start_index >> 0x1f != 0) { return ResultNcmInvalidOffset; } R_TRY(this->EnsureEnabled()); const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); const auto header = GetValueHeader(value); const auto content_meta_infos = GetValueContentMetaInfos(value); size_t entries_written = 0; if (out_meta_info.num_elements == 0) { out_entries_written.SetValue(0); return ResultSuccess; } for (size_t i = start_index; i < out_meta_info.num_elements; i++) { /* We have no more entries we can read out. */ if (header->content_meta_count <= start_index + i) { break; } out_meta_info[i] = content_meta_infos[i]; entries_written = i + 1; } out_entries_written.SetValue(entries_written); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetAttributes(Out out_attributes, ContentMetaKey key) { R_DEBUG_START R_TRY(this->EnsureEnabled()); const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); const auto header = GetValueHeader(value); out_attributes.SetValue(header->attributes); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetRequiredApplicationVersion(Out out_version, ContentMetaKey key) { R_DEBUG_START R_TRY(this->EnsureEnabled()); if (key.type != ContentMetaType::AddOnContent) { return ResultNcmInvalidContentMetaKey; } const void* value = nullptr; size_t value_size = 0; R_TRY(GetContentMetaValuePointer(&value, &value_size, key, this->kvs)); const auto ext_header = GetValueExtendedHeader(value); out_version.SetValue(ext_header->required_application_version); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetContentIdByTypeAndIdOffset(Out out_content_id, ContentMetaKey key, ContentType type, u8 id_offset) { R_DEBUG_START ContentId content_id; R_TRY(this->GetContentIdByTypeImpl(&content_id, key, type, std::optional(id_offset))); out_content_id.SetValue(content_id); return ResultSuccess; R_DEBUG_END } Result ContentMetaDatabaseInterface::GetLatestProgram(ContentId* out_content_id, TitleId title_id) { ContentMetaKey key; R_TRY(this->GetLatestContentMetaKey(&key, title_id)); R_TRY(this->GetContentIdByType(out_content_id, key, ContentType::Program)); return ResultSuccess; } Result ContentMetaDatabaseInterface::GetLatestData(ContentId* out_content_id, TitleId title_id) { ContentMetaKey key; R_TRY(this->GetLatestContentMetaKey(&key, title_id)); R_TRY(this->GetContentIdByType(out_content_id, key, ContentType::Data)); return ResultSuccess; } Result OnMemoryContentMetaDatabaseInterface::GetLatestContentMetaKey(Out out_key, TitleId title_id) { R_DEBUG_START R_TRY(this->EnsureEnabled()); const ContentMetaKey key = ContentMetaKey::Make(title_id, 0, ContentMetaType::Unknown); std::optional found_key; for (auto entry = this->kvs->lower_bound(key); entry != this->kvs->end(); entry++) { if (entry->GetKey().id != title_id) { break; } found_key = entry->GetKey(); } if (!found_key) { return ResultNcmContentMetaNotFound; } *out_key = *found_key; return ResultSuccess; R_DEBUG_END } Result OnMemoryContentMetaDatabaseInterface::LookupOrphanContent(OutBuffer out_orphaned, InBuffer content_ids) { return ResultNcmInvalidContentMetaDatabase; } Result OnMemoryContentMetaDatabaseInterface::Commit() { R_TRY(this->EnsureEnabled()); return ResultSuccess; } }