Skip to content
File

Blob: src/workerd/jsg/ser-test.c++

9.3 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "jsg-test.h"
6#include "ser.h"
7 
8namespace workerd::jsg::test {
9namespace {
10 
11V8System v8System;
12class ContextGlobalObject: public jsg::Object, public ContextGlobal {};
13 
14kj::Array<kj::byte> lastSerializedData;
15 
16struct SerTestContext: public ContextGlobalObject {
17 enum class SerializationTag {
18 FOO,
19 BAR,
20 BAZ,
21 QUX,
22 };
23 
24 struct Foo: public jsg::Object {
25 uint i;
26 Foo(uint i): i(i) {}
27 
28 static jsg::Ref<Foo> constructor(jsg::Lock& js, uint i) {
29 return js.alloc<Foo>(i);
30 }
31 
32 int getI() {
33 return i;
34 }
35 
36 JSG_RESOURCE_TYPE(Foo) {
37 JSG_READONLY_PROTOTYPE_PROPERTY(i, getI);
38 }
39 
40 void serialize(jsg::Lock& js, jsg::Serializer& serializer) {
41 serializer.writeRawUint32(i);
42 }
43 static jsg::Ref<Foo> deserialize(Lock& js, SerializationTag tag, Deserializer& deserializer) {
44 KJ_ASSERT(tag == SerializationTag::FOO);
45 
46 // Intentionally deserialize differently so we can detect it.
47 return js.alloc<Foo>(deserializer.readRawUint32() + 2);
48 }
49 JSG_SERIALIZABLE(SerializationTag::FOO);
50 };
51 
52 struct Bar: public jsg::Object {
53 kj::String text;
54 Bar(kj::String text): text(kj::mv(text)) {}
55 
56 static jsg::Ref<Bar> constructor(jsg::Lock& js, kj::String text) {
57 return js.alloc<Bar>(kj::mv(text));
58 }
59 
60 kj::String getText() {
61 return kj::str(text);
62 }
63 
64 JSG_RESOURCE_TYPE(Bar) {
65 JSG_READONLY_PROTOTYPE_PROPERTY(text, getText);
66 }
67 
68 void serialize(jsg::Lock& js, jsg::Serializer& serializer) {
69 serializer.writeRawUint64(text.size());
70 serializer.writeRawBytes(text.asBytes());
71 }
72 static jsg::Ref<Bar> deserialize(Lock& js, SerializationTag tag, Deserializer& deserializer) {
73 KJ_ASSERT(tag == SerializationTag::BAR);
74 
75 size_t size = deserializer.readRawUint64();
76 auto bytes = deserializer.readRawBytes(size);
77 // Intentionally deserialize differently so we can detect it.
78 return js.alloc<Bar>(kj::str(bytes.asChars(), '!'));
79 }
80 JSG_SERIALIZABLE(SerializationTag::BAR);
81 };
82 
83 struct Baz: public jsg::Object {
84 bool serializeThrows;
85 Baz(bool serializeThrows): serializeThrows(serializeThrows) {}
86 static jsg::Ref<Baz> constructor(jsg::Lock& js, bool serializeThrows) {
87 return js.alloc<Baz>(serializeThrows);
88 }
89 
90 JSG_RESOURCE_TYPE(Baz) {}
91 
92 void serialize(jsg::Lock& js, jsg::Serializer& serializer) {
93 JSG_REQUIRE(!serializeThrows, Error, "throw from serialize()");
94 }
95 static jsg::Ref<Bar> deserialize(Lock& js, SerializationTag tag, Deserializer& deserializer) {
96 JSG_FAIL_REQUIRE(Error, "throw from deserialize()");
97 }
98 JSG_SERIALIZABLE(SerializationTag::BAZ);
99 };
100 
101 // Qux is like Bar but serializes its string by converting it to a JS value first.
102 struct Qux: public jsg::Object {
103 kj::String text;
104 Qux(kj::String text): text(kj::mv(text)) {}
105 
106 static jsg::Ref<Qux> constructor(jsg::Lock& js, kj::String text) {
107 return js.alloc<Qux>(kj::mv(text));
108 }
109 
110 kj::String getText() {
111 return kj::str(text);
112 }
113 
114 JSG_RESOURCE_TYPE(Qux) {
115 JSG_READONLY_PROTOTYPE_PROPERTY(text, getText);
116 }
117 
118 void serialize(
119 jsg::Lock& js, jsg::Serializer& serializer, const TypeHandler<kj::String>& stringHandler) {
120 // V2 prefers to serialize the string as a JS value.
121 serializer.write(js, JsValue(stringHandler.wrap(js, kj::str(text, '?'))));
122 }
123 static jsg::Ref<Qux> deserialize(Lock& js,
124 SerializationTag tag,
125 Deserializer& deserializer,
126 const TypeHandler<kj::String>& stringHandler) {
127 KJ_ASSERT(tag == SerializationTag::QUX);
128 
129 return js.alloc<Qux>(
130 KJ_ASSERT_NONNULL(stringHandler.tryUnwrap(js, deserializer.readValue(js))));
131 }
132 JSG_SERIALIZABLE(SerializationTag::QUX);
133 };
134 
135 JsValue roundTrip(Lock& js, JsValue in) {
136 auto content = ({
137 Serializer ser(js);
138 ser.write(js, in);
139 ser.release();
140 });
141 
142 auto result = ({
143 Deserializer deser(js, content);
144 deser.readValue(js);
145 });
146 
147 // Save the last serialization off to the side.
148 lastSerializedData = kj::mv(content.data);
149 
150 return result;
151 }
152 
153 JSG_RESOURCE_TYPE(SerTestContext) {
154 JSG_NESTED_TYPE(Foo);
155 JSG_NESTED_TYPE(Bar);
156 JSG_NESTED_TYPE(Baz);
157 JSG_NESTED_TYPE(Qux);
158 JSG_METHOD(roundTrip);
159 }
160};
161JSG_DECLARE_ISOLATE_TYPE(SerTestIsolate,
162 SerTestContext,
163 SerTestContext::Foo,
164 SerTestContext::Bar,
165 SerTestContext::Baz,
166 SerTestContext::Qux);
167 
168// Define a whole second JSG isolate type that contains "updated" code where Bar no longer wraps
169// a string, it wraps an arbitrary value.
170struct SerTestContextV2: public ContextGlobalObject {
171 enum class SerializationTag { FOO, BAR_OLD, BAZ, QUX, BAR_V2 };
172 
173 struct Bar: public jsg::Object {
174 JsRef<JsValue> val;
175 Bar(JsRef<JsValue> val): val(kj::mv(val)) {}
176 
177 static jsg::Ref<Bar> constructor(jsg::Lock& js, JsRef<JsValue> val) {
178 return js.alloc<Bar>(kj::mv(val));
179 }
180 
181 JsRef<JsValue> getVal(Lock& js) {
182 return val.addRef(js);
183 }
184 
185 JSG_RESOURCE_TYPE(Bar) {
186 JSG_READONLY_PROTOTYPE_PROPERTY(val, getVal);
187 }
188 
189 void serialize(jsg::Lock& js, jsg::Serializer& serializer) {
190 // V2 just writes a value!
191 serializer.write(js, JsValue(val.getHandle(js)));
192 }
193 static jsg::Ref<Bar> deserialize(Lock& js, SerializationTag tag, Deserializer& deserializer) {
194 if (tag == SerializationTag::BAR_OLD) {
195 // Oh, it's an old value.
196 size_t size = deserializer.readRawUint64();
197 auto bytes = deserializer.readRawBytes(size);
198 
199 return js.alloc<Bar>(JsRef<JsValue>(js, js.str(kj::str("old:", bytes.asChars()))));
200 } else {
201 KJ_ASSERT(tag == SerializationTag::BAR_V2);
202 
203 return js.alloc<Bar>(JsRef<JsValue>(js, deserializer.readValue(js)));
204 }
205 }
206 JSG_SERIALIZABLE(SerializationTag::BAR_V2, SerializationTag::BAR_OLD);
207 };
208 
209 JsValue roundTrip(Lock& js, JsValue in) {
210 auto content = ({
211 Serializer ser(js);
212 ser.write(js, in);
213 ser.release();
214 });
215 
216 auto result = ({
217 Deserializer deser(js, content);
218 deser.readValue(js);
219 });
220 
221 // Save the last serialization off to the side.
222 lastSerializedData = kj::mv(content.data);
223 
224 return result;
225 }
226 
227 JsValue deserializeLast(Lock& js) {
228 Deserializer deser(js, lastSerializedData);
229 return deser.readValue(js);
230 }
231 
232 JSG_RESOURCE_TYPE(SerTestContextV2) {
233 JSG_NESTED_TYPE(Bar);
234 JSG_METHOD(roundTrip);
235 JSG_METHOD(deserializeLast);
236 }
237};
238JSG_DECLARE_ISOLATE_TYPE(SerTestIsolateV2, SerTestContextV2, SerTestContextV2::Bar);
239 
240KJ_TEST("serialization") {
241 Evaluator<SerTestContext, SerTestIsolate> e(v8System);
242 
243 // Test serializing built-in values.
244 e.expectEval("roundTrip(123)", "number", "123");
245 e.expectEval("JSON.stringify(roundTrip({foo: 123}))", "string", "{\"foo\":123}");
246 
247 // Test serializing host objects.
248 e.expectEval("roundTrip(new Foo(123)).i", "number", "125");
249 e.expectEval("roundTrip(new Qux(\"hello\")).text", "string", "hello?");
250 e.expectEval("roundTrip(new Bar(\"hello\")).text", "string", "hello!");
251 
252 // Test throwing from serialize/deserialize
253 e.expectEval("roundTrip(new Baz(true)).text", "throws", "Error: throw from serialize()");
254 e.expectEval("roundTrip(new Baz(false)).text", "throws", "Error: throw from deserialize()");
255 
256 // Let's set up the "new version" of the code.
257 Evaluator<SerTestContextV2, SerTestIsolateV2> e2(v8System);
258 
259 // This will deserialize the last-serialized bytes from above, where we serialized Bar("hello").
260 // However, it is using a "new version" of the code where Bar's serialization has changed, but
261 // the old version is still accepted.
262 e2.expectEval("deserializeLast().val", "string", "old:hello");
263 
264 // Also try round-tripping the new version. It now accepts arbitrary values, not just strings.
265 e2.expectEval("roundTrip(new Bar(123)).val", "number", "123");
266 
267 // Note that cycles through host objects are correctly serialized!
268 //
269 // V8 BUG ALERT: The below works if we use `obj` as the root of serialization, but NOT if we
270 // use `bar` as the root. The reason is a flaw in the design of V8's callbacks for parsing
271 // host objects. V8 makes a single callback to the embedder which fully reads the object and
272 // returns a handle. However, this means that V8 cannot put the object into the backreference
273 // table until this callback returns. If, while parsing the object, we encounter a
274 // backreference to the object itself (a cycle), the deserializer will find the backreference
275 // is not in the table and therefore raises an error. This is not a problem for native objects
276 // because V8 allocates the object first, then immediately adds it to the backreference table,
277 // and only then parses its content -- and this is why everything works fine if we start with
278 // a native object as the root, as in this test. The API for host objects needs to be extended
279 // somehow to allow the object to be inserted into the table before parsing its content.
280 e2.expectEval("let obj = {i: 321};\n"
281 "let bar = new Bar(obj);\n"
282 "obj.bar = bar;\n"
283 "roundTrip(obj).bar.val.bar.val.bar.val.i",
284 "number", "321");
285}
286} // namespace
287} // namespace workerd::jsg::test