mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	file_sys: add class for Title Metadata (TMD)
This commit is contained in:
		
							parent
							
								
									e21f2348e7
								
							
						
					
					
						commit
						8e10c9bb2e
					
				
					 3 changed files with 338 additions and 0 deletions
				
			
		|  | @ -29,6 +29,7 @@ set(SRCS | ||||||
|             file_sys/ncch_container.cpp |             file_sys/ncch_container.cpp | ||||||
|             file_sys/path_parser.cpp |             file_sys/path_parser.cpp | ||||||
|             file_sys/savedata_archive.cpp |             file_sys/savedata_archive.cpp | ||||||
|  |             file_sys/title_metadata.cpp | ||||||
|             frontend/camera/blank_camera.cpp |             frontend/camera/blank_camera.cpp | ||||||
|             frontend/camera/factory.cpp |             frontend/camera/factory.cpp | ||||||
|             frontend/camera/interface.cpp |             frontend/camera/interface.cpp | ||||||
|  |  | ||||||
							
								
								
									
										212
									
								
								src/core/file_sys/title_metadata.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								src/core/file_sys/title_metadata.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,212 @@ | ||||||
|  | // Copyright 2017 Citra Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <cinttypes> | ||||||
|  | #include <cryptopp/sha.h> | ||||||
|  | #include "common/alignment.h" | ||||||
|  | #include "common/file_util.h" | ||||||
|  | #include "common/logging/log.h" | ||||||
|  | #include "core/file_sys/title_metadata.h" | ||||||
|  | #include "core/loader/loader.h" | ||||||
|  | 
 | ||||||
|  | ////////////////////////////////////////////////////////////////////////////////////////////////////
 | ||||||
|  | // FileSys namespace
 | ||||||
|  | 
 | ||||||
|  | namespace FileSys { | ||||||
|  | 
 | ||||||
|  | static u32 GetSignatureSize(u32 signature_type) { | ||||||
|  |     switch (signature_type) { | ||||||
|  |     case Rsa4096Sha1: | ||||||
|  |     case Rsa4096Sha256: | ||||||
|  |         return 0x200; | ||||||
|  | 
 | ||||||
|  |     case Rsa2048Sha1: | ||||||
|  |     case Rsa2048Sha256: | ||||||
|  |         return 0x100; | ||||||
|  | 
 | ||||||
|  |     case EllipticSha1: | ||||||
|  |     case EcdsaSha256: | ||||||
|  |         return 0x3C; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Loader::ResultStatus TitleMetadata::Load() { | ||||||
|  |     FileUtil::IOFile file(filepath, "rb"); | ||||||
|  |     if (!file.IsOpen()) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     if (!file.ReadBytes(&signature_type, sizeof(u32_be))) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     // Signature lengths are variable, and the body follows the signature
 | ||||||
|  |     u32 signature_size = GetSignatureSize(signature_type); | ||||||
|  | 
 | ||||||
|  |     tmd_signature.resize(signature_size); | ||||||
|  |     if (!file.ReadBytes(&tmd_signature[0], signature_size)) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     // The TMD body start position is rounded to the nearest 0x40 after the signature
 | ||||||
|  |     size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); | ||||||
|  |     file.Seek(body_start, SEEK_SET); | ||||||
|  | 
 | ||||||
|  |     // Read our TMD body, then load the amount of ContentChunks specified
 | ||||||
|  |     if (file.ReadBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     for (u16 i = 0; i < tmd_body.content_count; i++) { | ||||||
|  |         ContentChunk chunk; | ||||||
|  |         if (file.ReadBytes(&chunk, sizeof(ContentChunk)) == sizeof(ContentChunk)) { | ||||||
|  |             tmd_chunks.push_back(chunk); | ||||||
|  |         } else { | ||||||
|  |             LOG_ERROR(Service_FS, "Malformed TMD %s, failed to load content chunk index %u!", | ||||||
|  |                       filepath.c_str(), i); | ||||||
|  |             return Loader::ResultStatus::ErrorInvalidFormat; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Loader::ResultStatus::Success; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Loader::ResultStatus TitleMetadata::Save() { | ||||||
|  |     FileUtil::IOFile file(filepath, "wb"); | ||||||
|  |     if (!file.IsOpen()) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     if (!file.WriteBytes(&signature_type, sizeof(u32_be))) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     // Signature lengths are variable, and the body follows the signature
 | ||||||
|  |     u32 signature_size = GetSignatureSize(signature_type); | ||||||
|  | 
 | ||||||
|  |     if (!file.WriteBytes(tmd_signature.data(), signature_size)) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     // The TMD body start position is rounded to the nearest 0x40 after the signature
 | ||||||
|  |     size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); | ||||||
|  |     file.Seek(body_start, SEEK_SET); | ||||||
|  | 
 | ||||||
|  |     // Update our TMD body values and hashes
 | ||||||
|  |     tmd_body.content_count = static_cast<u16>(tmd_chunks.size()); | ||||||
|  | 
 | ||||||
|  |     // TODO(shinyquagsire23): Do TMDs with more than one contentinfo exist?
 | ||||||
|  |     // For now we'll just adjust the first index to hold all content chunks
 | ||||||
|  |     // and ensure that no further content info data exists.
 | ||||||
|  |     tmd_body.contentinfo = {}; | ||||||
|  |     tmd_body.contentinfo[0].index = 0; | ||||||
|  |     tmd_body.contentinfo[0].command_count = static_cast<u16>(tmd_chunks.size()); | ||||||
|  | 
 | ||||||
|  |     CryptoPP::SHA256 chunk_hash; | ||||||
|  |     for (u16 i = 0; i < tmd_body.content_count; i++) { | ||||||
|  |         chunk_hash.Update(reinterpret_cast<u8*>(&tmd_chunks[i]), sizeof(ContentChunk)); | ||||||
|  |     } | ||||||
|  |     chunk_hash.Final(tmd_body.contentinfo[0].hash.data()); | ||||||
|  | 
 | ||||||
|  |     CryptoPP::SHA256 contentinfo_hash; | ||||||
|  |     for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { | ||||||
|  |         chunk_hash.Update(reinterpret_cast<u8*>(&tmd_body.contentinfo[i]), sizeof(ContentInfo)); | ||||||
|  |     } | ||||||
|  |     chunk_hash.Final(tmd_body.contentinfo_hash.data()); | ||||||
|  | 
 | ||||||
|  |     // Write our TMD body, then write each of our ContentChunks
 | ||||||
|  |     if (file.WriteBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) | ||||||
|  |         return Loader::ResultStatus::Error; | ||||||
|  | 
 | ||||||
|  |     for (u16 i = 0; i < tmd_body.content_count; i++) { | ||||||
|  |         ContentChunk chunk = tmd_chunks[i]; | ||||||
|  |         if (file.WriteBytes(&chunk, sizeof(ContentChunk)) != sizeof(ContentChunk)) | ||||||
|  |             return Loader::ResultStatus::Error; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Loader::ResultStatus::Success; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u64 TitleMetadata::GetTitleID() const { | ||||||
|  |     return tmd_body.title_id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u32 TitleMetadata::GetTitleType() const { | ||||||
|  |     return tmd_body.title_type; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u16 TitleMetadata::GetTitleVersion() const { | ||||||
|  |     return tmd_body.title_version; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u64 TitleMetadata::GetSystemVersion() const { | ||||||
|  |     return tmd_body.system_version; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | size_t TitleMetadata::GetContentCount() const { | ||||||
|  |     return tmd_chunks.size(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u32 TitleMetadata::GetBootContentID() const { | ||||||
|  |     return tmd_chunks[TMDContentIndex::Main].id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u32 TitleMetadata::GetManualContentID() const { | ||||||
|  |     return tmd_chunks[TMDContentIndex::Manual].id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u32 TitleMetadata::GetDLPContentID() const { | ||||||
|  |     return tmd_chunks[TMDContentIndex::DLP].id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TitleMetadata::SetTitleID(u64 title_id) { | ||||||
|  |     tmd_body.title_id = title_id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TitleMetadata::SetTitleType(u32 type) { | ||||||
|  |     tmd_body.title_type = type; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TitleMetadata::SetTitleVersion(u16 version) { | ||||||
|  |     tmd_body.title_version = version; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TitleMetadata::SetSystemVersion(u64 version) { | ||||||
|  |     tmd_body.system_version = version; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TitleMetadata::AddContentChunk(const ContentChunk& chunk) { | ||||||
|  |     tmd_chunks.push_back(chunk); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TitleMetadata::Print() const { | ||||||
|  |     LOG_DEBUG(Service_FS, "%s - %u chunks", filepath.c_str(), | ||||||
|  |               static_cast<u32>(tmd_body.content_count)); | ||||||
|  | 
 | ||||||
|  |     // Content info describes ranges of content chunks
 | ||||||
|  |     LOG_DEBUG(Service_FS, "Content info:"); | ||||||
|  |     for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { | ||||||
|  |         if (tmd_body.contentinfo[i].command_count == 0) | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |         LOG_DEBUG(Service_FS, "    Index %04X, Command Count %04X", | ||||||
|  |                   static_cast<u32>(tmd_body.contentinfo[i].index), | ||||||
|  |                   static_cast<u32>(tmd_body.contentinfo[i].command_count)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // For each content info, print their content chunk range
 | ||||||
|  |     for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { | ||||||
|  |         u16 index = static_cast<u16>(tmd_body.contentinfo[i].index); | ||||||
|  |         u16 count = static_cast<u16>(tmd_body.contentinfo[i].command_count); | ||||||
|  | 
 | ||||||
|  |         if (count == 0) | ||||||
|  |             continue; | ||||||
|  | 
 | ||||||
|  |         LOG_DEBUG(Service_FS, "Content chunks for content info index %zu:", i); | ||||||
|  |         for (u16 j = index; j < index + count; j++) { | ||||||
|  |             // Don't attempt to print content we don't have
 | ||||||
|  |             if (j > tmd_body.content_count) | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             const ContentChunk& chunk = tmd_chunks[j]; | ||||||
|  |             LOG_DEBUG(Service_FS, "    ID %08X, Index %04X, Type %04x, Size %016" PRIX64, | ||||||
|  |                       static_cast<u32>(chunk.id), static_cast<u32>(chunk.index), | ||||||
|  |                       static_cast<u32>(chunk.type), static_cast<u64>(chunk.size)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | } // namespace FileSys
 | ||||||
							
								
								
									
										125
									
								
								src/core/file_sys/title_metadata.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/core/file_sys/title_metadata.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,125 @@ | ||||||
|  | // Copyright 2017 Citra Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <string> | ||||||
|  | #include <vector> | ||||||
|  | #include "common/common_types.h" | ||||||
|  | #include "common/swap.h" | ||||||
|  | 
 | ||||||
|  | namespace Loader { | ||||||
|  | enum class ResultStatus; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ////////////////////////////////////////////////////////////////////////////////////////////////////
 | ||||||
|  | // FileSys namespace
 | ||||||
|  | 
 | ||||||
|  | namespace FileSys { | ||||||
|  | 
 | ||||||
|  | enum TMDSignatureType : u32 { | ||||||
|  |     Rsa4096Sha1 = 0x10000, | ||||||
|  |     Rsa2048Sha1 = 0x10001, | ||||||
|  |     EllipticSha1 = 0x10002, | ||||||
|  |     Rsa4096Sha256 = 0x10003, | ||||||
|  |     Rsa2048Sha256 = 0x10004, | ||||||
|  |     EcdsaSha256 = 0x10005 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | enum TMDContentTypeFlag : u16 { | ||||||
|  |     Encrypted = 1 << 1, | ||||||
|  |     Disc = 1 << 2, | ||||||
|  |     CFM = 1 << 3, | ||||||
|  |     Optional = 1 << 14, | ||||||
|  |     Shared = 1 << 15 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Helper which implements an interface to read and write Title Metadata (TMD) files. | ||||||
|  |  * If a file path is provided and the file exists, it can be parsed and used, otherwise | ||||||
|  |  * it must be created. The TMD file can then be interpreted, modified and/or saved. | ||||||
|  |  */ | ||||||
|  | class TitleMetadata { | ||||||
|  | public: | ||||||
|  |     struct ContentChunk { | ||||||
|  |         u32_be id; | ||||||
|  |         u16_be index; | ||||||
|  |         u16_be type; | ||||||
|  |         u64_be size; | ||||||
|  |         std::array<u8, 0x20> hash; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     static_assert(sizeof(ContentChunk) == 0x30, "TMD ContentChunk structure size is wrong"); | ||||||
|  | 
 | ||||||
|  |     struct ContentInfo { | ||||||
|  |         u16_be index; | ||||||
|  |         u16_be command_count; | ||||||
|  |         std::array<u8, 0x20> hash; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     static_assert(sizeof(ContentInfo) == 0x24, "TMD ContentInfo structure size is wrong"); | ||||||
|  | 
 | ||||||
|  | #pragma pack(push, 1) | ||||||
|  | 
 | ||||||
|  |     struct Body { | ||||||
|  |         std::array<u8, 0x40> issuer; | ||||||
|  |         u8 version; | ||||||
|  |         u8 ca_crl_version; | ||||||
|  |         u8 signer_crl_version; | ||||||
|  |         u8 reserved; | ||||||
|  |         u64_be system_version; | ||||||
|  |         u64_be title_id; | ||||||
|  |         u32_be title_type; | ||||||
|  |         u16_be group_id; | ||||||
|  |         u32_be savedata_size; | ||||||
|  |         u32_be srl_private_savedata_size; | ||||||
|  |         std::array<u8, 4> reserved_2; | ||||||
|  |         u8 srl_flag; | ||||||
|  |         std::array<u8, 0x31> reserved_3; | ||||||
|  |         u32_be access_rights; | ||||||
|  |         u16_be title_version; | ||||||
|  |         u16_be content_count; | ||||||
|  |         u16_be boot_content; | ||||||
|  |         std::array<u8, 2> reserved_4; | ||||||
|  |         std::array<u8, 0x20> contentinfo_hash; | ||||||
|  |         std::array<ContentInfo, 64> contentinfo; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     static_assert(sizeof(Body) == 0x9C4, "TMD body structure size is wrong"); | ||||||
|  | 
 | ||||||
|  | #pragma pack(pop) | ||||||
|  | 
 | ||||||
|  |     explicit TitleMetadata(std::string& path) : filepath(std::move(path)) {} | ||||||
|  |     Loader::ResultStatus Load(); | ||||||
|  |     Loader::ResultStatus Save(); | ||||||
|  | 
 | ||||||
|  |     u64 GetTitleID() const; | ||||||
|  |     u32 GetTitleType() const; | ||||||
|  |     u16 GetTitleVersion() const; | ||||||
|  |     u64 GetSystemVersion() const; | ||||||
|  |     size_t GetContentCount() const; | ||||||
|  |     u32 GetBootContentID() const; | ||||||
|  |     u32 GetManualContentID() const; | ||||||
|  |     u32 GetDLPContentID() const; | ||||||
|  | 
 | ||||||
|  |     void SetTitleID(u64 title_id); | ||||||
|  |     void SetTitleType(u32 type); | ||||||
|  |     void SetTitleVersion(u16 version); | ||||||
|  |     void SetSystemVersion(u64 version); | ||||||
|  |     void AddContentChunk(const ContentChunk& chunk); | ||||||
|  | 
 | ||||||
|  |     void Print() const; | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     enum TMDContentIndex { Main = 0, Manual = 1, DLP = 2 }; | ||||||
|  | 
 | ||||||
|  |     Body tmd_body; | ||||||
|  |     u32_be signature_type; | ||||||
|  |     std::vector<u8> tmd_signature; | ||||||
|  |     std::vector<ContentChunk> tmd_chunks; | ||||||
|  | 
 | ||||||
|  |     std::string filepath; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | } // namespace FileSys
 | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue