1 // =================================================================================================
2 // ADOBE SYSTEMS INCORPORATED
3 // Copyright 2010 Adobe Systems Incorporated
4 // All Rights Reserved
5 //
6 // NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms
7 // of the Adobe license agreement accompanying it.
8 // =================================================================================================
9 
10 #include "public/include/XMP_Environment.h"	// ! XMP_Environment.h must be the first included header.
11 #include "public/include/XMP_Const.h"
12 #include "source/XIO.hpp"
13 
14 #include "XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.h"
15 #include "XMPFiles/source/FormatSupport/IFF/Chunk.h"
16 
17 #include <algorithm>
18 
19 using namespace IFF_RIFF;
20 
21 //
22 // Static init
23 //
24 const LittleEndian& WAVEBehavior::mEndian = LittleEndian::getInstance();
25 
26 
27 //-----------------------------------------------------------------------------
28 //
29 // WAVEBehavior::getRealSize(...)
30 //
31 // Purpose: Validate the passed in size value, identify the valid size if the
32 //			passed in isn't valid and return the valid size.
33 //			Throw an exception if the passed in size isn't valid and there's
34 //			no way to identify a valid size.
35 //
36 //-----------------------------------------------------------------------------
37 
getRealSize(const XMP_Uns64 size,const ChunkIdentifier & id,IChunkContainer & tree,XMP_IO * stream)38 XMP_Uns64 WAVEBehavior::getRealSize( const XMP_Uns64 size, const ChunkIdentifier& id, IChunkContainer& tree, XMP_IO* stream )
39 {
40 	XMP_Uns64 realSize = size;
41 
42 	if( size >= kNormalRF64ChunkSize ) // 4GB
43 	{
44 		if( this->isRF64( tree ) )
45 		{
46 			//
47 			// RF64 supports sizes beyond 4GB
48 			//
49 			DS64* rf64 = this->getDS64( tree, stream );
50 
51 			if( rf64 != NULL )
52 			{
53 				//
54 				// get 64bit size from RF64 structure
55 				//
56 				switch( id.id )
57 				{
58 					case kChunk_RF64:	realSize = rf64->riffSize;	break;
59 					case kChunk_data:	realSize = rf64->dataSize;	break;
60 
61 					default:
62 					{
63 						bool found = false;
64 
65 						//
66 						// try to find size value for passed chunk id in the ds64 table
67 						//
68 						if( rf64->tableLength > 0 )
69 						{
70 							for( std::vector<ChunkSize64>::iterator iter=rf64->table.begin(); iter!=rf64->table.end(); iter++ )
71 							{
72 								if( iter->id == id.id )
73 								{
74 									realSize = iter->size;
75 									found = true;
76 									break;
77 								}
78 							}
79 						}
80 
81 						if( !found )
82 						{
83 							//
84 							// no size for passed id available
85 							//
86 							XMP_Throw( "Unknown size value", kXMPErr_BadFileFormat );
87 						}
88 					}
89 				}
90 			}
91 			else
92 			{
93 				//
94 				// no RF64 size info available
95 				//
96 				XMP_Throw( "Unknown size value", kXMPErr_BadFileFormat );
97 			}
98 		}
99 		else
100 		{
101 			//
102 			// WAVE doesn't support that size
103 			//
104 			XMP_Throw( "Unknown size value", kXMPErr_BadFileFormat );
105 		}
106 	}
107 
108 	return realSize;
109 }
110 
111 //-----------------------------------------------------------------------------
112 //
113 // WAVEBehavior::getMaxChunkSize(...)
114 //
115 // Purpose: Return the maximum size of a single chunk, i.e. the maximum size
116 //			of a top-level chunk.
117 //
118 //-----------------------------------------------------------------------------
119 
getMaxChunkSize() const120 XMP_Uns64 WAVEBehavior::getMaxChunkSize() const
121 {
122 	// simple WAVE 4GByte
123 	XMP_Uns64 ret = 0x00000000FFFFFFFFLL;
124 
125 	if( mIsRF64 )
126 	{
127 		// RF64: full possible 64bit size
128 		ret = 0xFFFFFFFFFFFFFFFFLL;
129 	}
130 
131 	return ret;
132 }
133 
134 //-----------------------------------------------------------------------------
135 //
136 // WAVEBehavior::isValidTopLevelChunk(...)
137 //
138 // Purpose: Return true if the passed identifier is valid for top-level chunks
139 //			of a certain format.
140 //
141 //-----------------------------------------------------------------------------
142 
isValidTopLevelChunk(const ChunkIdentifier & id,XMP_Uns32 chunkNo)143 bool WAVEBehavior::isValidTopLevelChunk( const ChunkIdentifier& id, XMP_Uns32 chunkNo )
144 {
145 	return ( chunkNo == 0 )												&&
146 		   ( ( ( id.id == kChunk_RIFF ) && ( id.type == kType_WAVE ) )	||
147 		     ( ( id.id == kChunk_RF64 ) && ( id.type == kType_WAVE ) ) );
148 }
149 
150 //-----------------------------------------------------------------------------
151 //
152 // WAVEBehavior::fixHierarchy(...)
153 //
154 // Purpose: Fix the hierarchy of chunks depending ones based on size changes of
155 //			one or more chunks and second based on format specific rules.
156 //			Throw an exception if the hierarchy can't be fixed.
157 //
158 //-----------------------------------------------------------------------------
159 
fixHierarchy(IChunkContainer & tree)160 void WAVEBehavior::fixHierarchy( IChunkContainer& tree )
161 {
162 	XMP_Validate( tree.numChildren() == 1, "WAVE files should only have one top level chunk (RIFF)", kXMPErr_BadFileFormat);
163 
164 	Chunk* riffChunk = tree.getChildAt(0);
165 
166 	XMP_Validate( (riffChunk->getType() == kType_WAVE || riffChunk->getType() == kChunk_RF64) , "Invalid type for WAVE/RF64 top level chunk (RIFF)", kXMPErr_BadFileFormat);
167 
168 	if( riffChunk->hasChanged() )
169 	{
170 		//
171 		// move new added chunks to temporary container
172 		//
173 		Chunk* tmpContainer = Chunk::createChunk( mEndian );
174 		this->moveChunks( *riffChunk, *tmpContainer, riffChunk->numChildren() - mChunksAdded );
175 
176 		//
177 		// try to arrange chunks at their current position
178 		//
179 		this->arrangeChunksInPlace( *riffChunk, *tmpContainer );
180 
181 		//
182 		// for all chunks that were moved to the end try to find a FREE chunk for them
183 		//
184 		this->arrangeChunksInTree( *tmpContainer, *riffChunk );
185 
186 		//
187 		// append all remaining new added chunks to the end of the tree
188 		//
189 		this->moveChunks( *tmpContainer, *riffChunk, 0 );
190 		delete tmpContainer;
191 
192 		//
193 		// check for FREE chunks at the end
194 		//
195 		Chunk* endFREE = this->mergeFreeChunks( *riffChunk, riffChunk->numChildren() - 1 );
196 
197 		if( endFREE != NULL )
198 		{
199 			riffChunk->removeChildAt( riffChunk->numChildren() - 1 );
200 			delete endFREE;
201 		}
202 
203 		//
204 		// Fix the offset values of all chunks. Throw an exception in the case that
205 		// the offset of a non-modified chunk needs to be reset.
206 		//
207 		XMP_Validate( riffChunk->getOffset() == 0, "Invalid offset for RIFF top level chunk", kXMPErr_InternalFailure );
208 
209 		this->validateOffsets( tree );
210 
211 		//
212 		// update the RF64 chunk (if this is RF64) based on the current chunk sizes
213 		//
214 		this->updateRF64( tree );
215 	}
216 }
217 
insertChunk(IChunkContainer & tree,Chunk & chunk)218 void WAVEBehavior::insertChunk( IChunkContainer& tree, Chunk& chunk )
219 {
220 	XMP_Validate( tree.numChildren() == 1, "WAVE files should only have one top level chunk (RIFF)", kXMPErr_BadFileFormat);
221 	Chunk* riffChunk = tree.getChildAt(0);
222 
223 	XMP_Validate( riffChunk->getType() == kType_WAVE , "Invalid type for WAVE top level chunk (RIFF)", kXMPErr_BadFileFormat);
224 
225 	//
226 	// add new chunk to the end of the RIFF:WAVE
227 	//
228 	riffChunk->appendChild(&chunk);
229 
230 	mChunksAdded++;
231 }
232 
removeChunk(IChunkContainer & tree,Chunk & chunk)233 bool WAVEBehavior::removeChunk( IChunkContainer& tree, Chunk& chunk )
234 {
235 	//
236 	// validate parameter
237 	//
238 	XMP_Validate( chunk.getID() != kChunk_RIFF, "Can't remove RIFF chunk!", kXMPErr_InternalFailure );
239 	XMP_Validate( chunk.getChunkMode() != CHUNK_UNKNOWN, "Cant' remove UNKNOWN Chunk", kXMPErr_InternalFailure );
240 	XMP_Validate( tree.numChildren() == 1, "WAVE files should only have one top level chunk (RIFF)", kXMPErr_BadFileFormat);
241 
242 	//
243 	// get top-level chunk
244 	//
245 	Chunk* riffChunk = tree.getChildAt(0);
246 
247 	//
248 	// validate top-level chunk
249 	//
250 	XMP_Validate( (riffChunk->getType() == kType_WAVE || riffChunk->getType() == kChunk_RF64) , "Invalid type for WAVE/RF64 top level chunk (RIFF)", kXMPErr_BadFileFormat);
251 
252 	//
253 	// calculate index of chunk to remove
254 	//
255 	XMP_Uns32 i = std::find( riffChunk->firstChild(), riffChunk->lastChild(), &chunk ) - riffChunk->firstChild();
256 
257 	//
258 	// validate index
259 	//
260 	XMP_Validate( i < riffChunk->numChildren(), "Invalid chunk in tree", kXMPErr_InternalFailure );
261 
262 	//
263 	// adjust new chunks counter
264 	//
265 	if( i > riffChunk->numChildren() - mChunksAdded - 1 )
266 	{
267 		mChunksAdded--;
268 	}
269 
270 	if( i < riffChunk->numChildren()-1 )
271 	{
272 		//
273 		// fill gap with free chunk
274 		//
275 		Chunk* free = this->createFREE( chunk.getPadSize( true ) );
276 		riffChunk->replaceChildAt( i, free );
277 		free->setAsNew();
278 
279 		//
280 		// merge JUNK chunks
281 		//
282 		this->mergeFreeChunks( *riffChunk, i );
283 	}
284 	else
285 	{
286 		//
287 		// remove chunk from tree
288 		//
289 		riffChunk->removeChildAt( i );
290 	}
291 
292 	//
293 	// if there is an entry in the ds64 table for the removed chunk
294 	// then update the ds64 table entry
295 	//
296 	if( mDS64Data != NULL && mDS64Data->tableLength > 0 )
297 	{
298 		for( std::vector<ChunkSize64>::iterator iter=mDS64Data->table.begin(); iter!=mDS64Data->table.end(); iter++ )
299 		{
300 			if( iter->id == chunk.getID() )
301 			{
302 				//
303 				// don't remove entry but set its size to zero
304 				//
305 				iter->size = 0LL;
306 				break;
307 			}
308 		}
309 	}
310 
311 	return true;
312 }
313 
createFREE(XMP_Uns64 chunkSize)314 Chunk* WAVEBehavior::createFREE( XMP_Uns64 chunkSize )
315 {
316 	XMP_Int64 alloc = chunkSize - Chunk::HEADER_SIZE;
317 
318 	Chunk* chunk = NULL;
319 
320 	//
321 	// create a 'JUNK' chunk
322 	//
323 	if( alloc > 0 )
324 	{
325 		XMP_Uns8* data = new XMP_Uns8[static_cast<size_t>( alloc )];
326 		memset( data, 0, static_cast<size_t>( alloc ) );
327 
328 		chunk = Chunk::createUnknownChunk( mEndian, kChunk_JUNK, kType_NONE, alloc );
329 
330 		chunk->setData( data, alloc );
331 
332 		delete[] data;
333 	}
334 	else
335 	{
336 		chunk = Chunk::createHeaderChunk( mEndian, kChunk_JUNK );
337 	}
338 
339 	// force set dirty flag
340 	chunk->setChanged();
341 
342 	return chunk;
343 }
344 
isFREEChunk(const Chunk & chunk) const345 XMP_Bool WAVEBehavior::isFREEChunk( const Chunk& chunk ) const
346 {
347 	// Check for sigature JUNK and JUNQ
348 	return ( chunk.getID() == kChunk_JUNK || chunk.getID() == kChunk_JUNQ );
349 }
350 
351 
getMinFREESize() const352 XMP_Uns64 WAVEBehavior::getMinFREESize() const
353 {
354 	// avoid creation of chunks with size==0
355 	return static_cast<XMP_Uns64>( Chunk::HEADER_SIZE ) + 2;
356 }
357 
358 //-----------------------------------------------------------------------------
359 //
360 // WAVEBehavior::isRF64(...)
361 //
362 // Purpose: Is the current file a RF64 file
363 //
364 //-----------------------------------------------------------------------------
365 
isRF64(const IChunkContainer & tree)366 bool WAVEBehavior::isRF64( const IChunkContainer& tree )
367 {
368 	// The file format will not change at runtime
369 	// So if the flag is not already set, have a look at the tree
370 	if( ! mIsRF64 && tree.numChildren() != 0 )
371 	{
372 		Chunk *chunk = tree.getChildAt(0);
373 		// Only the TopLevel chunk is interesting
374 		mIsRF64 = chunk->getID() == kChunk_RF64 &&
375 				chunk->getType() == kType_WAVE;
376 	}
377 
378 	return mIsRF64;
379 }
380 
381 //-----------------------------------------------------------------------------
382 //
383 // WAVEBehavior::getDS64(...)
384 //
385 // Purpose: Return RF64 structure.
386 //
387 //-----------------------------------------------------------------------------
388 
getDS64(IChunkContainer & tree,XMP_IO * stream)389 WAVEBehavior::DS64* WAVEBehavior::getDS64( IChunkContainer& tree, XMP_IO* stream )
390 {
391 	DS64* ret = mDS64Data;
392 
393 	if( ret == NULL )
394 	{
395 		//
396 		// try to find 'ds64' chunk in the tree
397 		//
398 		Chunk* ds64 = NULL;
399 		Chunk* rf64 = NULL;
400 
401 		if( tree.numChildren() > 0 )
402 		{
403 			rf64 = tree.getChildAt(0);
404 
405 			if( rf64 != NULL && rf64->getID() == kChunk_RF64 && rf64->numChildren() > 0 )
406 			{
407 				//
408 				// 'ds64' chunk needs to be the very first child of the 'RF64' chunk
409 				//
410 				ds64 = rf64->getChildAt(0);
411 			}
412 
413 			//
414 			// Try to create 'ds64' chunk by parsing the stream
415 			//
416 			if( ds64 == NULL && stream != NULL )
417 			{
418 				//
419 				// remember file position before start reading from the stream
420 				//
421 				XMP_Uns64 filePos = stream->Offset();
422 
423 				try
424 				{
425 					ds64 = Chunk::createChunk( mEndian );
426 					ds64->readChunk( stream );
427 				}
428 				catch( ... )
429 				{
430 					delete ds64;
431 					ds64 = NULL;
432 				}
433 
434 				if( rf64 != NULL && ds64 != NULL && ds64->getID() == kChunk_ds64 )
435 				{
436 					//
437 					// Successfully read 'ds64' chunk.
438 					// Now read its data area as well and
439 					// add chunk to the 'RF64' chunk
440 					//
441 					ds64->cacheChunkData( stream );
442 					rf64->appendChild( ds64, false );
443 				}
444 				else
445 				{
446 					//
447 					// Either the reading failed or the 'ds64' chunk
448 					// doesn't exists at the expected position.
449 					// Now clean up and reject the stream position.
450 					//
451 					delete ds64;
452 					ds64 = NULL;
453 
454 					stream->Seek( filePos, kXMP_SeekFromStart );
455 				}
456 			}
457 			else if( ds64 != NULL && ds64->getID() != kChunk_ds64 )
458 			{
459 				//
460 				// first child of 'RF64' chunk is NOT 'ds64'!
461 				//
462 				ds64 = NULL;
463 			}
464 		}
465 
466 		//
467 		// parse 'ds64' chunk, store the RF64 struct and return it
468 		//
469 		if( ds64 != NULL )
470 		{
471 			DS64* ds64data = new DS64();
472 
473 			if( this->parseDS64Chunk( *ds64, *ds64data ) )
474 			{
475 				mDS64Data	= ds64data;
476 				ret			= mDS64Data;
477 			}
478 			else
479 			{
480 				delete ds64data;
481 			}
482 		}
483 	}
484 
485 	return ret;
486 }
487 
488 //-----------------------------------------------------------------------------
489 //
490 // WAVEBehavior::updateRF64(...)
491 //
492 // Purpose: update the RF64 chunk (if this is RF64) based on the current chunk sizes
493 //
494 //-----------------------------------------------------------------------------
495 
updateRF64(IChunkContainer & tree)496 void WAVEBehavior::updateRF64( IChunkContainer& tree )
497 {
498 	if( this->isRF64( tree ) )
499 	{
500 		XMP_Validate( mDS64Data != NULL, "Missing DS64 structure", kXMPErr_InternalFailure );
501 		XMP_Validate( tree.numChildren() == 1, "Invalid RF64 tree", kXMPErr_InternalFailure );
502 
503 		//
504 		// Check all chunks that sizes have changed and update their related value in the DS64 chunk
505 		//
506 		Chunk* rf64 = tree.getChildAt(0);
507 		XMP_Validate( rf64 != NULL && rf64->getID() == kChunk_RF64 && rf64->numChildren() > 0, "Invalid RF64 chunk", kXMPErr_InternalFailure );
508 
509 		this->doUpdateRF64( *rf64 );
510 
511 		//
512 		// try to find 'ds64' chunk in the tree
513 		// (needs to be the very first child of the 'RF64' chunk)
514 		//
515 		Chunk* ds64 = rf64->getChildAt(0);
516 		XMP_Validate( ds64 != NULL && ds64->getID() == kChunk_ds64, "Missing 'ds64' chunk", kXMPErr_InternalFailure );
517 
518 		//
519 		// serialize DS64 structure and write into ds64 chunk
520 		//
521 		this->serializeDS64Chunk( *mDS64Data, *ds64 );
522 	}
523 }
524 
doUpdateRF64(Chunk & chunk)525 void WAVEBehavior::doUpdateRF64( Chunk& chunk )
526 {
527 	//
528 	// update ds64 entry for chunk if its size has changed
529 	//
530 	if( chunk.hasChanged() && chunk.getOriginalSize() > kNormalRF64ChunkSize )
531 	{
532 		switch( chunk.getID() )
533 		{
534 			case kChunk_RF64:	mDS64Data->riffSize = chunk.getSize();		break;
535 			case kChunk_data:
536 				if( chunk.getSize() != chunk.getOriginalSize() )
537 				{
538 					XMP_Throw( "Data chunk must not change", kXMPErr_InternalFailure );
539 				}
540 				break;
541 			default:
542 			{
543 				bool requireEntry = ( chunk.getSize() > kNormalRF64ChunkSize );
544 				bool found = false;
545 
546 				//
547 				// try to find entry for passed chunk id in the ds64 table
548 				//
549 				if( mDS64Data->tableLength > 0 )
550 				{
551 					for( std::vector<ChunkSize64>::iterator iter=mDS64Data->table.begin(); iter!=mDS64Data->table.end(); iter++ )
552 					{
553 						if( iter->id == chunk.getID() )
554 						{
555 							// always set new size even if it's less than 4GB
556 							iter->size = chunk.getSize();
557 							found = true;
558 							break;
559 						}
560 					}
561 				}
562 
563 				//
564 				// We can't add new entries to the table. So if we found no entry within 'ds64'
565 				// for the passed chunk ID and the size of the chunk is larger than 4GB then
566 				// we have to throw an exception
567 				//
568 				XMP_Validate( found || ( ! found && ! requireEntry ), "Can't update 'ds64' chunk", kXMPErr_Unimplemented );
569 			}
570 		}
571 	}
572 
573 	//
574 	// go through all children to update ds64 data
575 	//
576 	for( XMP_Uns32 i=0; i<chunk.numChildren(); i++ )
577 	{
578 		Chunk* child = chunk.getChildAt(i);
579 
580 		this->doUpdateRF64( *child );
581 	}
582 }
583 
584 //-----------------------------------------------------------------------------
585 //
586 // WAVEBehavior::parseRF64Chunk(...)
587 //
588 // Purpose: Parses the data block of the given RF64 chunk into the internal data structures
589 //
590 //-----------------------------------------------------------------------------
591 
parseDS64Chunk(const Chunk & ds64Chunk,WAVEBehavior::DS64 & ds64)592 bool WAVEBehavior::parseDS64Chunk( const Chunk& ds64Chunk, WAVEBehavior::DS64& ds64 )
593 {
594 	bool ret = false;
595 
596 	// It is a valid ds64 chunk
597 	if( ds64Chunk.getID() == kChunk_ds64 && ds64Chunk.getSize() >= kMinimumDS64ChunkSize )
598 	{
599 		const XMP_Uns8* data;
600 		XMP_Uns64 size = ds64Chunk.getData(&data);
601 
602 		memset( &ds64, 0, kMinimumDS64ChunkSize);
603 
604 		//
605 		// copy fix input data into RF64 block (except chunk size table)
606 		// Safe as fixed size matches size of struct that is #pragma packed(1)
607 		//
608 		memcpy( &ds64, data, kMinimumDS64ChunkSize );
609 
610 		// If there is more data but the table length is <= 0 then this is not a valid ds64 chunk
611 		if( size > kMinimumDS64ChunkSize && ds64.tableLength > 0 )
612 		{
613 			// copy chunk sizes table
614 			//
615 			XMP_Assert( size - kMinimumDS64ChunkSize >= ds64.tableLength * sizeof(ChunkSize64));
616 
617 			XMP_Uns32 offset = kMinimumDS64ChunkSize;
618 			ChunkSize64 chunkSize;
619 
620 			for( XMP_Uns32 i = 0 ; i < ds64.tableLength ; i++, offset += sizeof(ChunkSize64) )
621 			{
622 				chunkSize.id = mEndian.getUns32( data + offset );
623 				chunkSize.size = mEndian.getUns64( data + offset + 4 );
624 
625 				ds64.table.push_back( chunkSize );
626 			}
627 		}
628 
629 		// remember any existing table buffer
630 		ds64.trailingBytes = static_cast<XMP_Uns32>(size - kMinimumDS64ChunkSize - ds64.tableLength * sizeof(ChunkSize64));
631 
632 		// Either a table has been correctly parsed or there was no table
633 		ret = (size - kMinimumDS64ChunkSize) >= (ds64.tableLength * sizeof(ChunkSize64));
634 	}
635 
636 	return ret;
637 }
638 
639 //-----------------------------------------------------------------------------
640 //
641 // WAVEBehavior::serializeRF64Chunk(...)
642 //
643 // Purpose: Serializes the internal RF64 data structures into the data part of the given chunk
644 //
645 //-----------------------------------------------------------------------------
serializeDS64Chunk(const WAVEBehavior::DS64 & ds64,Chunk & ds64Chunk)646 bool WAVEBehavior::serializeDS64Chunk( const WAVEBehavior::DS64& ds64, Chunk& ds64Chunk )
647 {
648 	if( ds64Chunk.getID() != kChunk_ds64 )
649 	{
650 		return false; // not a valid ds64 chunk
651 	}
652 
653 	// Calculate needed size
654 	XMP_Uns32 size = kMinimumDS64ChunkSize + ds64.tableLength * sizeof(ChunkSize64) + ds64.trailingBytes;
655 	// Create tmp buffer
656 	XMP_Uns8* data = new XMP_Uns8[size];
657 	memset( data, 0, size );
658 
659 	// copy fix input data into buffer (except chunk sizes table)
660 	// Safe as fixed size matches size of struct that is #pragma packed(1)
661 	memcpy( data, &ds64, kMinimumDS64ChunkSize );
662 
663 	// copy chunk sizes table
664 	if( ds64.tableLength > 0 )
665 	{
666 		XMP_Uns32 offset = kMinimumDS64ChunkSize;
667 
668 		for( XMP_Uns32 i = 0 ; i < ds64.tableLength ; i++, offset += sizeof(ChunkSize64) )
669 		{
670 			mEndian.putUns32( ds64.table.at(i).id, data + offset );
671 			mEndian.putUns64( ds64.table.at(i).size, data + offset + 4 );
672 		}
673 	}
674 
675 	ds64Chunk.setData( data, size );
676 
677 	// free tmp buffer
678 	delete []data;
679 
680 	return true;
681 }
682