diff --git a/Source/Core/Common/NandPaths.cpp b/Source/Core/Common/NandPaths.cpp index 3984d2d620..9206fbe4e6 100644 --- a/Source/Core/Common/NandPaths.cpp +++ b/Source/Core/Common/NandPaths.cpp @@ -23,6 +23,13 @@ std::string RootUserPath(FromWhichRoot from) return File::GetUserPath(idx); } +std::string GetImportTitlePath(u64 title_id, FromWhichRoot from) +{ + return RootUserPath(from) + StringFromFormat("/import/%08x/%08x", + static_cast(title_id >> 32), + static_cast(title_id)); +} + std::string GetTicketFileName(u64 _titleID, FromWhichRoot from) { return StringFromFormat("%s/ticket/%08x/%08x.tik", RootUserPath(from).c_str(), diff --git a/Source/Core/Common/NandPaths.h b/Source/Core/Common/NandPaths.h index c5a3234e8c..4910e29465 100644 --- a/Source/Core/Common/NandPaths.h +++ b/Source/Core/Common/NandPaths.h @@ -22,6 +22,10 @@ enum FromWhichRoot }; std::string RootUserPath(FromWhichRoot from); + +// Returns /import/%08x/%08x. Intended for use by ES. +std::string GetImportTitlePath(u64 title_id, FromWhichRoot from = FROM_SESSION_ROOT); + std::string GetTicketFileName(u64 _titleID, FromWhichRoot from); std::string GetTMDFileName(u64 _titleID, FromWhichRoot from); std::string GetTitleDataPath(u64 _titleID, FromWhichRoot from); diff --git a/Source/Core/Core/IOS/ES/ES.cpp b/Source/Core/Core/IOS/ES/ES.cpp index 7e0f13d338..18cb05a632 100644 --- a/Source/Core/Core/IOS/ES/ES.cpp +++ b/Source/Core/Core/IOS/ES/ES.cpp @@ -431,33 +431,44 @@ IPCCommandResult ES::AddTicket(const IOCtlVRequest& request) return GetDefaultReply(IPC_SUCCESS); } -// TODO: write this to /tmp (or /import?) first, as title imports can be cancelled. -static bool WriteTMD(const IOS::ES::TMDReader& tmd) +static bool WriteImportTMD(const IOS::ES::TMDReader& tmd) { - const std::string tmd_path = Common::GetTMDFileName(tmd.GetTitleId(), Common::FROM_SESSION_ROOT); + const std::string tmd_path = Common::GetImportTitlePath(tmd.GetTitleId()) + "/content/title.tmd"; File::CreateFullPath(tmd_path); - File::IOFile fp(tmd_path, "wb"); - return fp.WriteBytes(tmd.GetRawTMD().data(), tmd.GetRawTMD().size()); + File::IOFile file(tmd_path, "wb"); + return file.WriteBytes(tmd.GetRawTMD().data(), tmd.GetRawTMD().size()); +} + +static bool MoveImportTMDToTitleDirectory(const IOS::ES::TMDReader& tmd) +{ + const std::string src = Common::GetImportTitlePath(tmd.GetTitleId()) + "/content/title.tmd"; + const std::string dest = Common::GetTMDFileName(tmd.GetTitleId(), Common::FROM_SESSION_ROOT); + return File::RenameSync(src, dest); +} + +static std::string GetImportContentPath(const IOS::ES::TMDReader& tmd, u32 content_id) +{ + return Common::GetImportTitlePath(tmd.GetTitleId()) + + StringFromFormat("/content/%08x.app", content_id); } IPCCommandResult ES::AddTMD(const IOCtlVRequest& request) { - // This may appear to be very similar to AddTitleStart, but AddTitleStart takes - // three additional vectors and may do some additional processing -- so let's keep these separate. - if (!request.HasNumberOfValidVectors(1, 0)) return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT); std::vector tmd(request.in_vectors[0].size); Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, request.in_vectors[0].size); - m_addtitle_tmd.SetBytes(tmd); + // Ioctlv 0x2b writes the TMD to /tmp/title.tmd (for imports) and doesn't seem to write it + // to either /import or /title. So here we simply have to set the import TMD. + m_addtitle_tmd.SetBytes(std::move(tmd)); if (!m_addtitle_tmd.IsValid()) return GetDefaultReply(ES_INVALID_TMD); - if (!WriteTMD(m_addtitle_tmd)) - return GetDefaultReply(ES_WRITE_FAILURE); + DiscIO::cUIDsys uid_sys{Common::FROM_CONFIGURED_ROOT}; + uid_sys.AddTitle(m_addtitle_tmd.GetTitleId()); return GetDefaultReply(IPC_SUCCESS); } @@ -478,12 +489,11 @@ IPCCommandResult ES::AddTitleStart(const IOCtlVRequest& request) return GetDefaultReply(ES_INVALID_TMD); } - if (!WriteTMD(m_addtitle_tmd)) - return GetDefaultReply(ES_WRITE_FAILURE); - DiscIO::cUIDsys uid_sys{Common::FROM_CONFIGURED_ROOT}; uid_sys.AddTitle(m_addtitle_tmd.GetTitleId()); + // TODO: check and use the other vectors. + return GetDefaultReply(IPC_SUCCESS); } @@ -555,6 +565,9 @@ IPCCommandResult ES::AddContentFinish(const IOCtlVRequest& request) if (!request.HasNumberOfValidVectors(1, 0)) return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT); + if (m_addtitle_content_id == 0xFFFFFFFF) + return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT); + u32 content_fd = Memory::Read_U32(request.in_vectors[0].address); INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTFINISH: content fd %08x", content_fd); @@ -590,32 +603,83 @@ IPCCommandResult ES::AddContentFinish(const IOCtlVRequest& request) return GetDefaultReply(ES_HASH_DOESNT_MATCH); } - std::string content_path; - if (content_info.IsShared()) - { - DiscIO::CSharedContent shared_content{Common::FROM_SESSION_ROOT}; - content_path = shared_content.AddSharedContent(content_info.sha1.data()); - } - else - { - content_path = StringFromFormat( - "%s%08x.app", - Common::GetTitleContentPath(m_addtitle_tmd.GetTitleId(), Common::FROM_SESSION_ROOT).c_str(), - m_addtitle_content_id); - } + // Just write all contents to the title import directory. AddTitleFinish will + // move the contents to the proper location. + const std::string tmp_path = GetImportContentPath(m_addtitle_tmd, m_addtitle_content_id); + File::CreateFullPath(tmp_path); - File::IOFile fp(content_path, "wb"); - fp.WriteBytes(decrypted_data.data(), content_info.size); + File::IOFile fp(tmp_path, "wb"); + if (!fp.WriteBytes(decrypted_data.data(), content_info.size)) + { + ERROR_LOG(IOS_ES, "AddContentFinish: Failed to write to %s", tmp_path.c_str()); + return GetDefaultReply(ES_WRITE_FAILURE); + } m_addtitle_content_id = 0xFFFFFFFF; return GetDefaultReply(IPC_SUCCESS); } +static void AbortImport(const u64 title_id, const std::vector& processed_paths) +{ + for (const auto& path : processed_paths) + File::Delete(path); + + const std::string import_dir = Common::GetImportTitlePath(title_id); + File::DeleteDirRecursively(import_dir); +} + IPCCommandResult ES::AddTitleFinish(const IOCtlVRequest& request) { if (!request.HasNumberOfValidVectors(0, 0) || !m_addtitle_tmd.IsValid()) return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT); + std::vector processed_paths; + + for (const auto& content_info : m_addtitle_tmd.GetContents()) + { + const std::string source = GetImportContentPath(m_addtitle_tmd, content_info.id); + + // Contents may not have been all imported. This is normal and this isn't an error condition. + if (!File::Exists(source)) + continue; + + std::string content_path; + if (content_info.IsShared()) + { + DiscIO::CSharedContent shared_content{Common::FROM_SESSION_ROOT}; + content_path = shared_content.AddSharedContent(content_info.sha1.data()); + } + else + { + content_path = + StringFromFormat("%s%08x.app", Common::GetTitleContentPath(m_addtitle_tmd.GetTitleId(), + Common::FROM_SESSION_ROOT) + .c_str(), + content_info.id); + } + + File::CreateFullPath(content_path); + if (!File::RenameSync(source, content_path)) + { + ERROR_LOG(IOS_ES, "AddTitleFinish: Failed to rename %s to %s", source.c_str(), + content_path.c_str()); + AbortImport(m_addtitle_tmd.GetTitleId(), processed_paths); + return GetDefaultReply(ES_WRITE_FAILURE); + } + + // Do not delete shared contents even if the import fails. This is because + // they can be used by several titles and it's not safe to delete them. + // + // The reason we delete private contents is to avoid having a title with half-complete + // contents, as it can cause issues with the system menu. On the other hand, leaving + // shared contents does not cause any issue. + if (!content_info.IsShared()) + processed_paths.push_back(content_path); + } + + if (!WriteImportTMD(m_addtitle_tmd) || !MoveImportTMDToTitleDirectory(m_addtitle_tmd)) + return GetDefaultReply(ES_WRITE_FAILURE); + INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLEFINISH"); m_addtitle_tmd.SetBytes({}); return GetDefaultReply(IPC_SUCCESS);