mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
a8be1daca4
Each of the four byte-order helpers computed the swap into Rax and then returned via ctx.SetReturn(0). SetReturn writes its argument into Rax, so it immediately overwrote the converted value with 0 and the guest saw every sceNetHtonl / sceNetHtons / sceNetNtohl / sceNetNtohs call return 0. Leave the swapped value in Rax and return ORBIS_GEN2_OK as the dispatch status instead, matching how the value-returning exports in this file (e.g. sceNetPoolCreate) already work. Add a NetExports test suite covering the swaps, the 16-bit width masking, an htonl/ntohl round-trip, and a regression guard that a non-zero input never converts to 0.
563 lines
20 KiB
C#
563 lines
20 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Concurrent;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using SharpEmu.HLE;
|
|
|
|
namespace SharpEmu.Libs.Network;
|
|
|
|
public static class NetExports
|
|
{
|
|
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
|
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
|
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
|
|
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
|
|
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
|
|
private const int NetErrnoBadFileDescriptor = 9;
|
|
private const int NetErrnoInvalidArgument = 22;
|
|
private const int NetErrnoWouldBlock = 35;
|
|
private const int NetErrnoAddressInUse = 48;
|
|
private const int NetErrnoNotInitialized = 200;
|
|
private const int MaxNameLength = 256;
|
|
|
|
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
|
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
|
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
|
|
private static int _nextPoolId;
|
|
private static int _nextResolverId = 0x2000;
|
|
private static int _nextSocketId = 0x4000;
|
|
// The platform networking module is usable immediately after it is loaded.
|
|
// Games and middleware (notably FMOD) can create internal sockets before an
|
|
// explicit sceNetInit call reaches application code.
|
|
private static bool _initialized = true;
|
|
|
|
[ThreadStatic]
|
|
private static nint _errnoAddress;
|
|
|
|
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",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetInit(CpuContext ctx)
|
|
{
|
|
_initialized = true;
|
|
TraceNet("init", 0, 0, 0, 0);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "cTGkc6-TBlI",
|
|
ExportName = "sceNetTerm",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetTerm(CpuContext ctx)
|
|
{
|
|
_initialized = false;
|
|
_pools.Clear();
|
|
_resolvers.Clear();
|
|
foreach (var socket in _sockets.Values)
|
|
{
|
|
socket.Dispose();
|
|
}
|
|
_sockets.Clear();
|
|
TraceNet("term", 0, 0, 0, 0);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Q4qBuN-c0ZM",
|
|
ExportName = "sceNetSocket",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetSocket(CpuContext ctx)
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
|
|
}
|
|
|
|
var nameAddress = ctx[CpuRegister.Rdi];
|
|
var family = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
var type = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
var protocol = unchecked((int)ctx[CpuRegister.Rcx]);
|
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
|
? value
|
|
: string.Empty;
|
|
|
|
if (!TryTranslateSocketParameters(family, type, protocol, out var addressFamily, out var socketType, out var protocolType))
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
|
|
try
|
|
{
|
|
var socket = new Socket(addressFamily, socketType, protocolType);
|
|
var id = Interlocked.Increment(ref _nextSocketId);
|
|
_sockets[id] = socket;
|
|
TraceNet("socket.create", id, unchecked((ulong)family), unchecked((ulong)type), unchecked((ulong)protocol));
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "45ggEzakPJQ",
|
|
ExportName = "sceNetSocketClose",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetSocketClose(CpuContext ctx)
|
|
{
|
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
if (!_sockets.TryRemove(id, out var socket))
|
|
{
|
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
}
|
|
|
|
socket.Dispose();
|
|
TraceNet("socket.close", id, 0, 0, 0);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "2mKX2Spso7I",
|
|
ExportName = "sceNetSetsockopt",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetSetsockopt(CpuContext ctx)
|
|
{
|
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
var level = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
var option = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
var valueAddress = ctx[CpuRegister.Rcx];
|
|
var valueLength = unchecked((int)ctx[CpuRegister.R8]);
|
|
if (!_sockets.TryGetValue(id, out var socket))
|
|
{
|
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
}
|
|
|
|
// ORBIS_NET_SOL_SOCKET / ORBIS_NET_SO_NBIO. This is the first option
|
|
// used by FMOD's discovery socket and maps directly to host blocking.
|
|
if (level == 0xFFFF && option == 0x1200)
|
|
{
|
|
Span<byte> value = stackalloc byte[sizeof(int)];
|
|
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
|
|
socket.Blocking = BinaryPrimitives.ReadInt32LittleEndian(value) == 0;
|
|
TraceNet("socket.nonblocking", id, socket.Blocking ? 0UL : 1UL, 0, 0);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
// ORBIS_NET_SO_REUSEADDR uses the BSD value 0x0004.
|
|
if (level == 0xFFFF && option == 0x0004)
|
|
{
|
|
Span<byte> value = stackalloc byte[sizeof(int)];
|
|
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
|
|
socket.SetSocketOption(
|
|
SocketOptionLevel.Socket,
|
|
SocketOptionName.ReuseAddress,
|
|
BinaryPrimitives.ReadInt32LittleEndian(value) != 0);
|
|
TraceNet("socket.reuseaddr", id, BinaryPrimitives.ReadUInt32LittleEndian(value), 0, 0);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "bErx49PgxyY",
|
|
ExportName = "sceNetBind",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetBind(CpuContext ctx)
|
|
{
|
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
if (!_sockets.TryGetValue(id, out var socket))
|
|
{
|
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
}
|
|
if (!TryReadSocketAddress(ctx, ctx[CpuRegister.Rsi], unchecked((int)ctx[CpuRegister.Rdx]), out var endpoint))
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
|
|
try
|
|
{
|
|
socket.Bind(endpoint);
|
|
TraceNet("socket.bind", id, unchecked((ulong)endpoint.Port), 0, 0);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
|
|
{
|
|
return SetNetError(ctx, NetErrorAddressInUse, NetErrnoAddressInUse);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "kOj1HiAGE54",
|
|
ExportName = "sceNetListen",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetListen(CpuContext ctx)
|
|
{
|
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
if (!_sockets.TryGetValue(id, out var socket))
|
|
{
|
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
}
|
|
|
|
try
|
|
{
|
|
socket.Listen(Math.Max(0, unchecked((int)ctx[CpuRegister.Rsi])));
|
|
TraceNet("socket.listen", id, ctx[CpuRegister.Rsi], 0, 0);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "PIWqhn9oSxc",
|
|
ExportName = "sceNetAccept",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetAccept(CpuContext ctx)
|
|
{
|
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
if (!_sockets.TryGetValue(id, out var socket))
|
|
{
|
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
|
}
|
|
|
|
try
|
|
{
|
|
var accepted = socket.Accept();
|
|
var acceptedId = Interlocked.Increment(ref _nextSocketId);
|
|
_sockets[acceptedId] = accepted;
|
|
TraceNet("socket.accept", acceptedId, unchecked((ulong)id), 0, 0);
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)acceptedId);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.WouldBlock or SocketError.IOPending)
|
|
{
|
|
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "HQOwnfMGipQ",
|
|
ExportName = "sceNetErrnoLoc",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetErrnoLoc(CpuContext ctx)
|
|
{
|
|
if (_errnoAddress == 0)
|
|
{
|
|
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
|
|
Marshal.WriteInt32(_errnoAddress, 0);
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)_errnoAddress);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "dgJBaeJnGpo",
|
|
ExportName = "sceNetPoolCreate",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetPoolCreate(CpuContext ctx)
|
|
{
|
|
var nameAddress = ctx[CpuRegister.Rdi];
|
|
var size = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
|
|
if (size <= 0)
|
|
{
|
|
return ctx.SetReturn(NetErrorInvalidArgument);
|
|
}
|
|
|
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
|
? value
|
|
: string.Empty;
|
|
|
|
var id = Interlocked.Increment(ref _nextPoolId);
|
|
_pools[id] = new NetPool(name, size, flags);
|
|
|
|
TraceNet("pool.create", id, unchecked((ulong)size), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "K7RlrTkI-mw",
|
|
ExportName = "sceNetPoolDestroy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetPoolDestroy(CpuContext ctx)
|
|
{
|
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
if (!_pools.TryRemove(id, out _))
|
|
{
|
|
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
|
}
|
|
|
|
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
|
return ctx.SetReturn(0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "9T2pDF2Ryqg",
|
|
ExportName = "sceNetHtonl",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetHtonl(CpuContext ctx)
|
|
{
|
|
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "iWQWrwiSt8A",
|
|
ExportName = "sceNetHtons",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetHtons(CpuContext ctx)
|
|
{
|
|
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "pQGpHYopAIY",
|
|
ExportName = "sceNetNtohl",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetNtohl(CpuContext ctx)
|
|
{
|
|
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Rbvt+5Y2iEw",
|
|
ExportName = "sceNetNtohs",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libSceNet")]
|
|
public static int NetNtohs(CpuContext ctx)
|
|
{
|
|
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
|
// The byte-swapped result is the return value and already lives in Rax; return OK as the
|
|
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
|
|
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[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 ctx.SetReturn(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 _)
|
|
? ctx.SetReturn(0)
|
|
: ctx.SetReturn(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 ctx.SetReturn(NetErrorInvalidArgument);
|
|
}
|
|
|
|
if (!_resolvers.TryGetValue(id, out var resolver))
|
|
{
|
|
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
|
}
|
|
|
|
Span<byte> status = stackalloc byte[sizeof(int)];
|
|
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
|
return ctx.Memory.TryWrite(statusAddress, status)
|
|
? ctx.SetReturn(0)
|
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
private static int SetNetError(CpuContext ctx, int result, int errno)
|
|
{
|
|
if (_errnoAddress == 0)
|
|
{
|
|
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
|
|
}
|
|
Marshal.WriteInt32(_errnoAddress, errno);
|
|
return ctx.SetReturn(result);
|
|
}
|
|
|
|
private static bool TryTranslateSocketParameters(
|
|
int family,
|
|
int type,
|
|
int protocol,
|
|
out AddressFamily addressFamily,
|
|
out SocketType socketType,
|
|
out ProtocolType protocolType)
|
|
{
|
|
addressFamily = family switch
|
|
{
|
|
2 => AddressFamily.InterNetwork,
|
|
28 => AddressFamily.InterNetworkV6,
|
|
_ => AddressFamily.Unspecified,
|
|
};
|
|
socketType = type switch
|
|
{
|
|
1 => SocketType.Stream,
|
|
2 => SocketType.Dgram,
|
|
_ => SocketType.Unknown,
|
|
};
|
|
protocolType = protocol switch
|
|
{
|
|
0 when socketType == SocketType.Stream => ProtocolType.Tcp,
|
|
0 when socketType == SocketType.Dgram => ProtocolType.Udp,
|
|
6 => ProtocolType.Tcp,
|
|
17 => ProtocolType.Udp,
|
|
_ => ProtocolType.Unknown,
|
|
};
|
|
|
|
return addressFamily != AddressFamily.Unspecified &&
|
|
socketType != SocketType.Unknown &&
|
|
protocolType != ProtocolType.Unknown;
|
|
}
|
|
|
|
private static bool TryReadSocketAddress(CpuContext ctx, ulong address, int length, out IPEndPoint endpoint)
|
|
{
|
|
endpoint = new IPEndPoint(IPAddress.Any, 0);
|
|
if (address == 0 || length < 16)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<byte> bytes = stackalloc byte[16];
|
|
if (!ctx.Memory.TryRead(address, bytes) || bytes[1] != 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var port = BinaryPrimitives.ReadUInt16BigEndian(bytes[2..4]);
|
|
endpoint = new IPEndPoint(new IPAddress(bytes[4..8]), port);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
|
{
|
|
value = string.Empty;
|
|
if (address == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Span<byte> one = stackalloc byte[1];
|
|
var bytes = new byte[maxLength];
|
|
var count = 0;
|
|
for (; count < maxLength; count++)
|
|
{
|
|
if (!ctx.Memory.TryRead(address + (ulong)count, one))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (one[0] == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
bytes[count] = one[0];
|
|
}
|
|
|
|
value = Encoding.UTF8.GetString(bytes, 0, count);
|
|
return true;
|
|
}
|
|
|
|
private static void TraceNet(string operation, int id, ulong arg0, ulong arg1, ulong arg2)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] net.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16}");
|
|
}
|
|
}
|