From e3b36db71c62a34a26b30683dd5ad5410c97cc9c Mon Sep 17 00:00:00 2001 From: Ac_K Date: Tue, 22 Mar 2022 20:46:16 +0100 Subject: hle: Some cleanup (#3210) * hle: Some cleanup This PR cleaned up a bit the HLE folder and the VirtualFileSystem one, since we use LibHac, we can use some class of it directly instead of duplicate things. The "Content" of VFS folder is removed since it should be handled in the NCM service directly. A larger cleanup should be done later since there is still be duplicated code here and there. * Fix Headless.SDL2 * Addresses gdkchan feedback --- Ryujinx.HLE/FileSystem/Content/ContentManager.cs | 1054 -------------------- Ryujinx.HLE/FileSystem/Content/ContentPath.cs | 19 - Ryujinx.HLE/FileSystem/Content/LocationEntry.cs | 25 - Ryujinx.HLE/FileSystem/Content/LocationHelper.cs | 91 -- Ryujinx.HLE/FileSystem/Content/StorageId.cs | 9 - Ryujinx.HLE/FileSystem/Content/SystemVersion.cs | 41 - Ryujinx.HLE/FileSystem/Content/TitleType.cs | 15 - Ryujinx.HLE/FileSystem/ContentManager.cs | 1053 +++++++++++++++++++ Ryujinx.HLE/FileSystem/ContentPath.cs | 82 ++ .../FileSystem/EncryptedFileSystemCreator.cs | 5 +- Ryujinx.HLE/FileSystem/LocationEntry.cs | 25 + Ryujinx.HLE/FileSystem/SaveDataType.cs | 12 - Ryujinx.HLE/FileSystem/SaveInfo.cs | 27 - Ryujinx.HLE/FileSystem/SaveSpaceId.cs | 10 - Ryujinx.HLE/FileSystem/StorageId.cs | 12 - Ryujinx.HLE/FileSystem/SystemVersion.cs | 40 + Ryujinx.HLE/FileSystem/VirtualFileSystem.cs | 88 +- 17 files changed, 1233 insertions(+), 1375 deletions(-) delete mode 100644 Ryujinx.HLE/FileSystem/Content/ContentManager.cs delete mode 100644 Ryujinx.HLE/FileSystem/Content/ContentPath.cs delete mode 100644 Ryujinx.HLE/FileSystem/Content/LocationEntry.cs delete mode 100644 Ryujinx.HLE/FileSystem/Content/LocationHelper.cs delete mode 100644 Ryujinx.HLE/FileSystem/Content/StorageId.cs delete mode 100644 Ryujinx.HLE/FileSystem/Content/SystemVersion.cs delete mode 100644 Ryujinx.HLE/FileSystem/Content/TitleType.cs create mode 100644 Ryujinx.HLE/FileSystem/ContentManager.cs create mode 100644 Ryujinx.HLE/FileSystem/ContentPath.cs create mode 100644 Ryujinx.HLE/FileSystem/LocationEntry.cs delete mode 100644 Ryujinx.HLE/FileSystem/SaveDataType.cs delete mode 100644 Ryujinx.HLE/FileSystem/SaveInfo.cs delete mode 100644 Ryujinx.HLE/FileSystem/SaveSpaceId.cs delete mode 100644 Ryujinx.HLE/FileSystem/StorageId.cs create mode 100644 Ryujinx.HLE/FileSystem/SystemVersion.cs (limited to 'Ryujinx.HLE/FileSystem') diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs deleted file mode 100644 index 8ac31a4c..00000000 --- a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs +++ /dev/null @@ -1,1054 +0,0 @@ -using LibHac.Common; -using LibHac.Common.Keys; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.Ncm; -using LibHac.Tools.Fs; -using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; -using LibHac.Tools.Ncm; -using Ryujinx.Common.Logging; -using Ryujinx.HLE.Exceptions; -using Ryujinx.HLE.HOS.Services.Ssl; -using Ryujinx.HLE.HOS.Services.Time; -using Ryujinx.HLE.Utilities; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using Path = System.IO.Path; - -namespace Ryujinx.HLE.FileSystem.Content -{ - public class ContentManager - { - private const ulong SystemVersionTitleId = 0x0100000000000809; - private const ulong SystemUpdateTitleId = 0x0100000000000816; - - private Dictionary> _locationEntries; - - private Dictionary _sharedFontTitleDictionary; - private Dictionary _systemTitlesNameDictionary; - private Dictionary _sharedFontFilenameDictionary; - - private SortedDictionary<(ulong titleId, NcaContentType type), string> _contentDictionary; - - private struct AocItem - { - public readonly string ContainerPath; - public readonly string NcaPath; - public bool Enabled; - - public AocItem(string containerPath, string ncaPath, bool enabled) - { - ContainerPath = containerPath; - NcaPath = ncaPath; - Enabled = enabled; - } - } - - private SortedList _aocData { get; } - - private VirtualFileSystem _virtualFileSystem; - - private readonly object _lock = new object(); - - public ContentManager(VirtualFileSystem virtualFileSystem) - { - _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>(); - _locationEntries = new Dictionary>(); - - _sharedFontTitleDictionary = new Dictionary - { - { "FontStandard", 0x0100000000000811 }, - { "FontChineseSimplified", 0x0100000000000814 }, - { "FontExtendedChineseSimplified", 0x0100000000000814 }, - { "FontKorean", 0x0100000000000812 }, - { "FontChineseTraditional", 0x0100000000000813 }, - { "FontNintendoExtended", 0x0100000000000810 } - }; - - _systemTitlesNameDictionary = new Dictionary() - { - { 0x010000000000080E, "TimeZoneBinary" }, - { 0x0100000000000810, "FontNintendoExtension" }, - { 0x0100000000000811, "FontStandard" }, - { 0x0100000000000812, "FontKorean" }, - { 0x0100000000000813, "FontChineseTraditional" }, - { 0x0100000000000814, "FontChineseSimple" }, - }; - - _sharedFontFilenameDictionary = new Dictionary - { - { "FontStandard", "nintendo_udsg-r_std_003.bfttf" }, - { "FontChineseSimplified", "nintendo_udsg-r_org_zh-cn_003.bfttf" }, - { "FontExtendedChineseSimplified", "nintendo_udsg-r_ext_zh-cn_003.bfttf" }, - { "FontKorean", "nintendo_udsg-r_ko_003.bfttf" }, - { "FontChineseTraditional", "nintendo_udjxh-db_zh-tw_003.bfttf" }, - { "FontNintendoExtended", "nintendo_ext_003.bfttf" } - }; - - _virtualFileSystem = virtualFileSystem; - - _aocData = new SortedList(); - } - - public void LoadEntries(Switch device = null) - { - lock (_lock) - { - _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>(); - _locationEntries = new Dictionary>(); - - foreach (StorageId storageId in Enum.GetValues()) - { - string contentDirectory = null; - string contentPathString = null; - string registeredDirectory = null; - - try - { - contentPathString = LocationHelper.GetContentRoot(storageId); - contentDirectory = LocationHelper.GetRealPath(_virtualFileSystem, contentPathString); - registeredDirectory = Path.Combine(contentDirectory, "registered"); - } - catch (NotSupportedException) - { - continue; - } - - Directory.CreateDirectory(registeredDirectory); - - LinkedList locationList = new LinkedList(); - - void AddEntry(LocationEntry entry) - { - locationList.AddLast(entry); - } - - foreach (string directoryPath in Directory.EnumerateDirectories(registeredDirectory)) - { - if (Directory.GetFiles(directoryPath).Length > 0) - { - string ncaName = new DirectoryInfo(directoryPath).Name.Replace(".nca", string.Empty); - - using (FileStream ncaFile = File.OpenRead(Directory.GetFiles(directoryPath)[0])) - { - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); - - string switchPath = contentPathString + ":/" + ncaFile.Name.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar); - - // Change path format to switch's - switchPath = switchPath.Replace('\\', '/'); - - LocationEntry entry = new LocationEntry(switchPath, - 0, - nca.Header.TitleId, - nca.Header.ContentType); - - AddEntry(entry); - - _contentDictionary.Add((nca.Header.TitleId, nca.Header.ContentType), ncaName); - } - } - } - - foreach (string filePath in Directory.EnumerateFiles(contentDirectory)) - { - if (Path.GetExtension(filePath) == ".nca") - { - string ncaName = Path.GetFileNameWithoutExtension(filePath); - - using (FileStream ncaFile = new FileStream(filePath, FileMode.Open, FileAccess.Read)) - { - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); - - string switchPath = contentPathString + ":/" + filePath.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar); - - // Change path format to switch's - switchPath = switchPath.Replace('\\', '/'); - - LocationEntry entry = new LocationEntry(switchPath, - 0, - nca.Header.TitleId, - nca.Header.ContentType); - - AddEntry(entry); - - _contentDictionary.Add((nca.Header.TitleId, nca.Header.ContentType), ncaName); - } - } - } - - if (_locationEntries.ContainsKey(storageId) && _locationEntries[storageId]?.Count == 0) - { - _locationEntries.Remove(storageId); - } - - if (!_locationEntries.ContainsKey(storageId)) - { - _locationEntries.Add(storageId, locationList); - } - } - - if (device != null) - { - TimeManager.Instance.InitializeTimeZone(device); - BuiltInCertificateManager.Instance.Initialize(device); - device.System.SharedFontManager.Initialize(); - } - } - } - - // fs must contain AOC nca files in its root - public void AddAocData(IFileSystem fs, string containerPath, ulong aocBaseId, IntegrityCheckLevel integrityCheckLevel) - { - _virtualFileSystem.ImportTickets(fs); - - foreach (var ncaPath in fs.EnumerateEntries("*.cnmt.nca", SearchOptions.Default)) - { - using var ncaFile = new UniqueRef(); - - fs.OpenFile(ref ncaFile.Ref(), ncaPath.FullPath.ToU8Span(), OpenMode.Read); - var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - if (nca.Header.ContentType != NcaContentType.Meta) - { - Logger.Warning?.Print(LogClass.Application, $"{ncaPath} is not a valid metadata file"); - - continue; - } - - using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel); - using var cnmtFile = new UniqueRef(); - - pfs0.OpenFile(ref cnmtFile.Ref(), pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read); - - var cnmt = new Cnmt(cnmtFile.Get.AsStream()); - - if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId) - { - continue; - } - - string ncaId = BitConverter.ToString(cnmt.ContentEntries[0].NcaId).Replace("-", "").ToLower(); - if (!_aocData.TryAdd(cnmt.TitleId, new AocItem(containerPath, $"{ncaId}.nca", true))) - { - Logger.Warning?.Print(LogClass.Application, $"Duplicate AddOnContent detected. TitleId {cnmt.TitleId:X16}"); - } - else - { - Logger.Info?.Print(LogClass.Application, $"Found AddOnContent with TitleId {cnmt.TitleId:X16}"); - } - } - } - - public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool enabled) - { - if (!_aocData.TryAdd(titleId, new AocItem(containerPath, ncaPath, enabled))) - { - Logger.Warning?.Print(LogClass.Application, $"Duplicate AddOnContent detected. TitleId {titleId:X16}"); - } - else - { - Logger.Info?.Print(LogClass.Application, $"Found AddOnContent with TitleId {titleId:X16}"); - - using (FileStream fileStream = File.OpenRead(containerPath)) - using (PartitionFileSystem pfs = new PartitionFileSystem(fileStream.AsStorage())) - { - _virtualFileSystem.ImportTickets(pfs); - } - } - } - - public void ClearAocData() => _aocData.Clear(); - - public int GetAocCount() => _aocData.Where(e => e.Value.Enabled).Count(); - - public IList GetAocTitleIds() => _aocData.Where(e => e.Value.Enabled).Select(e => e.Key).ToList(); - - public bool GetAocDataStorage(ulong aocTitleId, out IStorage aocStorage, IntegrityCheckLevel integrityCheckLevel) - { - aocStorage = null; - - if (_aocData.TryGetValue(aocTitleId, out AocItem aoc) && aoc.Enabled) - { - var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read); - using var ncaFile = new UniqueRef(); - PartitionFileSystem pfs; - - switch (Path.GetExtension(aoc.ContainerPath)) - { - case ".xci": - pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - pfs.OpenFile(ref ncaFile.Ref(), aoc.NcaPath.ToU8Span(), OpenMode.Read); - break; - case ".nsp": - pfs = new PartitionFileSystem(file.AsStorage()); - pfs.OpenFile(ref ncaFile.Ref(), aoc.NcaPath.ToU8Span(), OpenMode.Read); - break; - default: - return false; // Print error? - } - - aocStorage = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()).OpenStorage(NcaSectionType.Data, integrityCheckLevel); - - return true; - } - - return false; - } - - public void ClearEntry(ulong titleId, NcaContentType contentType, StorageId storageId) - { - lock (_lock) - { - RemoveLocationEntry(titleId, contentType, storageId); - } - } - - public void RefreshEntries(StorageId storageId, int flag) - { - lock (_lock) - { - LinkedList locationList = _locationEntries[storageId]; - LinkedListNode locationEntry = locationList.First; - - while (locationEntry != null) - { - LinkedListNode nextLocationEntry = locationEntry.Next; - - if (locationEntry.Value.Flag == flag) - { - locationList.Remove(locationEntry.Value); - } - - locationEntry = nextLocationEntry; - } - } - } - - public bool HasNca(string ncaId, StorageId storageId) - { - lock (_lock) - { - if (_contentDictionary.ContainsValue(ncaId)) - { - var content = _contentDictionary.FirstOrDefault(x => x.Value == ncaId); - ulong titleId = content.Key.Item1; - - NcaContentType contentType = content.Key.type; - StorageId storage = GetInstalledStorage(titleId, contentType, storageId); - - return storage == storageId; - } - } - - return false; - } - - public UInt128 GetInstalledNcaId(ulong titleId, NcaContentType contentType) - { - lock (_lock) - { - if (_contentDictionary.ContainsKey((titleId, contentType))) - { - return new UInt128(_contentDictionary[(titleId, contentType)]); - } - } - - return new UInt128(); - } - - public StorageId GetInstalledStorage(ulong titleId, NcaContentType contentType, StorageId storageId) - { - lock (_lock) - { - LocationEntry locationEntry = GetLocation(titleId, contentType, storageId); - - return locationEntry.ContentPath != null ? - LocationHelper.GetStorageId(locationEntry.ContentPath) : StorageId.None; - } - } - - public string GetInstalledContentPath(ulong titleId, StorageId storageId, NcaContentType contentType) - { - lock (_lock) - { - LocationEntry locationEntry = GetLocation(titleId, contentType, storageId); - - if (VerifyContentType(locationEntry, contentType)) - { - return locationEntry.ContentPath; - } - } - - return string.Empty; - } - - public void RedirectLocation(LocationEntry newEntry, StorageId storageId) - { - lock (_lock) - { - LocationEntry locationEntry = GetLocation(newEntry.TitleId, newEntry.ContentType, storageId); - - if (locationEntry.ContentPath != null) - { - RemoveLocationEntry(newEntry.TitleId, newEntry.ContentType, storageId); - } - - AddLocationEntry(newEntry, storageId); - } - } - - private bool VerifyContentType(LocationEntry locationEntry, NcaContentType contentType) - { - if (locationEntry.ContentPath == null) - { - return false; - } - - string installedPath = _virtualFileSystem.SwitchPathToSystemPath(locationEntry.ContentPath); - - if (!string.IsNullOrWhiteSpace(installedPath)) - { - if (File.Exists(installedPath)) - { - using (FileStream file = new FileStream(installedPath, FileMode.Open, FileAccess.Read)) - { - Nca nca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); - bool contentCheck = nca.Header.ContentType == contentType; - - return contentCheck; - } - } - } - - return false; - } - - private void AddLocationEntry(LocationEntry entry, StorageId storageId) - { - LinkedList locationList = null; - - if (_locationEntries.ContainsKey(storageId)) - { - locationList = _locationEntries[storageId]; - } - - if (locationList != null) - { - if (locationList.Contains(entry)) - { - locationList.Remove(entry); - } - - locationList.AddLast(entry); - } - } - - private void RemoveLocationEntry(ulong titleId, NcaContentType contentType, StorageId storageId) - { - LinkedList locationList = null; - - if (_locationEntries.ContainsKey(storageId)) - { - locationList = _locationEntries[storageId]; - } - - if (locationList != null) - { - LocationEntry entry = - locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType); - - if (entry.ContentPath != null) - { - locationList.Remove(entry); - } - } - } - - public bool TryGetFontTitle(string fontName, out ulong titleId) - { - return _sharedFontTitleDictionary.TryGetValue(fontName, out titleId); - } - - public bool TryGetFontFilename(string fontName, out string filename) - { - return _sharedFontFilenameDictionary.TryGetValue(fontName, out filename); - } - - public bool TryGetSystemTitlesName(ulong titleId, out string name) - { - return _systemTitlesNameDictionary.TryGetValue(titleId, out name); - } - - private LocationEntry GetLocation(ulong titleId, NcaContentType contentType, StorageId storageId) - { - LinkedList locationList = _locationEntries[storageId]; - - return locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType); - } - - public void InstallFirmware(string firmwareSource) - { - string contentPathString = LocationHelper.GetContentRoot(StorageId.NandSystem); - string contentDirectory = LocationHelper.GetRealPath(_virtualFileSystem, contentPathString); - string registeredDirectory = Path.Combine(contentDirectory, "registered"); - string temporaryDirectory = Path.Combine(contentDirectory, "temp"); - - if (Directory.Exists(temporaryDirectory)) - { - Directory.Delete(temporaryDirectory, true); - } - - if (Directory.Exists(firmwareSource)) - { - InstallFromDirectory(firmwareSource, temporaryDirectory); - FinishInstallation(temporaryDirectory, registeredDirectory); - - return; - } - - if (!File.Exists(firmwareSource)) - { - throw new FileNotFoundException("Firmware file does not exist."); - } - - FileInfo info = new FileInfo(firmwareSource); - - using (FileStream file = File.OpenRead(firmwareSource)) - { - switch (info.Extension) - { - case ".zip": - using (ZipArchive archive = ZipFile.OpenRead(firmwareSource)) - { - InstallFromZip(archive, temporaryDirectory); - } - break; - case ".xci": - Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); - InstallFromCart(xci, temporaryDirectory); - break; - default: - throw new InvalidFirmwarePackageException("Input file is not a valid firmware package"); - } - - FinishInstallation(temporaryDirectory, registeredDirectory); - } - } - - private void FinishInstallation(string temporaryDirectory, string registeredDirectory) - { - if (Directory.Exists(registeredDirectory)) - { - new DirectoryInfo(registeredDirectory).Delete(true); - } - - Directory.Move(temporaryDirectory, registeredDirectory); - - LoadEntries(); - } - - private void InstallFromDirectory(string firmwareDirectory, string temporaryDirectory) - { - InstallFromPartition(new LocalFileSystem(firmwareDirectory), temporaryDirectory); - } - - private void InstallFromPartition(IFileSystem filesystem, string temporaryDirectory) - { - foreach (var entry in filesystem.EnumerateEntries("/", "*.nca")) - { - Nca nca = new Nca(_virtualFileSystem.KeySet, OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage()); - - SaveNca(nca, entry.Name.Remove(entry.Name.IndexOf('.')), temporaryDirectory); - } - } - - private void InstallFromCart(Xci gameCard, string temporaryDirectory) - { - if (gameCard.HasPartition(XciPartitionType.Update)) - { - XciPartition partition = gameCard.OpenPartition(XciPartitionType.Update); - - InstallFromPartition(partition, temporaryDirectory); - } - else - { - throw new Exception("Update not found in xci file."); - } - } - - private void InstallFromZip(ZipArchive archive, string temporaryDirectory) - { - using (archive) - { - foreach (var entry in archive.Entries) - { - if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00")) - { - // Clean up the name and get the NcaId - - string[] pathComponents = entry.FullName.Replace(".cnmt", "").Split('/'); - - string ncaId = pathComponents[pathComponents.Length - 1]; - - // If this is a fragmented nca, we need to get the previous element.GetZip - if (ncaId.Equals("00")) - { - ncaId = pathComponents[pathComponents.Length - 2]; - } - - if (ncaId.Contains(".nca")) - { - string newPath = Path.Combine(temporaryDirectory, ncaId); - - Directory.CreateDirectory(newPath); - - entry.ExtractToFile(Path.Combine(newPath, "00")); - } - } - } - } - } - - public void SaveNca(Nca nca, string ncaId, string temporaryDirectory) - { - string newPath = Path.Combine(temporaryDirectory, ncaId + ".nca"); - - Directory.CreateDirectory(newPath); - - using (FileStream file = File.Create(Path.Combine(newPath, "00"))) - { - nca.BaseStorage.AsStream().CopyTo(file); - } - } - - private IFile OpenPossibleFragmentedFile(IFileSystem filesystem, string path, OpenMode mode) - { - using var file = new UniqueRef(); - - if (filesystem.FileExists($"{path}/00")) - { - filesystem.OpenFile(ref file.Ref(), $"{path}/00".ToU8Span(), mode); - } - else - { - filesystem.OpenFile(ref file.Ref(), path.ToU8Span(), mode); - } - - return file.Release(); - } - - private Stream GetZipStream(ZipArchiveEntry entry) - { - MemoryStream dest = new MemoryStream(); - - Stream src = entry.Open(); - - src.CopyTo(dest); - src.Dispose(); - - return dest; - } - - public SystemVersion VerifyFirmwarePackage(string firmwarePackage) - { - _virtualFileSystem.ReloadKeySet(); - - // LibHac.NcaHeader's DecryptHeader doesn't check if HeaderKey is empty and throws InvalidDataException instead - // So, we check it early for a better user experience. - if (_virtualFileSystem.KeySet.HeaderKey.IsZeros()) - { - throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers."); - } - - Dictionary> updateNcas = new Dictionary>(); - - if (Directory.Exists(firmwarePackage)) - { - return VerifyAndGetVersionDirectory(firmwarePackage); - } - - if (!File.Exists(firmwarePackage)) - { - throw new FileNotFoundException("Firmware file does not exist."); - } - - FileInfo info = new FileInfo(firmwarePackage); - - using (FileStream file = File.OpenRead(firmwarePackage)) - { - switch (info.Extension) - { - case ".zip": - using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage)) - { - return VerifyAndGetVersionZip(archive); - } - case ".xci": - Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); - - if (xci.HasPartition(XciPartitionType.Update)) - { - XciPartition partition = xci.OpenPartition(XciPartitionType.Update); - - return VerifyAndGetVersion(partition); - } - else - { - throw new InvalidFirmwarePackageException("Update not found in xci file."); - } - default: - break; - } - } - - SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory) - { - return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory)); - } - - SystemVersion VerifyAndGetVersionZip(ZipArchive archive) - { - SystemVersion systemVersion = null; - - foreach (var entry in archive.Entries) - { - if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00")) - { - using (Stream ncaStream = GetZipStream(entry)) - { - IStorage storage = ncaStream.AsStorage(); - - Nca nca = new Nca(_virtualFileSystem.KeySet, storage); - - if (updateNcas.ContainsKey(nca.Header.TitleId)) - { - updateNcas[nca.Header.TitleId].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.ContainsKey(SystemUpdateTitleId)) - { - var ncaEntry = updateNcas[SystemUpdateTitleId]; - - 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 Nca(_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.ContainsKey(SystemVersionTitleId)) - { - string versionEntry = updateNcas[SystemVersionTitleId].Find(x => x.type != NcaContentType.Meta).path; - - using (Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry))) - { - Nca nca = new Nca(_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.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 Nca(_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 Span(contentData); - - contentStorage.Read(0, content); - - Span hash = new Span(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) - { - string extraNcas = string.Empty; - - foreach (var entry in updateNcas) - { - foreach (var nca in entry.Value) - { - extraNcas += nca.path + Environment.NewLine; - } - } - - throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}"); - } - } - else - { - throw new FileNotFoundException("System update title was not found in the firmware package."); - } - - return systemVersion; - } - - SystemVersion VerifyAndGetVersion(IFileSystem filesystem) - { - 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 Nca(_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.ContainsKey(nca.Header.TitleId)) - { - updateNcas[nca.Header.TitleId].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)) - { - var metaNcaEntry = ncaEntry.Find(x => x.type == NcaContentType.Meta); - 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, metaNcaEntry.path, OpenMode.Read).AsStorage(); - IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage(); - - Nca metaNca = new Nca(_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 Span(contentData); - - contentStorage.Read(0, content); - - Span hash = new Span(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) - { - string extraNcas = string.Empty; - - foreach (var entry in updateNcas) - { - foreach (var nca in entry.Value) - { - extraNcas += nca.path + Environment.NewLine; - } - } - - throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}"); - } - - return systemVersion; - } - - return null; - } - - public SystemVersion GetCurrentFirmwareVersion() - { - LoadEntries(); - - lock (_lock) - { - var locationEnties = _locationEntries[StorageId.NandSystem]; - - foreach (var entry in locationEnties) - { - if (entry.ContentType == NcaContentType.Data) - { - var path = _virtualFileSystem.SwitchPathToSystemPath(entry.ContentPath); - - using (FileStream fileStream = File.OpenRead(path)) - { - Nca nca = new Nca(_virtualFileSystem.KeySet, fileStream.AsStorage()); - - 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()) - { - return new SystemVersion(systemVersionFile.Get.AsStream()); - } - } - - } - } - } - } - - return null; - } - } -} diff --git a/Ryujinx.HLE/FileSystem/Content/ContentPath.cs b/Ryujinx.HLE/FileSystem/Content/ContentPath.cs deleted file mode 100644 index 1e2c8ab3..00000000 --- a/Ryujinx.HLE/FileSystem/Content/ContentPath.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Ryujinx.HLE.FileSystem.Content -{ - static class ContentPath - { - public const string SystemContent = "@SystemContent"; - public const string UserContent = "@UserContent"; - public const string SdCardContent = "@SdCardContent"; - public const string SdCard = "@SdCard"; - public const string CalibFile = "@CalibFile"; - public const string Safe = "@Safe"; - public const string User = "@User"; - public const string System = "@System"; - public const string Host = "@Host"; - public const string GamecardApp = "@GcApp"; - public const string GamecardContents = "@GcS00000001"; - public const string GamecardUpdate = "@upp"; - public const string RegisteredUpdate = "@RegUpdate"; - } -} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/Content/LocationEntry.cs b/Ryujinx.HLE/FileSystem/Content/LocationEntry.cs deleted file mode 100644 index bf938746..00000000 --- a/Ryujinx.HLE/FileSystem/Content/LocationEntry.cs +++ /dev/null @@ -1,25 +0,0 @@ -using LibHac.FsSystem; - -namespace Ryujinx.HLE.FileSystem.Content -{ - public struct LocationEntry - { - public string ContentPath { get; private set; } - public int Flag { get; private set; } - public ulong TitleId { get; private set; } - public NcaContentType ContentType { get; private set; } - - public LocationEntry(string contentPath, int flag, ulong titleId, NcaContentType contentType) - { - ContentPath = contentPath; - Flag = flag; - TitleId = titleId; - ContentType = contentType; - } - - public void SetFlag(int flag) - { - Flag = flag; - } - } -} diff --git a/Ryujinx.HLE/FileSystem/Content/LocationHelper.cs b/Ryujinx.HLE/FileSystem/Content/LocationHelper.cs deleted file mode 100644 index c522b053..00000000 --- a/Ryujinx.HLE/FileSystem/Content/LocationHelper.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.IO; - -using static Ryujinx.HLE.FileSystem.VirtualFileSystem; - -namespace Ryujinx.HLE.FileSystem.Content -{ - internal static class LocationHelper - { - public static string GetRealPath(VirtualFileSystem fileSystem, string switchContentPath) - { - string basePath = fileSystem.GetBasePath(); - - switch (switchContentPath) - { - case ContentPath.SystemContent: - return Path.Combine(basePath, SystemNandPath, "Contents"); - case ContentPath.UserContent: - return Path.Combine(basePath, UserNandPath, "Contents"); - case ContentPath.SdCardContent: - return Path.Combine(fileSystem.GetSdCardPath(), "Nintendo", "Contents"); - case ContentPath.System: - return Path.Combine(basePath, SystemNandPath); - case ContentPath.User: - return Path.Combine(basePath, UserNandPath); - default: - throw new NotSupportedException($"Content Path `{switchContentPath}` is not supported."); - } - } - - public static string GetContentPath(ContentStorageId contentStorageId) - { - switch (contentStorageId) - { - case ContentStorageId.NandSystem: - return ContentPath.SystemContent; - case ContentStorageId.NandUser: - return ContentPath.UserContent; - case ContentStorageId.SdCard: - return ContentPath.SdCardContent; - default: - throw new NotSupportedException($"Content Storage `{contentStorageId}` is not supported."); - } - } - - public static string GetContentRoot(StorageId storageId) - { - switch (storageId) - { - case StorageId.NandSystem: - return ContentPath.SystemContent; - case StorageId.NandUser: - return ContentPath.UserContent; - case StorageId.SdCard: - return ContentPath.SdCardContent; - default: - throw new NotSupportedException($"Storage Id `{storageId}` is not supported."); - } - } - - public static StorageId GetStorageId(string contentPathString) - { - string cleanedPath = contentPathString.Split(':')[0]; - - switch (cleanedPath) - { - case ContentPath.SystemContent: - case ContentPath.System: - return StorageId.NandSystem; - - case ContentPath.UserContent: - case ContentPath.User: - return StorageId.NandUser; - - case ContentPath.SdCardContent: - return StorageId.SdCard; - - case ContentPath.Host: - return StorageId.Host; - - case ContentPath.GamecardApp: - case ContentPath.GamecardContents: - case ContentPath.GamecardUpdate: - return StorageId.GameCard; - - default: - return StorageId.None; - } - } - } -} diff --git a/Ryujinx.HLE/FileSystem/Content/StorageId.cs b/Ryujinx.HLE/FileSystem/Content/StorageId.cs deleted file mode 100644 index 4ff3dd65..00000000 --- a/Ryujinx.HLE/FileSystem/Content/StorageId.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ryujinx.HLE.FileSystem.Content -{ - public enum ContentStorageId - { - NandSystem, - NandUser, - SdCard - } -} diff --git a/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs b/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs deleted file mode 100644 index 3f19e135..00000000 --- a/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Ryujinx.HLE.Utilities; -using System.IO; -using System.Text; - -namespace Ryujinx.HLE.FileSystem.Content -{ - public class SystemVersion - { - public byte Major { get; } - public byte Minor { get; } - public byte Micro { get; } - public byte RevisionMajor { get; } - public byte RevisionMinor { get; } - public string PlatformString { get; } - public string Hex { get; } - public string VersionString { get; } - public string VersionTitle { get; } - - public SystemVersion(Stream systemVersionFile) - { - using (BinaryReader reader = new BinaryReader(systemVersionFile)) - { - Major = reader.ReadByte(); - Minor = reader.ReadByte(); - Micro = reader.ReadByte(); - - reader.ReadByte(); // Padding - - RevisionMajor = reader.ReadByte(); - RevisionMinor = reader.ReadByte(); - - reader.ReadBytes(2); // Padding - - PlatformString = StringUtils.ReadInlinedAsciiString(reader, 0x20); - Hex = StringUtils.ReadInlinedAsciiString(reader, 0x40); - VersionString = StringUtils.ReadInlinedAsciiString(reader, 0x18); - VersionTitle = StringUtils.ReadInlinedAsciiString(reader, 0x80); - } - } - } -} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/Content/TitleType.cs b/Ryujinx.HLE/FileSystem/Content/TitleType.cs deleted file mode 100644 index 6ad26c9c..00000000 --- a/Ryujinx.HLE/FileSystem/Content/TitleType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Ryujinx.HLE.FileSystem.Content -{ - enum TitleType - { - SystemPrograms = 0x01, - SystemDataArchive = 0x02, - SystemUpdate = 0x03, - FirmwarePackageA = 0x04, - FirmwarePackageB = 0x05, - RegularApplication = 0x80, - Update = 0x81, - AddOnContent = 0x82, - DeltaTitle = 0x83 - } -} diff --git a/Ryujinx.HLE/FileSystem/ContentManager.cs b/Ryujinx.HLE/FileSystem/ContentManager.cs new file mode 100644 index 00000000..2544cdda --- /dev/null +++ b/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -0,0 +1,1053 @@ +using LibHac.Common; +using LibHac.Common.Keys; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.Exceptions; +using Ryujinx.HLE.HOS.Services.Ssl; +using Ryujinx.HLE.HOS.Services.Time; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Path = System.IO.Path; + +namespace Ryujinx.HLE.FileSystem +{ + public class ContentManager + { + private const ulong SystemVersionTitleId = 0x0100000000000809; + private const ulong SystemUpdateTitleId = 0x0100000000000816; + + private Dictionary> _locationEntries; + + private Dictionary _sharedFontTitleDictionary; + private Dictionary _systemTitlesNameDictionary; + private Dictionary _sharedFontFilenameDictionary; + + private SortedDictionary<(ulong titleId, NcaContentType type), string> _contentDictionary; + + private struct AocItem + { + public readonly string ContainerPath; + public readonly string NcaPath; + public bool Enabled; + + public AocItem(string containerPath, string ncaPath, bool enabled) + { + ContainerPath = containerPath; + NcaPath = ncaPath; + Enabled = enabled; + } + } + + private SortedList _aocData { get; } + + private VirtualFileSystem _virtualFileSystem; + + private readonly object _lock = new object(); + + public ContentManager(VirtualFileSystem virtualFileSystem) + { + _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>(); + _locationEntries = new Dictionary>(); + + _sharedFontTitleDictionary = new Dictionary + { + { "FontStandard", 0x0100000000000811 }, + { "FontChineseSimplified", 0x0100000000000814 }, + { "FontExtendedChineseSimplified", 0x0100000000000814 }, + { "FontKorean", 0x0100000000000812 }, + { "FontChineseTraditional", 0x0100000000000813 }, + { "FontNintendoExtended", 0x0100000000000810 } + }; + + _systemTitlesNameDictionary = new Dictionary() + { + { 0x010000000000080E, "TimeZoneBinary" }, + { 0x0100000000000810, "FontNintendoExtension" }, + { 0x0100000000000811, "FontStandard" }, + { 0x0100000000000812, "FontKorean" }, + { 0x0100000000000813, "FontChineseTraditional" }, + { 0x0100000000000814, "FontChineseSimple" }, + }; + + _sharedFontFilenameDictionary = new Dictionary + { + { "FontStandard", "nintendo_udsg-r_std_003.bfttf" }, + { "FontChineseSimplified", "nintendo_udsg-r_org_zh-cn_003.bfttf" }, + { "FontExtendedChineseSimplified", "nintendo_udsg-r_ext_zh-cn_003.bfttf" }, + { "FontKorean", "nintendo_udsg-r_ko_003.bfttf" }, + { "FontChineseTraditional", "nintendo_udjxh-db_zh-tw_003.bfttf" }, + { "FontNintendoExtended", "nintendo_ext_003.bfttf" } + }; + + _virtualFileSystem = virtualFileSystem; + + _aocData = new SortedList(); + } + + public void LoadEntries(Switch device = null) + { + lock (_lock) + { + _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>(); + _locationEntries = new Dictionary>(); + + foreach (StorageId storageId in Enum.GetValues()) + { + string contentDirectory = null; + string contentPathString = null; + string registeredDirectory = null; + + try + { + contentPathString = ContentPath.GetContentPath(storageId); + contentDirectory = ContentPath.GetRealPath(_virtualFileSystem, contentPathString); + registeredDirectory = Path.Combine(contentDirectory, "registered"); + } + catch (NotSupportedException) + { + continue; + } + + Directory.CreateDirectory(registeredDirectory); + + LinkedList locationList = new LinkedList(); + + void AddEntry(LocationEntry entry) + { + locationList.AddLast(entry); + } + + foreach (string directoryPath in Directory.EnumerateDirectories(registeredDirectory)) + { + if (Directory.GetFiles(directoryPath).Length > 0) + { + string ncaName = new DirectoryInfo(directoryPath).Name.Replace(".nca", string.Empty); + + using (FileStream ncaFile = File.OpenRead(Directory.GetFiles(directoryPath)[0])) + { + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + + string switchPath = contentPathString + ":/" + ncaFile.Name.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar); + + // Change path format to switch's + switchPath = switchPath.Replace('\\', '/'); + + LocationEntry entry = new LocationEntry(switchPath, + 0, + nca.Header.TitleId, + nca.Header.ContentType); + + AddEntry(entry); + + _contentDictionary.Add((nca.Header.TitleId, nca.Header.ContentType), ncaName); + } + } + } + + foreach (string filePath in Directory.EnumerateFiles(contentDirectory)) + { + if (Path.GetExtension(filePath) == ".nca") + { + string ncaName = Path.GetFileNameWithoutExtension(filePath); + + using (FileStream ncaFile = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + + string switchPath = contentPathString + ":/" + filePath.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar); + + // Change path format to switch's + switchPath = switchPath.Replace('\\', '/'); + + LocationEntry entry = new LocationEntry(switchPath, + 0, + nca.Header.TitleId, + nca.Header.ContentType); + + AddEntry(entry); + + _contentDictionary.Add((nca.Header.TitleId, nca.Header.ContentType), ncaName); + } + } + } + + if (_locationEntries.ContainsKey(storageId) && _locationEntries[storageId]?.Count == 0) + { + _locationEntries.Remove(storageId); + } + + if (!_locationEntries.ContainsKey(storageId)) + { + _locationEntries.Add(storageId, locationList); + } + } + + if (device != null) + { + TimeManager.Instance.InitializeTimeZone(device); + BuiltInCertificateManager.Instance.Initialize(device); + device.System.SharedFontManager.Initialize(); + } + } + } + + // fs must contain AOC nca files in its root + public void AddAocData(IFileSystem fs, string containerPath, ulong aocBaseId, IntegrityCheckLevel integrityCheckLevel) + { + _virtualFileSystem.ImportTickets(fs); + + foreach (var ncaPath in fs.EnumerateEntries("*.cnmt.nca", SearchOptions.Default)) + { + using var ncaFile = new UniqueRef(); + + fs.OpenFile(ref ncaFile.Ref(), ncaPath.FullPath.ToU8Span(), OpenMode.Read); + var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + if (nca.Header.ContentType != NcaContentType.Meta) + { + Logger.Warning?.Print(LogClass.Application, $"{ncaPath} is not a valid metadata file"); + + continue; + } + + using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel); + using var cnmtFile = new UniqueRef(); + + pfs0.OpenFile(ref cnmtFile.Ref(), pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read); + + var cnmt = new Cnmt(cnmtFile.Get.AsStream()); + + if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId) + { + continue; + } + + string ncaId = BitConverter.ToString(cnmt.ContentEntries[0].NcaId).Replace("-", "").ToLower(); + if (!_aocData.TryAdd(cnmt.TitleId, new AocItem(containerPath, $"{ncaId}.nca", true))) + { + Logger.Warning?.Print(LogClass.Application, $"Duplicate AddOnContent detected. TitleId {cnmt.TitleId:X16}"); + } + else + { + Logger.Info?.Print(LogClass.Application, $"Found AddOnContent with TitleId {cnmt.TitleId:X16}"); + } + } + } + + public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool enabled) + { + if (!_aocData.TryAdd(titleId, new AocItem(containerPath, ncaPath, enabled))) + { + Logger.Warning?.Print(LogClass.Application, $"Duplicate AddOnContent detected. TitleId {titleId:X16}"); + } + else + { + Logger.Info?.Print(LogClass.Application, $"Found AddOnContent with TitleId {titleId:X16}"); + + using (FileStream fileStream = File.OpenRead(containerPath)) + using (PartitionFileSystem pfs = new PartitionFileSystem(fileStream.AsStorage())) + { + _virtualFileSystem.ImportTickets(pfs); + } + } + } + + public void ClearAocData() => _aocData.Clear(); + + public int GetAocCount() => _aocData.Where(e => e.Value.Enabled).Count(); + + public IList GetAocTitleIds() => _aocData.Where(e => e.Value.Enabled).Select(e => e.Key).ToList(); + + public bool GetAocDataStorage(ulong aocTitleId, out IStorage aocStorage, IntegrityCheckLevel integrityCheckLevel) + { + aocStorage = null; + + if (_aocData.TryGetValue(aocTitleId, out AocItem aoc) && aoc.Enabled) + { + var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read); + using var ncaFile = new UniqueRef(); + PartitionFileSystem pfs; + + switch (Path.GetExtension(aoc.ContainerPath)) + { + case ".xci": + pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + pfs.OpenFile(ref ncaFile.Ref(), aoc.NcaPath.ToU8Span(), OpenMode.Read); + break; + case ".nsp": + pfs = new PartitionFileSystem(file.AsStorage()); + pfs.OpenFile(ref ncaFile.Ref(), aoc.NcaPath.ToU8Span(), OpenMode.Read); + break; + default: + return false; // Print error? + } + + aocStorage = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()).OpenStorage(NcaSectionType.Data, integrityCheckLevel); + + return true; + } + + return false; + } + + public void ClearEntry(ulong titleId, NcaContentType contentType, StorageId storageId) + { + lock (_lock) + { + RemoveLocationEntry(titleId, contentType, storageId); + } + } + + public void RefreshEntries(StorageId storageId, int flag) + { + lock (_lock) + { + LinkedList locationList = _locationEntries[storageId]; + LinkedListNode locationEntry = locationList.First; + + while (locationEntry != null) + { + LinkedListNode nextLocationEntry = locationEntry.Next; + + if (locationEntry.Value.Flag == flag) + { + locationList.Remove(locationEntry.Value); + } + + locationEntry = nextLocationEntry; + } + } + } + + public bool HasNca(string ncaId, StorageId storageId) + { + lock (_lock) + { + if (_contentDictionary.ContainsValue(ncaId)) + { + var content = _contentDictionary.FirstOrDefault(x => x.Value == ncaId); + ulong titleId = content.Key.Item1; + + NcaContentType contentType = content.Key.type; + StorageId storage = GetInstalledStorage(titleId, contentType, storageId); + + return storage == storageId; + } + } + + return false; + } + + public UInt128 GetInstalledNcaId(ulong titleId, NcaContentType contentType) + { + lock (_lock) + { + if (_contentDictionary.ContainsKey((titleId, contentType))) + { + return new UInt128(_contentDictionary[(titleId, contentType)]); + } + } + + return new UInt128(); + } + + public StorageId GetInstalledStorage(ulong titleId, NcaContentType contentType, StorageId storageId) + { + lock (_lock) + { + LocationEntry locationEntry = GetLocation(titleId, contentType, storageId); + + return locationEntry.ContentPath != null ? ContentPath.GetStorageId(locationEntry.ContentPath) : StorageId.None; + } + } + + public string GetInstalledContentPath(ulong titleId, StorageId storageId, NcaContentType contentType) + { + lock (_lock) + { + LocationEntry locationEntry = GetLocation(titleId, contentType, storageId); + + if (VerifyContentType(locationEntry, contentType)) + { + return locationEntry.ContentPath; + } + } + + return string.Empty; + } + + public void RedirectLocation(LocationEntry newEntry, StorageId storageId) + { + lock (_lock) + { + LocationEntry locationEntry = GetLocation(newEntry.TitleId, newEntry.ContentType, storageId); + + if (locationEntry.ContentPath != null) + { + RemoveLocationEntry(newEntry.TitleId, newEntry.ContentType, storageId); + } + + AddLocationEntry(newEntry, storageId); + } + } + + private bool VerifyContentType(LocationEntry locationEntry, NcaContentType contentType) + { + if (locationEntry.ContentPath == null) + { + return false; + } + + string installedPath = _virtualFileSystem.SwitchPathToSystemPath(locationEntry.ContentPath); + + if (!string.IsNullOrWhiteSpace(installedPath)) + { + if (File.Exists(installedPath)) + { + using (FileStream file = new FileStream(installedPath, FileMode.Open, FileAccess.Read)) + { + Nca nca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); + bool contentCheck = nca.Header.ContentType == contentType; + + return contentCheck; + } + } + } + + return false; + } + + private void AddLocationEntry(LocationEntry entry, StorageId storageId) + { + LinkedList locationList = null; + + if (_locationEntries.ContainsKey(storageId)) + { + locationList = _locationEntries[storageId]; + } + + if (locationList != null) + { + if (locationList.Contains(entry)) + { + locationList.Remove(entry); + } + + locationList.AddLast(entry); + } + } + + private void RemoveLocationEntry(ulong titleId, NcaContentType contentType, StorageId storageId) + { + LinkedList locationList = null; + + if (_locationEntries.ContainsKey(storageId)) + { + locationList = _locationEntries[storageId]; + } + + if (locationList != null) + { + LocationEntry entry = + locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType); + + if (entry.ContentPath != null) + { + locationList.Remove(entry); + } + } + } + + public bool TryGetFontTitle(string fontName, out ulong titleId) + { + return _sharedFontTitleDictionary.TryGetValue(fontName, out titleId); + } + + public bool TryGetFontFilename(string fontName, out string filename) + { + return _sharedFontFilenameDictionary.TryGetValue(fontName, out filename); + } + + public bool TryGetSystemTitlesName(ulong titleId, out string name) + { + return _systemTitlesNameDictionary.TryGetValue(titleId, out name); + } + + private LocationEntry GetLocation(ulong titleId, NcaContentType contentType, StorageId storageId) + { + LinkedList locationList = _locationEntries[storageId]; + + return locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType); + } + + public void InstallFirmware(string firmwareSource) + { + string contentPathString = ContentPath.GetContentPath(StorageId.BuiltInSystem); + string contentDirectory = ContentPath.GetRealPath(_virtualFileSystem, contentPathString); + string registeredDirectory = Path.Combine(contentDirectory, "registered"); + string temporaryDirectory = Path.Combine(contentDirectory, "temp"); + + if (Directory.Exists(temporaryDirectory)) + { + Directory.Delete(temporaryDirectory, true); + } + + if (Directory.Exists(firmwareSource)) + { + InstallFromDirectory(firmwareSource, temporaryDirectory); + FinishInstallation(temporaryDirectory, registeredDirectory); + + return; + } + + if (!File.Exists(firmwareSource)) + { + throw new FileNotFoundException("Firmware file does not exist."); + } + + FileInfo info = new FileInfo(firmwareSource); + + using (FileStream file = File.OpenRead(firmwareSource)) + { + switch (info.Extension) + { + case ".zip": + using (ZipArchive archive = ZipFile.OpenRead(firmwareSource)) + { + InstallFromZip(archive, temporaryDirectory); + } + break; + case ".xci": + Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); + InstallFromCart(xci, temporaryDirectory); + break; + default: + throw new InvalidFirmwarePackageException("Input file is not a valid firmware package"); + } + + FinishInstallation(temporaryDirectory, registeredDirectory); + } + } + + private void FinishInstallation(string temporaryDirectory, string registeredDirectory) + { + if (Directory.Exists(registeredDirectory)) + { + new DirectoryInfo(registeredDirectory).Delete(true); + } + + Directory.Move(temporaryDirectory, registeredDirectory); + + LoadEntries(); + } + + private void InstallFromDirectory(string firmwareDirectory, string temporaryDirectory) + { + InstallFromPartition(new LocalFileSystem(firmwareDirectory), temporaryDirectory); + } + + private void InstallFromPartition(IFileSystem filesystem, string temporaryDirectory) + { + foreach (var entry in filesystem.EnumerateEntries("/", "*.nca")) + { + Nca nca = new Nca(_virtualFileSystem.KeySet, OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage()); + + SaveNca(nca, entry.Name.Remove(entry.Name.IndexOf('.')), temporaryDirectory); + } + } + + private void InstallFromCart(Xci gameCard, string temporaryDirectory) + { + if (gameCard.HasPartition(XciPartitionType.Update)) + { + XciPartition partition = gameCard.OpenPartition(XciPartitionType.Update); + + InstallFromPartition(partition, temporaryDirectory); + } + else + { + throw new Exception("Update not found in xci file."); + } + } + + private void InstallFromZip(ZipArchive archive, string temporaryDirectory) + { + using (archive) + { + foreach (var entry in archive.Entries) + { + if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00")) + { + // Clean up the name and get the NcaId + + string[] pathComponents = entry.FullName.Replace(".cnmt", "").Split('/'); + + string ncaId = pathComponents[pathComponents.Length - 1]; + + // If this is a fragmented nca, we need to get the previous element.GetZip + if (ncaId.Equals("00")) + { + ncaId = pathComponents[pathComponents.Length - 2]; + } + + if (ncaId.Contains(".nca")) + { + string newPath = Path.Combine(temporaryDirectory, ncaId); + + Directory.CreateDirectory(newPath); + + entry.ExtractToFile(Path.Combine(newPath, "00")); + } + } + } + } + } + + public void SaveNca(Nca nca, string ncaId, string temporaryDirectory) + { + string newPath = Path.Combine(temporaryDirectory, ncaId + ".nca"); + + Directory.CreateDirectory(newPath); + + using (FileStream file = File.Create(Path.Combine(newPath, "00"))) + { + nca.BaseStorage.AsStream().CopyTo(file); + } + } + + private IFile OpenPossibleFragmentedFile(IFileSystem filesystem, string path, OpenMode mode) + { + using var file = new UniqueRef(); + + if (filesystem.FileExists($"{path}/00")) + { + filesystem.OpenFile(ref file.Ref(), $"{path}/00".ToU8Span(), mode); + } + else + { + filesystem.OpenFile(ref file.Ref(), path.ToU8Span(), mode); + } + + return file.Release(); + } + + private Stream GetZipStream(ZipArchiveEntry entry) + { + MemoryStream dest = new MemoryStream(); + + Stream src = entry.Open(); + + src.CopyTo(dest); + src.Dispose(); + + return dest; + } + + public SystemVersion VerifyFirmwarePackage(string firmwarePackage) + { + _virtualFileSystem.ReloadKeySet(); + + // LibHac.NcaHeader's DecryptHeader doesn't check if HeaderKey is empty and throws InvalidDataException instead + // So, we check it early for a better user experience. + if (_virtualFileSystem.KeySet.HeaderKey.IsZeros()) + { + throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers."); + } + + Dictionary> updateNcas = new Dictionary>(); + + if (Directory.Exists(firmwarePackage)) + { + return VerifyAndGetVersionDirectory(firmwarePackage); + } + + if (!File.Exists(firmwarePackage)) + { + throw new FileNotFoundException("Firmware file does not exist."); + } + + FileInfo info = new FileInfo(firmwarePackage); + + using (FileStream file = File.OpenRead(firmwarePackage)) + { + switch (info.Extension) + { + case ".zip": + using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage)) + { + return VerifyAndGetVersionZip(archive); + } + case ".xci": + Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); + + if (xci.HasPartition(XciPartitionType.Update)) + { + XciPartition partition = xci.OpenPartition(XciPartitionType.Update); + + return VerifyAndGetVersion(partition); + } + else + { + throw new InvalidFirmwarePackageException("Update not found in xci file."); + } + default: + break; + } + } + + SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory) + { + return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory)); + } + + SystemVersion VerifyAndGetVersionZip(ZipArchive archive) + { + SystemVersion systemVersion = null; + + foreach (var entry in archive.Entries) + { + if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00")) + { + using (Stream ncaStream = GetZipStream(entry)) + { + IStorage storage = ncaStream.AsStorage(); + + Nca nca = new Nca(_virtualFileSystem.KeySet, storage); + + if (updateNcas.ContainsKey(nca.Header.TitleId)) + { + updateNcas[nca.Header.TitleId].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.ContainsKey(SystemUpdateTitleId)) + { + var ncaEntry = updateNcas[SystemUpdateTitleId]; + + 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 Nca(_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.ContainsKey(SystemVersionTitleId)) + { + string versionEntry = updateNcas[SystemVersionTitleId].Find(x => x.type != NcaContentType.Meta).path; + + using (Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry))) + { + Nca nca = new Nca(_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.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 Nca(_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 Span(contentData); + + contentStorage.Read(0, content); + + Span hash = new Span(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) + { + string extraNcas = string.Empty; + + foreach (var entry in updateNcas) + { + foreach (var nca in entry.Value) + { + extraNcas += nca.path + Environment.NewLine; + } + } + + throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}"); + } + } + else + { + throw new FileNotFoundException("System update title was not found in the firmware package."); + } + + return systemVersion; + } + + SystemVersion VerifyAndGetVersion(IFileSystem filesystem) + { + 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 Nca(_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.ContainsKey(nca.Header.TitleId)) + { + updateNcas[nca.Header.TitleId].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)) + { + var metaNcaEntry = ncaEntry.Find(x => x.type == NcaContentType.Meta); + 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, metaNcaEntry.path, OpenMode.Read).AsStorage(); + IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage(); + + Nca metaNca = new Nca(_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 Span(contentData); + + contentStorage.Read(0, content); + + Span hash = new Span(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) + { + string extraNcas = string.Empty; + + foreach (var entry in updateNcas) + { + foreach (var (type, path) in entry.Value) + { + extraNcas += path + Environment.NewLine; + } + } + + throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}"); + } + + return systemVersion; + } + + return null; + } + + public SystemVersion GetCurrentFirmwareVersion() + { + LoadEntries(); + + lock (_lock) + { + var locationEnties = _locationEntries[StorageId.BuiltInSystem]; + + foreach (var entry in locationEnties) + { + if (entry.ContentType == NcaContentType.Data) + { + var path = _virtualFileSystem.SwitchPathToSystemPath(entry.ContentPath); + + using (FileStream fileStream = File.OpenRead(path)) + { + Nca nca = new Nca(_virtualFileSystem.KeySet, fileStream.AsStorage()); + + 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()) + { + return new SystemVersion(systemVersionFile.Get.AsStream()); + } + } + + } + } + } + } + + return null; + } + } +} diff --git a/Ryujinx.HLE/FileSystem/ContentPath.cs b/Ryujinx.HLE/FileSystem/ContentPath.cs new file mode 100644 index 00000000..c8663081 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/ContentPath.cs @@ -0,0 +1,82 @@ +using LibHac.Fs; +using LibHac.Ncm; +using Ryujinx.Common.Configuration; +using System; + +using static Ryujinx.HLE.FileSystem.VirtualFileSystem; +using Path = System.IO.Path; + +namespace Ryujinx.HLE.FileSystem +{ + internal static class ContentPath + { + public const string SystemContent = "@SystemContent"; + public const string UserContent = "@UserContent"; + public const string SdCardContent = "@SdCardContent"; + public const string SdCard = "@Sdcard"; + public const string CalibFile = "@CalibFile"; + public const string Safe = "@Safe"; + public const string User = "@User"; + public const string System = "@System"; + public const string Host = "@Host"; + public const string GamecardApp = "@GcApp"; + public const string GamecardContents = "@GcS00000001"; + public const string GamecardUpdate = "@upp"; + public const string RegisteredUpdate = "@RegUpdate"; + + public const string Nintendo = "Nintendo"; + public const string Contents = "Contents"; + + public static string GetRealPath(VirtualFileSystem fileSystem, string switchContentPath) + { + return switchContentPath switch + { + SystemContent => Path.Combine(AppDataManager.BaseDirPath, SystemNandPath, Contents), + UserContent => Path.Combine(AppDataManager.BaseDirPath, UserNandPath, Contents), + SdCardContent => Path.Combine(fileSystem.GetSdCardPath(), Nintendo, Contents), + System => Path.Combine(AppDataManager.BaseDirPath, SystemNandPath), + User => Path.Combine(AppDataManager.BaseDirPath, UserNandPath), + _ => throw new NotSupportedException($"Content Path \"`{switchContentPath}`\" is not supported.") + }; + } + + public static string GetContentPath(ContentStorageId contentStorageId) + { + return contentStorageId switch + { + ContentStorageId.System => SystemContent, + ContentStorageId.User => UserContent, + ContentStorageId.SdCard => SdCardContent, + _ => throw new NotSupportedException($"Content Storage Id \"`{contentStorageId}`\" is not supported.") + }; + } + + public static string GetContentPath(StorageId storageId) + { + return storageId switch + { + StorageId.BuiltInSystem => SystemContent, + StorageId.BuiltInUser => UserContent, + StorageId.SdCard => SdCardContent, + _ => throw new NotSupportedException($"Storage Id \"`{storageId}`\" is not supported.") + }; + } + + public static StorageId GetStorageId(string contentPathString) + { + return contentPathString.Split(':')[0] switch + { + SystemContent or + System => StorageId.BuiltInSystem, + UserContent or + User => StorageId.BuiltInUser, + SdCardContent => StorageId.SdCard, + Host => StorageId.Host, + GamecardApp or + GamecardContents or + GamecardUpdate => StorageId.GameCard, + _ => StorageId.None + }; + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/EncryptedFileSystemCreator.cs b/Ryujinx.HLE/FileSystem/EncryptedFileSystemCreator.cs index 60c3cfdb..f32dc2d7 100644 --- a/Ryujinx.HLE/FileSystem/EncryptedFileSystemCreator.cs +++ b/Ryujinx.HLE/FileSystem/EncryptedFileSystemCreator.cs @@ -8,7 +8,6 @@ namespace Ryujinx.HLE.FileSystem { public class EncryptedFileSystemCreator : IEncryptedFileSystemCreator { - public Result Create(ref SharedRef outEncryptedFileSystem, ref SharedRef baseFileSystem, IEncryptedFileSystemCreator.KeyId idIndex, in EncryptionSeed encryptionSeed) @@ -18,10 +17,10 @@ namespace Ryujinx.HLE.FileSystem return ResultFs.InvalidArgument.Log(); } - // Todo: Reenable when AesXtsFileSystem is fixed + // TODO: Reenable when AesXtsFileSystem is fixed. outEncryptedFileSystem = SharedRef.CreateMove(ref baseFileSystem); return Result.Success; } } -} +} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/LocationEntry.cs b/Ryujinx.HLE/FileSystem/LocationEntry.cs new file mode 100644 index 00000000..45cbc8cd --- /dev/null +++ b/Ryujinx.HLE/FileSystem/LocationEntry.cs @@ -0,0 +1,25 @@ +using LibHac.FsSystem; + +namespace Ryujinx.HLE.FileSystem +{ + public struct LocationEntry + { + public string ContentPath { get; private set; } + public int Flag { get; private set; } + public ulong TitleId { get; private set; } + public NcaContentType ContentType { get; private set; } + + public LocationEntry(string contentPath, int flag, ulong titleId, NcaContentType contentType) + { + ContentPath = contentPath; + Flag = flag; + TitleId = titleId; + ContentType = contentType; + } + + public void SetFlag(int flag) + { + Flag = flag; + } + } +} diff --git a/Ryujinx.HLE/FileSystem/SaveDataType.cs b/Ryujinx.HLE/FileSystem/SaveDataType.cs deleted file mode 100644 index 2207fc23..00000000 --- a/Ryujinx.HLE/FileSystem/SaveDataType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ryujinx.HLE.FileSystem -{ - enum SaveDataType : byte - { - SystemSaveData, - SaveData, - BcatDeliveryCacheStorage, - DeviceSaveData, - TemporaryStorage, - CacheStorage - } -} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/SaveInfo.cs b/Ryujinx.HLE/FileSystem/SaveInfo.cs deleted file mode 100644 index 96f2f020..00000000 --- a/Ryujinx.HLE/FileSystem/SaveInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Ryujinx.HLE.HOS.Services.Account.Acc; - -namespace Ryujinx.HLE.FileSystem -{ - struct SaveInfo - { - public ulong TitleId { get; private set; } - public long SaveId { get; private set; } - public SaveDataType SaveDataType { get; private set; } - public SaveSpaceId SaveSpaceId { get; private set; } - public UserId UserId { get; private set; } - - public SaveInfo( - ulong titleId, - long saveId, - SaveDataType saveDataType, - SaveSpaceId saveSpaceId, - UserId userId = new UserId()) - { - TitleId = titleId; - SaveId = saveId; - SaveDataType = saveDataType; - SaveSpaceId = saveSpaceId; - UserId = userId; - } - } -} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/SaveSpaceId.cs b/Ryujinx.HLE/FileSystem/SaveSpaceId.cs deleted file mode 100644 index d51922df..00000000 --- a/Ryujinx.HLE/FileSystem/SaveSpaceId.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ryujinx.HLE.FileSystem -{ - enum SaveSpaceId - { - NandSystem, - NandUser, - SdCard, - TemporaryStorage - } -} diff --git a/Ryujinx.HLE/FileSystem/StorageId.cs b/Ryujinx.HLE/FileSystem/StorageId.cs deleted file mode 100644 index d4043e2c..00000000 --- a/Ryujinx.HLE/FileSystem/StorageId.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ryujinx.HLE.FileSystem -{ - public enum StorageId - { - None, - Host, - GameCard, - NandSystem, - NandUser, - SdCard - } -} diff --git a/Ryujinx.HLE/FileSystem/SystemVersion.cs b/Ryujinx.HLE/FileSystem/SystemVersion.cs new file mode 100644 index 00000000..a7926d5d --- /dev/null +++ b/Ryujinx.HLE/FileSystem/SystemVersion.cs @@ -0,0 +1,40 @@ +using Ryujinx.HLE.Utilities; +using System.IO; + +namespace Ryujinx.HLE.FileSystem +{ + public class SystemVersion + { + public byte Major { get; } + public byte Minor { get; } + public byte Micro { get; } + public byte RevisionMajor { get; } + public byte RevisionMinor { get; } + public string PlatformString { get; } + public string Hex { get; } + public string VersionString { get; } + public string VersionTitle { get; } + + public SystemVersion(Stream systemVersionFile) + { + using (BinaryReader reader = new BinaryReader(systemVersionFile)) + { + Major = reader.ReadByte(); + Minor = reader.ReadByte(); + Micro = reader.ReadByte(); + + reader.ReadByte(); // Padding + + RevisionMajor = reader.ReadByte(); + RevisionMinor = reader.ReadByte(); + + reader.ReadBytes(2); // Padding + + PlatformString = StringUtils.ReadInlinedAsciiString(reader, 0x20); + Hex = StringUtils.ReadInlinedAsciiString(reader, 0x40); + VersionString = StringUtils.ReadInlinedAsciiString(reader, 0x18); + VersionTitle = StringUtils.ReadInlinedAsciiString(reader, 0x80); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index 9359b03c..001a1f5f 100644 --- a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -13,7 +13,6 @@ using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; -using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.HOS; using System; using System.Buffers.Text; @@ -28,20 +27,29 @@ namespace Ryujinx.HLE.FileSystem { public class VirtualFileSystem : IDisposable { - public const string NandPath = AppDataManager.DefaultNandDir; - public const string SdCardPath = AppDataManager.DefaultSdcardDir; + public static string SafeNandPath = Path.Combine(AppDataManager.DefaultNandDir, "safe"); + public static string SystemNandPath = Path.Combine(AppDataManager.DefaultNandDir, "system"); + public static string UserNandPath = Path.Combine(AppDataManager.DefaultNandDir, "user"); - public static string SafeNandPath = Path.Combine(NandPath, "safe"); - public static string SystemNandPath = Path.Combine(NandPath, "system"); - public static string UserNandPath = Path.Combine(NandPath, "user"); + public KeySet KeySet { get; private set; } + public EmulatedGameCard GameCard { get; private set; } + public EmulatedSdCard SdCard { get; private set; } + public ModLoader ModLoader { get; private set; } + public Stream RomFs { get; private set; } private static bool _isInitialized = false; - public KeySet KeySet { get; private set; } - public EmulatedGameCard GameCard { get; private set; } - public EmulatedSdCard SdCard { get; private set; } + public static VirtualFileSystem CreateInstance() + { + if (_isInitialized) + { + throw new InvalidOperationException("VirtualFileSystem can only be instantiated once!"); + } - public ModLoader ModLoader { get; private set; } + _isInitialized = true; + + return new VirtualFileSystem(); + } private VirtualFileSystem() { @@ -49,8 +57,6 @@ namespace Ryujinx.HLE.FileSystem ModLoader = new ModLoader(); // Should only be created once } - public Stream RomFs { get; private set; } - public void LoadRomFs(string fileName) { RomFs = new FileStream(fileName, FileMode.Open, FileAccess.Read); @@ -79,7 +85,7 @@ namespace Ryujinx.HLE.FileSystem string fullPath = Path.GetFullPath(Path.Combine(basePath, fileName)); - if (!fullPath.StartsWith(GetBasePath())) + if (!fullPath.StartsWith(AppDataManager.BaseDirPath)) { return null; } @@ -87,14 +93,8 @@ namespace Ryujinx.HLE.FileSystem return fullPath; } - internal string GetBasePath() => AppDataManager.BaseDirPath; - internal string GetSdCardPath() => MakeFullPath(SdCardPath); - public string GetNandPath() => MakeFullPath(NandPath); - - public string GetFullPartitionPath(string partitionPath) - { - return MakeFullPath(partitionPath); - } + internal string GetSdCardPath() => MakeFullPath(AppDataManager.DefaultSdcardDir); + public string GetNandPath() => MakeFullPath(AppDataManager.DefaultNandDir); public string SwitchPathToSystemPath(string switchPath) { @@ -110,7 +110,7 @@ namespace Ryujinx.HLE.FileSystem public string SystemPathToSwitchPath(string systemPath) { - string baseSystemPath = GetBasePath() + Path.DirectorySeparatorChar; + string baseSystemPath = AppDataManager.BaseDirPath + Path.DirectorySeparatorChar; if (systemPath.StartsWith(baseSystemPath)) { @@ -136,8 +136,7 @@ namespace Ryujinx.HLE.FileSystem switch (path) { case ContentPath.SdCard: - case "@Sdcard": - path = SdCardPath; + path = AppDataManager.DefaultSdcardDir; break; case ContentPath.User: path = UserNandPath; @@ -146,7 +145,7 @@ namespace Ryujinx.HLE.FileSystem path = SystemNandPath; break; case ContentPath.SdCardContent: - path = Path.Combine(SdCardPath, "Nintendo", "Contents"); + path = Path.Combine(AppDataManager.DefaultSdcardDir, "Nintendo", "Contents"); break; case ContentPath.UserContent: path = Path.Combine(UserNandPath, "Contents"); @@ -156,27 +155,19 @@ namespace Ryujinx.HLE.FileSystem break; } - string fullPath = Path.Combine(GetBasePath(), path); + string fullPath = Path.Combine(AppDataManager.BaseDirPath, path); - if (isDirectory) + if (isDirectory && !Directory.Exists(fullPath)) { - if (!Directory.Exists(fullPath)) - { - Directory.CreateDirectory(fullPath); - } + Directory.CreateDirectory(fullPath); } return fullPath; } - public DriveInfo GetDrive() - { - return new DriveInfo(Path.GetPathRoot(GetBasePath())); - } - public void InitializeFsServer(LibHac.Horizon horizon, out HorizonClient fsServerClient) { - LocalFileSystem serverBaseFs = new LocalFileSystem(GetBasePath()); + LocalFileSystem serverBaseFs = new LocalFileSystem(AppDataManager.BaseDirPath); fsServerClient = horizon.CreatePrivilegedHorizonClient(); var fsServer = new FileSystemServer(fsServerClient); @@ -505,7 +496,7 @@ namespace Ryujinx.HLE.FileSystem bool canFixBySaveDataId = extraData.Attribute.StaticSaveDataId == 0 && info.StaticSaveDataId != 0; - bool hasEmptyOwnerId = extraData.OwnerId == 0 && info.Type != LibHac.Fs.SaveDataType.System; + bool hasEmptyOwnerId = extraData.OwnerId == 0 && info.Type != SaveDataType.System; if (!canFixByProgramId && !canFixBySaveDataId && !hasEmptyOwnerId) { @@ -523,7 +514,7 @@ namespace Ryujinx.HLE.FileSystem // The rest of the extra data can't be created from the save data info. // On user saves the owner ID will almost certainly be the same as the program ID. - if (info.Type != LibHac.Fs.SaveDataType.System) + if (info.Type != SaveDataType.System) { extraData.OwnerId = info.ProgramId.Value; } @@ -580,11 +571,6 @@ namespace Ryujinx.HLE.FileSystem } }; - public void Unload() - { - RomFs?.Dispose(); - } - public void Dispose() { Dispose(true); @@ -594,20 +580,8 @@ namespace Ryujinx.HLE.FileSystem { if (disposing) { - Unload(); + RomFs?.Dispose(); } } - - public static VirtualFileSystem CreateInstance() - { - if (_isInitialized) - { - throw new InvalidOperationException("VirtualFileSystem can only be instantiated once!"); - } - - _isInitialized = true; - - return new VirtualFileSystem(); - } } } \ No newline at end of file -- cgit v1.2.3