From 9d530f63f9e35857c5947d0ffbd32a55915c1431 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:25:29 +0300 Subject: [PATCH] [Tests] Cover controller ports, user exports, trials and open-file renames Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com> --- .../Kernel/KernelFileRenameTests.cs | 102 ++++++++++++++++++ .../Np/NpEntitlementAccessExportsTests.cs | 23 ++++ .../Pad/PadExportsTests.cs | 93 +++++++++++++++- .../UserService/UserServiceExportsTests.cs | 58 ++++++++++ 4 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Kernel/KernelFileRenameTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/UserService/UserServiceExportsTests.cs diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelFileRenameTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelFileRenameTests.cs new file mode 100644 index 0000000..6c0e90f --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelFileRenameTests.cs @@ -0,0 +1,102 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Kernel; + +[Collection(KernelMemoryCompatStateCollection.Name)] +public sealed class KernelFileRenameTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong SourcePathAddress = MemoryBase + 0x100; + private const ulong DestinationPathAddress = MemoryBase + 0x300; + private const ulong ReadBufferAddress = MemoryBase + 0x500; + + [Fact] + public void KernelRename_ReplacesDestinationWhileSourceDescriptorRemainsOpen() + { + var identity = Guid.NewGuid().ToString("N"); + var mountPoint = $"/rename-{identity}"; + var hostRoot = Path.Combine(Path.GetTempPath(), $"sharpemu-rename-{identity}"); + var sourceHostPath = Path.Combine(hostRoot, "STEMP000.DAT"); + var destinationHostPath = Path.Combine(hostRoot, "SDATA000.DAT"); + var sourceGuestPath = $"{mountPoint}/STEMP000.DAT"; + var destinationGuestPath = $"{mountPoint}/SDATA000.DAT"; + byte[] payload = [0x10, 0x20, 0x30, 0x40]; + var fileDescriptor = -1; + + Directory.CreateDirectory(hostRoot); + File.WriteAllBytes(sourceHostPath, payload); + File.WriteAllBytes(destinationHostPath, [0xFF]); + KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, hostRoot); + + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + memory.WriteCString(SourcePathAddress, sourceGuestPath); + memory.WriteCString(DestinationPathAddress, destinationGuestPath); + + try + { + context[CpuRegister.Rdi] = SourcePathAddress; + context[CpuRegister.Rsi] = 0x2; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelExports.KernelOpen(context)); + fileDescriptor = unchecked((int)context[CpuRegister.Rax]); + Assert.True(fileDescriptor >= 3); + + context[CpuRegister.Rdi] = SourcePathAddress; + context[CpuRegister.Rsi] = DestinationPathAddress; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelMemoryCompatExports.KernelRename(context)); + Assert.False(File.Exists(sourceHostPath)); + var renamedPayload = new byte[payload.Length]; + using (var destinationStream = new FileStream( + destinationHostPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete)) + { + destinationStream.ReadExactly(renamedPayload); + } + Assert.Equal(payload, renamedPayload); + + context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor); + context[CpuRegister.Rsi] = ReadBufferAddress; + context[CpuRegister.Rdx] = unchecked((uint)payload.Length); + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelMemoryCompatExports.KernelReadUnderscore(context)); + Assert.Equal((ulong)payload.Length, context[CpuRegister.Rax]); + + Span actual = stackalloc byte[payload.Length]; + Assert.True(memory.TryRead(ReadBufferAddress, actual)); + Assert.True(actual.SequenceEqual(payload)); + + context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor); + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelMemoryCompatExports.KernelClose(context)); + fileDescriptor = -1; + Assert.Equal(payload, File.ReadAllBytes(destinationHostPath)); + } + finally + { + if (fileDescriptor >= 0) + { + context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor); + _ = KernelMemoryCompatExports.KernelClose(context); + } + + _ = KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint); + if (Directory.Exists(hostRoot)) + { + Directory.Delete(hostRoot, recursive: true); + } + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Np/NpEntitlementAccessExportsTests.cs b/tests/SharpEmu.Libs.Tests/Np/NpEntitlementAccessExportsTests.cs index 7cf2556..6288d9c 100644 --- a/tests/SharpEmu.Libs.Tests/Np/NpEntitlementAccessExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Np/NpEntitlementAccessExportsTests.cs @@ -12,6 +12,29 @@ public sealed class NpEntitlementAccessExportsTests { private const ulong MemoryBase = 0x1_0000_0000; + [Fact] + public void GetGameTrialsFlagReportsAFullGameAndOnlyWritesOneWord() + { + var memory = new FakeCpuMemory(MemoryBase, 0x100); + var context = new CpuContext(memory, Generation.Gen5); + Assert.True(memory.TryWrite(MemoryBase, new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 })); + context[CpuRegister.Rdi] = MemoryBase; + Assert.Equal(0, NpEntitlementAccessExports.NpEntitlementAccessGetGameTrialsFlag(context)); + Span actual = stackalloc byte[8]; + Assert.True(memory.TryRead(MemoryBase, actual)); + Assert.Equal(new byte[] { 0, 0, 0, 0, 5, 6, 7, 8 }, actual.ToArray()); + } + + [Theory] + [InlineData(0UL, unchecked((int)0x817D0002))] + [InlineData(0x2_0000_0000UL, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)] + public void GetGameTrialsFlagValidatesOutputMemory(ulong address, int expected) + { + var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x100), Generation.Gen5); + context[CpuRegister.Rdi] = address; + Assert.Equal(expected, NpEntitlementAccessExports.NpEntitlementAccessGetGameTrialsFlag(context)); + } + [Theory] [InlineData("GHOST2APP0000000")] [InlineData("GHOST2BASE000000")] diff --git a/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs b/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs index 243d4e9..2d3efa6 100644 --- a/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs @@ -7,7 +7,7 @@ using Xunit; namespace SharpEmu.Libs.Tests.Pad; -public sealed class PadExportsTests +public sealed class PadExportsTests : IDisposable { private const ulong Base = 0x1_0000_0000; private const int InvalidHandle = unchecked((int)0x80920003); @@ -17,29 +17,42 @@ public sealed class PadExportsTests public PadExportsTests() { + PadExports.ResetOpenedPadForTests(); _ctx = new CpuContext(_memory, Generation.Gen5); } - [Theory] + public void Dispose() => PadExports.ResetOpenedPadForTests(); + + [NativeX64Theory] [InlineData(0, 0)] [InlineData(1, 0)] [InlineData(2, InvalidHandle)] [InlineData(-1, InvalidHandle)] public void SetTiltCorrectionState_ValidatesHandle(int handle, int expected) { + if (handle == 1) + { + Assert.Equal(1, OpenPad(0x10000000, 0)); + } + _ctx[CpuRegister.Rdi] = unchecked((ulong)handle); Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx)); } // ABI: int scePadResetOrientation(int32_t handle) — handle only, no out // parameter, so the only failure mode is a bad handle. - [Theory] + [NativeX64Theory] [InlineData(0, 0)] [InlineData(1, 0)] [InlineData(2, InvalidHandle)] [InlineData(-1, InvalidHandle)] public void ResetOrientation_ValidatesHandle(int handle, int expected) { + if (handle == 1) + { + Assert.Equal(1, OpenPad(0x10000000, 0)); + } + _ctx[CpuRegister.Rdi] = unchecked((ulong)handle); Assert.Equal(expected, PadExports.PadResetOrientation(_ctx)); Assert.Equal(unchecked((ulong)expected), _ctx[CpuRegister.Rax]); @@ -132,4 +145,78 @@ public sealed class PadExportsTests PadExports.ResetOpenedPadForTests(); } } + + [NativeX64Fact] + public void SpecialAndRemotePortsHaveIndependentLiveHandles() + { + const int noHandle = unchecked((int)0x80920008); + const int alreadyOpened = unchecked((int)0x80920004); + const ulong dataAddress = Base + 0x200; + + Assert.Equal(0, PadExports.PadInit(_ctx)); + _ctx[CpuRegister.Rdi] = 0xFF; + _ctx[CpuRegister.Rsi] = 16; + _ctx[CpuRegister.Rdx] = 0; + Assert.Equal(noHandle, PadExports.PadGetHandle(_ctx)); + + var standard = OpenPad(0x10000000, 0); + var special = OpenPad(0x10000000, 2); + var remote = OpenPad(0xFF, 16); + Assert.Equal(1, standard); + Assert.Equal(2, special); + Assert.Equal(3, remote); + Assert.Equal(alreadyOpened, OpenPad(0xFF, 16)); + Assert.Equal(remote, PadExports.PadGetHandle(_ctx)); + + foreach (var handle in new[] { standard, special, remote }) + { + _ctx[CpuRegister.Rdi] = unchecked((ulong)handle); + _ctx[CpuRegister.Rsi] = dataAddress; + Assert.Equal(0, PadExports.PadGetControllerInformation(_ctx)); + Assert.Equal(0, PadExports.PadReadState(_ctx)); + } + + _ctx[CpuRegister.Rdi] = unchecked((ulong)remote); + Assert.Equal(0, PadExports.PadClose(_ctx)); + Assert.Equal(InvalidHandle, PadExports.PadReadState(_ctx)); + Assert.Equal(InvalidHandle, PadExports.PadClose(_ctx)); + + _ctx[CpuRegister.Rdi] = 0xFF; + _ctx[CpuRegister.Rsi] = 16; + _ctx[CpuRegister.Rdx] = 0; + Assert.Equal(noHandle, PadExports.PadGetHandle(_ctx)); + _ctx[CpuRegister.Rdi] = 0x10000000; + _ctx[CpuRegister.Rsi] = 0; + Assert.Equal(standard, PadExports.PadGetHandle(_ctx)); + _ctx[CpuRegister.Rsi] = 2; + Assert.Equal(special, PadExports.PadGetHandle(_ctx)); + Assert.True(OpenPad(0xFF, 16) > remote); + } + + [NativeX64Theory] + [InlineData(-1, 0, 0, unchecked((int)0x80920008))] + [InlineData(0x10000001, 0, 0, unchecked((int)0x80920007))] + [InlineData(0x10000000, 1, 0, unchecked((int)0x80920007))] + [InlineData(0x10000000, 16, 0, unchecked((int)0x80920007))] + [InlineData(0xFF, 0, 0, unchecked((int)0x80920007))] + [InlineData(0xFF, 16, 1, unchecked((int)0x80920007))] + public void Open_RejectsUnsupportedTuples(int userId, int type, int index, int expected) + { + PadExports.PadInit(_ctx); + _ctx[CpuRegister.Rdi] = unchecked((ulong)userId); + _ctx[CpuRegister.Rsi] = unchecked((ulong)type); + _ctx[CpuRegister.Rdx] = unchecked((ulong)index); + _ctx[CpuRegister.Rcx] = 0; + Assert.Equal(expected, PadExports.PadOpen(_ctx)); + } + + private int OpenPad(int userId, int type) + { + PadExports.PadInit(_ctx); + _ctx[CpuRegister.Rdi] = unchecked((ulong)userId); + _ctx[CpuRegister.Rsi] = unchecked((ulong)type); + _ctx[CpuRegister.Rdx] = 0; + _ctx[CpuRegister.Rcx] = 0; + return PadExports.PadOpen(_ctx); + } } diff --git a/tests/SharpEmu.Libs.Tests/UserService/UserServiceExportsTests.cs b/tests/SharpEmu.Libs.Tests/UserService/UserServiceExportsTests.cs new file mode 100644 index 0000000..85f39a1 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/UserService/UserServiceExportsTests.cs @@ -0,0 +1,58 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.UserService; +using Xunit; + +namespace SharpEmu.Libs.Tests.UserService; + +public sealed class UserServiceExportsTests +{ + [Theory] + [InlineData(0x10000000)] + [InlineData(1)] + public void GetUserNumberWritesOneBasedSlotWithoutOverwritingAdjacentData(int userId) + { + const ulong address = 0x1_0000_0000; + var memory = new FakeCpuMemory(address, 0x100); + var ctx = new CpuContext(memory, Generation.Gen5); + Assert.True(memory.TryWrite(address, new byte[] { 0xEE, 0xEE, 0xEE, 0xEE, 0xAA, 0xBB, 0xCC, 0xDD })); + ctx[CpuRegister.Rdi] = unchecked((ulong)userId); + ctx[CpuRegister.Rsi] = address; + Assert.Equal(0, UserServiceExports.UserServiceGetUserNumber(ctx)); + Span actual = stackalloc byte[8]; + Assert.True(memory.TryRead(address, actual)); + Assert.Equal(new byte[] { 1, 0, 0, 0, 0xAA, 0xBB, 0xCC, 0xDD }, actual.ToArray()); + } + + [Theory] + [InlineData(0x10000001, 0x1_0000_0000UL, unchecked((int)0x80960009))] + [InlineData(0x10000000, 0UL, unchecked((int)0x80960005))] + [InlineData(0x10000000, 0x2_0000_0000UL, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)] + public void GetUserNumberValidatesUserAndOutput(int userId, ulong address, int expected) + { + var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x100), Generation.Gen5); + ctx[CpuRegister.Rdi] = unchecked((ulong)userId); + ctx[CpuRegister.Rsi] = address; + Assert.Equal(expected, UserServiceExports.UserServiceGetUserNumber(ctx)); + } + + [Fact] + public void TerminateResetsTheLoginEvent() + { + const ulong address = 0x1_0000_0000; + var memory = new FakeCpuMemory(address, 0x100); + var ctx = new CpuContext(memory, Generation.Gen5); + Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx)); + Assert.Equal(0, UserServiceExports.UserServiceInitialize(ctx)); + ctx[CpuRegister.Rdi] = address; + Assert.Equal(0, UserServiceExports.UserServiceGetEvent(ctx)); + Assert.Equal(unchecked((int)0x80960007), UserServiceExports.UserServiceGetEvent(ctx)); + Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx)); + Assert.Equal(0UL, ctx[CpuRegister.Rax]); + Assert.Equal(0, UserServiceExports.UserServiceInitialize(ctx)); + Assert.Equal(0, UserServiceExports.UserServiceGetEvent(ctx)); + Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx)); + } +}