/ src / Ryujinx.HLE / FileSystem / ContentManager.cs
ContentManager.cs
  1  using LibHac.Common;
  2  using LibHac.Common.Keys;
  3  using LibHac.Fs;
  4  using LibHac.Fs.Fsa;
  5  using LibHac.FsSystem;
  6  using LibHac.Ncm;
  7  using LibHac.Tools.Fs;
  8  using LibHac.Tools.FsSystem;
  9  using LibHac.Tools.FsSystem.NcaUtils;
 10  using LibHac.Tools.Ncm;
 11  using Ryujinx.Common.Logging;
 12  using Ryujinx.Common.Memory;
 13  using Ryujinx.Common.Utilities;
 14  using Ryujinx.HLE.Exceptions;
 15  using Ryujinx.HLE.HOS.Services.Ssl;
 16  using Ryujinx.HLE.HOS.Services.Time;
 17  using Ryujinx.HLE.Utilities;
 18  using System;
 19  using System.Collections.Generic;
 20  using System.IO;
 21  using System.IO.Compression;
 22  using System.Linq;
 23  using System.Text;
 24  using Path = System.IO.Path;
 25  
 26  namespace Ryujinx.HLE.FileSystem
 27  {
 28      public class ContentManager
 29      {
 30          private const ulong SystemVersionTitleId = 0x0100000000000809;
 31          private const ulong SystemUpdateTitleId = 0x0100000000000816;
 32  
 33          private Dictionary<StorageId, LinkedList<LocationEntry>> _locationEntries;
 34  
 35          private readonly Dictionary<string, ulong> _sharedFontTitleDictionary;
 36          private readonly Dictionary<ulong, string> _systemTitlesNameDictionary;
 37          private readonly Dictionary<string, string> _sharedFontFilenameDictionary;
 38  
 39          private SortedDictionary<(ulong titleId, NcaContentType type), string> _contentDictionary;
 40  
 41          private readonly struct AocItem
 42          {
 43              public readonly string ContainerPath;
 44              public readonly string NcaPath;
 45  
 46              public AocItem(string containerPath, string ncaPath)
 47              {
 48                  ContainerPath = containerPath;
 49                  NcaPath = ncaPath;
 50              }
 51          }
 52  
 53          private SortedList<ulong, AocItem> AocData { get; }
 54  
 55          private readonly VirtualFileSystem _virtualFileSystem;
 56  
 57          private readonly object _lock = new();
 58  
 59          public ContentManager(VirtualFileSystem virtualFileSystem)
 60          {
 61              _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>();
 62              _locationEntries = new Dictionary<StorageId, LinkedList<LocationEntry>>();
 63  
 64              _sharedFontTitleDictionary = new Dictionary<string, ulong>
 65              {
 66                  { "FontStandard",                  0x0100000000000811 },
 67                  { "FontChineseSimplified",         0x0100000000000814 },
 68                  { "FontExtendedChineseSimplified", 0x0100000000000814 },
 69                  { "FontKorean",                    0x0100000000000812 },
 70                  { "FontChineseTraditional",        0x0100000000000813 },
 71                  { "FontNintendoExtended",          0x0100000000000810 },
 72              };
 73  
 74              _systemTitlesNameDictionary = new Dictionary<ulong, string>()
 75              {
 76                  { 0x010000000000080E, "TimeZoneBinary"         },
 77                  { 0x0100000000000810, "FontNintendoExtension"  },
 78                  { 0x0100000000000811, "FontStandard"           },
 79                  { 0x0100000000000812, "FontKorean"             },
 80                  { 0x0100000000000813, "FontChineseTraditional" },
 81                  { 0x0100000000000814, "FontChineseSimple"      },
 82              };
 83  
 84              _sharedFontFilenameDictionary = new Dictionary<string, string>
 85              {
 86                  { "FontStandard",                  "nintendo_udsg-r_std_003.bfttf" },
 87                  { "FontChineseSimplified",         "nintendo_udsg-r_org_zh-cn_003.bfttf" },
 88                  { "FontExtendedChineseSimplified", "nintendo_udsg-r_ext_zh-cn_003.bfttf" },
 89                  { "FontKorean",                    "nintendo_udsg-r_ko_003.bfttf" },
 90                  { "FontChineseTraditional",        "nintendo_udjxh-db_zh-tw_003.bfttf" },
 91                  { "FontNintendoExtended",          "nintendo_ext_003.bfttf" },
 92              };
 93  
 94              _virtualFileSystem = virtualFileSystem;
 95  
 96              AocData = new SortedList<ulong, AocItem>();
 97          }
 98  
 99          public void LoadEntries(Switch device = null)
100          {
101              lock (_lock)
102              {
103                  _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>();
104                  _locationEntries = new Dictionary<StorageId, LinkedList<LocationEntry>>();
105  
106                  foreach (StorageId storageId in Enum.GetValues<StorageId>())
107                  {
108                      if (!ContentPath.TryGetContentPath(storageId, out var contentPathString))
109                      {
110                          continue;
111                      }
112                      if (!ContentPath.TryGetRealPath(contentPathString, out var contentDirectory))
113                      {
114                          continue;
115                      }
116                      var registeredDirectory = Path.Combine(contentDirectory, "registered");
117  
118                      Directory.CreateDirectory(registeredDirectory);
119  
120                      LinkedList<LocationEntry> locationList = new();
121  
122                      void AddEntry(LocationEntry entry)
123                      {
124                          locationList.AddLast(entry);
125                      }
126  
127                      foreach (string directoryPath in Directory.EnumerateDirectories(registeredDirectory))
128                      {
129                          if (Directory.GetFiles(directoryPath).Length > 0)
130                          {
131                              string ncaName = new DirectoryInfo(directoryPath).Name.Replace(".nca", string.Empty);
132  
133                              using FileStream ncaFile = File.OpenRead(Directory.GetFiles(directoryPath)[0]);
134                              Nca nca = new(_virtualFileSystem.KeySet, ncaFile.AsStorage());
135  
136                              string switchPath = contentPathString + ":/" + ncaFile.Name.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar);
137  
138                              // Change path format to switch's
139                              switchPath = switchPath.Replace('\\', '/');
140  
141                              LocationEntry entry = new(switchPath, 0, nca.Header.TitleId, nca.Header.ContentType);
142  
143                              AddEntry(entry);
144  
145                              _contentDictionary.Add((nca.Header.TitleId, nca.Header.ContentType), ncaName);
146                          }
147                      }
148  
149                      foreach (string filePath in Directory.EnumerateFiles(contentDirectory))
150                      {
151                          if (Path.GetExtension(filePath) == ".nca")
152                          {
153                              string ncaName = Path.GetFileNameWithoutExtension(filePath);
154  
155                              using FileStream ncaFile = new(filePath, FileMode.Open, FileAccess.Read);
156                              Nca nca = new(_virtualFileSystem.KeySet, ncaFile.AsStorage());
157  
158                              string switchPath = contentPathString + ":/" + filePath.Replace(contentDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar);
159  
160                              // Change path format to switch's
161                              switchPath = switchPath.Replace('\\', '/');
162  
163                              LocationEntry entry = new(switchPath, 0, nca.Header.TitleId, nca.Header.ContentType);
164  
165                              AddEntry(entry);
166  
167                              _contentDictionary.Add((nca.Header.TitleId, nca.Header.ContentType), ncaName);
168                          }
169                      }
170  
171                      if (_locationEntries.TryGetValue(storageId, out var locationEntriesItem) && locationEntriesItem?.Count == 0)
172                      {
173                          _locationEntries.Remove(storageId);
174                      }
175  
176                      _locationEntries.TryAdd(storageId, locationList);
177                  }
178  
179                  if (device != null)
180                  {
181                      TimeManager.Instance.InitializeTimeZone(device);
182                      BuiltInCertificateManager.Instance.Initialize(device);
183                      device.System.SharedFontManager.Initialize();
184                  }
185              }
186          }
187  
188          public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool mergedToContainer = false)
189          {
190              // TODO: Check Aoc version.
191              if (!AocData.TryAdd(titleId, new AocItem(containerPath, ncaPath)))
192              {
193                  Logger.Warning?.Print(LogClass.Application, $"Duplicate AddOnContent detected. TitleId {titleId:X16}");
194              }
195              else
196              {
197                  Logger.Info?.Print(LogClass.Application, $"Found AddOnContent with TitleId {titleId:X16}");
198  
199                  if (!mergedToContainer)
200                  {
201                      using var pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(containerPath, _virtualFileSystem);
202                  }
203              }
204          }
205  
206          public void ClearAocData() => AocData.Clear();
207  
208          public int GetAocCount() => AocData.Count;
209  
210          public IList<ulong> GetAocTitleIds() => AocData.Select(e => e.Key).ToList();
211  
212          public bool GetAocDataStorage(ulong aocTitleId, out IStorage aocStorage, IntegrityCheckLevel integrityCheckLevel)
213          {
214              aocStorage = null;
215  
216              if (AocData.TryGetValue(aocTitleId, out AocItem aoc))
217              {
218                  var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read);
219                  using var ncaFile = new UniqueRef<IFile>();
220  
221                  switch (Path.GetExtension(aoc.ContainerPath))
222                  {
223                      case ".xci":
224                          var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
225                          xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
226                          break;
227                      case ".nsp":
228                          var pfs = new PartitionFileSystem();
229                          pfs.Initialize(file.AsStorage());
230                          pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
231                          break;
232                      default:
233                          return false; // Print error?
234                  }
235  
236                  aocStorage = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()).OpenStorage(NcaSectionType.Data, integrityCheckLevel);
237  
238                  return true;
239              }
240  
241              return false;
242          }
243  
244          public void ClearEntry(ulong titleId, NcaContentType contentType, StorageId storageId)
245          {
246              lock (_lock)
247              {
248                  RemoveLocationEntry(titleId, contentType, storageId);
249              }
250          }
251  
252          public void RefreshEntries(StorageId storageId, int flag)
253          {
254              lock (_lock)
255              {
256                  LinkedList<LocationEntry> locationList = _locationEntries[storageId];
257                  LinkedListNode<LocationEntry> locationEntry = locationList.First;
258  
259                  while (locationEntry != null)
260                  {
261                      LinkedListNode<LocationEntry> nextLocationEntry = locationEntry.Next;
262  
263                      if (locationEntry.Value.Flag == flag)
264                      {
265                          locationList.Remove(locationEntry.Value);
266                      }
267  
268                      locationEntry = nextLocationEntry;
269                  }
270              }
271          }
272  
273          public bool HasNca(string ncaId, StorageId storageId)
274          {
275              lock (_lock)
276              {
277                  if (_contentDictionary.ContainsValue(ncaId))
278                  {
279                      var content = _contentDictionary.FirstOrDefault(x => x.Value == ncaId);
280                      ulong titleId = content.Key.titleId;
281  
282                      NcaContentType contentType = content.Key.type;
283                      StorageId storage = GetInstalledStorage(titleId, contentType, storageId);
284  
285                      return storage == storageId;
286                  }
287              }
288  
289              return false;
290          }
291  
292          public UInt128 GetInstalledNcaId(ulong titleId, NcaContentType contentType)
293          {
294              lock (_lock)
295              {
296                  if (_contentDictionary.TryGetValue((titleId, contentType), out var contentDictionaryItem))
297                  {
298                      return UInt128Utils.FromHex(contentDictionaryItem);
299                  }
300              }
301  
302              return new UInt128();
303          }
304  
305          public StorageId GetInstalledStorage(ulong titleId, NcaContentType contentType, StorageId storageId)
306          {
307              lock (_lock)
308              {
309                  LocationEntry locationEntry = GetLocation(titleId, contentType, storageId);
310  
311                  return locationEntry.ContentPath != null ? ContentPath.GetStorageId(locationEntry.ContentPath) : StorageId.None;
312              }
313          }
314  
315          public string GetInstalledContentPath(ulong titleId, StorageId storageId, NcaContentType contentType)
316          {
317              lock (_lock)
318              {
319                  LocationEntry locationEntry = GetLocation(titleId, contentType, storageId);
320  
321                  if (VerifyContentType(locationEntry, contentType))
322                  {
323                      return locationEntry.ContentPath;
324                  }
325              }
326  
327              return string.Empty;
328          }
329  
330          public void RedirectLocation(LocationEntry newEntry, StorageId storageId)
331          {
332              lock (_lock)
333              {
334                  LocationEntry locationEntry = GetLocation(newEntry.TitleId, newEntry.ContentType, storageId);
335  
336                  if (locationEntry.ContentPath != null)
337                  {
338                      RemoveLocationEntry(newEntry.TitleId, newEntry.ContentType, storageId);
339                  }
340  
341                  AddLocationEntry(newEntry, storageId);
342              }
343          }
344  
345          private bool VerifyContentType(LocationEntry locationEntry, NcaContentType contentType)
346          {
347              if (locationEntry.ContentPath == null)
348              {
349                  return false;
350              }
351  
352              string installedPath = VirtualFileSystem.SwitchPathToSystemPath(locationEntry.ContentPath);
353  
354              if (!string.IsNullOrWhiteSpace(installedPath))
355              {
356                  if (File.Exists(installedPath))
357                  {
358                      using FileStream file = new(installedPath, FileMode.Open, FileAccess.Read);
359                      Nca nca = new(_virtualFileSystem.KeySet, file.AsStorage());
360                      bool contentCheck = nca.Header.ContentType == contentType;
361  
362                      return contentCheck;
363                  }
364              }
365  
366              return false;
367          }
368  
369          private void AddLocationEntry(LocationEntry entry, StorageId storageId)
370          {
371              LinkedList<LocationEntry> locationList = null;
372  
373              if (_locationEntries.TryGetValue(storageId, out LinkedList<LocationEntry> locationEntry))
374              {
375                  locationList = locationEntry;
376              }
377  
378              if (locationList != null)
379              {
380                  locationList.Remove(entry);
381  
382                  locationList.AddLast(entry);
383              }
384          }
385  
386          private void RemoveLocationEntry(ulong titleId, NcaContentType contentType, StorageId storageId)
387          {
388              LinkedList<LocationEntry> locationList = null;
389  
390              if (_locationEntries.TryGetValue(storageId, out LinkedList<LocationEntry> locationEntry))
391              {
392                  locationList = locationEntry;
393              }
394  
395              if (locationList != null)
396              {
397                  LocationEntry entry =
398                      locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType);
399  
400                  if (entry.ContentPath != null)
401                  {
402                      locationList.Remove(entry);
403                  }
404              }
405          }
406  
407          public bool TryGetFontTitle(string fontName, out ulong titleId)
408          {
409              return _sharedFontTitleDictionary.TryGetValue(fontName, out titleId);
410          }
411  
412          public bool TryGetFontFilename(string fontName, out string filename)
413          {
414              return _sharedFontFilenameDictionary.TryGetValue(fontName, out filename);
415          }
416  
417          public bool TryGetSystemTitlesName(ulong titleId, out string name)
418          {
419              return _systemTitlesNameDictionary.TryGetValue(titleId, out name);
420          }
421  
422          private LocationEntry GetLocation(ulong titleId, NcaContentType contentType, StorageId storageId)
423          {
424              LinkedList<LocationEntry> locationList = _locationEntries[storageId];
425  
426              return locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType);
427          }
428  
429          public void InstallFirmware(string firmwareSource)
430          {
431              ContentPath.TryGetContentPath(StorageId.BuiltInSystem, out var contentPathString);
432              ContentPath.TryGetRealPath(contentPathString, out var contentDirectory);
433              string registeredDirectory = Path.Combine(contentDirectory, "registered");
434              string temporaryDirectory = Path.Combine(contentDirectory, "temp");
435  
436              if (Directory.Exists(temporaryDirectory))
437              {
438                  Directory.Delete(temporaryDirectory, true);
439              }
440  
441              if (Directory.Exists(firmwareSource))
442              {
443                  InstallFromDirectory(firmwareSource, temporaryDirectory);
444                  FinishInstallation(temporaryDirectory, registeredDirectory);
445  
446                  return;
447              }
448  
449              if (!File.Exists(firmwareSource))
450              {
451                  throw new FileNotFoundException("Firmware file does not exist.");
452              }
453  
454              FileInfo info = new(firmwareSource);
455  
456              using FileStream file = File.OpenRead(firmwareSource);
457  
458              switch (info.Extension)
459              {
460                  case ".zip":
461                      using (ZipArchive archive = ZipFile.OpenRead(firmwareSource))
462                      {
463                          InstallFromZip(archive, temporaryDirectory);
464                      }
465                      break;
466                  case ".xci":
467                      Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
468                      InstallFromCart(xci, temporaryDirectory);
469                      break;
470                  default:
471                      throw new InvalidFirmwarePackageException("Input file is not a valid firmware package");
472              }
473  
474              FinishInstallation(temporaryDirectory, registeredDirectory);
475          }
476  
477          private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
478          {
479              if (Directory.Exists(registeredDirectory))
480              {
481                  new DirectoryInfo(registeredDirectory).Delete(true);
482              }
483  
484              Directory.Move(temporaryDirectory, registeredDirectory);
485  
486              LoadEntries();
487          }
488  
489          private void InstallFromDirectory(string firmwareDirectory, string temporaryDirectory)
490          {
491              InstallFromPartition(new LocalFileSystem(firmwareDirectory), temporaryDirectory);
492          }
493  
494          private void InstallFromPartition(IFileSystem filesystem, string temporaryDirectory)
495          {
496              foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
497              {
498                  Nca nca = new(_virtualFileSystem.KeySet, OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage());
499  
500                  SaveNca(nca, entry.Name.Remove(entry.Name.IndexOf('.')), temporaryDirectory);
501              }
502          }
503  
504          private void InstallFromCart(Xci gameCard, string temporaryDirectory)
505          {
506              if (gameCard.HasPartition(XciPartitionType.Update))
507              {
508                  XciPartition partition = gameCard.OpenPartition(XciPartitionType.Update);
509  
510                  InstallFromPartition(partition, temporaryDirectory);
511              }
512              else
513              {
514                  throw new Exception("Update not found in xci file.");
515              }
516          }
517  
518          private static void InstallFromZip(ZipArchive archive, string temporaryDirectory)
519          {
520              foreach (var entry in archive.Entries)
521              {
522                  if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
523                  {
524                      // Clean up the name and get the NcaId
525  
526                      string[] pathComponents = entry.FullName.Replace(".cnmt", "").Split('/');
527  
528                      string ncaId = pathComponents[^1];
529  
530                      // If this is a fragmented nca, we need to get the previous element.GetZip
531                      if (ncaId.Equals("00"))
532                      {
533                          ncaId = pathComponents[^2];
534                      }
535  
536                      if (ncaId.Contains(".nca"))
537                      {
538                          string newPath = Path.Combine(temporaryDirectory, ncaId);
539  
540                          Directory.CreateDirectory(newPath);
541  
542                          entry.ExtractToFile(Path.Combine(newPath, "00"));
543                      }
544                  }
545              }
546          }
547  
548          public static void SaveNca(Nca nca, string ncaId, string temporaryDirectory)
549          {
550              string newPath = Path.Combine(temporaryDirectory, ncaId + ".nca");
551  
552              Directory.CreateDirectory(newPath);
553  
554              using FileStream file = File.Create(Path.Combine(newPath, "00"));
555              nca.BaseStorage.AsStream().CopyTo(file);
556          }
557  
558          private static IFile OpenPossibleFragmentedFile(IFileSystem filesystem, string path, OpenMode mode)
559          {
560              using var file = new UniqueRef<IFile>();
561  
562              if (filesystem.FileExists($"{path}/00"))
563              {
564                  filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode).ThrowIfFailure();
565              }
566              else
567              {
568                  filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode).ThrowIfFailure();
569              }
570  
571              return file.Release();
572          }
573  
574          private static Stream GetZipStream(ZipArchiveEntry entry)
575          {
576              MemoryStream dest = MemoryStreamManager.Shared.GetStream();
577  
578              using Stream src = entry.Open();
579              src.CopyTo(dest);
580  
581              return dest;
582          }
583  
584          public SystemVersion VerifyFirmwarePackage(string firmwarePackage)
585          {
586              _virtualFileSystem.ReloadKeySet();
587  
588              // LibHac.NcaHeader's DecryptHeader doesn't check if HeaderKey is empty and throws InvalidDataException instead
589              // So, we check it early for a better user experience.
590              if (_virtualFileSystem.KeySet.HeaderKey.IsZeros())
591              {
592                  throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
593              }
594  
595              Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
596  
597              if (Directory.Exists(firmwarePackage))
598              {
599                  return VerifyAndGetVersionDirectory(firmwarePackage);
600              }
601  
602              if (!File.Exists(firmwarePackage))
603              {
604                  throw new FileNotFoundException("Firmware file does not exist.");
605              }
606  
607              FileInfo info = new(firmwarePackage);
608  
609              using FileStream file = File.OpenRead(firmwarePackage);
610  
611              switch (info.Extension)
612              {
613                  case ".zip":
614                      using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage))
615                      {
616                          return VerifyAndGetVersionZip(archive);
617                      }
618                  case ".xci":
619                      Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
620  
621                      if (xci.HasPartition(XciPartitionType.Update))
622                      {
623                          XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
624  
625                          return VerifyAndGetVersion(partition);
626                      }
627                      else
628                      {
629                          throw new InvalidFirmwarePackageException("Update not found in xci file.");
630                      }
631                  default:
632                      break;
633              }
634  
635              SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
636              {
637                  return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
638              }
639  
640              SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
641              {
642                  SystemVersion systemVersion = null;
643  
644                  foreach (var entry in archive.Entries)
645                  {
646                      if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
647                      {
648                          using Stream ncaStream = GetZipStream(entry);
649                          IStorage storage = ncaStream.AsStorage();
650  
651                          Nca nca = new(_virtualFileSystem.KeySet, storage);
652  
653                          if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
654                          {
655                              updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
656                          }
657                          else
658                          {
659                              updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
660                              updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
661                          }
662                      }
663                  }
664  
665                  if (updateNcas.TryGetValue(SystemUpdateTitleId, out var ncaEntry))
666                  {
667                      string metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
668  
669                      CnmtContentMetaEntry[] metaEntries = null;
670  
671                      var fileEntry = archive.GetEntry(metaPath);
672  
673                      using (Stream ncaStream = GetZipStream(fileEntry))
674                      {
675                          Nca metaNca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
676  
677                          IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
678  
679                          string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
680  
681                          using var metaFile = new UniqueRef<IFile>();
682  
683                          if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
684                          {
685                              var meta = new Cnmt(metaFile.Get.AsStream());
686  
687                              if (meta.Type == ContentMetaType.SystemUpdate)
688                              {
689                                  metaEntries = meta.MetaEntries;
690  
691                                  updateNcas.Remove(SystemUpdateTitleId);
692                              }
693                          }
694                      }
695  
696                      if (metaEntries == null)
697                      {
698                          throw new FileNotFoundException("System update title was not found in the firmware package.");
699                      }
700  
701                      if (updateNcas.TryGetValue(SystemVersionTitleId, out var updateNcasItem))
702                      {
703                          string versionEntry = updateNcasItem.Find(x => x.type != NcaContentType.Meta).path;
704  
705                          using Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry));
706                          Nca nca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
707  
708                          var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
709  
710                          using var systemVersionFile = new UniqueRef<IFile>();
711  
712                          if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
713                          {
714                              systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
715                          }
716                      }
717  
718                      foreach (CnmtContentMetaEntry metaEntry in metaEntries)
719                      {
720                          if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry))
721                          {
722                              metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
723  
724                              string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
725  
726                              // Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
727                              // This is a perfect valid case, so we should just ignore the missing content nca and continue.
728                              if (contentPath == null)
729                              {
730                                  updateNcas.Remove(metaEntry.TitleId);
731  
732                                  continue;
733                              }
734  
735                              ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath);
736                              ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath);
737  
738                              using Stream metaNcaStream = GetZipStream(metaZipEntry);
739                              using Stream contentNcaStream = GetZipStream(contentZipEntry);
740                              Nca metaNca = new(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
741  
742                              IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
743  
744                              string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
745  
746                              using var metaFile = new UniqueRef<IFile>();
747  
748                              if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
749                              {
750                                  var meta = new Cnmt(metaFile.Get.AsStream());
751  
752                                  IStorage contentStorage = contentNcaStream.AsStorage();
753                                  if (contentStorage.GetSize(out long size).IsSuccess())
754                                  {
755                                      byte[] contentData = new byte[size];
756  
757                                      Span<byte> content = new(contentData);
758  
759                                      contentStorage.Read(0, content);
760  
761                                      Span<byte> hash = new(new byte[32]);
762  
763                                      LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
764  
765                                      if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
766                                      {
767                                          updateNcas.Remove(metaEntry.TitleId);
768                                      }
769                                  }
770                              }
771                          }
772                      }
773  
774                      if (updateNcas.Count > 0)
775                      {
776                          StringBuilder extraNcas = new();
777  
778                          foreach (var entry in updateNcas)
779                          {
780                              foreach (var (type, path) in entry.Value)
781                              {
782                                  extraNcas.AppendLine(path);
783                              }
784                          }
785  
786                          throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
787                      }
788                  }
789                  else
790                  {
791                      throw new FileNotFoundException("System update title was not found in the firmware package.");
792                  }
793  
794                  return systemVersion;
795              }
796  
797              SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
798              {
799                  SystemVersion systemVersion = null;
800  
801                  CnmtContentMetaEntry[] metaEntries = null;
802  
803                  foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
804                  {
805                      IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
806  
807                      Nca nca = new(_virtualFileSystem.KeySet, ncaStorage);
808  
809                      if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
810                      {
811                          IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
812  
813                          string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
814  
815                          using var metaFile = new UniqueRef<IFile>();
816  
817                          if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
818                          {
819                              var meta = new Cnmt(metaFile.Get.AsStream());
820  
821                              if (meta.Type == ContentMetaType.SystemUpdate)
822                              {
823                                  metaEntries = meta.MetaEntries;
824                              }
825                          }
826  
827                          continue;
828                      }
829                      else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
830                      {
831                          var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
832  
833                          using var systemVersionFile = new UniqueRef<IFile>();
834  
835                          if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
836                          {
837                              systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
838                          }
839                      }
840  
841                      if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
842                      {
843                          updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
844                      }
845                      else
846                      {
847                          updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
848                          updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
849                      }
850  
851                      ncaStorage.Dispose();
852                  }
853  
854                  if (metaEntries == null)
855                  {
856                      throw new FileNotFoundException("System update title was not found in the firmware package.");
857                  }
858  
859                  foreach (CnmtContentMetaEntry metaEntry in metaEntries)
860                  {
861                      if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry))
862                      {
863                          string metaNcaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
864                          string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
865  
866                          // Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
867                          // This is a perfect valid case, so we should just ignore the missing content nca and continue.
868                          if (contentPath == null)
869                          {
870                              updateNcas.Remove(metaEntry.TitleId);
871  
872                              continue;
873                          }
874  
875                          IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage();
876                          IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
877  
878                          Nca metaNca = new(_virtualFileSystem.KeySet, metaStorage);
879  
880                          IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
881  
882                          string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
883  
884                          using var metaFile = new UniqueRef<IFile>();
885  
886                          if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
887                          {
888                              var meta = new Cnmt(metaFile.Get.AsStream());
889  
890                              if (contentStorage.GetSize(out long size).IsSuccess())
891                              {
892                                  byte[] contentData = new byte[size];
893  
894                                  Span<byte> content = new(contentData);
895  
896                                  contentStorage.Read(0, content);
897  
898                                  Span<byte> hash = new(new byte[32]);
899  
900                                  LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
901  
902                                  if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
903                                  {
904                                      updateNcas.Remove(metaEntry.TitleId);
905                                  }
906                              }
907                          }
908                      }
909                  }
910  
911                  if (updateNcas.Count > 0)
912                  {
913                      StringBuilder extraNcas = new();
914  
915                      foreach (var entry in updateNcas)
916                      {
917                          foreach (var (type, path) in entry.Value)
918                          {
919                              extraNcas.AppendLine(path);
920                          }
921                      }
922  
923                      throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
924                  }
925  
926                  return systemVersion;
927              }
928  
929              return null;
930          }
931  
932          public SystemVersion GetCurrentFirmwareVersion()
933          {
934              LoadEntries();
935  
936              lock (_lock)
937              {
938                  var locationEnties = _locationEntries[StorageId.BuiltInSystem];
939  
940                  foreach (var entry in locationEnties)
941                  {
942                      if (entry.ContentType == NcaContentType.Data)
943                      {
944                          var path = VirtualFileSystem.SwitchPathToSystemPath(entry.ContentPath);
945  
946                          using FileStream fileStream = File.OpenRead(path);
947                          Nca nca = new(_virtualFileSystem.KeySet, fileStream.AsStorage());
948  
949                          if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
950                          {
951                              var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
952  
953                              using var systemVersionFile = new UniqueRef<IFile>();
954  
955                              if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
956                              {
957                                  return new SystemVersion(systemVersionFile.Get.AsStream());
958                              }
959                          }
960                      }
961                  }
962              }
963  
964              return null;
965          }
966      }
967  }