diff --git a/API/hermes/TracingRuntime.cpp b/API/hermes/TracingRuntime.cpp index 31fac258e98..dbfdce201fa 100644 --- a/API/hermes/TracingRuntime.cpp +++ b/API/hermes/TracingRuntime.cpp @@ -757,6 +757,14 @@ jsi::String TracingRuntime::createStringFromUtf16( return res; } +jsi::Value TracingRuntime::createValueFromJsonUtf8( + const uint8_t *json, + size_t length) { + // Deliberately not RD::createValueFromJsonUtf8, which would parse in the + // plain runtime and return a value that was never recorded. See the header. + return jsi::Runtime::createValueFromJsonUtf8(json, length); +} + jsi::PropNameID TracingRuntime::createPropNameIDFromAscii( const char *str, size_t length) { diff --git a/API/hermes/TracingRuntime.h b/API/hermes/TracingRuntime.h index bc338c9d7c5..59c72b17089 100644 --- a/API/hermes/TracingRuntime.h +++ b/API/hermes/TracingRuntime.h @@ -69,6 +69,13 @@ class TracingRuntime : public jsi::RuntimeDecorator jsi::String createStringFromUtf8(const uint8_t *utf8, size_t length) override; jsi::String createStringFromUtf16(const char16_t *utf16, size_t length) override; + /// Parse JSON with the JS-based default implementation in jsi::Runtime + /// rather than forwarding to the plain runtime. The default calls back into + /// this runtime for global(), getProperty() and call(), so parsing is + /// recorded as ordinary records and the returned value gets a def, without + /// needing a dedicated record type. + jsi::Value createValueFromJsonUtf8(const uint8_t *json, size_t length) + override; std::string utf8(const jsi::PropNameID &) override; jsi::PropNameID createPropNameIDFromAscii(const char *str, size_t length) diff --git a/API/jsi/jsi/decorator.h b/API/jsi/jsi/decorator.h index f101c1832e6..7a049ec4c00 100644 --- a/API/jsi/jsi/decorator.h +++ b/API/jsi/jsi/decorator.h @@ -148,6 +148,9 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { Instrumentation& instrumentation() override { return *this; } + Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { + return plain_.createValueFromJsonUtf8(json, length); + } protected: // plain is generally going to be a reference to an object managed diff --git a/API/jsi/jsi/test/testlib.cpp b/API/jsi/jsi/test/testlib.cpp index 9c056ef0379..52c6e732f00 100644 --- a/API/jsi/jsi/test/testlib.cpp +++ b/API/jsi/jsi/test/testlib.cpp @@ -1926,6 +1926,54 @@ TEST_P(JSITest, CreateFromUtf16Test) { EXPECT_EQ(cp, 55357); // 0xD83D in decimal } +TEST_P(JSITest, CreateValueFromJsonUtf8Test) { + uint8_t jsonText[] = "{\"a\": 1}"; + + // This Runtime Decorator is used to test the default createValueFromJsonUtf8 + // implementation for VMs that do not provide their own implementation + class DefaultRD : public RuntimeDecorator { + public: + explicit DefaultRD(Runtime& rt) : RuntimeDecorator(rt) {} + + Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { + return Runtime::createValueFromJsonUtf8(json, length); + } + }; + + DefaultRD defaultRd(rt); + Object obj = + Value::createFromJsonUtf8(defaultRd, jsonText, sizeof(jsonText) - 1) + .getObject(defaultRd); + EXPECT_EQ(obj.getProperty(defaultRd, "a").getNumber(), 1); + + // A RuntimeDecorator must forward createValueFromJsonUtf8 to the runtime it + // decorates, like every other method. + class CountingRD : public RuntimeDecorator { + public: + explicit CountingRD(Runtime& rt) : RuntimeDecorator(rt) {} + + Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { + ++count; + return plain().createValueFromJsonUtf8(json, length); + } + + int count = 0; + }; + + // Overrides nothing, so it should forward everything to CountingRD. + class OuterRD : public RuntimeDecorator { + public: + explicit OuterRD(Runtime& rt) : RuntimeDecorator(rt) {} + }; + + CountingRD counting(rt); + OuterRD outer(counting); + obj = outer.createValueFromJsonUtf8(jsonText, sizeof(jsonText) - 1) + .getObject(outer); + EXPECT_EQ(counting.count, 1); + EXPECT_EQ(obj.getProperty(outer, "a").getNumber(), 1); +} + TEST_P(JSITest, GetStringDataTest) { // This Runtime Decorator is used to test the default getStringData // implementation for VMs that do not provide their own implementation diff --git a/unittests/API/APITest.cpp b/unittests/API/APITest.cpp index aae9e34229d..4187e36145e 100644 --- a/unittests/API/APITest.cpp +++ b/unittests/API/APITest.cpp @@ -185,6 +185,16 @@ TEST_P(HermesRuntimeTest, StrictHostFunctionBindTest) { .getBool()); } +TEST_P(HermesRuntimeTest, CreateValueFromJsonUtf8IgnoresGlobalJSON) { + // Hermes parses JSON natively, so replacing the global JSON.parse must not + // change the result, also when the runtime is decorated (e.g. thread-safe). + eval("JSON.parse = function() { return 'hijacked'; }"); + uint8_t json[] = "{\"a\": 1}"; + Value v = Value::createFromJsonUtf8(*rt, json, sizeof(json) - 1); + ASSERT_TRUE(v.isObject()); + EXPECT_EQ(v.getObject(*rt).getProperty(*rt, "a").getNumber(), 1); +} + TEST_P(HermesRuntimeTest, DrainMicrotasksThrowsFinalizationRegistryError) { eval(R"( var target = {}; diff --git a/unittests/API/SynthTraceTest.cpp b/unittests/API/SynthTraceTest.cpp index 6e3b5cf7dbb..736e8899862 100644 --- a/unittests/API/SynthTraceTest.cpp +++ b/unittests/API/SynthTraceTest.cpp @@ -2259,6 +2259,26 @@ TEST_F(SynthTraceReplayTest, CreateJSErrorReplay) { } } +/// JSON parsed from native code must be recorded, so that the tracing runtime +/// knows the resulting object when native code uses it afterwards. +TEST_F(SynthTraceReplayTest, CreateValueFromJsonUtf8Replay) { + { + auto &rt = *traceRt; + uint8_t json[] = "{\"a\": 1, \"b\": \"str\"}"; + jsi::Object obj = jsi::Value::createFromJsonUtf8(rt, json, sizeof(json) - 1) + .getObject(rt); + rt.global().setProperty(rt, "a", obj.getProperty(rt, "a")); + rt.global().setProperty(rt, "parsed", obj); + } + replay(); + { + auto &rt = *replayRt; + EXPECT_EQ(rt.global().getProperty(rt, "a").getNumber(), 1); + jsi::Object parsed = rt.global().getPropertyAsObject(rt, "parsed"); + EXPECT_EQ(parsed.getProperty(rt, "b").getString(rt).utf8(rt), "str"); + } +} + /// This test is here to make sure that the replayed string match is happening /// during replay. TEST_F(SynthTraceReplayTest, PropNameIDUtf8) {