From e485ee049d8d9418f3bdceed8d3342cc09977f50 Mon Sep 17 00:00:00 2001
From: emmauss <emmausssss@gmail.com>
Date: Sun, 12 Jan 2020 02:10:55 +0000
Subject: System firmware installer (#791)

* firmware installer

* Add directory installation option and fix 9.x support for directory

* Fix missing system font error while installing for the first time

* Address code style comments

* Create and use InvalidFirmwarePackageException

* Fix LDj3SNuD's comments

* addressed alex's comments

* add label to status bar to show current firmware version

Co-authored-by: Thog <thog@protonmail.com>
---
 Ryujinx.HLE/FileSystem/Content/ContentManager.cs | 573 ++++++++++++++++++++++-
 1 file changed, 559 insertions(+), 14 deletions(-)

(limited to 'Ryujinx.HLE/FileSystem/Content/ContentManager.cs')

diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
index 17faa19f..b83ae440 100644
--- a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
+++ b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
@@ -1,22 +1,30 @@
-using LibHac.FsSystem;
+using LibHac;
+using LibHac.Fs;
+using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
+using LibHac.Ncm;
+using Ryujinx.HLE.Exceptions;
 using Ryujinx.HLE.HOS.Services.Time;
 using Ryujinx.HLE.Utilities;
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.IO.Compression;
 using System.Linq;
 
 namespace Ryujinx.HLE.FileSystem.Content
 {
     internal class ContentManager
     {
+        private const ulong SystemVersionTitleId = 0x0100000000000809;
+        private const ulong SystemUpdateTitleId  = 0x0100000000000816;
+
         private Dictionary<StorageId, LinkedList<LocationEntry>> _locationEntries;
 
-        private Dictionary<string, long> _sharedFontTitleDictionary;
+        private Dictionary<string, long>   _sharedFontTitleDictionary;
         private Dictionary<string, string> _sharedFontFilenameDictionary;
 
-        private SortedDictionary<(ulong, NcaContentType), string> _contentDictionary;
+        private SortedDictionary<(ulong titleId, NcaContentType type), string> _contentDictionary;
 
         private Switch _device;
 
@@ -48,9 +56,10 @@ namespace Ryujinx.HLE.FileSystem.Content
             _device = device;
         }
 
-        public void LoadEntries()
+        public void LoadEntries(bool ignoreMissingFonts = false)
         {
             _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>();
+            _locationEntries   = new Dictionary<StorageId, LinkedList<LocationEntry>>();
 
             foreach (StorageId storageId in Enum.GetValues(typeof(StorageId)))
             {
@@ -144,6 +153,8 @@ namespace Ryujinx.HLE.FileSystem.Content
             }
 
             TimeManager.Instance.InitializeTimeZone(_device);
+
+            _device.System.Font.Initialize(this, ignoreMissingFonts);
         }
 
         public void ClearEntry(long titleId, NcaContentType contentType, StorageId storageId)
@@ -153,7 +164,7 @@ namespace Ryujinx.HLE.FileSystem.Content
 
         public void RefreshEntries(StorageId storageId, int flag)
         {
-            LinkedList<LocationEntry> locationList      = _locationEntries[storageId];
+            LinkedList<LocationEntry> locationList = _locationEntries[storageId];
             LinkedListNode<LocationEntry> locationEntry = locationList.First;
 
             while (locationEntry != null)
@@ -173,9 +184,10 @@ namespace Ryujinx.HLE.FileSystem.Content
         {
             if (_contentDictionary.ContainsValue(ncaId))
             {
-                var            content     = _contentDictionary.FirstOrDefault(x => x.Value == ncaId);
-                long           titleId     = (long)content.Key.Item1;
-                NcaContentType contentType = content.Key.Item2;
+                var  content = _contentDictionary.FirstOrDefault(x => x.Value == ncaId);
+                long titleId = (long)content.Key.Item1;
+
+                NcaContentType contentType = content.Key.type;
                 StorageId      storage     = GetInstalledStorage(titleId, contentType, storageId);
 
                 return storage == storageId;
@@ -186,9 +198,9 @@ namespace Ryujinx.HLE.FileSystem.Content
 
         public UInt128 GetInstalledNcaId(long titleId, NcaContentType contentType)
         {
-            if (_contentDictionary.ContainsKey(((ulong)titleId,contentType)))
+            if (_contentDictionary.ContainsKey(((ulong)titleId, contentType)))
             {
-                return new UInt128(_contentDictionary[((ulong)titleId,contentType)]);
+                return new UInt128(_contentDictionary[((ulong)titleId, contentType)]);
             }
 
             return new UInt128();
@@ -232,9 +244,8 @@ namespace Ryujinx.HLE.FileSystem.Content
             {
                 return false;
             }
-
-            StorageId storageId     = LocationHelper.GetStorageId(locationEntry.ContentPath);
-            string    installedPath = _device.FileSystem.SwitchPathToSystemPath(locationEntry.ContentPath);
+            
+            string installedPath = _device.FileSystem.SwitchPathToSystemPath(locationEntry.ContentPath);
 
             if (!string.IsNullOrWhiteSpace(installedPath))
             {
@@ -242,7 +253,7 @@ namespace Ryujinx.HLE.FileSystem.Content
                 {
                     using (FileStream file = new FileStream(installedPath, FileMode.Open, FileAccess.Read))
                     {
-                        Nca  nca          = new Nca(_device.System.KeySet, file.AsStorage());
+                        Nca nca = new Nca(_device.System.KeySet, file.AsStorage());
                         bool contentCheck = nca.Header.ContentType == contentType;
 
                         return contentCheck;
@@ -310,5 +321,539 @@ namespace Ryujinx.HLE.FileSystem.Content
 
             return locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType);
         }
+
+        public void InstallFirmware(string firmwareSource)
+        {
+            string contentPathString   = LocationHelper.GetContentRoot(StorageId.NandSystem);
+            string contentDirectory    = LocationHelper.GetRealPath(_device.FileSystem, contentPathString);
+            string registeredDirectory = Path.Combine(contentDirectory, "registered");
+            string temporaryDirectory  = Path.Combine(contentDirectory, "temp");
+
+            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.");
+            }
+
+            FileInfo info = new FileInfo(firmwareSource);
+
+            using (FileStream file = File.OpenRead(firmwareSource))
+            {
+                switch (info.Extension)
+                {
+                    case ".zip":
+                        using (ZipArchive archive = ZipFile.OpenRead(firmwareSource))
+                        {
+                            InstallFromZip(archive, temporaryDirectory);
+                        }
+                        break;
+                    case ".xci":
+                        Xci xci = new Xci(_device.System.KeySet, file.AsStorage());
+                        InstallFromCart(xci, temporaryDirectory);
+                        break;
+                    default:
+                        throw new InvalidFirmwarePackageException("Input file is not a valid firmware package");
+                }
+
+                FinishInstallation(temporaryDirectory, registeredDirectory);
+            }
+        }
+
+        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)
+        {
+            foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
+            {
+                Nca nca = new Nca(_device.System.KeySet, OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage());
+
+                SaveNca(nca, entry.Name.Remove(entry.Name.IndexOf('.')), temporaryDirectory);
+            }
+        }
+
+        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.");
+            }
+        }
+
+        private void InstallFromZip(ZipArchive archive, string temporaryDirectory)
+        {
+            using (archive)
+            {
+                foreach (var entry in archive.Entries)
+                {
+                    if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
+                    {
+                        // Clean up the name and get the NcaId
+
+                        string[] pathComponents = entry.FullName.Replace(".cnmt", "").Split('/');
+
+                        string ncaId = pathComponents[pathComponents.Length - 1];
+
+                        // If this is a fragmented nca, we need to get the previous element.GetZip
+                        if (ncaId.Equals("00"))
+                        {
+                            ncaId = pathComponents[pathComponents.Length - 2];
+                        }
+
+                        if (ncaId.Contains(".nca"))
+                        {
+                            string newPath = Path.Combine(temporaryDirectory, ncaId);
+
+                            Directory.CreateDirectory(newPath);
+
+                            entry.ExtractToFile(Path.Combine(newPath, "00"));
+                        }
+                    }
+                }
+            }
+        }
+
+        public void SaveNca(Nca nca, string ncaId, string temporaryDirectory)
+        {
+            string newPath = Path.Combine(temporaryDirectory, ncaId + ".nca");
+
+            Directory.CreateDirectory(newPath);
+
+            using (FileStream file = File.Create(Path.Combine(newPath, "00")))
+            {
+                nca.BaseStorage.AsStream().CopyTo(file);
+            }
+        }
+
+        private IFile OpenPossibleFragmentedFile(IFileSystem filesystem, string path, OpenMode mode)
+        {
+            IFile file;
+
+            if (filesystem.FileExists($"{path}/00"))
+            {
+                filesystem.OpenFile(out file, $"{path}/00", mode);
+            }
+            else
+            {
+                filesystem.OpenFile(out file, path, mode);
+            }
+
+            return file;
+        }
+
+        private Stream GetZipStream(ZipArchiveEntry entry)
+        {
+            MemoryStream dest = new MemoryStream();
+
+            Stream src = entry.Open();
+
+            src.CopyTo(dest);
+            src.Dispose();
+
+            return dest;
+        }
+
+        public SystemVersion VerifyFirmwarePackage(string firmwarePackage)
+        {
+            Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new Dictionary<ulong, List<(NcaContentType, string)>>();
+
+            if (Directory.Exists(firmwarePackage))
+            {
+                return VerifyAndGetVersionDirectory(firmwarePackage);
+            }
+
+            if (!File.Exists(firmwarePackage))
+            {
+                throw new FileNotFoundException("Firmware file does not exist.");
+            }
+
+            FileInfo info = new FileInfo(firmwarePackage);
+
+            using (FileStream file = File.OpenRead(firmwarePackage))
+            {
+                switch (info.Extension)
+                {
+                    case ".zip":
+                        using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage))
+                        {
+                            return VerifyAndGetVersionZip(archive);
+                        }
+                    case ".xci":
+                        Xci xci = new Xci(_device.System.KeySet, file.AsStorage());
+
+                        if (xci.HasPartition(XciPartitionType.Update))
+                        {
+                            XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
+
+                            return VerifyAndGetVersion(partition);
+                        }
+                        else
+                        {
+                            throw new InvalidFirmwarePackageException("Update not found in xci file.");
+                        }
+                    default:
+                        break;
+                }
+            }
+
+            SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
+            {
+                return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
+            }
+
+            SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
+            {
+                SystemVersion systemVersion = null;
+
+                foreach (var entry in archive.Entries)
+                {
+                    if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
+                    {
+                        using (Stream ncaStream = GetZipStream(entry))
+                        {
+                            IStorage storage = ncaStream.AsStorage();
+
+                            Nca nca = new Nca(_device.System.KeySet, storage);
+
+                            if (updateNcas.ContainsKey(nca.Header.TitleId))
+                            {
+                                updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
+                            }
+                            else
+                            {
+                                updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
+                                updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
+                            }
+                        }
+                    }
+                }
+
+                if (updateNcas.ContainsKey(SystemUpdateTitleId))
+                {
+                    var ncaEntry = updateNcas[SystemUpdateTitleId];
+
+                    string metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
+
+                    CnmtContentMetaEntry[] metaEntries = null;
+
+                    var fileEntry = archive.GetEntry(metaPath);
+
+                    using (Stream ncaStream = GetZipStream(fileEntry))
+                    {
+                        Nca metaNca = new Nca(_device.System.KeySet, ncaStream.AsStorage());
+
+                        IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+
+                        string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
+
+                        if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess())
+                        {
+                            var meta = new Cnmt(metaFile.AsStream());
+
+                            if (meta.Type == ContentMetaType.SystemUpdate)
+                            {
+                                metaEntries = meta.MetaEntries;
+
+                                updateNcas.Remove(SystemUpdateTitleId);
+                            };
+                        }
+                    }
+
+                    if (metaEntries == null)
+                    {
+                        throw new FileNotFoundException("System update title was not found in the firmware package.");
+                    }
+
+                    if (updateNcas.ContainsKey(SystemVersionTitleId))
+                    {
+                        string versionEntry = updateNcas[SystemVersionTitleId].Find(x => x.type != NcaContentType.Meta).path;
+
+                        using (Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry)))
+                        {
+                            Nca nca = new Nca(_device.System.KeySet, ncaStream.AsStorage());
+
+                            var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+
+                            if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess())
+                            {
+                                systemVersion = new SystemVersion(systemVersionFile.AsStream());
+                            }
+                        }
+                    }
+
+                    foreach (CnmtContentMetaEntry metaEntry in metaEntries)
+                    {
+                        if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry))
+                        {
+                            metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
+
+                            string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
+
+                            // 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;
+                            }
+
+                            ZipArchiveEntry metaZipEntry    = archive.GetEntry(metaPath);
+                            ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath);
+
+                            using (Stream metaNcaStream = GetZipStream(metaZipEntry))
+                            {
+                                using (Stream contentNcaStream = GetZipStream(contentZipEntry))
+                                {
+                                    Nca metaNca = new Nca(_device.System.KeySet, metaNcaStream.AsStorage());
+
+                                    IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+
+                                    string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
+
+                                    if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess())
+                                    {
+                                        var meta = new Cnmt(metaFile.AsStream());
+
+                                        IStorage contentStorage = contentNcaStream.AsStorage();
+                                        if (contentStorage.GetSize(out long size).IsSuccess())
+                                        {
+                                            byte[] contentData = new byte[size];
+
+                                            Span<byte> content = new Span<byte>(contentData);
+
+                                            contentStorage.Read(0, content);
+
+                                            Span<byte> hash = new Span<byte>(new byte[32]);
+
+                                            LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
+
+                                            if (LibHac.Util.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
+                                            {
+                                                updateNcas.Remove(metaEntry.TitleId);
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    if (updateNcas.Count > 0)
+                    {
+                        string extraNcas = string.Empty;
+
+                        foreach (var entry in updateNcas)
+                        {
+                            foreach (var nca in entry.Value)
+                            {
+                                extraNcas += nca.path + Environment.NewLine;
+                            }
+                        }
+
+                        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;
+
+                foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
+                {
+                    IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
+
+                    Nca nca = new Nca(_device.System.KeySet, ncaStorage);
+
+                    if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
+                    {
+                        IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+
+                        string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
+
+                        if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess())
+                        {
+                            var meta = new Cnmt(metaFile.AsStream());
+
+                            if (meta.Type == ContentMetaType.SystemUpdate)
+                            {
+                                metaEntries = meta.MetaEntries;
+                            }
+                        };
+
+                        continue;
+                    }
+                    else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
+                    {
+                        var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+
+                        if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess())
+                        {
+                            systemVersion = new SystemVersion(systemVersionFile.AsStream());
+                        }
+                    }
+
+                    if (updateNcas.ContainsKey(nca.Header.TitleId))
+                    {
+                        updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
+                    }
+                    else
+                    {
+                        updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
+                        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)
+                {
+                    if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry))
+                    {
+                        var    metaNcaEntry = ncaEntry.Find(x => x.type == NcaContentType.Meta);
+                        string contentPath  = ncaEntry.Find(x => x.type != NcaContentType.Meta).path;
+
+                        // 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;
+                        }
+
+                        IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaEntry.path, OpenMode.Read).AsStorage();
+                        IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
+
+                        Nca metaNca = new Nca(_device.System.KeySet, metaStorage);
+
+                        IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+
+                        string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
+
+                        if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess())
+                        {
+                            var meta = new Cnmt(metaFile.AsStream());
+
+                            if (contentStorage.GetSize(out long size).IsSuccess())
+                            {
+                                byte[] contentData = new byte[size];
+
+                                Span<byte> content = new Span<byte>(contentData);
+
+                                contentStorage.Read(0, content);
+
+                                Span<byte> hash = new Span<byte>(new byte[32]);
+
+                                LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
+
+                                if (LibHac.Util.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
+                                {
+                                    updateNcas.Remove(metaEntry.TitleId);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (updateNcas.Count > 0)
+                {
+                    string extraNcas = string.Empty;
+
+                    foreach (var entry in updateNcas)
+                    {
+                        foreach (var nca in entry.Value)
+                        {
+                            extraNcas += nca.path + Environment.NewLine;
+                        }
+                    }
+
+                    throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
+                }
+
+                return systemVersion;
+            }
+
+            return null;
+        }
+
+        public SystemVersion GetCurrentFirmwareVersion()
+        {
+            LoadEntries(true);
+
+            var locationEnties = _locationEntries[StorageId.NandSystem];
+
+            foreach (var entry in locationEnties)
+            {
+                if (entry.ContentType == NcaContentType.Data)
+                {
+                    var path = _device.FileSystem.SwitchPathToSystemPath(entry.ContentPath);
+
+                    using (IStorage ncaStorage = File.Open(path, FileMode.Open).AsStorage())
+                    {
+                        Nca nca = new Nca(_device.System.KeySet, ncaStorage);
+
+                        if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data)
+                        {
+                            var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
+
+                            if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess())
+                            {
+                                return new SystemVersion(systemVersionFile.AsStream());
+                            }
+                        }
+
+                    }
+                }
+            }
+
+            return null;
+        }
     }
 }
-- 
cgit v1.2.3-70-g09d2