From e485ee049d8d9418f3bdceed8d3342cc09977f50 Mon Sep 17 00:00:00 2001
From: emmauss <emmausssss@gmail.com>
Date: Sun, 12 Jan 2020 02:10:55 +0000
Subject: [PATCH] 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>
---
 .../InvalidFirmwarePackageException.cs        |   9 +
 .../FileSystem/Content/ContentManager.cs      | 573 +++++++++++++++++-
 .../FileSystem/Content/SystemVersion.cs       |  41 ++
 Ryujinx.HLE/HOS/Font/SharedFontManager.cs     |  20 +-
 Ryujinx.HLE/HOS/Horizon.cs                    |  15 +
 .../HOS/Services/Sdb/Pl/ISharedFontManager.cs |   2 +-
 Ryujinx/Ui/MainWindow.cs                      | 202 +++++-
 Ryujinx/Ui/MainWindow.glade                   | 101 ++-
 8 files changed, 936 insertions(+), 27 deletions(-)
 create mode 100644 Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs
 create mode 100644 Ryujinx.HLE/FileSystem/Content/SystemVersion.cs

diff --git a/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs b/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs
new file mode 100644
index 0000000000..bddd827a01
--- /dev/null
+++ b/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Ryujinx.HLE.Exceptions
+{
+    class InvalidFirmwarePackageException : Exception
+    {
+        public InvalidFirmwarePackageException(string message) : base(message) { }
+    }
+}
diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs
index 17faa19f62..b83ae44064 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;
+        }
     }
 }
diff --git a/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs b/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs
new file mode 100644
index 0000000000..08ec351255
--- /dev/null
+++ b/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs
@@ -0,0 +1,41 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace Ryujinx.HLE.FileSystem.Content
+{
+    public class SystemVersion
+    {
+        public byte   Major          { get; }
+        public byte   Minor          { get; }
+        public byte   Micro          { get; }
+        public byte   RevisionMajor  { get; }
+        public byte   RevisionMinor  { get; }
+        public string PlatformString { get; }
+        public string Hex            { get; }
+        public string VersionString  { get; }
+        public string VersionTitle   { get; }
+
+        public SystemVersion(Stream systemVersionFile)
+        {
+            using (BinaryReader reader = new BinaryReader(systemVersionFile))
+            {
+                Major = reader.ReadByte();
+                Minor = reader.ReadByte();
+                Micro = reader.ReadByte();
+
+                reader.ReadByte(); // Padding
+
+                RevisionMajor = reader.ReadByte();
+                RevisionMinor = reader.ReadByte();
+
+                reader.ReadBytes(2); // Padding
+
+                PlatformString = Encoding.ASCII.GetString(reader.ReadBytes(0x20)).TrimEnd('\0');
+                Hex            = Encoding.ASCII.GetString(reader.ReadBytes(0x40)).TrimEnd('\0');
+                VersionString  = Encoding.ASCII.GetString(reader.ReadBytes(0x18)).TrimEnd('\0');
+                VersionTitle   = Encoding.ASCII.GetString(reader.ReadBytes(0x80)).TrimEnd('\0');
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs
index 99b662c0d1..e126cd5799 100644
--- a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs
+++ b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs
@@ -44,7 +44,15 @@ namespace Ryujinx.HLE.HOS.Font
             _fontsPath = Path.Combine(device.FileSystem.GetSystemPath(), "fonts");
         }
 
-        public void EnsureInitialized(ContentManager contentManager)
+        public void Initialize(ContentManager contentManager, bool ignoreMissingFonts)
+        {
+            _fontData?.Clear();
+            _fontData = null;
+
+            EnsureInitialized(contentManager, ignoreMissingFonts);
+        }
+
+        public void EnsureInitialized(ContentManager contentManager, bool ignoreMissingFonts)
         {
             if (_fontData == null)
             {
@@ -112,10 +120,12 @@ namespace Ryujinx.HLE.HOS.Font
 
                         return info;
                     }
-                    else
+                    else if (!ignoreMissingFonts)
                     {
                         throw new InvalidSystemResourceException($"Font \"{name}.ttf\" not found. Please provide it in \"{_fontsPath}\".");
                     }
+
+                    return new FontInfo();
                 }
 
                 _fontData = new Dictionary<SharedFontType, FontInfo>
@@ -128,7 +138,7 @@ namespace Ryujinx.HLE.HOS.Font
                     { SharedFontType.NintendoEx,          CreateFont("FontNintendoExtended")          }
                 };
 
-                if (fontOffset > Horizon.FontSize)
+                if (fontOffset > Horizon.FontSize && !ignoreMissingFonts)
                 {
                     throw new InvalidSystemResourceException(
                         $"The sum of all fonts size exceed the shared memory size. " +
@@ -151,14 +161,14 @@ namespace Ryujinx.HLE.HOS.Font
 
         public int GetFontSize(SharedFontType fontType)
         {
-            EnsureInitialized(_device.System.ContentManager);
+            EnsureInitialized(_device.System.ContentManager, false);
 
             return _fontData[fontType].Size;
         }
 
         public int GetSharedMemoryAddressOffset(SharedFontType fontType)
         {
-            EnsureInitialized(_device.System.ContentManager);
+            EnsureInitialized(_device.System.ContentManager, false);
 
             return _fontData[fontType].Offset + 8;
         }
diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs
index 164a49a002..855d89148c 100644
--- a/Ryujinx.HLE/HOS/Horizon.cs
+++ b/Ryujinx.HLE/HOS/Horizon.cs
@@ -715,6 +715,21 @@ namespace Ryujinx.HLE.HOS
             }
         }
 
+        public SystemVersion VerifyFirmwarePackage(string firmwarePackage)
+        {
+            return ContentManager.VerifyFirmwarePackage(firmwarePackage);
+        }
+
+        public SystemVersion GetCurrentFirmwareVersion()
+        {
+            return ContentManager.GetCurrentFirmwareVersion();
+        }
+
+        public void InstallFirmware(string firmwarePackage)
+        {
+            ContentManager.InstallFirmware(firmwarePackage);
+        }
+
         public void SignalVsync()
         {
             VsyncEvent.ReadableEvent.Signal();
diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
index 4560d9545c..418c15f2c2 100644
--- a/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
+++ b/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
@@ -61,7 +61,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl
         // GetSharedMemoryNativeHandle() -> handle<copy>
         public ResultCode GetSharedMemoryNativeHandle(ServiceCtx context)
         {
-            context.Device.System.Font.EnsureInitialized(context.Device.System.ContentManager);
+            context.Device.System.Font.EnsureInitialized(context.Device.System.ContentManager, false);
 
             if (context.Process.HandleTable.GenerateHandle(context.Device.System.FontSharedMem, out int handle) != KernelResult.Success)
             {
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index 2c4d011268..667ea5a5b9 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -44,6 +44,8 @@ namespace Ryujinx.Ui
         [GUI] CheckMenuItem _fullScreen;
         [GUI] MenuItem      _stopEmulation;
         [GUI] CheckMenuItem _favToggle;
+        [GUI] MenuItem      _firmwareInstallFile;
+        [GUI] MenuItem      _firmwareInstallDirectory;
         [GUI] CheckMenuItem _iconToggle;
         [GUI] CheckMenuItem _appToggle;
         [GUI] CheckMenuItem _developerToggle;
@@ -56,6 +58,7 @@ namespace Ryujinx.Ui
         [GUI] TreeView      _gameTable;
         [GUI] TreeSelection _gameTableSelection;
         [GUI] Label         _progressLabel;
+        [GUI] Label         _firmwareVersionLabel;
         [GUI] LevelBar      _progressBar;
 #pragma warning restore CS0649
 #pragma warning restore IDE0044
@@ -134,6 +137,8 @@ namespace Ryujinx.Ui
 #pragma warning disable CS4014
             UpdateGameTable();
 #pragma warning restore CS4014
+
+            Task.Run(RefreshFirmwareLabel);
         }
 
         internal static void ApplyTheme()
@@ -297,6 +302,9 @@ namespace Ryujinx.Ui
                 _gameLoaded              = true;
                 _stopEmulation.Sensitive = true;
 
+                _firmwareInstallFile.Sensitive      = false;
+                _firmwareInstallDirectory.Sensitive = false;
+
                 DiscordIntegrationModule.SwitchToPlayingState(_device.System.TitleId, _device.System.TitleName);
 
                 string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleId, "gui");
@@ -556,7 +564,199 @@ namespace Ryujinx.Ui
             _gameLoaded = false;
         }
 
-        private void FullScreen_Toggled(object sender, EventArgs args)
+        private void Installer_File_Pressed(object o, EventArgs args)
+        {
+            FileChooserDialog fileChooser = new FileChooserDialog("Choose the firmware file to open",
+                                                                  this,
+                                                                  FileChooserAction.Open,
+                                                                  "Cancel",
+                                                                  ResponseType.Cancel,
+                                                                  "Open",
+                                                                  ResponseType.Accept);
+
+            fileChooser.Filter = new FileFilter();
+            fileChooser.Filter.AddPattern("*.zip");
+            fileChooser.Filter.AddPattern("*.xci");
+
+            HandleInstallerDialog(fileChooser);
+        }
+
+        private void Installer_Directory_Pressed(object o, EventArgs args)
+        {
+            FileChooserDialog directoryChooser = new FileChooserDialog("Choose the firmware directory to open",
+                                                                       this,
+                                                                       FileChooserAction.SelectFolder,
+                                                                       "Cancel",
+                                                                       ResponseType.Cancel,
+                                                                       "Open",
+                                                                       ResponseType.Accept);
+
+            HandleInstallerDialog(directoryChooser);
+        }
+
+        private void RefreshFirmwareLabel()
+        {
+            var currentFirmware = _device.System.GetCurrentFirmwareVersion();
+
+            GLib.Idle.Add(new GLib.IdleHandler(() =>
+            {
+                _firmwareVersionLabel.Text = currentFirmware != null ? currentFirmware.VersionString : "0.0.0";
+
+                return false;
+            }));
+        }
+
+        private void HandleInstallerDialog(FileChooserDialog fileChooser)
+        {
+            if (fileChooser.Run() == (int)ResponseType.Accept)
+            {
+                MessageDialog dialog = null;
+
+                try
+                {
+                    string filename = fileChooser.Filename;
+
+                    fileChooser.Dispose();
+
+                    var firmwareVersion = _device.System.VerifyFirmwarePackage(filename);
+
+                    if (firmwareVersion == null)
+                    {
+                        dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, "");
+
+                        dialog.Text = "Firmware not found.";
+
+                        dialog.SecondaryText = $"A valid system firmware was not found in {filename}.";
+
+                        Logger.PrintError(LogClass.Application, $"A valid system firmware was not found in {filename}.");
+
+                        dialog.Run();
+                        dialog.Hide();
+                        dialog.Dispose();
+
+                        return;
+                    }
+
+                    var currentVersion = _device.System.GetCurrentFirmwareVersion();
+
+                    string dialogMessage = $"System version {firmwareVersion.VersionString} will be installed.";
+
+                    if (currentVersion != null)
+                    {
+                        dialogMessage += $"This will replace the current system version {currentVersion.VersionString}. ";
+                    }
+
+                    dialogMessage += "Do you want to continue?";
+
+                    dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, false, "");
+
+                    dialog.Text = $"Install Firmware {firmwareVersion.VersionString}";
+                    dialog.SecondaryText = dialogMessage;
+
+                    int response = dialog.Run();
+
+                    dialog.Dispose();
+
+                    dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.None, false, "");
+
+                    dialog.Text = $"Install Firmware {firmwareVersion.VersionString}";
+
+                    dialog.SecondaryText = "Installing firmware...";
+
+                    if (response == (int)ResponseType.Yes)
+                    {
+                        Logger.PrintInfo(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}");
+                        
+                        Thread thread = new Thread(() =>
+                        {
+                            GLib.Idle.Add(new GLib.IdleHandler(() =>
+                            {
+                                dialog.Run();
+                                return false;
+                            }));
+
+                            try
+                            {
+                                _device.System.InstallFirmware(filename);
+
+                                GLib.Idle.Add(new GLib.IdleHandler(() =>
+                                {
+                                    dialog.Dispose();
+
+                                    dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, "");
+
+                                    dialog.Text = $"Install Firmware {firmwareVersion.VersionString}";
+
+                                    dialog.SecondaryText = $"System version {firmwareVersion.VersionString} successfully installed.";
+
+                                    Logger.PrintInfo(LogClass.Application, $"System version {firmwareVersion.VersionString} successfully installed.");
+
+                                    dialog.Run();
+                                    dialog.Dispose();
+
+                                    return false;
+                                }));
+                            }
+                            catch (Exception ex)
+                            {
+                                GLib.Idle.Add(new GLib.IdleHandler(() =>
+                                {
+                                    dialog.Dispose();
+
+                                    dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, "");
+
+                                    dialog.Text = $"Install Firmware {firmwareVersion.VersionString} Failed.";
+
+                                    dialog.SecondaryText = $"An error occured while installing system version {firmwareVersion.VersionString}." +
+                                     " Please check logs for more info.";
+
+                                    Logger.PrintError(LogClass.Application, ex.Message);
+
+                                    dialog.Run();
+                                    dialog.Dispose();
+
+                                    return false;
+                                }));
+                            }
+                            finally
+                            {
+                                RefreshFirmwareLabel();
+                            }
+                        });
+
+                        thread.Start();
+                    }
+                    else
+                    {
+                        dialog.Dispose();
+                    }
+                }
+                catch (Exception ex)
+                {
+                    if (dialog != null)
+                    {
+                        dialog.Dispose();
+                    }
+
+                    dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, "");
+
+                    dialog.Text = "Parsing Firmware Failed.";
+
+                    dialog.SecondaryText = "An error occured while parsing firmware. Please check the logs for more info.";
+
+                    Logger.PrintError(LogClass.Application, ex.Message);
+
+                    dialog.Run();
+                    dialog.Dispose();
+                }
+            }
+            else
+            {
+                fileChooser.Dispose();
+            }
+        }
+
+        private void FullScreen_Toggled(object o, EventArgs args)
         {
             if (_fullScreen.Active)
             {
diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade
index fcf91bc4c5..8e2eab9391 100644
--- a/Ryujinx/Ui/MainWindow.glade
+++ b/Ryujinx/Ui/MainWindow.glade
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.1 -->
+<!-- Generated with glade 3.21.0 -->
 <interface>
   <requires lib="gtk+" version="3.20"/>
   <object class="GtkApplicationWindow" id="_mainWin">
@@ -8,9 +8,6 @@
     <property name="window_position">center</property>
     <property name="default_width">1280</property>
     <property name="default_height">750</property>
-    <child>
-      <placeholder/>
-    </child>
     <child>
       <object class="GtkBox" id="_box">
         <property name="visible">True</property>
@@ -263,6 +260,44 @@
                 <property name="can_focus">False</property>
                 <property name="label" translatable="yes">Tools</property>
                 <property name="use_underline">True</property>
+                <child type="submenu">
+                  <object class="GtkMenu">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkMenuItem" id="FirmwareSubMenu">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">Install Firmware</property>
+                        <property name="use_underline">True</property>
+                        <child type="submenu">
+                          <object class="GtkMenu">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <child>
+                              <object class="GtkMenuItem" id="_firmwareInstallFile">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Install a firmware from XCI or ZIP</property>
+                                <property name="use_underline">True</property>
+                                <signal name="activate" handler="Installer_File_Pressed" swapped="no"/>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkMenuItem" id="_firmwareInstallDirectory">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Install a firmware from a directory</property>
+                                <property name="use_underline">True</property>
+                                <signal name="activate" handler="Installer_Directory_Pressed" swapped="no"/>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
               </object>
             </child>
             <child>
@@ -370,7 +405,7 @@
                   <object class="GtkLabel" id="_progressLabel">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
-                    <property name="margin_left">5</property>
+                    <property name="margin_left">10</property>
                     <property name="margin_right">5</property>
                     <property name="margin_top">2</property>
                     <property name="margin_bottom">2</property>
@@ -388,7 +423,7 @@
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
                     <property name="halign">start</property>
-                    <property name="margin_left">5</property>
+                    <property name="margin_left">10</property>
                     <property name="margin_right">5</property>
                   </object>
                   <packing>
@@ -397,6 +432,57 @@
                     <property name="position">2</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkSeparator">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="margin_left">5</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">System Version</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="_firmwareVersionLabel">
+                        <property name="width_request">50</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="margin_left">5</property>
+                        <property name="margin_right">5</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="pack_type">end</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="pack_type">end</property>
+                    <property name="position">4</property>
+                  </packing>
+                </child>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -413,5 +499,8 @@
         </child>
       </object>
     </child>
+    <child type="titlebar">
+      <placeholder/>
+    </child>
   </object>
 </interface>