From 810ff6a606fac2307ed6ce42cf92c6d7010f807e Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:43:11 +0300 Subject: [PATCH] [Tests] Cover the AudioOut2, save data, Http2 and dialog exports Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> --- ...dioOut2ContextGetQueueLevelExportsTests.cs | 92 ++++++ ...AudioOut2ContextQueryMemoryExportsTests.cs | 68 +++++ .../Audio/AudioOut2MasteringExportsTests.cs | 40 +++ .../Audio/AudioOut2PcmSnapshotTests.cs | 255 ++++++++++++++++ .../AudioOut2PortGetStateExportsTests.cs | 19 +- .../AvPlayer/AvPlayerControlTests.cs | 54 ++++ .../WebBrowserDialogExportsTests.cs | 65 +++++ .../Json/JsonExportRegistrationTests.cs | 34 +++ .../Network/Http2ExportsTests.cs | 272 ++++++++++++++++++ .../SaveData/SaveDataExportsTests.cs | 81 ++++++ 10 files changed, 974 insertions(+), 6 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs new file mode 100644 index 0000000..2e7a5b1 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextGetQueueLevelExportsTests.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Audio; +using Xunit; + +namespace SharpEmu.Libs.Tests.Audio; + +public sealed class AudioOut2ContextGetQueueLevelExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong ParamAddress = MemoryBase + 0x100; + private const ulong ContextMemoryAddress = MemoryBase + 0x200; + private const ulong OutContextAddress = MemoryBase + 0x300; + private const ulong LevelAddress = MemoryBase + 0x400; + private const ulong AvailableAddress = MemoryBase + 0x404; + private const uint QueueDepth = 7; + + private static (CpuContext Context, FakeCpuMemory Memory, ulong Handle) CreateContext() + { + var memory = new FakeCpuMemory(MemoryBase, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + + Span param = stackalloc byte[0x40]; + param.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], QueueDepth); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x10..], 512); + Assert.True(memory.TryWrite(ParamAddress, param)); + + ctx[CpuRegister.Rdi] = ParamAddress; + ctx[CpuRegister.Rsi] = ContextMemoryAddress; + ctx[CpuRegister.Rdx] = 0x1000; + ctx[CpuRegister.Rcx] = OutContextAddress; + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextCreate(ctx)); + + Span handleBytes = stackalloc byte[8]; + Assert.True(memory.TryRead(OutContextAddress, handleBytes)); + return (ctx, memory, BinaryPrimitives.ReadUInt64LittleEndian(handleBytes)); + } + + private static uint ReadU32(FakeCpuMemory memory, ulong address) + { + Span value = stackalloc byte[4]; + Assert.True(memory.TryRead(address, value)); + return BinaryPrimitives.ReadUInt32LittleEndian(value); + } + + [Fact] + public void ContextGetQueueLevel_AvailableOnlyWritesQueueCapacity() + { + var (ctx, memory, handle) = CreateContext(); + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = 0; + ctx[CpuRegister.Rdx] = AvailableAddress; + + var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx); + + Assert.Equal(0, result); + Assert.Equal(QueueDepth, ReadU32(memory, AvailableAddress)); + } + + [Fact] + public void ContextGetQueueLevel_LevelOnlyWritesZero() + { + var (ctx, memory, handle) = CreateContext(); + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = LevelAddress; + ctx[CpuRegister.Rdx] = 0; + + var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx); + + Assert.Equal(0, result); + Assert.Equal(0u, ReadU32(memory, LevelAddress)); + } + + [Fact] + public void ContextGetQueueLevel_BothOutputsRemainDistinct() + { + var (ctx, memory, handle) = CreateContext(); + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = LevelAddress; + ctx[CpuRegister.Rdx] = AvailableAddress; + + var result = AudioOut2Exports.AudioOut2ContextGetQueueLevel(ctx); + + Assert.Equal(0, result); + Assert.Equal(0u, ReadU32(memory, LevelAddress)); + Assert.Equal(QueueDepth, ReadU32(memory, AvailableAddress)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs new file mode 100644 index 0000000..1ebfe22 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2ContextQueryMemoryExportsTests.cs @@ -0,0 +1,68 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Audio; +using Xunit; + +namespace SharpEmu.Libs.Tests.Audio; + +public sealed class AudioOut2ContextQueryMemoryExportsTests +{ + private const int InvalidPointer = unchecked((int)0x8026_800C); + private const ulong MemoryBase = 0x10_0000_0000; + private const ulong ParamAddress = MemoryBase + 0x100; + private const ulong StackPointer = MemoryBase + 0x400; + private const ulong MemorySizeAddress = StackPointer + 0x10; + private const ulong StaleThirdArgument = MemoryBase + 0x800; + + [Fact] + public void ContextQueryMemory_WritesOnlyOneSizeTAndIgnoresRdx() + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var ctx = new CpuContext(memory, Generation.Gen5); + + Span param = stackalloc byte[0x40]; + param.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 4); + Assert.True(memory.TryWrite(ParamAddress, param)); + + Span guard = stackalloc byte[0x10]; + guard.Fill(0xA5); + Assert.True(memory.TryWrite(MemorySizeAddress, guard)); + Assert.True(memory.TryWrite(StaleThirdArgument, guard)); + + ctx[CpuRegister.Rdi] = ParamAddress; + ctx[CpuRegister.Rsi] = MemorySizeAddress; + ctx[CpuRegister.Rdx] = StaleThirdArgument; + ctx[CpuRegister.Rsp] = StackPointer; + ctx[CpuRegister.Rbp] = StackPointer + 0x48; + + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextQueryMemory(ctx)); + + Span output = stackalloc byte[0x10]; + Assert.True(memory.TryRead(MemorySizeAddress, output)); + Assert.Equal(0x11640UL, BinaryPrimitives.ReadUInt64LittleEndian(output)); + Assert.Equal(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5 }, output[8..].ToArray()); + + Span stale = stackalloc byte[0x10]; + Assert.True(memory.TryRead(StaleThirdArgument, stale)); + Assert.Equal(guard.ToArray(), stale.ToArray()); + } + + [Fact] + public void ContextQueryMemory_RejectsMissingSecondArgument() + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var ctx = new CpuContext(memory, Generation.Gen5); + + ctx[CpuRegister.Rdi] = ParamAddress; + ctx[CpuRegister.Rsi] = 0; + ctx[CpuRegister.Rdx] = StaleThirdArgument; + + Assert.Equal( + InvalidPointer, + AudioOut2Exports.AudioOut2ContextQueryMemory(ctx)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs new file mode 100644 index 0000000..457fc7c --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2MasteringExportsTests.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Audio; +using Xunit; + +namespace SharpEmu.Libs.Tests.Audio; + +public sealed class AudioOut2MasteringExportsTests +{ + [Fact] + public void MasteringSetParam_AcceptsOpaqueParameters() + { + var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000); + var ctx = new CpuContext(memory, Generation.Gen5); + ctx[CpuRegister.Rdi] = 0x1_0000_0100; + ctx[CpuRegister.Rsi] = 2; + ctx[CpuRegister.Rdx] = 1; + ctx[CpuRegister.Rax] = ulong.MaxValue; + + var result = AudioOut2Exports.AudioOut2MasteringSetParam(ctx); + + Assert.Equal(0, result); + Assert.Equal(0UL, ctx[CpuRegister.Rax]); + } + + [Fact] + public void MasteringSetParam_RegistersForGen5() + { + var manager = new ModuleManager(); + manager.RegisterExports( + SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + + Assert.True(manager.TryGetExport("v8iOE+j8a5o", out var export)); + Assert.Equal("sceAudioOut2MasteringSetParam", export.Name); + Assert.Equal("libSceAudioOut2", export.LibraryName); + Assert.Equal(Generation.Gen5, export.Target); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs new file mode 100644 index 0000000..8a79ae7 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PcmSnapshotTests.cs @@ -0,0 +1,255 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.HLE.Host; +using SharpEmu.Libs.Audio; +using Xunit; + +namespace SharpEmu.Libs.Tests.Audio; + +[Collection(AudioOutStateCollection.Name)] +public sealed class AudioOut2PcmSnapshotTests : IDisposable +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong ContextParamAddress = MemoryBase + 0x100; + private const ulong ContextOutAddress = MemoryBase + 0x200; + private const ulong PortParamAddress = MemoryBase + 0x300; + private const ulong FirstPortOutAddress = MemoryBase + 0x400; + private const ulong SecondPortOutAddress = MemoryBase + 0x408; + private const ulong AttributeAddress = MemoryBase + 0x500; + private const ulong PcmAttributeValueAddress = MemoryBase + 0x600; + private const ulong ContextMemoryAddress = MemoryBase + 0x1000; + private const ulong SharedPcmAddress = MemoryBase + 0x4000; + + private readonly FakeCpuMemory _memory = new(MemoryBase, 0x10000); + private readonly CpuContext _ctx; + + public AudioOut2PcmSnapshotTests() + { + AudioOut2Exports.ResetForTests(); + _ctx = new CpuContext(_memory, Generation.Gen5); + } + + [Fact] + public void PortSetAttributes_SnapshotsPcmWhenPortsReuseGuestBuffer() + { + var context = CreateContext(grainSamples: 64); + var firstPort = CreateSigned16StereoPort(context, FirstPortOutAddress); + var secondPort = CreateSigned16StereoPort(context, SecondPortOutAddress); + WritePcmAttribute(); + + var firstPcm = Enumerable.Range(0, 64 * 2 * sizeof(short)) + .Select(static value => unchecked((byte)(value + 1))) + .ToArray(); + var secondPcm = Enumerable.Range(0, firstPcm.Length) + .Select(static value => unchecked((byte)(255 - value))) + .ToArray(); + + Assert.True(_memory.TryWrite(SharedPcmAddress, firstPcm)); + SetPcmAttribute(firstPort); + + Assert.True(_memory.TryWrite(SharedPcmAddress, secondPcm)); + SetPcmAttribute(secondPort); + + Assert.True(_memory.TryWrite(SharedPcmAddress, new byte[firstPcm.Length])); + + Assert.Equal(firstPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort)); + Assert.Equal(secondPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(secondPort)); + + Assert.Equal(firstPcm, AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort)); + Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort)); + Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort)); + + Assert.Equal(secondPcm, AudioOut2Exports.TakePortPcmSnapshotForTests(secondPort)); + Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(secondPort)); + } + + [Theory] + [InlineData((ushort)0x0100)] + [InlineData((ushort)6)] + public void ContextPush_DoesNotSubmitObjectOrVibrationPorts(ushort portType) + { + var stream = new RecordingAudioStream(); + var openCalls = 0; + AudioOut2Exports.SetStreamFactoryForTests(_ => + { + openCalls++; + return stream; + }); + + var context = CreateContext(grainSamples: 64); + var port = CreateSigned16StereoPort(context, FirstPortOutAddress, portType); + WritePcmAttribute(); + var pcm = Enumerable.Range(0, 64 * 2 * sizeof(short)) + .Select(static value => unchecked((byte)(value + 1))) + .ToArray(); + Assert.True(_memory.TryWrite(SharedPcmAddress, pcm)); + SetPcmAttribute(port); + + _ctx[CpuRegister.Rdi] = context; + _ctx[CpuRegister.Rsi] = 1; + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx)); + + Assert.Equal(0, openCalls); + Assert.Empty(stream.Submissions); + Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(port)); + } + + [Fact] + public void ContextPush_SubmitsDevicePortSnapshotOnlyOnce() + { + var stream = new RecordingAudioStream(); + AudioOut2Exports.SetStreamFactoryForTests(_ => stream); + + var context = CreateContext(grainSamples: 64); + var port = CreateSigned16StereoPort(context, FirstPortOutAddress); + WritePcmAttribute(); + Assert.True(_memory.TryWrite( + SharedPcmAddress, + Enumerable.Range(0, 64 * 2 * sizeof(short)) + .Select(static value => unchecked((byte)(value + 1))) + .ToArray())); + SetPcmAttribute(port); + + _ctx[CpuRegister.Rdi] = context; + _ctx[CpuRegister.Rsi] = 1; + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx)); + Assert.Single(stream.Submissions); + Assert.Equal(64 * 2 * sizeof(short), stream.Submissions[0].Length); + + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextPush(_ctx)); + Assert.Single(stream.Submissions); + } + + [Fact] + public void ContextAdvance_SubmitsDevicePortSnapshotOnlyOnce() + { + var stream = new RecordingAudioStream(); + AudioOut2Exports.SetStreamFactoryForTests(_ => stream); + + var context = CreateContext(grainSamples: 64); + var port = CreateSigned16StereoPort(context, FirstPortOutAddress); + WritePcmAttribute(); + Assert.True(_memory.TryWrite( + SharedPcmAddress, + Enumerable.Range(0, 64 * 2 * sizeof(short)) + .Select(static value => unchecked((byte)(value + 1))) + .ToArray())); + SetPcmAttribute(port); + + _ctx[CpuRegister.Rdi] = context; + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextAdvance(_ctx)); + Assert.Single(stream.Submissions); + Assert.Equal(64 * 2 * sizeof(short), stream.Submissions[0].Length); + + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextAdvance(_ctx)); + Assert.Single(stream.Submissions); + } + + [Fact] + public void ContextDestroy_RemovesOwnedPortsAndPendingSnapshots() + { + var firstContext = CreateContext(grainSamples: 64); + var secondContext = CreateContext(grainSamples: 64); + var firstPort = CreateSigned16StereoPort(firstContext, FirstPortOutAddress); + var secondPort = CreateSigned16StereoPort(secondContext, SecondPortOutAddress); + WritePcmAttribute(); + + byte[] firstPcm = new byte[64 * 2 * sizeof(short)]; + byte[] secondPcm = Enumerable.Repeat((byte)0x5A, firstPcm.Length).ToArray(); + Assert.True(_memory.TryWrite(SharedPcmAddress, firstPcm)); + SetPcmAttribute(firstPort); + Assert.True(_memory.TryWrite(SharedPcmAddress, secondPcm)); + SetPcmAttribute(secondPort); + + _ctx[CpuRegister.Rdi] = firstContext; + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextDestroy(_ctx)); + + Assert.False(AudioOut2Exports.HasPortForTests(firstPort)); + Assert.Null(AudioOut2Exports.GetPortPcmSnapshotForTests(firstPort)); + Assert.Null(AudioOut2Exports.TakePortPcmSnapshotForTests(firstPort)); + Assert.True(AudioOut2Exports.HasPortForTests(secondPort)); + Assert.Equal(secondPcm, AudioOut2Exports.GetPortPcmSnapshotForTests(secondPort)); + } + + public void Dispose() => AudioOut2Exports.ResetForTests(); + + private ulong CreateContext(uint grainSamples) + { + Span param = stackalloc byte[0x40]; + param.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 2); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x10..], grainSamples); + Assert.True(_memory.TryWrite(ContextParamAddress, param)); + + _ctx[CpuRegister.Rdi] = ContextParamAddress; + _ctx[CpuRegister.Rsi] = ContextMemoryAddress; + _ctx[CpuRegister.Rdx] = 0x2000; + _ctx[CpuRegister.Rcx] = ContextOutAddress; + Assert.Equal(0, AudioOut2Exports.AudioOut2ContextCreate(_ctx)); + return ReadUInt64(ContextOutAddress); + } + + private ulong CreateSigned16StereoPort(ulong context, ulong outAddress, ushort portType = 0) + { + Span param = stackalloc byte[0x40]; + param.Clear(); + BinaryPrimitives.WriteUInt16LittleEndian(param, portType); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 0x201); + BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000); + Assert.True(_memory.TryWrite(PortParamAddress, param)); + + _ctx[CpuRegister.Rdi] = context; + _ctx[CpuRegister.Rsi] = PortParamAddress; + _ctx[CpuRegister.Rdx] = outAddress; + _ctx[CpuRegister.Rcx] = 0; + Assert.Equal(0, AudioOut2Exports.AudioOut2PortCreate(_ctx)); + return ReadUInt64(outAddress); + } + + private void WritePcmAttribute() + { + Span pcm = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(pcm, SharedPcmAddress); + Assert.True(_memory.TryWrite(PcmAttributeValueAddress, pcm)); + + Span attribute = stackalloc byte[0x18]; + attribute.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(attribute, 0); + BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x08..], PcmAttributeValueAddress); + BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x10..], 8); + Assert.True(_memory.TryWrite(AttributeAddress, attribute)); + } + + private void SetPcmAttribute(ulong port) + { + _ctx[CpuRegister.Rdi] = port; + _ctx[CpuRegister.Rsi] = AttributeAddress; + _ctx[CpuRegister.Rdx] = 1; + Assert.Equal(0, AudioOut2Exports.AudioOut2PortSetAttributes(_ctx)); + } + + private ulong ReadUInt64(ulong address) + { + Span value = stackalloc byte[8]; + Assert.True(_memory.TryRead(address, value)); + return BinaryPrimitives.ReadUInt64LittleEndian(value); + } + + private sealed class RecordingAudioStream : IHostAudioStream + { + public List Submissions { get; } = []; + + public bool Submit(ReadOnlySpan stereoPcm16) + { + Submissions.Add(stereoPcm16.ToArray()); + return true; + } + + public void Dispose() + { + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs index 0119875..d3ab7b5 100644 --- a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs @@ -42,9 +42,12 @@ public sealed class AudioOut2PortGetStateExportsTests // SceAudioOut2PortState: output@0, numChannels@2, volume@4. Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state)); Assert.Equal(2, state[2]); - Assert.Equal(-1, BinaryPrimitives.ReadInt16LittleEndian(state[4..])); + Assert.Equal(0, state[3]); + Assert.Equal(127, BinaryPrimitives.ReadInt16LittleEndian(state[4..])); + Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(state[6..])); Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[8..])); - // sizeof(SceAudioOut2PortState) is 0x40; nothing past it may be touched. + Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(state[0x0C..])); + Assert.All(state[0x10..0x40].ToArray(), value => Assert.Equal(0, value)); Assert.Equal(0xAB, state[0x40]); Assert.Equal(0xAB, state[0x7F]); } @@ -102,15 +105,19 @@ public sealed class AudioOut2PortGetStateExportsTests Assert.Equal(0, result); Span info = stackalloc byte[0x80]; Assert.True(memory.TryRead(StateAddress, info)); - // type@0 = SPEAKER_TYPE_TV, availableBits@4 = FRONT_LEFT | FRONT_RIGHT. Assert.Equal(0, info[0]); - Assert.Equal(3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..])); + Assert.Equal(0, info[1]); + Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[2..])); + Assert.Equal(0x3u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..])); Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[8..])); - // aSpeakerAngle[0..1] = { -30, 0 }, { +30, 0 } degrees. + Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(info[0x0C..])); Assert.Equal(-30, BinaryPrimitives.ReadInt16LittleEndian(info[0x10..])); + Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x12..])); Assert.Equal(30, BinaryPrimitives.ReadInt16LittleEndian(info[0x14..])); - // sizeof(SceAudioOut2SpeakerInfo) is 0x50. + Assert.Equal(0, BinaryPrimitives.ReadInt16LittleEndian(info[0x16..])); + Assert.All(info[0x18..0x50].ToArray(), value => Assert.Equal(0, value)); Assert.Equal(0xCD, info[0x50]); + Assert.Equal(0xCD, info[0x7F]); } [Fact] diff --git a/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs new file mode 100644 index 0000000..9701ee8 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerControlTests.cs @@ -0,0 +1,54 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.AvPlayer; +using SharpEmu.Libs.Tests.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.AvPlayer; + +[Collection(KernelMemoryCompatStateCollection.Name)] +public sealed class AvPlayerControlTests : IDisposable +{ + private const int InvalidParameters = unchecked((int)0x806A0001); + private const ulong Handle = 0xA0_0000_2400; + private readonly CpuContext _context = new( + new FakeCpuMemory(0x1_0000_0000, 0x1000), + Generation.Gen5); + + public AvPlayerControlTests() + { + AvPlayerExports.RegisterPlayerForTest( + Handle, + width: 16, + height: 16, + durationMilliseconds: 1000); + _context[CpuRegister.Rdi] = Handle; + } + + [Fact] + public void AvailableBandwidthAcceptsAValidPlayerWithoutAStreamingSource() + { + _context[CpuRegister.Rsi] = 0; + + Assert.Equal(0, AvPlayerExports.AvPlayerSetAvailableBandwidth(_context)); + Assert.Equal(0UL, _context[CpuRegister.Rax]); + } + + [Fact] + public void AvailableBandwidthRejectsAnUnknownPlayer() + { + _context[CpuRegister.Rdi] = Handle + 1; + _context[CpuRegister.Rsi] = 25_000_000; + + Assert.Equal( + InvalidParameters, + AvPlayerExports.AvPlayerSetAvailableBandwidth(_context)); + Assert.Equal( + unchecked((ulong)InvalidParameters), + _context[CpuRegister.Rax]); + } + + public void Dispose() => AvPlayerExports.RemovePlayerForTest(Handle); +} diff --git a/tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs b/tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs new file mode 100644 index 0000000..f5bc4a6 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/CommonDialog/WebBrowserDialogExportsTests.cs @@ -0,0 +1,65 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.CommonDialog; +using Xunit; + +namespace SharpEmu.Libs.Tests.CommonDialog; + +[CollectionDefinition("WebBrowserDialog", DisableParallelization = true)] +public sealed class WebBrowserDialogCollection; + +[Collection("WebBrowserDialog")] +public sealed class WebBrowserDialogExportsTests : IDisposable +{ + private const ulong MemoryBase = 0x1_0000_0000; + private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1000); + private readonly CpuContext _context; + + public WebBrowserDialogExportsTests() + { + WebBrowserDialogExports.ResetForTests(); + _context = new CpuContext(_memory, Generation.Gen5); + } + + public void Dispose() => WebBrowserDialogExports.ResetForTests(); + + [Fact] + public void HeadlessLifecycle_CompletesOnFirstStatusPoll() + { + Assert.Equal(WebBrowserDialogExports.StatusNone, + WebBrowserDialogExports.WebBrowserDialogGetStatus(_context)); + Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogInitialize(_context)); + Assert.Equal(WebBrowserDialogExports.StatusInitialized, + WebBrowserDialogExports.WebBrowserDialogGetStatus(_context)); + + _context[CpuRegister.Rdi] = MemoryBase + 0x100; + Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogOpen(_context)); + Assert.Equal(WebBrowserDialogExports.StatusRunning, WebBrowserDialogExports.StatusForTests); + Assert.Equal(WebBrowserDialogExports.StatusFinished, + WebBrowserDialogExports.WebBrowserDialogGetStatus(_context)); + + var resultAddress = MemoryBase + 0x200; + _context[CpuRegister.Rdi] = resultAddress; + Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogGetResult(_context)); + Span result = stackalloc byte[sizeof(int)]; + Assert.True(_memory.TryRead(resultAddress, result)); + Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(result)); + + Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogTerminate(_context)); + Assert.Equal(WebBrowserDialogExports.StatusNone, + WebBrowserDialogExports.WebBrowserDialogGetStatus(_context)); + } + + [Fact] + public void Close_FinishesRunningDialogWithoutPolling() + { + Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogInitialize(_context)); + _context[CpuRegister.Rdi] = MemoryBase + 0x100; + Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogOpenForPredeterminedContent(_context)); + Assert.Equal(0, WebBrowserDialogExports.WebBrowserDialogClose(_context)); + Assert.Equal(WebBrowserDialogExports.StatusFinished, WebBrowserDialogExports.StatusForTests); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs index f51f7ea..ce5227d 100644 --- a/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs +++ b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs @@ -26,6 +26,8 @@ public sealed class JsonExportRegistrationTests ("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"), ("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"), ("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"), + ("GvGvswb0v34", "_ZN3sce4Json14InitParameter2C2Ev"), + ("W72B9ylU2JA", "_ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv"), }; private static ModuleManager CreateRegisteredManager() @@ -131,4 +133,36 @@ public sealed class JsonExportRegistrationTests Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]); Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind); } + + [Fact] + public void InitParameter2Aliases_InitializeAndWriteTheirDocumentedFields() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong parameterAddress = memoryBase + 0x100; + const ulong allocatorAddress = 0x8_0012_3000; + const ulong userDataAddress = memoryBase + 0x500; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var manager = CreateRegisteredManager(); + var ctx = new CpuContext(memory, Generation.Gen5); + Assert.True(memory.TryWrite(parameterAddress, Enumerable.Repeat((byte)0xA5, 0x28).ToArray())); + + ctx[CpuRegister.Rdi] = parameterAddress; + Assert.True(manager.TryDispatch("GvGvswb0v34", ctx, out var constructorResult)); + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, constructorResult); + + var parameter = new byte[0x28]; + Assert.True(memory.TryRead(parameterAddress, parameter)); + Assert.All(parameter, value => Assert.Equal(0, value)); + + ctx[CpuRegister.Rdi] = parameterAddress; + ctx[CpuRegister.Rsi] = allocatorAddress; + ctx[CpuRegister.Rdx] = userDataAddress; + Assert.True(manager.TryDispatch("W72B9ylU2JA", ctx, out var allocatorResult)); + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, allocatorResult); + + Assert.True(memory.TryRead(parameterAddress, parameter)); + Assert.Equal(allocatorAddress, BitConverter.ToUInt64(parameter, 0)); + Assert.Equal(userDataAddress, BitConverter.ToUInt64(parameter, 8)); + Assert.All(parameter[0x10..], value => Assert.Equal(0, value)); + } } diff --git a/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs b/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs index 5da305c..0c6acd1 100644 --- a/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Network/Http2ExportsTests.cs @@ -72,4 +72,276 @@ public sealed class Http2ExportsTests Assert.Equal(unchecked((int)0x817B106A), Http2Exports.Http2CreateTemplate(ctx)); } + + [Fact] + public void SetMinSslVersion_AcceptsLiveIdsAndRejectsDeletedIds() + { + Http2Exports.ResetRuntimeState(); + var memory = new FakeCpuMemory(MemoryBase, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + + ctx[CpuRegister.Rdi] = 1; + ctx[CpuRegister.Rsi] = 2; + ctx[CpuRegister.Rdx] = 0x1000; + ctx[CpuRegister.Rcx] = 2; + Assert.Equal(0, Http2Exports.Http2Init(ctx)); + var contextId = ctx[CpuRegister.Rax]; + + ctx[CpuRegister.Rdi] = contextId; + ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x100, "test"); + ctx[CpuRegister.Rdx] = 3; + ctx[CpuRegister.Rcx] = 0; + Assert.True(Http2Exports.Http2CreateTemplate(ctx) > 0); + var templateId = ctx[CpuRegister.Rax]; + + ctx[CpuRegister.Rdi] = templateId; + ctx[CpuRegister.Rsi] = 4; + Assert.Equal(0, Http2Exports.Http2SetMinSslVersion(ctx)); + + ctx[CpuRegister.Rdi] = templateId; + ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x200, "GET"); + ctx[CpuRegister.Rdx] = memory.WriteCString(MemoryBase + 0x300, "https://example.invalid/"); + ctx[CpuRegister.Rcx] = 0; + Assert.True(Http2Exports.Http2CreateRequestWithUrl(ctx) > 0); + var requestId = ctx[CpuRegister.Rax]; + + ctx[CpuRegister.Rdi] = requestId; + ctx[CpuRegister.Rsi] = 4; + Assert.Equal(0, Http2Exports.Http2SetMinSslVersion(ctx)); + + ctx[CpuRegister.Rdi] = templateId; + Assert.Equal(0, Http2Exports.Http2DeleteTemplate(ctx)); + Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetMinSslVersion(ctx)); + + ctx[CpuRegister.Rdi] = requestId; + Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetMinSslVersion(ctx)); + } + + [Fact] + public void SetMinSslVersion_RegistersPs5Nid() + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + + Assert.True(manager.TryGetExport("09tk+kIA1Ns", out var export)); + Assert.Equal("sceHttp2SetMinSslVersion", export.Name); + Assert.Equal("libSceHttp2", export.LibraryName); + } + + [Fact] + public void Options_AreStoredInheritedAndIndependentlyUpdated() + { + var (memory, ctx, templateId) = CreateTemplate(); + + ctx[CpuRegister.Rdi] = unchecked((uint)templateId); + ctx[CpuRegister.Rsi] = 0x0F; + Assert.Equal(0, Http2Exports.Http2SslEnableOption(ctx)); + + ctx[CpuRegister.Rsi] = 0x05; + Assert.Equal(0, Http2Exports.Http2SslDisableOption(ctx)); + + ctx[CpuRegister.Rsi] = 0x1234_5678; + ctx[CpuRegister.Rdx] = 0x8765_4321; + Assert.Equal(0, Http2Exports.Http2SetRedirectCallback(ctx)); + + ctx[CpuRegister.Rsi] = 0; + Assert.Equal(0, Http2Exports.Http2SetAutoRedirect(ctx)); + + ctx[CpuRegister.Rsi] = 101; + Assert.Equal(0, Http2Exports.Http2SetConnectTimeOut(ctx)); + ctx[CpuRegister.Rsi] = 202; + Assert.Equal(0, Http2Exports.Http2SetRecvTimeOut(ctx)); + ctx[CpuRegister.Rsi] = 303; + Assert.Equal(0, Http2Exports.Http2SetSendTimeOut(ctx)); + + ctx[CpuRegister.Rsi] = 7; + Assert.Equal(0, Http2Exports.Http2SetAuthEnabled(ctx)); + + ctx[CpuRegister.Rsi] = 0x1111_2222; + ctx[CpuRegister.Rdx] = 0x3333_4444; + Assert.Equal(0, Http2Exports.Http2SetSslCallback(ctx)); + + Assert.True(Http2Exports.TryGetOptionsForTests(templateId, out var templateOptions)); + Assert.Equal(0x0Au, templateOptions.SslOptions); + Assert.Equal(0x1234_5678UL, templateOptions.RedirectCallback); + Assert.Equal(0x8765_4321UL, templateOptions.RedirectUserArgument); + Assert.False(templateOptions.AutoRedirect); + Assert.Equal(101u, templateOptions.ConnectTimeoutMicroseconds); + Assert.Equal(202u, templateOptions.ReceiveTimeoutMicroseconds); + Assert.Equal(303u, templateOptions.SendTimeoutMicroseconds); + Assert.True(templateOptions.AuthEnabled); + Assert.Equal(0x1111_2222UL, templateOptions.SslCallback); + Assert.Equal(0x3333_4444UL, templateOptions.SslUserArgument); + + var requestId = CreateRequest(memory, ctx, templateId); + Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var inheritedOptions)); + Assert.Equal(templateOptions, inheritedOptions); + + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + ctx[CpuRegister.Rsi] = 404; + Assert.Equal(0, Http2Exports.Http2SetRecvTimeOut(ctx)); + Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var requestOptions)); + Assert.Equal(404u, requestOptions.ReceiveTimeoutMicroseconds); + Assert.True(Http2Exports.TryGetOptionsForTests(templateId, out var unchangedTemplateOptions)); + Assert.Equal(202u, unchangedTemplateOptions.ReceiveTimeoutMicroseconds); + } + + [Fact] + public void OptionSetters_RejectUnknownHandles() + { + Http2Exports.ResetRuntimeState(); + var ctx = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + ctx[CpuRegister.Rdi] = 0x7FFF_FFFF; + ctx[CpuRegister.Rsi] = 1; + ctx[CpuRegister.Rdx] = 2; + + Func[] setters = + [ + Http2Exports.Http2SslDisableOption, + Http2Exports.Http2SslEnableOption, + Http2Exports.Http2SetRedirectCallback, + Http2Exports.Http2SetAutoRedirect, + Http2Exports.Http2SetCookieBox, + Http2Exports.Http2SetConnectTimeOut, + Http2Exports.Http2SetRecvTimeOut, + Http2Exports.Http2SetSendTimeOut, + Http2Exports.Http2SetAuthEnabled, + Http2Exports.Http2SetSslCallback, + ]; + + foreach (var setter in setters) + { + Assert.Equal(unchecked((int)0x817B1100), setter(ctx)); + } + } + + [Fact] + public void AddRequestHeader_ValidatesPointersAndTracksGuestStrings() + { + var (memory, ctx, templateId) = CreateTemplate(); + var requestId = CreateRequest(memory, ctx, templateId); + var nameAddress = memory.WriteCString(MemoryBase + 0x500, "X-SharpEmu-Test"); + var valueAddress = memory.WriteCString(MemoryBase + 0x600, "enabled"); + + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + ctx[CpuRegister.Rsi] = nameAddress; + ctx[CpuRegister.Rdx] = valueAddress; + ctx[CpuRegister.Rcx] = 1; + Assert.Equal(0, Http2Exports.Http2AddRequestHeader(ctx)); + + var header = Assert.Single(Http2Exports.GetRequestHeadersForTests(requestId)); + Assert.Equal("X-SharpEmu-Test", header.Name); + Assert.Equal("enabled", header.Value); + Assert.Equal(1u, header.Mode); + + ctx[CpuRegister.Rsi] = 0; + Assert.Equal(unchecked((int)0x817B1225), Http2Exports.Http2AddRequestHeader(ctx)); + + ctx[CpuRegister.Rsi] = MemoryBase + 0x3000; + Assert.Equal(unchecked((int)0x817B11FE), Http2Exports.Http2AddRequestHeader(ctx)); + + ctx[CpuRegister.Rdi] = 0x7FFF_FFFF; + ctx[CpuRegister.Rsi] = nameAddress; + Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2AddRequestHeader(ctx)); + } + + [Fact] + public void CookieBox_IsCreatedBoundInheritedAndCanBeDetached() + { + var (memory, ctx, templateId) = CreateTemplate(); + + ctx[CpuRegister.Rdi] = 0x80000; // Deliberately stale sceHttp2Init register state. + Assert.Equal(0x3001, Http2Exports.Http2CreateCookieBox(ctx)); + var cookieBoxId = unchecked((int)ctx[CpuRegister.Rax]); + + ctx[CpuRegister.Rdi] = unchecked((uint)templateId); + ctx[CpuRegister.Rsi] = unchecked((uint)cookieBoxId); + Assert.Equal(0, Http2Exports.Http2SetCookieBox(ctx)); + Assert.True(Http2Exports.TryGetOptionsForTests(templateId, out var templateOptions)); + Assert.Equal(cookieBoxId, templateOptions.CookieBoxId); + + var requestId = CreateRequest(memory, ctx, templateId); + Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var inheritedOptions)); + Assert.Equal(cookieBoxId, inheritedOptions.CookieBoxId); + + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + ctx[CpuRegister.Rsi] = 0; + Assert.Equal(0, Http2Exports.Http2SetCookieBox(ctx)); + Assert.True(Http2Exports.TryGetOptionsForTests(requestId, out var detachedOptions)); + Assert.Equal(0, detachedOptions.CookieBoxId); + + ctx[CpuRegister.Rsi] = 0x7FFF_FFFF; + Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetCookieBox(ctx)); + } + + [Fact] + public void SetRequestContentLength_UpdatesOnlyLiveRequests() + { + var (memory, ctx, templateId) = CreateTemplate(); + var requestId = CreateRequest(memory, ctx, templateId); + + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + ctx[CpuRegister.Rsi] = 0x1_0000_0042; + Assert.Equal(0, Http2Exports.Http2SetRequestContentLength(ctx)); + Assert.True(Http2Exports.TryGetRequestContentLengthForTests(requestId, out var contentLength)); + Assert.Equal(0x1_0000_0042UL, contentLength); + + ctx[CpuRegister.Rdi] = 0x7FFF_FFFF; + Assert.Equal(unchecked((int)0x817B1100), Http2Exports.Http2SetRequestContentLength(ctx)); + } + + [Theory] + [InlineData("B37SruheQ5Y", "sceHttp2SslDisableOption")] + [InlineData("EWcwMpbr5F8", "sceHttp2SslEnableOption")] + [InlineData("BJgi0CH7al4", "sceHttp2SetRedirectCallback")] + [InlineData("b9AvoIaOuHI", "sceHttp2SetAutoRedirect")] + [InlineData("N4UfjvWJsMw", "sceHttp2CreateCookieBox")] + [InlineData("jrVHsKCXA0g", "sceHttp2SetCookieBox")] + [InlineData("FSAFOzi0FpM", "sceHttp2SetRequestContentLength")] + [InlineData("-HIO4VT87v8", "sceHttp2SetConnectTimeOut")] + [InlineData("izvHhqgDt44", "sceHttp2SetRecvTimeOut")] + [InlineData("XPtW45xiLHk", "sceHttp2SetSendTimeOut")] + [InlineData("jjFahkBPCYs", "sceHttp2SetAuthEnabled")] + [InlineData("nrPfOE8TQu0", "sceHttp2AddRequestHeader")] + [InlineData("YrWX+DhPHQY", "sceHttp2SetSslCallback")] + public void CompatibilityExports_RegisterPs5Nids(string nid, string name) + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + + Assert.True(manager.TryGetExport(nid, out var export)); + Assert.Equal(name, export.Name); + Assert.Equal("libSceHttp2", export.LibraryName); + } + + private static (FakeCpuMemory Memory, CpuContext Context, int TemplateId) CreateTemplate() + { + Http2Exports.ResetRuntimeState(); + var memory = new FakeCpuMemory(MemoryBase, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + + ctx[CpuRegister.Rdi] = 1; + ctx[CpuRegister.Rsi] = 2; + ctx[CpuRegister.Rdx] = 0x1000; + ctx[CpuRegister.Rcx] = 2; + Assert.Equal(0, Http2Exports.Http2Init(ctx)); + var contextId = unchecked((int)ctx[CpuRegister.Rax]); + + ctx[CpuRegister.Rdi] = unchecked((uint)contextId); + ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x100, "test"); + ctx[CpuRegister.Rdx] = 3; + ctx[CpuRegister.Rcx] = 0; + Assert.True(Http2Exports.Http2CreateTemplate(ctx) > 0); + return (memory, ctx, unchecked((int)ctx[CpuRegister.Rax])); + } + + private static int CreateRequest(FakeCpuMemory memory, CpuContext ctx, int templateId) + { + ctx[CpuRegister.Rdi] = unchecked((uint)templateId); + ctx[CpuRegister.Rsi] = memory.WriteCString(MemoryBase + 0x200, "GET"); + ctx[CpuRegister.Rdx] = memory.WriteCString(MemoryBase + 0x300, "https://example.invalid/"); + ctx[CpuRegister.Rcx] = 0; + Assert.True(Http2Exports.Http2CreateRequestWithUrl(ctx) > 0); + return unchecked((int)ctx[CpuRegister.Rax]); + } } diff --git a/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs b/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs index 970d39c..4ae3e75 100644 --- a/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs @@ -44,11 +44,18 @@ public sealed class SaveDataExportsTests : IDisposable private const ulong SearchResult = Base + 0x1140; private const ulong SearchDirNames = Base + 0x1180; private const ulong SearchInfos = Base + 0x1200; + private const ulong BackupParam = Base + 0x1300; + private const ulong BackupTitleId = Base + 0x1380; + private const ulong BackupDirName = Base + 0x13C0; + private const ulong Unmapped = Base + 0x2_0000; private const int NoEvent = unchecked((int)0x809F0008); private const int ParameterError = unchecked((int)0x809F0000); + private const int MemoryFault = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; private const int ResourceBusy = unchecked((int)0x809F001B); private const uint MountModeCreate = 1u << 2; + private const string BackupTitle = "PPSA10067"; + private const string BackupDirectory = "SLOT_WITH_32_CHARACTERS_12345678"; private readonly FakeCpuMemory _memory = new(Base, 0x10000); private readonly CpuContext _ctx; @@ -121,6 +128,65 @@ public sealed class SaveDataExportsTests : IDisposable Assert.Equal(ParameterError, SaveDataExports.SaveDataGetEventResult(Reg(rsi: 0))); } + [Fact] + public void Backup_QueuesCompleteBackupEndEvent_ThenDrains() + { + WriteAscii(BackupTitleId, BackupTitle); + WriteAscii(BackupDirName, BackupDirectory); + WriteBackupParam(BackupTitleId, BackupDirName); + + Assert.Equal(0, SaveDataExports.SaveDataBackup(Reg(rdi: BackupParam))); + + var initialized = new byte[0x70]; + Array.Fill(initialized, (byte)0xCC); + Assert.True(_memory.TryWrite(EventOut, initialized)); + Assert.Equal(0, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut))); + + var ev = new byte[0x70]; + Assert.True(_memory.TryRead(EventOut, ev)); + Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(ev)); + Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(ev.AsSpan(0x04))); + Assert.Equal(UserId, BinaryPrimitives.ReadInt32LittleEndian(ev.AsSpan(0x08))); + Assert.Equal(BackupTitle, Encoding.ASCII.GetString(ev, 0x10, 0x10).TrimEnd('\0')); + Assert.Equal(BackupDirectory, Encoding.ASCII.GetString(ev, 0x20, 0x20).TrimEnd('\0')); + Assert.All(ev[0x1A..0x20], value => Assert.Equal((byte)0, value)); + Assert.All(ev[0x40..0x68], value => Assert.Equal((byte)0, value)); + Assert.All(ev[0x68..0x70], value => Assert.Equal((byte)0xCC, value)); + Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut))); + } + + [Fact] + public void Backup_NullParameterOrDirectory_ReturnsParameterWithoutEvent() + { + Assert.Equal(ParameterError, SaveDataExports.SaveDataBackup(Reg(rdi: 0))); + + WriteBackupParam(titleIdAddress: 0, dirNameAddress: 0); + Assert.Equal(ParameterError, SaveDataExports.SaveDataBackup(Reg(rdi: BackupParam))); + Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut))); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Backup_InvalidNestedPointer_ReturnsMemoryFaultWithoutEvent(bool invalidTitle) + { + WriteAscii(BackupTitleId, BackupTitle); + WriteAscii(BackupDirName, BackupDirectory); + WriteBackupParam( + invalidTitle ? Unmapped : BackupTitleId, + invalidTitle ? BackupDirName : Unmapped); + + Assert.Equal(MemoryFault, SaveDataExports.SaveDataBackup(Reg(rdi: BackupParam))); + Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut))); + } + + [Fact] + public void Backup_InvalidStructurePointer_ReturnsMemoryFaultWithoutEvent() + { + Assert.Equal(MemoryFault, SaveDataExports.SaveDataBackup(Reg(rdi: Unmapped))); + Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut))); + } + [Fact] public void SyncSaveDataMemory_PostsSyncEndEvent_DrainedOnce() { @@ -145,6 +211,11 @@ public sealed class SaveDataExportsTests : IDisposable Assert.True(_ctx.TryReadInt32(EventOut + 0x04, out var errorCode)); Assert.Equal(0, errorCode); + var ev = new byte[0x68]; + Assert.True(_memory.TryRead(EventOut, ev)); + Assert.Equal(TitleId[..10], Encoding.ASCII.GetString(ev, 0x10, 0x10).TrimEnd('\0')); + Assert.Equal("sce_sdmemory", Encoding.ASCII.GetString(ev, 0x20, 0x20).TrimEnd('\0')); + Assert.Equal(NoEvent, SaveDataExports.SaveDataGetEventResult(Reg(rsi: EventOut))); } @@ -397,4 +468,14 @@ public sealed class SaveDataExportsTests : IDisposable 0, SaveDataExports.SaveDataPrepare(Reg(rdi: MountPointStr, rsi: PrepareParam))); } + + private void WriteBackupParam(ulong titleIdAddress, ulong dirNameAddress) + { + Span backup = stackalloc byte[0x40]; + backup.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(backup, UserId); + BinaryPrimitives.WriteUInt64LittleEndian(backup[0x08..], titleIdAddress); + BinaryPrimitives.WriteUInt64LittleEndian(backup[0x10..], dirNameAddress); + Assert.True(_memory.TryWrite(BackupParam, backup)); + } }