ApplicationLibrary.cs
1 using LibHac; 2 using LibHac.Common; 3 using LibHac.Common.Keys; 4 using LibHac.Fs; 5 using LibHac.Fs.Fsa; 6 using LibHac.FsSystem; 7 using LibHac.Ncm; 8 using LibHac.Ns; 9 using LibHac.Tools.Fs; 10 using LibHac.Tools.FsSystem; 11 using LibHac.Tools.FsSystem.NcaUtils; 12 using Ryujinx.Common.Configuration; 13 using Ryujinx.Common.Logging; 14 using Ryujinx.Common.Utilities; 15 using Ryujinx.HLE.FileSystem; 16 using Ryujinx.HLE.HOS.SystemState; 17 using Ryujinx.HLE.Loaders.Npdm; 18 using Ryujinx.HLE.Loaders.Processes.Extensions; 19 using Ryujinx.UI.Common.Configuration; 20 using Ryujinx.UI.Common.Configuration.System; 21 using System; 22 using System.Collections.Generic; 23 using System.IO; 24 using System.Linq; 25 using System.Reflection; 26 using System.Text; 27 using System.Text.Json; 28 using System.Threading; 29 using ContentType = LibHac.Ncm.ContentType; 30 using Path = System.IO.Path; 31 using TimeSpan = System.TimeSpan; 32 33 namespace Ryujinx.UI.App.Common 34 { 35 public class ApplicationLibrary 36 { 37 public Language DesiredLanguage { get; set; } 38 public event EventHandler<ApplicationAddedEventArgs> ApplicationAdded; 39 public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated; 40 41 private readonly byte[] _nspIcon; 42 private readonly byte[] _xciIcon; 43 private readonly byte[] _ncaIcon; 44 private readonly byte[] _nroIcon; 45 private readonly byte[] _nsoIcon; 46 47 private readonly VirtualFileSystem _virtualFileSystem; 48 private readonly IntegrityCheckLevel _checkLevel; 49 private CancellationTokenSource _cancellationToken; 50 51 private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); 52 53 public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) 54 { 55 _virtualFileSystem = virtualFileSystem; 56 _checkLevel = checkLevel; 57 58 _nspIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSP.png"); 59 _xciIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_XCI.png"); 60 _ncaIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NCA.png"); 61 _nroIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NRO.png"); 62 _nsoIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSO.png"); 63 } 64 65 private static byte[] GetResourceBytes(string resourceName) 66 { 67 Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); 68 byte[] resourceByteArray = new byte[resourceStream.Length]; 69 70 resourceStream.ReadExactly(resourceByteArray); 71 72 return resourceByteArray; 73 } 74 75 /// <exception cref="Ryujinx.HLE.Exceptions.InvalidNpdmException">The npdm file doesn't contain valid data.</exception> 76 /// <exception cref="NotImplementedException">The FsAccessHeader.ContentOwnerId section is not implemented.</exception> 77 /// <exception cref="ArgumentException">An error occured while reading bytes from the stream.</exception> 78 /// <exception cref="EndOfStreamException">The end of the stream is reached.</exception> 79 /// <exception cref="IOException">An I/O error occurred.</exception> 80 private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath) 81 { 82 ApplicationData data = new() 83 { 84 Icon = _nspIcon, 85 Path = filePath, 86 }; 87 88 using UniqueRef<IFile> npdmFile = new(); 89 90 Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); 91 92 if (ResultFs.PathNotFound.Includes(result)) 93 { 94 Npdm npdm = new(npdmFile.Get.AsStream()); 95 96 data.Name = npdm.TitleName; 97 data.Id = npdm.Aci0.TitleId; 98 } 99 100 return data; 101 } 102 103 /// <exception cref="MissingKeyException">The configured key set is missing a key.</exception> 104 /// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception> 105 /// <exception cref="NotSupportedException">The NCA version is not supported.</exception> 106 /// <exception cref="HorizonResultException">An error occured while reading PFS data.</exception> 107 /// <exception cref="Ryujinx.HLE.Exceptions.InvalidNpdmException">The npdm file doesn't contain valid data.</exception> 108 /// <exception cref="NotImplementedException">The FsAccessHeader.ContentOwnerId section is not implemented.</exception> 109 /// <exception cref="ArgumentException">An error occured while reading bytes from the stream.</exception> 110 /// <exception cref="EndOfStreamException">The end of the stream is reached.</exception> 111 /// <exception cref="IOException">An I/O error occurred.</exception> 112 private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) 113 { 114 bool isExeFs = false; 115 116 // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. 117 bool hasMainNca = false; 118 119 foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) 120 { 121 if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") 122 { 123 using UniqueRef<IFile> ncaFile = new(); 124 125 try 126 { 127 pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); 128 129 Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); 130 int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); 131 132 // Some main NCAs don't have a data partition, so check if the partition exists before opening it 133 if (nca.Header.ContentType == NcaContentType.Program && 134 !(nca.SectionExists(NcaSectionType.Data) && 135 nca.Header.GetFsHeader(dataIndex).IsPatchSection())) 136 { 137 hasMainNca = true; 138 139 break; 140 } 141 } 142 catch (Exception exception) 143 { 144 Logger.Warning?.Print(LogClass.Application, $"Encountered an error while trying to load applications from file '{filePath}': {exception}"); 145 146 return null; 147 } 148 } 149 else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") 150 { 151 isExeFs = true; 152 } 153 } 154 155 if (hasMainNca) 156 { 157 List<ApplicationData> applications = GetApplicationsFromPfs(pfs, filePath); 158 159 switch (applications.Count) 160 { 161 case 1: 162 return applications[0]; 163 case >= 1: 164 Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}"); 165 return applications[0]; 166 default: 167 return null; 168 } 169 } 170 171 if (isExeFs) 172 { 173 return GetApplicationFromExeFs(pfs, filePath); 174 } 175 176 return null; 177 } 178 179 /// <exception cref="MissingKeyException">The configured key set is missing a key.</exception> 180 /// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception> 181 /// <exception cref="NotSupportedException">The NCA version is not supported.</exception> 182 /// <exception cref="HorizonResultException">An error occured while reading PFS data.</exception> 183 private List<ApplicationData> GetApplicationsFromPfs(IFileSystem pfs, string filePath) 184 { 185 var applications = new List<ApplicationData>(); 186 string extension = Path.GetExtension(filePath).ToLower(); 187 188 foreach ((ulong titleId, ContentMetaData content) in pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel)) 189 { 190 ApplicationData applicationData = new() 191 { 192 Id = titleId, 193 Path = filePath, 194 }; 195 196 Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); 197 Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); 198 199 BlitStruct<ApplicationControlProperty> controlHolder = new(1); 200 201 IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, _checkLevel); 202 203 // Check if there is an update available. 204 if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) 205 { 206 // Replace the original ControlFs by the updated one. 207 controlFs = updatedControlFs; 208 } 209 210 if (controlFs == null) 211 { 212 continue; 213 } 214 215 ReadControlData(controlFs, controlHolder.ByteSpan); 216 217 GetApplicationInformation(ref controlHolder.Value, ref applicationData); 218 219 // Read the icon from the ControlFS and store it as a byte array 220 try 221 { 222 using UniqueRef<IFile> icon = new(); 223 224 controlFs.OpenFile(ref icon.Ref, $"/icon_{DesiredLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); 225 226 using MemoryStream stream = new(); 227 228 icon.Get.AsStream().CopyTo(stream); 229 applicationData.Icon = stream.ToArray(); 230 } 231 catch (HorizonResultException) 232 { 233 foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) 234 { 235 if (entry.Name == "control.nacp") 236 { 237 continue; 238 } 239 240 using var icon = new UniqueRef<IFile>(); 241 242 controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); 243 244 using MemoryStream stream = new(); 245 246 icon.Get.AsStream().CopyTo(stream); 247 applicationData.Icon = stream.ToArray(); 248 249 if (applicationData.Icon != null) 250 { 251 break; 252 } 253 } 254 255 applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; 256 } 257 258 applicationData.ControlHolder = controlHolder; 259 260 applications.Add(applicationData); 261 } 262 263 return applications; 264 } 265 266 public bool TryGetApplicationsFromFile(string applicationPath, out List<ApplicationData> applications) 267 { 268 applications = []; 269 long fileSize; 270 271 try 272 { 273 fileSize = new FileInfo(applicationPath).Length; 274 } 275 catch (FileNotFoundException) 276 { 277 Logger.Warning?.Print(LogClass.Application, $"The file was not found: '{applicationPath}'"); 278 279 return false; 280 } 281 282 BlitStruct<ApplicationControlProperty> controlHolder = new(1); 283 284 try 285 { 286 string extension = Path.GetExtension(applicationPath).ToLower(); 287 288 using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); 289 290 switch (extension) 291 { 292 case ".xci": 293 { 294 Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); 295 296 applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath); 297 298 if (applications.Count == 0) 299 { 300 return false; 301 } 302 303 break; 304 } 305 case ".nsp": 306 case ".pfs0": 307 { 308 var pfs = new PartitionFileSystem(); 309 pfs.Initialize(file.AsStorage()).ThrowIfFailure(); 310 311 ApplicationData result = GetApplicationFromNsp(pfs, applicationPath); 312 313 if (result == null) 314 { 315 return false; 316 } 317 318 applications.Add(result); 319 320 break; 321 } 322 case ".nro": 323 { 324 BinaryReader reader = new(file); 325 ApplicationData application = new(); 326 327 file.Seek(24, SeekOrigin.Begin); 328 329 int assetOffset = reader.ReadInt32(); 330 331 if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") 332 { 333 byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); 334 335 long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); 336 long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); 337 338 ulong nacpOffset = reader.ReadUInt64(); 339 ulong nacpSize = reader.ReadUInt64(); 340 341 // Reads and stores game icon as byte array 342 if (iconSize > 0) 343 { 344 application.Icon = Read(assetOffset + iconOffset, (int)iconSize); 345 } 346 else 347 { 348 application.Icon = _nroIcon; 349 } 350 351 // Read the NACP data 352 Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); 353 354 GetApplicationInformation(ref controlHolder.Value, ref application); 355 } 356 else 357 { 358 application.Icon = _nroIcon; 359 application.Name = Path.GetFileNameWithoutExtension(applicationPath); 360 } 361 362 application.ControlHolder = controlHolder; 363 applications.Add(application); 364 365 break; 366 367 byte[] Read(long position, int size) 368 { 369 file.Seek(position, SeekOrigin.Begin); 370 371 return reader.ReadBytes(size); 372 } 373 } 374 case ".nca": 375 { 376 ApplicationData application = new(); 377 378 Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); 379 380 if (!nca.IsProgram() || nca.IsPatch()) 381 { 382 return false; 383 } 384 385 application.Icon = _ncaIcon; 386 application.Name = Path.GetFileNameWithoutExtension(applicationPath); 387 application.ControlHolder = controlHolder; 388 389 applications.Add(application); 390 391 break; 392 } 393 // If its an NSO we just set defaults 394 case ".nso": 395 { 396 ApplicationData application = new() 397 { 398 Icon = _nsoIcon, 399 Name = Path.GetFileNameWithoutExtension(applicationPath), 400 }; 401 402 applications.Add(application); 403 404 break; 405 } 406 } 407 } 408 catch (MissingKeyException exception) 409 { 410 Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); 411 412 return false; 413 } 414 catch (InvalidDataException) 415 { 416 Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); 417 418 return false; 419 } 420 catch (IOException exception) 421 { 422 Logger.Warning?.Print(LogClass.Application, exception.Message); 423 424 return false; 425 } 426 catch (Exception exception) 427 { 428 Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); 429 430 return false; 431 } 432 433 foreach (var data in applications) 434 { 435 // Only load metadata for applications with an ID 436 if (data.Id != 0) 437 { 438 ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => 439 { 440 appMetadata.Title = data.Name; 441 442 // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. 443 if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) 444 { 445 appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); 446 appMetadata.TimePlayedOld = default; 447 } 448 449 // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. 450 if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) 451 { 452 // Migrate from string-based last_played to DateTime-based last_played_utc. 453 if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) 454 { 455 appMetadata.LastPlayed = lastPlayedOldParsed; 456 457 // Migration successful: deleting last_played from the metadata file. 458 appMetadata.LastPlayedOld = default; 459 } 460 461 } 462 }); 463 464 data.Favorite = appMetadata.Favorite; 465 data.TimePlayed = appMetadata.TimePlayed; 466 data.LastPlayed = appMetadata.LastPlayed; 467 } 468 469 data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); 470 data.FileSize = fileSize; 471 data.Path = applicationPath; 472 } 473 474 return true; 475 } 476 477 public void CancelLoading() 478 { 479 _cancellationToken?.Cancel(); 480 } 481 482 public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty) 483 { 484 using UniqueRef<IFile> controlFile = new(); 485 486 controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); 487 controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); 488 } 489 490 public void LoadApplications(List<string> appDirs) 491 { 492 int numApplicationsFound = 0; 493 int numApplicationsLoaded = 0; 494 495 _cancellationToken = new CancellationTokenSource(); 496 497 // Builds the applications list with paths to found applications 498 List<string> applicationPaths = new(); 499 500 try 501 { 502 foreach (string appDir in appDirs) 503 { 504 if (_cancellationToken.Token.IsCancellationRequested) 505 { 506 return; 507 } 508 509 if (!Directory.Exists(appDir)) 510 { 511 Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist."); 512 513 continue; 514 } 515 516 try 517 { 518 EnumerationOptions options = new() 519 { 520 RecurseSubdirectories = true, 521 IgnoreInaccessible = false, 522 }; 523 524 IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", options).Where(file => 525 { 526 return 527 (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || 528 (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) || 529 (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) || 530 (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) || 531 (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) || 532 (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value); 533 }); 534 535 foreach (string app in files) 536 { 537 if (_cancellationToken.Token.IsCancellationRequested) 538 { 539 return; 540 } 541 542 var fileInfo = new FileInfo(app); 543 544 try 545 { 546 var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; 547 548 applicationPaths.Add(fullPath); 549 numApplicationsFound++; 550 } 551 catch (IOException exception) 552 { 553 Logger.Warning?.Print(LogClass.Application, $"Failed to resolve the full path to file: \"{app}\" Error: {exception}"); 554 } 555 } 556 } 557 catch (UnauthorizedAccessException) 558 { 559 Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\""); 560 } 561 } 562 563 // Loops through applications list, creating a struct and then firing an event containing the struct for each application 564 foreach (string applicationPath in applicationPaths) 565 { 566 if (_cancellationToken.Token.IsCancellationRequested) 567 { 568 return; 569 } 570 571 if (TryGetApplicationsFromFile(applicationPath, out List<ApplicationData> applications)) 572 { 573 foreach (var application in applications) 574 { 575 OnApplicationAdded(new ApplicationAddedEventArgs 576 { 577 AppData = application, 578 }); 579 } 580 581 if (applications.Count > 1) 582 { 583 numApplicationsFound += applications.Count - 1; 584 } 585 586 numApplicationsLoaded += applications.Count; 587 } 588 else 589 { 590 numApplicationsFound--; 591 } 592 593 OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs 594 { 595 NumAppsFound = numApplicationsFound, 596 NumAppsLoaded = numApplicationsLoaded, 597 }); 598 } 599 600 OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs 601 { 602 NumAppsFound = numApplicationsFound, 603 NumAppsLoaded = numApplicationsLoaded, 604 }); 605 } 606 finally 607 { 608 _cancellationToken.Dispose(); 609 _cancellationToken = null; 610 } 611 } 612 613 protected void OnApplicationAdded(ApplicationAddedEventArgs e) 614 { 615 ApplicationAdded?.Invoke(null, e); 616 } 617 618 protected void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e) 619 { 620 ApplicationCountUpdated?.Invoke(null, e); 621 } 622 623 public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null) 624 { 625 string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); 626 string metadataFile = Path.Combine(metadataFolder, "metadata.json"); 627 628 ApplicationMetadata appMetadata; 629 630 if (!File.Exists(metadataFile)) 631 { 632 Directory.CreateDirectory(metadataFolder); 633 634 appMetadata = new ApplicationMetadata(); 635 636 JsonHelper.SerializeToFile(metadataFile, appMetadata, _serializerContext.ApplicationMetadata); 637 } 638 639 try 640 { 641 appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata); 642 } 643 catch (JsonException) 644 { 645 Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); 646 647 appMetadata = new ApplicationMetadata(); 648 } 649 650 if (modifyFunction != null) 651 { 652 modifyFunction(appMetadata); 653 654 JsonHelper.SerializeToFile(metadataFile, appMetadata, _serializerContext.ApplicationMetadata); 655 } 656 657 return appMetadata; 658 } 659 660 public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong applicationId) 661 { 662 byte[] applicationIcon = null; 663 664 if (applicationId == 0) 665 { 666 if (Directory.Exists(applicationPath)) 667 { 668 return _ncaIcon; 669 } 670 671 return Path.GetExtension(applicationPath).ToLower() switch 672 { 673 ".nsp" => _nspIcon, 674 ".pfs0" => _nspIcon, 675 ".xci" => _xciIcon, 676 ".nso" => _nsoIcon, 677 ".nro" => _nroIcon, 678 ".nca" => _ncaIcon, 679 _ => _ncaIcon, 680 }; 681 } 682 683 try 684 { 685 // Look for icon only if applicationPath is not a directory 686 if (!Directory.Exists(applicationPath)) 687 { 688 string extension = Path.GetExtension(applicationPath).ToLower(); 689 690 using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); 691 692 if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") 693 { 694 try 695 { 696 IFileSystem pfs; 697 698 bool isExeFs = false; 699 700 if (extension == ".xci") 701 { 702 Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); 703 704 pfs = xci.OpenPartition(XciPartitionType.Secure); 705 } 706 else 707 { 708 var pfsTemp = new PartitionFileSystem(); 709 pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); 710 pfs = pfsTemp; 711 712 foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) 713 { 714 if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") 715 { 716 isExeFs = true; 717 } 718 } 719 } 720 721 if (isExeFs) 722 { 723 applicationIcon = _nspIcon; 724 } 725 else 726 { 727 // Store the ControlFS in variable called controlFs 728 Dictionary<ulong, ContentMetaData> programs = pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel); 729 IFileSystem controlFs = null; 730 731 if (programs.TryGetValue(applicationId, out ContentMetaData value)) 732 { 733 if (value.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca) 734 { 735 controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); 736 } 737 } 738 739 // Read the icon from the ControlFS and store it as a byte array 740 try 741 { 742 using var icon = new UniqueRef<IFile>(); 743 744 controlFs.OpenFile(ref icon.Ref, $"/icon_{desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); 745 746 using MemoryStream stream = new(); 747 748 icon.Get.AsStream().CopyTo(stream); 749 applicationIcon = stream.ToArray(); 750 } 751 catch (HorizonResultException) 752 { 753 foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) 754 { 755 if (entry.Name == "control.nacp") 756 { 757 continue; 758 } 759 760 using var icon = new UniqueRef<IFile>(); 761 762 controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); 763 764 using MemoryStream stream = new(); 765 icon.Get.AsStream().CopyTo(stream); 766 applicationIcon = stream.ToArray(); 767 768 break; 769 } 770 771 applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; 772 } 773 } 774 } 775 catch (MissingKeyException) 776 { 777 applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; 778 } 779 catch (InvalidDataException) 780 { 781 applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; 782 } 783 catch (Exception exception) 784 { 785 Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); 786 } 787 } 788 else if (extension == ".nro") 789 { 790 BinaryReader reader = new(file); 791 792 byte[] Read(long position, int size) 793 { 794 file.Seek(position, SeekOrigin.Begin); 795 796 return reader.ReadBytes(size); 797 } 798 799 try 800 { 801 file.Seek(24, SeekOrigin.Begin); 802 803 int assetOffset = reader.ReadInt32(); 804 805 if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") 806 { 807 byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); 808 809 long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); 810 long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); 811 812 // Reads and stores game icon as byte array 813 if (iconSize > 0) 814 { 815 applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); 816 } 817 else 818 { 819 applicationIcon = _nroIcon; 820 } 821 } 822 else 823 { 824 applicationIcon = _nroIcon; 825 } 826 } 827 catch 828 { 829 Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); 830 } 831 } 832 else if (extension == ".nca") 833 { 834 applicationIcon = _ncaIcon; 835 } 836 // If its an NSO we just set defaults 837 else if (extension == ".nso") 838 { 839 applicationIcon = _nsoIcon; 840 } 841 } 842 } 843 catch (Exception) 844 { 845 Logger.Warning?.Print(LogClass.Application, $"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}"); 846 } 847 848 return applicationIcon ?? _ncaIcon; 849 } 850 851 private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) 852 { 853 _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); 854 855 if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) 856 { 857 data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); 858 data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); 859 } 860 else 861 { 862 data.Name = null; 863 data.Developer = null; 864 } 865 866 if (string.IsNullOrWhiteSpace(data.Name)) 867 { 868 foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) 869 { 870 if (!controlTitle.NameString.IsEmpty()) 871 { 872 data.Name = controlTitle.NameString.ToString(); 873 874 break; 875 } 876 } 877 } 878 879 if (string.IsNullOrWhiteSpace(data.Developer)) 880 { 881 foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) 882 { 883 if (!controlTitle.PublisherString.IsEmpty()) 884 { 885 data.Developer = controlTitle.PublisherString.ToString(); 886 887 break; 888 } 889 } 890 } 891 892 if (data.Id == 0) 893 { 894 if (controlData.SaveDataOwnerId != 0) 895 { 896 data.Id = controlData.SaveDataOwnerId; 897 } 898 else if (controlData.PresenceGroupId != 0) 899 { 900 data.Id = controlData.PresenceGroupId; 901 } 902 else if (controlData.AddOnContentBaseId != 0) 903 { 904 data.Id = (controlData.AddOnContentBaseId - 0x1000); 905 } 906 } 907 908 data.Version = controlData.DisplayVersionString.ToString(); 909 } 910 911 private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs) 912 { 913 updatedControlFs = null; 914 915 string updatePath = null; 916 917 try 918 { 919 (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath); 920 921 if (patchNca != null && controlNca != null) 922 { 923 updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); 924 925 return true; 926 } 927 } 928 catch (InvalidDataException) 929 { 930 Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); 931 } 932 catch (MissingKeyException exception) 933 { 934 Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); 935 } 936 937 return false; 938 } 939 } 940 }