diff --git a/src/SharpEmu.Libs/Network/HttpExports.cs b/src/SharpEmu.Libs/Network/HttpExports.cs new file mode 100644 index 0000000..98a80b9 --- /dev/null +++ b/src/SharpEmu.Libs/Network/HttpExports.cs @@ -0,0 +1,122 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Collections.Concurrent; +using System.Threading; + +namespace SharpEmu.Libs.Network; + +public static class HttpExports +{ + private const int HttpErrorInvalidId = unchecked((int)0x80431100); + private const int HttpErrorInvalidValue = unchecked((int)0x804311FE); + + private static readonly ConcurrentDictionary Contexts = new(); + private static readonly ConcurrentDictionary Templates = new(); + private static int _nextContextId; + private static int _nextTemplateId = 0x1000; + + private sealed record HttpContext(int NetMemoryId, int SslContextId, ulong PoolSize); + + private sealed record HttpTemplate(int ContextId, ulong UserAgentAddress, int HttpVersion, bool AutoProxyConfig); + + [SysAbiExport( + Nid = "A9cVMUtEp4Y", + ExportName = "sceHttpInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceHttp")] + public static int HttpInit(CpuContext ctx) + { + var netMemoryId = unchecked((int)ctx[CpuRegister.Rdi]); + var sslContextId = unchecked((int)ctx[CpuRegister.Rsi]); + var poolSize = ctx[CpuRegister.Rdx]; + if (poolSize == 0) + { + return SetReturn(ctx, HttpErrorInvalidValue); + } + + var id = Interlocked.Increment(ref _nextContextId); + Contexts[id] = new HttpContext(netMemoryId, sslContextId, poolSize); + TraceHttp("init", id, unchecked((ulong)netMemoryId), unchecked((ulong)sslContextId), poolSize, 0); + ctx[CpuRegister.Rax] = unchecked((ulong)id); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "0gYjPTR-6cY", + ExportName = "sceHttpCreateTemplate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceHttp")] + public static int HttpCreateTemplate(CpuContext ctx) + { + var contextId = unchecked((int)ctx[CpuRegister.Rdi]); + if (!Contexts.ContainsKey(contextId)) + { + return SetReturn(ctx, HttpErrorInvalidId); + } + + var userAgentAddress = ctx[CpuRegister.Rsi]; + var httpVersion = unchecked((int)ctx[CpuRegister.Rdx]); + var autoProxyConfig = ctx[CpuRegister.Rcx] != 0; + var id = Interlocked.Increment(ref _nextTemplateId); + Templates[id] = new HttpTemplate(contextId, userAgentAddress, httpVersion, autoProxyConfig); + TraceHttp("create_template", id, unchecked((ulong)contextId), userAgentAddress, unchecked((ulong)httpVersion), autoProxyConfig ? 1UL : 0UL); + ctx[CpuRegister.Rax] = unchecked((ulong)id); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "4I8vEpuEhZ8", + ExportName = "sceHttpDeleteTemplate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceHttp")] + public static int HttpDeleteTemplate(CpuContext ctx) + { + var templateId = unchecked((int)ctx[CpuRegister.Rdi]); + return Templates.TryRemove(templateId, out _) + ? SetReturn(ctx, 0) + : SetReturn(ctx, HttpErrorInvalidId); + } + + [SysAbiExport( + Nid = "Ik-KpLTlf7Q", + ExportName = "sceHttpTerm", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceHttp")] + public static int HttpTerm(CpuContext ctx) + { + var contextId = unchecked((int)ctx[CpuRegister.Rdi]); + if (!Contexts.TryRemove(contextId, out _)) + { + return SetReturn(ctx, HttpErrorInvalidId); + } + + foreach (var pair in Templates) + { + if (pair.Value.ContextId == contextId) + { + Templates.TryRemove(pair.Key, out _); + } + } + + return SetReturn(ctx, 0); + } + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } + + private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_HTTP"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine( + $"[LOADER][TRACE] http.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16} arg3=0x{arg3:X16}"); + } +} diff --git a/src/SharpEmu.Libs/Network/NetCtlExports.cs b/src/SharpEmu.Libs/Network/NetCtlExports.cs index 1c4eb27..3020dcd 100644 --- a/src/SharpEmu.Libs/Network/NetCtlExports.cs +++ b/src/SharpEmu.Libs/Network/NetCtlExports.cs @@ -2,11 +2,40 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using System.Buffers.Binary; namespace SharpEmu.Libs.Network; public static class NetCtlExports { + private const int MaxCallbacks = 8; + private const int NatInfoSize = 16; + private const int NetCtlErrorNoSpace = unchecked((int)0x80412103); + private const int NetCtlErrorInvalidAddress = unchecked((int)0x80412107); + private const int NetCtlErrorNotConnected = unchecked((int)0x80412108); + private const int NetCtlInfoDevice = 1; + private const int NetCtlInfoEtherAddress = 2; + private const int NetCtlInfoMtu = 3; + private const int NetCtlInfoLink = 4; + private const int NetCtlInfoIpConfig = 11; + private const int NetCtlInfoDhcpHostname = 12; + private const int NetCtlInfoPppoeAuthName = 13; + private const int NetCtlInfoIpAddress = 14; + private const int NetCtlInfoNetmask = 15; + private const int NetCtlInfoDefaultRoute = 16; + private const int NetCtlInfoPrimaryDns = 17; + private const int NetCtlInfoSecondaryDns = 18; + private const int NetCtlInfoHttpProxyConfig = 19; + private const int NetCtlInfoHttpProxyServer = 20; + private const int NetCtlInfoHttpProxyPort = 21; + private const int NetCtlDeviceWired = 0; + private const int NetCtlLinkDisconnected = 0; + private const int NetCtlIpConfigStatic = 0; + private static readonly object CallbackGate = new(); + private static readonly CallbackRegistration[] Callbacks = new CallbackRegistration[MaxCallbacks]; + + private readonly record struct CallbackRegistration(ulong Function, ulong Argument); + [SysAbiExport( Nid = "gky0+oaNM4k", ExportName = "sceNetCtlInit", @@ -17,4 +46,184 @@ public static class NetCtlExports ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + + [SysAbiExport( + Nid = "JO4yuTuMoKI", + ExportName = "sceNetCtlGetNatInfo", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNetCtl")] + public static int NetCtlGetNatInfo(CpuContext ctx) + { + var natInfoAddress = ctx[CpuRegister.Rdi]; + if (natInfoAddress == 0) + { + return SetReturn(ctx, NetCtlErrorInvalidAddress); + } + + Span natInfo = stackalloc byte[NatInfoSize]; + if (!ctx.Memory.TryRead(natInfoAddress, natInfo)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]); + if (size != NatInfoSize) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1); + BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3); + BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001); + return ctx.Memory.TryWrite(natInfoAddress, natInfo) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + [SysAbiExport( + Nid = "iQw3iQPhvUQ", + ExportName = "sceNetCtlCheckCallback", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNetCtl")] + public static int NetCtlCheckCallback(CpuContext ctx) + { + return SetReturn(ctx, 0); + } + + [SysAbiExport( + Nid = "uBPlr0lbuiI", + ExportName = "sceNetCtlGetState", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNetCtl")] + public static int NetCtlGetState(CpuContext ctx) + { + var stateAddress = ctx[CpuRegister.Rdi]; + if (stateAddress == 0) + { + return SetReturn(ctx, NetCtlErrorInvalidAddress); + } + + Span stateBytes = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0); + return ctx.Memory.TryWrite(stateAddress, stateBytes) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + [SysAbiExport( + Nid = "UJ+Z7Q+4ck0", + ExportName = "sceNetCtlRegisterCallback", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNetCtl")] + public static int NetCtlRegisterCallback(CpuContext ctx) + { + var function = ctx[CpuRegister.Rdi]; + var argument = ctx[CpuRegister.Rsi]; + var callbackIdAddress = ctx[CpuRegister.Rdx]; + if (function == 0 || callbackIdAddress == 0) + { + return SetReturn(ctx, NetCtlErrorInvalidAddress); + } + + lock (CallbackGate) + { + var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0); + if (callbackId < 0) + { + return SetReturn(ctx, NetCtlErrorNoSpace); + } + + Span callbackIdBytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId)); + if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + Callbacks[callbackId] = new CallbackRegistration(function, argument); + } + + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK); + } + + [SysAbiExport( + Nid = "obuxdTiwkF8", + ExportName = "sceNetCtlGetInfo", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNetCtl")] + public static int NetCtlGetInfo(CpuContext ctx) + { + var code = unchecked((int)ctx[CpuRegister.Rdi]); + var infoAddress = ctx[CpuRegister.Rsi]; + if (infoAddress == 0) + { + return SetReturn(ctx, NetCtlErrorInvalidAddress); + } + + return code switch + { + NetCtlInfoDevice => WriteUInt32(ctx, infoAddress, NetCtlDeviceWired), + NetCtlInfoEtherAddress => WriteZeroBytes(ctx, infoAddress, 6), + NetCtlInfoMtu => WriteUInt32(ctx, infoAddress, 1500), + NetCtlInfoLink => WriteUInt32(ctx, infoAddress, NetCtlLinkDisconnected), + NetCtlInfoIpConfig => WriteUInt32(ctx, infoAddress, NetCtlIpConfigStatic), + NetCtlInfoDhcpHostname => WriteAsciiZ(ctx, infoAddress, string.Empty, 256), + NetCtlInfoPppoeAuthName => WriteAsciiZ(ctx, infoAddress, string.Empty, 128), + NetCtlInfoIpAddress => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16), + NetCtlInfoNetmask => WriteAsciiZ(ctx, infoAddress, "255.0.0.0", 16), + NetCtlInfoDefaultRoute => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16), + NetCtlInfoPrimaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16), + NetCtlInfoSecondaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16), + NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0), + NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256), + NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0), + _ => SetReturn(ctx, NetCtlErrorNotConnected), + }; + } + + private static int WriteZeroBytes(CpuContext ctx, ulong address, int count) + { + Span bytes = stackalloc byte[count]; + return ctx.Memory.TryWrite(address, bytes) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + private static int WriteUInt32(CpuContext ctx, ulong address, uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + return ctx.Memory.TryWrite(address, bytes) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + private static int WriteUInt16(CpuContext ctx, ulong address, ushort value) + { + Span bytes = stackalloc byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16LittleEndian(bytes, value); + return ctx.Memory.TryWrite(address, bytes) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount) + { + Span bytes = stackalloc byte[byteCount]; + var copyCount = Math.Min(value.Length, byteCount - 1); + for (var i = 0; i < copyCount; i++) + { + bytes[i] = (byte)value[i]; + } + + return ctx.Memory.TryWrite(address, bytes) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)(long)result); + return result; + } } diff --git a/src/SharpEmu.Libs/Network/NetExports.cs b/src/SharpEmu.Libs/Network/NetExports.cs index 8852220..19247d5 100644 --- a/src/SharpEmu.Libs/Network/NetExports.cs +++ b/src/SharpEmu.Libs/Network/NetExports.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using System; +using System.Buffers.Binary; using System.Collections.Concurrent; using System.Text; using System.Threading; @@ -16,11 +17,15 @@ public static class NetExports private const int MaxNameLength = 256; private static readonly ConcurrentDictionary _pools = new(); + private static readonly ConcurrentDictionary _resolvers = new(); private static int _nextPoolId; + private static int _nextResolverId = 0x2000; private static bool _initialized; private sealed record NetPool(string Name, int Size, int Flags); + private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError); + [SysAbiExport( Nid = "Nlev7Lg8k3A", ExportName = "sceNetInit", @@ -42,6 +47,7 @@ public static class NetExports { _initialized = false; _pools.Clear(); + _resolvers.Clear(); TraceNet("term", 0, 0, 0, 0); return SetReturn(ctx, 0); } @@ -91,6 +97,70 @@ public static class NetExports return SetReturn(ctx, 0); } + [SysAbiExport( + Nid = "C4UgDHHPvdw", + ExportName = "sceNetResolverCreate", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetResolverCreate(CpuContext ctx) + { + var nameAddress = ctx[CpuRegister.Rdi]; + var poolId = unchecked((int)ctx[CpuRegister.Rsi]); + var flags = unchecked((int)ctx[CpuRegister.Rdx]); + if (flags != 0) + { + return SetReturn(ctx, NetErrorInvalidArgument); + } + + var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value) + ? value + : string.Empty; + var id = Interlocked.Increment(ref _nextResolverId); + _resolvers[id] = new ResolverContext(name, poolId, flags, 0); + TraceNet("resolver.create", id, unchecked((ulong)poolId), unchecked((ulong)flags), _initialized ? 1UL : 0UL); + ctx[CpuRegister.Rax] = unchecked((ulong)id); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "kJlYH5uMAWI", + ExportName = "sceNetResolverDestroy", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetResolverDestroy(CpuContext ctx) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + return _resolvers.TryRemove(id, out _) + ? SetReturn(ctx, 0) + : SetReturn(ctx, NetErrorBadFileDescriptor); + } + + [SysAbiExport( + Nid = "J5i3hiLJMPk", + ExportName = "sceNetResolverGetError", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNet")] + public static int NetResolverGetError(CpuContext ctx) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + var statusAddress = ctx[CpuRegister.Rsi]; + if (statusAddress == 0) + { + return SetReturn(ctx, NetErrorInvalidArgument); + } + + if (!_resolvers.TryGetValue(id, out var resolver)) + { + return SetReturn(ctx, NetErrorBadFileDescriptor); + } + + Span status = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError); + return ctx.Memory.TryWrite(statusAddress, status) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + private static int SetReturn(CpuContext ctx, int result) { ctx[CpuRegister.Rax] = unchecked((ulong)result);