android - implement firmware installation

This commit is contained in:
Emmanuel Hansen 2023-12-17 19:09:52 +00:00
parent 60f320bc07
commit aa87b4abd8
9 changed files with 669 additions and 283 deletions

View file

@ -525,6 +525,27 @@ namespace Ryujinx.HLE.FileSystem
FinishInstallation(temporaryDirectory, registeredDirectory);
}
public void InstallFirmware(Stream stream, bool isXci)
{
string contentPathString = ContentPath.GetContentPath(StorageId.BuiltInSystem);
string contentDirectory = ContentPath.GetRealPath(contentPathString);
string registeredDirectory = Path.Combine(contentDirectory, "registered");
string temporaryDirectory = Path.Combine(contentDirectory, "temp");
if (!isXci)
{
using ZipArchive archive = new ZipArchive(stream);
InstallFromZip(archive, temporaryDirectory);
}
else
{
Xci xci = new(_virtualFileSystem.KeySet, stream.AsStorage());
InstallFromCart(xci, temporaryDirectory);
}
FinishInstallation(temporaryDirectory, registeredDirectory);
}
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
{
if (Directory.Exists(registeredDirectory))
@ -643,13 +664,16 @@ namespace Ryujinx.HLE.FileSystem
throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
}
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
if (Directory.Exists(firmwarePackage))
{
return VerifyAndGetVersionDirectory(firmwarePackage);
}
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
{
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
}
if (!File.Exists(firmwarePackage))
{
throw new FileNotFoundException("Firmware file does not exist.");
@ -657,249 +681,99 @@ namespace Ryujinx.HLE.FileSystem
FileInfo info = new(firmwarePackage);
using FileStream file = File.OpenRead(firmwarePackage);
switch (info.Extension)
if (info.Extension == ".zip" || info.Extension == ".xci")
{
case ".zip":
using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage))
{
return VerifyAndGetVersionZip(archive);
}
case ".xci":
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
using FileStream file = File.OpenRead(firmwarePackage);
if (xci.HasPartition(XciPartitionType.Update))
{
XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
var isXci = info.Extension == ".xci";
return VerifyAndGetVersion(partition);
}
else
{
throw new InvalidFirmwarePackageException("Update not found in xci file.");
}
default:
break;
return VerifyFirmwarePackage(file, isXci);
}
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
return null;
}
public SystemVersion VerifyFirmwarePackage(Stream file, bool isXci)
{
if (!isXci)
{
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
using ZipArchive archive = new ZipArchive(file, ZipArchiveMode.Read);
return VerifyAndGetVersionZip(archive);
}
SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
else
{
SystemVersion systemVersion = null;
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
foreach (var entry in archive.Entries)
if (xci.HasPartition(XciPartitionType.Update))
{
if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
{
using Stream ncaStream = GetZipStream(entry);
IStorage storage = ncaStream.AsStorage();
XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
Nca nca = new(_virtualFileSystem.KeySet, storage);
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
}
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
}
}
}
if (updateNcas.TryGetValue(SystemUpdateTitleId, out var ncaEntry))
{
string metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
CnmtContentMetaEntry[] metaEntries = null;
var fileEntry = archive.GetEntry(metaPath);
using (Stream ncaStream = GetZipStream(fileEntry))
{
Nca metaNca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
updateNcas.Remove(SystemUpdateTitleId);
}
}
}
if (metaEntries == null)
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
if (updateNcas.TryGetValue(SystemVersionTitleId, out var updateNcasItem))
{
string versionEntry = updateNcasItem.Find(x => x.type != NcaContentType.Meta).path;
using Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry));
Nca nca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
{
if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry))
{
metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
// This is a perfect valid case, so we should just ignore the missing content nca and continue.
if (contentPath == null)
{
updateNcas.Remove(metaEntry.TitleId);
continue;
}
ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath);
ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath);
using Stream metaNcaStream = GetZipStream(metaZipEntry);
using Stream contentNcaStream = GetZipStream(contentZipEntry);
Nca metaNca = new(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
IStorage contentStorage = contentNcaStream.AsStorage();
if (contentStorage.GetSize(out long size).IsSuccess())
{
byte[] contentData = new byte[size];
Span<byte> content = new(contentData);
contentStorage.Read(0, content);
Span<byte> hash = new(new byte[32]);
LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
{
updateNcas.Remove(metaEntry.TitleId);
}
}
}
}
}
if (updateNcas.Count > 0)
{
StringBuilder extraNcas = new();
foreach (var entry in updateNcas)
{
foreach (var (type, path) in entry.Value)
{
extraNcas.AppendLine(path);
}
}
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
}
return VerifyAndGetVersion(partition);
}
else
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
throw new InvalidFirmwarePackageException("Update not found in xci file.");
}
return systemVersion;
}
}
SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
private SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
{
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
SystemVersion systemVersion = null;
foreach (var entry in archive.Entries)
{
SystemVersion systemVersion = null;
CnmtContentMetaEntry[] metaEntries = null;
foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
{
IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
using Stream ncaStream = GetZipStream(entry);
IStorage storage = ncaStream.AsStorage();
Nca nca = new(_virtualFileSystem.KeySet, ncaStorage);
if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
{
IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
}
}
continue;
}
else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
{
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
Nca nca = new(_virtualFileSystem.KeySet, storage);
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
}
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
}
}
}
ncaStorage.Dispose();
if (updateNcas.TryGetValue(SystemUpdateTitleId, out var ncaEntry))
{
string metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
CnmtContentMetaEntry[] metaEntries = null;
var fileEntry = archive.GetEntry(metaPath);
using (Stream ncaStream = GetZipStream(fileEntry))
{
Nca metaNca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
updateNcas.Remove(SystemUpdateTitleId);
}
}
}
if (metaEntries == null)
@ -907,11 +781,29 @@ namespace Ryujinx.HLE.FileSystem
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
if (updateNcas.TryGetValue(SystemVersionTitleId, out var updateNcasItem))
{
string versionEntry = updateNcasItem.Find(x => x.type != NcaContentType.Meta).path;
using Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry));
Nca nca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
{
if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry))
if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry))
{
string metaNcaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
@ -923,10 +815,12 @@ namespace Ryujinx.HLE.FileSystem
continue;
}
IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage();
IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath);
ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath);
Nca metaNca = new(_virtualFileSystem.KeySet, metaStorage);
using Stream metaNcaStream = GetZipStream(metaZipEntry);
using Stream contentNcaStream = GetZipStream(contentZipEntry);
Nca metaNca = new(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
@ -938,6 +832,7 @@ namespace Ryujinx.HLE.FileSystem
{
var meta = new Cnmt(metaFile.Get.AsStream());
IStorage contentStorage = contentNcaStream.AsStorage();
if (contentStorage.GetSize(out long size).IsSuccess())
{
byte[] contentData = new byte[size];
@ -973,11 +868,147 @@ namespace Ryujinx.HLE.FileSystem
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
}
return systemVersion;
}
else
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
return null;
return systemVersion;
}
private SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
{
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
SystemVersion systemVersion = null;
CnmtContentMetaEntry[] metaEntries = null;
foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
{
IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
Nca nca = new(_virtualFileSystem.KeySet, ncaStorage);
if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
{
IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (meta.Type == ContentMetaType.SystemUpdate)
{
metaEntries = meta.MetaEntries;
}
}
continue;
}
else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
{
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
using var systemVersionFile = new UniqueRef<IFile>();
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
{
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
}
}
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
}
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
}
ncaStorage.Dispose();
}
if (metaEntries == null)
{
throw new FileNotFoundException("System update title was not found in the firmware package.");
}
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
{
if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry))
{
string metaNcaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
// This is a perfect valid case, so we should just ignore the missing content nca and continue.
if (contentPath == null)
{
updateNcas.Remove(metaEntry.TitleId);
continue;
}
IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage();
IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
Nca metaNca = new(_virtualFileSystem.KeySet, metaStorage);
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
using var metaFile = new UniqueRef<IFile>();
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
{
var meta = new Cnmt(metaFile.Get.AsStream());
if (contentStorage.GetSize(out long size).IsSuccess())
{
byte[] contentData = new byte[size];
Span<byte> content = new(contentData);
contentStorage.Read(0, content);
Span<byte> hash = new(new byte[32]);
LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
{
updateNcas.Remove(metaEntry.TitleId);
}
}
}
}
}
if (updateNcas.Count > 0)
{
StringBuilder extraNcas = new();
foreach (var entry in updateNcas)
{
foreach (var (type, path) in entry.Value)
{
extraNcas.AppendLine(path);
}
}
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
}
return systemVersion;
}
public SystemVersion GetCurrentFirmwareVersion()