/ src / Ryujinx.HLE / Loaders / Processes / Extensions / NcaExtensions.cs
NcaExtensions.cs
  1  using LibHac;
  2  using LibHac.Common;
  3  using LibHac.Fs;
  4  using LibHac.Fs.Fsa;
  5  using LibHac.Loader;
  6  using LibHac.Ncm;
  7  using LibHac.Ns;
  8  using LibHac.Tools.FsSystem;
  9  using LibHac.Tools.FsSystem.NcaUtils;
 10  using LibHac.Tools.Ncm;
 11  using Ryujinx.Common.Configuration;
 12  using Ryujinx.Common.Logging;
 13  using Ryujinx.Common.Utilities;
 14  using Ryujinx.HLE.FileSystem;
 15  using Ryujinx.HLE.HOS;
 16  using Ryujinx.HLE.Utilities;
 17  using System.IO;
 18  using System.Linq;
 19  using ApplicationId = LibHac.Ncm.ApplicationId;
 20  using ContentType = LibHac.Ncm.ContentType;
 21  using Path = System.IO.Path;
 22  
 23  namespace Ryujinx.HLE.Loaders.Processes.Extensions
 24  {
 25      public static class NcaExtensions
 26      {
 27          private static readonly TitleUpdateMetadataJsonSerializerContext _applicationSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 28  
 29          public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
 30          {
 31              // Extract RomFs and ExeFs from NCA.
 32              IStorage romFs = nca.GetRomFs(device, patchNca);
 33              IFileSystem exeFs = nca.GetExeFs(device, patchNca);
 34  
 35              if (exeFs == null)
 36              {
 37                  Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA");
 38  
 39                  return ProcessResult.Failed;
 40              }
 41  
 42              // Load Npdm file.
 43              MetaLoader metaLoader = exeFs.GetNpdm();
 44  
 45              // Collecting mods related to AocTitleIds and ProgramId.
 46              device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
 47                  device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()),
 48                  ModLoader.GetModsBasePath(),
 49                  ModLoader.GetSdModsBasePath());
 50  
 51              // Load Nacp file.
 52              var nacpData = new BlitStruct<ApplicationControlProperty>(1);
 53  
 54              if (controlNca != null)
 55              {
 56                  nacpData = controlNca.GetNacp(device);
 57              }
 58  
 59              /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.
 60  
 61              // Load program 0 control NCA as we are going to need it for display version.
 62              (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
 63  
 64              // NOTE: Nintendo doesn't guarantee that the display version will be updated on sub programs when updating a multi program application.
 65              //       As such, to avoid PTC cache confusion, we only trust the program 0 display version when launching a sub program.
 66              if (updateProgram0ControlNca != null && _device.Configuration.UserChannelPersistence.Index != 0)
 67              {
 68                  nacpData.Value.DisplayVersion = updateProgram0ControlNca.GetNacp(_device).Value.DisplayVersion;
 69              }
 70  
 71              */
 72  
 73              ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader, (byte)nca.GetProgramIndex());
 74  
 75              // Load RomFS.
 76              if (romFs == null)
 77              {
 78                  Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA");
 79              }
 80              else
 81              {
 82                  romFs = device.Configuration.VirtualFileSystem.ModLoader.ApplyRomFsMods(processResult.ProgramId, romFs);
 83  
 84                  device.Configuration.VirtualFileSystem.SetRomFs(processResult.ProcessId, romFs.AsStream(FileAccess.Read));
 85              }
 86  
 87              // Don't create save data for system programs.
 88              if (processResult.ProgramId != 0 && (processResult.ProgramId < SystemProgramId.Start.Value || processResult.ProgramId > SystemAppletId.End.Value))
 89              {
 90                  // Multi-program applications can technically use any program ID for the main program, but in practice they always use 0 in the low nibble.
 91                  // We'll know if this changes in the future because applications will get errors when trying to mount the correct save.
 92                  ProcessLoaderHelper.EnsureSaveData(device, new ApplicationId(processResult.ProgramId & ~0xFul), nacpData);
 93              }
 94  
 95              return processResult;
 96          }
 97  
 98          public static ulong GetProgramIdBase(this Nca nca)
 99          {
100              return nca.Header.TitleId & ~0x1FFFUL;
101          }
102  
103          public static int GetProgramIndex(this Nca nca)
104          {
105              return (int)(nca.Header.TitleId & 0xF);
106          }
107  
108          public static bool IsProgram(this Nca nca)
109          {
110              return nca.Header.ContentType == NcaContentType.Program;
111          }
112  
113          public static bool IsMain(this Nca nca)
114          {
115              return nca.IsProgram() && !nca.IsPatch();
116          }
117  
118          public static bool IsPatch(this Nca nca)
119          {
120              int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
121  
122              return nca.IsProgram() && nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection();
123          }
124  
125          public static bool IsControl(this Nca nca)
126          {
127              return nca.Header.ContentType == NcaContentType.Control;
128          }
129  
130          public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath)
131          {
132              updatePath = null;
133  
134              // Load Update NCAs.
135              Nca updatePatchNca = null;
136              Nca updateControlNca = null;
137  
138              // Clear the program index part.
139              ulong titleIdBase = mainNca.GetProgramIdBase();
140  
141              // Load update information if exists.
142              string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
143              if (File.Exists(titleUpdateMetadataPath))
144              {
145                  updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _applicationSerializerContext.TitleUpdateMetadata).Selected;
146                  if (File.Exists(updatePath))
147                  {
148                      IFileSystem updatePartitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(updatePath, fileSystem);
149  
150                      foreach ((ulong applicationTitleId, ContentMetaData content) in updatePartitionFileSystem.GetContentData(ContentMetaType.Patch, fileSystem, checkLevel))
151                      {
152                          if ((applicationTitleId & ~0x1FFFUL) != titleIdBase)
153                          {
154                              continue;
155                          }
156  
157                          updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex);
158                          updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex);
159                          break;
160                      }
161                  }
162              }
163  
164              return (updatePatchNca, updateControlNca);
165          }
166  
167          public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
168          {
169              IFileSystem exeFs = null;
170  
171              if (patchNca == null)
172              {
173                  if (nca.CanOpenSection(NcaSectionType.Code))
174                  {
175                      exeFs = nca.OpenFileSystem(NcaSectionType.Code, device.System.FsIntegrityCheckLevel);
176                  }
177              }
178              else
179              {
180                  if (patchNca.CanOpenSection(NcaSectionType.Code))
181                  {
182                      exeFs = nca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, device.System.FsIntegrityCheckLevel);
183                  }
184              }
185  
186              return exeFs;
187          }
188  
189          public static IStorage GetRomFs(this Nca nca, Switch device, Nca patchNca = null)
190          {
191              IStorage romFs = null;
192  
193              if (patchNca == null)
194              {
195                  if (nca.CanOpenSection(NcaSectionType.Data))
196                  {
197                      romFs = nca.OpenStorage(NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
198                  }
199              }
200              else
201              {
202                  if (patchNca.CanOpenSection(NcaSectionType.Data))
203                  {
204                      romFs = nca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
205                  }
206              }
207  
208              return romFs;
209          }
210  
211          public static BlitStruct<ApplicationControlProperty> GetNacp(this Nca controlNca, Switch device)
212          {
213              var nacpData = new BlitStruct<ApplicationControlProperty>(1);
214  
215              using var controlFile = new UniqueRef<IFile>();
216  
217              Result result = controlNca.OpenFileSystem(NcaSectionType.Data, device.System.FsIntegrityCheckLevel)
218                                        .OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read);
219  
220              if (result.IsSuccess())
221              {
222                  result = controlFile.Get.Read(out long bytesRead, 0, nacpData.ByteSpan, ReadOption.None);
223              }
224              else
225              {
226                  nacpData.ByteSpan.Clear();
227              }
228  
229              return nacpData;
230          }
231  
232          public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType)
233          {
234              string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt";
235              using var cnmtFile = new UniqueRef<IFile>();
236  
237              try
238              {
239                  Result result = cnmtNca.OpenFileSystem(0, checkLevel)
240                                         .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read);
241  
242                  if (result.IsSuccess())
243                  {
244                      return new Cnmt(cnmtFile.Release().AsStream());
245                  }
246              }
247              catch (HorizonResultException ex)
248              {
249                  if (!ResultFs.PathNotFound.Includes(ex.ResultValue))
250                  {
251                      Logger.Warning?.Print(LogClass.Application, $"Failed get CNMT for '{cnmtNca.Header.TitleId:x16}' from NCA: {ex.Message}");
252                  }
253              }
254  
255              return null;
256          }
257      }
258  }