1 /*
2 * Copyright 2011 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "src/core/SkPictureData.h"
9
10 #include "include/core/SkFlattenable.h"
11 #include "include/core/SkSerialProcs.h"
12 #include "include/core/SkString.h"
13 #include "include/core/SkTypeface.h"
14 #include "include/private/base/SkDebug.h"
15 #include "include/private/base/SkTFitsIn.h"
16 #include "include/private/base/SkTemplates.h"
17 #include "include/private/base/SkTo.h"
18 #include "src/base/SkAutoMalloc.h"
19 #include "src/core/SkPicturePriv.h"
20 #include "src/core/SkPictureRecord.h"
21 #include "src/core/SkPtrRecorder.h"
22 #include "src/core/SkReadBuffer.h"
23 #include "src/core/SkStreamPriv.h"
24 #include "src/core/SkTHash.h"
25 #include "src/core/SkTextBlobPriv.h"
26 #include "src/core/SkVerticesPriv.h"
27 #include "src/core/SkWriteBuffer.h"
28
29 #include <cstring>
30 #include <utility>
31
32 #if defined(SK_GANESH)
33 #include "include/private/chromium/Slug.h"
34 #endif
35
36 using namespace skia_private;
37
SafeCount(const T * obj)38 template <typename T> int SafeCount(const T* obj) {
39 return obj ? obj->size() : 0;
40 }
41
SkPictureData(const SkPictInfo & info)42 SkPictureData::SkPictureData(const SkPictInfo& info)
43 : fInfo(info) {}
44
initForPlayback() const45 void SkPictureData::initForPlayback() const {
46 // ensure that the paths bounds are pre-computed
47 for (int i = 0; i < fPaths.size(); i++) {
48 fPaths[i].updateBoundsCache();
49 }
50 }
51
SkPictureData(const SkPictureRecord & record,const SkPictInfo & info)52 SkPictureData::SkPictureData(const SkPictureRecord& record,
53 const SkPictInfo& info)
54 : fPictures(record.getPictures())
55 , fDrawables(record.getDrawables())
56 , fTextBlobs(record.getTextBlobs())
57 , fVertices(record.getVertices())
58 , fImages(record.getImages())
59 #if defined(SK_GANESH)
60 , fSlugs(record.getSlugs())
61 #endif
62 , fInfo(info) {
63
64 fOpData = record.opData();
65
66 fPaints = record.fPaints;
67
68 fPaths.reset(record.fPaths.count());
69 record.fPaths.foreach([this](const SkPath& path, int n) {
70 // These indices are logically 1-based, but we need to serialize them
71 // 0-based to keep the deserializing SkPictureData::getPath() working.
72 fPaths[n-1] = path;
73 });
74
75 this->initForPlayback();
76 }
77
78 ///////////////////////////////////////////////////////////////////////////////
79 ///////////////////////////////////////////////////////////////////////////////
80
81 #include "include/core/SkStream.h"
82
compute_chunk_size(SkFlattenable::Factory * array,int count)83 static size_t compute_chunk_size(SkFlattenable::Factory* array, int count) {
84 size_t size = 4; // for 'count'
85
86 for (int i = 0; i < count; i++) {
87 const char* name = SkFlattenable::FactoryToName(array[i]);
88 if (nullptr == name || 0 == *name) {
89 size += SkWStream::SizeOfPackedUInt(0);
90 } else {
91 size_t len = strlen(name);
92 size += SkWStream::SizeOfPackedUInt(len);
93 size += len;
94 }
95 }
96
97 return size;
98 }
99
write_tag_size(SkWriteBuffer & buffer,uint32_t tag,size_t size)100 static void write_tag_size(SkWriteBuffer& buffer, uint32_t tag, size_t size) {
101 buffer.writeUInt(tag);
102 buffer.writeUInt(SkToU32(size));
103 }
104
write_tag_size(SkWStream * stream,uint32_t tag,size_t size)105 static void write_tag_size(SkWStream* stream, uint32_t tag, size_t size) {
106 stream->write32(tag);
107 stream->write32(SkToU32(size));
108 }
109
WriteFactories(SkWStream * stream,const SkFactorySet & rec)110 void SkPictureData::WriteFactories(SkWStream* stream, const SkFactorySet& rec) {
111 int count = rec.count();
112
113 AutoSTMalloc<16, SkFlattenable::Factory> storage(count);
114 SkFlattenable::Factory* array = (SkFlattenable::Factory*)storage.get();
115 rec.copyToArray(array);
116
117 size_t size = compute_chunk_size(array, count);
118
119 // TODO: write_tag_size should really take a size_t
120 write_tag_size(stream, SK_PICT_FACTORY_TAG, (uint32_t) size);
121 SkDEBUGCODE(size_t start = stream->bytesWritten());
122 stream->write32(count);
123
124 for (int i = 0; i < count; i++) {
125 const char* name = SkFlattenable::FactoryToName(array[i]);
126 if (nullptr == name || 0 == *name) {
127 stream->writePackedUInt(0);
128 } else {
129 size_t len = strlen(name);
130 stream->writePackedUInt(len);
131 stream->write(name, len);
132 }
133 }
134
135 SkASSERT(size == (stream->bytesWritten() - start));
136 }
137
WriteTypefaces(SkWStream * stream,const SkRefCntSet & rec,const SkSerialProcs & procs)138 void SkPictureData::WriteTypefaces(SkWStream* stream, const SkRefCntSet& rec,
139 const SkSerialProcs& procs) {
140 int count = rec.count();
141
142 write_tag_size(stream, SK_PICT_TYPEFACE_TAG, count);
143
144 AutoSTMalloc<16, SkTypeface*> storage(count);
145 SkTypeface** array = (SkTypeface**)storage.get();
146 rec.copyToArray((SkRefCnt**)array);
147
148 for (int i = 0; i < count; i++) {
149 SkTypeface* tf = array[i];
150 if (procs.fTypefaceProc) {
151 auto data = procs.fTypefaceProc(tf, procs.fTypefaceCtx);
152 if (data) {
153 stream->write(data->data(), data->size());
154 continue;
155 }
156 }
157 array[i]->serialize(stream);
158 }
159 }
160
flattenToBuffer(SkWriteBuffer & buffer,bool textBlobsOnly) const161 void SkPictureData::flattenToBuffer(SkWriteBuffer& buffer, bool textBlobsOnly) const {
162 if (!textBlobsOnly) {
163 int numPaints = fPaints.size();
164 if (numPaints > 0) {
165 write_tag_size(buffer, SK_PICT_PAINT_BUFFER_TAG, numPaints);
166 for (const SkPaint& paint : fPaints) {
167 buffer.writePaint(paint);
168 }
169 }
170
171 int numPaths = fPaths.size();
172 if (numPaths > 0) {
173 write_tag_size(buffer, SK_PICT_PATH_BUFFER_TAG, numPaths);
174 buffer.writeInt(numPaths);
175 for (const SkPath& path : fPaths) {
176 buffer.writePath(path);
177 }
178 }
179 }
180
181 if (!fTextBlobs.empty()) {
182 write_tag_size(buffer, SK_PICT_TEXTBLOB_BUFFER_TAG, fTextBlobs.size());
183 for (const auto& blob : fTextBlobs) {
184 SkTextBlobPriv::Flatten(*blob, buffer);
185 }
186 }
187
188 #if defined(SK_GANESH)
189 if (!textBlobsOnly) {
190 write_tag_size(buffer, SK_PICT_SLUG_BUFFER_TAG, fSlugs.size());
191 for (const auto& slug : fSlugs) {
192 slug->doFlatten(buffer);
193 }
194 }
195 #endif
196
197 if (!textBlobsOnly) {
198 if (!fVertices.empty()) {
199 write_tag_size(buffer, SK_PICT_VERTICES_BUFFER_TAG, fVertices.size());
200 for (const auto& vert : fVertices) {
201 vert->priv().encode(buffer);
202 }
203 }
204
205 if (!fImages.empty()) {
206 write_tag_size(buffer, SK_PICT_IMAGE_BUFFER_TAG, fImages.size());
207 for (const auto& img : fImages) {
208 buffer.writeImage(img.get());
209 }
210 }
211 }
212 }
213
214 // SkPictureData::serialize() will write out paints, and then write out an array of typefaces
215 // (unique set). However, paint's serializer will respect SerialProcs, which can cause us to
216 // call that custom typefaceproc on *every* typeface, not just on the unique ones. To avoid this,
217 // we ignore the custom proc (here) when we serialize the paints, and then do respect it when
218 // we serialize the typefaces.
skip_typeface_proc(const SkSerialProcs & procs)219 static SkSerialProcs skip_typeface_proc(const SkSerialProcs& procs) {
220 SkSerialProcs newProcs = procs;
221 newProcs.fTypefaceProc = nullptr;
222 newProcs.fTypefaceCtx = nullptr;
223 return newProcs;
224 }
225
226 // topLevelTypeFaceSet is null only on the top level call.
227 // This method is called recursively on every subpicture in two passes.
228 // textBlobsOnly serves to indicate that we are on the first pass and skip as much work as
229 // possible that is not relevant to collecting text blobs in topLevelTypeFaceSet
230 // TODO(nifong): dedupe typefaces and all other shared resources in a faster and more readable way.
serialize(SkWStream * stream,const SkSerialProcs & procs,SkRefCntSet * topLevelTypeFaceSet,bool textBlobsOnly) const231 void SkPictureData::serialize(SkWStream* stream, const SkSerialProcs& procs,
232 SkRefCntSet* topLevelTypeFaceSet, bool textBlobsOnly) const {
233 // This can happen at pretty much any time, so might as well do it first.
234 write_tag_size(stream, SK_PICT_READER_TAG, fOpData->size());
235 stream->write(fOpData->bytes(), fOpData->size());
236
237 // We serialize all typefaces into the typeface section of the top-level picture.
238 SkRefCntSet localTypefaceSet;
239 SkRefCntSet* typefaceSet = topLevelTypeFaceSet ? topLevelTypeFaceSet : &localTypefaceSet;
240
241 // We delay serializing the bulk of our data until after we've serialized
242 // factories and typefaces by first serializing to an in-memory write buffer.
243 SkFactorySet factSet; // buffer refs factSet, so factSet must come first.
244 SkBinaryWriteBuffer buffer;
245 buffer.setFactoryRecorder(sk_ref_sp(&factSet));
246 buffer.setSerialProcs(skip_typeface_proc(procs));
247 buffer.setTypefaceRecorder(sk_ref_sp(typefaceSet));
248 this->flattenToBuffer(buffer, textBlobsOnly);
249
250 // Pretend to serialize our sub-pictures for the side effect of filling typefaceSet
251 // with typefaces from sub-pictures.
252 struct DevNull: public SkWStream {
253 DevNull() : fBytesWritten(0) {}
254 size_t fBytesWritten;
255 bool write(const void*, size_t size) override { fBytesWritten += size; return true; }
256 size_t bytesWritten() const override { return fBytesWritten; }
257 } devnull;
258 for (const auto& pic : fPictures) {
259 pic->serialize(&devnull, nullptr, typefaceSet, /*textBlobsOnly=*/ true);
260 }
261 if (textBlobsOnly) { return; } // return early from fake serialize
262
263 // We need to write factories before we write the buffer.
264 // We need to write typefaces before we write the buffer or any sub-picture.
265 WriteFactories(stream, factSet);
266 // Pass the original typefaceproc (if any) now that we're ready to actually serialize the
267 // typefaces. We skipped this proc before, when we were serializing paints, so that the
268 // paints would just write indices into our typeface set.
269 WriteTypefaces(stream, *typefaceSet, procs);
270
271 // Write the buffer.
272 write_tag_size(stream, SK_PICT_BUFFER_SIZE_TAG, buffer.bytesWritten());
273 buffer.writeToStream(stream);
274
275 // Write sub-pictures by calling serialize again.
276 if (!fPictures.empty()) {
277 write_tag_size(stream, SK_PICT_PICTURE_TAG, fPictures.size());
278 for (const auto& pic : fPictures) {
279 pic->serialize(stream, &procs, typefaceSet, /*textBlobsOnly=*/ false);
280 }
281 }
282
283 stream->write32(SK_PICT_EOF_TAG);
284 }
285
flatten(SkWriteBuffer & buffer) const286 void SkPictureData::flatten(SkWriteBuffer& buffer) const {
287 write_tag_size(buffer, SK_PICT_READER_TAG, fOpData->size());
288 buffer.writeByteArray(fOpData->bytes(), fOpData->size());
289
290 if (!fPictures.empty()) {
291 write_tag_size(buffer, SK_PICT_PICTURE_TAG, fPictures.size());
292 for (const auto& pic : fPictures) {
293 SkPicturePriv::Flatten(pic, buffer);
294 }
295 }
296
297 if (!fDrawables.empty()) {
298 write_tag_size(buffer, SK_PICT_DRAWABLE_TAG, fDrawables.size());
299 for (const auto& draw : fDrawables) {
300 buffer.writeFlattenable(draw.get());
301 }
302 }
303
304 // Write this picture playback's data into a writebuffer
305 this->flattenToBuffer(buffer, false);
306 buffer.write32(SK_PICT_EOF_TAG);
307 }
308
309 ///////////////////////////////////////////////////////////////////////////////
310
parseStreamTag(SkStream * stream,uint32_t tag,uint32_t size,const SkDeserialProcs & procs,SkTypefacePlayback * topLevelTFPlayback,int recursionLimit)311 bool SkPictureData::parseStreamTag(SkStream* stream,
312 uint32_t tag,
313 uint32_t size,
314 const SkDeserialProcs& procs,
315 SkTypefacePlayback* topLevelTFPlayback,
316 int recursionLimit) {
317 switch (tag) {
318 case SK_PICT_READER_TAG:
319 SkASSERT(nullptr == fOpData);
320 fOpData = SkData::MakeFromStream(stream, size);
321 if (!fOpData) {
322 return false;
323 }
324 break;
325 case SK_PICT_FACTORY_TAG: {
326 if (!stream->readU32(&size)) { return false; }
327 if (StreamRemainingLengthIsBelow(stream, size)) {
328 return false;
329 }
330 fFactoryPlayback = std::make_unique<SkFactoryPlayback>(size);
331 for (size_t i = 0; i < size; i++) {
332 SkString str;
333 size_t len;
334 if (!stream->readPackedUInt(&len)) { return false; }
335 if (StreamRemainingLengthIsBelow(stream, len)) {
336 return false;
337 }
338 str.resize(len);
339 if (stream->read(str.data(), len) != len) {
340 return false;
341 }
342 fFactoryPlayback->base()[i] = SkFlattenable::NameToFactory(str.c_str());
343 }
344 } break;
345 case SK_PICT_TYPEFACE_TAG: {
346 if (StreamRemainingLengthIsBelow(stream, size)) {
347 return false;
348 }
349 fTFPlayback.setCount(size);
350 for (uint32_t i = 0; i < size; ++i) {
351 if (stream->isAtEnd()) {
352 return false;
353 }
354 sk_sp<SkTypeface> tf;
355 if (procs.fTypefaceProc) {
356 tf = procs.fTypefaceProc(&stream, sizeof(stream), procs.fTypefaceCtx);
357 } else {
358 tf = SkTypeface::MakeDeserialize(stream);
359 }
360 if (!tf) { // failed to deserialize
361 // fTFPlayback asserts it never has a null, so we plop in
362 // the default here.
363 tf = SkTypeface::MakeDefault();
364 }
365 fTFPlayback[i] = std::move(tf);
366 }
367 } break;
368 case SK_PICT_PICTURE_TAG: {
369 SkASSERT(fPictures.empty());
370 if (StreamRemainingLengthIsBelow(stream, size)) {
371 return false;
372 }
373 fPictures.reserve_back(SkToInt(size));
374
375 for (uint32_t i = 0; i < size; i++) {
376 auto pic = SkPicture::MakeFromStreamPriv(stream, &procs,
377 topLevelTFPlayback, recursionLimit - 1);
378 if (!pic) {
379 return false;
380 }
381 fPictures.push_back(std::move(pic));
382 }
383 } break;
384 case SK_PICT_BUFFER_SIZE_TAG: {
385 if (StreamRemainingLengthIsBelow(stream, size)) {
386 return false;
387 }
388 SkAutoMalloc storage(size);
389 if (stream->read(storage.get(), size) != size) {
390 return false;
391 }
392
393 SkReadBuffer buffer(storage.get(), size);
394 buffer.setVersion(fInfo.getVersion());
395
396 if (!fFactoryPlayback) {
397 return false;
398 }
399 fFactoryPlayback->setupBuffer(buffer);
400 buffer.setDeserialProcs(procs);
401
402 if (fTFPlayback.count() > 0) {
403 // .skp files <= v43 have typefaces serialized with each sub picture.
404 fTFPlayback.setupBuffer(buffer);
405 } else {
406 // Newer .skp files serialize all typefaces with the top picture.
407 topLevelTFPlayback->setupBuffer(buffer);
408 }
409
410 while (!buffer.eof() && buffer.isValid()) {
411 tag = buffer.readUInt();
412 size = buffer.readUInt();
413 this->parseBufferTag(buffer, tag, size);
414 }
415 if (!buffer.isValid()) {
416 return false;
417 }
418 } break;
419 }
420 return true; // success
421 }
422
create_image_from_buffer(SkReadBuffer & buffer)423 static sk_sp<SkImage> create_image_from_buffer(SkReadBuffer& buffer) {
424 return buffer.readImage();
425 }
426
create_drawable_from_buffer(SkReadBuffer & buffer)427 static sk_sp<SkDrawable> create_drawable_from_buffer(SkReadBuffer& buffer) {
428 return sk_sp<SkDrawable>((SkDrawable*)buffer.readFlattenable(SkFlattenable::kSkDrawable_Type));
429 }
430
431 // We need two types 'cause SkDrawable is const-variant.
432 template <typename T, typename U>
new_array_from_buffer(SkReadBuffer & buffer,uint32_t inCount,SkTArray<sk_sp<T>> & array,sk_sp<U> (* factory)(SkReadBuffer &))433 bool new_array_from_buffer(SkReadBuffer& buffer, uint32_t inCount,
434 SkTArray<sk_sp<T>>& array, sk_sp<U> (*factory)(SkReadBuffer&)) {
435 if (!buffer.validate(array.empty() && SkTFitsIn<int>(inCount))) {
436 return false;
437 }
438 if (0 == inCount) {
439 return true;
440 }
441
442 for (uint32_t i = 0; i < inCount; ++i) {
443 auto obj = factory(buffer);
444
445 if (!buffer.validate(obj != nullptr)) {
446 array.clear();
447 return false;
448 }
449
450 array.push_back(std::move(obj));
451 }
452
453 return true;
454 }
455
parseBufferTag(SkReadBuffer & buffer,uint32_t tag,uint32_t size)456 void SkPictureData::parseBufferTag(SkReadBuffer& buffer, uint32_t tag, uint32_t size) {
457 switch (tag) {
458 case SK_PICT_PAINT_BUFFER_TAG: {
459 if (!buffer.validate(SkTFitsIn<int>(size))) {
460 return;
461 }
462 const int count = SkToInt(size);
463
464 for (int i = 0; i < count; ++i) {
465 fPaints.push_back(buffer.readPaint());
466 if (!buffer.isValid()) {
467 return;
468 }
469 }
470 } break;
471 case SK_PICT_PATH_BUFFER_TAG:
472 if (size > 0) {
473 const int count = buffer.readInt();
474 if (!buffer.validate(count >= 0)) {
475 return;
476 }
477 for (int i = 0; i < count; i++) {
478 buffer.readPath(&fPaths.push_back());
479 if (!buffer.isValid()) {
480 return;
481 }
482 }
483 } break;
484 case SK_PICT_TEXTBLOB_BUFFER_TAG:
485 new_array_from_buffer(buffer, size, fTextBlobs, SkTextBlobPriv::MakeFromBuffer);
486 break;
487 case SK_PICT_SLUG_BUFFER_TAG:
488 #if defined(SK_GANESH)
489 new_array_from_buffer(buffer, size, fSlugs, sktext::gpu::Slug::MakeFromBuffer);
490 #endif
491 break;
492 case SK_PICT_VERTICES_BUFFER_TAG:
493 new_array_from_buffer(buffer, size, fVertices, SkVerticesPriv::Decode);
494 break;
495 case SK_PICT_IMAGE_BUFFER_TAG:
496 new_array_from_buffer(buffer, size, fImages, create_image_from_buffer);
497 break;
498 case SK_PICT_READER_TAG: {
499 // Preflight check that we can initialize all data from the buffer
500 // before allocating it.
501 if (!buffer.validateCanReadN<uint8_t>(size)) {
502 return;
503 }
504 auto data(SkData::MakeUninitialized(size));
505 if (!buffer.readByteArray(data->writable_data(), size) ||
506 !buffer.validate(nullptr == fOpData)) {
507 return;
508 }
509 SkASSERT(nullptr == fOpData);
510 fOpData = std::move(data);
511 } break;
512 case SK_PICT_PICTURE_TAG:
513 new_array_from_buffer(buffer, size, fPictures, SkPicturePriv::MakeFromBuffer);
514 break;
515 case SK_PICT_DRAWABLE_TAG:
516 new_array_from_buffer(buffer, size, fDrawables, create_drawable_from_buffer);
517 break;
518 default:
519 buffer.validate(false); // The tag was invalid.
520 break;
521 }
522 }
523
CreateFromStream(SkStream * stream,const SkPictInfo & info,const SkDeserialProcs & procs,SkTypefacePlayback * topLevelTFPlayback,int recursionLimit)524 SkPictureData* SkPictureData::CreateFromStream(SkStream* stream,
525 const SkPictInfo& info,
526 const SkDeserialProcs& procs,
527 SkTypefacePlayback* topLevelTFPlayback,
528 int recursionLimit) {
529 std::unique_ptr<SkPictureData> data(new SkPictureData(info));
530 if (!topLevelTFPlayback) {
531 topLevelTFPlayback = &data->fTFPlayback;
532 }
533
534 if (!data->parseStream(stream, procs, topLevelTFPlayback, recursionLimit)) {
535 return nullptr;
536 }
537 return data.release();
538 }
539
CreateFromBuffer(SkReadBuffer & buffer,const SkPictInfo & info)540 SkPictureData* SkPictureData::CreateFromBuffer(SkReadBuffer& buffer,
541 const SkPictInfo& info) {
542 std::unique_ptr<SkPictureData> data(new SkPictureData(info));
543 buffer.setVersion(info.getVersion());
544
545 if (!data->parseBuffer(buffer)) {
546 return nullptr;
547 }
548 return data.release();
549 }
550
parseStream(SkStream * stream,const SkDeserialProcs & procs,SkTypefacePlayback * topLevelTFPlayback,int recursionLimit)551 bool SkPictureData::parseStream(SkStream* stream,
552 const SkDeserialProcs& procs,
553 SkTypefacePlayback* topLevelTFPlayback,
554 int recursionLimit) {
555 for (;;) {
556 uint32_t tag;
557 if (!stream->readU32(&tag)) { return false; }
558 if (SK_PICT_EOF_TAG == tag) {
559 break;
560 }
561
562 uint32_t size;
563 if (!stream->readU32(&size)) { return false; }
564 if (!this->parseStreamTag(stream, tag, size, procs, topLevelTFPlayback, recursionLimit)) {
565 return false; // we're invalid
566 }
567 }
568 return true;
569 }
570
parseBuffer(SkReadBuffer & buffer)571 bool SkPictureData::parseBuffer(SkReadBuffer& buffer) {
572 while (buffer.isValid()) {
573 uint32_t tag = buffer.readUInt();
574 if (SK_PICT_EOF_TAG == tag) {
575 break;
576 }
577 this->parseBufferTag(buffer, tag, buffer.readUInt());
578 }
579
580 // Check that we encountered required tags
581 if (!buffer.validate(this->opData() != nullptr)) {
582 // If we didn't build any opData, we are invalid. Even an EmptyPicture allocates the
583 // SkData for the ops (though its length may be zero).
584 return false;
585 }
586 return true;
587 }
588
optionalPaint(SkReadBuffer * reader) const589 const SkPaint* SkPictureData::optionalPaint(SkReadBuffer* reader) const {
590 int index = reader->readInt();
591 if (index == 0) {
592 return nullptr; // recorder wrote a zero for no paint (likely drawimage)
593 }
594 return reader->validate(index > 0 && index <= fPaints.size()) ?
595 &fPaints[index - 1] : nullptr;
596 }
597
requiredPaint(SkReadBuffer * reader) const598 const SkPaint& SkPictureData::requiredPaint(SkReadBuffer* reader) const {
599 const SkPaint* paint = this->optionalPaint(reader);
600 if (reader->validate(paint != nullptr)) {
601 return *paint;
602 }
603 static const SkPaint& stub = *(new SkPaint);
604 return stub;
605 }
606