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 }