diff --git a/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs index 5ea7a84f..8672031e 100644 --- a/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs +++ b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs @@ -17,7 +17,9 @@ public static class AvPlayerExports private const int FrameBufferCount = 3; private const int FrameInfoSize = 40; private const int FrameInfoExSize = 104; - private const int StreamInfoSize = 40; + // This structure is 32 bytes. A larger write can damage the guest stack. + private const int StreamInfoSize = 32; + private const int StreamInfoExSize = 32; private const int MaxGuestPathLength = 4096; private static readonly object StateGate = new(); private static readonly Dictionary Players = new(); @@ -404,7 +406,8 @@ public static class AvPlayerExports ExportName = "sceAvPlayerGetStreamInfoEx", Target = Generation.Gen5, LibraryName = "libSceAvPlayer")] - public static int AvPlayerSetDecoderMode(CpuContext ctx) => ValidatePlayer(ctx); + public static int AvPlayerGetStreamInfoEx(CpuContext ctx) => + GetStreamInfoCore(ctx, StreamInfoExSize); [SysAbiExport( Nid = "XC9wM+xULz8", @@ -561,12 +564,48 @@ public static class AvPlayerExports } } + internal static void RegisterPlayerForTest( + ulong handle, + int width, + int height, + ulong durationMilliseconds) + { + PlayerState? previous; + lock (StateGate) + { + Players.Remove(handle, out previous); + Players[handle] = new PlayerState + { + Handle = handle, + Width = width, + Height = height, + DurationMilliseconds = durationMilliseconds, + }; + } + + previous?.Dispose(); + } + + internal static void RemovePlayerForTest(ulong handle) + { + PlayerState? player; + lock (StateGate) + { + Players.Remove(handle, out player); + } + + player?.Dispose(); + } + [SysAbiExport( Nid = "d8FcbzfAdQw", ExportName = "sceAvPlayerGetStreamInfo", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAvPlayer")] - public static int AvPlayerGetStreamInfo(CpuContext ctx) + public static int AvPlayerGetStreamInfo(CpuContext ctx) => + GetStreamInfoCore(ctx, StreamInfoSize); + + private static int GetStreamInfoCore(CpuContext ctx, int infoSize) { var streamIndex = unchecked((uint)ctx[CpuRegister.Rsi]); var infoAddress = ctx[CpuRegister.Rdx]; @@ -578,7 +617,7 @@ public static class AvPlayerExports return SetReturn(ctx, InvalidParameters); } - Span info = stackalloc byte[StreamInfoSize]; + Span info = stackalloc byte[infoSize]; info.Clear(); BinaryPrimitives.WriteUInt32LittleEndian(info[0..], streamIndex); // 0=video, 1=audio if (streamIndex == 0) @@ -1009,7 +1048,7 @@ public static class AvPlayerExports { return false; } - var ffprobe = Path.Combine(Path.GetDirectoryName(ffmpeg) ?? string.Empty, "ffprobe"); + var ffprobe = GetFfprobePath(ffmpeg, OperatingSystem.IsWindows()); if (!File.Exists(ffprobe)) { return false; @@ -1092,13 +1131,33 @@ public static class AvPlayerExports } } - private static string? FindFfmpeg() + private static string? FindFfmpeg() => + FindFfmpeg( + Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"), + Environment.GetEnvironmentVariable("PATH"), + OperatingSystem.IsWindows()); + + internal static string? FindFfmpeg( + string? configured, + string? searchPath, + bool isWindows) { - var configured = Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"); if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured)) { return configured; } + + var executable = isWindows ? "ffmpeg.exe" : "ffmpeg"; + foreach (var directory in (searchPath ?? string.Empty) + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) + { + var candidate = Path.Combine(RemovePathQuotes(directory), executable); + if (File.Exists(candidate)) + { + return candidate; + } + } + foreach (var candidate in new[] { "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" }) { if (File.Exists(candidate)) @@ -1109,6 +1168,16 @@ public static class AvPlayerExports return null; } + internal static string GetFfprobePath(string ffmpeg, bool isWindows) => + Path.Combine( + Path.GetDirectoryName(ffmpeg) ?? string.Empty, + isWindows ? "ffprobe.exe" : "ffprobe"); + + private static string RemovePathQuotes(string directory) => + directory.Length >= 2 && directory[0] == '"' && directory[^1] == '"' + ? directory[1..^1] + : directory; + internal static string? ResolveGuestPath(string guestPath) { if (string.IsNullOrWhiteSpace(guestPath)) @@ -1118,7 +1187,9 @@ public static class AvPlayerExports var normalized = guestPath.Replace('\\', '/'); var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase); - var unrealProjectRelative = false; + var unrealProjectRelative = + normalized.StartsWith("../", StringComparison.Ordinal) || + normalized.StartsWith("./", StringComparison.Ordinal); if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) && Uri.TryCreate(normalized, UriKind.Absolute, out var uri) && uri.IsFile) @@ -1149,7 +1220,10 @@ public static class AvPlayerExports if (unrealProjectRelative) { - normalized = RemoveUnrealLeadingDotSegments(normalized); + if (!TryRemoveUnrealLeadingDotSegments(normalized, out normalized)) + { + return null; + } } var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); @@ -1233,15 +1307,20 @@ public static class AvPlayerExports } } - private static string RemoveUnrealLeadingDotSegments(string guestPath) + private static bool TryRemoveUnrealLeadingDotSegments( + string guestPath, + out string normalized) { + var removedParent = false; while (guestPath.StartsWith("../", StringComparison.Ordinal) || guestPath.StartsWith("./", StringComparison.Ordinal)) { + removedParent |= guestPath.StartsWith("../", StringComparison.Ordinal); guestPath = guestPath[(guestPath.IndexOf('/') + 1)..]; } - return guestPath; + normalized = guestPath; + return !removedParent || guestPath.Contains('/'); } private static bool TryDecodeFileReference(string encoded, out string decoded) diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 9ea4e43f..444a051f 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -1678,6 +1678,7 @@ public static partial class KernelMemoryCompatExports var count = ctx[CpuRegister.Rsi]; var idsAddress = ctx[CpuRegister.Rdx]; var sizesAddress = ctx[CpuRegister.Rcx]; + var errorIndexAddress = ctx[CpuRegister.R8]; if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024) { KernelRuntimeCompatExports.TrySetErrno(ctx, Einval); @@ -1702,12 +1703,8 @@ public static partial class KernelMemoryCompatExports var hostPath = ResolveGuestPath(guestPath); if (!TryGetAprFileSize(hostPath, out var fileSize)) { - // Per-file resolve: a missing entry gets an invalid id - // (0xFFFFFFFF, already written above) and size 0, and the batch - // CONTINUES. Aborting the whole batch on the first miss left the - // remaining paths unresolved and could stall the guest's asset - // streaming when a batch happens to include an absent (e.g. - // patch/DLC) file; the caller checks per-file id/size. + // Stop at the first miss and report its index. + // The caller can then use its normal file-open fallback. LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found"); if (sizesAddress != 0 && !TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0)) @@ -1716,7 +1713,16 @@ public static partial class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - continue; + if (errorIndexAddress != 0 && + !TryWriteUInt32Compat(ctx, errorIndexAddress, (uint)i)) + { + KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + KernelRuntimeCompatExports.TrySetErrno(ctx, 2); // ENOENT + ctx[CpuRegister.Rax] = ulong.MaxValue; + return -1; } var fileId = AmprFileRegistry.Register(guestPath, hostPath); diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 3aad12e1..3234c6ef 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -771,6 +771,13 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + if (!tryOnly && state.Type == MutexTypeAdaptiveNp && + IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId)) + { + TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + if (state.Type == MutexTypeAdaptiveNp) { var adaptiveResult = tryOnly @@ -816,6 +823,13 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + if (!tryOnly && state.Type == MutexTypeAdaptiveNp && + IsGuestTrackedSelfLock(ctx, mutexAddress, currentThreadId)) + { + TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + if (state.Type == MutexTypeAdaptiveNp) { if (tryOnly) @@ -1812,6 +1826,10 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; } + private static bool IsGuestTrackedSelfLock(CpuContext ctx, ulong mutexAddress, ulong currentThreadId) => + KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestOwner) && + guestOwner == currentThreadId; + private static bool CompleteCondWaiterLocked( PthreadCondState state, PthreadCondWaiter waiter, diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs index 68a19e73..8ef02f2d 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs @@ -90,6 +90,133 @@ public sealed class AprStreamingContractTests } } + [Fact] + public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong pathListAddress = memoryBase + 0x100; + const ulong pathAddress = memoryBase + 0x200; + const ulong idsAddress = memoryBase + 0x800; + const ulong sizesAddress = memoryBase + 0x880; + const ulong errorIndexAddress = memoryBase + 0x8F0; + var memory = new FakeCpuMemory(memoryBase, 0x4000); + var context = new CpuContext(memory, Generation.Gen5); + var missingHostPath = Path.Combine( + Path.GetTempPath(), + $"sharpemu-apr-missing-{Guid.NewGuid():N}.bin"); + memory.WriteCString(pathAddress, missingHostPath); + WriteUInt64(memory, pathListAddress, pathAddress); + + context[CpuRegister.Rdi] = pathListAddress; + context[CpuRegister.Rsi] = 1; + context[CpuRegister.Rdx] = idsAddress; + context[CpuRegister.Rcx] = sizesAddress; + context[CpuRegister.R8] = errorIndexAddress; + + Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context)); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress)); + Assert.Equal(0ul, ReadUInt64(memory, sizesAddress)); + Assert.Equal(0u, ReadUInt32(memory, errorIndexAddress)); + } + + [Fact] + public void ResolveFilepathsToIdsAndFileSizes_InvalidErrorIndex_ReturnsMemoryFault() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong pathListAddress = memoryBase + 0x100; + const ulong pathAddress = memoryBase + 0x200; + const ulong idsAddress = memoryBase + 0x800; + const ulong sizesAddress = memoryBase + 0x880; + var memory = new FakeCpuMemory(memoryBase, 0x4000); + var context = new CpuContext(memory, Generation.Gen5); + var missingHostPath = Path.Combine( + Path.GetTempPath(), + $"sharpemu-apr-missing-{Guid.NewGuid():N}.bin"); + memory.WriteCString(pathAddress, missingHostPath); + WriteUInt64(memory, pathListAddress, pathAddress); + + context[CpuRegister.Rdi] = pathListAddress; + context[CpuRegister.Rsi] = 1; + context[CpuRegister.Rdx] = idsAddress; + context[CpuRegister.Rcx] = sizesAddress; + context[CpuRegister.R8] = memoryBase + 0x5000; + + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, + KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context)); + } + + [Fact] + public void ResolveFilepathsToIdsAndFileSizes_MissingMidBatch_StopsAtFailingEntry() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong pathListAddress = memoryBase + 0x100; + const ulong idsAddress = memoryBase + 0x800; + const ulong sizesAddress = memoryBase + 0x880; + const ulong errorIndexAddress = memoryBase + 0x8F0; + byte[] fileContents = [1, 2, 3, 4, 5]; + var hostPath = Path.GetTempFileName(); + var missingHostPath = Path.Combine( + Path.GetTempPath(), + $"sharpemu-apr-missing-{Guid.NewGuid():N}.bin"); + + try + { + File.WriteAllBytes(hostPath, fileContents); + var memory = new FakeCpuMemory(memoryBase, 0x4000); + var context = new CpuContext(memory, Generation.Gen5); + memory.WriteCString(memoryBase + 0x200, hostPath); + memory.WriteCString(memoryBase + 0x400, missingHostPath); + memory.WriteCString(memoryBase + 0x600, hostPath); + WriteUInt64(memory, pathListAddress, memoryBase + 0x200); + WriteUInt64(memory, pathListAddress + 8, memoryBase + 0x400); + WriteUInt64(memory, pathListAddress + 16, memoryBase + 0x600); + WriteUInt32(memory, idsAddress + 8, 0x1234_5678); // sentinel: entry 2 untouched + WriteUInt64(memory, sizesAddress + 16, 0xDEAD); + + context[CpuRegister.Rdi] = pathListAddress; + context[CpuRegister.Rsi] = 3; + context[CpuRegister.Rdx] = idsAddress; + context[CpuRegister.Rcx] = sizesAddress; + context[CpuRegister.R8] = errorIndexAddress; + + Assert.Equal(-1, KernelMemoryCompatExports.KernelAprResolveFilepathsToIdsAndFileSizes(context)); + Assert.NotEqual(uint.MaxValue, ReadUInt32(memory, idsAddress)); + Assert.Equal((ulong)fileContents.Length, ReadUInt64(memory, sizesAddress)); + Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress + 4)); + Assert.Equal(0ul, ReadUInt64(memory, sizesAddress + 8)); + Assert.Equal(1u, ReadUInt32(memory, errorIndexAddress)); + Assert.Equal(0x1234_5678u, ReadUInt32(memory, idsAddress + 8)); + Assert.Equal(0xDEADul, ReadUInt64(memory, sizesAddress + 16)); + } + finally + { + File.Delete(hostPath); + } + } + + private static uint ReadUInt32(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(uint)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + private static ulong ReadUInt64(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(ulong)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt64LittleEndian(bytes); + } + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + Assert.True(memory.TryWrite(address, bytes)); + } + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) { Span bytes = stackalloc byte[sizeof(ulong)]; diff --git a/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerPathTests.cs b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerPathTests.cs index f35e6b79..30cb0f01 100644 --- a/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerPathTests.cs +++ b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerPathTests.cs @@ -40,6 +40,65 @@ public sealed class AvPlayerPathTests : IDisposable AssertPathIsInsideApp0(resolved); } + [Fact] + public void UnrealRelativeRawPathAnchorsAtApp0AndResolvesMedia() + { + var mediaPath = CreateFile("SampleProject/Content/Movies/Startup.mp4"); + + var resolved = AvPlayerExports.ResolveGuestPath( + "../../../SampleProject/Content/Movies/Startup.mp4"); + + Assert.NotNull(resolved); + Assert.Equal(File.ReadAllBytes(mediaPath), File.ReadAllBytes(resolved)); + AssertPathIsInsideApp0(resolved); + } + + [Fact] + public void UnrealRelativeRawPathCannotEscapeApp0() + { + var outsidePath = Path.Combine(_tempRoot, "outside.mp4"); + File.WriteAllBytes(outsidePath, [0x7F]); + CreateFile("outside.mp4"); + + Assert.Null(AvPlayerExports.ResolveGuestPath("../../../outside.mp4")); + } + + [Fact] + public void CurrentDirectoryRawPathResolvesInsideApp0() + { + var mediaPath = CreateFile("Movies/Intro.mp4"); + + var resolved = AvPlayerExports.ResolveGuestPath("./Movies/Intro.mp4"); + + Assert.NotNull(resolved); + Assert.Equal(Path.GetFullPath(mediaPath), resolved); + AssertPathIsInsideApp0(resolved); + } + + [Theory] + [InlineData(false, "ffmpeg", "ffprobe")] + [InlineData(true, "ffmpeg.exe", "ffprobe.exe")] + public void MediaToolLookupUsesPlatformNames( + bool isWindows, + string ffmpegName, + string ffprobeName) + { + var toolDirectory = Path.Combine(_tempRoot, "Media Tools"); + Directory.CreateDirectory(toolDirectory); + var ffmpeg = Path.Combine(toolDirectory, ffmpegName); + File.WriteAllBytes(ffmpeg, []); + + var resolved = AvPlayerExports.FindFfmpeg( + configured: null, + searchPath: $"\"{toolDirectory}\"", + isWindows); + + Assert.Equal(ffmpeg, resolved); + Assert.Equal( + Path.Combine(toolDirectory, ffprobeName), + AvPlayerExports.GetFfprobePath(ffmpeg, isWindows)); + } + [Fact] public void RelativeFileUriCannotEscapeApp0() { @@ -143,12 +202,14 @@ public sealed class AvPlayerPathTests : IDisposable private void AssertPathIsInsideApp0(string resolved) { - var rootWithSeparator = - Path.TrimEndingDirectorySeparator(Path.GetFullPath(_app0Root)) + - Path.DirectorySeparatorChar; - Assert.StartsWith( - rootWithSeparator, - Path.GetFullPath(resolved), - StringComparison.OrdinalIgnoreCase); + var relative = Path.GetRelativePath( + Path.GetFullPath(_app0Root), + Path.GetFullPath(resolved)); + Assert.False(Path.IsPathFullyQualified(relative)); + Assert.NotEqual("..", relative); + Assert.False( + relative.StartsWith( + ".." + Path.DirectorySeparatorChar, + StringComparison.Ordinal)); } } diff --git a/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerStreamInfoTests.cs b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerStreamInfoTests.cs new file mode 100644 index 00000000..ecb55ecb --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerStreamInfoTests.cs @@ -0,0 +1,126 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.AvPlayer; +using Xunit; + +namespace SharpEmu.Libs.Tests.AvPlayer; + +public sealed class AvPlayerStreamInfoTests +{ + private const string StreamInfoExNid = "ctTAcF5DiKQ"; + private const ulong BaseAddress = 0x1_0000_0000; + private const int MemorySize = 0x2000; + private const ulong InfoAddress = BaseAddress + 0x100; + private const ulong Handle = 0xA0_0000_0001; + private const ulong DurationMilliseconds = 0x0102_0304_0506_0708; + private const byte Sentinel = 0xAB; + + [Theory] + [InlineData(false, 0u)] + [InlineData(true, 0u)] + [InlineData(false, 1u)] + [InlineData(true, 1u)] + public void GetStreamInfoFunctionsDoNotWritePastThe32ByteStructure( + bool useExtendedFunction, + uint streamIndex) + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var context = new CpuContext(memory, Generation.Gen5); + + AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds); + try + { + Span window = stackalloc byte[40]; + window.Fill(Sentinel); + Assert.True(memory.TryWrite(InfoAddress, window)); + + context[CpuRegister.Rdi] = Handle; + context[CpuRegister.Rsi] = streamIndex; + context[CpuRegister.Rdx] = InfoAddress; + + var resultCode = useExtendedFunction + ? AvPlayerExports.AvPlayerGetStreamInfoEx(context) + : AvPlayerExports.AvPlayerGetStreamInfo(context); + Assert.Equal(0, resultCode); + + Span result = stackalloc byte[40]; + Assert.True(memory.TryRead(InfoAddress, result)); + Assert.Equal(streamIndex, BinaryPrimitives.ReadUInt32LittleEndian(result)); + if (streamIndex == 0) + { + Assert.Equal(1280u, BinaryPrimitives.ReadUInt32LittleEndian(result[8..])); + Assert.Equal(720u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..])); + } + else + { + Assert.Equal(2, BinaryPrimitives.ReadUInt16LittleEndian(result[8..])); + Assert.Equal(48_000u, BinaryPrimitives.ReadUInt32LittleEndian(result[12..])); + } + Assert.Equal(DurationMilliseconds, BinaryPrimitives.ReadUInt64LittleEndian(result[24..])); + + for (var index = 32; index < result.Length; index++) + { + Assert.Equal(Sentinel, result[index]); + } + } + finally + { + AvPlayerExports.RemovePlayerForTest(Handle); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetStreamInfoFunctionsRejectInvalidArguments(bool useExtendedFunction) + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var context = new CpuContext(memory, Generation.Gen5); + AvPlayerExports.RegisterPlayerForTest(Handle, 1280, 720, DurationMilliseconds); + + try + { + context[CpuRegister.Rdi] = Handle; + context[CpuRegister.Rsi] = 2; + context[CpuRegister.Rdx] = InfoAddress; + Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction)); + + context[CpuRegister.Rsi] = 0; + context[CpuRegister.Rdx] = 0; + Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction)); + + context[CpuRegister.Rdi] = Handle + 1; + context[CpuRegister.Rdx] = InfoAddress; + Assert.NotEqual(0, InvokeGetStreamInfo(context, useExtendedFunction)); + } + finally + { + AvPlayerExports.RemovePlayerForTest(Handle); + } + } + + [Fact] + public void StreamInfoExExportIsRegisteredForGen5Only() + { + var gen4Manager = new ModuleManager(); + gen4Manager.RegisterExports( + SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4)); + Assert.False(gen4Manager.TryGetExport(StreamInfoExNid, out _)); + + var gen5Manager = new ModuleManager(); + gen5Manager.RegisterExports( + SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + Assert.True(gen5Manager.TryGetExport(StreamInfoExNid, out var export)); + Assert.Equal("sceAvPlayerGetStreamInfoEx", export.Name); + Assert.Equal("libSceAvPlayer", export.LibraryName); + Assert.Equal(Generation.Gen5, export.Target); + } + + private static int InvokeGetStreamInfo(CpuContext context, bool useExtendedFunction) => + useExtendedFunction + ? AvPlayerExports.AvPlayerGetStreamInfoEx(context) + : AvPlayerExports.AvPlayerGetStreamInfo(context); +} diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs index 03b44c21..90a8f65a 100644 --- a/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs @@ -25,6 +25,57 @@ public sealed class PthreadMutexSemanticsTests Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); } + [Fact] + public void AdaptiveMutex_GuestTrackedSelfLockReturnsDeadlockAndSingleUnlockReleases() + { + const ulong memoryBase = 0x1_0001_0000; + const ulong mutexAddress = memoryBase + 0x100; + var memory = new AllocatingCpuMemory(memoryBase, 0x4000); + var context = new CpuContext(memory, Generation.Gen5); + Assert.True(context.TryWriteUInt64(mutexAddress, 1)); // Static adaptive initializer. + context[CpuRegister.Rdi] = mutexAddress; + + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context)); + + var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); + Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle)); + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK, + KernelPthreadCompatExports.PthreadMutexLock(context)); + + Assert.True(context.TryWriteUInt64(mutexAddress + 8, 0)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexTrylock(context)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); + } + + [Fact] + public void RecursiveMutex_GuestTrackedSelfLockKeepsRecursiveSemantics() + { + const ulong memoryBase = 0x1_0002_0000; + const ulong attrAddress = memoryBase + 0x100; + const ulong mutexAddress = memoryBase + 0x200; + var memory = new AllocatingCpuMemory(memoryBase, 0x4000); + var context = new CpuContext(memory, Generation.Gen5); + + context[CpuRegister.Rdi] = attrAddress; + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrInit(context)); + context[CpuRegister.Rsi] = 2; + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexattrSettype(context)); + + context[CpuRegister.Rdi] = mutexAddress; + context[CpuRegister.Rsi] = attrAddress; + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexInit(context)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context)); + + var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); + Assert.True(context.TryWriteUInt64(mutexAddress + 8, currentThreadHandle)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); + Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); + } + [Fact] public async Task ContendedMutex_HandsOffOneHostWaiterAtATime() {