service/am: Clean up and optimize CIA installation. (#6718)
This commit is contained in:
parent
22c4eb86d7
commit
335fb78c5c
4 changed files with 101 additions and 55 deletions
|
@ -181,6 +181,12 @@ std::array<u8, 16> TitleMetadata::GetContentCTRByIndex(std::size_t index) const
|
||||||
return ctr;
|
return ctr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TitleMetadata::HasEncryptedContent() const {
|
||||||
|
return std::any_of(tmd_chunks.begin(), tmd_chunks.end(), [](auto& chunk) {
|
||||||
|
return (static_cast<u16>(chunk.type) & FileSys::TMDContentTypeFlag::Encrypted) != 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void TitleMetadata::SetTitleID(u64 title_id) {
|
void TitleMetadata::SetTitleID(u64 title_id) {
|
||||||
tmd_body.title_id = title_id;
|
tmd_body.title_id = title_id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,7 @@ public:
|
||||||
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;
|
||||||
std::array<u8, 16> GetContentCTRByIndex(std::size_t index) const;
|
std::array<u8, 16> GetContentCTRByIndex(std::size_t index) const;
|
||||||
|
bool HasEncryptedContent() const;
|
||||||
|
|
||||||
void SetTitleID(u64 title_id);
|
void SetTitleID(u64 title_id);
|
||||||
void SetTitleType(u32 type);
|
void SetTitleType(u32 type);
|
||||||
|
|
|
@ -96,22 +96,38 @@ ResultVal<std::size_t> CIAFile::Read(u64 offset, std::size_t length, u8* buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode CIAFile::WriteTicket() {
|
ResultCode CIAFile::WriteTicket() {
|
||||||
container.LoadTicket(data, container.GetTicketOffset());
|
auto load_result = container.LoadTicket(data, container.GetTicketOffset());
|
||||||
|
if (load_result != Loader::ResultStatus::Success) {
|
||||||
|
LOG_ERROR(Service_AM, "Could not read ticket from CIA.");
|
||||||
|
// TODO: Correct result code.
|
||||||
|
return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument,
|
||||||
|
ErrorLevel::Permanent};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Write out .tik files to nand?
|
||||||
|
|
||||||
install_state = CIAInstallState::TicketLoaded;
|
install_state = CIAInstallState::TicketLoaded;
|
||||||
return RESULT_SUCCESS;
|
return RESULT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode CIAFile::WriteTitleMetadata() {
|
ResultCode CIAFile::WriteTitleMetadata() {
|
||||||
container.LoadTitleMetadata(data, container.GetTitleMetadataOffset());
|
auto load_result = container.LoadTitleMetadata(data, container.GetTitleMetadataOffset());
|
||||||
|
if (load_result != Loader::ResultStatus::Success) {
|
||||||
|
LOG_ERROR(Service_AM, "Could not read title metadata from CIA.");
|
||||||
|
// TODO: Correct result code.
|
||||||
|
return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument,
|
||||||
|
ErrorLevel::Permanent};
|
||||||
|
}
|
||||||
|
|
||||||
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
|
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
|
||||||
tmd.Print();
|
tmd.Print();
|
||||||
|
|
||||||
// If a TMD already exists for this app (ie 00000000.tmd), the incoming TMD
|
// If a TMD already exists for this app (ie 00000000.tmd), the incoming TMD
|
||||||
// will be the same plus one, (ie 00000001.tmd), both will be kept until
|
// will be the same plus one, (ie 00000001.tmd), both will be kept until
|
||||||
// the install is finalized and old contents can be discarded.
|
// the install is finalized and old contents can be discarded.
|
||||||
if (FileUtil::Exists(GetTitleMetadataPath(media_type, tmd.GetTitleID())))
|
if (FileUtil::Exists(GetTitleMetadataPath(media_type, tmd.GetTitleID()))) {
|
||||||
is_update = true;
|
is_update = true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string tmd_path = GetTitleMetadataPath(media_type, tmd.GetTitleID(), is_update);
|
std::string tmd_path = GetTitleMetadataPath(media_type, tmd.GetTitleID(), is_update);
|
||||||
|
|
||||||
|
@ -121,28 +137,49 @@ ResultCode CIAFile::WriteTitleMetadata() {
|
||||||
FileUtil::CreateFullPath(tmd_folder);
|
FileUtil::CreateFullPath(tmd_folder);
|
||||||
|
|
||||||
// Save TMD so that we can start getting new .app paths
|
// Save TMD so that we can start getting new .app paths
|
||||||
if (tmd.Save(tmd_path) != Loader::ResultStatus::Success)
|
if (tmd.Save(tmd_path) != Loader::ResultStatus::Success) {
|
||||||
return FileSys::ERROR_INSUFFICIENT_SPACE;
|
LOG_ERROR(Service_AM, "Failed to install title metadata file from CIA.");
|
||||||
|
// TODO: Correct result code.
|
||||||
|
return FileSys::ERROR_FILE_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
// Create any other .app folders which may not exist yet
|
// Create any other .app folders which may not exist yet
|
||||||
std::string app_folder;
|
std::string app_folder;
|
||||||
Common::SplitPath(GetTitleContentPath(media_type, tmd.GetTitleID(),
|
auto main_content_path = GetTitleContentPath(media_type, tmd.GetTitleID(),
|
||||||
FileSys::TMDContentIndex::Main, is_update),
|
FileSys::TMDContentIndex::Main, is_update);
|
||||||
&app_folder, nullptr, nullptr);
|
Common::SplitPath(main_content_path, &app_folder, nullptr, nullptr);
|
||||||
FileUtil::CreateFullPath(app_folder);
|
FileUtil::CreateFullPath(app_folder);
|
||||||
|
|
||||||
auto content_count = container.GetTitleMetadata().GetContentCount();
|
auto content_count = container.GetTitleMetadata().GetContentCount();
|
||||||
content_written.resize(content_count);
|
content_written.resize(content_count);
|
||||||
|
|
||||||
if (auto title_key = container.GetTicket().GetTitleKey()) {
|
content_files.clear();
|
||||||
decryption_state->content.resize(content_count);
|
for (std::size_t i = 0; i < content_count; i++) {
|
||||||
for (std::size_t i = 0; i < content_count; ++i) {
|
auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update);
|
||||||
auto ctr = tmd.GetContentCTRByIndex(i);
|
auto& file = content_files.emplace_back(path, "wb");
|
||||||
decryption_state->content[i].SetKeyWithIV(title_key->data(), title_key->size(),
|
if (!file.IsOpen()) {
|
||||||
ctr.data());
|
LOG_ERROR(Service_AM, "Could not open output file '{}' for content {}.", path, i);
|
||||||
|
// TODO: Correct error code.
|
||||||
|
return FileSys::ERROR_FILE_NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container.GetTitleMetadata().HasEncryptedContent()) {
|
||||||
|
if (auto title_key = container.GetTicket().GetTitleKey()) {
|
||||||
|
decryption_state->content.resize(content_count);
|
||||||
|
for (std::size_t i = 0; i < content_count; ++i) {
|
||||||
|
auto ctr = tmd.GetContentCTRByIndex(i);
|
||||||
|
decryption_state->content[i].SetKeyWithIV(title_key->data(), title_key->size(),
|
||||||
|
ctr.data());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Service_AM, "Could not read title key from ticket for encrypted CIA.");
|
||||||
|
// TODO: Correct error code.
|
||||||
|
return FileSys::ERROR_FILE_NOT_FOUND;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR(Service_AM, "Can't get title key from ticket");
|
LOG_INFO(Service_AM,
|
||||||
|
"Title has no encrypted content, skipping initializing decryption state.");
|
||||||
}
|
}
|
||||||
|
|
||||||
install_state = CIAInstallState::TMDLoaded;
|
install_state = CIAInstallState::TMDLoaded;
|
||||||
|
@ -155,7 +192,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
|
||||||
// has been written since we might get a written buffer which contains multiple .app
|
// has been written since we might get a written buffer which contains multiple .app
|
||||||
// contents or only part of a larger .app's contents.
|
// contents or only part of a larger .app's contents.
|
||||||
const u64 offset_max = offset + length;
|
const u64 offset_max = offset + length;
|
||||||
for (std::size_t i = 0; i < container.GetTitleMetadata().GetContentCount(); i++) {
|
for (std::size_t i = 0; i < content_written.size(); i++) {
|
||||||
if (content_written[i] < container.GetContentSize(i)) {
|
if (content_written[i] < container.GetContentSize(i)) {
|
||||||
// The size, minimum unwritten offset, and maximum unwritten offset of this content
|
// The size, minimum unwritten offset, and maximum unwritten offset of this content
|
||||||
const u64 size = container.GetContentSize(i);
|
const u64 size = container.GetContentSize(i);
|
||||||
|
@ -174,22 +211,12 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
|
||||||
// Since the incoming TMD has already been written, we can use GetTitleContentPath
|
// Since the incoming TMD has already been written, we can use GetTitleContentPath
|
||||||
// to get the content paths to write to.
|
// to get the content paths to write to.
|
||||||
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
|
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
|
||||||
FileUtil::IOFile file(GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update),
|
auto& file = content_files[i];
|
||||||
content_written[i] ? "ab" : "wb");
|
|
||||||
|
|
||||||
if (!file.IsOpen()) {
|
|
||||||
return FileSys::ERROR_INSUFFICIENT_SPACE;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<u8> temp(buffer + (range_min - offset),
|
std::vector<u8> temp(buffer + (range_min - offset),
|
||||||
buffer + (range_min - offset) + available_to_write);
|
buffer + (range_min - offset) + available_to_write);
|
||||||
|
|
||||||
if ((tmd.GetContentTypeByIndex(i) & FileSys::TMDContentTypeFlag::Encrypted) != 0) {
|
if ((tmd.GetContentTypeByIndex(i) & FileSys::TMDContentTypeFlag::Encrypted) != 0) {
|
||||||
if (decryption_state->content.size() <= i) {
|
|
||||||
// TODO: There is probably no correct error to return here. What error should be
|
|
||||||
// returned?
|
|
||||||
return FileSys::ERROR_INSUFFICIENT_SPACE;
|
|
||||||
}
|
|
||||||
decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size());
|
decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,8 +261,9 @@ ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have a header yet, we can't pull offsets of other sections
|
// If we don't have a header yet, we can't pull offsets of other sections
|
||||||
if (install_state == CIAInstallState::InstallStarted)
|
if (install_state == CIAInstallState::InstallStarted) {
|
||||||
return length;
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
// If we have been given data before (or including) .app content, pull it into
|
// If we have been given data before (or including) .app content, pull it into
|
||||||
// our buffer, but only pull *up to* the content offset, no further.
|
// our buffer, but only pull *up to* the content offset, no further.
|
||||||
|
@ -251,28 +279,30 @@ ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush
|
||||||
std::memcpy(data.data() + copy_offset, buffer + buf_offset, buf_copy_size);
|
std::memcpy(data.data() + copy_offset, buffer + buf_offset, buf_copy_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(shinyquagsire23): Write out .tik files to nand?
|
|
||||||
|
|
||||||
// The end of our TMD is at the beginning of Content data, so ensure we have that much
|
// The end of our TMD is at the beginning of Content data, so ensure we have that much
|
||||||
// buffered before trying to parse.
|
// buffered before trying to parse.
|
||||||
if (written >= container.GetContentOffset() && install_state != CIAInstallState::TMDLoaded) {
|
if (written >= container.GetContentOffset() && install_state != CIAInstallState::TMDLoaded) {
|
||||||
auto result = WriteTicket();
|
auto result = WriteTicket();
|
||||||
if (result.IsError())
|
if (result.IsError()) {
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
result = WriteTitleMetadata();
|
result = WriteTitleMetadata();
|
||||||
if (result.IsError())
|
if (result.IsError()) {
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content data sizes can only be retrieved from TMD data
|
// Content data sizes can only be retrieved from TMD data
|
||||||
if (install_state != CIAInstallState::TMDLoaded)
|
if (install_state != CIAInstallState::TMDLoaded) {
|
||||||
return length;
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
// From this point forward, data will no longer be buffered in data
|
// From this point forward, data will no longer be buffered in data
|
||||||
auto result = WriteContentData(offset, length, buffer);
|
auto result = WriteContentData(offset, length, buffer);
|
||||||
if (result.Failed())
|
if (result.Failed()) {
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
@ -286,11 +316,13 @@ bool CIAFile::SetSize(u64 size) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CIAFile::Close() const {
|
bool CIAFile::Close() const {
|
||||||
bool complete = true;
|
bool complete =
|
||||||
for (std::size_t i = 0; i < container.GetTitleMetadata().GetContentCount(); i++) {
|
install_state >= CIAInstallState::TMDLoaded &&
|
||||||
if (content_written[i] < container.GetContentSize(static_cast<u16>(i)))
|
content_written.size() == container.GetTitleMetadata().GetContentCount() &&
|
||||||
complete = false;
|
std::all_of(content_written.begin(), content_written.end(),
|
||||||
}
|
[this, i = 0](auto& bytes_written) mutable {
|
||||||
|
return bytes_written >= container.GetContentSize(static_cast<u16>(i++));
|
||||||
|
});
|
||||||
|
|
||||||
// Install aborted
|
// Install aborted
|
||||||
if (!complete) {
|
if (!complete) {
|
||||||
|
@ -314,16 +346,17 @@ bool CIAFile::Close() const {
|
||||||
// For each content ID in the old TMD, check if there is a matching ID in the new
|
// For each content ID in the old TMD, check if there is a matching ID in the new
|
||||||
// TMD. If a CIA contains (and wrote to) an identical ID, it should be kept while
|
// TMD. If a CIA contains (and wrote to) an identical ID, it should be kept while
|
||||||
// IDs which only existed for the old TMD should be deleted.
|
// IDs which only existed for the old TMD should be deleted.
|
||||||
for (u16 old_index = 0; old_index < old_tmd.GetContentCount(); old_index++) {
|
for (std::size_t old_index = 0; old_index < old_tmd.GetContentCount(); old_index++) {
|
||||||
bool abort = false;
|
bool abort = false;
|
||||||
for (u16 new_index = 0; new_index < new_tmd.GetContentCount(); new_index++) {
|
for (std::size_t new_index = 0; new_index < new_tmd.GetContentCount(); new_index++) {
|
||||||
if (old_tmd.GetContentIDByIndex(old_index) ==
|
if (old_tmd.GetContentIDByIndex(old_index) ==
|
||||||
new_tmd.GetContentIDByIndex(new_index)) {
|
new_tmd.GetContentIDByIndex(new_index)) {
|
||||||
abort = true;
|
abort = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (abort)
|
if (abort) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// If the file to delete is the current launched rom, signal the system to delete
|
// If the file to delete is the current launched rom, signal the system to delete
|
||||||
// the current rom instead of deleting it now, once all the handles to the file
|
// the current rom instead of deleting it now, once all the handles to the file
|
||||||
|
@ -331,8 +364,9 @@ bool CIAFile::Close() const {
|
||||||
std::string to_delete =
|
std::string to_delete =
|
||||||
GetTitleContentPath(media_type, old_tmd.GetTitleID(), old_index);
|
GetTitleContentPath(media_type, old_tmd.GetTitleID(), old_index);
|
||||||
if (!(Core::System::GetInstance().IsPoweredOn() &&
|
if (!(Core::System::GetInstance().IsPoweredOn() &&
|
||||||
Core::System::GetInstance().SetSelfDelete(to_delete)))
|
Core::System::GetInstance().SetSelfDelete(to_delete))) {
|
||||||
FileUtil::Delete(to_delete);
|
FileUtil::Delete(to_delete);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileUtil::Delete(old_tmd_path);
|
FileUtil::Delete(old_tmd_path);
|
||||||
|
@ -357,29 +391,29 @@ InstallStatus InstallCIA(const std::string& path,
|
||||||
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
|
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
|
||||||
|
|
||||||
bool title_key_available = container.GetTicket().GetTitleKey().has_value();
|
bool title_key_available = container.GetTicket().GetTitleKey().has_value();
|
||||||
|
if (!title_key_available && container.GetTitleMetadata().HasEncryptedContent()) {
|
||||||
for (std::size_t i = 0; i < container.GetTitleMetadata().GetContentCount(); i++) {
|
LOG_ERROR(Service_AM, "File {} is encrypted and no title key is available! Aborting...",
|
||||||
if ((container.GetTitleMetadata().GetContentTypeByIndex(static_cast<u16>(i)) &
|
path);
|
||||||
FileSys::TMDContentTypeFlag::Encrypted) &&
|
return InstallStatus::ErrorEncrypted;
|
||||||
!title_key_available) {
|
|
||||||
LOG_ERROR(Service_AM, "File {} is encrypted! Aborting...", path);
|
|
||||||
return InstallStatus::ErrorEncrypted;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileUtil::IOFile file(path, "rb");
|
FileUtil::IOFile file(path, "rb");
|
||||||
if (!file.IsOpen())
|
if (!file.IsOpen()) {
|
||||||
|
LOG_ERROR(Service_AM, "Could not open CIA file '{}'.", path);
|
||||||
return InstallStatus::ErrorFailedToOpenFile;
|
return InstallStatus::ErrorFailedToOpenFile;
|
||||||
|
}
|
||||||
|
|
||||||
std::array<u8, 0x10000> buffer;
|
std::array<u8, 0x10000> buffer;
|
||||||
|
auto file_size = file.GetSize();
|
||||||
std::size_t total_bytes_read = 0;
|
std::size_t total_bytes_read = 0;
|
||||||
while (total_bytes_read != file.GetSize()) {
|
while (total_bytes_read != file_size) {
|
||||||
std::size_t bytes_read = file.ReadBytes(buffer.data(), buffer.size());
|
std::size_t bytes_read = file.ReadBytes(buffer.data(), buffer.size());
|
||||||
auto result = installFile.Write(static_cast<u64>(total_bytes_read), bytes_read, true,
|
auto result = installFile.Write(static_cast<u64>(total_bytes_read), bytes_read, true,
|
||||||
static_cast<u8*>(buffer.data()));
|
static_cast<u8*>(buffer.data()));
|
||||||
|
|
||||||
if (update_callback)
|
if (update_callback) {
|
||||||
update_callback(total_bytes_read, file.GetSize());
|
update_callback(total_bytes_read, file_size);
|
||||||
|
}
|
||||||
if (result.Failed()) {
|
if (result.Failed()) {
|
||||||
LOG_ERROR(Service_AM, "CIA file installation aborted with error code {:08x}",
|
LOG_ERROR(Service_AM, "CIA file installation aborted with error code {:08x}",
|
||||||
result.Code().raw);
|
result.Code().raw);
|
||||||
|
|
|
@ -26,6 +26,10 @@ namespace Core {
|
||||||
class System;
|
class System;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace FileUtil {
|
||||||
|
class IOFile;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Service::FS {
|
namespace Service::FS {
|
||||||
enum class MediaType : u32;
|
enum class MediaType : u32;
|
||||||
}
|
}
|
||||||
|
@ -96,6 +100,7 @@ private:
|
||||||
FileSys::CIAContainer container;
|
FileSys::CIAContainer container;
|
||||||
std::vector<u8> data;
|
std::vector<u8> data;
|
||||||
std::vector<u64> content_written;
|
std::vector<u64> content_written;
|
||||||
|
std::vector<FileUtil::IOFile> content_files;
|
||||||
Service::FS::MediaType media_type;
|
Service::FS::MediaType media_type;
|
||||||
|
|
||||||
class DecryptionState;
|
class DecryptionState;
|
||||||
|
|
Loading…
Reference in a new issue