ApplicationHelper.cs
1 using Avalonia.Controls.Notifications; 2 using Avalonia.Platform.Storage; 3 using Avalonia.Threading; 4 using LibHac; 5 using LibHac.Account; 6 using LibHac.Common; 7 using LibHac.Fs; 8 using LibHac.Fs.Fsa; 9 using LibHac.Fs.Shim; 10 using LibHac.FsSystem; 11 using LibHac.Ns; 12 using LibHac.Tools.Fs; 13 using LibHac.Tools.FsSystem; 14 using LibHac.Tools.FsSystem.NcaUtils; 15 using Ryujinx.Ava.Common.Locale; 16 using Ryujinx.Ava.UI.Controls; 17 using Ryujinx.Ava.UI.Helpers; 18 using Ryujinx.Common.Logging; 19 using Ryujinx.HLE.FileSystem; 20 using Ryujinx.HLE.HOS.Services.Account.Acc; 21 using Ryujinx.HLE.Loaders.Processes.Extensions; 22 using Ryujinx.UI.Common.Configuration; 23 using Ryujinx.UI.Common.Helper; 24 using System; 25 using System.Buffers; 26 using System.IO; 27 using System.Threading; 28 using System.Threading.Tasks; 29 using ApplicationId = LibHac.Ncm.ApplicationId; 30 using Path = System.IO.Path; 31 32 namespace Ryujinx.Ava.Common 33 { 34 internal static class ApplicationHelper 35 { 36 private static HorizonClient _horizonClient; 37 private static AccountManager _accountManager; 38 private static VirtualFileSystem _virtualFileSystem; 39 40 public static void Initialize(VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient) 41 { 42 _virtualFileSystem = virtualFileSystem; 43 _horizonClient = horizonClient; 44 _accountManager = accountManager; 45 } 46 47 private static bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId) 48 { 49 saveDataId = default; 50 51 Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter); 52 if (ResultFs.TargetNotFound.Includes(result)) 53 { 54 ref ApplicationControlProperty control = ref controlHolder.Value; 55 56 Logger.Info?.Print(LogClass.Application, $"Creating save directory for Title: {titleName} [{titleId:x16}]"); 57 58 if (controlHolder.ByteSpan.IsZeros()) 59 { 60 // If the current application doesn't have a loaded control property, create a dummy one 61 // and set the savedata sizes so a user savedata will be created. 62 control = ref new BlitStruct<ApplicationControlProperty>(1).Value; 63 64 // The set sizes don't actually matter as long as they're non-zero because we use directory savedata. 65 control.UserAccountSaveDataSize = 0x4000; 66 control.UserAccountSaveDataJournalSize = 0x4000; 67 68 Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); 69 } 70 71 Uid user = new((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); 72 73 result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new ApplicationId(titleId), in control, in user); 74 if (result.IsFailure()) 75 { 76 Dispatcher.UIThread.InvokeAsync(async () => 77 { 78 await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageCreateSaveErrorMessage, result.ToStringWithName())); 79 }); 80 81 return false; 82 } 83 84 // Try to find the savedata again after creating it 85 result = _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, in filter); 86 } 87 88 if (result.IsSuccess()) 89 { 90 saveDataId = saveDataInfo.SaveDataId; 91 92 return true; 93 } 94 95 Dispatcher.UIThread.InvokeAsync(async () => 96 { 97 await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageFindSaveErrorMessage, result.ToStringWithName())); 98 }); 99 100 return false; 101 } 102 103 public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId, BlitStruct<ApplicationControlProperty> controlData, string titleName) 104 { 105 if (!TryFindSaveData(titleName, titleId, controlData, in saveDataFilter, out ulong saveDataId)) 106 { 107 return; 108 } 109 110 OpenSaveDir(saveDataId); 111 } 112 113 public static void OpenSaveDir(ulong saveDataId) 114 { 115 string saveRootPath = Path.Combine(VirtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}"); 116 117 if (!Directory.Exists(saveRootPath)) 118 { 119 // Inconsistent state. Create the directory 120 Directory.CreateDirectory(saveRootPath); 121 } 122 123 string committedPath = Path.Combine(saveRootPath, "0"); 124 string workingPath = Path.Combine(saveRootPath, "1"); 125 126 // If the committed directory exists, that path will be loaded the next time the savedata is mounted 127 if (Directory.Exists(committedPath)) 128 { 129 OpenHelper.OpenFolder(committedPath); 130 } 131 else 132 { 133 // If the working directory exists and the committed directory doesn't, 134 // the working directory will be loaded the next time the savedata is mounted 135 if (!Directory.Exists(workingPath)) 136 { 137 Directory.CreateDirectory(workingPath); 138 } 139 140 OpenHelper.OpenFolder(workingPath); 141 } 142 } 143 144 public static async Task ExtractSection(IStorageProvider storageProvider, NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0) 145 { 146 var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions 147 { 148 Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], 149 AllowMultiple = false, 150 }); 151 152 if (result.Count == 0) 153 { 154 return; 155 } 156 157 var destination = result[0].Path.LocalPath; 158 var cancellationToken = new CancellationTokenSource(); 159 160 UpdateWaitWindow waitingDialog = new( 161 LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], 162 LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(titleFilePath)), 163 cancellationToken); 164 165 Thread extractorThread = new(() => 166 { 167 Dispatcher.UIThread.Post(waitingDialog.Show); 168 169 using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); 170 171 Nca mainNca = null; 172 Nca patchNca = null; 173 174 string extension = Path.GetExtension(titleFilePath).ToLower(); 175 if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") 176 { 177 IFileSystem pfs; 178 179 if (extension == ".xci") 180 { 181 pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); 182 } 183 else 184 { 185 var pfsTemp = new PartitionFileSystem(); 186 pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); 187 pfs = pfsTemp; 188 } 189 190 foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) 191 { 192 using var ncaFile = new UniqueRef<IFile>(); 193 194 pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); 195 196 Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); 197 if (nca.Header.ContentType == NcaContentType.Program) 198 { 199 int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); 200 if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) 201 { 202 patchNca = nca; 203 } 204 else 205 { 206 mainNca = nca; 207 } 208 } 209 } 210 } 211 else if (extension == ".nca") 212 { 213 mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); 214 } 215 216 if (mainNca == null) 217 { 218 Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA was not present in the selected file"); 219 220 Dispatcher.UIThread.InvokeAsync(async () => 221 { 222 waitingDialog.Close(); 223 224 await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]); 225 }); 226 227 return; 228 } 229 230 IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks 231 ? IntegrityCheckLevel.ErrorOnInvalid 232 : IntegrityCheckLevel.None; 233 234 (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); 235 if (updatePatchNca != null) 236 { 237 patchNca = updatePatchNca; 238 } 239 240 int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType); 241 242 try 243 { 244 bool sectionExistsInPatch = false; 245 if (patchNca != null) 246 { 247 sectionExistsInPatch = patchNca.CanOpenSection(index); 248 } 249 250 IFileSystem ncaFileSystem = sectionExistsInPatch ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid) 251 : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid); 252 253 FileSystemClient fsClient = _horizonClient.Fs; 254 255 string source = DateTime.Now.ToFileTime().ToString()[10..]; 256 string output = DateTime.Now.ToFileTime().ToString()[10..]; 257 258 using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem); 259 using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination)); 260 261 fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref); 262 fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref); 263 264 (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token); 265 266 if (!canceled) 267 { 268 if (resultCode.Value.IsFailure()) 269 { 270 Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}"); 271 272 Dispatcher.UIThread.InvokeAsync(async () => 273 { 274 waitingDialog.Close(); 275 276 await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]); 277 }); 278 } 279 else if (resultCode.Value.IsSuccess()) 280 { 281 Dispatcher.UIThread.Post(waitingDialog.Close); 282 283 NotificationHelper.Show( 284 LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], 285 $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}", 286 NotificationType.Information); 287 } 288 } 289 290 fsClient.Unmount(source.ToU8Span()); 291 fsClient.Unmount(output.ToU8Span()); 292 } 293 catch (ArgumentException ex) 294 { 295 Logger.Error?.Print(LogClass.Application, $"{ex.Message}"); 296 297 Dispatcher.UIThread.InvokeAsync(async () => 298 { 299 waitingDialog.Close(); 300 301 await ContentDialogHelper.CreateErrorDialog(ex.Message); 302 }); 303 } 304 }) 305 { 306 Name = "GUI.NcaSectionExtractorThread", 307 IsBackground = true, 308 }; 309 extractorThread.Start(); 310 } 311 312 public static (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath, CancellationToken token) 313 { 314 Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath.ToU8Span(), OpenDirectoryMode.All); 315 if (rc.IsFailure()) 316 { 317 return (rc, false); 318 } 319 320 using (sourceHandle) 321 { 322 foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) 323 { 324 if (token.IsCancellationRequested) 325 { 326 return (null, true); 327 } 328 329 string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); 330 string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); 331 332 if (entry.Type == DirectoryEntryType.Directory) 333 { 334 fs.EnsureDirectoryExists(subDstPath); 335 336 (Result? result, bool canceled) = CopyDirectory(fs, subSrcPath, subDstPath, token); 337 if (canceled || result.Value.IsFailure()) 338 { 339 return (result, canceled); 340 } 341 } 342 343 if (entry.Type == DirectoryEntryType.File) 344 { 345 fs.CreateOrOverwriteFile(subDstPath, entry.Size); 346 347 rc = CopyFile(fs, subSrcPath, subDstPath); 348 if (rc.IsFailure()) 349 { 350 return (rc, false); 351 } 352 } 353 } 354 } 355 356 return (Result.Success, false); 357 } 358 359 public static Result CopyFile(FileSystemClient fs, string sourcePath, string destPath) 360 { 361 Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath.ToU8Span(), OpenMode.Read); 362 if (rc.IsFailure()) 363 { 364 return rc; 365 } 366 367 using (sourceHandle) 368 { 369 rc = fs.OpenFile(out FileHandle destHandle, destPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend); 370 if (rc.IsFailure()) 371 { 372 return rc; 373 } 374 375 using (destHandle) 376 { 377 const int MaxBufferSize = 1024 * 1024; 378 379 rc = fs.GetFileSize(out long fileSize, sourceHandle); 380 if (rc.IsFailure()) 381 { 382 return rc; 383 } 384 385 int bufferSize = (int)Math.Min(MaxBufferSize, fileSize); 386 387 byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); 388 try 389 { 390 for (long offset = 0; offset < fileSize; offset += bufferSize) 391 { 392 int toRead = (int)Math.Min(fileSize - offset, bufferSize); 393 Span<byte> buf = buffer.AsSpan(0, toRead); 394 395 rc = fs.ReadFile(out long _, sourceHandle, offset, buf); 396 if (rc.IsFailure()) 397 { 398 return rc; 399 } 400 401 rc = fs.WriteFile(destHandle, offset, buf, WriteOption.None); 402 if (rc.IsFailure()) 403 { 404 return rc; 405 } 406 } 407 } 408 finally 409 { 410 ArrayPool<byte>.Shared.Return(buffer); 411 } 412 413 rc = fs.FlushFile(destHandle); 414 if (rc.IsFailure()) 415 { 416 return rc; 417 } 418 } 419 } 420 421 return Result.Success; 422 } 423 } 424 }