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 }