diff --git a/build/CodeGen/result_modules.csv b/build/CodeGen/result_modules.csv index 3edc8e78..ac03ac76 100644 --- a/build/CodeGen/result_modules.csv +++ b/build/CodeGen/result_modules.csv @@ -1,6 +1,9 @@ Name,Index Fs,2 Loader,9 +Sf,10 Kvdb,20 +Sm,21 Sdmmc,24 +Bcat,122 LibHac,428 \ No newline at end of file diff --git a/build/CodeGen/result_paths.csv b/build/CodeGen/result_paths.csv index d29d8f20..4004fe98 100644 --- a/build/CodeGen/result_paths.csv +++ b/build/CodeGen/result_paths.csv @@ -1,6 +1,9 @@ Name,Namespace,Path Fs,LibHac.Fs,LibHac/Fs/ResultFs.cs Loader,LibHac.Loader,LibHac/Loader/ResultLoader.cs +Sf,LibHac.Sf,LibHac/Sf/ResultSf.cs Kvdb,LibHac.Kvdb,LibHac/Kvdb/ResultKvdb.cs +Sm,LibHac.Sm,LibHac/Sm/ResultSm.cs Sdmmc,LibHac.FsService,LibHac/FsService/ResultSdmmc.cs +Bcat,LibHac.Bcat,LibHac/Bcat/ResultBcat.cs LibHac,LibHac.Common,LibHac/Common/ResultLibHac.cs \ No newline at end of file diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv index 10e9eeb6..110e62c5 100644 --- a/build/CodeGen/results.csv +++ b/build/CodeGen/results.csv @@ -268,15 +268,65 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 9,200,,InternalError, +10,1,,NotSupported, +10,3,,PreconditionViolation, + +# These should be in the sf::cmif namespace +10,202,,InvalidHeaderSize, +10,211,,InvalidInHeader, +10,221,,UnknownCommandId, +10,232,,InvalidOutRawSize, +10,235,,InvalidNumInObjects, +10,236,,InvalidNumOutObjects, +10,239,,InvalidInObject, +10,261,,TargetNotFound, +10,301,,OutOfDomainEntries, + +# These should be in the sf::impl namespace +10,800,899,RequestContextChanged, +10,801,809,RequestInvalidated, +10,802,,RequestInvalidatedByUser, + +10,811,819,RequestDeferred, +10,812,,RequestDeferredByUser, + 20,1,,TooLargeKeyOrDbFull, 20,2,,KeyNotFound, 20,4,,AllocationFailed, 20,5,,InvalidKeyValue, 20,6,,BufferInsufficient, +21,1,,OutOfProcesses, +21,2,,InvalidClient, +21,3,,OutOfSessions, +21,4,,AlreadyRegistered, +21,5,,OutOfServices, +21,6,,InvalidServiceName, +21,7,,NotRegistered, +21,8,,NotAllowed, +21,9,,TooLargeAccessControl, + 24,1,,DeviceNotFound, 24,4,,DeviceAsleep, +122,1,,InvalidArgument, +122,2,,NotFound, +122,3,,TargetLocked, +122,6,,AlreadyOpen, +122,7,,NotOpen, +122,9,,ServiceOpenLimitReached, +122,10,,SaveDataNotFound, + +122,31,,NetworkServiceAccountNotAvailable, + +122,80,,PassphrasePathNotFound, + +122,90,,PermissionDenied, +122,91,,AllocationFailed, + +122,204,,InvalidDeliveryCacheStorageFile, +122,205,,StorageOpenLimitReached, + 123,0,4999,SslService, 124,0,,Cancelled, @@ -307,6 +357,8 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 428,3,,ArgumentOutOfRange, 428,4,,BufferTooSmall, +428,51,,ServiceNotInitialized, + 428,1000,1999,InvalidData, 428,1001,1019,InvalidKip, 428,1002,,InvalidKipFileSize,The size of the KIP file was smaller than expected. diff --git a/src/LibHac/ApplicationId.cs b/src/LibHac/ApplicationId.cs new file mode 100644 index 00000000..c00d623a --- /dev/null +++ b/src/LibHac/ApplicationId.cs @@ -0,0 +1,20 @@ +using System; + +namespace LibHac +{ + public readonly struct ApplicationId : IEquatable + { + public static ApplicationId InvalidId => default; + + public readonly ulong Value; + + public ApplicationId(ulong value) + { + Value = value; + } + + public override bool Equals(object obj) => obj is ApplicationId id && Equals(id); + public bool Equals(ApplicationId other) => Value == other.Value; + public override int GetHashCode() => HashCode.Combine(Value); + } +} diff --git a/src/LibHac/Arp/ApplicationLaunchProperty.cs b/src/LibHac/Arp/ApplicationLaunchProperty.cs new file mode 100644 index 00000000..57e5ef21 --- /dev/null +++ b/src/LibHac/Arp/ApplicationLaunchProperty.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace LibHac.Arp +{ + [StructLayout(LayoutKind.Explicit, Size = 0x10)] + public struct ApplicationLaunchProperty + { + [FieldOffset(0x0)] public ApplicationId ApplicationId; + [FieldOffset(0x8)] public uint Version; + [FieldOffset(0xC)] public Ncm.StorageId BaseStorageId; + [FieldOffset(0xD)] public Ncm.StorageId UpdateStorageId; + } +} diff --git a/src/LibHac/Arp/ArpClient.cs b/src/LibHac/Arp/ArpClient.cs new file mode 100644 index 00000000..71cc6684 --- /dev/null +++ b/src/LibHac/Arp/ArpClient.cs @@ -0,0 +1,67 @@ +using LibHac.Arp.Impl; +using LibHac.Ns; + +namespace LibHac.Arp +{ + public class ArpClient + { + private HorizonClient HosClient { get; } + private IReader Reader { get; set; } + + private readonly object _readerInitLocker = new object(); + + internal ArpClient(HorizonClient horizonClient) + { + HosClient = horizonClient; + } + + public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty launchProperty, ulong processId) + { + EnsureReaderInitialized(); + + return Reader.GetApplicationLaunchProperty(out launchProperty, processId); + } + + public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty launchProperty, ApplicationId applicationId) + { + EnsureReaderInitialized(); + + return Reader.GetApplicationLaunchPropertyWithApplicationId(out launchProperty, applicationId); + } + + public Result GetApplicationControlProperty(out ApplicationControlProperty controlProperty, ulong processId) + { + EnsureReaderInitialized(); + + return Reader.GetApplicationControlProperty(out controlProperty, processId); + } + + public Result GetApplicationControlProperty(out ApplicationControlProperty controlProperty, ApplicationId applicationId) + { + EnsureReaderInitialized(); + + return Reader.GetApplicationControlPropertyWithApplicationId(out controlProperty, applicationId); + } + + private void EnsureReaderInitialized() + { + if (Reader != null) + return; + + lock (_readerInitLocker) + { + if (Reader != null) + return; + + Result rc = HosClient.Sm.GetService(out IReader reader, "arp:r"); + + if (rc.IsFailure()) + { + throw new HorizonResultException(rc, "Failed to initialize arp reader."); + } + + Reader = reader; + } + } + } +} diff --git a/src/LibHac/Arp/Impl/IReader.cs b/src/LibHac/Arp/Impl/IReader.cs new file mode 100644 index 00000000..3a1ee9aa --- /dev/null +++ b/src/LibHac/Arp/Impl/IReader.cs @@ -0,0 +1,12 @@ +using LibHac.Ns; + +namespace LibHac.Arp.Impl +{ + public interface IReader + { + Result GetApplicationLaunchProperty(out ApplicationLaunchProperty launchProperty, ulong processId); + Result GetApplicationLaunchPropertyWithApplicationId(out ApplicationLaunchProperty launchProperty, ApplicationId applicationId); + Result GetApplicationControlProperty(out ApplicationControlProperty controlProperty, ulong processId); + Result GetApplicationControlPropertyWithApplicationId(out ApplicationControlProperty controlProperty, ApplicationId applicationId); + } +} diff --git a/src/LibHac/Bcat/BcatServer.cs b/src/LibHac/Bcat/BcatServer.cs new file mode 100644 index 00000000..1ddf0c96 --- /dev/null +++ b/src/LibHac/Bcat/BcatServer.cs @@ -0,0 +1,85 @@ +using System.Diagnostics; +using LibHac.Bcat.Detail.Ipc; +using LibHac.Bcat.Detail.Service; +using LibHac.Bcat.Detail.Service.Core; +using LibHac.Fs; + +namespace LibHac.Bcat +{ + public class BcatServer + { + private const int ServiceTypeCount = 4; + + internal HorizonClient Hos { get; } + private ServiceCreator[] ServiceCreators { get; } = new ServiceCreator[ServiceTypeCount]; + + private readonly object _bcatServiceInitLocker = new object(); + private readonly object _storageManagerInitLocker = new object(); + + private DeliveryCacheStorageManager StorageManager { get; set; } + + public BcatServer(HorizonClient horizonClient) + { + Hos = horizonClient; + + InitBcatService(BcatServiceType.BcatU, "bcat:u", AccessControl.MountOwnDeliveryCacheStorage); + InitBcatService(BcatServiceType.BcatS, "bcat:s", AccessControl.MountOthersDeliveryCacheStorage); + InitBcatService(BcatServiceType.BcatM, "bcat:m", AccessControl.MountOthersDeliveryCacheStorage | AccessControl.DeliveryTaskManagement); + InitBcatService(BcatServiceType.BcatA, "bcat:a", AccessControl.All); + } + + private void InitBcatService(BcatServiceType type, string name, AccessControl accessControl) + { + InitServiceCreator(type, name, accessControl); + + IServiceCreator service = GetServiceCreator(type); + + Result rc = Hos.Sm.RegisterService(service, name); + if (rc.IsFailure()) + { + throw new HorizonResultException(rc, "Abort"); + } + } + + private void InitServiceCreator(BcatServiceType type, string name, AccessControl accessControl) + { + lock (_bcatServiceInitLocker) + { + Debug.Assert((uint)type < ServiceTypeCount); + + ServiceCreators[(int)type] = new ServiceCreator(this, name, accessControl); + } + } + + private IServiceCreator GetServiceCreator(BcatServiceType type) + { + Debug.Assert((uint)type < ServiceTypeCount); + + return ServiceCreators[(int)type]; + } + + internal DeliveryCacheStorageManager GetStorageManager() + { + return StorageManager ?? InitStorageManager(); + } + + internal FileSystemClient GetFsClient() + { + return Hos.Fs; + } + + private DeliveryCacheStorageManager InitStorageManager() + { + lock (_storageManagerInitLocker) + { + if (StorageManager != null) + { + return StorageManager; + } + + StorageManager = new DeliveryCacheStorageManager(this); + return StorageManager; + } + } + } +} diff --git a/src/LibHac/Bcat/BcatServiceType.cs b/src/LibHac/Bcat/BcatServiceType.cs new file mode 100644 index 00000000..f2882b77 --- /dev/null +++ b/src/LibHac/Bcat/BcatServiceType.cs @@ -0,0 +1,10 @@ +namespace LibHac.Bcat +{ + public enum BcatServiceType + { + BcatU, + BcatS, + BcatM, + BcatA + } +} diff --git a/src/LibHac/Bcat/DeliveryCacheDirectoryEntry.cs b/src/LibHac/Bcat/DeliveryCacheDirectoryEntry.cs new file mode 100644 index 00000000..797027f3 --- /dev/null +++ b/src/LibHac/Bcat/DeliveryCacheDirectoryEntry.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace LibHac.Bcat +{ + [StructLayout(LayoutKind.Explicit, Size = 0x38)] + public struct DeliveryCacheDirectoryEntry + { + [FieldOffset(0x00)] public FileName Name; + [FieldOffset(0x20)] public long Size; + [FieldOffset(0x28)] public Digest Digest; + + public DeliveryCacheDirectoryEntry(ref FileName name, long size, ref Digest digest) + { + Name = name; + Size = size; + Digest = digest; + } + } +} diff --git a/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheDirectoryService.cs b/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheDirectoryService.cs new file mode 100644 index 00000000..0e064361 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheDirectoryService.cs @@ -0,0 +1,11 @@ +using System; + +namespace LibHac.Bcat.Detail.Ipc +{ + public interface IDeliveryCacheDirectoryService : IDisposable + { + Result Open(ref DirectoryName name); + Result Read(out int entriesRead, Span entryBuffer); + Result GetCount(out int count); + } +} diff --git a/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheFileService.cs b/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheFileService.cs new file mode 100644 index 00000000..0956f7d2 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheFileService.cs @@ -0,0 +1,12 @@ +using System; + +namespace LibHac.Bcat.Detail.Ipc +{ + public interface IDeliveryCacheFileService : IDisposable + { + Result Open(ref DirectoryName directoryName, ref FileName fileName); + Result Read(out long bytesRead, long offset, Span destination); + Result GetSize(out long size); + Result GetDigest(out Digest digest); + } +} diff --git a/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheStorageService.cs b/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheStorageService.cs new file mode 100644 index 00000000..4429c1af --- /dev/null +++ b/src/LibHac/Bcat/Detail/Ipc/IDeliveryCacheStorageService.cs @@ -0,0 +1,11 @@ +using System; + +namespace LibHac.Bcat.Detail.Ipc +{ + public interface IDeliveryCacheStorageService : IDisposable + { + Result CreateFileService(out IDeliveryCacheFileService fileService); + Result CreateDirectoryService(out IDeliveryCacheDirectoryService directoryService); + Result EnumerateDeliveryCacheDirectory(out int namesRead, Span nameBuffer); + } +} diff --git a/src/LibHac/Bcat/Detail/Ipc/IServiceCreator.cs b/src/LibHac/Bcat/Detail/Ipc/IServiceCreator.cs new file mode 100644 index 00000000..015d39ba --- /dev/null +++ b/src/LibHac/Bcat/Detail/Ipc/IServiceCreator.cs @@ -0,0 +1,13 @@ +using LibHac.Ncm; + +namespace LibHac.Bcat.Detail.Ipc +{ + public interface IServiceCreator + { + Result CreateDeliveryCacheStorageService(out IDeliveryCacheStorageService service, + ulong processId); + + Result CreateDeliveryCacheStorageServiceWithApplicationId(out IDeliveryCacheStorageService service, + ApplicationId applicationId); + } +} diff --git a/src/LibHac/Bcat/Detail/Service/AccessControl.cs b/src/LibHac/Bcat/Detail/Service/AccessControl.cs new file mode 100644 index 00000000..007f1194 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/AccessControl.cs @@ -0,0 +1,15 @@ +using System; + +namespace LibHac.Bcat.Detail.Service +{ + [Flags] + internal enum AccessControl + { + None = 0, + MountOwnDeliveryCacheStorage = 1 << 1, + MountOthersDeliveryCacheStorage = 1 << 2, + DeliveryTaskManagement = 1 << 3, + Debug = 1 << 4, + All = ~0 + } +} diff --git a/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheFileMetaAccessor.cs b/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheFileMetaAccessor.cs new file mode 100644 index 00000000..da3af499 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheFileMetaAccessor.cs @@ -0,0 +1,118 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.Bcat.Detail.Service.Core +{ + internal class DeliveryCacheFileMetaAccessor + { + private const int MaxEntryCount = 100; + private const int MetaFileHeaderValue = 1; + + private BcatServer Server { get; } + private object Locker { get; } = new object(); + private DeliveryCacheFileMetaEntry[] Entries { get; } = new DeliveryCacheFileMetaEntry[MaxEntryCount]; + public int Count { get; private set; } + + public DeliveryCacheFileMetaAccessor(BcatServer server) + { + Server = server; + } + + public Result ReadApplicationFileMeta(ulong applicationId, ref DirectoryName directoryName, + bool allowMissingMetaFile) + { + Span metaPath = stackalloc byte[0x50]; + Server.GetStorageManager().GetFilesMetaPath(metaPath, applicationId, ref directoryName); + + return Read(new U8Span(metaPath), allowMissingMetaFile); + } + + public Result GetEntry(out DeliveryCacheFileMetaEntry entry, int index) + { + lock (Locker) + { + if (index >= Count) + { + entry = default; + return ResultBcat.NotFound.Log(); + } + + entry = Entries[index]; + return Result.Success; + } + } + + public Result FindEntry(out DeliveryCacheFileMetaEntry entry, ref FileName fileName) + { + lock (Locker) + { + for (int i = 0; i < Count; i++) + { + if (StringUtils.CompareCaseInsensitive(Entries[i].Name.Bytes, fileName.Bytes) == 0) + { + entry = Entries[i]; + return Result.Success; + } + } + + entry = default; + return ResultBcat.NotFound.Log(); + } + } + + private Result Read(U8Span path, bool allowMissingMetaFile) + { + lock (Locker) + { + FileSystemClient fs = Server.GetFsClient(); + + Result rc = fs.OpenFile(out FileHandle handle, path, OpenMode.Read); + + if (rc.IsFailure()) + { + if (ResultFs.PathNotFound.Includes(rc)) + { + if (allowMissingMetaFile) + { + Count = 0; + return Result.Success; + } + + return ResultBcat.NotFound.LogConverted(rc); + } + + return rc; + } + + try + { + Count = 0; + int header = 0; + + // Verify the header value + rc = fs.ReadFile(out long bytesRead, handle, 0, SpanHelpers.AsByteSpan(ref header)); + if (rc.IsFailure()) return rc; + + if (bytesRead != sizeof(int) || header != MetaFileHeaderValue) + return ResultBcat.InvalidDeliveryCacheStorageFile.Log(); + + // Read all the file entries + Span buffer = MemoryMarshal.Cast(Entries); + rc = fs.ReadFile(out bytesRead, handle, 4, buffer); + if (rc.IsFailure()) return rc; + + Count = (int)((uint)bytesRead / Unsafe.SizeOf()); + + return Result.Success; + } + finally + { + fs.CloseFile(handle); + } + } + } + } +} diff --git a/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheFileMetaEntry.cs b/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheFileMetaEntry.cs new file mode 100644 index 00000000..53d2392f --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheFileMetaEntry.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace LibHac.Bcat.Detail.Service.Core +{ + [StructLayout(LayoutKind.Explicit, Size = 0x80)] + internal struct DeliveryCacheFileMetaEntry + { + [FieldOffset(0x00)] public FileName Name; + [FieldOffset(0x20)] public long Size; + [FieldOffset(0x30)] public Digest Digest; + } +} diff --git a/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheStorageManager.cs b/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheStorageManager.cs new file mode 100644 index 00000000..66b0ee13 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/Core/DeliveryCacheStorageManager.cs @@ -0,0 +1,398 @@ +using System; +using System.Diagnostics; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using LibHac.Ncm; +using static LibHac.Fs.StringTraits; + +namespace LibHac.Bcat.Detail.Service.Core +{ + internal class DeliveryCacheStorageManager + { + private const int MaxEntryCount = 4; + + private BcatServer Server { get; } + + private readonly object _locker = new object(); + private Entry[] Entries { get; } = new Entry[MaxEntryCount]; + private bool DisableStorage { get; set; } + + private struct Entry + { + public ulong ApplicationId { get; set; } + public long RefCount { get; set; } + } + + public DeliveryCacheStorageManager(BcatServer server) + { + Server = server; + DisableStorage = false; + } + + public Result Open(ulong applicationId) + { + lock (_locker) + { + // Find an existing storage entry for this application ID or get an empty one + Result rc = FindOrGetUnusedEntry(out int index, applicationId); + if (rc.IsFailure()) return rc; + + ref Entry entry = ref Entries[index]; + + if (entry.RefCount != 0) + { + return ResultBcat.TargetLocked.Log(); + } + + // Get the mount name + var mountName = new MountName(); + + var sb = new U8StringBuilder(mountName.Name); + sb.Append(DeliveryCacheMountNamePrefix) + .AppendFormat(index, 'd', 2); + + // Mount the save if enabled + if (!DisableStorage) + { + rc = Server.GetFsClient() + .MountBcatSaveData(new U8Span(mountName.Name), new TitleId(applicationId)); + + if (rc.IsFailure()) + { + if (ResultFs.TargetNotFound.Includes(rc)) + return ResultBcat.SaveDataNotFound.LogConverted(rc); + + return rc; + } + } + + // Update the storage entry + entry.ApplicationId = applicationId; + entry.RefCount++; + + return Result.Success; + } + } + + public void Release(ulong applicationId) + { + lock (_locker) + { + int index = FindEntry(applicationId); + ref Entry entry = ref Entries[index]; + + entry.RefCount--; + + // Free the entry if there are no more references + if (entry.RefCount == 0) + { + var mountName = new MountName(); + + var sb = new U8StringBuilder(mountName.Name); + sb.Append(DeliveryCacheMountNamePrefix) + .AppendFormat(index, 'd', 2); + + // Unmount the entry's savedata + if (!DisableStorage) + { + Server.GetFsClient().Unmount(new U8Span(mountName.Name)); + } + + // Clear the entry + entry.ApplicationId = 0; + + // todo: Call nn::bcat::detail::service::core::PassphraseManager::Remove + } + } + } + + public void Commit(ulong applicationId) + { + lock (_locker) + { + int index = FindEntry(applicationId); + + var mountName = new MountName(); + + var sb = new U8StringBuilder(mountName.Name); + sb.Append(DeliveryCacheMountNamePrefix) + .AppendFormat(index, 'd', 2); + + if (!DisableStorage) + { + Result rc = Server.GetFsClient().Commit(new U8Span(mountName.Name)); + + if (rc.IsFailure()) + { + throw new HorizonResultException(rc, "Abort"); + } + } + } + } + + public Result GetFreeSpaceSize(out long size, ulong applicationId) + { + lock (_locker) + { + Span path = stackalloc byte[0x20]; + + var sb = new U8StringBuilder(path); + AppendMountName(ref sb, applicationId); + sb.Append(RootPath); + + Result rc; + + if (DisableStorage) + { + size = 0x4400000; + rc = Result.Success; + } + else + { + rc = Server.GetFsClient().GetFreeSpaceSize(out size, new U8Span(path)); + } + + return rc; + } + } + + public void GetPassphrasePath(Span pathBuffer, ulong applicationId) + { + // returns "mount:/passphrase.bin" + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + sb.Append(PassphrasePath); + } + } + + public void GetDeliveryListPath(Span pathBuffer, ulong applicationId) + { + // returns "mount:/list.msgpack" + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + sb.Append(DeliveryListPath); + } + } + + public void GetEtagFilePath(Span pathBuffer, ulong applicationId) + { + // returns "mount:/etag.bin" + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + sb.Append(EtagPath); + } + } + + public void GetNaRequiredPath(Span pathBuffer, ulong applicationId) + { + // returns "mount:/na_required" + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + sb.Append(NaRequiredPath); + } + } + + public void GetIndexLockPath(Span pathBuffer, ulong applicationId) + { + // returns "mount:/index.lock" + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + sb.Append(IndexLockPath); + } + } + + public void GetFilePath(Span pathBuffer, ulong applicationId, ref DirectoryName directoryName, + ref FileName fileName) + { + // returns "mount:/directories/%s/files/%s", directoryName, fileName + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + + sb.Append(DirectoriesPath) + .Append(DirectorySeparator).Append(directoryName.Bytes) + .Append(DirectorySeparator).Append(FilesDirectoryName) + .Append(DirectorySeparator).Append(fileName.Bytes); + } + } + + public void GetFilesMetaPath(Span pathBuffer, ulong applicationId, ref DirectoryName directoryName) + { + // returns "mount:/directories/%s/files.meta", directoryName + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + + sb.Append(DirectoriesPath) + .Append(DirectorySeparator).Append(directoryName.Bytes) + .Append(DirectorySeparator).Append(FilesMetaFileName); + } + } + + public void GetDirectoriesPath(Span pathBuffer, ulong applicationId) + { + // returns "mount:/directories" + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + sb.Append(DirectoriesPath); + } + } + + public void GetDirectoryPath(Span pathBuffer, ulong applicationId, ref DirectoryName directoryName) + { + // returns "mount:/directories/%s", directoryName + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + + sb.Append(DirectoriesPath) + .Append(DirectorySeparator).Append(directoryName.Bytes); + } + } + + public void GetDirectoriesMetaPath(Span pathBuffer, ulong applicationId) + { + // returns "mount:/directories.meta" + lock (_locker) + { + var sb = new U8StringBuilder(pathBuffer); + AppendMountName(ref sb, applicationId); + + sb.Append(DirectoriesMetaPath); + } + } + + private void AppendMountName(ref U8StringBuilder sb, ulong applicationId) + { + int index = FindEntry(applicationId); + + sb.Append(DeliveryCacheMountNamePrefix) + .AppendFormat(index, 'd', 2); + } + + private Result FindOrGetUnusedEntry(out int entryIndex, ulong applicationId) + { + // Try to find an existing entry + for (int i = 0; i < Entries.Length; i++) + { + if (Entries[i].ApplicationId == applicationId) + { + entryIndex = i; + return Result.Success; + } + } + + // Try to find an unused entry + for (int i = 0; i < Entries.Length; i++) + { + if (Entries[i].ApplicationId == 0) + { + entryIndex = i; + return Result.Success; + } + } + + entryIndex = default; + return ResultBcat.StorageOpenLimitReached.Log(); + } + + private int FindEntry(ulong applicationId) + { + Entry[] entries = Entries; + + for (int i = 0; i < entries.Length; i++) + { + if (entries[i].ApplicationId == applicationId) + { + return i; + } + } + + // Nintendo uses 1 as the entry index if it wasn't found + Debug.Assert(false, "Entry not found."); + return 1; + } + + private static ReadOnlySpan DeliveryCacheMountNamePrefix => // bcat-dc- + new[] { (byte)'b', (byte)'c', (byte)'a', (byte)'t', (byte)'-', (byte)'d', (byte)'c', (byte)'-' }; + + private static ReadOnlySpan RootPath => // :/ + new[] { (byte)':', (byte)'/' }; + + private static ReadOnlySpan PassphrasePath => // :/passphrase.bin + new[] + { + (byte) ':', (byte) '/', (byte) 'p', (byte) 'a', (byte) 's', (byte) 's', (byte) 'p', (byte) 'h', + (byte) 'r', (byte) 'a', (byte) 's', (byte) 'e', (byte) '.', (byte) 'b', (byte) 'i', (byte) 'n' + }; + + private static ReadOnlySpan DeliveryListPath => // :/list.msgpack + new[] + { + (byte) ':', (byte) '/', (byte) 'l', (byte) 'i', (byte) 's', (byte) 't', (byte) '.', (byte) 'm', + (byte) 's', (byte) 'g', (byte) 'p', (byte) 'a', (byte) 'c', (byte) 'k' + }; + + private static ReadOnlySpan EtagPath => // :/etag.bin + new[] + { + (byte) ':', (byte) '/', (byte) 'e', (byte) 't', (byte) 'a', (byte) 'g', (byte) '.', (byte) 'b', + (byte) 'i', (byte) 'n' + }; + + private static ReadOnlySpan NaRequiredPath => // :/na_required + new[] + { + (byte) ':', (byte) '/', (byte) 'n', (byte) 'a', (byte) '_', (byte) 'r', (byte) 'e', (byte) 'q', + (byte) 'u', (byte) 'i', (byte) 'r', (byte) 'e', (byte) 'd' + }; + + private static ReadOnlySpan IndexLockPath => // :/index.lock + new[] + { + (byte) ':', (byte) '/', (byte) 'i', (byte) 'n', (byte) 'd', (byte) 'e', (byte) 'x', (byte) '.', + (byte) 'l', (byte) 'o', (byte) 'c', (byte) 'k' + }; + + private static ReadOnlySpan DirectoriesPath => // :/directories + new[] + { + (byte) ':', (byte) '/', (byte) 'd', (byte) 'i', (byte) 'r', (byte) 'e', (byte) 'c', (byte) 't', + (byte) 'o', (byte) 'r', (byte) 'i', (byte) 'e', (byte) 's' + }; + + private static ReadOnlySpan FilesMetaFileName => // files.meta + new[] + { + (byte) 'f', (byte) 'i', (byte) 'l', (byte) 'e', (byte) 's', (byte) '.', (byte) 'm', (byte) 'e', + (byte) 't', (byte) 'a' + }; + + private static ReadOnlySpan DirectoriesMetaPath => // :/directories.meta + new[] + { + (byte) ':', (byte) '/', (byte) 'd', (byte) 'i', (byte) 'r', (byte) 'e', (byte) 'c', (byte) 't', + (byte) 'o', (byte) 'r', (byte) 'i', (byte) 'e', (byte) 's', (byte) '.', (byte) 'm', (byte) 'e', + (byte) 't', (byte) 'a' + }; + + private static ReadOnlySpan FilesDirectoryName => // files + new[] { (byte)'f', (byte)'i', (byte)'l', (byte)'e', (byte)'s' }; + } +} diff --git a/src/LibHac/Bcat/Detail/Service/DeliveryCacheDirectoryService.cs b/src/LibHac/Bcat/Detail/Service/DeliveryCacheDirectoryService.cs new file mode 100644 index 00000000..17416413 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/DeliveryCacheDirectoryService.cs @@ -0,0 +1,105 @@ +using System; +using LibHac.Bcat.Detail.Ipc; +using LibHac.Bcat.Detail.Service.Core; + +namespace LibHac.Bcat.Detail.Service +{ + internal class DeliveryCacheDirectoryService : IDeliveryCacheDirectoryService + { + private BcatServer Server { get; } + private object Locker { get; } = new object(); + private DeliveryCacheStorageService Parent { get; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private AccessControl Access { get; } + private ulong ApplicationId { get; } + private DirectoryName _name; + private bool IsDirectoryOpen { get; set; } + private int Count { get; set; } + + public DeliveryCacheDirectoryService(BcatServer server, DeliveryCacheStorageService parent, ulong applicationId, + AccessControl accessControl) + { + Server = server; + Parent = parent; + ApplicationId = applicationId; + Access = accessControl; + } + + public Result Open(ref DirectoryName name) + { + if (!name.IsValid()) + return ResultBcat.InvalidArgument.Log(); + + lock (Locker) + { + if (IsDirectoryOpen) + return ResultBcat.AlreadyOpen.Log(); + + var metaReader = new DeliveryCacheFileMetaAccessor(Server); + Result rc = metaReader.ReadApplicationFileMeta(ApplicationId, ref name, false); + if (rc.IsFailure()) return rc; + + Count = metaReader.Count; + _name = name; + IsDirectoryOpen = true; + + return Result.Success; + } + } + + public Result Read(out int entriesRead, Span entryBuffer) + { + lock (Locker) + { + entriesRead = default; + + if (!IsDirectoryOpen) + return ResultBcat.NotOpen.Log(); + + var metaReader = new DeliveryCacheFileMetaAccessor(Server); + Result rc = metaReader.ReadApplicationFileMeta(ApplicationId, ref _name, true); + if (rc.IsFailure()) return rc; + + int i; + for (i = 0; i < entryBuffer.Length; i++) + { + rc = metaReader.GetEntry(out DeliveryCacheFileMetaEntry entry, i); + + if (rc.IsFailure()) + { + if (!ResultBcat.NotFound.Includes(rc)) + return rc; + + break; + } + + entryBuffer[i] = new DeliveryCacheDirectoryEntry(ref entry.Name, entry.Size, ref entry.Digest); + } + + entriesRead = i; + return Result.Success; + } + } + + public Result GetCount(out int count) + { + lock (Locker) + { + if (!IsDirectoryOpen) + { + count = default; + return ResultBcat.NotOpen.Log(); + } + + count = Count; + return Result.Success; + } + } + + public void Dispose() + { + Parent.NotifyCloseDirectory(); + } + } +} diff --git a/src/LibHac/Bcat/Detail/Service/DeliveryCacheFileService.cs b/src/LibHac/Bcat/Detail/Service/DeliveryCacheFileService.cs new file mode 100644 index 00000000..a4f65911 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/DeliveryCacheFileService.cs @@ -0,0 +1,120 @@ +using System; +using LibHac.Bcat.Detail.Ipc; +using LibHac.Bcat.Detail.Service.Core; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.Bcat.Detail.Service +{ + internal class DeliveryCacheFileService : IDeliveryCacheFileService + { + private BcatServer Server { get; } + private object Locker { get; } = new object(); + private DeliveryCacheStorageService Parent { get; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private AccessControl Access { get; } + private ulong ApplicationId { get; } + private FileHandle _handle; + private DeliveryCacheFileMetaEntry _metaEntry; + private bool IsFileOpen { get; set; } + + public DeliveryCacheFileService(BcatServer server, DeliveryCacheStorageService parent, ulong applicationId, + AccessControl accessControl) + { + Server = server; + Parent = parent; + ApplicationId = applicationId; + Access = accessControl; + } + + public Result Open(ref DirectoryName directoryName, ref FileName fileName) + { + if (!directoryName.IsValid()) + return ResultBcat.InvalidArgument.Log(); + + if (!fileName.IsValid()) + return ResultBcat.InvalidArgument.Log(); + + lock (Locker) + { + if (IsFileOpen) + return ResultBcat.AlreadyOpen.Log(); + + var metaReader = new DeliveryCacheFileMetaAccessor(Server); + Result rc = metaReader.ReadApplicationFileMeta(ApplicationId, ref directoryName, true); + if (rc.IsFailure()) return rc; + + rc = metaReader.FindEntry(out DeliveryCacheFileMetaEntry entry, ref fileName); + if (rc.IsFailure()) return rc; + + Span filePath = stackalloc byte[0x80]; + Server.GetStorageManager().GetFilePath(filePath, ApplicationId, ref directoryName, ref fileName); + + rc = Server.GetFsClient().OpenFile(out _handle, new U8Span(filePath), OpenMode.Read); + if (rc.IsFailure()) return rc; + + _metaEntry = entry; + IsFileOpen = true; + + return Result.Success; + } + } + + public Result Read(out long bytesRead, long offset, Span destination) + { + lock (Locker) + { + bytesRead = 0; + + if (!IsFileOpen) + return ResultBcat.NotOpen.Log(); + + Result rc = Server.GetFsClient().ReadFile(out long read, _handle, offset, destination); + if (rc.IsFailure()) return rc; + + bytesRead = read; + return Result.Success; + } + } + + public Result GetSize(out long size) + { + lock (Locker) + { + if (!IsFileOpen) + { + size = default; + return ResultBcat.NotOpen.Log(); + } + + return Server.GetFsClient().GetFileSize(out size, _handle); + } + } + + public Result GetDigest(out Digest digest) + { + lock (Locker) + { + if (!IsFileOpen) + { + digest = default; + return ResultBcat.NotOpen.Log(); + } + + digest = _metaEntry.Digest; + return Result.Success; + } + } + + public void Dispose() + { + if (IsFileOpen) + { + Server.GetFsClient().CloseFile(_handle); + } + + Parent.NotifyCloseFile(); + } + } +} diff --git a/src/LibHac/Bcat/Detail/Service/DeliveryCacheStorageService.cs b/src/LibHac/Bcat/Detail/Service/DeliveryCacheStorageService.cs new file mode 100644 index 00000000..2ee0d976 --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/DeliveryCacheStorageService.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using LibHac.Bcat.Detail.Ipc; + +namespace LibHac.Bcat.Detail.Service +{ + internal class DeliveryCacheStorageService : IDeliveryCacheStorageService + { + private const int MaxOpenCount = 8; + private BcatServer Server { get; } + + private object Locker { get; } = new object(); + private AccessControl Access { get; } + private ulong ApplicationId { get; } + private int FileServiceOpenCount { get; set; } + private int DirectoryServiceOpenCount { get; set; } + + public DeliveryCacheStorageService(BcatServer server, ulong applicationId, AccessControl accessControl) + { + Server = server; + ApplicationId = applicationId; + Access = accessControl; + } + + public Result CreateFileService(out IDeliveryCacheFileService service) + { + lock (Locker) + { + service = default; + + if (FileServiceOpenCount >= MaxOpenCount) + return ResultBcat.ServiceOpenLimitReached.Log(); + + service = new DeliveryCacheFileService(Server, this, ApplicationId, Access); + + FileServiceOpenCount++; + return Result.Success; + } + } + + public Result CreateDirectoryService(out IDeliveryCacheDirectoryService service) + { + lock (Locker) + { + service = default; + + if (DirectoryServiceOpenCount >= MaxOpenCount) + return ResultBcat.ServiceOpenLimitReached.Log(); + + service = new DeliveryCacheDirectoryService(Server, this, ApplicationId, Access); + + DirectoryServiceOpenCount++; + return Result.Success; + } + } + + public Result EnumerateDeliveryCacheDirectory(out int namesRead, Span nameBuffer) + { + throw new NotImplementedException(); + } + + internal void NotifyCloseFile() + { + lock (Locker) + { + FileServiceOpenCount--; + + Debug.Assert(FileServiceOpenCount >= 0); + } + } + + internal void NotifyCloseDirectory() + { + lock (Locker) + { + DirectoryServiceOpenCount--; + + Debug.Assert(DirectoryServiceOpenCount >= 0); + } + } + + public void Dispose() + { + Server.GetStorageManager().Release(ApplicationId); + } + } +} diff --git a/src/LibHac/Bcat/Detail/Service/ServiceCreator.cs b/src/LibHac/Bcat/Detail/Service/ServiceCreator.cs new file mode 100644 index 00000000..423e08ad --- /dev/null +++ b/src/LibHac/Bcat/Detail/Service/ServiceCreator.cs @@ -0,0 +1,62 @@ +using LibHac.Arp; +using LibHac.Bcat.Detail.Ipc; + +namespace LibHac.Bcat.Detail.Service +{ + internal class ServiceCreator : IServiceCreator + { + private BcatServer Server { get; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private string ServiceName { get; } + private AccessControl AccessControl { get; } + + public ServiceCreator(BcatServer server, string serviceName, AccessControl accessControl) + { + Server = server; + ServiceName = serviceName; + AccessControl = accessControl; + } + + public Result CreateDeliveryCacheStorageService(out IDeliveryCacheStorageService service, ulong processId) + { + Result rc = Server.Hos.Arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty launchProperty, + processId); + + if (rc.IsFailure()) + { + service = default; + return ResultBcat.NotFound.LogConverted(rc); + } + + return CreateDeliveryCacheStorageServiceImpl(out service, launchProperty.ApplicationId); + } + + public Result CreateDeliveryCacheStorageServiceWithApplicationId(out IDeliveryCacheStorageService service, + ApplicationId applicationId) + { + if (!AccessControl.HasFlag(AccessControl.MountOthersDeliveryCacheStorage)) + { + service = default; + return ResultBcat.PermissionDenied.Log(); + } + + return CreateDeliveryCacheStorageServiceImpl(out service, applicationId); + } + + private Result CreateDeliveryCacheStorageServiceImpl(out IDeliveryCacheStorageService service, + ApplicationId applicationId) + { + service = default; + + Result rc = Server.GetStorageManager().Open(applicationId.Value); + if (rc.IsFailure()) return rc; + + // todo: Check if network account required + + service = new DeliveryCacheStorageService(Server, applicationId.Value, AccessControl); + + return Result.Success; + } + } +} diff --git a/src/LibHac/Bcat/Digest.cs b/src/LibHac/Bcat/Digest.cs new file mode 100644 index 00000000..cec171c9 --- /dev/null +++ b/src/LibHac/Bcat/Digest.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Bcat +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = 16)] + public struct Digest + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy0; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy1; + + public byte this[int i] + { + get => Bytes[i]; + set => Bytes[i] = value; + } + + public Span Bytes => SpanHelpers.AsByteSpan(ref this); + + public override string ToString() + { + return Bytes.ToHexString(); + } + } +} diff --git a/src/LibHac/Bcat/DirectoryName.cs b/src/LibHac/Bcat/DirectoryName.cs new file mode 100644 index 00000000..009f276e --- /dev/null +++ b/src/LibHac/Bcat/DirectoryName.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Bcat +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = MaxSize)] + public struct DirectoryName + { + private const int MaxSize = 0x20; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy0; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy1; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy2; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy3; + + public byte this[int i] + { + get => Bytes[i]; + set => Bytes[i] = value; + } + + public Span Bytes => SpanHelpers.AsByteSpan(ref this); + + public bool IsValid() + { + Span name = Bytes; + + int i; + for (i = 0; i < name.Length; i++) + { + if (name[i] == 0) + break; + + if (!StringUtils.IsDigit(name[i]) && !StringUtils.IsAlpha(name[i]) && name[i] != '_' && name[i] != '-') + return false; + } + + if (i == 0 || i == MaxSize) + return false; + + return name[i] == 0; + } + + public override string ToString() + { + return StringUtils.Utf8ZToString(Bytes); + } + } +} diff --git a/src/LibHac/Bcat/FileName.cs b/src/LibHac/Bcat/FileName.cs new file mode 100644 index 00000000..6911a7eb --- /dev/null +++ b/src/LibHac/Bcat/FileName.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Bcat +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = MaxSize)] + public struct FileName + { + private const int MaxSize = 0x20; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy0; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy1; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy2; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy3; + + public byte this[int i] + { + get => Bytes[i]; + set => Bytes[i] = value; + } + + public Span Bytes => SpanHelpers.AsByteSpan(ref this); + + public bool IsValid() + { + Span name = Bytes; + + int i; + for (i = 0; i < name.Length; i++) + { + if (name[i] == 0) + break; + + if (!StringUtils.IsDigit(name[i]) && !StringUtils.IsAlpha(name[i]) && name[i] != '_' && name[i] != '.') + return false; + } + + if (i == 0 || i == MaxSize) + return false; + + if (name[i] != 0) + return false; + + return name[i - 1] != '.'; + } + + public override string ToString() + { + return StringUtils.Utf8ZToString(Bytes); + } + } +} \ No newline at end of file diff --git a/src/LibHac/Bcat/ResultBcat.cs b/src/LibHac/Bcat/ResultBcat.cs new file mode 100644 index 00000000..9a5fd6b1 --- /dev/null +++ b/src/LibHac/Bcat/ResultBcat.cs @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------------- +// This file was automatically generated. +// Changes to this file will be lost when the file is regenerated. +// +// To change this file, modify /build/CodeGen/results.csv at the root of this +// repo and run the build script. +// +// The script can be run with the "codegen" option to run only the +// code generation portion of the build. +//----------------------------------------------------------------------------- + +namespace LibHac.Bcat +{ + public static class ResultBcat + { + public const int ModuleBcat = 122; + + /// Error code: 2122-0001; Inner value: 0x27a + public static Result.Base InvalidArgument => new Result.Base(ModuleBcat, 1); + /// Error code: 2122-0002; Inner value: 0x47a + public static Result.Base NotFound => new Result.Base(ModuleBcat, 2); + /// Error code: 2122-0003; Inner value: 0x67a + public static Result.Base TargetLocked => new Result.Base(ModuleBcat, 3); + /// Error code: 2122-0006; Inner value: 0xc7a + public static Result.Base AlreadyOpen => new Result.Base(ModuleBcat, 6); + /// Error code: 2122-0007; Inner value: 0xe7a + public static Result.Base NotOpen => new Result.Base(ModuleBcat, 7); + /// Error code: 2122-0009; Inner value: 0x127a + public static Result.Base ServiceOpenLimitReached => new Result.Base(ModuleBcat, 9); + /// Error code: 2122-0010; Inner value: 0x147a + public static Result.Base SaveDataNotFound => new Result.Base(ModuleBcat, 10); + /// Error code: 2122-0031; Inner value: 0x3e7a + public static Result.Base NetworkServiceAccountNotAvailable => new Result.Base(ModuleBcat, 31); + /// Error code: 2122-0080; Inner value: 0xa07a + public static Result.Base PassphrasePathNotFound => new Result.Base(ModuleBcat, 80); + /// Error code: 2122-0090; Inner value: 0xb47a + public static Result.Base PermissionDenied => new Result.Base(ModuleBcat, 90); + /// Error code: 2122-0091; Inner value: 0xb67a + public static Result.Base AllocationFailed => new Result.Base(ModuleBcat, 91); + /// Error code: 2122-0204; Inner value: 0x1987a + public static Result.Base InvalidDeliveryCacheStorageFile => new Result.Base(ModuleBcat, 204); + /// Error code: 2122-0205; Inner value: 0x19a7a + public static Result.Base StorageOpenLimitReached => new Result.Base(ModuleBcat, 205); + } +} diff --git a/src/LibHac/Common/ResultLibHac.cs b/src/LibHac/Common/ResultLibHac.cs index ddb27727..4f0f1e82 100644 --- a/src/LibHac/Common/ResultLibHac.cs +++ b/src/LibHac/Common/ResultLibHac.cs @@ -26,6 +26,9 @@ namespace LibHac.Common /// Error code: 2428-0004; Inner value: 0x9ac public static Result.Base BufferTooSmall => new Result.Base(ModuleLibHac, 4); + /// Error code: 2428-0051; Inner value: 0x67ac + public static Result.Base ServiceNotInitialized => new Result.Base(ModuleLibHac, 51); + /// Error code: 2428-1000; Range: 1000-1999; Inner value: 0x7d1ac public static Result.Base InvalidData { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleLibHac, 1000, 1999); } /// Error code: 2428-1001; Range: 1001-1019; Inner value: 0x7d3ac diff --git a/src/LibHac/Common/StringUtils.cs b/src/LibHac/Common/StringUtils.cs index bfd8e8fe..4f548869 100644 --- a/src/LibHac/Common/StringUtils.cs +++ b/src/LibHac/Common/StringUtils.cs @@ -168,5 +168,15 @@ namespace LibHac.Common { return Utf8ToString(value.Slice(0, GetLength(value))); } + + public static bool IsAlpha(byte c) + { + return (c | 0x20u) - (byte)'a' <= 'z' - 'a'; + } + + public static bool IsDigit(byte c) + { + return (uint)(c - (byte)'0') <= 9; + } } } diff --git a/src/LibHac/Common/U8StringBuilder.cs b/src/LibHac/Common/U8StringBuilder.cs index e4ae004d..088e46bc 100644 --- a/src/LibHac/Common/U8StringBuilder.cs +++ b/src/LibHac/Common/U8StringBuilder.cs @@ -1,5 +1,8 @@ using System; +using System.Buffers; +using System.Buffers.Text; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace LibHac.Common { @@ -8,18 +11,20 @@ namespace LibHac.Common { private const int NullTerminatorLength = 1; - private readonly Span _buffer; - private int _length; - + public Span Buffer { get; } + public int Length { get; private set; } public bool Overflowed { get; private set; } - public readonly int Length => _length; - public readonly int Capacity => _buffer.Length - NullTerminatorLength; - public readonly Span Buffer => _buffer; + + public readonly int Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Buffer.Length - NullTerminatorLength; + } public U8StringBuilder(Span buffer) { - _buffer = buffer; - _length = 0; + Buffer = buffer; + Length = 0; Overflowed = false; ThrowIfBufferLengthIsZero(); @@ -27,62 +32,287 @@ namespace LibHac.Common AddNullTerminator(); } - public U8StringBuilder Append(ReadOnlySpan value) + // These functions are internal so they can be called by the extension methods + // in U8StringBuilderExtensions. It's not an ideal setup, but it allows append + // calls to be chained without accidentally creating a copy of the U8StringBuilder. + internal void AppendInternal(ReadOnlySpan value) { - if (Overflowed) return this; + if (Overflowed) return; int valueLength = StringUtils.GetLength(value); if (!HasAdditionalCapacity(valueLength)) { Overflowed = true; - return this; + return; } - value.Slice(0, valueLength).CopyTo(_buffer.Slice(_length)); - _length += valueLength; + value.Slice(0, valueLength).CopyTo(Buffer.Slice(Length)); + Length += valueLength; AddNullTerminator(); - - return this; } - public U8StringBuilder Append(byte value) + internal void AppendInternal(byte value) { - if (Overflowed) return this; + if (Overflowed) return; if (!HasAdditionalCapacity(1)) { Overflowed = true; - return this; + return; } - _buffer[_length] = value; - _length++; + Buffer[Length] = value; + Length++; AddNullTerminator(); - - return this; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(byte value, char format = 'G', byte precision = 255) => + AppendFormatUInt64(value, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(sbyte value, char format = 'G', byte precision = 255) => + AppendFormatInt64(value, 0xff, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(ushort value, char format = 'G', byte precision = 255) => + AppendFormatUInt64(value, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(short value, char format = 'G', byte precision = 255) => + AppendFormatInt64(value, 0xffff, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(uint value, char format = 'G', byte precision = 255) => + AppendFormatUInt64(value, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(int value, char format = 'G', byte precision = 255) => + AppendFormatInt64(value, 0xffffff, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(ulong value, char format = 'G', byte precision = 255) => + AppendFormatUInt64(value, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(long value, char format = 'G', byte precision = 255) => + AppendFormatInt64(value, 0xffffffff, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(float value, char format = 'G', byte precision = 255) => + AppendFormatFloat(value, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendFormatInternal(double value, char format = 'G', byte precision = 255) => + AppendFormatDouble(value, format, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly bool HasCapacity(int requiredCapacity) { return requiredCapacity <= Capacity; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly bool HasAdditionalCapacity(int requiredAdditionalCapacity) { - return HasCapacity(_length + requiredAdditionalCapacity); + return HasCapacity(Length + requiredAdditionalCapacity); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddNullTerminator() { - _buffer[_length] = 0; + Buffer[Length] = 0; } private readonly void ThrowIfBufferLengthIsZero() { - if (_buffer.Length == 0) throw new ArgumentException("Buffer length must be greater than 0."); + if (Buffer.Length == 0) throw new ArgumentException("Buffer length must be greater than 0."); } - public override readonly string ToString() => StringUtils.Utf8ZToString(_buffer); + private void AppendFormatInt64(long value, ulong mask, char format, byte precision) + { + if (Overflowed) return; + + // Remove possible sign extension if needed + if (mask == 'x' | mask == 'X') + { + value &= (long)mask; + } + + // Exclude the null terminator from the buffer because Utf8Formatter doesn't handle it + Span availableBuffer = Buffer.Slice(Length, Capacity - Length); + + bool bufferLargeEnough = Utf8Formatter.TryFormat(value, availableBuffer, out int bytesWritten, + new StandardFormat(format, precision)); + + if (!bufferLargeEnough) + { + Overflowed = true; + return; + } + + Length += bytesWritten; + AddNullTerminator(); + } + + private void AppendFormatUInt64(ulong value, char format, byte precision) + { + if (Overflowed) return; + + // Exclude the null terminator from the buffer because Utf8Formatter doesn't handle it + Span availableBuffer = Buffer.Slice(Length, Capacity - Length); + + bool bufferLargeEnough = Utf8Formatter.TryFormat(value, availableBuffer, out int bytesWritten, + new StandardFormat(format, precision)); + + if (!bufferLargeEnough) + { + Overflowed = true; + return; + } + + Length += bytesWritten; + AddNullTerminator(); + } + + private void AppendFormatFloat(float value, char format, byte precision) + { + if (Overflowed) return; + + // Exclude the null terminator from the buffer because Utf8Formatter doesn't handle it + Span availableBuffer = Buffer.Slice(Length, Capacity - Length); + + bool bufferLargeEnough = Utf8Formatter.TryFormat(value, availableBuffer, out int bytesWritten, + new StandardFormat(format, precision)); + + if (!bufferLargeEnough) + { + Overflowed = true; + return; + } + + Length += bytesWritten; + AddNullTerminator(); + } + + private void AppendFormatDouble(double value, char format, byte precision) + { + if (Overflowed) return; + + // Exclude the null terminator from the buffer because Utf8Formatter doesn't handle it + Span availableBuffer = Buffer.Slice(Length, Capacity - Length); + + bool bufferLargeEnough = Utf8Formatter.TryFormat(value, availableBuffer, out int bytesWritten, + new StandardFormat(format, precision)); + + if (!bufferLargeEnough) + { + Overflowed = true; + return; + } + + Length += bytesWritten; + AddNullTerminator(); + } + + public override readonly string ToString() => StringUtils.Utf8ZToString(Buffer); + } + + public static class U8StringBuilderExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder Append(this ref U8StringBuilder sb, ReadOnlySpan value) + { + sb.AppendInternal(value); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder Append(this ref U8StringBuilder sb, byte value) + { + sb.AppendInternal(value); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, byte value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, sbyte value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, ushort value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, short value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, uint value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, int value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, ulong value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, long value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, float value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref U8StringBuilder AppendFormat(this ref U8StringBuilder sb, double value, char format = 'G', + byte precision = 255) + { + sb.AppendFormatInternal(value, format, precision); + return ref sb; + } } } diff --git a/src/LibHac/Fs/Shim/Bis.cs b/src/LibHac/Fs/Shim/Bis.cs index cd152d02..ba4f5ba5 100644 --- a/src/LibHac/Fs/Shim/Bis.cs +++ b/src/LibHac/Fs/Shim/Bis.cs @@ -28,9 +28,10 @@ namespace LibHac.Fs.Shim Debug.Assert(nameBuffer.Length >= requiredNameBufferSize); - // ReSharper disable once RedundantAssignment - int size = new U8StringBuilder(nameBuffer).Append(mountName).Append(StringTraits.DriveSeparator).Length; - Debug.Assert(size == requiredNameBufferSize - 1); + var sb = new U8StringBuilder(nameBuffer); + sb.Append(mountName).Append(StringTraits.DriveSeparator); + + Debug.Assert(sb.Length == requiredNameBufferSize - 1); return Result.Success; } @@ -144,7 +145,8 @@ namespace LibHac.Fs.Shim ? StringTraits.NullTerminator : StringTraits.DirectorySeparator; - Result rc = new U8StringBuilder(sfPath.Str).Append(rootPath).Append(endingSeparator).ToSfPath(); + var sb = new U8StringBuilder(sfPath.Str); + Result rc = sb.Append(rootPath).Append(endingSeparator).ToSfPath(); if (rc.IsFailure()) return rc; } else diff --git a/src/LibHac/Fs/Shim/Host.cs b/src/LibHac/Fs/Shim/Host.cs index 48786e31..e26fae22 100644 --- a/src/LibHac/Fs/Shim/Host.cs +++ b/src/LibHac/Fs/Shim/Host.cs @@ -43,9 +43,10 @@ namespace LibHac.Fs.Shim if (nameBuffer.Length < requiredNameBufferSize) return ResultFs.TooLongPath.Log(); - // ReSharper disable once RedundantAssignment - int size = new U8StringBuilder(nameBuffer).Append(HostRootFileSystemPath).Append(_path.Str).Length; - Debug.Assert(size == requiredNameBufferSize - 1); + var sb = new U8StringBuilder(nameBuffer); + sb.Append(HostRootFileSystemPath).Append(_path.Str); + + Debug.Assert(sb.Length == requiredNameBufferSize - 1); return Result.Success; } diff --git a/src/LibHac/FsSystem/FsPath.cs b/src/LibHac/FsSystem/FsPath.cs index ca71e059..208a2983 100644 --- a/src/LibHac/FsSystem/FsPath.cs +++ b/src/LibHac/FsSystem/FsPath.cs @@ -26,9 +26,10 @@ namespace LibHac.FsSystem { fsPath = new FsPath(); - U8StringBuilder builder = new U8StringBuilder(fsPath.Str).Append(path); + var sb = new U8StringBuilder(fsPath.Str); + bool overflowed = sb.Append(path).Overflowed; - return builder.Overflowed ? ResultFs.TooLongPath.Log() : Result.Success; + return overflowed ? ResultFs.TooLongPath.Log() : Result.Success; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/LibHac/Horizon.cs b/src/LibHac/Horizon.cs index 302acee5..117850ca 100644 --- a/src/LibHac/Horizon.cs +++ b/src/LibHac/Horizon.cs @@ -1,38 +1,70 @@ -using LibHac.Fs; +using LibHac.Common; +using LibHac.Fs; using LibHac.FsService; using LibHac.FsService.Creators; +using LibHac.Sm; namespace LibHac { public class Horizon { internal ITimeSpanGenerator Time { get; } - - public FileSystemClient Fs { get; } - public FileSystemServer FsSrv { get; private set; } + private FileSystemServer FileSystemServer { get; set; } + internal ServiceManager ServiceManager { get; } private readonly object _initLocker = new object(); public Horizon(ITimeSpanGenerator timer) { - Time = timer; + Time = timer ?? new StopWatchTimeSpanGenerator(); + ServiceManager = new ServiceManager(this); + } - Fs = new FileSystemClient(timer); + public Horizon(ITimeSpanGenerator timer, FileSystemServer fsServer) + { + Time = timer ?? new StopWatchTimeSpanGenerator(); + FileSystemServer = fsServer; + ServiceManager = new ServiceManager(this); + } + + private Result OpenFileSystemClient(out FileSystemClient client) + { + if (FileSystemServer is null) + { + client = default; + return ResultLibHac.ServiceNotInitialized.Log(); + } + + client = FileSystemServer.CreateFileSystemClient(); + return Result.Success; + } + + public Result CreateHorizonClient(out HorizonClient client) + { + Result rc = OpenFileSystemClient(out FileSystemClient fsClient); + if (rc.IsFailure()) + { + client = default; + return rc; + } + + client = new HorizonClient(this, fsClient); + return Result.Success; } public void InitializeFileSystemServer(FileSystemCreators fsCreators, IDeviceOperator deviceOperator) { - if (FsSrv != null) return; + if (FileSystemServer != null) return; lock (_initLocker) { - if (FsSrv != null) return; + if (FileSystemServer != null) return; var config = new FileSystemServerConfig(); config.FsCreators = fsCreators; config.DeviceOperator = deviceOperator; - FsSrv = new FileSystemServer(config); + FileSystemServer = new FileSystemServer(config); } } } diff --git a/src/LibHac/HorizonClient.cs b/src/LibHac/HorizonClient.cs new file mode 100644 index 00000000..16207b76 --- /dev/null +++ b/src/LibHac/HorizonClient.cs @@ -0,0 +1,33 @@ +using System; +using LibHac.Arp; +using LibHac.Fs; +using LibHac.Sm; + +namespace LibHac +{ + public class HorizonClient + { + private Horizon Horizon { get; } + + private Lazy ArpLazy { get; } + + public FileSystemClient Fs { get; } + public ServiceManagerClient Sm { get; } + public ArpClient Arp => ArpLazy.Value; + + internal HorizonClient(Horizon horizon, FileSystemClient fsClient) + { + Horizon = horizon; + + Fs = fsClient; + Sm = new ServiceManagerClient(horizon.ServiceManager); + + ArpLazy = new Lazy(InitArpClient, true); + } + + private ArpClient InitArpClient() + { + return new ArpClient(this); + } + } +} diff --git a/src/LibHac/Sf/ResultSf.cs b/src/LibHac/Sf/ResultSf.cs new file mode 100644 index 00000000..d38b7509 --- /dev/null +++ b/src/LibHac/Sf/ResultSf.cs @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +// This file was automatically generated. +// Changes to this file will be lost when the file is regenerated. +// +// To change this file, modify /build/CodeGen/results.csv at the root of this +// repo and run the build script. +// +// The script can be run with the "codegen" option to run only the +// code generation portion of the build. +//----------------------------------------------------------------------------- + +using System.Runtime.CompilerServices; + +namespace LibHac.Sf +{ + public static class ResultSf + { + public const int ModuleSf = 10; + + /// Error code: 2010-0001; Inner value: 0x20a + public static Result.Base NotSupported => new Result.Base(ModuleSf, 1); + /// Error code: 2010-0003; Inner value: 0x60a + public static Result.Base PreconditionViolation => new Result.Base(ModuleSf, 3); + /// Error code: 2010-0202; Inner value: 0x1940a + public static Result.Base InvalidHeaderSize => new Result.Base(ModuleSf, 202); + /// Error code: 2010-0211; Inner value: 0x1a60a + public static Result.Base InvalidInHeader => new Result.Base(ModuleSf, 211); + /// Error code: 2010-0221; Inner value: 0x1ba0a + public static Result.Base UnknownCommandId => new Result.Base(ModuleSf, 221); + /// Error code: 2010-0232; Inner value: 0x1d00a + public static Result.Base InvalidOutRawSize => new Result.Base(ModuleSf, 232); + /// Error code: 2010-0235; Inner value: 0x1d60a + public static Result.Base InvalidNumInObjects => new Result.Base(ModuleSf, 235); + /// Error code: 2010-0236; Inner value: 0x1d80a + public static Result.Base InvalidNumOutObjects => new Result.Base(ModuleSf, 236); + /// Error code: 2010-0239; Inner value: 0x1de0a + public static Result.Base InvalidInObject => new Result.Base(ModuleSf, 239); + /// Error code: 2010-0261; Inner value: 0x20a0a + public static Result.Base TargetNotFound => new Result.Base(ModuleSf, 261); + /// Error code: 2010-0301; Inner value: 0x25a0a + public static Result.Base OutOfDomainEntries => new Result.Base(ModuleSf, 301); + + /// Error code: 2010-0800; Range: 800-899; Inner value: 0x6400a + public static Result.Base RequestContextChanged { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleSf, 800, 899); } + /// Error code: 2010-0801; Range: 801-809; Inner value: 0x6420a + public static Result.Base RequestInvalidated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleSf, 801, 809); } + /// Error code: 2010-0802; Inner value: 0x6440a + public static Result.Base RequestInvalidatedByUser => new Result.Base(ModuleSf, 802); + + /// Error code: 2010-0811; Range: 811-819; Inner value: 0x6560a + public static Result.Base RequestDeferred { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleSf, 811, 819); } + /// Error code: 2010-0812; Inner value: 0x6580a + public static Result.Base RequestDeferredByUser => new Result.Base(ModuleSf, 812); + } +} diff --git a/src/LibHac/Sm/ResultSm.cs b/src/LibHac/Sm/ResultSm.cs new file mode 100644 index 00000000..c2248033 --- /dev/null +++ b/src/LibHac/Sm/ResultSm.cs @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +// This file was automatically generated. +// Changes to this file will be lost when the file is regenerated. +// +// To change this file, modify /build/CodeGen/results.csv at the root of this +// repo and run the build script. +// +// The script can be run with the "codegen" option to run only the +// code generation portion of the build. +//----------------------------------------------------------------------------- + +namespace LibHac.Sm +{ + public static class ResultSm + { + public const int ModuleSm = 21; + + /// Error code: 2021-0001; Inner value: 0x215 + public static Result.Base OutOfProcesses => new Result.Base(ModuleSm, 1); + /// Error code: 2021-0002; Inner value: 0x415 + public static Result.Base InvalidClient => new Result.Base(ModuleSm, 2); + /// Error code: 2021-0003; Inner value: 0x615 + public static Result.Base OutOfSessions => new Result.Base(ModuleSm, 3); + /// Error code: 2021-0004; Inner value: 0x815 + public static Result.Base AlreadyRegistered => new Result.Base(ModuleSm, 4); + /// Error code: 2021-0005; Inner value: 0xa15 + public static Result.Base OutOfServices => new Result.Base(ModuleSm, 5); + /// Error code: 2021-0006; Inner value: 0xc15 + public static Result.Base InvalidServiceName => new Result.Base(ModuleSm, 6); + /// Error code: 2021-0007; Inner value: 0xe15 + public static Result.Base NotRegistered => new Result.Base(ModuleSm, 7); + /// Error code: 2021-0008; Inner value: 0x1015 + public static Result.Base NotAllowed => new Result.Base(ModuleSm, 8); + /// Error code: 2021-0009; Inner value: 0x1215 + public static Result.Base TooLargeAccessControl => new Result.Base(ModuleSm, 9); + } +} diff --git a/src/LibHac/Sm/ServiceManager.cs b/src/LibHac/Sm/ServiceManager.cs new file mode 100644 index 00000000..9f37c8d2 --- /dev/null +++ b/src/LibHac/Sm/ServiceManager.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Sf; + +namespace LibHac.Sm +{ + // This is basically a makeshift service manager that doesn't do anything + // other than keep service objects for now. It's just here so other stuff + // isn't blocked waiting for something better. + internal class ServiceManager + { + private Horizon Horizon { get; } + private Dictionary Services { get; } = new Dictionary(); + + public ServiceManager(Horizon horizon) + { + Horizon = horizon; + } + + internal Result GetService(out object serviceObject, ServiceName serviceName) + { + serviceObject = default; + + Result rc = ValidateServiceName(serviceName); + if (rc.IsFailure()) return rc; + + if (!Services.TryGetValue(serviceName, out serviceObject)) + { + return ResultSf.RequestDeferredByUser.Log(); + } + + return Result.Success; + } + + internal Result RegisterService(object serviceObject, ServiceName serviceName) + { + Result rc = ValidateServiceName(serviceName); + if (rc.IsFailure()) return rc; + + if (!Services.TryAdd(serviceName, serviceObject)) + { + return ResultSm.AlreadyRegistered.Log(); + } + + return Result.Success; + } + + internal Result UnregisterService(ServiceName serviceName) + { + Result rc = ValidateServiceName(serviceName); + if (rc.IsFailure()) return rc; + + if (!Services.Remove(serviceName, out object service)) + { + return ResultSm.NotRegistered.Log(); + } + + if (service is IDisposable disposable) + { + disposable.Dispose(); + } + + return Result.Success; + } + + private Result ValidateServiceName(ServiceName name) + { + // Service names must be non-empty. + if (name.Name == 0) + return ResultSm.InvalidServiceName.Log(); + + // Get name length. + int nameLen; + for (nameLen = 1; nameLen < Unsafe.SizeOf(); nameLen++) + { + if (SpanHelpers.AsReadOnlyByteSpan(ref name)[nameLen] == 0) + { + break; + } + } + + // Names must be all-zero after they end. + for (; nameLen < Unsafe.SizeOf(); nameLen++) + { + if (SpanHelpers.AsReadOnlyByteSpan(ref name)[nameLen] != 0) + { + return ResultSm.InvalidServiceName.Log(); + } + } + + return Result.Success; + } + } +} diff --git a/src/LibHac/Sm/ServiceManagerClient.cs b/src/LibHac/Sm/ServiceManagerClient.cs new file mode 100644 index 00000000..0930e6ed --- /dev/null +++ b/src/LibHac/Sm/ServiceManagerClient.cs @@ -0,0 +1,42 @@ +using System; + +namespace LibHac.Sm +{ + public class ServiceManagerClient + { + private ServiceManager Server { get; } + + internal ServiceManagerClient(ServiceManager server) + { + Server = server; + } + + public Result GetService(out T serviceObject, ReadOnlySpan name) + { + Result rc = Server.GetService(out object service, ServiceName.Encode(name)); + if (rc.IsFailure()) + { + serviceObject = default; + return rc; + } + + if (service is T typedService) + { + serviceObject = typedService; + return Result.Success; + } + + throw new InvalidCastException("The service object is not of the specified type."); + } + + public Result RegisterService(object serviceObject, ReadOnlySpan name) + { + return Server.RegisterService(serviceObject, ServiceName.Encode(name)); + } + + public Result UnregisterService(ReadOnlySpan name) + { + return Server.UnregisterService(ServiceName.Encode(name)); + } + } +} diff --git a/src/LibHac/Sm/ServiceName.cs b/src/LibHac/Sm/ServiceName.cs new file mode 100644 index 00000000..7c3deb3a --- /dev/null +++ b/src/LibHac/Sm/ServiceName.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics; +using LibHac.Common; + +namespace LibHac.Sm +{ + [DebuggerDisplay("{ToString()}")] + public readonly struct ServiceName : IEquatable + { + private const int MaxLength = 8; + + public readonly ulong Name; + + public static ServiceName Encode(ReadOnlySpan name) + { + var outName = new ServiceName(); + int length = Math.Min(MaxLength, name.Length); + + for (int i = 0; i < length; i++) + { + SpanHelpers.AsByteSpan(ref outName)[i] = (byte)name[i]; + } + + return outName; + } + + public override bool Equals(object obj) => obj is ServiceName name && Equals(name); + public bool Equals(ServiceName other) => Name == other.Name; + + public override int GetHashCode() => Name.GetHashCode(); + + public static bool operator ==(ServiceName left, ServiceName right) => left.Equals(right); + public static bool operator !=(ServiceName left, ServiceName right) => !(left == right); + + public override string ToString() + { + ulong name = Name; + return StringUtils.Utf8ZToString(SpanHelpers.AsReadOnlyByteSpan(ref name)); + } + } +} diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index 4a633aab..447280b9 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -1,4 +1,5 @@ using LibHac; +using LibHac.Fs; using LibHac.FsSystem; namespace hactoolnet @@ -93,6 +94,6 @@ namespace hactoolnet public Options Options; public Keyset Keyset; public ProgressBar Logger; - public Horizon Horizon; + public FileSystemClient FsClient; } } diff --git a/src/hactoolnet/ProcessNca.cs b/src/hactoolnet/ProcessNca.cs index 47697630..eef9771e 100644 --- a/src/hactoolnet/ProcessNca.cs +++ b/src/hactoolnet/ProcessNca.cs @@ -44,7 +44,7 @@ namespace hactoolnet if (ctx.Options.SectionOutDir[i] != null) { - FileSystemClient fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.FsClient; string mountName = $"section{i}"; @@ -95,7 +95,7 @@ namespace hactoolnet if (ctx.Options.RomfsOutDir != null) { - FileSystemClient fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.FsClient; fs.Register("rom".ToU8Span(), OpenFileSystemByType(NcaSectionType.Data)); fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.RomfsOutDir)); @@ -152,7 +152,7 @@ namespace hactoolnet if (ctx.Options.ExefsOutDir != null) { - FileSystemClient fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.FsClient; fs.Register("code".ToU8Span(), OpenFileSystemByType(NcaSectionType.Code)); fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.ExefsOutDir)); @@ -232,10 +232,12 @@ namespace hactoolnet PrintItem(sb, colLen, $"NPDM Signature{nca.VerifySignature2().GetValidityString()}:", nca.Header.Signature2.ToArray()); PrintItem(sb, colLen, "Content Size:", $"0x{nca.Header.NcaSize:x12}"); PrintItem(sb, colLen, "TitleID:", $"{nca.Header.TitleId:X16}"); - if (nca.CanOpenSection(NcaSectionType.Code)) { + if (nca.CanOpenSection(NcaSectionType.Code)) + { IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.None); Result r = fs.OpenFile(out IFile file, "/main.npdm".ToU8String(), OpenMode.Read); - if (r.IsSuccess()) { + if (r.IsSuccess()) + { var npdm = new NpdmBinary(file.AsStream(), null); PrintItem(sb, colLen, "Title Name:", npdm.TitleName); } diff --git a/src/hactoolnet/ProcessSave.cs b/src/hactoolnet/ProcessSave.cs index 170b0b55..e663fccb 100644 --- a/src/hactoolnet/ProcessSave.cs +++ b/src/hactoolnet/ProcessSave.cs @@ -29,7 +29,7 @@ namespace hactoolnet bool signNeeded = ctx.Options.SignSave; var save = new SaveDataFileSystem(ctx.Keyset, file, ctx.Options.IntegrityLevel, true); - FileSystemClient fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.FsClient; fs.Register("save".ToU8Span(), save); diff --git a/src/hactoolnet/Program.cs b/src/hactoolnet/Program.cs index 108f8526..f2b4ab8a 100644 --- a/src/hactoolnet/Program.cs +++ b/src/hactoolnet/Program.cs @@ -59,17 +59,17 @@ namespace hactoolnet using (var logger = new ProgressBar()) { ctx.Logger = logger; - ctx.Horizon = new Horizon(new StopWatchTimeSpanGenerator()); + ctx.FsClient = new FileSystemClient(new StopWatchTimeSpanGenerator()); if (ctx.Options.AccessLog != null) { logWriter = new StreamWriter(ctx.Options.AccessLog); var accessLog = new TextWriterAccessLog(logWriter); - ctx.Horizon.Fs.SetAccessLogTarget(AccessLogTarget.All); - ctx.Horizon.Fs.SetGlobalAccessLogMode(GlobalAccessLogMode.Log); + ctx.FsClient.SetAccessLogTarget(AccessLogTarget.All); + ctx.FsClient.SetGlobalAccessLogMode(GlobalAccessLogMode.Log); - ctx.Horizon.Fs.SetAccessLogObject(accessLog); + ctx.FsClient.SetAccessLogObject(accessLog); } if (ctx.Options.ResultLog != null)