IOS/ES: Write import files to /import first
This is slightly safer than writing contents to /title directly. We still cannot rename everything in one go atomically, but this allows implementing AddTitleCancel very easily. Also, this ensures that when a title import fails, no incomplete files will be left in the title directory, which can mess up the system menu.
This commit is contained in:
parent
af4da70902
commit
e656258949
|
@ -23,6 +23,13 @@ std::string RootUserPath(FromWhichRoot from)
|
||||||
return File::GetUserPath(idx);
|
return File::GetUserPath(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string GetImportTitlePath(u64 title_id, FromWhichRoot from)
|
||||||
|
{
|
||||||
|
return RootUserPath(from) + StringFromFormat("/import/%08x/%08x",
|
||||||
|
static_cast<u32>(title_id >> 32),
|
||||||
|
static_cast<u32>(title_id));
|
||||||
|
}
|
||||||
|
|
||||||
std::string GetTicketFileName(u64 _titleID, FromWhichRoot from)
|
std::string GetTicketFileName(u64 _titleID, FromWhichRoot from)
|
||||||
{
|
{
|
||||||
return StringFromFormat("%s/ticket/%08x/%08x.tik", RootUserPath(from).c_str(),
|
return StringFromFormat("%s/ticket/%08x/%08x.tik", RootUserPath(from).c_str(),
|
||||||
|
|
|
@ -22,6 +22,10 @@ enum FromWhichRoot
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string RootUserPath(FromWhichRoot from);
|
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 GetTicketFileName(u64 _titleID, FromWhichRoot from);
|
||||||
std::string GetTMDFileName(u64 _titleID, FromWhichRoot from);
|
std::string GetTMDFileName(u64 _titleID, FromWhichRoot from);
|
||||||
std::string GetTitleDataPath(u64 _titleID, FromWhichRoot from);
|
std::string GetTitleDataPath(u64 _titleID, FromWhichRoot from);
|
||||||
|
|
|
@ -431,33 +431,44 @@ IPCCommandResult ES::AddTicket(const IOCtlVRequest& request)
|
||||||
return GetDefaultReply(IPC_SUCCESS);
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: write this to /tmp (or /import?) first, as title imports can be cancelled.
|
static bool WriteImportTMD(const IOS::ES::TMDReader& tmd)
|
||||||
static bool WriteTMD(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::CreateFullPath(tmd_path);
|
||||||
|
|
||||||
File::IOFile fp(tmd_path, "wb");
|
File::IOFile file(tmd_path, "wb");
|
||||||
return fp.WriteBytes(tmd.GetRawTMD().data(), tmd.GetRawTMD().size());
|
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)
|
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))
|
if (!request.HasNumberOfValidVectors(1, 0))
|
||||||
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
|
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
|
||||||
|
|
||||||
std::vector<u8> tmd(request.in_vectors[0].size);
|
std::vector<u8> tmd(request.in_vectors[0].size);
|
||||||
Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, 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())
|
if (!m_addtitle_tmd.IsValid())
|
||||||
return GetDefaultReply(ES_INVALID_TMD);
|
return GetDefaultReply(ES_INVALID_TMD);
|
||||||
|
|
||||||
if (!WriteTMD(m_addtitle_tmd))
|
DiscIO::cUIDsys uid_sys{Common::FROM_CONFIGURED_ROOT};
|
||||||
return GetDefaultReply(ES_WRITE_FAILURE);
|
uid_sys.AddTitle(m_addtitle_tmd.GetTitleId());
|
||||||
|
|
||||||
return GetDefaultReply(IPC_SUCCESS);
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
}
|
}
|
||||||
|
@ -478,12 +489,11 @@ IPCCommandResult ES::AddTitleStart(const IOCtlVRequest& request)
|
||||||
return GetDefaultReply(ES_INVALID_TMD);
|
return GetDefaultReply(ES_INVALID_TMD);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WriteTMD(m_addtitle_tmd))
|
|
||||||
return GetDefaultReply(ES_WRITE_FAILURE);
|
|
||||||
|
|
||||||
DiscIO::cUIDsys uid_sys{Common::FROM_CONFIGURED_ROOT};
|
DiscIO::cUIDsys uid_sys{Common::FROM_CONFIGURED_ROOT};
|
||||||
uid_sys.AddTitle(m_addtitle_tmd.GetTitleId());
|
uid_sys.AddTitle(m_addtitle_tmd.GetTitleId());
|
||||||
|
|
||||||
|
// TODO: check and use the other vectors.
|
||||||
|
|
||||||
return GetDefaultReply(IPC_SUCCESS);
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,6 +565,9 @@ IPCCommandResult ES::AddContentFinish(const IOCtlVRequest& request)
|
||||||
if (!request.HasNumberOfValidVectors(1, 0))
|
if (!request.HasNumberOfValidVectors(1, 0))
|
||||||
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
|
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);
|
u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
|
||||||
INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTFINISH: content fd %08x", content_fd);
|
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);
|
return GetDefaultReply(ES_HASH_DOESNT_MATCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string content_path;
|
// Just write all contents to the title import directory. AddTitleFinish will
|
||||||
if (content_info.IsShared())
|
// move the contents to the proper location.
|
||||||
{
|
const std::string tmp_path = GetImportContentPath(m_addtitle_tmd, m_addtitle_content_id);
|
||||||
DiscIO::CSharedContent shared_content{Common::FROM_SESSION_ROOT};
|
File::CreateFullPath(tmp_path);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
File::IOFile fp(content_path, "wb");
|
File::IOFile fp(tmp_path, "wb");
|
||||||
fp.WriteBytes(decrypted_data.data(), content_info.size);
|
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;
|
m_addtitle_content_id = 0xFFFFFFFF;
|
||||||
return GetDefaultReply(IPC_SUCCESS);
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void AbortImport(const u64 title_id, const std::vector<std::string>& 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)
|
IPCCommandResult ES::AddTitleFinish(const IOCtlVRequest& request)
|
||||||
{
|
{
|
||||||
if (!request.HasNumberOfValidVectors(0, 0) || !m_addtitle_tmd.IsValid())
|
if (!request.HasNumberOfValidVectors(0, 0) || !m_addtitle_tmd.IsValid())
|
||||||
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
|
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
|
||||||
|
|
||||||
|
std::vector<std::string> 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");
|
INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLEFINISH");
|
||||||
m_addtitle_tmd.SetBytes({});
|
m_addtitle_tmd.SetBytes({});
|
||||||
return GetDefaultReply(IPC_SUCCESS);
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
|
Loading…
Reference in New Issue