2020-03-25 01:14:35 -07:00
using LibHac.Common ;
2022-01-12 04:22:19 -07:00
using LibHac.Common.Keys ;
2020-01-12 02:10:55 +00:00
using LibHac.Fs ;
2020-09-01 13:08:59 -07:00
using LibHac.Fs.Fsa ;
2020-01-12 02:10:55 +00:00
using LibHac.FsSystem ;
using LibHac.Ncm ;
2022-01-12 04:22:19 -07:00
using LibHac.Tools.Fs ;
using LibHac.Tools.FsSystem ;
using LibHac.Tools.FsSystem.NcaUtils ;
using LibHac.Tools.Ncm ;
2020-06-20 23:08:14 +05:30
using Ryujinx.Common.Logging ;
2023-03-17 08:14:50 -04:00
using Ryujinx.Common.Memory ;
2022-11-09 20:22:43 +01:00
using Ryujinx.Common.Utilities ;
2020-01-12 02:10:55 +00:00
using Ryujinx.HLE.Exceptions ;
2022-01-13 23:29:04 +01:00
using Ryujinx.HLE.HOS.Services.Ssl ;
2019-10-08 05:48:49 +02:00
using Ryujinx.HLE.HOS.Services.Time ;
2024-07-16 23:17:32 +02:00
using Ryujinx.HLE.Utilities ;
2018-11-18 21:37:41 +02:00
using System ;
using System.Collections.Generic ;
using System.IO ;
2020-01-12 02:10:55 +00:00
using System.IO.Compression ;
2018-11-18 21:37:41 +02:00
using System.Linq ;
2023-10-05 07:41:00 -03:00
using System.Text ;
2024-11-29 00:32:07 +01:00
using System.Text.RegularExpressions ;
2024-12-19 21:52:25 -03:00
using System.Threading ;
2021-12-23 09:55:50 -07:00
using Path = System . IO . Path ;
2018-11-18 21:37:41 +02:00
2022-03-22 20:46:16 +01:00
namespace Ryujinx.HLE.FileSystem
2018-11-18 21:37:41 +02:00
{
2020-01-21 23:23:11 +01:00
public class ContentManager
2018-11-18 21:37:41 +02:00
{
2020-01-12 02:10:55 +00:00
private const ulong SystemVersionTitleId = 0x0100000000000809 ;
2023-07-16 19:31:14 +02:00
private const ulong SystemUpdateTitleId = 0x0100000000000816 ;
2020-01-12 02:10:55 +00:00
2018-12-06 05:16:24 -06:00
private Dictionary < StorageId , LinkedList < LocationEntry > > _locationEntries ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
private readonly Dictionary < string , ulong > _sharedFontTitleDictionary ;
private readonly Dictionary < ulong , string > _systemTitlesNameDictionary ;
private readonly Dictionary < string , string > _sharedFontFilenameDictionary ;
2018-11-18 21:37:41 +02:00
2020-01-12 02:10:55 +00:00
private SortedDictionary < ( ulong titleId , NcaContentType type ) , string > _contentDictionary ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
private readonly struct AocItem
2020-06-20 23:08:14 +05:30
{
public readonly string ContainerPath ;
public readonly string NcaPath ;
2022-12-07 15:00:28 +01:00
public AocItem ( string containerPath , string ncaPath )
2020-06-20 23:08:14 +05:30
{
ContainerPath = containerPath ;
NcaPath = ncaPath ;
}
}
2023-07-16 19:31:14 +02:00
private SortedList < ulong , AocItem > AocData { get ; }
2020-06-20 23:08:14 +05:30
2023-07-16 19:31:14 +02:00
private readonly VirtualFileSystem _virtualFileSystem ;
2018-11-18 21:37:41 +02:00
2024-12-19 21:52:25 -03:00
private readonly Lock _lock = new ( ) ;
2020-01-13 01:17:44 +01:00
2020-01-21 23:23:11 +01:00
public ContentManager ( VirtualFileSystem virtualFileSystem )
2018-11-18 21:37:41 +02:00
{
2019-10-17 01:17:44 -05:00
_contentDictionary = new SortedDictionary < ( ulong , NcaContentType ) , string > ( ) ;
2023-07-16 19:31:14 +02:00
_locationEntries = new Dictionary < StorageId , LinkedList < LocationEntry > > ( ) ;
2018-11-18 21:37:41 +02:00
2021-04-24 12:16:01 +02:00
_sharedFontTitleDictionary = new Dictionary < string , ulong >
2018-11-18 21:37:41 +02:00
{
{ "FontStandard" , 0x0100000000000811 } ,
{ "FontChineseSimplified" , 0x0100000000000814 } ,
{ "FontExtendedChineseSimplified" , 0x0100000000000814 } ,
{ "FontKorean" , 0x0100000000000812 } ,
{ "FontChineseTraditional" , 0x0100000000000813 } ,
2023-07-16 19:31:14 +02:00
{ "FontNintendoExtended" , 0x0100000000000810 } ,
2018-11-18 21:37:41 +02:00
} ;
2021-04-24 12:16:01 +02:00
_systemTitlesNameDictionary = new Dictionary < ulong , string > ( )
2020-03-29 23:23:05 +02:00
{
{ 0x010000000000080E , "TimeZoneBinary" } ,
{ 0x0100000000000810 , "FontNintendoExtension" } ,
{ 0x0100000000000811 , "FontStandard" } ,
{ 0x0100000000000812 , "FontKorean" } ,
{ 0x0100000000000813 , "FontChineseTraditional" } ,
{ 0x0100000000000814 , "FontChineseSimple" } ,
} ;
2019-05-31 19:31:10 -05:00
_sharedFontFilenameDictionary = new Dictionary < string , string >
{
{ "FontStandard" , "nintendo_udsg-r_std_003.bfttf" } ,
{ "FontChineseSimplified" , "nintendo_udsg-r_org_zh-cn_003.bfttf" } ,
{ "FontExtendedChineseSimplified" , "nintendo_udsg-r_ext_zh-cn_003.bfttf" } ,
{ "FontKorean" , "nintendo_udsg-r_ko_003.bfttf" } ,
{ "FontChineseTraditional" , "nintendo_udjxh-db_zh-tw_003.bfttf" } ,
2023-07-16 19:31:14 +02:00
{ "FontNintendoExtended" , "nintendo_ext_003.bfttf" } ,
2019-05-31 19:31:10 -05:00
} ;
2020-01-21 23:23:11 +01:00
_virtualFileSystem = virtualFileSystem ;
2020-06-20 23:08:14 +05:30
2023-07-16 19:31:14 +02:00
AocData = new SortedList < ulong , AocItem > ( ) ;
2018-11-18 21:37:41 +02:00
}
2020-01-21 23:23:11 +01:00
public void LoadEntries ( Switch device = null )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
_contentDictionary = new SortedDictionary < ( ulong , NcaContentType ) , string > ( ) ;
2023-07-16 19:31:14 +02:00
_locationEntries = new Dictionary < StorageId , LinkedList < LocationEntry > > ( ) ;
2018-11-18 21:37:41 +02:00
2022-02-13 14:50:07 +01:00
foreach ( StorageId storageId in Enum . GetValues < StorageId > ( ) )
2018-11-18 21:37:41 +02:00
{
2025-01-25 14:13:18 -06:00
if ( ! ContentPath . TryGetContentPath ( storageId , out string contentPathString ) )
2020-01-13 01:17:44 +01:00
{
2024-04-07 17:55:34 -03:00
continue ;
2020-01-13 01:17:44 +01:00
}
2025-05-30 17:08:34 -05:00
2025-01-25 14:13:18 -06:00
if ( ! ContentPath . TryGetRealPath ( contentPathString , out string contentDirectory ) )
2020-01-13 01:17:44 +01:00
{
continue ;
}
2025-05-30 17:08:34 -05:00
2025-01-25 14:13:18 -06:00
string registeredDirectory = Path . Combine ( contentDirectory , "registered" ) ;
2018-11-18 21:37:41 +02:00
2020-01-13 01:17:44 +01:00
Directory . CreateDirectory ( registeredDirectory ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
LinkedList < LocationEntry > locationList = new ( ) ;
2018-11-18 21:37:41 +02:00
2020-01-13 01:17:44 +01:00
void AddEntry ( LocationEntry entry )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
locationList . AddLast ( entry ) ;
}
2018-11-18 21:37:41 +02:00
2020-01-13 01:17:44 +01:00
foreach ( string directoryPath in Directory . EnumerateDirectories ( registeredDirectory ) )
{
if ( Directory . GetFiles ( directoryPath ) . Length > 0 )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
string ncaName = new DirectoryInfo ( directoryPath ) . Name . Replace ( ".nca" , string . Empty ) ;
2023-07-16 19:31:14 +02:00
using FileStream ncaFile = File . OpenRead ( Directory . GetFiles ( directoryPath ) [ 0 ] ) ;
Nca nca = new ( _virtualFileSystem . KeySet , ncaFile . AsStorage ( ) ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
string switchPath = contentPathString + ":/" + ncaFile . Name . Replace ( contentDirectory , string . Empty ) . TrimStart ( Path . DirectorySeparatorChar ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
// Change path format to switch's
switchPath = switchPath . Replace ( '\\' , '/' ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
LocationEntry entry = new ( switchPath , 0 , nca . Header . TitleId , nca . Header . ContentType ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
AddEntry ( entry ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
_contentDictionary . Add ( ( nca . Header . TitleId , nca . Header . ContentType ) , ncaName ) ;
2018-11-18 21:37:41 +02:00
}
}
2020-01-13 01:17:44 +01:00
foreach ( string filePath in Directory . EnumerateFiles ( contentDirectory ) )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
if ( Path . GetExtension ( filePath ) = = ".nca" )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
string ncaName = Path . GetFileNameWithoutExtension ( filePath ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
using FileStream ncaFile = new ( filePath , FileMode . Open , FileAccess . Read ) ;
Nca nca = new ( _virtualFileSystem . KeySet , ncaFile . AsStorage ( ) ) ;
2020-01-13 01:17:44 +01:00
2023-07-16 19:31:14 +02:00
string switchPath = contentPathString + ":/" + filePath . Replace ( contentDirectory , string . Empty ) . TrimStart ( Path . DirectorySeparatorChar ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
// Change path format to switch's
switchPath = switchPath . Replace ( '\\' , '/' ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
LocationEntry entry = new ( switchPath , 0 , nca . Header . TitleId , nca . Header . ContentType ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
AddEntry ( entry ) ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
_contentDictionary . Add ( ( nca . Header . TitleId , nca . Header . ContentType ) , ncaName ) ;
2018-11-18 21:37:41 +02:00
}
}
2025-01-25 14:13:18 -06:00
if ( _locationEntries . TryGetValue ( storageId , out LinkedList < LocationEntry > locationEntriesItem ) & & locationEntriesItem ? . Count = = 0 )
2020-01-13 01:17:44 +01:00
{
_locationEntries . Remove ( storageId ) ;
}
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
_locationEntries . TryAdd ( storageId , locationList ) ;
2018-11-18 21:37:41 +02:00
}
2019-07-04 17:20:40 +02:00
2020-01-21 23:23:11 +01:00
if ( device ! = null )
{
TimeManager . Instance . InitializeTimeZone ( device ) ;
2022-01-13 23:29:04 +01:00
BuiltInCertificateManager . Instance . Initialize ( device ) ;
2021-09-15 01:24:49 +02:00
device . System . SharedFontManager . Initialize ( ) ;
2020-01-21 23:23:11 +01:00
}
2020-01-13 01:17:44 +01:00
}
2018-11-18 21:37:41 +02:00
}
2022-12-07 15:00:28 +01:00
public void AddAocItem ( ulong titleId , string containerPath , string ncaPath , bool mergedToContainer = false )
2020-06-23 01:32:07 +01:00
{
2022-12-07 15:00:28 +01:00
// TODO: Check Aoc version.
2023-07-16 19:31:14 +02:00
if ( ! AocData . TryAdd ( titleId , new AocItem ( containerPath , ncaPath ) ) )
2020-06-23 01:32:07 +01:00
{
2020-08-04 05:02:53 +05:30
Logger . Warning ? . Print ( LogClass . Application , $"Duplicate AddOnContent detected. TitleId {titleId:X16}" ) ;
2020-06-23 01:32:07 +01:00
}
else
{
2020-08-04 05:02:53 +05:30
Logger . Info ? . Print ( LogClass . Application , $"Found AddOnContent with TitleId {titleId:X16}" ) ;
2020-06-23 12:25:12 +01:00
2022-12-07 15:00:28 +01:00
if ( ! mergedToContainer )
2020-06-23 12:25:12 +01:00
{
2025-01-25 14:13:18 -06:00
using IFileSystem pfs = PartitionFileSystemUtils . OpenApplicationFileSystem ( containerPath , _virtualFileSystem ) ;
2020-06-23 12:25:12 +01:00
}
2020-06-23 01:32:07 +01:00
}
}
2023-07-16 19:31:14 +02:00
public void ClearAocData ( ) = > AocData . Clear ( ) ;
2020-06-20 23:08:14 +05:30
2023-07-16 19:31:14 +02:00
public int GetAocCount ( ) = > AocData . Count ;
2020-06-20 23:08:14 +05:30
2023-07-16 19:31:14 +02:00
public IList < ulong > GetAocTitleIds ( ) = > AocData . Select ( e = > e . Key ) . ToList ( ) ;
2020-06-20 23:08:14 +05:30
2021-05-16 17:12:14 +02:00
public bool GetAocDataStorage ( ulong aocTitleId , out IStorage aocStorage , IntegrityCheckLevel integrityCheckLevel )
2020-06-20 23:08:14 +05:30
{
aocStorage = null ;
2023-07-16 19:31:14 +02:00
if ( AocData . TryGetValue ( aocTitleId , out AocItem aoc ) )
2020-06-20 23:08:14 +05:30
{
2025-01-26 15:15:26 -06:00
FileStream file = new ( aoc . ContainerPath , FileMode . Open , FileAccess . Read ) ;
using UniqueRef < IFile > ncaFile = new ( ) ;
2020-06-20 23:08:14 +05:30
switch ( Path . GetExtension ( aoc . ContainerPath ) )
{
case ".xci" :
2025-01-25 14:13:18 -06:00
XciPartition xci = new Xci ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) . OpenPartition ( XciPartitionType . Secure ) ;
2023-10-23 10:34:31 -07:00
xci . OpenFile ( ref ncaFile . Ref , aoc . NcaPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-06-20 23:08:14 +05:30
break ;
case ".nsp" :
2025-01-26 15:15:26 -06:00
PartitionFileSystem pfs = new ( ) ;
2023-10-22 16:30:46 -07:00
pfs . Initialize ( file . AsStorage ( ) ) ;
2023-10-23 10:34:31 -07:00
pfs . OpenFile ( ref ncaFile . Ref , aoc . NcaPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-06-20 23:08:14 +05:30
break ;
default :
return false ; // Print error?
}
2021-12-23 09:55:50 -07:00
aocStorage = new Nca ( _virtualFileSystem . KeySet , ncaFile . Get . AsStorage ( ) ) . OpenStorage ( NcaSectionType . Data , integrityCheckLevel ) ;
2022-11-09 20:22:43 +01:00
2020-06-20 23:08:14 +05:30
return true ;
}
return false ;
}
2021-04-24 12:16:01 +02:00
public void ClearEntry ( ulong titleId , NcaContentType contentType , StorageId storageId )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
{
RemoveLocationEntry ( titleId , contentType , storageId ) ;
}
2018-11-18 21:37:41 +02:00
}
2018-12-06 05:16:24 -06:00
public void RefreshEntries ( StorageId storageId , int flag )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
2018-11-18 21:37:41 +02:00
{
2023-07-16 19:31:14 +02:00
LinkedList < LocationEntry > locationList = _locationEntries [ storageId ] ;
2020-01-13 01:17:44 +01:00
LinkedListNode < LocationEntry > locationEntry = locationList . First ;
2018-11-18 21:37:41 +02:00
2020-01-13 01:17:44 +01:00
while ( locationEntry ! = null )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
LinkedListNode < LocationEntry > nextLocationEntry = locationEntry . Next ;
2018-11-18 21:37:41 +02:00
2020-01-13 01:17:44 +01:00
if ( locationEntry . Value . Flag = = flag )
{
locationList . Remove ( locationEntry . Value ) ;
}
locationEntry = nextLocationEntry ;
}
2018-11-18 21:37:41 +02:00
}
}
2018-12-06 05:16:24 -06:00
public bool HasNca ( string ncaId , StorageId storageId )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
if ( _contentDictionary . ContainsValue ( ncaId ) )
{
2025-01-25 14:13:18 -06:00
KeyValuePair < ( ulong titleId , NcaContentType type ) , string > content = _contentDictionary . FirstOrDefault ( x = > x . Value = = ncaId ) ;
2023-07-16 19:31:14 +02:00
ulong titleId = content . Key . titleId ;
2020-01-12 02:10:55 +00:00
2020-01-13 01:17:44 +01:00
NcaContentType contentType = content . Key . type ;
StorageId storage = GetInstalledStorage ( titleId , contentType , storageId ) ;
2018-11-18 21:37:41 +02:00
2020-01-13 01:17:44 +01:00
return storage = = storageId ;
}
2018-11-18 21:37:41 +02:00
}
return false ;
}
2021-04-24 12:16:01 +02:00
public UInt128 GetInstalledNcaId ( ulong titleId , NcaContentType contentType )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
2018-11-18 21:37:41 +02:00
{
2025-01-25 14:13:18 -06:00
if ( _contentDictionary . TryGetValue ( ( titleId , contentType ) , out string contentDictionaryItem ) )
2020-01-13 01:17:44 +01:00
{
2023-06-09 08:05:32 -03:00
return UInt128Utils . FromHex ( contentDictionaryItem ) ;
2020-01-13 01:17:44 +01:00
}
2018-11-18 21:37:41 +02:00
}
return new UInt128 ( ) ;
}
2021-04-24 12:16:01 +02:00
public StorageId GetInstalledStorage ( ulong titleId , NcaContentType contentType , StorageId storageId )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
{
LocationEntry locationEntry = GetLocation ( titleId , contentType , storageId ) ;
2018-11-18 21:37:41 +02:00
2022-03-22 20:46:16 +01:00
return locationEntry . ContentPath ! = null ? ContentPath . GetStorageId ( locationEntry . ContentPath ) : StorageId . None ;
2020-01-13 01:17:44 +01:00
}
2018-11-18 21:37:41 +02:00
}
2021-04-24 12:16:01 +02:00
public string GetInstalledContentPath ( ulong titleId , StorageId storageId , NcaContentType contentType )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
LocationEntry locationEntry = GetLocation ( titleId , contentType , storageId ) ;
if ( VerifyContentType ( locationEntry , contentType ) )
{
return locationEntry . ContentPath ;
}
2018-11-18 21:37:41 +02:00
}
return string . Empty ;
}
2018-12-06 05:16:24 -06:00
public void RedirectLocation ( LocationEntry newEntry , StorageId storageId )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
lock ( _lock )
2018-11-18 21:37:41 +02:00
{
2020-01-13 01:17:44 +01:00
LocationEntry locationEntry = GetLocation ( newEntry . TitleId , newEntry . ContentType , storageId ) ;
2018-11-18 21:37:41 +02:00
2020-01-13 01:17:44 +01:00
if ( locationEntry . ContentPath ! = null )
{
RemoveLocationEntry ( newEntry . TitleId , newEntry . ContentType , storageId ) ;
}
AddLocationEntry ( newEntry , storageId ) ;
}
2018-11-18 21:37:41 +02:00
}
2019-10-17 01:17:44 -05:00
private bool VerifyContentType ( LocationEntry locationEntry , NcaContentType contentType )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
if ( locationEntry . ContentPath = = null )
2018-11-19 11:20:17 +11:00
{
return false ;
}
2022-11-09 20:22:43 +01:00
2023-07-16 19:31:14 +02:00
string installedPath = VirtualFileSystem . SwitchPathToSystemPath ( locationEntry . ContentPath ) ;
2018-11-18 21:37:41 +02:00
2018-12-06 05:16:24 -06:00
if ( ! string . IsNullOrWhiteSpace ( installedPath ) )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
if ( File . Exists ( installedPath ) )
2018-11-18 21:37:41 +02:00
{
2023-07-16 19:31:14 +02:00
using FileStream file = new ( installedPath , FileMode . Open , FileAccess . Read ) ;
Nca nca = new ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
bool contentCheck = nca . Header . ContentType = = contentType ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
return contentCheck ;
2018-11-18 21:37:41 +02:00
}
}
return false ;
}
2018-12-06 05:16:24 -06:00
private void AddLocationEntry ( LocationEntry entry , StorageId storageId )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
LinkedList < LocationEntry > locationList = null ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
if ( _locationEntries . TryGetValue ( storageId , out LinkedList < LocationEntry > locationEntry ) )
2018-11-18 21:37:41 +02:00
{
2023-07-16 19:31:14 +02:00
locationList = locationEntry ;
2018-11-18 21:37:41 +02:00
}
2018-12-06 05:16:24 -06:00
if ( locationList ! = null )
2018-11-18 21:37:41 +02:00
{
2023-11-15 10:41:31 -06:00
locationList . Remove ( entry ) ;
2018-11-18 21:37:41 +02:00
2018-12-06 05:16:24 -06:00
locationList . AddLast ( entry ) ;
2018-11-18 21:37:41 +02:00
}
}
2021-04-24 12:16:01 +02:00
private void RemoveLocationEntry ( ulong titleId , NcaContentType contentType , StorageId storageId )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
LinkedList < LocationEntry > locationList = null ;
2018-11-18 21:37:41 +02:00
2023-07-16 19:31:14 +02:00
if ( _locationEntries . TryGetValue ( storageId , out LinkedList < LocationEntry > locationEntry ) )
2018-11-18 21:37:41 +02:00
{
2023-07-16 19:31:14 +02:00
locationList = locationEntry ;
2018-11-18 21:37:41 +02:00
}
2018-12-06 05:16:24 -06:00
if ( locationList ! = null )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
LocationEntry entry =
2024-12-19 21:52:25 -03:00
locationList . ToList ( ) . FirstOrDefault ( x = > x . TitleId = = titleId & & x . ContentType = = contentType ) ;
2018-11-18 21:37:41 +02:00
2018-12-06 05:16:24 -06:00
if ( entry . ContentPath ! = null )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
locationList . Remove ( entry ) ;
2018-11-18 21:37:41 +02:00
}
}
}
2021-04-24 12:16:01 +02:00
public bool TryGetFontTitle ( string fontName , out ulong titleId )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
return _sharedFontTitleDictionary . TryGetValue ( fontName , out titleId ) ;
2018-11-18 21:37:41 +02:00
}
2019-05-31 19:31:10 -05:00
public bool TryGetFontFilename ( string fontName , out string filename )
{
return _sharedFontFilenameDictionary . TryGetValue ( fontName , out filename ) ;
}
2021-04-24 12:16:01 +02:00
public bool TryGetSystemTitlesName ( ulong titleId , out string name )
2020-03-29 23:23:05 +02:00
{
return _systemTitlesNameDictionary . TryGetValue ( titleId , out name ) ;
}
2021-04-24 12:16:01 +02:00
private LocationEntry GetLocation ( ulong titleId , NcaContentType contentType , StorageId storageId )
2018-11-18 21:37:41 +02:00
{
2018-12-06 05:16:24 -06:00
LinkedList < LocationEntry > locationList = _locationEntries [ storageId ] ;
2018-11-18 21:37:41 +02:00
2024-12-19 21:52:25 -03:00
return locationList . ToList ( ) . FirstOrDefault ( x = > x . TitleId = = titleId & & x . ContentType = = contentType ) ;
2018-11-18 21:37:41 +02:00
}
2020-01-12 02:10:55 +00:00
public void InstallFirmware ( string firmwareSource )
{
2025-01-25 14:13:18 -06:00
ContentPath . TryGetContentPath ( StorageId . BuiltInSystem , out string contentPathString ) ;
ContentPath . TryGetRealPath ( contentPathString , out string contentDirectory ) ;
2020-01-12 02:10:55 +00:00
string registeredDirectory = Path . Combine ( contentDirectory , "registered" ) ;
2023-07-16 19:31:14 +02:00
string temporaryDirectory = Path . Combine ( contentDirectory , "temp" ) ;
2020-01-12 02:10:55 +00:00
if ( Directory . Exists ( temporaryDirectory ) )
{
Directory . Delete ( temporaryDirectory , true ) ;
}
if ( Directory . Exists ( firmwareSource ) )
{
InstallFromDirectory ( firmwareSource , temporaryDirectory ) ;
FinishInstallation ( temporaryDirectory , registeredDirectory ) ;
return ;
}
if ( ! File . Exists ( firmwareSource ) )
{
throw new FileNotFoundException ( "Firmware file does not exist." ) ;
}
2023-07-16 19:31:14 +02:00
FileInfo info = new ( firmwareSource ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
using FileStream file = File . OpenRead ( firmwareSource ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
switch ( info . Extension )
{
case ".zip" :
using ( ZipArchive archive = ZipFile . OpenRead ( firmwareSource ) )
{
InstallFromZip ( archive , temporaryDirectory ) ;
}
2025-05-30 17:08:34 -05:00
2023-07-16 19:31:14 +02:00
break ;
case ".xci" :
Xci xci = new ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
InstallFromCart ( xci , temporaryDirectory ) ;
break ;
default :
throw new InvalidFirmwarePackageException ( "Input file is not a valid firmware package" ) ;
2020-01-12 02:10:55 +00:00
}
2023-07-16 19:31:14 +02:00
FinishInstallation ( temporaryDirectory , registeredDirectory ) ;
2020-01-12 02:10:55 +00:00
}
2025-05-30 17:08:34 -05:00
public static void InstallKeys ( string keysSource , string installDirectory )
2024-11-29 00:32:07 +01:00
{
if ( Directory . Exists ( keysSource ) )
{
2025-01-25 14:13:18 -06:00
foreach ( string filePath in Directory . EnumerateFiles ( keysSource , "*.keys" ) )
2024-11-29 00:32:07 +01:00
{
VerifyKeysFile ( filePath ) ;
File . Copy ( filePath , Path . Combine ( installDirectory , Path . GetFileName ( filePath ) ) , true ) ;
}
return ;
}
if ( ! File . Exists ( keysSource ) )
{
throw new FileNotFoundException ( "Keys file does not exist." ) ;
}
FileInfo info = new ( keysSource ) ;
using FileStream file = File . OpenRead ( keysSource ) ;
switch ( info . Extension )
{
case ".zip" :
using ( ZipArchive archive = ZipFile . OpenRead ( keysSource ) )
{
InstallKeysFromZip ( archive , installDirectory ) ;
}
2025-05-30 17:08:34 -05:00
2024-11-29 00:32:07 +01:00
break ;
case ".keys" :
VerifyKeysFile ( keysSource ) ;
File . Copy ( keysSource , Path . Combine ( installDirectory , info . Name ) , true ) ;
break ;
default :
throw new InvalidFirmwarePackageException ( "Input file is not a valid key package" ) ;
}
}
2025-05-30 17:08:34 -05:00
private static void InstallKeysFromZip ( ZipArchive archive , string installDirectory )
2024-11-29 00:32:07 +01:00
{
string temporaryDirectory = Path . Combine ( installDirectory , "temp" ) ;
if ( Directory . Exists ( temporaryDirectory ) )
{
Directory . Delete ( temporaryDirectory , true ) ;
}
2025-05-30 17:08:34 -05:00
2024-11-29 00:32:07 +01:00
Directory . CreateDirectory ( temporaryDirectory ) ;
2025-01-25 14:13:18 -06:00
foreach ( ZipArchiveEntry entry in archive . Entries )
2024-11-29 00:32:07 +01:00
{
if ( Path . GetExtension ( entry . FullName ) . Equals ( ".keys" , StringComparison . OrdinalIgnoreCase ) )
{
string extractDestination = Path . Combine ( temporaryDirectory , entry . Name ) ;
entry . ExtractToFile ( extractDestination , overwrite : true ) ;
try
{
VerifyKeysFile ( extractDestination ) ;
File . Move ( extractDestination , Path . Combine ( installDirectory , entry . Name ) , true ) ;
}
catch ( Exception )
{
Directory . Delete ( temporaryDirectory , true ) ;
throw ;
}
}
}
2025-05-30 17:08:34 -05:00
2024-11-29 00:32:07 +01:00
Directory . Delete ( temporaryDirectory , true ) ;
}
2020-01-12 02:10:55 +00:00
private void FinishInstallation ( string temporaryDirectory , string registeredDirectory )
{
if ( Directory . Exists ( registeredDirectory ) )
{
new DirectoryInfo ( registeredDirectory ) . Delete ( true ) ;
}
Directory . Move ( temporaryDirectory , registeredDirectory ) ;
LoadEntries ( ) ;
}
private void InstallFromDirectory ( string firmwareDirectory , string temporaryDirectory )
{
InstallFromPartition ( new LocalFileSystem ( firmwareDirectory ) , temporaryDirectory ) ;
}
private void InstallFromPartition ( IFileSystem filesystem , string temporaryDirectory )
{
2025-01-25 14:13:18 -06:00
foreach ( DirectoryEntryEx entry in filesystem . EnumerateEntries ( "/" , "*.nca" ) )
2020-01-12 02:10:55 +00:00
{
2023-07-16 19:31:14 +02:00
Nca nca = new ( _virtualFileSystem . KeySet , OpenPossibleFragmentedFile ( filesystem , entry . FullPath , OpenMode . Read ) . AsStorage ( ) ) ;
2020-01-12 02:10:55 +00:00
2025-05-30 17:08:34 -05:00
SaveNca ( nca , entry . Name [ . . entry . Name . IndexOf ( '.' ) ] , temporaryDirectory ) ;
2020-01-12 02:10:55 +00:00
}
}
private void InstallFromCart ( Xci gameCard , string temporaryDirectory )
{
if ( gameCard . HasPartition ( XciPartitionType . Update ) )
{
XciPartition partition = gameCard . OpenPartition ( XciPartitionType . Update ) ;
InstallFromPartition ( partition , temporaryDirectory ) ;
}
else
{
throw new Exception ( "Update not found in xci file." ) ;
}
}
2023-07-16 19:31:14 +02:00
private static void InstallFromZip ( ZipArchive archive , string temporaryDirectory )
2020-01-12 02:10:55 +00:00
{
2025-01-25 14:13:18 -06:00
foreach ( ZipArchiveEntry entry in archive . Entries )
2020-01-12 02:10:55 +00:00
{
2023-07-16 19:31:14 +02:00
if ( entry . FullName . EndsWith ( ".nca" ) | | entry . FullName . EndsWith ( ".nca/00" ) )
2020-01-12 02:10:55 +00:00
{
2023-07-16 19:31:14 +02:00
// Clean up the name and get the NcaId
2020-01-12 02:10:55 +00:00
2024-11-01 11:57:23 -05:00
string [ ] pathComponents = entry . FullName . Replace ( ".cnmt" , string . Empty ) . Split ( '/' ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
string ncaId = pathComponents [ ^ 1 ] ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
// If this is a fragmented nca, we need to get the previous element.GetZip
if ( ncaId . Equals ( "00" ) )
{
ncaId = pathComponents [ ^ 2 ] ;
}
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
if ( ncaId . Contains ( ".nca" ) )
{
string newPath = Path . Combine ( temporaryDirectory , ncaId ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
Directory . CreateDirectory ( newPath ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
entry . ExtractToFile ( Path . Combine ( newPath , "00" ) ) ;
2020-01-12 02:10:55 +00:00
}
}
}
}
2023-07-16 19:31:14 +02:00
public static void SaveNca ( Nca nca , string ncaId , string temporaryDirectory )
2020-01-12 02:10:55 +00:00
{
string newPath = Path . Combine ( temporaryDirectory , ncaId + ".nca" ) ;
Directory . CreateDirectory ( newPath ) ;
2023-07-16 19:31:14 +02:00
using FileStream file = File . Create ( Path . Combine ( newPath , "00" ) ) ;
nca . BaseStorage . AsStream ( ) . CopyTo ( file ) ;
2020-01-12 02:10:55 +00:00
}
2023-07-16 19:31:14 +02:00
private static IFile OpenPossibleFragmentedFile ( IFileSystem filesystem , string path , OpenMode mode )
2020-01-12 02:10:55 +00:00
{
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > file = new ( ) ;
2020-01-12 02:10:55 +00:00
if ( filesystem . FileExists ( $"{path}/00" ) )
{
2023-10-23 10:34:31 -07:00
filesystem . OpenFile ( ref file . Ref , $"{path}/00" . ToU8Span ( ) , mode ) . ThrowIfFailure ( ) ;
2020-01-12 02:10:55 +00:00
}
else
{
2023-10-23 10:34:31 -07:00
filesystem . OpenFile ( ref file . Ref , path . ToU8Span ( ) , mode ) . ThrowIfFailure ( ) ;
2020-01-12 02:10:55 +00:00
}
2021-12-23 09:55:50 -07:00
return file . Release ( ) ;
2020-01-12 02:10:55 +00:00
}
2025-05-30 17:08:34 -05:00
private static MemoryStream GetZipStream ( ZipArchiveEntry entry )
2020-01-12 02:10:55 +00:00
{
2023-03-17 08:14:50 -04:00
MemoryStream dest = MemoryStreamManager . Shared . GetStream ( ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
using Stream src = entry . Open ( ) ;
src . CopyTo ( dest ) ;
2020-01-12 02:10:55 +00:00
return dest ;
}
public SystemVersion VerifyFirmwarePackage ( string firmwarePackage )
{
2021-08-12 14:56:24 -07:00
_virtualFileSystem . ReloadKeySet ( ) ;
2021-01-26 23:15:07 +05:30
// LibHac.NcaHeader's DecryptHeader doesn't check if HeaderKey is empty and throws InvalidDataException instead
// So, we check it early for a better user experience.
2021-08-12 14:56:24 -07:00
if ( _virtualFileSystem . KeySet . HeaderKey . IsZeros ( ) )
2021-01-26 23:15:07 +05:30
throw new MissingKeyException ( "HeaderKey is empty. Cannot decrypt NCA headers." ) ;
2024-10-21 23:16:41 -05:00
2023-07-16 19:31:14 +02:00
Dictionary < ulong , List < ( NcaContentType type , string path ) > > updateNcas = new ( ) ;
2020-01-12 02:10:55 +00:00
if ( Directory . Exists ( firmwarePackage ) )
return VerifyAndGetVersionDirectory ( firmwarePackage ) ;
2024-10-21 23:16:41 -05:00
2020-01-12 02:10:55 +00:00
if ( ! File . Exists ( firmwarePackage ) )
throw new FileNotFoundException ( "Firmware file does not exist." ) ;
2023-07-16 19:31:14 +02:00
FileInfo info = new ( firmwarePackage ) ;
using FileStream file = File . OpenRead ( firmwarePackage ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
switch ( info . Extension )
2020-01-12 02:10:55 +00:00
{
2023-07-16 19:31:14 +02:00
case ".zip" :
using ( ZipArchive archive = ZipFile . OpenRead ( firmwarePackage ) )
return VerifyAndGetVersionZip ( archive ) ;
case ".xci" :
Xci xci = new ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
2020-01-12 02:10:55 +00:00
2024-10-12 21:43:02 -05:00
if ( ! xci . HasPartition ( XciPartitionType . Update ) )
2023-07-16 19:31:14 +02:00
throw new InvalidFirmwarePackageException ( "Update not found in xci file." ) ;
2020-01-12 02:10:55 +00:00
2024-10-12 21:43:02 -05:00
XciPartition partition = xci . OpenPartition ( XciPartitionType . Update ) ;
return VerifyAndGetVersion ( partition ) ;
2020-01-12 02:10:55 +00:00
}
2024-10-12 21:43:02 -05:00
return null ;
2024-10-21 23:16:41 -05:00
SystemVersion VerifyAndGetVersionDirectory ( string firmwareDirectory )
2024-10-12 21:43:02 -05:00
= > VerifyAndGetVersion ( new LocalFileSystem ( firmwareDirectory ) ) ;
2020-01-12 02:10:55 +00:00
SystemVersion VerifyAndGetVersionZip ( ZipArchive archive )
{
SystemVersion systemVersion = null ;
2025-01-25 14:13:18 -06:00
foreach ( ZipArchiveEntry entry in archive . Entries )
2020-01-12 02:10:55 +00:00
{
if ( entry . FullName . EndsWith ( ".nca" ) | | entry . FullName . EndsWith ( ".nca/00" ) )
{
2023-07-16 19:31:14 +02:00
using Stream ncaStream = GetZipStream ( entry ) ;
IStorage storage = ncaStream . AsStorage ( ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
Nca nca = new ( _virtualFileSystem . KeySet , storage ) ;
2020-01-12 02:10:55 +00:00
2025-01-25 14:13:18 -06:00
if ( updateNcas . TryGetValue ( nca . Header . TitleId , out List < ( NcaContentType type , string path ) > updateNcasItem ) )
2023-07-16 19:31:14 +02:00
{
updateNcasItem . Add ( ( nca . Header . ContentType , entry . FullName ) ) ;
}
2025-01-22 23:58:11 -06:00
else if ( updateNcas . TryAdd ( nca . Header . TitleId , new List < ( NcaContentType , string ) > ( ) ) )
2023-07-16 19:31:14 +02:00
{
updateNcas [ nca . Header . TitleId ] . Add ( ( nca . Header . ContentType , entry . FullName ) ) ;
2020-01-12 02:10:55 +00:00
}
}
}
2025-01-25 14:13:18 -06:00
if ( updateNcas . TryGetValue ( SystemUpdateTitleId , out List < ( NcaContentType type , string path ) > ncaEntry ) )
2020-01-12 02:10:55 +00:00
{
2024-12-19 21:52:25 -03:00
string metaPath = ncaEntry . FirstOrDefault ( x = > x . type = = NcaContentType . Meta ) . path ;
2020-01-12 02:10:55 +00:00
CnmtContentMetaEntry [ ] metaEntries = null ;
2025-01-25 14:13:18 -06:00
ZipArchiveEntry fileEntry = archive . GetEntry ( metaPath ) ;
2020-01-12 02:10:55 +00:00
using ( Stream ncaStream = GetZipStream ( fileEntry ) )
{
2023-07-16 19:31:14 +02:00
Nca metaNca = new ( _virtualFileSystem . KeySet , ncaStream . AsStorage ( ) ) ;
2020-01-12 02:10:55 +00:00
2021-05-16 17:12:14 +02:00
IFileSystem fs = metaNca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . ErrorOnInvalid ) ;
2020-01-12 02:10:55 +00:00
string cnmtPath = fs . EnumerateEntries ( "/" , "*.cnmt" ) . Single ( ) . FullPath ;
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > metaFile = new ( ) ;
2021-12-23 09:55:50 -07:00
2023-03-01 18:42:27 -08:00
if ( fs . OpenFile ( ref metaFile . Ref , cnmtPath . ToU8Span ( ) , OpenMode . Read ) . IsSuccess ( ) )
2020-01-12 02:10:55 +00:00
{
2025-01-26 15:15:26 -06:00
Cnmt meta = new ( metaFile . Get . AsStream ( ) ) ;
2020-01-12 02:10:55 +00:00
if ( meta . Type = = ContentMetaType . SystemUpdate )
{
metaEntries = meta . MetaEntries ;
updateNcas . Remove ( SystemUpdateTitleId ) ;
2022-08-17 02:05:15 -05:00
}
2020-01-12 02:10:55 +00:00
}
}
if ( metaEntries = = null )
{
throw new FileNotFoundException ( "System update title was not found in the firmware package." ) ;
}
2025-01-25 14:13:18 -06:00
if ( updateNcas . TryGetValue ( SystemVersionTitleId , out List < ( NcaContentType type , string path ) > updateNcasItem ) )
2020-01-12 02:10:55 +00:00
{
2024-12-19 21:52:25 -03:00
string versionEntry = updateNcasItem . FirstOrDefault ( x = > x . type ! = NcaContentType . Meta ) . path ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
using Stream ncaStream = GetZipStream ( archive . GetEntry ( versionEntry ) ) ;
Nca nca = new ( _virtualFileSystem . KeySet , ncaStream . AsStorage ( ) ) ;
2020-01-12 02:10:55 +00:00
2025-01-25 14:13:18 -06:00
IFileSystem romfs = nca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . ErrorOnInvalid ) ;
2020-01-12 02:10:55 +00:00
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > systemVersionFile = new ( ) ;
2021-12-23 09:55:50 -07:00
2023-07-16 19:31:14 +02:00
if ( romfs . OpenFile ( ref systemVersionFile . Ref , "/file" . ToU8Span ( ) , OpenMode . Read ) . IsSuccess ( ) )
{
systemVersion = new SystemVersion ( systemVersionFile . Get . AsStream ( ) ) ;
2020-01-12 02:10:55 +00:00
}
}
foreach ( CnmtContentMetaEntry metaEntry in metaEntries )
{
if ( updateNcas . TryGetValue ( metaEntry . TitleId , out ncaEntry ) )
{
2024-12-19 21:52:25 -03:00
metaPath = ncaEntry . FirstOrDefault ( x = > x . type = = NcaContentType . Meta ) . path ;
2020-01-12 02:10:55 +00:00
2024-12-19 21:52:25 -03:00
string contentPath = ncaEntry . FirstOrDefault ( x = > x . type ! = NcaContentType . Meta ) . path ;
2020-01-12 02:10:55 +00:00
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
// This is a perfect valid case, so we should just ignore the missing content nca and continue.
if ( contentPath = = null )
{
updateNcas . Remove ( metaEntry . TitleId ) ;
continue ;
}
2023-07-16 19:31:14 +02:00
ZipArchiveEntry metaZipEntry = archive . GetEntry ( metaPath ) ;
2020-01-12 02:10:55 +00:00
ZipArchiveEntry contentZipEntry = archive . GetEntry ( contentPath ) ;
2023-07-16 19:31:14 +02:00
using Stream metaNcaStream = GetZipStream ( metaZipEntry ) ;
using Stream contentNcaStream = GetZipStream ( contentZipEntry ) ;
Nca metaNca = new ( _virtualFileSystem . KeySet , metaNcaStream . AsStorage ( ) ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
IFileSystem fs = metaNca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . ErrorOnInvalid ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
string cnmtPath = fs . EnumerateEntries ( "/" , "*.cnmt" ) . Single ( ) . FullPath ;
2020-01-12 02:10:55 +00:00
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > metaFile = new ( ) ;
2021-12-23 09:55:50 -07:00
2023-07-16 19:31:14 +02:00
if ( fs . OpenFile ( ref metaFile . Ref , cnmtPath . ToU8Span ( ) , OpenMode . Read ) . IsSuccess ( ) )
{
2025-01-26 15:15:26 -06:00
Cnmt meta = new ( metaFile . Get . AsStream ( ) ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
IStorage contentStorage = contentNcaStream . AsStorage ( ) ;
if ( contentStorage . GetSize ( out long size ) . IsSuccess ( ) )
{
byte [ ] contentData = new byte [ size ] ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
Span < byte > content = new ( contentData ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
contentStorage . Read ( 0 , content ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
Span < byte > hash = new ( new byte [ 32 ] ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
LibHac . Crypto . Sha256 . GenerateSha256Hash ( content , hash ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
if ( LibHac . Common . Utilities . ArraysEqual ( hash . ToArray ( ) , meta . ContentEntries [ 0 ] . Hash ) )
{
updateNcas . Remove ( metaEntry . TitleId ) ;
2020-01-12 02:10:55 +00:00
}
}
}
}
}
if ( updateNcas . Count > 0 )
{
2023-10-05 07:41:00 -03:00
StringBuilder extraNcas = new ( ) ;
2020-01-12 02:10:55 +00:00
2025-01-25 14:13:18 -06:00
foreach ( KeyValuePair < ulong , List < ( NcaContentType type , string path ) > > entry in updateNcas )
2020-01-12 02:10:55 +00:00
{
2025-01-25 14:13:18 -06:00
foreach ( ( NcaContentType type , string path ) in entry . Value )
2020-01-12 02:10:55 +00:00
{
2023-10-05 07:41:00 -03:00
extraNcas . AppendLine ( path ) ;
2020-01-12 02:10:55 +00:00
}
}
throw new InvalidFirmwarePackageException ( $"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}" ) ;
}
}
else
{
throw new FileNotFoundException ( "System update title was not found in the firmware package." ) ;
}
return systemVersion ;
}
SystemVersion VerifyAndGetVersion ( IFileSystem filesystem )
{
SystemVersion systemVersion = null ;
CnmtContentMetaEntry [ ] metaEntries = null ;
2025-01-25 14:13:18 -06:00
foreach ( DirectoryEntryEx entry in filesystem . EnumerateEntries ( "/" , "*.nca" ) )
2020-01-12 02:10:55 +00:00
{
IStorage ncaStorage = OpenPossibleFragmentedFile ( filesystem , entry . FullPath , OpenMode . Read ) . AsStorage ( ) ;
2023-07-16 19:31:14 +02:00
Nca nca = new ( _virtualFileSystem . KeySet , ncaStorage ) ;
2020-01-12 02:10:55 +00:00
if ( nca . Header . TitleId = = SystemUpdateTitleId & & nca . Header . ContentType = = NcaContentType . Meta )
{
2021-05-16 17:12:14 +02:00
IFileSystem fs = nca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . ErrorOnInvalid ) ;
2020-01-12 02:10:55 +00:00
string cnmtPath = fs . EnumerateEntries ( "/" , "*.cnmt" ) . Single ( ) . FullPath ;
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > metaFile = new ( ) ;
2021-12-23 09:55:50 -07:00
2023-03-01 18:42:27 -08:00
if ( fs . OpenFile ( ref metaFile . Ref , cnmtPath . ToU8Span ( ) , OpenMode . Read ) . IsSuccess ( ) )
2020-01-12 02:10:55 +00:00
{
2025-01-26 15:15:26 -06:00
Cnmt meta = new ( metaFile . Get . AsStream ( ) ) ;
2020-01-12 02:10:55 +00:00
if ( meta . Type = = ContentMetaType . SystemUpdate )
{
metaEntries = meta . MetaEntries ;
}
2022-08-17 02:05:15 -05:00
}
2020-01-12 02:10:55 +00:00
continue ;
}
else if ( nca . Header . TitleId = = SystemVersionTitleId & & nca . Header . ContentType = = NcaContentType . Data )
{
2025-01-25 14:13:18 -06:00
IFileSystem romfs = nca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . ErrorOnInvalid ) ;
2020-01-12 02:10:55 +00:00
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > systemVersionFile = new ( ) ;
2021-12-23 09:55:50 -07:00
2023-03-01 18:42:27 -08:00
if ( romfs . OpenFile ( ref systemVersionFile . Ref , "/file" . ToU8Span ( ) , OpenMode . Read ) . IsSuccess ( ) )
2020-01-12 02:10:55 +00:00
{
2021-12-23 09:55:50 -07:00
systemVersion = new SystemVersion ( systemVersionFile . Get . AsStream ( ) ) ;
2020-01-12 02:10:55 +00:00
}
}
2025-01-25 14:13:18 -06:00
if ( updateNcas . TryGetValue ( nca . Header . TitleId , out List < ( NcaContentType type , string path ) > updateNcasItem ) )
2020-01-12 02:10:55 +00:00
{
2023-06-09 08:05:32 -03:00
updateNcasItem . Add ( ( nca . Header . ContentType , entry . FullPath ) ) ;
2020-01-12 02:10:55 +00:00
}
2025-01-22 23:58:11 -06:00
else if ( updateNcas . TryAdd ( nca . Header . TitleId , new List < ( NcaContentType , string ) > ( ) ) )
2020-01-12 02:10:55 +00:00
{
updateNcas [ nca . Header . TitleId ] . Add ( ( nca . Header . ContentType , entry . FullPath ) ) ;
}
ncaStorage . Dispose ( ) ;
}
if ( metaEntries = = null )
{
throw new FileNotFoundException ( "System update title was not found in the firmware package." ) ;
}
foreach ( CnmtContentMetaEntry metaEntry in metaEntries )
{
2025-01-25 14:13:18 -06:00
if ( updateNcas . TryGetValue ( metaEntry . TitleId , out List < ( NcaContentType type , string path ) > ncaEntry ) )
2020-01-12 02:10:55 +00:00
{
2024-12-19 21:52:25 -03:00
string metaNcaPath = ncaEntry . FirstOrDefault ( x = > x . type = = NcaContentType . Meta ) . path ;
string contentPath = ncaEntry . FirstOrDefault ( x = > x . type ! = NcaContentType . Meta ) . path ;
2020-01-12 02:10:55 +00:00
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
// This is a perfect valid case, so we should just ignore the missing content nca and continue.
if ( contentPath = = null )
{
updateNcas . Remove ( metaEntry . TitleId ) ;
continue ;
}
2023-07-16 19:31:14 +02:00
IStorage metaStorage = OpenPossibleFragmentedFile ( filesystem , metaNcaPath , OpenMode . Read ) . AsStorage ( ) ;
2020-01-12 02:10:55 +00:00
IStorage contentStorage = OpenPossibleFragmentedFile ( filesystem , contentPath , OpenMode . Read ) . AsStorage ( ) ;
2023-07-16 19:31:14 +02:00
Nca metaNca = new ( _virtualFileSystem . KeySet , metaStorage ) ;
2020-01-12 02:10:55 +00:00
2021-05-16 17:12:14 +02:00
IFileSystem fs = metaNca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . ErrorOnInvalid ) ;
2020-01-12 02:10:55 +00:00
string cnmtPath = fs . EnumerateEntries ( "/" , "*.cnmt" ) . Single ( ) . FullPath ;
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > metaFile = new ( ) ;
2021-12-23 09:55:50 -07:00
2023-03-01 18:42:27 -08:00
if ( fs . OpenFile ( ref metaFile . Ref , cnmtPath . ToU8Span ( ) , OpenMode . Read ) . IsSuccess ( ) )
2020-01-12 02:10:55 +00:00
{
2025-01-26 15:15:26 -06:00
Cnmt meta = new ( metaFile . Get . AsStream ( ) ) ;
2020-01-12 02:10:55 +00:00
if ( contentStorage . GetSize ( out long size ) . IsSuccess ( ) )
{
byte [ ] contentData = new byte [ size ] ;
2023-07-16 19:31:14 +02:00
Span < byte > content = new ( contentData ) ;
2020-01-12 02:10:55 +00:00
contentStorage . Read ( 0 , content ) ;
2023-07-16 19:31:14 +02:00
Span < byte > hash = new ( new byte [ 32 ] ) ;
2020-01-12 02:10:55 +00:00
LibHac . Crypto . Sha256 . GenerateSha256Hash ( content , hash ) ;
2022-01-12 04:22:19 -07:00
if ( LibHac . Common . Utilities . ArraysEqual ( hash . ToArray ( ) , meta . ContentEntries [ 0 ] . Hash ) )
2020-01-12 02:10:55 +00:00
{
updateNcas . Remove ( metaEntry . TitleId ) ;
}
}
}
}
}
if ( updateNcas . Count > 0 )
{
2023-10-05 07:41:00 -03:00
StringBuilder extraNcas = new ( ) ;
2020-01-12 02:10:55 +00:00
2025-01-25 14:13:18 -06:00
foreach ( KeyValuePair < ulong , List < ( NcaContentType type , string path ) > > entry in updateNcas )
2020-01-12 02:10:55 +00:00
{
2025-01-25 14:13:18 -06:00
foreach ( ( NcaContentType type , string path ) in entry . Value )
2020-01-12 02:10:55 +00:00
{
2023-10-05 07:41:00 -03:00
extraNcas . AppendLine ( path ) ;
2020-01-12 02:10:55 +00:00
}
}
throw new InvalidFirmwarePackageException ( $"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}" ) ;
}
return systemVersion ;
}
}
public SystemVersion GetCurrentFirmwareVersion ( )
{
2020-01-21 23:23:11 +01:00
LoadEntries ( ) ;
2020-01-12 02:10:55 +00:00
2020-01-13 01:17:44 +01:00
lock ( _lock )
2020-01-12 02:10:55 +00:00
{
2025-01-25 14:13:18 -06:00
LinkedList < LocationEntry > locationEnties = _locationEntries [ StorageId . BuiltInSystem ] ;
2020-01-12 02:10:55 +00:00
2025-01-25 14:13:18 -06:00
foreach ( LocationEntry entry in locationEnties )
2020-01-13 01:17:44 +01:00
{
if ( entry . ContentType = = NcaContentType . Data )
2020-01-12 02:10:55 +00:00
{
2025-01-25 14:13:18 -06:00
string path = VirtualFileSystem . SwitchPathToSystemPath ( entry . ContentPath ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
using FileStream fileStream = File . OpenRead ( path ) ;
Nca nca = new ( _virtualFileSystem . KeySet , fileStream . AsStorage ( ) ) ;
2020-01-12 02:10:55 +00:00
2023-07-16 19:31:14 +02:00
if ( nca . Header . TitleId = = SystemVersionTitleId & & nca . Header . ContentType = = NcaContentType . Data )
{
2025-01-25 14:13:18 -06:00
IFileSystem romfs = nca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . ErrorOnInvalid ) ;
2020-01-13 01:17:44 +01:00
2025-01-26 15:15:26 -06:00
using UniqueRef < IFile > systemVersionFile = new ( ) ;
2021-12-23 09:55:50 -07:00
2023-07-16 19:31:14 +02:00
if ( romfs . OpenFile ( ref systemVersionFile . Ref , "/file" . ToU8Span ( ) , OpenMode . Read ) . IsSuccess ( ) )
{
return new SystemVersion ( systemVersionFile . Get . AsStream ( ) ) ;
2020-01-12 02:10:55 +00:00
}
2020-01-13 01:17:44 +01:00
}
2020-01-12 02:10:55 +00:00
}
}
}
return null ;
}
2024-11-29 00:32:07 +01:00
2025-05-30 17:08:34 -05:00
public static void VerifyKeysFile ( string filePath )
2024-11-29 00:32:07 +01:00
{
// Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md
string genericPattern = @"^[a-z0-9_]+ = [a-z0-9]+$" ;
string titlePattern = @"^[a-z0-9]{32} = [a-z0-9]{32}$" ;
if ( File . Exists ( filePath ) )
{
// Read all lines from the file
string fileName = Path . GetFileName ( filePath ) ;
string [ ] lines = File . ReadAllLines ( filePath ) ;
2025-05-30 17:08:34 -05:00
bool verified ;
2024-11-29 00:32:07 +01:00
switch ( fileName )
{
case "prod.keys" :
2024-12-24 20:47:14 -06:00
verified = VerifyKeys ( lines , genericPattern ) ;
2024-11-29 00:32:07 +01:00
break ;
case "title.keys" :
2024-12-24 20:47:14 -06:00
verified = VerifyKeys ( lines , titlePattern ) ;
2024-11-29 00:32:07 +01:00
break ;
case "console.keys" :
2024-12-24 20:47:14 -06:00
verified = VerifyKeys ( lines , genericPattern ) ;
2024-11-29 00:32:07 +01:00
break ;
case "dev.keys" :
2024-12-24 20:47:14 -06:00
verified = VerifyKeys ( lines , genericPattern ) ;
2024-11-29 00:32:07 +01:00
break ;
default :
throw new FormatException ( $"Keys file name \" { fileName } \ " not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported." ) ;
}
2025-05-30 17:08:34 -05:00
2024-11-29 00:32:07 +01:00
if ( ! verified )
{
throw new FormatException ( $"Invalid \" { filePath } \ " file format." ) ;
}
2025-05-30 17:08:34 -05:00
}
else
2024-11-29 00:32:07 +01:00
{
throw new FileNotFoundException ( $"Keys file not found at \" { filePath } \ "." ) ;
}
2024-12-24 20:47:14 -06:00
return ;
2025-05-30 17:08:34 -05:00
static bool VerifyKeys ( string [ ] lines , string regex )
2024-11-29 00:32:07 +01:00
{
2024-12-24 20:47:14 -06:00
foreach ( string line in lines )
2024-11-29 00:32:07 +01:00
{
2024-12-24 20:47:14 -06:00
if ( ! Regex . IsMatch ( line , regex ) )
{
return false ;
}
2024-11-29 00:32:07 +01:00
}
2025-05-30 17:08:34 -05:00
2024-12-24 20:47:14 -06:00
return true ;
2024-11-29 00:32:07 +01:00
}
}
2025-05-30 17:08:34 -05:00
public static bool AreKeysAlredyPresent ( string pathToCheck )
2024-11-29 00:32:07 +01:00
{
2025-01-26 15:43:02 -06:00
string [ ] fileNames = [ "prod.keys" , "title.keys" , "console.keys" , "dev.keys" ] ;
2025-01-25 14:13:18 -06:00
foreach ( string file in fileNames )
2024-11-29 00:32:07 +01:00
{
if ( File . Exists ( Path . Combine ( pathToCheck , file ) ) )
{
2024-12-19 21:52:25 -03:00
return true ;
2024-11-29 00:32:07 +01:00
}
}
2025-05-30 17:08:34 -05:00
2024-11-29 00:32:07 +01:00
return false ;
}
2018-11-18 21:37:41 +02:00
}
}