diff --git a/src/LibHac/Fs/Shim/Debug.cs b/src/LibHac/Fs/Shim/Debug.cs
new file mode 100644
index 00000000..be7307a7
--- /dev/null
+++ b/src/LibHac/Fs/Shim/Debug.cs
@@ -0,0 +1,74 @@
+using System;
+using LibHac.Common;
+using LibHac.FsSrv.Sf;
+using LibHac.Sf;
+
+namespace LibHac.Fs.Shim;
+
+public enum DebugOptionKey : uint
+{
+ SaveDataEncryption = 0x20454453 // "SDE "
+}
+
+///
+/// Contains debug-related functions for the FS service.
+///
+/// Based on nnSdk 13.4.0
+public static class DebugShim
+{
+ public static Result CreatePaddingFile(this FileSystemClient fs, long size)
+ {
+ using SharedRef fileSystemProxy = fs.Impl.GetFileSystemProxyServiceObject();
+
+ Result rc = fileSystemProxy.Get.CreatePaddingFile(size);
+ fs.Impl.AbortIfNeeded(rc);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public static Result DeleteAllPaddingFiles(this FileSystemClient fs)
+ {
+ using SharedRef fileSystemProxy = fs.Impl.GetFileSystemProxyServiceObject();
+
+ Result rc = fileSystemProxy.Get.DeleteAllPaddingFiles();
+ fs.Impl.AbortIfNeeded(rc);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public static Result OverrideSaveDataTransferTokenSignVerificationKey(this FileSystemClient fs,
+ ReadOnlySpan keyBuffer)
+ {
+ using SharedRef fileSystemProxy = fs.Impl.GetFileSystemProxyServiceObject();
+
+ Result rc = fileSystemProxy.Get.OverrideSaveDataTransferTokenSignVerificationKey(new InBuffer(keyBuffer));
+ fs.Impl.AbortIfNeeded(rc);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public static Result SetDebugOption(this FileSystemClient fs, DebugOptionKey key, long value)
+ {
+ using SharedRef fileSystemProxy = fs.Impl.GetFileSystemProxyServiceObject();
+
+ Result rc = fileSystemProxy.Get.RegisterDebugConfiguration((uint)key, value);
+ fs.Impl.AbortIfNeeded(rc);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public static Result UnsetDebugOption(this FileSystemClient fs, DebugOptionKey key)
+ {
+ using SharedRef fileSystemProxy = fs.Impl.GetFileSystemProxyServiceObject();
+
+ Result rc = fileSystemProxy.Get.UnregisterDebugConfiguration((uint)key);
+ fs.Impl.AbortIfNeeded(rc);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+}
\ No newline at end of file
diff --git a/src/LibHac/FsSrv/DebugConfigurationService.cs b/src/LibHac/FsSrv/DebugConfigurationService.cs
new file mode 100644
index 00000000..cd3593aa
--- /dev/null
+++ b/src/LibHac/FsSrv/DebugConfigurationService.cs
@@ -0,0 +1,163 @@
+using System;
+using LibHac.Common.FixedArrays;
+using LibHac.Diag;
+using LibHac.Fs;
+using LibHac.Fs.Shim;
+using LibHac.FsSrv.Impl;
+using LibHac.Os;
+
+namespace LibHac.FsSrv;
+
+///
+/// Handles debug configuration calls for .
+///
+/// Based on FS 13.1.0 (nnSdk 13.4.0)
+public struct DebugConfigurationService
+{
+ private DebugConfigurationServiceImpl _serviceImpl;
+ private ulong _processId;
+
+ // LibHac addition
+ private readonly FileSystemServer _fsServer;
+
+ public DebugConfigurationService(FileSystemServer fsServer, DebugConfigurationServiceImpl serviceImpl,
+ ulong processId)
+ {
+ _serviceImpl = serviceImpl;
+ _processId = processId;
+ _fsServer = fsServer;
+ }
+
+ private Result GetProgramInfo(out ProgramInfo programInfo, ulong processId)
+ {
+ using var programRegistry = new ProgramRegistryImpl(_fsServer);
+
+ return programRegistry.GetProgramInfo(out programInfo, processId);
+ }
+
+ public Result Register(uint key, long value)
+ {
+ Result rc = GetProgramInfo(out ProgramInfo programInfo, _processId);
+ if (rc.IsFailure()) return rc.Miss();
+
+ if (!programInfo.AccessControl.CanCall(OperationType.SetDebugConfiguration))
+ return ResultFs.PermissionDenied.Log();
+
+ _serviceImpl.Register(key, value);
+ return Result.Success;
+ }
+
+ public Result Unregister(uint key)
+ {
+ Result rc = GetProgramInfo(out ProgramInfo programInfo, _processId);
+ if (rc.IsFailure()) return rc.Miss();
+
+ if (!programInfo.AccessControl.CanCall(OperationType.SetDebugConfiguration))
+ return ResultFs.PermissionDenied.Log();
+
+ _serviceImpl.Unregister(key);
+ return Result.Success;
+ }
+}
+
+///
+/// Manages a key-value list of debug settings.
+///
+/// Based on FS 13.1.0 (nnSdk 13.4.0)
+public class DebugConfigurationServiceImpl : IDisposable
+{
+ private Configuration _config;
+ private Array4 _entries;
+ private SdkMutexType _mutex;
+
+ public struct Configuration
+ {
+ public bool IsDisabled;
+ }
+
+ private struct Entry
+ {
+ public uint Key;
+ public long Value;
+ }
+
+ public DebugConfigurationServiceImpl(in Configuration config)
+ {
+ _config = config;
+ _mutex = new SdkMutexType();
+ }
+
+ public void Dispose() { }
+
+ public void Register(uint key, long value)
+ {
+ Abort.DoAbortUnless(key != 0);
+
+ if (_config.IsDisabled)
+ return;
+
+ using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex);
+
+ for (int i = 0; i < _entries.ItemsRo.Length; i++)
+ {
+ // Update the existing value if the key is already registered
+ if (_entries[i].Key == key)
+ {
+ _entries[i].Value = value;
+ return;
+ }
+ }
+
+ for (int i = 0; i < _entries.ItemsRo.Length; i++)
+ {
+ if (_entries[i].Key == 0)
+ {
+ _entries[i].Key = key;
+ _entries[i].Value = value;
+ return;
+ }
+ }
+
+ Abort.DoAbort();
+ }
+
+ public void Unregister(uint key)
+ {
+ Abort.DoAbortUnless(key != 0);
+
+ if (_config.IsDisabled)
+ return;
+
+ using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex);
+
+ for (int i = 0; i < _entries.ItemsRo.Length; i++)
+ {
+ if (_entries[i].Key == key)
+ {
+ _entries[i].Key = 0;
+ _entries[i].Value = 0;
+ break;
+ }
+ }
+ }
+
+ public long Get(DebugOptionKey key, long defaultValue)
+ {
+ Abort.DoAbortUnless(key != 0);
+
+ if (_config.IsDisabled)
+ return defaultValue;
+
+ using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex);
+
+ for (int i = 0; i < _entries.ItemsRo.Length; i++)
+ {
+ if (_entries[i].Key == (uint)key)
+ {
+ return _entries[i].Value;
+ }
+ }
+
+ return defaultValue;
+ }
+}
\ No newline at end of file
diff --git a/src/LibHac/FsSrv/FileSystemProxyConfiguration.cs b/src/LibHac/FsSrv/FileSystemProxyConfiguration.cs
index 16332bcf..90a97cd9 100644
--- a/src/LibHac/FsSrv/FileSystemProxyConfiguration.cs
+++ b/src/LibHac/FsSrv/FileSystemProxyConfiguration.cs
@@ -14,4 +14,5 @@ public class FileSystemProxyConfiguration
public StatusReportServiceImpl StatusReportService { get; set; }
public ProgramRegistryServiceImpl ProgramRegistryService { get; set; }
public AccessLogServiceImpl AccessLogService { get; set; }
-}
+ public DebugConfigurationServiceImpl DebugConfigurationService { get; set; }
+}
\ No newline at end of file
diff --git a/src/LibHac/FsSrv/FileSystemProxyImpl.cs b/src/LibHac/FsSrv/FileSystemProxyImpl.cs
index 615e1dbc..0299f61d 100644
--- a/src/LibHac/FsSrv/FileSystemProxyImpl.cs
+++ b/src/LibHac/FsSrv/FileSystemProxyImpl.cs
@@ -35,6 +35,7 @@ public static class FileSystemProxyImplGlobalMethods
g.StatusReportServiceImpl = configuration.StatusReportService;
g.ProgramRegistryServiceImpl = configuration.ProgramRegistryService;
g.AccessLogServiceImpl = configuration.AccessLogService;
+ g.DebugConfigurationServiceImpl = configuration.DebugConfigurationService;
}
}
@@ -49,6 +50,7 @@ internal struct FileSystemProxyImplGlobals
public StatusReportServiceImpl StatusReportServiceImpl;
public ProgramRegistryServiceImpl ProgramRegistryServiceImpl;
public AccessLogServiceImpl AccessLogServiceImpl;
+ public DebugConfigurationServiceImpl DebugConfigurationServiceImpl;
public Optional FileSystemProxyCoreImpl;
}
@@ -146,6 +148,11 @@ public class FileSystemProxyImpl : IFileSystemProxy, IFileSystemProxyForLoader
return new AccessLogService(Globals.AccessLogServiceImpl, _currentProcess);
}
+ private DebugConfigurationService GetDebugConfigurationService()
+ {
+ return new DebugConfigurationService(_fsServer, Globals.DebugConfigurationServiceImpl, _currentProcess);
+ }
+
public Result OpenFileSystemWithId(ref SharedRef outFileSystem, in FspPath path,
ulong id, FileSystemProxyType fsType)
{
@@ -1151,4 +1158,14 @@ public class FileSystemProxyImpl : IFileSystemProxy, IFileSystemProxyForLoader
{
return GetBaseFileSystemService().OpenBisWiper(ref outBisWiper, transferMemoryHandle, transferMemorySize);
}
+
+ public Result RegisterDebugConfiguration(uint key, long value)
+ {
+ return GetDebugConfigurationService().Register(key, value);
+ }
+
+ public Result UnregisterDebugConfiguration(uint key)
+ {
+ return GetDebugConfigurationService().Unregister(key);
+ }
}
\ No newline at end of file
diff --git a/src/LibHac/FsSrv/FileSystemServerInitializer.cs b/src/LibHac/FsSrv/FileSystemServerInitializer.cs
index 8210b936..f36d95c6 100644
--- a/src/LibHac/FsSrv/FileSystemServerInitializer.cs
+++ b/src/LibHac/FsSrv/FileSystemServerInitializer.cs
@@ -74,6 +74,11 @@ public static class FileSystemServerInitializer
Memory heapBuffer = GC.AllocateArray(BufferManagerHeapSize, true);
bufferManager.Initialize(BufferManagerCacheSize, heapBuffer, BufferManagerBlockSize);
+ // Todo: Assign based on the value of "IsDevelopment"
+ var debugConfigurationServiceConfig = new DebugConfigurationServiceImpl.Configuration();
+ debugConfigurationServiceConfig.IsDisabled = false;
+ var debugConfigurationService = new DebugConfigurationServiceImpl(in debugConfigurationServiceConfig);
+
var saveDataIndexerManager = new SaveDataIndexerManager(server.Hos.Fs, Fs.SaveData.SaveIndexerId,
new ArrayPoolMemoryResource(), new SdHandleManager(), false);
@@ -175,7 +180,8 @@ public static class FileSystemServerInitializer
TimeService = timeService,
StatusReportService = statusReportService,
ProgramRegistryService = programRegistryService,
- AccessLogService = accessLogService
+ AccessLogService = accessLogService,
+ DebugConfigurationService = debugConfigurationService
};
server.InitializeFileSystemProxy(fspConfig);
diff --git a/src/LibHac/FsSrv/Sf/IFileSystemProxy.cs b/src/LibHac/FsSrv/Sf/IFileSystemProxy.cs
index d6145bb0..8a179974 100644
--- a/src/LibHac/FsSrv/Sf/IFileSystemProxy.cs
+++ b/src/LibHac/FsSrv/Sf/IFileSystemProxy.cs
@@ -124,8 +124,10 @@ public interface IFileSystemProxy : IDisposable
Result FlushAccessLogOnSdCard();
Result OutputApplicationInfoAccessLog(in ApplicationInfo applicationInfo);
Result OutputMultiProgramTagAccessLog();
+ Result RegisterDebugConfiguration(uint key, long value);
+ Result UnregisterDebugConfiguration(uint key);
Result OverrideSaveDataTransferTokenSignVerificationKey(InBuffer key);
Result CorruptSaveDataFileSystemByOffset(SaveDataSpaceId spaceId, ulong saveDataId, long offset);
Result OpenMultiCommitManager(ref SharedRef outCommitManager);
Result OpenBisWiper(ref SharedRef outBisWiper, NativeHandle transferMemoryHandle, ulong transferMemorySize);
-}
+}
\ No newline at end of file
diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Debug.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Debug.cs
new file mode 100644
index 00000000..72254ad9
--- /dev/null
+++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Debug.cs
@@ -0,0 +1,93 @@
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Shim;
+using LibHac.Ncm;
+using Xunit;
+
+namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests;
+
+public class Debug
+{
+ [Fact]
+ public void SetDebugOption_KeyIsZero_Aborts()
+ {
+ FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
+
+ Assert.Throws(() => fs.SetDebugOption(0, 1));
+ }
+
+ [Fact]
+ public void SetDebugOption_NoPermissions_ReturnsPermissionDenied()
+ {
+ Horizon hos = HorizonFactory.CreateBasicHorizon();
+
+ HorizonClient client =
+ hos.CreateHorizonClient(new ProgramLocation(new ProgramId(1), StorageId.BuiltInSystem), 0);
+
+ Assert.Result(ResultFs.PermissionDenied, client.Fs.SetDebugOption((DebugOptionKey)1, 0));
+ }
+
+ [Fact]
+ public void SetDebugOption_DebugConfigIsFull_Aborts()
+ {
+ FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
+
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)1, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)2, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)3, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)4, 0));
+
+ Assert.Throws(() => fs.SetDebugOption((DebugOptionKey)5, 0));
+ }
+
+ [Fact]
+ public void SetDebugOption_ReplaceExistingValueWhenFull_ReturnsSuccess()
+ {
+ FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
+
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)1, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)2, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)3, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)4, 0));
+
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)1, 10));
+ }
+
+ [Fact]
+ public void SetDebugOption_AfterRemovingKeyWhenFull_ReturnsSuccess()
+ {
+ FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
+
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)1, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)2, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)3, 0));
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)4, 0));
+
+ Assert.Success(fs.UnsetDebugOption((DebugOptionKey)2));
+
+ Assert.Success(fs.SetDebugOption((DebugOptionKey)2, 4));
+ }
+
+ [Fact]
+ public void UnsetDebugOption_UnsetExistingKey_ReturnsSuccess()
+ {
+ const DebugOptionKey key = DebugOptionKey.SaveDataEncryption;
+ const long value = 0;
+
+ FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
+
+ Assert.Success(fs.SetDebugOption(key, value));
+ Assert.Success(fs.UnsetDebugOption(key));
+ }
+
+ [Fact]
+ public void UnsetDebugOption_NoPermissions_ReturnsPermissionDenied()
+ {
+ Horizon hos = HorizonFactory.CreateBasicHorizon();
+
+ HorizonClient client =
+ hos.CreateHorizonClient(new ProgramLocation(new ProgramId(1), StorageId.BuiltInSystem), 0);
+
+ Assert.Result(ResultFs.PermissionDenied, client.Fs.UnsetDebugOption((DebugOptionKey)1));
+ }
+}
\ No newline at end of file