diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 515e60a398..78db96d5b8 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1993,6 +1993,9 @@ struct SerializationIterator { } }; +template +struct SerializationPosition { TLink* first; TLink* head; bool invalid; }; + class CallbackSerializer { public: CallbackSerializer(String& buffer, String* destination, SerializationCallback callback, void* context, bool collect) @@ -2022,17 +2025,25 @@ public: } if (value.type == TypeArray) { if (!value.array) { NotImplemented_nid_no_patch(__func__); return 0; } - auto* items = value.array->items; - auto* head = items ? items->head : nullptr; - return Collection(head ? head->next : nullptr, head, items && !head, '[', ']', + auto position = [&value] { + if (value.type != TypeArray || !value.array) NotImplemented_nid_no_patch(__func__); + auto* items = value.array->items; + auto* head = items ? items->head : nullptr; + return SerializationPosition{head ? head->next : nullptr, head, items && !head}; + }; + return Collection(position, '[', ']', [](ArrayLink* link) { return link->next; }, [this](ArrayLink* link) { return SerializeValue(link->value); }); } if (value.type == TypeObject) { if (!value.object) { NotImplemented_nid_no_patch(__func__); return 0; } - auto* items = value.object->items; - auto* head = items ? items->head : nullptr; - return Collection(head ? head->left : nullptr, head, items && !head, '{', '}', + auto position = [&value] { + if (value.type != TypeObject || !value.object) NotImplemented_nid_no_patch(__func__); + auto* items = value.object->items; + auto* head = items ? items->head : nullptr; + return SerializationPosition{head ? head->left : nullptr, head, items && !head}; + }; + return Collection(position, '{', '}', [](ObjectLink* link) { return NextObjectLink(link); }, [this](ObjectLink* link) { Escape(link->pair.key); Character(':'); return SerializeValue(link->pair.value); }); } @@ -2076,21 +2087,25 @@ private: } Character('"'); } - template - int Collection(TLink* first, TLink* head, bool invalid, char opening, char closing, TNext next, TSerialize serialize) { + template + int Collection(TPosition position, char opening, char closing, TNext next, TSerialize serialize) { Character(opening); int status = Emit(); if (status < 0) return status; { - SerializationIterator iterator(first, invalid); - if (iterator.Different(head, invalid)) { + auto current = position(); + SerializationIterator iterator(current.first, current.invalid); + current = position(); + if (iterator.Different(current.head, current.invalid)) { do { - if (iterator.Different(first, invalid)) Character(','); + current = position(); + if (iterator.Different(current.first, current.invalid)) Character(','); if (!*iterator.state) { NotImplemented_nid_no_patch(__func__); return 0; } status = serialize(*iterator.state); if (status < 0) break; *iterator.state = next(*iterator.state); - } while (iterator.Different(head, invalid)); + current = position(); + } while (iterator.Different(current.head, current.invalid)); } } if (status < 0) return status; @@ -2099,6 +2114,8 @@ private: } }; +int APS5_VABI SerializationSucceeded(String&, void*) { return 0; } + int SerializeCallbackValue(Value* value, String* destination, SerializationCallback callback, void* context, bool collect) { { auto& state = State(); @@ -2935,6 +2952,12 @@ int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringEPFiS3_PvES4_(Value* self return SerializeCallbackValue(self, output, callback, context, true); } int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value* self, String* out) { + { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.allocation) return JsonErrorNotInitialized; + } + if (ModernValues()) return SerializeCallbackValue(self, out, SerializationSucceeded, nullptr, true); std::string text; const int result = Serialize(text, *self); AppendText(*out, text); diff --git a/core/libs/tests/GuestJsonSerializationCallback.cpp b/core/libs/tests/GuestJsonSerializationCallback.cpp index 933204be64..bffb7adf8c 100644 --- a/core/libs/tests/GuestJsonSerializationCallback.cpp +++ b/core/libs/tests/GuestJsonSerializationCallback.cpp @@ -19,6 +19,9 @@ struct Pair { String key; std::uint64_t reserved; Value value; }; struct Parameter { void* allocator; void* context; std::size_t bufferSize; std::int32_t mode; std::uint32_t reserved[3]; }; using SerializationCallback = int (APS5_VABI*)(String&, void*); extern "C" { +void APS5_VABI _ZN3sce4Json5Array10push_frontERKNS0_5ValueE(Array*, const Value*); +const Array* APS5_VABI _ZNK3sce4Json5Value8getArrayEv(const Value*); +int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); int APS5_VABI _ZN3sce4Json5Value9serializeEPFiRNS0_6StringEPvES4_(Value*, SerializationCallback, void*); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringEPFiS3_PvES4_(Value*, String*, SerializationCallback, void*); String* APS5_VABI _ZN3sce4Json6String6appendEPKc(String*, const char*); @@ -116,18 +119,46 @@ static int APS5_VABI Observe(String& chunk, void* context) { if (trace.mutate) _ZN3sce4Json6String6appendEPKc(&chunk, "!"); return static_cast(trace.chunks.size()) == trace.stopAt ? -77 : trace.result; } +struct CollectionChange { + const Array* array; + int mode; + std::vector chunks; +}; +static int APS5_VABI PrependAtOpening(String& chunk, void* context) { + auto& change = *static_cast(context); + change.chunks.emplace_back(_ZNK3sce4Json6String5c_strEv(&chunk)); + if (change.chunks.size() == 1) { + Value value{}; _ZN3sce4Json5ValueC1El(&value, 0); + if (change.mode == 0) _ZN3sce4Json5Array10push_frontERKNS0_5ValueE(const_cast(change.array), &value); + else if (change.mode == 1) _ZN3sce4Json5Array5clearEv(const_cast(change.array)); + else _ZN3sce4Json5Array9push_backERKNS0_5ValueE(const_cast(change.array), &value); + _ZN3sce4Json5ValueD1Ev(&value); + } + return 0; +} int main() { ArrayTrace allocation; ArrayAllocator allocator; Parameter parameter{&allocator, &allocation, 8192, 2, {}}; Value root{}; _ZN3sce4Json5ValueC1Ev(&root); SerializationTrace before; Require(static_cast(_ZN3sce4Json5Value9serializeEPFiRNS0_6StringEPvES4_(&root, Observe, &before)) == 0x80848110 && before.chunks.empty(), "Callback serialization before initialization must refuse without invoking the callback"); + String beforeOutput{}; + Require(static_cast(_ZN3sce4Json5Value9serializeERNS0_6StringE(&root, &beforeOutput)) == 0x80848110 && !beforeOutput.storage, "Ordinary serialization before initialization must refuse without constructing output storage"); std::uint8_t initializer = 0; _ZN3sce4Json11InitializerC1Ev(&initializer); Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(&initializer, ¶meter) == 0, "Initialize serialization callbacks"); const char json[] = R"({"a":[1,true,"x\n/\u007f"],"b":{}})"; Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, json, sizeof(json) - 1) == 0, "Parse nested callback serialization input"); const std::vector chunks{"{", "\"a\":[", "1", ",true", ",\"x\\n\\/\\u007f\"", "]", ",\"b\":{", "}", "}"}; const auto baseline = allocation.live.size(); + String ordinary{}; + _ZN3sce4Json6StringC1EPKc(&ordinary, "prefix"); + Require(_ZN3sce4Json5Value9serializeERNS0_6StringE(&root, &ordinary) == 0, "Ordinary serialization must use the collecting protocol with a successful internal sink"); + std::string expectedOrdinary = "prefix"; + for (const auto& chunk : chunks) expectedOrdinary += chunk; + Require(std::string(_ZNK3sce4Json6String5c_strEv(&ordinary)) == expectedOrdinary, "Ordinary serialization must preserve the same slash and DEL escaping as callback serialization"); + _ZN3sce4Json6StringD1Ev(&ordinary); + Require(allocation.live.size() == baseline, "Ordinary serialization must release its temporary buffer and collection iterators"); + for (bool collect : {false, true}) for (bool mutate : {false, true}) for (int stop : {-1, 1, 3, 9}) { String output{}; _ZN3sce4Json6StringC1EPKc(&output, "prefix"); SerializationTrace trace; trace.stopAt = stop; trace.mutate = mutate; @@ -146,6 +177,16 @@ int main() { _ZN3sce4Json6StringD1Ev(&output); Require(allocation.live.size() == baseline, "Successful and interrupted traversals must release all temporary buffers and iterator states"); } + for (int mode : {0, 1, 2}) { + Value changing{}; _ZN3sce4Json5ValueC1Ev(&changing); + const char arrayJson[] = "[1,2]"; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&changing, arrayJson, sizeof(arrayJson) - 1) == 0, "Parse collection mutation input"); + CollectionChange change{_ZNK3sce4Json5Value8getArrayEv(&changing), mode, {}}; + const auto expected = mode == 0 ? std::vector({"[", "0", ",1", ",2", "]"}) : mode == 1 ? std::vector({"[", "]"}) : std::vector({"[", "1", ",2", ",0", "]"}); + Require(_ZN3sce4Json5Value9serializeEPFiRNS0_6StringEPvES4_(&changing, PrependAtOpening, &change) == 0 && change.chunks == expected, "Serialization must obtain the live collection after the opening callback prepends, clears or appends entries"); + _ZN3sce4Json5ValueD1Ev(&changing); + Require(allocation.live.size() == baseline, "Collection mutation during serialization must leave no temporary or transferred allocations"); + } Value scalar{}; _ZN3sce4Json5ValueC1El(&scalar, 1234); for (bool collect : {false, true}) for (bool once : {false, true}) for (int budget : {0, 1, 2, 3}) { String output{}; _ZN3sce4Json6StringC1EPKc(&output, "p"); @@ -165,6 +206,20 @@ int main() { _ZN3sce4Json6StringD1Ev(&output); Require(allocation.live.size() == baseline, "Rejected serialization allocations must not leak admitted storage"); } + for (bool once : {false, true}) for (int budget : {0, 1, 2, 3}) { + String output{}; _ZN3sce4Json6StringC1EPKc(&output, "p"); + const auto requests = allocation.requests.size(); const auto notices = allocation.notifications; + allocation.budget = budget; allocation.once = once; + const int status = _ZN3sce4Json5Value9serializeERNS0_6StringE(&scalar, &output); + allocation.budget = -1; allocation.once = false; + Require(status == 0 && std::string(_ZNK3sce4Json6String5c_strEv(&output)) == (budget >= 3 ? "p1234" : "p"), "Ordinary serialization must preserve the native partial-output and return contracts under allocation failure"); + std::vector expected{24}; + if (budget >= 1) expected.push_back(21); + if (budget >= 2) expected.push_back(22); + Require(std::vector(allocation.requests.begin() + requests, allocation.requests.end()) == expected && allocation.notifications == notices + (budget < 3 ? 1 : 0), "Ordinary serialization must use the same allocator requests and failure notifications as the collecting callback path"); + _ZN3sce4Json6StringD1Ev(&output); + Require(allocation.live.size() == baseline, "Ordinary serialization failures must not leak storage"); + } _ZN3sce4Json5ValueD1Ev(&scalar); _ZN3sce4Json5ValueD1Ev(&root); Require(_ZN3sce4Json11Initializer9terminateEv(&initializer) == 0 && allocation.live.empty(), "Terminate callback serialization allocation context"); return 0; diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 72b8d2fd4d..70fa0d94fa 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -62,4 +62,4 @@ - [Value keyed lookup](../../../core/libs/prx/libSceJson2/Export.cpp) - valid string-key overloads and C-string key-construction failures follow the observed iterator allocation and fallback protocol. Missing object handles, corrupted tree storage, lifecycle reentry during lookup and remaining older-SDK callback combinations remain unverified; invalid reference arguments are explicitly unsupported. -- [Value callback serialization](../../../core/libs/prx/libSceJson2/Export.cpp) - modern chunk boundaries, callback return propagation, output mutation and scalar allocation rejection are reference-tested. Older-SDK serialization is explicitly unsupported until its parent-rebinding protocol is verified. Structural mutation of the traversed collection, nested iterator failures, non-finite recovery allocation failures and lifecycle reentry require further audit. Null callback or collecting-output references are explicitly unsupported. The existing non-callback serializer still follows a separate path and needs the same streaming/allocation audit. +- [Value callback serialization](../../../core/libs/prx/libSceJson2/Export.cpp) - modern chunk boundaries, callback return propagation, output mutation and scalar allocation rejection are reference-tested. Older-SDK serialization is explicitly unsupported until its parent-rebinding protocol is verified. Array prepend, clear and append during the opening callback are reference-tested; other structural mutations, nested iterator failures, non-finite recovery allocation failures and lifecycle reentry require further audit. Null callback or collecting-output references are explicitly unsupported. The modern non-callback serializer shares the verified collecting path, including escaping and scalar allocation failures. Its older-SDK path retains the previous implementation and still needs the streaming/allocation audit.