diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs new file mode 100644 index 0000000000..93c5472de4 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs @@ -0,0 +1,293 @@ +using LibHac; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ryujinx.HLE.FileSystem.Content +{ + internal class ContentManager + { + private Dictionary> LocationEntries; + + private Dictionary SharedFontTitleDictionary; + + private SortedDictionary<(ulong, ContentType), string> ContentDictionary; + + private Switch Device; + + public ContentManager(Switch Device) + { + ContentDictionary = new SortedDictionary<(ulong, ContentType), string>(); + + LocationEntries = new Dictionary>(); + + SharedFontTitleDictionary = new Dictionary() + { + {"FontStandard", 0x0100000000000811 }, + {"FontChineseSimplified", 0x0100000000000814 }, + {"FontExtendedChineseSimplified", 0x0100000000000814 }, + {"FontKorean", 0x0100000000000812 }, + {"FontChineseTraditional", 0x0100000000000813 }, + {"FontNintendoExtended" , 0x0100000000000810 }, + }; + + this.Device = Device; + } + + public void LoadEntries() + { + ContentDictionary = new SortedDictionary<(ulong, ContentType), string>(); + + foreach (StorageId StorageId in Enum.GetValues(typeof(StorageId))) + { + string ContentInstallationDirectory = null; + string ContentPathString = null; + + try + { + ContentPathString = LocationHelper.GetContentPath(StorageId); + ContentInstallationDirectory = LocationHelper.GetRealPath(Device.FileSystem, ContentPathString); + } + catch (NotSupportedException NEx) + { + continue; + } + + Directory.CreateDirectory(ContentInstallationDirectory); + + LinkedList LocationList = new LinkedList(); + + void AddEntry(LocationEntry Entry) + { + LocationList.AddLast(Entry); + } + + foreach (string DirectoryPath in Directory.EnumerateDirectories(ContentInstallationDirectory)) + { + if (Directory.GetFiles(DirectoryPath).Length > 0) + { + string NcaName = new DirectoryInfo(DirectoryPath).Name.Replace(".nca", string.Empty); + + using (FileStream NcaFile = new FileStream(Directory.GetFiles(DirectoryPath)[0], FileMode.Open, FileAccess.Read)) + { + Nca Nca = new Nca(Device.System.KeySet, NcaFile, false); + + LocationEntry Entry = new LocationEntry(ContentPathString, + 0, + (long)Nca.Header.TitleId, + Nca.Header.ContentType); + + + AddEntry(Entry); + + ContentDictionary.Add((Nca.Header.TitleId, Nca.Header.ContentType), NcaName); + + NcaFile.Close(); + + Nca.Dispose(); + + NcaFile.Dispose(); + } + } + } + + foreach (string FilePath in Directory.EnumerateFiles(ContentInstallationDirectory)) + { + if (Path.GetExtension(FilePath) == ".nca") + { + string NcaName = Path.GetFileNameWithoutExtension(FilePath); + + using (FileStream NcaFile = new FileStream(FilePath, FileMode.Open, FileAccess.Read)) + { + Nca Nca = new Nca(Device.System.KeySet, NcaFile, false); + + LocationEntry Entry = new LocationEntry(ContentPathString, + 0, + (long)Nca.Header.TitleId, + Nca.Header.ContentType); + + AddEntry(Entry); + + ContentDictionary.Add((Nca.Header.TitleId, Nca.Header.ContentType), NcaName); + + NcaFile.Close(); + + Nca.Dispose(); + + NcaFile.Dispose(); + } + } + } + + if(LocationEntries.ContainsKey(StorageId) && LocationEntries[StorageId]?.Count == 0) + { + LocationEntries.Remove(StorageId); + } + + if (!LocationEntries.ContainsKey(StorageId)) + { + LocationEntries.Add(StorageId, LocationList); + } + } + } + + public void ClearEntry(long TitleId, ContentType ContentType,StorageId StorageId) + { + RemoveLocationEntry(TitleId, ContentType, StorageId); + } + + public void RefreshEntries(StorageId StorageId, int Flag) + { + LinkedList LocationList = LocationEntries[StorageId]; + + LinkedListNode LocationEntry = LocationList.First; + + while (LocationEntry != null) + { + LinkedListNode NextLocationEntry = LocationEntry.Next; + + if (LocationEntry.Value.Flag == Flag) + { + LocationList.Remove(LocationEntry.Value); + } + + LocationEntry = NextLocationEntry; + } + } + + public UInt128 GetInstalledNcaId(long TitleId, ContentType ContentType) + { + if (ContentDictionary.ContainsKey(((ulong)TitleId,ContentType))) + { + return new UInt128(ContentDictionary[((ulong)TitleId,ContentType)]); + } + + return new UInt128(); + } + + public string GetInstalledPath(long TitleId, ContentType ContentType, StorageId StorageId) + { + LocationEntry LocationEntry = GetLocation(TitleId, ContentType, StorageId); + + string ContentPath = LocationHelper.GetRealPath(Device.FileSystem, LocationEntry.ContentPath); + + string NcaPath = Path.Combine(ContentPath, ContentDictionary[((ulong)TitleId, ContentType)]) + ".nca"; + + if (!File.Exists(NcaPath)) + { + NcaPath = Path.Combine(NcaPath, "00"); + } + + return NcaPath; + } + + public StorageId GetInstalledStorage(long TitleId, ContentType ContentType, StorageId StorageId) + { + LocationEntry LocationEntry = GetLocation(TitleId, ContentType, StorageId); + + return LocationEntry.ContentPath != null ? + LocationHelper.GetStorageId(LocationEntry.ContentPath) : StorageId.None; + } + + public string GetInstalledContentStorage(long TitleId, StorageId StorageId, ContentType ContentType) + { + LocationEntry LocationEntry = GetLocation(TitleId, ContentType, StorageId); + + if (VerifyContentType(LocationEntry, ContentType)) + { + return LocationEntry.ContentPath; + } + + return string.Empty; + } + + public void RedirectLocation(LocationEntry NewEntry, StorageId StorageId) + { + LocationEntry LocationEntry = GetLocation(NewEntry.TitleId, NewEntry.ContentType, StorageId); + + if (LocationEntry.ContentPath != null) + { + RemoveLocationEntry(NewEntry.TitleId, NewEntry.ContentType, StorageId); + } + + AddLocationEntry(NewEntry, StorageId); + } + + private bool VerifyContentType(LocationEntry LocationEntry, ContentType ContentType) + { + StorageId StorageId = LocationHelper.GetStorageId(LocationEntry.ContentPath); + + string InstalledPath = GetInstalledPath(LocationEntry.TitleId, ContentType, StorageId); + + if (!string.IsNullOrWhiteSpace(InstalledPath)) + { + if (File.Exists(InstalledPath)) + { + FileStream File = new FileStream(InstalledPath, FileMode.Open, FileAccess.Read); + + Nca Nca = new Nca(Device.System.KeySet, File, false); + + return Nca.Header.ContentType == ContentType; + } + } + + return false; + } + + private void AddLocationEntry(LocationEntry Entry, StorageId StorageId) + { + LinkedList LocationList = null; + + if (LocationEntries.ContainsKey(StorageId)) + { + LocationList = LocationEntries[StorageId]; + } + + if (LocationList != null) + { + if (LocationList.Contains(Entry)) + { + LocationList.Remove(Entry); + } + + LocationList.AddLast(Entry); + } + } + + private void RemoveLocationEntry(long TitleId, ContentType ContentType, StorageId StorageId) + { + LinkedList LocationList = null; + + if (LocationEntries.ContainsKey(StorageId)) + { + LocationList = LocationEntries[StorageId]; + } + + if (LocationList != null) + { + LocationEntry Entry = + LocationList.ToList().Find(x => x.TitleId == TitleId && x.ContentType == ContentType); + + + if (Entry.ContentPath != null) + { + LocationList.Remove(Entry); + } + } + } + + public bool TryGetFontTitle(string FontName, out long TitleId) + { + return SharedFontTitleDictionary.TryGetValue(FontName, out TitleId); + } + + private LocationEntry GetLocation(long TitleId, ContentType ContentType,StorageId StorageId) + { + LinkedList LocationList = LocationEntries[StorageId]; + + return LocationList.ToList().Find(x => x.TitleId == TitleId && x.ContentType == ContentType); + } + } +} diff --git a/Ryujinx.HLE/FileSystem/Content/ContentPath.cs b/Ryujinx.HLE/FileSystem/Content/ContentPath.cs new file mode 100644 index 0000000000..1e2c8ab32a --- /dev/null +++ b/Ryujinx.HLE/FileSystem/Content/ContentPath.cs @@ -0,0 +1,19 @@ +namespace Ryujinx.HLE.FileSystem.Content +{ + static class ContentPath + { + public const string SystemContent = "@SystemContent"; + public const string UserContent = "@UserContent"; + public const string SdCardContent = "@SdCardContent"; + public const string SdCard = "@SdCard"; + public const string CalibFile = "@CalibFile"; + public const string Safe = "@Safe"; + public const string User = "@User"; + public const string System = "@System"; + public const string Host = "@Host"; + public const string GamecardApp = "@GcApp"; + public const string GamecardContents = "@GcS00000001"; + public const string GamecardUpdate = "@upp"; + public const string RegisteredUpdate = "@RegUpdate"; + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/FileSystem/Content/LocationEntry.cs b/Ryujinx.HLE/FileSystem/Content/LocationEntry.cs new file mode 100644 index 0000000000..c7c6133b58 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/Content/LocationEntry.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; +using LibHac; + +namespace Ryujinx.HLE.FileSystem.Content +{ + public struct LocationEntry + { + public string ContentPath { get; private set; } + public int Flag { get; private set; } + public long TitleId { get; private set; } + public ContentType ContentType { get; private set; } + + public LocationEntry(string ContentPath, int Flag, long TitleId, ContentType ContentType) + { + this.ContentPath = ContentPath; + this.Flag = Flag; + this.TitleId = TitleId; + this.ContentType = ContentType; + } + + public void SetFlag(int Flag) + { + this.Flag = Flag; + } + } +} diff --git a/Ryujinx.HLE/FileSystem/Content/LocationHelper.cs b/Ryujinx.HLE/FileSystem/Content/LocationHelper.cs new file mode 100644 index 0000000000..da020a848e --- /dev/null +++ b/Ryujinx.HLE/FileSystem/Content/LocationHelper.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; + +using static Ryujinx.HLE.FileSystem.VirtualFileSystem; + +namespace Ryujinx.HLE.FileSystem.Content +{ + internal static class LocationHelper + { + public static string GetRealPath(VirtualFileSystem FileSystem,string SwitchContentPath) + { + string BasePath = FileSystem.GetBasePath(); + + switch (SwitchContentPath) + { + case ContentPath.SystemContent: + return Path.Combine(FileSystem.GetBasePath(), SystemNandPath, "Contents", "registered"); + case ContentPath.UserContent: + return Path.Combine(FileSystem.GetBasePath(), UserNandPath, "Contents", "registered"); + case ContentPath.SdCardContent: + return Path.Combine(FileSystem.GetSdCardPath(), "Nintendo", "Contents", "registered"); + case ContentPath.System: + return Path.Combine(BasePath, SystemNandPath); + case ContentPath.User: + return Path.Combine(BasePath, UserNandPath); + default: + throw new NotSupportedException($"Content Path `{SwitchContentPath}` is not supported."); + } + } + + public static string GetContentPath(ContentStorageId ContentStorageId) + { + switch (ContentStorageId) + { + case ContentStorageId.NandSystem: + return ContentPath.SystemContent; + case ContentStorageId.NandUser: + return ContentPath.UserContent; + case ContentStorageId.SdCard: + return ContentPath.SdCardContent; + default: + throw new NotSupportedException($"Content Storage `{ContentStorageId}` is not supported."); + } + } + + public static string GetContentPath(StorageId StorageId) + { + switch (StorageId) + { + case StorageId.NandSystem: + return ContentPath.SystemContent; + case StorageId.NandUser: + return ContentPath.UserContent; + case StorageId.SdCard: + return ContentPath.SdCardContent; + default: + throw new NotSupportedException($"Storage Id `{StorageId}` is not supported."); + } + } + + public static StorageId GetStorageId(string ContentPathString) + { + switch (ContentPathString) + { + case ContentPath.SystemContent: + case ContentPath.System: + return StorageId.NandSystem; + + case ContentPath.UserContent: + case ContentPath.User: + return StorageId.NandUser; + + case ContentPath.SdCardContent: + return StorageId.SdCard; + + case ContentPath.Host: + return StorageId.Host; + + case ContentPath.GamecardApp: + case ContentPath.GamecardContents: + case ContentPath.GamecardUpdate: + return StorageId.GameCard; + + default: + return StorageId.None; + } + } + } +} diff --git a/Ryujinx.HLE/FileSystem/Content/StorageId.cs b/Ryujinx.HLE/FileSystem/Content/StorageId.cs new file mode 100644 index 0000000000..e7c5ed7ee5 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/Content/StorageId.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.FileSystem +{ + public enum ContentStorageId : byte + { + NandSystem, + NandUser, + SdCard + } +} diff --git a/Ryujinx.HLE/FileSystem/Content/TitleType.cs b/Ryujinx.HLE/FileSystem/Content/TitleType.cs new file mode 100644 index 0000000000..ecadc2dc99 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/Content/TitleType.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ryujinx.HLE.FileSystem.Content +{ + enum TitleType : byte + { + SystemPrograms = 0x01, + SystemDataArchive = 0x02, + SystemUpdate = 0x03, + FirmwarePackageA = 0x04, + FirmwarePackageB = 0x05, + RegularApplication = 0x80, + Update = 0x81, + AddOnContent = 0x82, + DeltaTitle = 0x83 + } +} diff --git a/Ryujinx.HLE/FileSystem/FileSystemProvider.cs b/Ryujinx.HLE/FileSystem/FileSystemProvider.cs new file mode 100644 index 0000000000..ac89a86128 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/FileSystemProvider.cs @@ -0,0 +1,221 @@ +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.HOS.Services.FspSrv; +using System; +using System.IO; + +using static Ryujinx.HLE.HOS.ErrorCode; + +namespace Ryujinx.HLE.FileSystem +{ + class FileSystemProvider : IFileSystemProvider + { + private string BasePath; + private string RootPath; + + public FileSystemProvider(string BasePath, string RootPath) + { + this.BasePath = BasePath; + this.RootPath = RootPath; + + CheckIfDecendentOfRootPath(BasePath); + } + + public long CreateDirectory(string Name) + { + if (Directory.Exists(Name)) + { + return MakeError(ErrorModule.Fs, FsErr.PathAlreadyExists); + } + + Directory.CreateDirectory(Name); + + return 0; + } + + public long CreateFile(string Name, long Size) + { + if (File.Exists(Name)) + { + return MakeError(ErrorModule.Fs, FsErr.PathAlreadyExists); + } + + using (FileStream NewFile = File.Create(Name)) + { + NewFile.SetLength(Size); + } + + return 0; + } + + public long DeleteDirectory(string Name, bool Recursive) + { + string DirName = Name; + + if (!Directory.Exists(DirName)) + { + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + Directory.Delete(DirName, Recursive); + + return 0; + } + + public long DeleteFile(string Name) + { + if (!File.Exists(Name)) + { + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + else + { + File.Delete(Name); + } + + return 0; + } + + public string[] GetDirectories(string Path) + { + return Directory.GetDirectories(Path); + } + + public string[] GetEntries(string Path) + { + string DirName = GetFullPath(Path); + + if (Directory.Exists(DirName)) + { + return Directory.GetFileSystemEntries(DirName); + } + + return null; + } + + public string[] GetFiles(string Path) + { + return Directory.GetFiles(Path); + } + + public long GetFreeSpace(ServiceCtx Context) + { + return Context.Device.FileSystem.GetDrive().AvailableFreeSpace; + } + + public string GetFullPath(string Name) + { + if (Name.StartsWith("//")) + { + Name = Name.Substring(2); + } + else if (Name.StartsWith('/')) + { + Name = Name.Substring(1); + } + else + { + return null; + } + + string FullPath = Path.Combine(BasePath, Name); + + CheckIfDecendentOfRootPath(FullPath); + + return FullPath; + } + + public long GetTotalSpace(ServiceCtx Context) + { + return Context.Device.FileSystem.GetDrive().TotalSize; + } + + public bool DirectoryExists(string Name) + { + return Directory.Exists(Name); + } + + public bool FileExists(string Name) + { + return File.Exists(Name); + } + + public long OpenDirectory(string Name, int FilterFlags,out IDirectory DirectoryInterface) + { + DirectoryInterface = null; + + if (Directory.Exists(Name)) + { + DirectoryInterface = new IDirectory(Name, FilterFlags, this); + + return 0; + } + + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + public long OpenFile(string Name, out IFile FileInterface) + { + FileInterface = null; + + if (File.Exists(Name)) + { + FileStream Stream = new FileStream(Name, FileMode.Open); + + FileInterface = new IFile(Stream, Name); + + return 0; + } + + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + public long RenameDirectory(string OldName, string NewName) + { + if (Directory.Exists(OldName)) + { + Directory.Move(OldName, NewName); + } + else + { + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + return 0; + } + + public long RenameFile(string OldName, string NewName) + { + if (File.Exists(OldName)) + { + File.Move(OldName, NewName); + } + else + { + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + return 0; + } + + public void CheckIfDecendentOfRootPath(string Path) + { + DirectoryInfo PathInfo = new DirectoryInfo(Path); + DirectoryInfo RootInfo = new DirectoryInfo(RootPath); + + while (PathInfo.Parent != null) + { + if (PathInfo.Parent.FullName == RootInfo.FullName) + { + return; + } + else + { + PathInfo = PathInfo.Parent; + } + } + + throw new InvalidOperationException($"Path {BasePath} is not a child directory of {RootPath}"); + } + } +} diff --git a/Ryujinx.HLE/FileSystem/IFileSystemProvider.cs b/Ryujinx.HLE/FileSystem/IFileSystemProvider.cs new file mode 100644 index 0000000000..d8cb741e09 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/IFileSystemProvider.cs @@ -0,0 +1,40 @@ +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.HOS.Services.FspSrv; + +namespace Ryujinx.HLE.FileSystem +{ + interface IFileSystemProvider + { + long CreateFile(string Name, long Size); + + long CreateDirectory(string Name); + + long RenameFile(string OldName, string NewName); + + long RenameDirectory(string OldName, string NewName); + + string[] GetEntries(string Path); + + string[] GetDirectories(string Path); + + string[] GetFiles(string Path); + + long DeleteFile(string Name); + + long DeleteDirectory(string Name, bool Recursive); + + bool FileExists(string Name); + + bool DirectoryExists(string Name); + + long OpenFile(string Name, out IFile FileInterface); + + long OpenDirectory(string Name, int FilterFlags, out IDirectory DirectoryInterface); + + string GetFullPath(string Name); + + long GetFreeSpace(ServiceCtx Context); + + long GetTotalSpace(ServiceCtx Context); + } +} diff --git a/Ryujinx.HLE/FileSystem/RomFileSystemProvider.cs b/Ryujinx.HLE/FileSystem/RomFileSystemProvider.cs new file mode 100644 index 0000000000..549152ce3b --- /dev/null +++ b/Ryujinx.HLE/FileSystem/RomFileSystemProvider.cs @@ -0,0 +1,153 @@ +using LibHac; +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.HOS.Services.FspSrv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using static Ryujinx.HLE.HOS.ErrorCode; + +namespace Ryujinx.HLE.FileSystem +{ + class RomFileSystemProvider : IFileSystemProvider + { + private Romfs RomFs; + + public RomFileSystemProvider(Stream StorageStream) + { + RomFs = new Romfs(StorageStream); + } + + public long CreateDirectory(string Name) + { + throw new NotSupportedException(); + } + + public long CreateFile(string Name, long Size) + { + throw new NotSupportedException(); + } + + public long DeleteDirectory(string Name, bool Recursive) + { + throw new NotSupportedException(); + } + + public long DeleteFile(string Name) + { + throw new NotSupportedException(); + } + + public string[] GetDirectories(string Path) + { + List Directories = new List(); + + foreach(RomfsDir Directory in RomFs.Directories) + { + Directories.Add(Directory.Name); + } + + return Directories.ToArray(); + } + + public string[] GetEntries(string Path) + { + List Entries = new List(); + + foreach (RomfsDir Directory in RomFs.Directories) + { + Entries.Add(Directory.Name); + } + + foreach (RomfsFile File in RomFs.Files) + { + Entries.Add(File.Name); + } + + return Entries.ToArray(); + } + + public string[] GetFiles(string Path) + { + List Files = new List(); + + foreach (RomfsFile File in RomFs.Files) + { + Files.Add(File.Name); + } + + return Files.ToArray(); + } + + public long GetFreeSpace(ServiceCtx Context) + { + return 0; + } + + public string GetFullPath(string Name) + { + return Name; + } + + public long GetTotalSpace(ServiceCtx Context) + { + return RomFs.Files.Sum(x => x.DataLength); + } + + public bool DirectoryExists(string Name) + { + return RomFs.Directories.Exists(x=>x.Name == Name); + } + + public bool FileExists(string Name) + { + return RomFs.FileExists(Name); + } + + public long OpenDirectory(string Name, int FilterFlags, out IDirectory DirectoryInterface) + { + DirectoryInterface = null; + + RomfsDir Directory = RomFs.Directories.Find(x => x.Name == Name); + + if (Directory != null) + { + DirectoryInterface = new IDirectory(Name, FilterFlags, this); + } + + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + public long OpenFile(string Name, out IFile FileInterface) + { + FileInterface = null; + + if (File.Exists(Name)) + { + Stream Stream = RomFs.OpenFile(Name); + + FileInterface = new IFile(Stream, Name); + + return 0; + } + + return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); + } + + public long RenameDirectory(string OldName, string NewName) + { + throw new NotSupportedException(); + } + + public long RenameFile(string OldName, string NewName) + { + throw new NotSupportedException(); + } + + public void CheckIfOutsideBasePath(string Path) + { + throw new NotSupportedException(); + } + } +} diff --git a/Ryujinx.HLE/FileSystem/StorageId.cs b/Ryujinx.HLE/FileSystem/StorageId.cs new file mode 100644 index 0000000000..170f53ec76 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/StorageId.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.FileSystem +{ + internal enum StorageId : byte + { + None, + Host, + GameCard, + NandSystem, + NandUser, + SdCard + } +} diff --git a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index e621ec2b11..82fc96ba6f 100644 --- a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -1,3 +1,4 @@ +using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.HOS; using System; using System.IO; @@ -11,6 +12,7 @@ namespace Ryujinx.HLE.FileSystem public const string SdCardPath = "sdmc"; public const string SystemPath = "system"; + public static string SafeNandPath = Path.Combine(NandPath, "safe"); public static string SystemNandPath = Path.Combine(NandPath, "system"); public static string UserNandPath = Path.Combine(NandPath, "user"); @@ -63,9 +65,15 @@ namespace Ryujinx.HLE.FileSystem return MakeDirAndGetFullPath(SaveHelper.GetSavePath(Save, Context)); } + public string GetFullPartitionPath(string PartitionPath) + { + return MakeDirAndGetFullPath(PartitionPath); + } + public string SwitchPathToSystemPath(string SwitchPath) { string[] Parts = SwitchPath.Split(":"); + if (Parts.Length != 2) { return null; @@ -76,10 +84,13 @@ namespace Ryujinx.HLE.FileSystem public string SystemPathToSwitchPath(string SystemPath) { string BaseSystemPath = GetBasePath() + Path.DirectorySeparatorChar; + if (SystemPath.StartsWith(BaseSystemPath)) { string RawPath = SystemPath.Replace(BaseSystemPath, ""); + int FirstSeparatorOffset = RawPath.IndexOf(Path.DirectorySeparatorChar); + if (FirstSeparatorOffset == -1) { return $"{RawPath}:/"; @@ -87,6 +98,7 @@ namespace Ryujinx.HLE.FileSystem string BasePath = RawPath.Substring(0, FirstSeparatorOffset); string FileName = RawPath.Substring(FirstSeparatorOffset + 1); + return $"{BasePath}:/{FileName}"; } return null; @@ -94,6 +106,30 @@ namespace Ryujinx.HLE.FileSystem private string MakeDirAndGetFullPath(string Dir) { + // Handles Common Switch Content Paths + switch (Dir) + { + case ContentPath.SdCard: + case "@Sdcard": + Dir = SdCardPath; + break; + case ContentPath.User: + Dir = UserNandPath; + break; + case ContentPath.System: + Dir = SystemNandPath; + break; + case ContentPath.SdCardContent: + Dir = Path.Combine(SdCardPath, "Nintendo", "Contents"); + break; + case ContentPath.UserContent: + Dir = Path.Combine(UserNandPath, "Contents"); + break; + case ContentPath.SystemContent: + Dir = Path.Combine(SystemNandPath, "Contents"); + break; + } + string FullPath = Path.Combine(GetBasePath(), Dir); if (!Directory.Exists(FullPath)) diff --git a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs index 0be5e89670..d7267c30d5 100644 --- a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs +++ b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs @@ -1,14 +1,19 @@ -using Ryujinx.HLE.Memory; +using LibHac; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.Resource; using Ryujinx.HLE.Utilities; using System.Collections.Generic; using System.IO; +using System.Linq; + +using static Ryujinx.HLE.Utilities.FontUtils; namespace Ryujinx.HLE.HOS.Font { class SharedFontManager { - private DeviceMemory Memory; + private Switch Device; private long PhysicalAddress; @@ -32,21 +37,68 @@ namespace Ryujinx.HLE.HOS.Font { this.PhysicalAddress = PhysicalAddress; - Memory = Device.Memory; + this.Device = Device; FontsPath = Path.Combine(Device.FileSystem.GetSystemPath(), "fonts"); } - public void EnsureInitialized() + public void EnsureInitialized(ContentManager ContentManager) { if (FontData == null) { - Memory.FillWithZeros(PhysicalAddress, Horizon.FontSize); + Device.Memory.FillWithZeros(PhysicalAddress, Horizon.FontSize); uint FontOffset = 0; FontInfo CreateFont(string Name) { + if (ContentManager.TryGetFontTitle(Name, out long FontTitle)) + { + string FontContentPath = ContentManager.GetInstalledPath(FontTitle, ContentType.Data, StorageId.NandSystem); + + if (!string.IsNullOrWhiteSpace(FontContentPath)) + { + int FileIndex = 0; + + //Use second file in Chinese Font title for standard + if(Name == "FontChineseSimplified") + { + FileIndex = 1; + } + + FileStream NcaFileStream = new FileStream(FontContentPath, FileMode.Open, FileAccess.Read); + + Nca Nca = new Nca(Device.System.KeySet, NcaFileStream, false); + + NcaSection RomfsSection = Nca.Sections.FirstOrDefault(x => x?.Type == SectionType.Romfs); + + Romfs Romfs = new Romfs(Nca.OpenSection(RomfsSection.SectionNum, false, Device.System.FsIntegrityCheckLevel)); + + Stream EncryptedFontFile = Romfs.OpenFile(Romfs.Files[FileIndex]); + + byte[] Data = DecryptFont(EncryptedFontFile); + + FontInfo Info = new FontInfo((int)FontOffset, Data.Length); + + WriteMagicAndSize(PhysicalAddress + FontOffset, Data.Length); + + FontOffset += 8; + + uint Start = FontOffset; + + for (; FontOffset - Start < Data.Length; FontOffset++) + { + Device.Memory.WriteByte(PhysicalAddress + FontOffset, Data[FontOffset - Start]); + } + + NcaFileStream.Dispose(); + + Nca.Dispose(); + + return Info; + } + } + string FontFilePath = Path.Combine(FontsPath, Name + ".ttf"); if (File.Exists(FontFilePath)) @@ -63,7 +115,7 @@ namespace Ryujinx.HLE.HOS.Font for (; FontOffset - Start < Data.Length; FontOffset++) { - Memory.WriteByte(PhysicalAddress + FontOffset, Data[FontOffset - Start]); + Device.Memory.WriteByte(PhysicalAddress + FontOffset, Data[FontOffset - Start]); } return Info; @@ -101,20 +153,20 @@ namespace Ryujinx.HLE.HOS.Font int EncryptedSize = EndianSwap.Swap32(Size ^ Key); - Memory.WriteInt32(Position + 0, DecMagic); - Memory.WriteInt32(Position + 4, EncryptedSize); + Device.Memory.WriteInt32(Position + 0, DecMagic); + Device.Memory.WriteInt32(Position + 4, EncryptedSize); } public int GetFontSize(SharedFontType FontType) { - EnsureInitialized(); + EnsureInitialized(Device.System.ContentManager); return FontData[FontType].Size; } public int GetSharedMemoryAddressOffset(SharedFontType FontType) { - EnsureInitialized(); + EnsureInitialized(Device.System.ContentManager); return FontData[FontType].Offset + 8; } diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index af175bd469..8c0ddae732 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -1,5 +1,6 @@ using LibHac; using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.HOS.Font; using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.SystemState; @@ -42,6 +43,8 @@ namespace Ryujinx.HLE.HOS internal SharedFontManager Font { get; private set; } + internal ContentManager ContentManager { get; private set; } + internal KEvent VsyncEvent { get; private set; } internal Keyset KeySet { get; private set; } @@ -90,6 +93,8 @@ namespace Ryujinx.HLE.HOS VsyncEvent = new KEvent(this); LoadKeySet(); + + ContentManager = new ContentManager(Device); } public void LoadCart(string ExeFsDir, string RomFsFile = null) @@ -156,6 +161,8 @@ namespace Ryujinx.HLE.HOS LoadNso("subsdk*"); LoadNso("sdk"); + ContentManager.LoadEntries(); + MainProcess.Run(); } @@ -174,6 +181,8 @@ namespace Ryujinx.HLE.HOS return; } + ContentManager.LoadEntries(); + LoadNca(MainNca, ControlNca); } @@ -412,6 +421,8 @@ namespace Ryujinx.HLE.HOS LoadNso("subsdk"); LoadNso("sdk"); + ContentManager.LoadEntries(); + MainProcess.Run(); } @@ -419,13 +430,13 @@ namespace Ryujinx.HLE.HOS { bool IsNro = Path.GetExtension(FilePath).ToLower() == ".nro"; - string Name = Path.GetFileNameWithoutExtension(FilePath); + string Name = Path.GetFileNameWithoutExtension(FilePath); string SwitchFilePath = Device.FileSystem.SystemPathToSwitchPath(FilePath); if (IsNro && (SwitchFilePath == null || !SwitchFilePath.StartsWith("sdmc:/"))) { string SwitchPath = $"sdmc:/switch/{Name}{Homebrew.TemporaryNroSuffix}"; - string TempPath = Device.FileSystem.SwitchPathToSystemPath(SwitchPath); + string TempPath = Device.FileSystem.SwitchPathToSystemPath(SwitchPath); string SwitchDir = Path.GetDirectoryName(TempPath); @@ -449,6 +460,9 @@ namespace Ryujinx.HLE.HOS } MainProcess.SetEmptyArgs(); + + ContentManager.LoadEntries(); + MainProcess.Run(IsNro); } diff --git a/Ryujinx.HLE/HOS/Services/Es/IETicketService.cs b/Ryujinx.HLE/HOS/Services/Es/IETicketService.cs new file mode 100644 index 0000000000..f11c78ccaa --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Es/IETicketService.cs @@ -0,0 +1,23 @@ +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Es +{ + class IETicketService : IpcService + { + private Dictionary m_Commands; + + public override IReadOnlyDictionary Commands => m_Commands; + + private bool IsInitialized; + + public IETicketService() + { + m_Commands = new Dictionary() + { + + }; + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/FspSrv/FileSystemType.cs b/Ryujinx.HLE/HOS/Services/FspSrv/FileSystemType.cs new file mode 100644 index 0000000000..2fd5967341 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/FspSrv/FileSystemType.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.FspSrv +{ + enum FileSystemType : int + { + Logo = 2, + ContentControl = 3, + ContentManual = 4, + ContentMeta = 5, + ContentData = 6, + ApplicationPackage = 7 + } +} diff --git a/Ryujinx.HLE/HOS/Services/FspSrv/FsErr.cs b/Ryujinx.HLE/HOS/Services/FspSrv/FsErr.cs index 39eadcec62..804ede2bc3 100644 --- a/Ryujinx.HLE/HOS/Services/FspSrv/FsErr.cs +++ b/Ryujinx.HLE/HOS/Services/FspSrv/FsErr.cs @@ -5,5 +5,6 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv public const int PathDoesNotExist = 1; public const int PathAlreadyExists = 2; public const int PathAlreadyInUse = 7; + public const int InvalidInput = 6001; } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/FspSrv/IDirectory.cs b/Ryujinx.HLE/HOS/Services/FspSrv/IDirectory.cs index d6ae084f89..87a6b36b1f 100644 --- a/Ryujinx.HLE/HOS/Services/FspSrv/IDirectory.cs +++ b/Ryujinx.HLE/HOS/Services/FspSrv/IDirectory.cs @@ -1,3 +1,4 @@ +using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Ipc; using System; using System.Collections.Generic; @@ -20,9 +21,11 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv public event EventHandler Disposed; - public string HostPath { get; private set; } + public string DirectoryPath { get; private set; } - public IDirectory(string HostPath, int Flags) + private IFileSystemProvider Provider; + + public IDirectory(string DirectoryPath, int Flags, IFileSystemProvider Provider) { m_Commands = new Dictionary() { @@ -30,18 +33,20 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv { 1, GetEntryCount } }; - this.HostPath = HostPath; + this.Provider = Provider; + + this.DirectoryPath = DirectoryPath; DirectoryEntries = new List(); if ((Flags & 1) != 0) { - DirectoryEntries.AddRange(Directory.GetDirectories(HostPath)); + DirectoryEntries.AddRange(Provider.GetDirectories(DirectoryPath)); } if ((Flags & 2) != 0) { - DirectoryEntries.AddRange(Directory.GetFiles(HostPath)); + DirectoryEntries.AddRange(Provider.GetFiles(DirectoryPath)); } CurrentItemIndex = 0; diff --git a/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystem.cs b/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystem.cs index bd249e5084..91faee9458 100644 --- a/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystem.cs +++ b/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystem.cs @@ -1,10 +1,11 @@ +using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Ipc; using System; using System.Collections.Generic; using System.IO; -using System.Text; using static Ryujinx.HLE.HOS.ErrorCode; +using static Ryujinx.HLE.Utilities.StringUtils; namespace Ryujinx.HLE.HOS.Services.FspSrv { @@ -18,7 +19,9 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv private string Path; - public IFileSystem(string Path) + private IFileSystemProvider Provider; + + public IFileSystem(string Path, IFileSystemProvider Provider) { m_Commands = new Dictionary() { @@ -42,6 +45,8 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv OpenPaths = new HashSet(); this.Path = Path; + + this.Provider = Provider; } public long CreateFile(ServiceCtx Context) @@ -51,14 +56,14 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv long Mode = Context.RequestData.ReadInt64(); int Size = Context.RequestData.ReadInt32(); - string FileName = Context.Device.FileSystem.GetFullPath(Path, Name); + string FileName = Provider.GetFullPath(Name); if (FileName == null) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } - if (File.Exists(FileName)) + if (Provider.FileExists(FileName)) { return MakeError(ErrorModule.Fs, FsErr.PathAlreadyExists); } @@ -68,21 +73,16 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - using (FileStream NewFile = File.Create(FileName)) - { - NewFile.SetLength(Size); - } - - return 0; + return Provider.CreateFile(FileName, Size); } public long DeleteFile(ServiceCtx Context) { string Name = ReadUtf8String(Context); - string FileName = Context.Device.FileSystem.GetFullPath(Path, Name); + string FileName = Provider.GetFullPath(Name); - if (!File.Exists(FileName)) + if (!Provider.FileExists(FileName)) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } @@ -92,23 +92,21 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - File.Delete(FileName); - - return 0; + return Provider.DeleteFile(FileName); } public long CreateDirectory(ServiceCtx Context) { string Name = ReadUtf8String(Context); - string DirName = Context.Device.FileSystem.GetFullPath(Path, Name); + string DirName = Provider.GetFullPath(Name); if (DirName == null) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } - if (Directory.Exists(DirName)) + if (Provider.DirectoryExists(DirName)) { return MakeError(ErrorModule.Fs, FsErr.PathAlreadyExists); } @@ -118,7 +116,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - Directory.CreateDirectory(DirName); + Provider.CreateDirectory(DirName); return 0; } @@ -137,7 +135,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv { string Name = ReadUtf8String(Context); - string DirName = Context.Device.FileSystem.GetFullPath(Path, Name); + string DirName = Provider.GetFullPath(Name); if (!Directory.Exists(DirName)) { @@ -149,7 +147,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - Directory.Delete(DirName, Recursive); + Provider.DeleteDirectory(DirName, Recursive); return 0; } @@ -159,15 +157,15 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv string OldName = ReadUtf8String(Context, 0); string NewName = ReadUtf8String(Context, 1); - string OldFileName = Context.Device.FileSystem.GetFullPath(Path, OldName); - string NewFileName = Context.Device.FileSystem.GetFullPath(Path, NewName); + string OldFileName = Provider.GetFullPath(OldName); + string NewFileName = Provider.GetFullPath(NewName); - if (!File.Exists(OldFileName)) + if (Provider.FileExists(OldFileName)) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } - if (File.Exists(NewFileName)) + if (Provider.FileExists(NewFileName)) { return MakeError(ErrorModule.Fs, FsErr.PathAlreadyExists); } @@ -177,9 +175,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - File.Move(OldFileName, NewFileName); - - return 0; + return Provider.RenameFile(OldFileName, NewFileName); } public long RenameDirectory(ServiceCtx Context) @@ -187,15 +183,15 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv string OldName = ReadUtf8String(Context, 0); string NewName = ReadUtf8String(Context, 1); - string OldDirName = Context.Device.FileSystem.GetFullPath(Path, OldName); - string NewDirName = Context.Device.FileSystem.GetFullPath(Path, NewName); + string OldDirName = Provider.GetFullPath(OldName); + string NewDirName = Provider.GetFullPath(NewName); - if (!Directory.Exists(OldDirName)) + if (!Provider.DirectoryExists(OldDirName)) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } - if (Directory.Exists(NewDirName)) + if (!Provider.DirectoryExists(NewDirName)) { return MakeError(ErrorModule.Fs, FsErr.PathAlreadyExists); } @@ -205,22 +201,20 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - Directory.Move(OldDirName, NewDirName); - - return 0; + return Provider.RenameDirectory(OldDirName, NewDirName); } public long GetEntryType(ServiceCtx Context) { string Name = ReadUtf8String(Context); - string FileName = Context.Device.FileSystem.GetFullPath(Path, Name); + string FileName = Provider.GetFullPath(Name); - if (File.Exists(FileName)) + if (Provider.FileExists(FileName)) { Context.ResponseData.Write(1); } - else if (Directory.Exists(FileName)) + else if (Provider.DirectoryExists(FileName)) { Context.ResponseData.Write(0); } @@ -240,9 +234,9 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv string Name = ReadUtf8String(Context); - string FileName = Context.Device.FileSystem.GetFullPath(Path, Name); + string FileName = Provider.GetFullPath(Name); - if (!File.Exists(FileName)) + if (!Provider.FileExists(FileName)) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } @@ -252,20 +246,24 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - FileStream Stream = new FileStream(FileName, FileMode.Open); - IFile FileInterface = new IFile(Stream, FileName); + long Error = Provider.OpenFile(FileName, out IFile FileInterface); - FileInterface.Disposed += RemoveFileInUse; - - lock (OpenPaths) + if (Error == 0) { - OpenPaths.Add(FileName); + FileInterface.Disposed += RemoveFileInUse; + + lock (OpenPaths) + { + OpenPaths.Add(FileName); + } + + MakeObject(Context, FileInterface); + + return 0; } - MakeObject(Context, FileInterface); - - return 0; + return Error; } public long OpenDirectory(ServiceCtx Context) @@ -274,25 +272,33 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv string Name = ReadUtf8String(Context); - string DirName = Context.Device.FileSystem.GetFullPath(Path, Name); + string DirName = Provider.GetFullPath(Name); - if (!Directory.Exists(DirName)) + if (!Provider.DirectoryExists(DirName)) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } - IDirectory DirInterface = new IDirectory(DirName, FilterFlags); - - DirInterface.Disposed += RemoveDirectoryInUse; - - lock (OpenPaths) + if (IsPathAlreadyInUse(DirName)) { - OpenPaths.Add(DirName); + return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - MakeObject(Context, DirInterface); + long Error = Provider.OpenDirectory(DirName, FilterFlags, out IDirectory DirInterface); - return 0; + if (Error == 0) + { + DirInterface.Disposed += RemoveDirectoryInUse; + + lock (OpenPaths) + { + OpenPaths.Add(DirName); + } + + MakeObject(Context, DirInterface); + } + + return Error; } public long Commit(ServiceCtx Context) @@ -304,7 +310,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv { string Name = ReadUtf8String(Context); - Context.ResponseData.Write(Context.Device.FileSystem.GetDrive().AvailableFreeSpace); + Context.ResponseData.Write(Provider.GetFreeSpace(Context)); return 0; } @@ -313,7 +319,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv { string Name = ReadUtf8String(Context); - Context.ResponseData.Write(Context.Device.FileSystem.GetDrive().TotalSize); + Context.ResponseData.Write(Provider.GetFreeSpace(Context)); return 0; } @@ -322,9 +328,9 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv { string Name = ReadUtf8String(Context); - string DirName = Context.Device.FileSystem.GetFullPath(Path, Name); + string DirName = Provider.GetFullPath(Name); - if (!Directory.Exists(DirName)) + if (!Provider.DirectoryExists(DirName)) { return MakeError(ErrorModule.Fs, FsErr.PathDoesNotExist); } @@ -334,15 +340,15 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return MakeError(ErrorModule.Fs, FsErr.PathAlreadyInUse); } - foreach (string Entry in Directory.EnumerateFileSystemEntries(DirName)) + foreach (string Entry in Provider.GetEntries(DirName)) { - if (Directory.Exists(Entry)) + if (Provider.DirectoryExists(Entry)) { - Directory.Delete(Entry, true); + Provider.DeleteDirectory(Entry, true); } - else if (File.Exists(Entry)) + else if (Provider.FileExists(Entry)) { - File.Delete(Entry); + Provider.DeleteFile(Entry); } } @@ -377,30 +383,7 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv { DirInterface.Disposed -= RemoveDirectoryInUse; - OpenPaths.Remove(DirInterface.HostPath); - } - } - - private string ReadUtf8String(ServiceCtx Context, int Index = 0) - { - long Position = Context.Request.PtrBuff[Index].Position; - long Size = Context.Request.PtrBuff[Index].Size; - - using (MemoryStream MS = new MemoryStream()) - { - while (Size-- > 0) - { - byte Value = Context.Memory.ReadByte(Position++); - - if (Value == 0) - { - break; - } - - MS.WriteByte(Value); - } - - return Encoding.UTF8.GetString(MS.ToArray()); + OpenPaths.Remove(DirInterface.DirectoryPath); } } } diff --git a/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs b/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs index daf5e0b269..82277bb68b 100644 --- a/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs +++ b/Ryujinx.HLE/HOS/Services/FspSrv/IFileSystemProxy.cs @@ -1,7 +1,14 @@ +using LibHac; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.Utilities; using System.Collections.Generic; +using System.IO; +using System.Linq; + +using static Ryujinx.HLE.FileSystem.VirtualFileSystem; +using static Ryujinx.HLE.HOS.ErrorCode; +using static Ryujinx.HLE.Utilities.StringUtils; namespace Ryujinx.HLE.HOS.Services.FspSrv { @@ -16,10 +23,13 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv m_Commands = new Dictionary() { { 1, SetCurrentProcess }, + { 11, OpenBisFileSystem }, + { 8, OpenFileSystemWithId }, { 18, OpenSdCardFileSystem }, { 51, OpenSaveDataFileSystem }, { 52, OpenSaveDataFileSystemBySystemSaveDataId }, { 200, OpenDataStorageByCurrentProcess }, + { 202, OpenDataStorageByDataId }, { 203, OpenPatchDataStorageByCurrentProcess }, { 1005, GetGlobalAccessLogMode } }; @@ -30,9 +40,76 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return 0; } + public long OpenBisFileSystem(ServiceCtx Context) + { + int BisPartitionId = Context.RequestData.ReadInt32(); + + string PartitionString = ReadUtf8String(Context); + + string BisPartitonPath = string.Empty; + + switch (BisPartitionId) + { + case 29: + BisPartitonPath = SafeNandPath; + break; + case 30: + case 31: + BisPartitonPath = SystemNandPath; + break; + case 32: + BisPartitonPath = UserNandPath; + break; + default: + return MakeError(ErrorModule.Fs, FsErr.InvalidInput); + } + + string FullPath = Context.Device.FileSystem.GetFullPartitionPath(BisPartitonPath); + + FileSystemProvider FileSystemProvider = new FileSystemProvider(FullPath, Context.Device.FileSystem.GetBasePath()); + + MakeObject(Context, new IFileSystem(FullPath, FileSystemProvider)); + + return 0; + } + + public long OpenFileSystemWithId(ServiceCtx Context) + { + FileSystemType FileSystemType = (FileSystemType)Context.RequestData.ReadInt32(); + + long TitleId = Context.RequestData.ReadInt64(); + + string Path = ReadUtf8String(Context); + + string FullPath = Context.Device.FileSystem.SwitchPathToSystemPath(Path); + + FileStream FileStream = new FileStream(FullPath, FileMode.Open, FileAccess.Read); + + Nca Nca = new Nca(Context.Device.System.KeySet, FileStream, false); + + NcaSection RomfsSection = Nca.Sections.FirstOrDefault(x => x?.Type == SectionType.Romfs); + + if (RomfsSection != null) + { + Stream RomfsStream = Nca.OpenSection(RomfsSection.SectionNum, false, Context.Device.System.FsIntegrityCheckLevel); + + IFileSystem NcaFileSystem = new IFileSystem(Path, new RomFileSystemProvider(RomfsStream)); + + MakeObject(Context, NcaFileSystem); + + return 0; + } + + return MakeError(ErrorModule.Fs, FsErr.InvalidInput); + } + public long OpenSdCardFileSystem(ServiceCtx Context) { - MakeObject(Context, new IFileSystem(Context.Device.FileSystem.GetSdCardPath())); + string SdCardPath = Context.Device.FileSystem.GetSdCardPath(); + + FileSystemProvider FileSystemProvider = new FileSystemProvider(SdCardPath, Context.Device.FileSystem.GetBasePath()); + + MakeObject(Context, new IFileSystem(SdCardPath, FileSystemProvider)); return 0; } @@ -58,6 +135,69 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv return 0; } + public long OpenDataStorageByDataId(ServiceCtx Context) + { + StorageId StorageId = (StorageId)Context.RequestData.ReadByte(); + + byte[] Padding = Context.RequestData.ReadBytes(7); + + long TitleId = Context.RequestData.ReadInt64(); + + StorageId InstalledStorage = + Context.Device.System.ContentManager.GetInstalledStorage(TitleId, ContentType.Data, StorageId); + + if (InstalledStorage == StorageId.None) + { + InstalledStorage = + Context.Device.System.ContentManager.GetInstalledStorage(TitleId, ContentType.AocData, StorageId); + } + + if (InstalledStorage != StorageId.None) + { + string InstallPath = + Context.Device.System.ContentManager.GetInstalledPath(TitleId, ContentType.AocData, StorageId); + + UInt128 NcaId = Context.Device.System.ContentManager.GetInstalledNcaId(TitleId, ContentType.AocData); + + if (string.IsNullOrWhiteSpace(InstallPath)) + { + InstallPath = + Context.Device.System.ContentManager.GetInstalledPath(TitleId, ContentType.Data, StorageId); + } + + if ((NcaId.High | NcaId.Low) == 0) + { + NcaId = Context.Device.System.ContentManager.GetInstalledNcaId(TitleId, ContentType.Data); + } + + if (!string.IsNullOrWhiteSpace(InstallPath)) + { + string NcaPath = InstallPath; + + if (File.Exists(NcaPath)) + { + FileStream NcaStream = new FileStream(NcaPath, FileMode.Open, FileAccess.Read); + + Nca Nca = new Nca(Context.Device.System.KeySet, NcaStream, false); + + NcaSection RomfsSection = Nca.Sections.FirstOrDefault(x => x?.Type == SectionType.Romfs); + + Stream RomfsStream = Nca.OpenSection(RomfsSection.SectionNum, false, Context.Device.System.FsIntegrityCheckLevel); + + MakeObject(Context, new IStorage(RomfsStream)); + + return 0; + } + else + throw new FileNotFoundException($"No Nca found in Path `{NcaPath}`."); + } + else + throw new DirectoryNotFoundException($"Path for title id {TitleId:x16} on Storage {StorageId} was not found in Path {InstallPath}."); + } + + throw new FileNotFoundException($"System archive with titleid {TitleId:x16} was not found on Storage {StorageId}. Found in {InstalledStorage}."); + } + public long OpenPatchDataStorageByCurrentProcess(ServiceCtx Context) { MakeObject(Context, new IStorage(Context.Device.FileSystem.RomFs)); @@ -88,7 +228,11 @@ namespace Ryujinx.HLE.HOS.Services.FspSrv SaveInfo SaveInfo = new SaveInfo(TitleId, SaveId, SaveDataType, UserId, SaveSpaceId); - MakeObject(Context, new IFileSystem(Context.Device.FileSystem.GetGameSavePath(SaveInfo, Context))); + string SavePath = Context.Device.FileSystem.GetGameSavePath(SaveInfo, Context); + + FileSystemProvider FileSystemProvider = new FileSystemProvider(SavePath, Context.Device.FileSystem.GetBasePath()); + + MakeObject(Context, new IFileSystem(SavePath, FileSystemProvider)); } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Lr/ILocationResolver.cs b/Ryujinx.HLE/HOS/Services/Lr/ILocationResolver.cs new file mode 100644 index 0000000000..4a50fc18f8 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Lr/ILocationResolver.cs @@ -0,0 +1,252 @@ +using LibHac; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.FileSystem.Content; +using Ryujinx.HLE.HOS.Ipc; +using System.Collections.Generic; +using System.Text; + +using static Ryujinx.HLE.HOS.ErrorCode; +using static Ryujinx.HLE.Utilities.StringUtils; + +namespace Ryujinx.HLE.HOS.Services.Lr +{ + class ILocationResolver : IpcService + { + private Dictionary m_Commands; + + public override IReadOnlyDictionary Commands => m_Commands; + + private StorageId StorageId; + + public ILocationResolver(StorageId StorageId) + { + m_Commands = new Dictionary() + { + { 0, ResolveProgramPath }, + { 1, RedirectProgramPath }, + { 2, ResolveApplicationControlPath }, + { 3, ResolveApplicationHtmlDocumentPath }, + { 4, ResolveDataPath }, + { 5, RedirectApplicationControlPath }, + { 6, RedirectApplicationHtmlDocumentPath }, + { 7, ResolveApplicationLegalInformationPath }, + { 8, RedirectApplicationLegalInformationPath }, + { 9, Refresh }, + { 10, SetProgramNcaPath2 }, + { 11, ClearLocationResolver2 }, + { 12, DeleteProgramNcaPath }, + { 13, DeleteControlNcaPath }, + { 14, DeleteDocHtmlNcaPath }, + { 15, DeleteInfoHtmlNcaPath } + }; + + this.StorageId = StorageId; + } + + public long DeleteInfoHtmlNcaPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + DeleteContentPath(Context, TitleId, ContentType.Manual); + + return 0; + } + + public long DeleteDocHtmlNcaPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + DeleteContentPath(Context, TitleId, ContentType.Manual); + + return 0; + } + + public long DeleteControlNcaPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + DeleteContentPath(Context, TitleId, ContentType.Control); + + return 0; + } + + public long DeleteProgramNcaPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + DeleteContentPath(Context, TitleId, ContentType.Program); + + return 0; + } + + public long ClearLocationResolver2(ServiceCtx Context) + { + Context.Device.System.ContentManager.RefreshEntries(StorageId, 1); + + return 0; + } + + public long SetProgramNcaPath2(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + RedirectPath(Context, TitleId, 1, ContentType.Program); + + return 0; + } + + public long RedirectApplicationControlPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + RedirectPath(Context, TitleId, 1, ContentType.Control); + + return 0; + } + + public long RedirectApplicationHtmlDocumentPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + RedirectPath(Context, TitleId, 1, ContentType.Manual); + + return 0; + } + + public long RedirectApplicationLegalInformationPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + RedirectPath(Context, TitleId, 1, ContentType.Manual); + + return 0; + } + + public long ResolveDataPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + if (ResolvePath(Context, TitleId, ContentType.Data) || ResolvePath(Context, TitleId, ContentType.AocData)) + { + return 0; + } + else + { + return MakeError(ErrorModule.Lr, LrErr.AccessDenied); + } + } + + public long ResolveApplicationHtmlDocumentPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + if (ResolvePath(Context, TitleId, ContentType.Manual)) + { + return 0; + } + else + { + return MakeError(ErrorModule.Lr, LrErr.AccessDenied); + } + } + + public long ResolveApplicationLegalInformationPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + if (ResolvePath(Context, TitleId, ContentType.Manual)) + { + return 0; + } + else + { + return MakeError(ErrorModule.Lr, LrErr.AccessDenied); + } + } + + public long ResolveApplicationControlPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + if (ResolvePath(Context, TitleId, ContentType.Control)) + { + return 0; + } + else + { + return MakeError(ErrorModule.Lr, LrErr.AccessDenied); + } + } + + public long RedirectProgramPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + RedirectPath(Context, TitleId, 0, ContentType.Program); + + return 0; + } + + public long Refresh(ServiceCtx Context) + { + Context.Device.System.ContentManager.RefreshEntries(StorageId, 1); + + return 0; + } + + public long ResolveProgramPath(ServiceCtx Context) + { + long TitleId = Context.RequestData.ReadInt64(); + + if (ResolvePath(Context, TitleId, ContentType.Program)) + { + return 0; + } + else + { + return MakeError(ErrorModule.Lr, LrErr.ProgramLocationEntryNotFound); + } + } + + private void RedirectPath(ServiceCtx Context, long TitleId, int Flag, ContentType ContentType) + { + string ContentPath = ReadUtf8String(Context); + + LocationEntry NewLocation = new LocationEntry(ContentPath, Flag, TitleId, ContentType); + + Context.Device.System.ContentManager.RedirectLocation(NewLocation, StorageId); + } + + private bool ResolvePath(ServiceCtx Context, long TitleId,ContentType ContentType) + { + ContentManager ContentManager = Context.Device.System.ContentManager; + + string ContentPath = ContentManager.GetInstalledContentStorage(TitleId, StorageId, ContentType.Program); + + if (!string.IsNullOrWhiteSpace(ContentPath)) + { + long Position = Context.Request.RecvListBuff[0].Position; + long Size = Context.Request.RecvListBuff[0].Size; + + byte[] ContentPathBuffer = Encoding.UTF8.GetBytes(ContentPath); + + Context.Memory.WriteBytes(Position, ContentPathBuffer); + } + else + { + return false; + } + + return true; + } + + private void DeleteContentPath(ServiceCtx Context, long TitleId, ContentType ContentType) + { + ContentManager ContentManager = Context.Device.System.ContentManager; + + string ContentPath = ContentManager.GetInstalledContentStorage(TitleId, StorageId, ContentType.Manual); + + ContentManager.ClearEntry(TitleId, ContentType.Manual, StorageId); + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Lr/ILocationResolverManager.cs b/Ryujinx.HLE/HOS/Services/Lr/ILocationResolverManager.cs new file mode 100644 index 0000000000..8092f07754 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Lr/ILocationResolverManager.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.FileSystem; + +namespace Ryujinx.HLE.HOS.Services.Lr +{ + class ILocationResolverManager : IpcService + { + private Dictionary m_Commands; + + public override IReadOnlyDictionary Commands => m_Commands; + + public ILocationResolverManager() + { + m_Commands = new Dictionary() + { + { 0, OpenLocationResolver }, + }; + } + + private long OpenLocationResolver(ServiceCtx Context) + { + StorageId StorageId = (StorageId)Context.RequestData.ReadByte(); + + MakeObject(Context, new ILocationResolver(StorageId)); + + return 0; + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Lr/LrErr.cs b/Ryujinx.HLE/HOS/Services/Lr/LrErr.cs new file mode 100644 index 0000000000..5c87b73be6 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Lr/LrErr.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Lr +{ + class LrErr + { + public const int ProgramLocationEntryNotFound = 2; + public const int AccessDenied = 5; + } +} diff --git a/Ryujinx.HLE/HOS/Services/Ncm/IContentManager.cs b/Ryujinx.HLE/HOS/Services/Ncm/IContentManager.cs new file mode 100644 index 0000000000..29792a1baf --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Ncm/IContentManager.cs @@ -0,0 +1,20 @@ +using Ryujinx.HLE.HOS.Ipc; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Ncm +{ + class IContentManager : IpcService + { + private Dictionary m_Commands; + + public override IReadOnlyDictionary Commands => m_Commands; + + public IContentManager() + { + m_Commands = new Dictionary() + { + + }; + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Ncm/IContentStorage.cs b/Ryujinx.HLE/HOS/Services/Ncm/IContentStorage.cs new file mode 100644 index 0000000000..a19fe079d5 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Ncm/IContentStorage.cs @@ -0,0 +1,20 @@ +using Ryujinx.HLE.HOS.Ipc; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Ncm +{ + class IContentStorage : IpcService + { + private Dictionary m_Commands; + + public override IReadOnlyDictionary Commands => m_Commands; + + public IContentStorage() + { + m_Commands = new Dictionary() + { + + }; + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Ns/IApplicationManagerInterface.cs b/Ryujinx.HLE/HOS/Services/Ns/IApplicationManagerInterface.cs new file mode 100644 index 0000000000..ee438d993a --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Ns/IApplicationManagerInterface.cs @@ -0,0 +1,23 @@ +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Ns +{ + class IApplicationManagerInterface : IpcService + { + private Dictionary m_Commands; + + public override IReadOnlyDictionary Commands => m_Commands; + + private bool IsInitialized; + + public IApplicationManagerInterface() + { + m_Commands = new Dictionary() + { + + }; + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Pl/ISharedFontManager.cs b/Ryujinx.HLE/HOS/Services/Pl/ISharedFontManager.cs index d73adc0efd..0495a388a3 100644 --- a/Ryujinx.HLE/HOS/Services/Pl/ISharedFontManager.cs +++ b/Ryujinx.HLE/HOS/Services/Pl/ISharedFontManager.cs @@ -65,7 +65,7 @@ namespace Ryujinx.HLE.HOS.Services.Pl public long GetSharedMemoryNativeHandle(ServiceCtx Context) { - Context.Device.System.Font.EnsureInitialized(); + Context.Device.System.Font.EnsureInitialized(Context.Device.System.ContentManager); if (Context.Process.HandleTable.GenerateHandle(Context.Device.System.FontSharedMem, out int Handle) != KernelResult.Success) { diff --git a/Ryujinx.HLE/HOS/Services/ServiceFactory.cs b/Ryujinx.HLE/HOS/Services/ServiceFactory.cs index 5908e81082..44455e27c0 100644 --- a/Ryujinx.HLE/HOS/Services/ServiceFactory.cs +++ b/Ryujinx.HLE/HOS/Services/ServiceFactory.cs @@ -4,12 +4,14 @@ using Ryujinx.HLE.HOS.Services.Apm; using Ryujinx.HLE.HOS.Services.Aud; using Ryujinx.HLE.HOS.Services.Bsd; using Ryujinx.HLE.HOS.Services.Caps; +using Ryujinx.HLE.HOS.Services.Es; using Ryujinx.HLE.HOS.Services.FspSrv; using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Irs; using Ryujinx.HLE.HOS.Services.Ldr; using Ryujinx.HLE.HOS.Services.Lm; using Ryujinx.HLE.HOS.Services.Mm; +using Ryujinx.HLE.HOS.Services.Ncm; using Ryujinx.HLE.HOS.Services.Nfp; using Ryujinx.HLE.HOS.Services.Ns; using Ryujinx.HLE.HOS.Services.Nv; @@ -87,6 +89,9 @@ namespace Ryujinx.HLE.HOS.Services case "csrng": return new IRandomInterface(); + case "es": + return new IETicketService(); + case "friend:a": return new Friend.IServiceCreator(); @@ -111,12 +116,18 @@ namespace Ryujinx.HLE.HOS.Services case "mm:u": return new IRequest(); + case "ncm": + return new IContentManager(); + case "nfp:user": return new IUserManager(); case "nifm:u": return new Nifm.IStaticService(); + case "ns:am": + return new IApplicationManagerInterface(); + case "ns:ec": return new IServiceGetterInterface(); diff --git a/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs b/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs index 3833ce9ec9..aa96a4163f 100644 --- a/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs +++ b/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs @@ -46,6 +46,8 @@ namespace Ryujinx.HLE.HOS.SystemState public ColorSet ThemeColor { get; set; } + public bool InstallContents { get; set; } + private ConcurrentDictionary Profiles; internal UserProfile LastOpenUser { get; private set; } diff --git a/Ryujinx.HLE/Utilities/FontUtils.cs b/Ryujinx.HLE/Utilities/FontUtils.cs new file mode 100644 index 0000000000..efe7560c68 --- /dev/null +++ b/Ryujinx.HLE/Utilities/FontUtils.cs @@ -0,0 +1,37 @@ +using System.IO; + +namespace Ryujinx.HLE.Utilities +{ + public static class FontUtils + { + private static readonly uint FontKey = 0x06186249; + + public static byte[] DecryptFont(Stream BFTTFStream) + { + uint KXor(uint In) => In ^ 0x06186249; + + using (BinaryReader Reader = new BinaryReader(BFTTFStream)) + { + using (MemoryStream TTFStream = new MemoryStream()) + { + using (BinaryWriter Output = new BinaryWriter(TTFStream)) + { + if (KXor(Reader.ReadUInt32()) != 0x18029a7f) + { + throw new InvalidDataException("Error: Input file is not in BFTTF format!"); + } + + BFTTFStream.Position += 4; + + for (int i = 0; i < (BFTTFStream.Length - 8) / 4; i++) + { + Output.Write(KXor(Reader.ReadUInt32())); + } + + return TTFStream.ToArray(); + } + } + } + } + } +} diff --git a/Ryujinx.HLE/Utilities/StringUtils.cs b/Ryujinx.HLE/Utilities/StringUtils.cs index e8d6550ad1..a10273eef7 100644 --- a/Ryujinx.HLE/Utilities/StringUtils.cs +++ b/Ryujinx.HLE/Utilities/StringUtils.cs @@ -1,5 +1,7 @@ -using System; +using Ryujinx.HLE.HOS; +using System; using System.Globalization; +using System.IO; using System.Linq; using System.Text; @@ -47,5 +49,28 @@ namespace Ryujinx.HLE.Utilities return Output; } + + public static string ReadUtf8String(ServiceCtx Context, int Index = 0) + { + long Position = Context.Request.PtrBuff[Index].Position; + long Size = Context.Request.PtrBuff[Index].Size; + + using (MemoryStream MS = new MemoryStream()) + { + while (Size-- > 0) + { + byte Value = Context.Memory.ReadByte(Position++); + + if (Value == 0) + { + break; + } + + MS.WriteByte(Value); + } + + return Encoding.UTF8.GetString(MS.ToArray()); + } + } } }