sharpemu/tests/SharpEmu.Libs.Tests/Network/HttpUriTests.cs

283 lines
12 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Network;
using Xunit;
namespace SharpEmu.Libs.Tests.Network;
public sealed class HttpUriTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong SourceAddress = MemoryBase + 0x100;
private const ulong OutputAddress = MemoryBase + 0x5000;
private const ulong RequiredSizeAddress = MemoryBase + 0x5100;
private const ulong PoolAddress = MemoryBase + 0x5200;
private const int MemorySize = 0xA000;
private const int InvalidUrl = unchecked((int)0x80433060);
private const int OutOfMemory = unchecked((int)0x80431022);
private readonly FakeCpuMemory _memory = new(MemoryBase, MemorySize);
[Fact]
public void ComponentLayout_HasFixedGuestOffsetsAndSize()
{
Assert.Equal(80, Marshal.SizeOf<HttpExports.HttpUriComponents>());
var fieldNames = new[]
{
"IsOpaque", "SchemeAddress", "UserNameAddress", "PasswordAddress", "HostNameAddress",
"PathAddress", "QueryAddress", "FragmentAddress", "Port"
};
for (var fieldIndex = 0; fieldIndex < fieldNames.Length; fieldIndex++)
{
Assert.Equal(fieldIndex * 8, Marshal.OffsetOf<HttpExports.HttpUriComponents>(fieldNames[fieldIndex]).ToInt32());
}
}
[Theory]
[InlineData(Generation.Gen4)]
[InlineData(Generation.Gen5)]
public void Export_IsRegisteredForBothGenerations(Generation generation)
{
var manager = new ModuleManager();
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(generation));
Assert.True(manager.TryGetExport("IWalAn-guFs", out var export));
Assert.Equal("sceHttpUriParse", export.Name);
Assert.Equal("libSceHttp", export.LibraryName);
}
[Theory]
[InlineData(Generation.Gen4)]
[InlineData(Generation.Gen5)]
public void Parse_QueryThenFillPublishesCompleteGuestStructure(Generation generation)
{
var context = CreateContext("https://user:pass@foo.com:1443/var/test.cgi?test#hoge", generation);
Fill(OutputAddress - 8, 96, 0xA5);
Fill(RequiredSizeAddress - 8, 24, 0xA5);
Fill(PoolAddress - 8, 256, 0xA5);
context[CpuRegister.Rdi] = 0;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.R8] = 0;
Assert.Equal(0, HttpExports.ParseHttpUri(context));
var expectedComponents = new[] { "https", "user", "pass", "foo.com", "/var/test.cgi", "?test", "#hoge" };
var expectedPool = Encoding.UTF8.GetBytes(string.Join('\0', expectedComponents) + '\0');
Assert.Equal((ulong)expectedPool.Length, ReadSize());
Assert.All(ReadBytes(OutputAddress, 80), value => Assert.Equal(0xA5, value));
context[CpuRegister.Rdi] = OutputAddress;
context[CpuRegister.Rdx] = PoolAddress;
context[CpuRegister.Rcx] = 0;
context[CpuRegister.R8] = ReadSize();
Assert.Equal(0, HttpExports.ParseHttpUri(context));
Assert.Equal(0UL, context[CpuRegister.Rax]);
var output = ReadBytes(OutputAddress, 80);
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(output));
var poolOffset = 0;
for (var componentIndex = 0; componentIndex < expectedComponents.Length; componentIndex++)
{
Assert.Equal(PoolAddress + (ulong)poolOffset, ReadComponentAddress(output, componentIndex));
poolOffset += expectedComponents[componentIndex].Length + 1;
}
Assert.Equal(1443, BinaryPrimitives.ReadUInt16LittleEndian(output.AsSpan(64)));
Assert.All(output[4..8], value => Assert.Equal(0, value));
Assert.All(output[66..], value => Assert.Equal(0, value));
Assert.Equal(expectedPool, ReadBytes(PoolAddress, expectedPool.Length));
foreach (var guardAddress in new[]
{
OutputAddress - 8, OutputAddress + 80, RequiredSizeAddress - 8,
RequiredSizeAddress + 8, PoolAddress - 8, PoolAddress + (ulong)expectedPool.Length
})
{
Assert.All(ReadBytes(guardAddress, 8), value => Assert.Equal(0xA5, value));
}
}
[Theory]
[InlineData("https://Example.COM", false, 0, "https", null, null, "Example.COM", null, null, null)]
[InlineData("https://[2001:db8::1]:65535/a?#", false, 65535, "https", null, null, "[2001:db8::1]", "/a", "?", "#")]
[InlineData("http://user@host:0/", false, 0, "http", "user", null, "host", "/", null, null)]
[InlineData("http://:@host/path", false, 0, "http", "", "", "host", "/path", null, null)]
[InlineData("file:///folder/file", false, 0, "file", null, null, null, "/folder/file", null, null)]
[InlineData("mailto:user@example.invalid?subject=test#part", true, 0, "mailto", null, null, null, "user@example.invalid", "?subject=test", "#part")]
[InlineData("a+1.-:value", true, 0, "a+1.-", null, null, null, "value", null, null)]
[InlineData("", true, 0, "", null, null, "", "", null, null)]
[InlineData("HTTP://host/%2f/../café?q=a+b%20c#X", false, 0, "HTTP", null, null, "host", "/%2f/../café", "?q=a+b%20c", "#X")]
public void Parse_PreservesComponentsWithoutNormalization(
string source, bool isOpaque, int port, string? scheme, string? userName, string? password,
string? hostName, string? path, string? query, string? fragment)
{
var context = CreateContext(source);
Assert.Equal(0, HttpExports.ParseHttpUri(context));
var output = ReadBytes(OutputAddress, 80);
Assert.Equal(isOpaque ? 1 : 0, BinaryPrimitives.ReadInt32LittleEndian(output));
Assert.Equal(port, BinaryPrimitives.ReadUInt16LittleEndian(output.AsSpan(64)));
var expectedComponents = new[] { scheme, userName, password, hostName, path, query, fragment };
var expectedSize = 0;
for (var componentIndex = 0; componentIndex < expectedComponents.Length; componentIndex++)
{
var componentAddress = ReadComponentAddress(output, componentIndex);
if (expectedComponents[componentIndex] is not { } expectedComponent)
{
Assert.Equal(0UL, componentAddress);
continue;
}
var expectedBytes = Encoding.UTF8.GetBytes(expectedComponent + '\0');
Assert.Equal(PoolAddress + (ulong)expectedSize, componentAddress);
Assert.Equal(expectedBytes, ReadBytes(componentAddress, expectedBytes.Length));
expectedSize += expectedBytes.Length;
}
Assert.Equal((ulong)expectedSize, ReadSize());
}
[Theory]
[InlineData("relative/path")]
[InlineData("1http://host/")]
[InlineData("ht_tp://host/")]
[InlineData(":value")]
[InlineData("http://host:")]
[InlineData("http://host:-1/")]
[InlineData("http://host:65536/")]
[InlineData("http://host:99999999999999999999/")]
[InlineData("http://host:12x/")]
[InlineData("http://[::1/path")]
[InlineData("http://[::1]extra/path")]
public void Parse_RejectsInvalidInputWithoutWritingOutputs(string source)
{
var context = CreateContext(source);
Fill(OutputAddress, 80, 0xA5);
Fill(RequiredSizeAddress, 8, 0xA5);
Fill(PoolAddress, 128, 0xA5);
Assert.Equal(InvalidUrl, HttpExports.ParseHttpUri(context));
Assert.Equal(unchecked((ulong)InvalidUrl), context[CpuRegister.Rax]);
Assert.All(ReadBytes(OutputAddress, 80), value => Assert.Equal(0xA5, value));
Assert.All(ReadBytes(RequiredSizeAddress, 8), value => Assert.Equal(0xA5, value));
Assert.All(ReadBytes(PoolAddress, 128), value => Assert.Equal(0xA5, value));
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(15)]
public void Parse_RejectsShortPoolAndReportsRequiredSize(int capacity)
{
var context = CreateContext("https://host/path");
context[CpuRegister.R8] = (ulong)capacity;
Fill(PoolAddress, 128, 0xA5);
Fill(OutputAddress, 80, 0xA5);
Assert.Equal(OutOfMemory, HttpExports.ParseHttpUri(context));
Assert.Equal(17UL, ReadSize());
Assert.All(ReadBytes(PoolAddress, 128), value => Assert.Equal(0xA5, value));
Assert.All(ReadBytes(OutputAddress, 80), value => Assert.Equal(0xA5, value));
}
[Fact]
public void Parse_WithoutPoolWritesOnlyMetadataAndNullPointers()
{
var context = CreateContext("https://host:443/path");
context[CpuRegister.Rdx] = 0;
context[CpuRegister.R8] = 0;
Fill(OutputAddress, 80, 0xA5);
Assert.Equal(0, HttpExports.ParseHttpUri(context));
var output = ReadBytes(OutputAddress, 80);
Assert.All(output[..64], value => Assert.Equal(0, value));
Assert.Equal(443, BinaryPrimitives.ReadUInt16LittleEndian(output.AsSpan(64)));
Assert.All(output[66..], value => Assert.Equal(0, value));
}
[Fact]
public void Parse_RejectsNullSourceAndMissingOutputs()
{
var context = CreateContext("https://host/");
context[CpuRegister.Rsi] = 0;
Assert.Equal(InvalidUrl, HttpExports.ParseHttpUri(context));
context[CpuRegister.Rsi] = SourceAddress;
context[CpuRegister.Rdi] = context[CpuRegister.Rdx] = context[CpuRegister.Rcx] = 0;
Assert.Equal(unchecked((int)0x804311FE), HttpExports.ParseHttpUri(context));
}
[Theory]
[InlineData(CpuRegister.Rdi, 79)]
[InlineData(CpuRegister.Rsi, 0)]
[InlineData(CpuRegister.Rdx, 16)]
[InlineData(CpuRegister.Rcx, 7)]
public void Parse_ReportsGuestMemoryFaultForTruncatedRanges(CpuRegister register, int availableBytes)
{
var context = CreateContext("https://host/path");
var truncatedAddress = MemoryBase + MemorySize - (ulong)availableBytes;
Fill(truncatedAddress, availableBytes, 0xA5);
context[register] = truncatedAddress;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, HttpExports.ParseHttpUri(context));
Assert.All(ReadBytes(truncatedAddress, availableBytes), value => Assert.Equal(0xA5, value));
}
[Theory]
[InlineData(CpuRegister.Rdi)]
[InlineData(CpuRegister.Rsi)]
[InlineData(CpuRegister.Rdx)]
[InlineData(CpuRegister.Rcx)]
public void Parse_RejectsAddressOverflow(CpuRegister register)
{
var context = CreateContext("https://host/path");
context[register] = ulong.MaxValue - 2;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, HttpExports.ParseHttpUri(context));
}
[Theory]
[InlineData(16383, 0)]
[InlineData(16384, InvalidUrl)]
public void Parse_EnforcesMaximumSourceLength(int length, int expectedResult)
{
var context = CreateContext("a:" + new string('x', length - 2));
context[CpuRegister.Rdi] = context[CpuRegister.Rdx] = 0;
Assert.Equal(expectedResult, HttpExports.ParseHttpUri(context));
}
[Fact]
public void Parse_SourceMayEndAtMappingBoundary()
{
var context = CreateContext("unused");
var source = "https://host/path";
var address = MemoryBase + MemorySize - (ulong)source.Length - 1;
_memory.WriteCString(address, source);
context[CpuRegister.Rsi] = address;
Assert.Equal(0, HttpExports.ParseHttpUri(context));
}
private CpuContext CreateContext(string source, Generation generation = Generation.Gen5)
{
_memory.WriteCString(SourceAddress, source);
var context = new CpuContext(_memory, generation);
context[CpuRegister.Rdi] = OutputAddress;
context[CpuRegister.Rsi] = SourceAddress;
context[CpuRegister.Rdx] = PoolAddress;
context[CpuRegister.Rcx] = RequiredSizeAddress;
context[CpuRegister.R8] = 0x4000;
return context;
}
private static ulong ReadComponentAddress(byte[] output, int componentIndex) =>
BinaryPrimitives.ReadUInt64LittleEndian(output.AsSpan(8 + componentIndex * 8));
private ulong ReadSize() => BinaryPrimitives.ReadUInt64LittleEndian(ReadBytes(RequiredSizeAddress, 8));
private byte[] ReadBytes(ulong address, int length)
{
var bytes = new byte[length];
Assert.True(_memory.TryRead(address, bytes));
return bytes;
}
private void Fill(ulong address, int length, byte value)
{
var bytes = new byte[length];
Array.Fill(bytes, value);
Assert.True(_memory.TryWrite(address, bytes));
}
}