fix(json): iterate sce::Json::Array values (#1087)

This commit is contained in:
Mathias 2026-10-12 18:32:19 +02:00 committed by GitHub
parent 28c922c7a5
commit b307b2f8fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 459 additions and 0 deletions

View File

@ -29,6 +29,18 @@ public static class JsonExports
private static readonly ConcurrentDictionary<ulong, JsonStringState> _strings = new();
private static readonly JsonElement _nullElement = CreateNullElement();
// sce::Json::Array is reached through Value::getArray and walked with an 8-byte iterator. The
// elements are laid out once as consecutive Value objects in guest memory; the Array object
// holds their base and count, and an iterator holds the address of its element, so stepping
// adds ValueObjectSize and dereferencing returns the element itself.
private sealed record JsonArrayState(ulong Elements, int Count);
private const int ArrayObjectSize = 0x10;
private static readonly ConcurrentDictionary<ulong, JsonArrayState> _arrays = new();
// The Array built for a parsed value, reused while that value is unchanged.
private static readonly ConcurrentDictionary<ulong, (JsonValueState Source, ulong Array)> _valueArrays = new();
[SysAbiExport(
Nid = "-hJRce8wn1U",
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
@ -598,6 +610,289 @@ public static class JsonExports
return 0;
}
[SysAbiExport(
Nid = "ONT8As5R1ug",
ExportName = "_ZNK3sce4Json5Value8getArrayEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetArray(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
JsonValueState? source = null;
var hasParsed = !JsonObjectHeap.Values.ContainsKey(valueAddress) &&
_values.TryGetValue(valueAddress, out source);
if (hasParsed &&
_valueArrays.TryGetValue(valueAddress, out var cached) &&
ReferenceEquals(cached.Source, source))
{
ctx[CpuRegister.Rax] = cached.Array;
return 0;
}
// A value that is not an array reads as an empty one.
var element = GetValue(valueAddress);
var count = element.ValueKind == System.Text.Json.JsonValueKind.Array ? element.GetArrayLength() : 0;
if (!TryAllocateGuestObject(ctx, ArrayObjectSize + (count * ValueObjectSize), out var arrayAddress))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
var elements = arrayAddress + ArrayObjectSize;
for (var index = 0; index < count; index++)
{
StoreValue(ctx, elements + (ulong)(index * ValueObjectSize), element[index]);
}
_arrays[arrayAddress] = new JsonArrayState(elements, count);
ctx.TryWriteUInt64(arrayAddress, elements);
ctx.TryWriteUInt64(arrayAddress + 8, (ulong)count);
if (hasParsed)
{
_valueArrays[valueAddress] = (source!, arrayAddress);
}
ctx[CpuRegister.Rax] = arrayAddress;
TraceJson("Value.getArray", valueAddress, (ulong)count);
return 0;
}
[SysAbiExport(
Nid = "JP-PtKMiI1E",
ExportName = "_ZN3sce4Json5ArrayC1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArrayConstructor(CpuContext ctx) => ConstructArray(ctx);
[SysAbiExport(
Nid = "t6AHhkWu4mY",
ExportName = "_ZN3sce4Json5ArrayC2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArrayBaseConstructor(CpuContext ctx) => ConstructArray(ctx);
[SysAbiExport(
Nid = "HJ8GpRT1aiw",
ExportName = "_ZN3sce4Json5ArrayD1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArrayDestructor(CpuContext ctx) => DestroyArray(ctx);
[SysAbiExport(
Nid = "MIyMPw7YTGM",
ExportName = "_ZN3sce4Json5ArrayD2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArrayBaseDestructor(CpuContext ctx) => DestroyArray(ctx);
[SysAbiExport(
Nid = "rQGJeNjOuUk",
ExportName = "_ZNK3sce4Json5Array4sizeEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArraySize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = (ulong)GetArray(ctx[CpuRegister.Rdi]).Count;
return 0;
}
[SysAbiExport(
Nid = "9uP25i6ipno",
ExportName = "_ZNK3sce4Json5Array5emptyEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArrayEmpty(CpuContext ctx)
{
ctx[CpuRegister.Rax] = GetArray(ctx[CpuRegister.Rdi]).Count == 0 ? 1UL : 0UL;
return 0;
}
// begin() and end() return the iterator through the hidden result pointer in RDI; the Array
// is in RSI.
[SysAbiExport(
Nid = "bcH5EnFE2xY",
ExportName = "_ZNK3sce4Json5Array5beginEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArrayBegin(CpuContext ctx) =>
ReturnIterator(ctx, GetArray(ctx[CpuRegister.Rsi]).Elements);
[SysAbiExport(
Nid = "WXF2ihRF+B8",
ExportName = "_ZNK3sce4Json5Array3endEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ArrayEnd(CpuContext ctx)
{
var array = GetArray(ctx[CpuRegister.Rsi]);
return ReturnIterator(ctx, array.Elements + (ulong)(array.Count * ValueObjectSize));
}
[SysAbiExport(
Nid = "TAPTWYaYf8Q",
ExportName = "_ZN3sce4Json5Array8iteratorC1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorConstructor(CpuContext ctx) => SetIterator(ctx, ctx[CpuRegister.Rdi], 0);
[SysAbiExport(
Nid = "JSg1MSo7NKE",
ExportName = "_ZN3sce4Json5Array8iteratorC2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorBaseConstructor(CpuContext ctx) => SetIterator(ctx, ctx[CpuRegister.Rdi], 0);
[SysAbiExport(
Nid = "IaIvX3kgyb0",
ExportName = "_ZN3sce4Json5Array8iteratorC1ERKS2_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorCopyConstructor(CpuContext ctx) => CopyIterator(ctx);
[SysAbiExport(
Nid = "Hdc3DSIsG2I",
ExportName = "_ZN3sce4Json5Array8iteratorC2ERKS2_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorBaseCopyConstructor(CpuContext ctx) => CopyIterator(ctx);
[SysAbiExport(
Nid = "T3WReElX4BY",
ExportName = "_ZN3sce4Json5Array8iteratoraSERKS2_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorAssignment(CpuContext ctx) => CopyIterator(ctx);
[SysAbiExport(
Nid = "9yLjn46Ypfs",
ExportName = "_ZN3sce4Json5Array8iteratorD1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorDestructor(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "9niNrXsF3Bc",
ExportName = "_ZN3sce4Json5Array8iteratorD2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorBaseDestructor(CpuContext ctx) => IteratorDestructor(ctx);
[SysAbiExport(
Nid = "5AZPp99ogrc",
ExportName = "_ZNK3sce4Json5Array8iteratorneERKS2_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorNotEqual(CpuContext ctx)
{
ctx[CpuRegister.Rax] =
ReadIterator(ctx, ctx[CpuRegister.Rdi]) != ReadIterator(ctx, ctx[CpuRegister.Rsi]) ? 1UL : 0UL;
return 0;
}
[SysAbiExport(
Nid = "wcgr5mte7T8",
ExportName = "_ZNK3sce4Json5Array8iteratordeEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorDereference(CpuContext ctx)
{
ctx[CpuRegister.Rax] = ReadIterator(ctx, ctx[CpuRegister.Rdi]);
return 0;
}
[SysAbiExport(
Nid = "iAIYn4oAWvI",
ExportName = "_ZNK3sce4Json5Array8iteratorptEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorArrow(CpuContext ctx) => IteratorDereference(ctx);
[SysAbiExport(
Nid = "w5+VCznos5E",
ExportName = "_ZN3sce4Json5Array8iteratorppEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorPreIncrement(CpuContext ctx)
{
var iterator = ctx[CpuRegister.Rdi];
return SetIterator(ctx, iterator, ReadIterator(ctx, iterator) + ValueObjectSize);
}
// Postfix ++ returns the old position through the hidden result pointer in RDI; the iterator
// is in RSI.
[SysAbiExport(
Nid = "GEylwSsiGG8",
ExportName = "_ZN3sce4Json5Array8iteratorppEi",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorPostIncrement(CpuContext ctx)
{
var iterator = ctx[CpuRegister.Rsi];
var position = ReadIterator(ctx, iterator);
ctx.TryWriteUInt64(iterator, position + ValueObjectSize);
return ReturnIterator(ctx, position);
}
[SysAbiExport(
Nid = "43MESLndGvQ",
ExportName = "_ZN3sce4Json5Array8iterator7advanceEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int IteratorAdvance(CpuContext ctx)
{
var iterator = ctx[CpuRegister.Rdi];
var step = ctx[CpuRegister.Rsi] * ValueObjectSize;
return SetIterator(ctx, iterator, ReadIterator(ctx, iterator) + step);
}
private static int ConstructArray(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress != 0)
{
_arrays[thisAddress] = new JsonArrayState(0, 0);
ctx.TryWriteUInt64(thisAddress, 0);
ctx.TryWriteUInt64(thisAddress + 8, 0);
}
ctx[CpuRegister.Rax] = thisAddress;
return 0;
}
private static int DestroyArray(CpuContext ctx)
{
_arrays.TryRemove(ctx[CpuRegister.Rdi], out _);
ctx[CpuRegister.Rax] = 0;
return 0;
}
private static JsonArrayState GetArray(ulong address) =>
_arrays.TryGetValue(address, out var array) ? array : new JsonArrayState(0, 0);
private static ulong ReadIterator(CpuContext ctx, ulong iterator) =>
iterator != 0 && ctx.TryReadUInt64(iterator, out var position) ? position : 0;
private static int SetIterator(CpuContext ctx, ulong iterator, ulong position)
{
if (iterator != 0)
{
ctx.TryWriteUInt64(iterator, position);
}
ctx[CpuRegister.Rax] = iterator;
return 0;
}
private static int CopyIterator(CpuContext ctx) =>
SetIterator(ctx, ctx[CpuRegister.Rdi], ReadIterator(ctx, ctx[CpuRegister.Rsi]));
private static int ReturnIterator(CpuContext ctx, ulong position) =>
SetIterator(ctx, ctx[CpuRegister.Rdi], position);
private static int ConstructValue(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
@ -642,6 +937,7 @@ public static class JsonExports
{
var thisAddress = ctx[CpuRegister.Rdi];
_values.TryRemove(thisAddress, out _);
_valueArrays.TryRemove(thisAddress, out _);
JsonObjectHeap.RemoveValue(thisAddress);
if (thisAddress != 0)
{

View File

@ -165,4 +165,167 @@ public sealed class JsonExportRegistrationTests
Assert.Equal(userDataAddress, BitConverter.ToUInt64(parameter, 8));
Assert.All(parameter[0x10..], value => Assert.Equal(0, value));
}
private const ulong ArrayMemoryBase = 0x1_0000_0000;
private const ulong ArrayRoot = ArrayMemoryBase;
private const ulong ArrayText = ArrayMemoryBase + 0x100;
private const ulong ArrayKey = ArrayMemoryBase + 0x300;
private const ulong ArrayBeginIterator = ArrayMemoryBase + 0x400;
private const ulong ArrayEndIterator = ArrayMemoryBase + 0x408;
private static (CpuContext Context, FakeCpuMemory Memory) Parse(string json)
{
JsonObjectHeap.ResetForTests();
var memory = new FakeCpuMemory(ArrayMemoryBase, 0x10000);
var ctx = new CpuContext(memory, Generation.Gen5);
ctx[CpuRegister.Rdi] = ArrayRoot;
JsonExports.ValueConstructor(ctx);
memory.WriteCString(ArrayText, json);
ctx[CpuRegister.Rdi] = ArrayRoot;
ctx[CpuRegister.Rsi] = ArrayText;
ctx[CpuRegister.Rdx] = (ulong)json.Length;
Assert.Equal(0, JsonExports.ParserParseBuffer(ctx));
return (ctx, memory);
}
private static ulong Member(CpuContext ctx, FakeCpuMemory memory, string name)
{
memory.WriteCString(ArrayKey, name);
ctx[CpuRegister.Rdi] = ArrayRoot;
ctx[CpuRegister.Rsi] = ArrayKey;
JsonExports.ValueIndexCString(ctx);
return ctx[CpuRegister.Rax];
}
private static ulong GetArray(CpuContext ctx, ulong value)
{
ctx[CpuRegister.Rdi] = value;
JsonExports.ValueGetArray(ctx);
return ctx[CpuRegister.Rax];
}
private static void Range(CpuContext ctx, ulong array)
{
ctx[CpuRegister.Rdi] = ArrayBeginIterator;
ctx[CpuRegister.Rsi] = array;
JsonExports.ArrayBegin(ctx);
Assert.Equal(ArrayBeginIterator, ctx[CpuRegister.Rax]);
ctx[CpuRegister.Rdi] = ArrayEndIterator;
ctx[CpuRegister.Rsi] = array;
JsonExports.ArrayEnd(ctx);
Assert.Equal(ArrayEndIterator, ctx[CpuRegister.Rax]);
}
private static bool NotAtEnd(CpuContext ctx)
{
ctx[CpuRegister.Rdi] = ArrayBeginIterator;
ctx[CpuRegister.Rsi] = ArrayEndIterator;
JsonExports.IteratorNotEqual(ctx);
return ctx[CpuRegister.Rax] != 0;
}
[Fact]
public void AnArrayOfRealsIsWalkedFromBeginToEnd()
{
var (ctx, memory) = Parse("{\"scale\":[1.5,-2.25,3.0]}");
var array = GetArray(ctx, Member(ctx, memory, "scale"));
Range(ctx, array);
var reals = new List<double>();
while (NotAtEnd(ctx))
{
ctx[CpuRegister.Rdi] = ArrayBeginIterator;
JsonExports.IteratorDereference(ctx);
var element = ctx[CpuRegister.Rax];
ctx[CpuRegister.Rdi] = element;
JsonExports.ValueGetType(ctx);
Assert.Equal(4UL, ctx[CpuRegister.Rax]);
ctx[CpuRegister.Rdi] = element;
JsonExports.ValueGetReal(ctx);
Assert.True(ctx.TryReadUInt64(ctx[CpuRegister.Rax], out var bits));
reals.Add(BitConverter.Int64BitsToDouble(unchecked((long)bits)));
ctx[CpuRegister.Rdi] = ArrayBeginIterator;
JsonExports.IteratorPreIncrement(ctx);
Assert.Equal(ArrayBeginIterator, ctx[CpuRegister.Rax]);
}
Assert.Equal([1.5, -2.25, 3.0], reals);
ctx[CpuRegister.Rdi] = array;
JsonExports.ArraySize(ctx);
Assert.Equal(3UL, ctx[CpuRegister.Rax]);
}
[Fact]
public void ReadingTheSameValueAgainReusesItsArray()
{
var (ctx, memory) = Parse("{\"list\":[1,2]}");
var member = Member(ctx, memory, "list");
Assert.Equal(GetArray(ctx, member), GetArray(ctx, member));
}
[Fact]
public void AValueThatIsNotAnArrayReadsAsEmpty()
{
var (ctx, memory) = Parse("{\"number\":7}");
var array = GetArray(ctx, Member(ctx, memory, "number"));
Assert.NotEqual(0UL, array);
Range(ctx, array);
Assert.False(NotAtEnd(ctx));
ctx[CpuRegister.Rdi] = array;
JsonExports.ArrayEmpty(ctx);
Assert.Equal(1UL, ctx[CpuRegister.Rax]);
}
[Fact]
public void PostfixIncrementAndCopiesKeepTheirOwnPosition()
{
const ulong old = ArrayMemoryBase + 0x410;
const ulong copy = ArrayMemoryBase + 0x418;
var (ctx, memory) = Parse("{\"list\":[10,20,30]}");
Range(ctx, GetArray(ctx, Member(ctx, memory, "list")));
Assert.True(ctx.TryReadUInt64(ArrayBeginIterator, out var first));
ctx[CpuRegister.Rdi] = copy;
ctx[CpuRegister.Rsi] = ArrayBeginIterator;
JsonExports.IteratorCopyConstructor(ctx);
ctx[CpuRegister.Rdi] = old;
ctx[CpuRegister.Rsi] = ArrayBeginIterator;
ctx[CpuRegister.Rdx] = 0;
JsonExports.IteratorPostIncrement(ctx);
Assert.Equal(old, ctx[CpuRegister.Rax]);
Assert.True(ctx.TryReadUInt64(old, out var oldPosition));
Assert.True(ctx.TryReadUInt64(copy, out var copyPosition));
Assert.True(ctx.TryReadUInt64(ArrayBeginIterator, out var advanced));
Assert.Equal(first, oldPosition);
Assert.Equal(first, copyPosition);
Assert.Equal(first + 0x20, advanced);
ctx[CpuRegister.Rdi] = ArrayBeginIterator;
ctx[CpuRegister.Rsi] = 2;
JsonExports.IteratorAdvance(ctx);
Assert.False(NotAtEnd(ctx));
}
[Theory]
[InlineData("ONT8As5R1ug", "_ZNK3sce4Json5Value8getArrayEv")]
[InlineData("bcH5EnFE2xY", "_ZNK3sce4Json5Array5beginEv")]
[InlineData("WXF2ihRF+B8", "_ZNK3sce4Json5Array3endEv")]
[InlineData("5AZPp99ogrc", "_ZNK3sce4Json5Array8iteratorneERKS2_")]
[InlineData("wcgr5mte7T8", "_ZNK3sce4Json5Array8iteratordeEv")]
[InlineData("w5+VCznos5E", "_ZN3sce4Json5Array8iteratorppEv")]
[InlineData("9yLjn46Ypfs", "_ZN3sce4Json5Array8iteratorD1Ev")]
public void ArrayExportsAreRegistered(string nid, string name)
{
var manager = CreateRegisteredManager();
Assert.True(manager.TryGetExport(nid, out var export));
Assert.Equal(name, export.Name);
Assert.Equal("libSceJson", export.LibraryName);
}
}