ContentManager.cs 37 KB

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