Merge pull request #129 from Thealexbarney/bcat-storage

Implement BCAT delivery cache access

- Add BCAT delivery cache reading services
- Add methods to `U8StringBuilder` for writing formatted values
- Add a `HorizonClient` class which mimics nnsdk from Horizon
- Add a simple service manager that tracks registered objects
This commit is contained in:
Alex Barney 2020-04-28 22:54:58 -07:00 committed by GitHub
commit fdf11b2621
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2069 additions and 55 deletions

View File

@ -1,6 +1,9 @@
Name,Index
Fs,2
Loader,9
Sf,10
Kvdb,20
Sm,21
Sdmmc,24
Bcat,122
LibHac,428
1 Name Index
2 Fs 2
3 Loader 9
4 Sf 10
5 Kvdb 20
6 Sm 21
7 Sdmmc 24
8 Bcat 122
9 LibHac 428

View File

@ -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
1 Name Namespace Path
2 Fs LibHac.Fs LibHac/Fs/ResultFs.cs
3 Loader LibHac.Loader LibHac/Loader/ResultLoader.cs
4 Sf LibHac.Sf LibHac/Sf/ResultSf.cs
5 Kvdb LibHac.Kvdb LibHac/Kvdb/ResultKvdb.cs
6 Sm LibHac.Sm LibHac/Sm/ResultSm.cs
7 Sdmmc LibHac.FsService LibHac/FsService/ResultSdmmc.cs
8 Bcat LibHac.Bcat LibHac/Bcat/ResultBcat.cs
9 LibHac LibHac.Common LibHac/Common/ResultLibHac.cs

View File

@ -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.

1 Module,DescriptionStart,DescriptionEnd,Name,Summary
268 122,10,,SaveDataNotFound,
269 122,31,,NetworkServiceAccountNotAvailable,
270 122,80,,PassphrasePathNotFound,
271 122,90,,PermissionDenied,
272 122,91,,AllocationFailed,
273 122,204,,InvalidDeliveryCacheStorageFile,
274 122,205,,StorageOpenLimitReached,
275 123,0,4999,SslService,
276 124,0,,Cancelled,
277 124,1,,CancelledByUser,
278 124,100,,UserNotExist,
279 124,200,269,NetworkServiceAccountUnavailable,
280 124,430,499,TokenCacheUnavailable,
281 124,3000,8191,NetworkCommunicationError,
282 202,140,149,Invalid,
283 202,601,,DualConnected,
284 202,602,,SameJoyTypeConnected,
285 202,603,,ColorNotAvailable,
286 202,604,,ControllerNotConnected,
287 202,3101,,Canceled,
288 202,3102,,NotSupportedNpadStyle,
289 202,3200,3209,ControllerFirmwareUpdateError,
290 202,3201,,ControllerFirmwareUpdateFailed,
291 205,110,119,IrsensorUnavailable,
292 205,110,,IrsensorUnconnected,
293 205,111,,IrsensorUnsupported,
294 205,120,,IrsensorNotReady,
295 205,122,139,IrsensorDeviceError,
296 428,1,49,InvalidArgument,
297 428,2,,NullArgument,
298 428,3,,ArgumentOutOfRange,
299 428,4,,BufferTooSmall,
300 428,51,,ServiceNotInitialized,
301 428,1000,1999,InvalidData,
302 428,1001,1019,InvalidKip,
303 428,1002,,InvalidKipFileSize,The size of the KIP file was smaller than expected.
304 428,1003,,InvalidKipMagic,The magic value of the KIP file was not KIP1.
305 428,1004,,InvalidKipSegmentSize,The size of the compressed KIP segment was smaller than expected.
306 428,1005,,KipSegmentDecompressionFailed,An error occurred while decompressing a KIP segment.
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
357
358
359
360
361
362
363
364

View File

@ -0,0 +1,20 @@
using System;
namespace LibHac
{
public readonly struct ApplicationId : IEquatable<ApplicationId>
{
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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,10 @@
namespace LibHac.Bcat
{
public enum BcatServiceType
{
BcatU,
BcatS,
BcatM,
BcatA
}
}

View File

@ -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;
}
}
}

View File

@ -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<DeliveryCacheDirectoryEntry> entryBuffer);
Result GetCount(out int count);
}
}

View File

@ -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<byte> destination);
Result GetSize(out long size);
Result GetDigest(out Digest digest);
}
}

View File

@ -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<DirectoryName> nameBuffer);
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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<byte> 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<byte> buffer = MemoryMarshal.Cast<DeliveryCacheFileMetaEntry, byte>(Entries);
rc = fs.ReadFile(out bytesRead, handle, 4, buffer);
if (rc.IsFailure()) return rc;
Count = (int)((uint)bytesRead / Unsafe.SizeOf<DeliveryCacheFileMetaEntry>());
return Result.Success;
}
finally
{
fs.CloseFile(handle);
}
}
}
}
}

View File

@ -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;
}
}

View File

@ -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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> pathBuffer, ulong applicationId)
{
// returns "mount:/directories"
lock (_locker)
{
var sb = new U8StringBuilder(pathBuffer);
AppendMountName(ref sb, applicationId);
sb.Append(DirectoriesPath);
}
}
public void GetDirectoryPath(Span<byte> 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<byte> 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<byte> DeliveryCacheMountNamePrefix => // bcat-dc-
new[] { (byte)'b', (byte)'c', (byte)'a', (byte)'t', (byte)'-', (byte)'d', (byte)'c', (byte)'-' };
private static ReadOnlySpan<byte> RootPath => // :/
new[] { (byte)':', (byte)'/' };
private static ReadOnlySpan<byte> 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<byte> 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<byte> EtagPath => // :/etag.bin
new[]
{
(byte) ':', (byte) '/', (byte) 'e', (byte) 't', (byte) 'a', (byte) 'g', (byte) '.', (byte) 'b',
(byte) 'i', (byte) 'n'
};
private static ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> FilesDirectoryName => // files
new[] { (byte)'f', (byte)'i', (byte)'l', (byte)'e', (byte)'s' };
}
}

View File

@ -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<DeliveryCacheDirectoryEntry> 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();
}
}
}

View File

@ -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<byte> 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<byte> 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();
}
}
}

View File

@ -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<DirectoryName> 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);
}
}
}

View File

@ -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;
}
}
}

28
src/LibHac/Bcat/Digest.cs Normal file
View File

@ -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<byte> Bytes => SpanHelpers.AsByteSpan(ref this);
public override string ToString()
{
return Bytes.ToHexString();
}
}
}

View File

@ -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<byte> Bytes => SpanHelpers.AsByteSpan(ref this);
public bool IsValid()
{
Span<byte> 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);
}
}
}

View File

@ -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<byte> Bytes => SpanHelpers.AsByteSpan(ref this);
public bool IsValid()
{
Span<byte> 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);
}
}
}

View File

@ -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;
/// <summary>Error code: 2122-0001; Inner value: 0x27a</summary>
public static Result.Base InvalidArgument => new Result.Base(ModuleBcat, 1);
/// <summary>Error code: 2122-0002; Inner value: 0x47a</summary>
public static Result.Base NotFound => new Result.Base(ModuleBcat, 2);
/// <summary>Error code: 2122-0003; Inner value: 0x67a</summary>
public static Result.Base TargetLocked => new Result.Base(ModuleBcat, 3);
/// <summary>Error code: 2122-0006; Inner value: 0xc7a</summary>
public static Result.Base AlreadyOpen => new Result.Base(ModuleBcat, 6);
/// <summary>Error code: 2122-0007; Inner value: 0xe7a</summary>
public static Result.Base NotOpen => new Result.Base(ModuleBcat, 7);
/// <summary>Error code: 2122-0009; Inner value: 0x127a</summary>
public static Result.Base ServiceOpenLimitReached => new Result.Base(ModuleBcat, 9);
/// <summary>Error code: 2122-0010; Inner value: 0x147a</summary>
public static Result.Base SaveDataNotFound => new Result.Base(ModuleBcat, 10);
/// <summary>Error code: 2122-0031; Inner value: 0x3e7a</summary>
public static Result.Base NetworkServiceAccountNotAvailable => new Result.Base(ModuleBcat, 31);
/// <summary>Error code: 2122-0080; Inner value: 0xa07a</summary>
public static Result.Base PassphrasePathNotFound => new Result.Base(ModuleBcat, 80);
/// <summary>Error code: 2122-0090; Inner value: 0xb47a</summary>
public static Result.Base PermissionDenied => new Result.Base(ModuleBcat, 90);
/// <summary>Error code: 2122-0091; Inner value: 0xb67a</summary>
public static Result.Base AllocationFailed => new Result.Base(ModuleBcat, 91);
/// <summary>Error code: 2122-0204; Inner value: 0x1987a</summary>
public static Result.Base InvalidDeliveryCacheStorageFile => new Result.Base(ModuleBcat, 204);
/// <summary>Error code: 2122-0205; Inner value: 0x19a7a</summary>
public static Result.Base StorageOpenLimitReached => new Result.Base(ModuleBcat, 205);
}
}

View File

@ -26,6 +26,9 @@ namespace LibHac.Common
/// <summary>Error code: 2428-0004; Inner value: 0x9ac</summary>
public static Result.Base BufferTooSmall => new Result.Base(ModuleLibHac, 4);
/// <summary>Error code: 2428-0051; Inner value: 0x67ac</summary>
public static Result.Base ServiceNotInitialized => new Result.Base(ModuleLibHac, 51);
/// <summary>Error code: 2428-1000; Range: 1000-1999; Inner value: 0x7d1ac</summary>
public static Result.Base InvalidData { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleLibHac, 1000, 1999); }
/// <summary>Error code: 2428-1001; Range: 1001-1019; Inner value: 0x7d3ac</summary>

View File

@ -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;
}
}
}

View File

@ -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<byte> _buffer;
private int _length;
public Span<byte> 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<byte> Buffer => _buffer;
public readonly int Capacity
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Buffer.Length - NullTerminatorLength;
}
public U8StringBuilder(Span<byte> buffer)
{
_buffer = buffer;
_length = 0;
Buffer = buffer;
Length = 0;
Overflowed = false;
ThrowIfBufferLengthIsZero();
@ -27,62 +32,287 @@ namespace LibHac.Common
AddNullTerminator();
}
public U8StringBuilder Append(ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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;
}
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -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)]

View File

@ -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);
}
}
}

View File

@ -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<ArpClient> 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<ArpClient>(InitArpClient, true);
}
private ArpClient InitArpClient()
{
return new ArpClient(this);
}
}
}

55
src/LibHac/Sf/ResultSf.cs Normal file
View File

@ -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;
/// <summary>Error code: 2010-0001; Inner value: 0x20a</summary>
public static Result.Base NotSupported => new Result.Base(ModuleSf, 1);
/// <summary>Error code: 2010-0003; Inner value: 0x60a</summary>
public static Result.Base PreconditionViolation => new Result.Base(ModuleSf, 3);
/// <summary>Error code: 2010-0202; Inner value: 0x1940a</summary>
public static Result.Base InvalidHeaderSize => new Result.Base(ModuleSf, 202);
/// <summary>Error code: 2010-0211; Inner value: 0x1a60a</summary>
public static Result.Base InvalidInHeader => new Result.Base(ModuleSf, 211);
/// <summary>Error code: 2010-0221; Inner value: 0x1ba0a</summary>
public static Result.Base UnknownCommandId => new Result.Base(ModuleSf, 221);
/// <summary>Error code: 2010-0232; Inner value: 0x1d00a</summary>
public static Result.Base InvalidOutRawSize => new Result.Base(ModuleSf, 232);
/// <summary>Error code: 2010-0235; Inner value: 0x1d60a</summary>
public static Result.Base InvalidNumInObjects => new Result.Base(ModuleSf, 235);
/// <summary>Error code: 2010-0236; Inner value: 0x1d80a</summary>
public static Result.Base InvalidNumOutObjects => new Result.Base(ModuleSf, 236);
/// <summary>Error code: 2010-0239; Inner value: 0x1de0a</summary>
public static Result.Base InvalidInObject => new Result.Base(ModuleSf, 239);
/// <summary>Error code: 2010-0261; Inner value: 0x20a0a</summary>
public static Result.Base TargetNotFound => new Result.Base(ModuleSf, 261);
/// <summary>Error code: 2010-0301; Inner value: 0x25a0a</summary>
public static Result.Base OutOfDomainEntries => new Result.Base(ModuleSf, 301);
/// <summary>Error code: 2010-0800; Range: 800-899; Inner value: 0x6400a</summary>
public static Result.Base RequestContextChanged { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleSf, 800, 899); }
/// <summary>Error code: 2010-0801; Range: 801-809; Inner value: 0x6420a</summary>
public static Result.Base RequestInvalidated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleSf, 801, 809); }
/// <summary>Error code: 2010-0802; Inner value: 0x6440a</summary>
public static Result.Base RequestInvalidatedByUser => new Result.Base(ModuleSf, 802);
/// <summary>Error code: 2010-0811; Range: 811-819; Inner value: 0x6560a</summary>
public static Result.Base RequestDeferred { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleSf, 811, 819); }
/// <summary>Error code: 2010-0812; Inner value: 0x6580a</summary>
public static Result.Base RequestDeferredByUser => new Result.Base(ModuleSf, 812);
}
}

37
src/LibHac/Sm/ResultSm.cs Normal file
View File

@ -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;
/// <summary>Error code: 2021-0001; Inner value: 0x215</summary>
public static Result.Base OutOfProcesses => new Result.Base(ModuleSm, 1);
/// <summary>Error code: 2021-0002; Inner value: 0x415</summary>
public static Result.Base InvalidClient => new Result.Base(ModuleSm, 2);
/// <summary>Error code: 2021-0003; Inner value: 0x615</summary>
public static Result.Base OutOfSessions => new Result.Base(ModuleSm, 3);
/// <summary>Error code: 2021-0004; Inner value: 0x815</summary>
public static Result.Base AlreadyRegistered => new Result.Base(ModuleSm, 4);
/// <summary>Error code: 2021-0005; Inner value: 0xa15</summary>
public static Result.Base OutOfServices => new Result.Base(ModuleSm, 5);
/// <summary>Error code: 2021-0006; Inner value: 0xc15</summary>
public static Result.Base InvalidServiceName => new Result.Base(ModuleSm, 6);
/// <summary>Error code: 2021-0007; Inner value: 0xe15</summary>
public static Result.Base NotRegistered => new Result.Base(ModuleSm, 7);
/// <summary>Error code: 2021-0008; Inner value: 0x1015</summary>
public static Result.Base NotAllowed => new Result.Base(ModuleSm, 8);
/// <summary>Error code: 2021-0009; Inner value: 0x1215</summary>
public static Result.Base TooLargeAccessControl => new Result.Base(ModuleSm, 9);
}
}

View File

@ -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<ServiceName, object> Services { get; } = new Dictionary<ServiceName, object>();
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<ServiceName>(); nameLen++)
{
if (SpanHelpers.AsReadOnlyByteSpan(ref name)[nameLen] == 0)
{
break;
}
}
// Names must be all-zero after they end.
for (; nameLen < Unsafe.SizeOf<ServiceName>(); nameLen++)
{
if (SpanHelpers.AsReadOnlyByteSpan(ref name)[nameLen] != 0)
{
return ResultSm.InvalidServiceName.Log();
}
}
return Result.Success;
}
}
}

View File

@ -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<T>(out T serviceObject, ReadOnlySpan<char> 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<char> name)
{
return Server.RegisterService(serviceObject, ServiceName.Encode(name));
}
public Result UnregisterService(ReadOnlySpan<char> name)
{
return Server.UnregisterService(ServiceName.Encode(name));
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Diagnostics;
using LibHac.Common;
namespace LibHac.Sm
{
[DebuggerDisplay("{ToString()}")]
public readonly struct ServiceName : IEquatable<ServiceName>
{
private const int MaxLength = 8;
public readonly ulong Name;
public static ServiceName Encode(ReadOnlySpan<char> 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));
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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)