Ryujinx/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
Ac_K b8f48bcf64
UI: Fix empty homebrew icon (#5189)
* UI: Fix empty homebrew icon

We currently don't check the icon size when we read it from the homebrew data. That could cause issues at UI side since the buffer isn't null but empty. Extra check have been added UI side too.
(I cleaned up some files during my research too)

Fixes #5188

* Remove additional check

* Remove unused using
2023-06-01 18:24:00 +02:00

944 lines
40 KiB
C#

using LibHac;
using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Configuration.System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
using Path = System.IO.Path;
namespace Ryujinx.Ui.App.Common
{
public class ApplicationLibrary
{
public event EventHandler<ApplicationAddedEventArgs> ApplicationAdded;
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
private readonly byte[] _nspIcon;
private readonly byte[] _xciIcon;
private readonly byte[] _ncaIcon;
private readonly byte[] _nroIcon;
private readonly byte[] _nsoIcon;
private readonly VirtualFileSystem _virtualFileSystem;
private Language _desiredTitleLanguage;
private CancellationTokenSource _cancellationToken;
private static readonly ApplicationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext TitleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public ApplicationLibrary(VirtualFileSystem virtualFileSystem)
{
_virtualFileSystem = virtualFileSystem;
_nspIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSP.png");
_xciIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_XCI.png");
_ncaIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NCA.png");
_nroIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NRO.png");
_nsoIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSO.png");
}
private static byte[] GetResourceBytes(string resourceName)
{
Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName);
byte[] resourceByteArray = new byte[resourceStream.Length];
resourceStream.Read(resourceByteArray);
return resourceByteArray;
}
public void CancelLoading()
{
_cancellationToken?.Cancel();
}
public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty)
{
using UniqueRef<IFile> controlFile = new();
controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
}
public void LoadApplications(List<string> appDirs, Language desiredTitleLanguage)
{
int numApplicationsFound = 0;
int numApplicationsLoaded = 0;
_desiredTitleLanguage = desiredTitleLanguage;
_cancellationToken = new CancellationTokenSource();
// Builds the applications list with paths to found applications
List<string> applications = new();
try
{
foreach (string appDir in appDirs)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
if (!Directory.Exists(appDir))
{
Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\"");
continue;
}
try
{
IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", SearchOption.AllDirectories).Where(file =>
{
return
(Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.Ui.ShownFileTypes.NSP.Value) ||
(Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.Ui.ShownFileTypes.PFS0.Value) ||
(Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.Ui.ShownFileTypes.XCI.Value) ||
(Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.Ui.ShownFileTypes.NCA.Value) ||
(Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.Ui.ShownFileTypes.NRO.Value) ||
(Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.Ui.ShownFileTypes.NSO.Value);
});
foreach (string app in files)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
var fileInfo = new FileInfo(app);
string extension = fileInfo.Extension.ToLower();
if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso")
{
var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName;
applications.Add(fullPath);
numApplicationsFound++;
}
}
}
catch (UnauthorizedAccessException)
{
Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\"");
}
}
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
foreach (string applicationPath in applications)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
string titleName = "Unknown";
string titleId = "0000000000000000";
string developer = "Unknown";
string version = "0";
byte[] applicationIcon = null;
BlitStruct<ApplicationControlProperty> controlHolder = new(1);
try
{
string extension = Path.GetExtension(applicationPath).ToLower();
using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
{
try
{
PartitionFileSystem pfs;
bool isExeFs = false;
if (extension == ".xci")
{
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
pfs = new PartitionFileSystem(file.AsStorage());
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
bool hasMainNca = false;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{
if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
{
using UniqueRef<IFile> ncaFile = new();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
{
hasMainNca = true;
break;
}
}
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
{
isExeFs = true;
}
}
if (!hasMainNca && !isExeFs)
{
numApplicationsFound--;
continue;
}
}
if (isExeFs)
{
applicationIcon = _nspIcon;
using UniqueRef<IFile> npdmFile = new();
Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
if (ResultFs.PathNotFound.Includes(result))
{
Npdm npdm = new(npdmFile.Get.AsStream());
titleName = npdm.TitleName;
titleId = npdm.Aci0.TitleId.ToString("x16");
}
}
else
{
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
// Check if there is an update available.
if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs))
{
// Replace the original ControlFs by the updated one.
controlFs = updatedControlFs;
}
ReadControlData(controlFs, controlHolder.ByteSpan);
GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version);
// Read the icon from the ControlFS and store it as a byte array
try
{
using UniqueRef<IFile> icon = new();
controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
catch (HorizonResultException)
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{
if (entry.Name == "control.nacp")
{
continue;
}
using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
if (applicationIcon != null)
{
break;
}
}
applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
}
}
}
catch (MissingKeyException exception)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
}
catch (InvalidDataException)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
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}");
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
numApplicationsFound--;
continue;
}
}
else if (extension == ".nro")
{
BinaryReader reader = new(file);
byte[] Read(long position, int size)
{
file.Seek(position, SeekOrigin.Begin);
return reader.ReadBytes(size);
}
try
{
file.Seek(24, SeekOrigin.Begin);
int assetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
{
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
// Reads and stores game icon as byte array
if (iconSize > 0)
{
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
applicationIcon = _nroIcon;
}
// Read the NACP data
Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version);
}
else
{
applicationIcon = _nroIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
numApplicationsFound--;
continue;
}
}
else if (extension == ".nca")
{
try
{
Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
{
numApplicationsFound--;
continue;
}
}
catch (InvalidDataException)
{
Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}");
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
numApplicationsFound--;
continue;
}
applicationIcon = _ncaIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
// If its an NSO we just set defaults
else if (extension == ".nso")
{
applicationIcon = _nsoIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
}
catch (IOException exception)
{
Logger.Warning?.Print(LogClass.Application, exception.Message);
numApplicationsFound--;
continue;
}
ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata =>
{
appMetadata.Title = titleName;
if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue)
{
// Don't do the migration if last_played doesn't exist or last_played_utc already has a value.
return;
}
// Migrate from string-based last_played to DateTime-based last_played_utc.
if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
{
Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc");
appMetadata.LastPlayed = lastPlayedOldParsed;
// Migration successful: deleting last_played from the metadata file.
appMetadata.LastPlayedOld = default;
}
else
{
// Migration failed: emitting warning but leaving the unparsable value in the metadata file so the user can fix it.
Logger.Warning?.Print(LogClass.Application, $"Last played string \"{appMetadata.LastPlayedOld}\" is invalid for current system culture, skipping (did current culture change?)");
}
});
ApplicationData data = new()
{
Favorite = appMetadata.Favorite,
Icon = applicationIcon,
TitleName = titleName,
TitleId = titleId,
Developer = developer,
Version = version,
TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
TimePlayedNum = appMetadata.TimePlayed,
LastPlayed = appMetadata.LastPlayed,
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB",
FileSizeBytes = fileSize,
Path = applicationPath,
ControlHolder = controlHolder
};
numApplicationsLoaded++;
OnApplicationAdded(new ApplicationAddedEventArgs()
{
AppData = data
});
OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs()
{
NumAppsFound = numApplicationsFound,
NumAppsLoaded = numApplicationsLoaded
});
}
OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs()
{
NumAppsFound = numApplicationsFound,
NumAppsLoaded = numApplicationsLoaded
});
}
finally
{
_cancellationToken.Dispose();
_cancellationToken = null;
}
}
protected void OnApplicationAdded(ApplicationAddedEventArgs e)
{
ApplicationAdded?.Invoke(null, e);
}
protected void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e)
{
ApplicationCountUpdated?.Invoke(null, e);
}
private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId)
{
(_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0);
// Return the ControlFS
controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
titleId = controlNca?.Header.TitleId.ToString("x16");
}
public ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
{
string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui");
string metadataFile = Path.Combine(metadataFolder, "metadata.json");
ApplicationMetadata appMetadata;
if (!File.Exists(metadataFile))
{
Directory.CreateDirectory(metadataFolder);
appMetadata = new ApplicationMetadata();
JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata);
}
try
{
appMetadata = JsonHelper.DeserializeFromFile(metadataFile, SerializerContext.ApplicationMetadata);
}
catch (JsonException)
{
Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults.");
appMetadata = new ApplicationMetadata();
}
if (modifyFunction != null)
{
modifyFunction(appMetadata);
JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata);
}
return appMetadata;
}
public byte[] GetApplicationIcon(string applicationPath)
{
byte[] applicationIcon = null;
try
{
// Look for icon only if applicationPath is not a directory
if (!Directory.Exists(applicationPath))
{
string extension = Path.GetExtension(applicationPath).ToLower();
using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
{
try
{
PartitionFileSystem pfs;
bool isExeFs = false;
if (extension == ".xci")
{
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
pfs = new PartitionFileSystem(file.AsStorage());
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{
if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
{
isExeFs = true;
}
}
}
if (isExeFs)
{
applicationIcon = _nspIcon;
}
else
{
// Store the ControlFS in variable called controlFs
GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _);
// Read the icon from the ControlFS and store it as a byte array
try
{
using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
catch (HorizonResultException)
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{
if (entry.Name == "control.nacp")
{
continue;
}
using var icon = new UniqueRef<IFile>();
controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new())
{
icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
if (applicationIcon != null)
{
break;
}
}
applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
}
}
}
catch (MissingKeyException)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
}
catch (InvalidDataException)
{
applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
}
}
else if (extension == ".nro")
{
BinaryReader reader = new(file);
byte[] Read(long position, int size)
{
file.Seek(position, SeekOrigin.Begin);
return reader.ReadBytes(size);
}
try
{
file.Seek(24, SeekOrigin.Begin);
int assetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
{
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
// Reads and stores game icon as byte array
if (iconSize > 0)
{
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
applicationIcon = _nroIcon;
}
}
else
{
applicationIcon = _nroIcon;
}
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
}
}
else if (extension == ".nca")
{
applicationIcon = _ncaIcon;
}
// If its an NSO we just set defaults
else if (extension == ".nso")
{
applicationIcon = _nsoIcon;
}
}
}
catch(Exception)
{
Logger.Warning?.Print(LogClass.Application, $"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}");
}
return applicationIcon ?? _ncaIcon;
}
private static string ConvertSecondsToFormattedString(double seconds)
{
System.TimeSpan time = System.TimeSpan.FromSeconds(seconds);
string timeString;
if (time.Days != 0)
{
timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m";
}
else if (time.Hours != 0)
{
timeString = $"{time.Hours:D2}h {time.Minutes:D2}m";
}
else if (time.Minutes != 0)
{
timeString = $"{time.Minutes:D2}m";
}
else
{
timeString = "Never";
}
return timeString;
}
private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
{
_ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage)
{
titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
}
else
{
titleName = null;
publisher = null;
}
if (string.IsNullOrWhiteSpace(titleName))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.NameString.IsEmpty())
{
titleName = controlTitle.NameString.ToString();
break;
}
}
}
if (string.IsNullOrWhiteSpace(publisher))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.PublisherString.IsEmpty())
{
publisher = controlTitle.PublisherString.ToString();
break;
}
}
}
if (controlData.PresenceGroupId != 0)
{
titleId = controlData.PresenceGroupId.ToString("x16");
}
else if (controlData.SaveDataOwnerId != 0)
{
titleId = controlData.SaveDataOwnerId.ToString();
}
else if (controlData.AddOnContentBaseId != 0)
{
titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
}
else
{
titleId = "0000000000000000";
}
version = controlData.DisplayVersionString.ToString();
}
private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs)
{
updatedControlFs = null;
string updatePath = "(unknown)";
try
{
(Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath);
if (patchNca != null && controlNca != null)
{
updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
return true;
}
}
catch (InvalidDataException)
{
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}");
}
catch (MissingKeyException exception)
{
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}");
}
return false;
}
public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
{
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
fileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
if (ncaProgramIndex != programIndex)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.Program)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
patchNca = nca;
}
else
{
mainNca = nca;
}
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
return (mainNca, patchNca, controlNca);
}
public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex)
{
Nca patchNca = null;
Nca controlNca = null;
fileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
if (ncaProgramIndex != programIndex)
{
continue;
}
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId)
{
break;
}
if (nca.Header.ContentType == NcaContentType.Program)
{
patchNca = nca;
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
return (patchNca, controlNca);
}
public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath)
{
updatePath = null;
if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
{
// Clear the program index part.
titleIdBase &= ~0xFUL;
// Load update information if exists.
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, TitleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath))
{
FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
}
}
}
return (null, null);
}
}
}