ContentManager.cs 41 KB

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