From c16559f20e31996d280dad3cc8e0926ffcb35410 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 2 Jul 2023 16:49:14 +0000 Subject: [PATCH] add stream based loaders * extend stream loading support * fix content manager rebase * fix update searching --- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 508 ++++++++++-------- .../Processes/Extensions/NcaExtensions.cs | 46 +- .../PartitionFileSystemExtensions.cs | 6 +- .../Loaders/Processes/ProcessLoader.cs | 132 ++++- src/Ryujinx.HLE/Switch.cs | 21 + .../Utilities/PartitionFileSystemUtils.cs | 11 +- 6 files changed, 462 insertions(+), 262 deletions(-) diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index d63234509..7126b173e 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -42,12 +42,14 @@ namespace Ryujinx.HLE.FileSystem private readonly struct AocItem { - public readonly string ContainerPath; + public readonly Stream ContainerStream; public readonly string NcaPath; + public readonly string Extension; - public AocItem(string containerPath, string ncaPath) + public AocItem(Stream containerStream, string ncaPath, string extension) { - ContainerPath = containerPath; + ContainerStream = containerStream; + Extension = extension; NcaPath = ncaPath; } } @@ -187,10 +189,10 @@ namespace Ryujinx.HLE.FileSystem } } - public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool mergedToContainer = false) + public void AddAocItem(ulong titleId, Stream containerStream, string ncaPath, string extension, bool mergedToContainer = false) { // TODO: Check Aoc version. - if (!AocData.TryAdd(titleId, new AocItem(containerPath, ncaPath))) + if (!AocData.TryAdd(titleId, new AocItem(containerStream, ncaPath, extension))) { Logger.Warning?.Print(LogClass.Application, $"Duplicate AddOnContent detected. TitleId {titleId:X16}"); } @@ -200,12 +202,20 @@ namespace Ryujinx.HLE.FileSystem if (!mergedToContainer) { - using var pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(containerPath, _virtualFileSystem); + using var pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(containerStream, extension == ".xci", _virtualFileSystem); } } } - public void ClearAocData() => AocData.Clear(); + public void ClearAocData() + { + foreach (var aoc in AocData) + { + aoc.Value.ContainerStream?.Dispose(); + } + + AocData.Clear(); + } public int GetAocCount() => AocData.Count; @@ -217,18 +227,17 @@ namespace Ryujinx.HLE.FileSystem if (AocData.TryGetValue(aocTitleId, out AocItem aoc)) { - var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read); using var ncaFile = new UniqueRef(); - switch (Path.GetExtension(aoc.ContainerPath)) + switch (aoc.Extension) { case ".xci": - var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + var xci = new Xci(_virtualFileSystem.KeySet, aoc.ContainerStream.AsStorage()).OpenPartition(XciPartitionType.Secure); + xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); break; case ".nsp": var pfs = new PartitionFileSystem(); - pfs.Initialize(file.AsStorage()); + pfs.Initialize(aoc.ContainerStream.AsStorage()); pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); break; default: @@ -476,6 +485,27 @@ namespace Ryujinx.HLE.FileSystem FinishInstallation(temporaryDirectory, registeredDirectory); } + public void InstallFirmware(Stream stream, bool isXci) + { + ContentPath.TryGetContentPath(StorageId.BuiltInSystem, out var contentPathString); + ContentPath.TryGetRealPath(contentPathString, out var contentDirectory); + 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); + } + public void InstallKeys(string keysSource, string installDirectory) { if (Directory.Exists(keysSource)) @@ -662,13 +692,16 @@ namespace Ryujinx.HLE.FileSystem throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers."); } - Dictionary> 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."); @@ -676,247 +709,98 @@ 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 if (updateNcas.TryAdd(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.FirstOrDefault(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(); - - 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.FirstOrDefault(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(); - - 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.FirstOrDefault(x => x.type == NcaContentType.Meta).path; - - string contentPath = ncaEntry.FirstOrDefault(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(); - - 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 content = new(contentData); - - contentStorage.Read(0, content); - - Span 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> 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(); - - 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(); - - 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 if (updateNcas.TryAdd(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.FirstOrDefault(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(); + + 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) @@ -924,11 +808,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.FirstOrDefault(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(); + + 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.FirstOrDefault(x => x.type == NcaContentType.Meta).path; + metaPath = ncaEntry.FirstOrDefault(x => x.type == NcaContentType.Meta).path; + string contentPath = ncaEntry.FirstOrDefault(x => x.type != NcaContentType.Meta).path; // Nintendo in 9.0.0, removed PPC and only kept the meta nca of it. @@ -940,10 +842,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); @@ -955,6 +859,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]; @@ -990,11 +895,146 @@ 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> 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(); + + 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(); + + 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 if (updateNcas.TryAdd(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.FirstOrDefault(x => x.type == NcaContentType.Meta).path; + string contentPath = ncaEntry.FirstOrDefault(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(); + + 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 content = new(contentData); + + contentStorage.Read(0, content); + + Span 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() diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index 2928ac7fe..0d69897c1 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -127,7 +127,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Control; } - public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath) + public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath, Stream updateStream = null) { updatePath = null; @@ -138,28 +138,40 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions // Clear the program index part. ulong titleIdBase = mainNca.GetProgramIdBase(); - // Load update information if exists. - string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); - if (File.Exists(titleUpdateMetadataPath)) + IFileSystem updatePartitionFileSystem = null; + + if (updateStream == null) { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _applicationSerializerContext.TitleUpdateMetadata).Selected; - if (File.Exists(updatePath)) + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + if (File.Exists(titleUpdateMetadataPath)) { - IFileSystem updatePartitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(updatePath, fileSystem); - - foreach ((ulong applicationTitleId, ContentMetaData content) in updatePartitionFileSystem.GetContentData(ContentMetaType.Patch, fileSystem, checkLevel)) + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _applicationSerializerContext.TitleUpdateMetadata).Selected; + if (File.Exists(updatePath)) { - if ((applicationTitleId & ~0x1FFFUL) != titleIdBase) - { - continue; - } - - updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex); - updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex); - break; + updatePartitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(updatePath, fileSystem); } } } + else + { + updatePartitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(updateStream, false, fileSystem); + } + + if (updatePartitionFileSystem != null) + { + foreach ((ulong applicationTitleId, ContentMetaData content) in updatePartitionFileSystem.GetContentData(ContentMetaType.Patch, fileSystem, checkLevel)) + { + if ((applicationTitleId & ~0x1FFFUL) != titleIdBase) + { + continue; + } + + updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex); + updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex); + break; + } + } return (updatePatchNca, updateControlNca); } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index b3590d9bd..e8feb719e 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -52,7 +52,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return programs; } - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, ulong applicationId, out string errorMessage) + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, Stream stream, ulong applicationId, out string errorMessage, string extension, Stream updateStream = null) where TMetaData : PartitionFileSystemMetaCore, new() where TFormat : IPartitionFileSystemFormat where THeader : unmanaged, IPartitionFileSystemHeader @@ -102,7 +102,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (false, ProcessResult.Failed); } - (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _); + (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _, updateStream); if (updatePatchNca != null) { @@ -131,7 +131,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions { if (downloadableContentNca.Enabled) { - device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath); + device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, stream, downloadableContentNca.FullPath, System.IO.Path.GetExtension(downloadableContentContainer.ContainerPath)); } } else diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 002926fc9..ee67a95ee 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -35,6 +35,12 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadXci(string path, ulong applicationId) { FileStream stream = new(path, FileMode.Open, FileAccess.Read); + + return LoadXci(stream, applicationId); + } + + public bool LoadXci(Stream stream, ulong applicationId, Stream updateStream = null) + { Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage()); if (!xci.HasPartition(XciPartitionType.Secure)) @@ -44,7 +50,7 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, applicationId, out string errorMessage); + (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, stream, applicationId, out string errorMessage, "xci", updateStream); if (!success) { @@ -69,10 +75,16 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadNsp(string path, ulong applicationId) { FileStream file = new(path, FileMode.Open, FileAccess.Read); - PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); - (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, applicationId, out string errorMessage); + return LoadNsp(file, applicationId); + } + + public bool LoadNsp(Stream stream, ulong applicationId, Stream updateStream = null) + { + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(stream.AsStorage()).ThrowIfFailure(); + + (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, stream, applicationId, out string errorMessage, "nsp", updateStream); if (processResult.ProcessId == 0) { @@ -101,7 +113,13 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadNca(string path) { FileStream file = new(path, FileMode.Open, FileAccess.Read); - Nca nca = new(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false)); + + return LoadNca(file); + } + + public bool LoadNca(Stream ncaStream) + { + Nca nca = new(_device.Configuration.VirtualFileSystem.KeySet, ncaStream.AsStorage(false)); ProcessResult processResult = nca.Load(_device, null, null); @@ -249,5 +267,109 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } + + public bool LoadNxo(Stream stream, bool isNro, string name) + { + var nacpData = new BlitStruct(1); + IFileSystem dummyExeFs = null; + Stream romfsStream = null; + + string programName = ""; + ulong programId = 0000000000000000; + + // Load executable. + IExecutable executable; + + if (isNro) + { + NroExecutable nro = new(stream.AsStorage()); + + executable = nro; + + // Open RomFS if exists. + IStorage romFsStorage = nro.OpenNroAssetSection(LibHac.Tools.Ro.NroAssetType.RomFs, false); + romFsStorage.GetSize(out long romFsSize).ThrowIfFailure(); + if (romFsSize != 0) + { + romfsStream = romFsStorage.AsStream(); + } + + // Load Nacp if exists. + IStorage nacpStorage = nro.OpenNroAssetSection(LibHac.Tools.Ro.NroAssetType.Nacp, false); + nacpStorage.GetSize(out long nacpSize).ThrowIfFailure(); + if (nacpSize != 0) + { + nacpStorage.Read(0, nacpData.ByteSpan); + + programName = nacpData.Value.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString(); + + if (string.IsNullOrWhiteSpace(programName)) + { + programName = Array.Find(nacpData.Value.Title.AsReadOnlySpan().ToArray(), x => x.Name[0] != 0).NameString.ToString(); + } + + if (nacpData.Value.PresenceGroupId != 0) + { + programId = nacpData.Value.PresenceGroupId; + } + else if (nacpData.Value.SaveDataOwnerId != 0) + { + programId = nacpData.Value.SaveDataOwnerId; + } + else if (nacpData.Value.AddOnContentBaseId != 0) + { + programId = nacpData.Value.AddOnContentBaseId - 0x1000; + } + } + + // TODO: Add icon maybe ? + } + else + { + executable = new NsoExecutable(new LocalStorage(name, FileAccess.Read), programName); + } + + // Explicitly null TitleId to disable the shader cache. + Graphics.Gpu.GraphicsConfig.TitleId = null; + _device.Gpu.HostInitalized.Set(); + + ProcessResult processResult = ProcessLoaderHelper.LoadNsos(_device, + _device.System.KernelContext, + dummyExeFs.GetNpdm(), + nacpData, + diskCacheEnabled: false, + diskCacheSelector: null, + allowCodeMemoryForJit: true, + programName, + programId, + 0, + null, + executable); + + // Make sure the process id is valid. + if (processResult.ProcessId != 0) + { + // Load RomFS. + if (romfsStream != null) + { + _device.Configuration.VirtualFileSystem.SetRomFs(processResult.ProcessId, romfsStream); + } + + // Start process. + if (_processesByPid.TryAdd(processResult.ProcessId, processResult)) + { + if (processResult.Start(_device)) + { + _latestPid = processResult.ProcessId; + + return true; + } + } + } + + return false; + } + + } } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 7b694bb80..22dd2b034 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -10,6 +10,7 @@ using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.UI; using Ryujinx.Memory; using System; +using System.IO; namespace Ryujinx.HLE { @@ -101,6 +102,26 @@ namespace Ryujinx.HLE return Processes.LoadNxo(fileName); } + public bool LoadXci(Stream xciStream, ulong applicationId = 0, Stream updateStream = null) + { + return Processes.LoadXci(xciStream, applicationId, updateStream); + } + + public bool LoadNca(Stream ncaStream) + { + return Processes.LoadNca(ncaStream); + } + + public bool LoadNsp(Stream nspStream, ulong applicationId = 0, Stream updateStream = null) + { + return Processes.LoadNsp(nspStream, applicationId, updateStream); + } + + public bool LoadProgram(Stream stream, bool isNro, string name) + { + return Processes.LoadNxo(stream, isNro, name); + } + public bool WaitFifo() { return Gpu.GPFifo.WaitForCommands(); diff --git a/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs index 3c4ce0850..e2a214da1 100644 --- a/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs +++ b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs @@ -14,16 +14,21 @@ namespace Ryujinx.HLE.Utilities { FileStream file = File.OpenRead(path); + return OpenApplicationFileSystem(file, Path.GetExtension(path).ToLower() == ".xci", fileSystem, throwOnFailure); + } + + public static IFileSystem OpenApplicationFileSystem(Stream stream, bool isXci, VirtualFileSystem fileSystem, bool throwOnFailure = true) + { IFileSystem partitionFileSystem; - if (Path.GetExtension(path).ToLower() == ".xci") + if (isXci) { - partitionFileSystem = new Xci(fileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + partitionFileSystem = new Xci(fileSystem.KeySet, stream.AsStorage()).OpenPartition(XciPartitionType.Secure); } else { var pfsTemp = new PartitionFileSystem(); - Result initResult = pfsTemp.Initialize(file.AsStorage()); + Result initResult = pfsTemp.Initialize(stream.AsStorage()); if (throwOnFailure) {