TitleUpdatesHelper.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. using LibHac.Common;
  2. using LibHac.Common.Keys;
  3. using LibHac.Fs;
  4. using LibHac.Fs.Fsa;
  5. using LibHac.Ncm;
  6. using LibHac.Ns;
  7. using LibHac.Tools.FsSystem;
  8. using LibHac.Tools.FsSystem.NcaUtils;
  9. using Ryujinx.Ava.Common.Models;
  10. using Ryujinx.Common.Configuration;
  11. using Ryujinx.Common.Logging;
  12. using Ryujinx.Common.Utilities;
  13. using Ryujinx.HLE.FileSystem;
  14. using Ryujinx.HLE.Loaders.Processes.Extensions;
  15. using Ryujinx.HLE.Utilities;
  16. using Ryujinx.UI.Common.Configuration;
  17. using System;
  18. using System.Collections.Generic;
  19. using System.IO;
  20. using ContentType = LibHac.Ncm.ContentType;
  21. using Path = System.IO.Path;
  22. using SpanHelpers = LibHac.Common.SpanHelpers;
  23. using TitleUpdateMetadata = Ryujinx.Common.Configuration.TitleUpdateMetadata;
  24. namespace Ryujinx.Ava.Utilities
  25. {
  26. public static class TitleUpdatesHelper
  27. {
  28. private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
  29. public static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase)
  30. {
  31. var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
  32. if (!File.Exists(titleUpdatesJsonPath))
  33. {
  34. return [];
  35. }
  36. try
  37. {
  38. var titleUpdateWindowData = JsonHelper.DeserializeFromFile(titleUpdatesJsonPath, _serializerContext.TitleUpdateMetadata);
  39. return LoadTitleUpdates(vfs, titleUpdateWindowData, applicationIdBase);
  40. }
  41. catch
  42. {
  43. Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {applicationIdBase:x16} at {titleUpdatesJsonPath}");
  44. return [];
  45. }
  46. }
  47. public static void SaveTitleUpdatesJson(ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates)
  48. {
  49. var titleUpdateWindowData = new TitleUpdateMetadata
  50. {
  51. Selected = string.Empty,
  52. Paths = [],
  53. };
  54. foreach ((TitleUpdateModel update, bool isSelected) in updates)
  55. {
  56. titleUpdateWindowData.Paths.Add(update.Path);
  57. if (isSelected)
  58. {
  59. if (!string.IsNullOrEmpty(titleUpdateWindowData.Selected))
  60. {
  61. Logger.Error?.Print(LogClass.Application,
  62. $"Tried to save two updates as 'IsSelected' for {applicationIdBase:x16}");
  63. return;
  64. }
  65. titleUpdateWindowData.Selected = update.Path;
  66. }
  67. }
  68. var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
  69. JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
  70. }
  71. private static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase)
  72. {
  73. var result = new List<(TitleUpdateModel, bool IsSelected)>();
  74. IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
  75. ? IntegrityCheckLevel.ErrorOnInvalid
  76. : IntegrityCheckLevel.None;
  77. foreach (string path in titleUpdateMetadata.Paths)
  78. {
  79. if (!File.Exists(path))
  80. continue;
  81. try
  82. {
  83. using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, vfs);
  84. Dictionary<ulong, ContentMetaData> updates =
  85. pfs.GetContentData(ContentMetaType.Patch, vfs, checkLevel);
  86. if (!updates.TryGetValue(applicationIdBase, out ContentMetaData content))
  87. continue;
  88. Nca patchNca = content.GetNcaByType(vfs.KeySet, ContentType.Program);
  89. Nca controlNca = content.GetNcaByType(vfs.KeySet, ContentType.Control);
  90. if (controlNca is null || patchNca is null)
  91. continue;
  92. ApplicationControlProperty controlData = new();
  93. using UniqueRef<IFile> nacpFile = new();
  94. controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
  95. .OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
  96. nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
  97. .ThrowIfFailure();
  98. var displayVersion = controlData.DisplayVersionString.ToString();
  99. var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version,
  100. displayVersion, path);
  101. result.Add((update, path == titleUpdateMetadata.Selected));
  102. }
  103. catch (MissingKeyException exception)
  104. {
  105. Logger.Warning?.Print(LogClass.Application,
  106. $"Your key set is missing a key with the name: {exception.Name}");
  107. }
  108. catch (InvalidDataException)
  109. {
  110. Logger.Warning?.Print(LogClass.Application,
  111. $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Malformed File: {path}");
  112. }
  113. catch (IOException exception)
  114. {
  115. Logger.Warning?.Print(LogClass.Application, exception.Message);
  116. }
  117. catch (Exception exception)
  118. {
  119. Logger.Warning?.Print(LogClass.Application,
  120. $"The file encountered was not of a valid type. File: '{path}' Error: {exception}");
  121. }
  122. }
  123. return result;
  124. }
  125. private static string PathToGameUpdatesJson(ulong applicationIdBase)
  126. => Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "updates.json");
  127. }
  128. }