File
Blob: src/workerd/jsg/ser.h
| 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 | #pragma once |
| 6 | |
| 7 | #include <workerd/jsg/jsg.h> |
| 8 | |
| 9 | #include <v8-value-serializer.h> |
| 10 | |
| 11 | #include <kj/vector.h> |
| 12 | |
| 13 | namespace workerd::jsg { |
| 14 | |
| 15 | // Wraps the v8::ValueSerializer and v8::ValueSerializer::Delegate implementation. |
| 16 | // Must be allocated on the stack, and requires that a v8::HandleScope exist in |
| 17 | // the stack. |
| 18 | // |
| 19 | // To declare a JSG_RESOURCE_TYPE as serializeable, you must declare two special methods |
| 20 | // `serialize()` and `deserialize()`, and also use the JSG_SERIALIZABLE macro, which must appear |
| 21 | // after the `JSG_RESOURCE_TYPE` block (NOT inside it). Example: |
| 22 | // |
| 23 | // class Foo: public jsg::Object { |
| 24 | // public: |
| 25 | // // ... |
| 26 | // |
| 27 | // JSG_RESOURCE_TYPE(Foo) { |
| 28 | // // ... |
| 29 | // } |
| 30 | // |
| 31 | // void serialize(jsg::Lock& js, jsg::Serializer& serializer); |
| 32 | // static jsg::Ref<Foo> deserialize(Lock& js, MyTag tag, Deserializer& deserializer); |
| 33 | // JSG_SERIALIZABLE(MyTag::FOO_V2, MyTag::FOO_V1); |
| 34 | // |
| 35 | // // ... |
| 36 | // }; |
| 37 | // |
| 38 | // * `MyTag` is some enum type declared in the application which enumerates all known serializeable |
| 39 | // types. This can be any enum, but it is suggested that all types in the application use the |
| 40 | // same enum type, and the numeric values of the tags must never change. A Cap'n Proto enum is |
| 41 | // suggested. |
| 42 | // * `JSG_SERIALIZABLE`'s parameters are a list of tags that encode this type. There may be more |
| 43 | // than one tag listed to support versioning. The first tag listed is the current version, which |
| 44 | // is the format that `serialize()` will write. The other tags are non-current versions that |
| 45 | // `deserialize()` accepts in addition to the current version. These are usually old versions, |
| 46 | // but could also include a new version that hasn't fully rolled out yet -- it will be necessary |
| 47 | // to fully roll out support for parsing a new version before anyone can start generating it. |
| 48 | // * The serialization system automatically handles writing and reading the tag values before |
| 49 | // calling the methods. |
| 50 | // * serialize() makes a series of calls to serializer.write*() methods to write the content of the |
| 51 | // object. |
| 52 | // * deserialize() makes a corresponding series of calls to deserializer.read*() methods to read |
| 53 | // the object. These must be the exact corresponding calls in the same order as serialize() |
| 54 | // would have made them. The sequence can never change once data has been written for a given |
| 55 | // tag version; the only way to change is to define a new version. |
| 56 | // * Both `serialize()` and `deserialize()` can take additional arguments of the form |
| 57 | // `const jsg::TypeHandler<SomeType>&`, which will automatically be filled in with the |
| 58 | // corresponding type handler. This is useful if the serializer wants to, say, assemble a |
| 59 | // JSG_STRUCT, convert it into an actual JS object, and serialize that. |
| 60 | class Serializer final: v8::ValueSerializer::Delegate { |
| 61 | public: |
| 62 | // "Externals" are values which can be serialized, but refer to some external resource, rather |
| 63 | // than being self-contained. The way externals are supported depends on the serialization |
| 64 | // context: passing externals over RPC, for example, is completely different from storing them |
| 65 | // to disk. |
| 66 | // |
| 67 | // A `Serializer` instance may have an `ExternalHandler` which can be used when serializing |
| 68 | // externals. This type has no methods, but is meant to be subclassed. A host object which |
| 69 | // represents an external and is trying to serialize itself should use dynamic_cast to try to |
| 70 | // downcast `ExternalHandler` to any particular handler interface it supports. If the handler |
| 71 | // doesn't implement any supported subclass, then serialization is not possible, and an |
| 72 | // appropriate exception should be thrown. |
| 73 | class ExternalHandler { |
| 74 | public: |
| 75 | // Tries to serialize a function as an external. The default implementation throws |
| 76 | // DataCloneError. |
| 77 | virtual void serializeFunction( |
| 78 | jsg::Lock& js, jsg::Serializer& serializer, v8::Local<v8::Function> func); |
| 79 | |
| 80 | // Tries to serialize a proxy as an external. The default implementation throws |
| 81 | // DataCloneError. |
| 82 | // |
| 83 | // TODO(cleanup): This is a bit of a hack to support an RpcTarget that is wrapped in a Proxy. |
| 84 | // For RpcTarget specifically, this works because inheriting RpcTarget is just a marker that |
| 85 | // opts into serializing by creating a stub pointing at the object -- we can create a stub |
| 86 | // pointing at the proxy instead. For any other type, serializing a Proxy probably isn't |
| 87 | // possible, since the serialization wouldn't actually capture the Proxy logic? But I'm |
| 88 | // not 100% certain of that. If we find other use cases in the future it may turn out that |
| 89 | // they call for a different design. |
| 90 | virtual void serializeProxy( |
| 91 | jsg::Lock& js, jsg::Serializer& serializer, v8::Local<v8::Proxy> proxy); |
| 92 | }; |
| 93 | |
| 94 | struct Options { |
| 95 | // When set, overrides the default wire format version with the one provided. |
| 96 | kj::Maybe<uint32_t> version; |
| 97 | // When set to true, the serialization header is not written to the output buffer. |
| 98 | bool omitHeader = false; |
| 99 | |
| 100 | // The structured clone spec states that instances of classes are serialized as if they were |
| 101 | // plain objects: their "own" properties are serialized, but the prototype is completely |
| 102 | // ignored. Upon deserialization, the value is no longer class instance, it's just a plain |
| 103 | // object. This is probably not useful behavior in any real use case, but that's what the spec |
| 104 | // says. |
| 105 | // |
| 106 | // If this flag is true, we'll follow the spec. If it's false, then instances of classes |
| 107 | // (i.e. objects which have a prototype which is not Object.prototype) will not be serializable |
| 108 | // (they throw DataCloneError). |
| 109 | // |
| 110 | // TODO(someday): Perhaps we could create a framework for application-defined classes to define |
| 111 | // their own serializers. However, we would need to be extremely careful about this when |
| 112 | // deserializing data from a possibly-malicious source. Such serialization frameworks have |
| 113 | // a history of creating security bugs as people declare various classes serializable without |
| 114 | // fully thinking through what an attacker could do by sending them an instance of that class |
| 115 | // when it isn't expected. Probably, we just shouldn't support this over RPC at all. For DO |
| 116 | // storage, it could be OK since the application would only be deserializing objects it wrote |
| 117 | // itself, but it may not be worth it to support for only that use case. |
| 118 | bool treatClassInstancesAsPlainObjects = true; |
| 119 | |
| 120 | // ExternalHandler, if any. Typically this would be allocated on the stack just before the |
| 121 | // Serializer. |
| 122 | kj::Maybe<ExternalHandler&> externalHandler; |
| 123 | }; |
| 124 | |
| 125 | struct Released { |
| 126 | // The serialized data. |
| 127 | kj::Array<kj::byte> data; |
| 128 | |
| 129 | // All instances of SharedArrayBuffer seen during serialization. Pass these along to the |
| 130 | // deserializer to achieve actual sharing of buffers. |
| 131 | kj::Array<std::shared_ptr<v8::BackingStore>> sharedArrayBuffers; |
| 132 | |
| 133 | // All ArrayBuffers that were passed to `transfer()`. |
| 134 | kj::Array<std::shared_ptr<v8::BackingStore>> transferredArrayBuffers; |
| 135 | }; |
| 136 | |
| 137 | explicit Serializer(Lock& js): Serializer(js, {}) {} |
| 138 | explicit Serializer(Lock& js, Options options); |
| 139 | inline ~Serializer() noexcept(true) {} // noexcept(true) because Delegate's is noexcept |
| 140 | |
| 141 | KJ_DISALLOW_COPY_AND_MOVE(Serializer); |
| 142 | |
| 143 | kj::Maybe<ExternalHandler&> getExternalHandler() { |
| 144 | return externalHandler; |
| 145 | } |
| 146 | |
| 147 | // Write a value. |
| 148 | // |
| 149 | // You can call this multiple times to write multiple values, then call `readValue()` the same |
| 150 | // number of times on the deserialization side. |
| 151 | void write(Lock& js, const JsValue& value); |
| 152 | |
| 153 | // Implements the `transfer` option of `structuredClone()`. Pass each item in the transfer array |
| 154 | // to this method before calling `write()`. This gives the Serializer permission to serialize |
| 155 | // these values by detaching them (destroying the caller's handle) rather than make a copy. The |
| 156 | // detached content will show up as part of `Released`, where it should then be delivered to the |
| 157 | // Deserializer later. |
| 158 | void transfer(Lock& js, const JsValue& value); |
| 159 | |
| 160 | Released release(); |
| 161 | |
| 162 | void writeRawUint32(uint32_t i) { |
| 163 | ser.WriteUint32(i); |
| 164 | } |
| 165 | void writeRawUint64(uint64_t i) { |
| 166 | ser.WriteUint64(i); |
| 167 | } |
| 168 | |
| 169 | void writeRawBytes(kj::ArrayPtr<const kj::byte> bytes) { |
| 170 | ser.WriteRawBytes(bytes.begin(), bytes.size()); |
| 171 | } |
| 172 | |
| 173 | // Write a size followed by bytes. |
| 174 | void writeLengthDelimited(kj::ArrayPtr<const kj::byte> bytes) { |
| 175 | writeRawUint32(bytes.size()); |
| 176 | writeRawBytes(bytes); |
| 177 | } |
| 178 | void writeLengthDelimited(kj::StringPtr text) { |
| 179 | writeLengthDelimited(text.asBytes()); |
| 180 | } |
| 181 | |
| 182 | private: |
| 183 | // Throw a DataCloneError, complaining that the given object cannot be serialized. (This is |
| 184 | // similar to ThrowDataCloneError() except that it formats the error message itself, and it |
| 185 | // is expected to be called from KJ-ish code so it throws JsExceptionThrown rather than |
| 186 | // returning.) |
| 187 | [[noreturn]] void throwDataCloneErrorForObject(jsg::Lock& js, v8::Local<v8::Object> obj); |
| 188 | |
| 189 | // v8::ValueSerializer::Delegate implementation |
| 190 | void ThrowDataCloneError(v8::Local<v8::String> message) override; |
| 191 | bool HasCustomHostObject(v8::Isolate* isolate) override; |
| 192 | v8::Maybe<bool> IsHostObject(v8::Isolate* isolate, v8::Local<v8::Object> object) override; |
| 193 | v8::Maybe<bool> WriteHostObject(v8::Isolate* isolate, v8::Local<v8::Object> object) override; |
| 194 | |
| 195 | v8::Maybe<uint32_t> GetSharedArrayBufferId( |
| 196 | v8::Isolate* isolate, v8::Local<v8::SharedArrayBuffer> sab) override; |
| 197 | |
| 198 | kj::Maybe<ExternalHandler&> externalHandler; |
| 199 | |
| 200 | kj::Vector<jsg::JsRef<JsValue>> sharedArrayBuffers; |
| 201 | kj::Vector<jsg::JsRef<JsValue>> arrayBuffers; |
| 202 | kj::Vector<std::shared_ptr<v8::BackingStore>> sharedBackingStores; |
| 203 | kj::Vector<std::shared_ptr<v8::BackingStore>> backingStores; |
| 204 | bool released = false; |
| 205 | bool treatClassInstancesAsPlainObjects; |
| 206 | bool treatErrorsAsHostObjects = false; |
| 207 | |
| 208 | // Initialized to point at the prototype of `Object` if and only if |
| 209 | // `treatClassInstancesAsPlainObjects` is false (in which case we will need to check against this |
| 210 | // prototype in IsHostObject()). |
| 211 | v8::Local<v8::Value> prototypeOfObject; |
| 212 | |
| 213 | // The actual ValueSerializer. Note that it's important to define this last because its |
| 214 | // constructor will call back to the Delegate, which is this object, so we hope that this object |
| 215 | // is fully initialized before that point! |
| 216 | v8::ValueSerializer ser; |
| 217 | }; |
| 218 | |
| 219 | // Wraps the v8::ValueDeserializer and v8::ValueDeserializer::Delegate implementation. |
| 220 | // Must be allocated on the stack, and requires that a v8::HandleScope exist in |
| 221 | // the stack. |
| 222 | class Deserializer final: v8::ValueDeserializer::Delegate { |
| 223 | public: |
| 224 | // Exactly like Serializer::ExternalHandler, but for Deserializer. |
| 225 | class ExternalHandler { |
| 226 | public: |
| 227 | virtual ~ExternalHandler() noexcept(false) = 0; |
| 228 | }; |
| 229 | |
| 230 | struct Options { |
| 231 | kj::Maybe<uint32_t> version; |
| 232 | bool readHeader = true; |
| 233 | |
| 234 | // When the enahnced error serialization feature is enabled, and we are deserializing |
| 235 | // a serialized error, this option controls whether to include the serialized stack |
| 236 | // property in the deserialized error. If false, the stack property is not restored |
| 237 | // and will instead be set to the captured stack at the time of deserialization. |
| 238 | // This flag has no effect if the enhanced error serialization feature is disabled, |
| 239 | // or the values being deserialized are not errors (or do not contain any error objects). |
| 240 | bool preserveStackInErrors = true; |
| 241 | |
| 242 | // ExternalHandler, if any. Typically this would be allocated on the stack just before the |
| 243 | // Deserializer. |
| 244 | kj::Maybe<ExternalHandler&> externalHandler; |
| 245 | }; |
| 246 | |
| 247 | explicit Deserializer(Lock& js, |
| 248 | kj::ArrayPtr<const kj::byte> data, |
| 249 | kj::Maybe<kj::ArrayPtr<std::shared_ptr<v8::BackingStore>>> transferredArrayBuffers = kj::none, |
| 250 | kj::Maybe<kj::ArrayPtr<std::shared_ptr<v8::BackingStore>>> sharedArrayBuffers = kj::none, |
| 251 | kj::Maybe<Options> maybeOptions = kj::none); |
| 252 | |
| 253 | explicit Deserializer( |
| 254 | Lock& js, Serializer::Released& released, kj::Maybe<Options> maybeOptions = kj::none); |
| 255 | |
| 256 | ~Deserializer() noexcept(true) {} // noexcept(true) because Delegate's is noexcept |
| 257 | |
| 258 | KJ_DISALLOW_COPY_AND_MOVE(Deserializer); |
| 259 | |
| 260 | kj::Maybe<ExternalHandler&> getExternalHandler() { |
| 261 | return externalHandler; |
| 262 | } |
| 263 | |
| 264 | JsValue readValue(Lock& js); |
| 265 | |
| 266 | uint32_t readRawUint32(); |
| 267 | uint64_t readRawUint64(); |
| 268 | |
| 269 | // Returns a view directly into the original buffer for the number of bytes requested. Always |
| 270 | // returns the exact amount; throws if not possible. |
| 271 | kj::ArrayPtr<const kj::byte> readRawBytes(size_t size); |
| 272 | |
| 273 | // Reads a size (readRawUint64) followed by that many bytes. |
| 274 | kj::ArrayPtr<const kj::byte> readLengthDelimitedBytes(); |
| 275 | |
| 276 | // Read a string and make a copy. The copy is necessary since the text is not NUL-terminated on |
| 277 | // the wire. If you don't need NUL termination, read bytes and use `.asChars()`. |
| 278 | kj::String readRawString(size_t size); |
| 279 | kj::String readLengthDelimitedString(); |
| 280 | |
| 281 | inline uint32_t getVersion() const { |
| 282 | return deser.GetWireFormatVersion(); |
| 283 | } |
| 284 | |
| 285 | private: |
| 286 | void init(Lock& js, |
| 287 | kj::Maybe<kj::ArrayPtr<std::shared_ptr<v8::BackingStore>>> transferredArrayBuffers = kj::none, |
| 288 | kj::Maybe<Options> maybeOptions = kj::none); |
| 289 | |
| 290 | v8::MaybeLocal<v8::SharedArrayBuffer> GetSharedArrayBufferFromId( |
| 291 | v8::Isolate* isolate, uint32_t clone_id) override; |
| 292 | v8::MaybeLocal<v8::Object> ReadHostObject(v8::Isolate* isolate) override; |
| 293 | |
| 294 | kj::Maybe<ExternalHandler&> externalHandler; |
| 295 | |
| 296 | size_t totalInputSize; |
| 297 | v8::ValueDeserializer deser; |
| 298 | kj::Maybe<kj::ArrayPtr<std::shared_ptr<v8::BackingStore>>> sharedBackingStores; |
| 299 | bool preserveStackInErrors = true; |
| 300 | }; |
| 301 | |
| 302 | // Intended for use with v8::ValueSerializer data released into a kj::Array. |
| 303 | class SerializedBufferDisposer: public kj::ArrayDisposer { |
| 304 | protected: |
| 305 | void disposeImpl(void* firstElement, |
| 306 | size_t elementSize, |
| 307 | size_t elementCount, |
| 308 | size_t capacity, |
| 309 | void (*destroyElement)(void*)) const override; |
| 310 | }; |
| 311 | constexpr SerializedBufferDisposer SERIALIZED_BUFFER_DISPOSER; |
| 312 | |
| 313 | JsValue structuredClone( |
| 314 | Lock& js, const JsValue& value, kj::Maybe<kj::Array<JsValue>> maybeTransfer = kj::none); |
| 315 | |
| 316 | } // namespace workerd::jsg |