mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Add DebugConfigurationService
This commit is contained in:
parent
37b8249a8b
commit
ed6a34d857
7 changed files with 359 additions and 3 deletions
74
src/LibHac/Fs/Shim/Debug.cs
Normal file
74
src/LibHac/Fs/Shim/Debug.cs
Normal file
|
@ -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 "
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains debug-related functions for the FS service.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Based on nnSdk 13.4.0</remarks>
|
||||||
|
public static class DebugShim
|
||||||
|
{
|
||||||
|
public static Result CreatePaddingFile(this FileSystemClient fs, long size)
|
||||||
|
{
|
||||||
|
using SharedRef<IFileSystemProxy> 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<IFileSystemProxy> 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<byte> keyBuffer)
|
||||||
|
{
|
||||||
|
using SharedRef<IFileSystemProxy> 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<IFileSystemProxy> 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<IFileSystemProxy> fileSystemProxy = fs.Impl.GetFileSystemProxyServiceObject();
|
||||||
|
|
||||||
|
Result rc = fileSystemProxy.Get.UnregisterDebugConfiguration((uint)key);
|
||||||
|
fs.Impl.AbortIfNeeded(rc);
|
||||||
|
if (rc.IsFailure()) return rc.Miss();
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
}
|
163
src/LibHac/FsSrv/DebugConfigurationService.cs
Normal file
163
src/LibHac/FsSrv/DebugConfigurationService.cs
Normal file
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles debug configuration calls for <see cref="FileSystemProxyImpl"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages a key-value list of debug settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||||
|
public class DebugConfigurationServiceImpl : IDisposable
|
||||||
|
{
|
||||||
|
private Configuration _config;
|
||||||
|
private Array4<Entry> _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<SdkMutexType> 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<SdkMutexType> 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<SdkMutexType> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,4 +14,5 @@ public class FileSystemProxyConfiguration
|
||||||
public StatusReportServiceImpl StatusReportService { get; set; }
|
public StatusReportServiceImpl StatusReportService { get; set; }
|
||||||
public ProgramRegistryServiceImpl ProgramRegistryService { get; set; }
|
public ProgramRegistryServiceImpl ProgramRegistryService { get; set; }
|
||||||
public AccessLogServiceImpl AccessLogService { get; set; }
|
public AccessLogServiceImpl AccessLogService { get; set; }
|
||||||
|
public DebugConfigurationServiceImpl DebugConfigurationService { get; set; }
|
||||||
}
|
}
|
|
@ -35,6 +35,7 @@ public static class FileSystemProxyImplGlobalMethods
|
||||||
g.StatusReportServiceImpl = configuration.StatusReportService;
|
g.StatusReportServiceImpl = configuration.StatusReportService;
|
||||||
g.ProgramRegistryServiceImpl = configuration.ProgramRegistryService;
|
g.ProgramRegistryServiceImpl = configuration.ProgramRegistryService;
|
||||||
g.AccessLogServiceImpl = configuration.AccessLogService;
|
g.AccessLogServiceImpl = configuration.AccessLogService;
|
||||||
|
g.DebugConfigurationServiceImpl = configuration.DebugConfigurationService;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ internal struct FileSystemProxyImplGlobals
|
||||||
public StatusReportServiceImpl StatusReportServiceImpl;
|
public StatusReportServiceImpl StatusReportServiceImpl;
|
||||||
public ProgramRegistryServiceImpl ProgramRegistryServiceImpl;
|
public ProgramRegistryServiceImpl ProgramRegistryServiceImpl;
|
||||||
public AccessLogServiceImpl AccessLogServiceImpl;
|
public AccessLogServiceImpl AccessLogServiceImpl;
|
||||||
|
public DebugConfigurationServiceImpl DebugConfigurationServiceImpl;
|
||||||
public Optional<FileSystemProxyCoreImpl> FileSystemProxyCoreImpl;
|
public Optional<FileSystemProxyCoreImpl> FileSystemProxyCoreImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,6 +148,11 @@ public class FileSystemProxyImpl : IFileSystemProxy, IFileSystemProxyForLoader
|
||||||
return new AccessLogService(Globals.AccessLogServiceImpl, _currentProcess);
|
return new AccessLogService(Globals.AccessLogServiceImpl, _currentProcess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DebugConfigurationService GetDebugConfigurationService()
|
||||||
|
{
|
||||||
|
return new DebugConfigurationService(_fsServer, Globals.DebugConfigurationServiceImpl, _currentProcess);
|
||||||
|
}
|
||||||
|
|
||||||
public Result OpenFileSystemWithId(ref SharedRef<IFileSystemSf> outFileSystem, in FspPath path,
|
public Result OpenFileSystemWithId(ref SharedRef<IFileSystemSf> outFileSystem, in FspPath path,
|
||||||
ulong id, FileSystemProxyType fsType)
|
ulong id, FileSystemProxyType fsType)
|
||||||
{
|
{
|
||||||
|
@ -1151,4 +1158,14 @@ public class FileSystemProxyImpl : IFileSystemProxy, IFileSystemProxyForLoader
|
||||||
{
|
{
|
||||||
return GetBaseFileSystemService().OpenBisWiper(ref outBisWiper, transferMemoryHandle, transferMemorySize);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -74,6 +74,11 @@ public static class FileSystemServerInitializer
|
||||||
Memory<byte> heapBuffer = GC.AllocateArray<byte>(BufferManagerHeapSize, true);
|
Memory<byte> heapBuffer = GC.AllocateArray<byte>(BufferManagerHeapSize, true);
|
||||||
bufferManager.Initialize(BufferManagerCacheSize, heapBuffer, BufferManagerBlockSize);
|
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,
|
var saveDataIndexerManager = new SaveDataIndexerManager(server.Hos.Fs, Fs.SaveData.SaveIndexerId,
|
||||||
new ArrayPoolMemoryResource(), new SdHandleManager(), false);
|
new ArrayPoolMemoryResource(), new SdHandleManager(), false);
|
||||||
|
|
||||||
|
@ -175,7 +180,8 @@ public static class FileSystemServerInitializer
|
||||||
TimeService = timeService,
|
TimeService = timeService,
|
||||||
StatusReportService = statusReportService,
|
StatusReportService = statusReportService,
|
||||||
ProgramRegistryService = programRegistryService,
|
ProgramRegistryService = programRegistryService,
|
||||||
AccessLogService = accessLogService
|
AccessLogService = accessLogService,
|
||||||
|
DebugConfigurationService = debugConfigurationService
|
||||||
};
|
};
|
||||||
|
|
||||||
server.InitializeFileSystemProxy(fspConfig);
|
server.InitializeFileSystemProxy(fspConfig);
|
||||||
|
|
|
@ -124,6 +124,8 @@ public interface IFileSystemProxy : IDisposable
|
||||||
Result FlushAccessLogOnSdCard();
|
Result FlushAccessLogOnSdCard();
|
||||||
Result OutputApplicationInfoAccessLog(in ApplicationInfo applicationInfo);
|
Result OutputApplicationInfoAccessLog(in ApplicationInfo applicationInfo);
|
||||||
Result OutputMultiProgramTagAccessLog();
|
Result OutputMultiProgramTagAccessLog();
|
||||||
|
Result RegisterDebugConfiguration(uint key, long value);
|
||||||
|
Result UnregisterDebugConfiguration(uint key);
|
||||||
Result OverrideSaveDataTransferTokenSignVerificationKey(InBuffer key);
|
Result OverrideSaveDataTransferTokenSignVerificationKey(InBuffer key);
|
||||||
Result CorruptSaveDataFileSystemByOffset(SaveDataSpaceId spaceId, ulong saveDataId, long offset);
|
Result CorruptSaveDataFileSystemByOffset(SaveDataSpaceId spaceId, ulong saveDataId, long offset);
|
||||||
Result OpenMultiCommitManager(ref SharedRef<IMultiCommitManager> outCommitManager);
|
Result OpenMultiCommitManager(ref SharedRef<IMultiCommitManager> outCommitManager);
|
||||||
|
|
|
@ -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<HorizonResultException>(() => 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<LibHacException>(() => 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));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue