1 // 7zEncode.cpp 2 3 #include "StdAfx.h" 4 5 #include "../../Common/CreateCoder.h" 6 #include "../../Common/FilterCoder.h" 7 #include "../../Common/LimitedStreams.h" 8 #include "../../Common/InOutTempBuffer.h" 9 #include "../../Common/ProgressUtils.h" 10 #include "../../Common/StreamObjects.h" 11 12 #include "7zEncode.h" 13 #include "7zSpecStream.h" 14 15 namespace NArchive { 16 namespace N7z { 17 18 void CEncoder::InitBindConv() 19 { 20 unsigned numIn = _bindInfo.Coders.Size(); 21 22 _SrcIn_to_DestOut.ClearAndSetSize(numIn); 23 _DestOut_to_SrcIn.ClearAndSetSize(numIn); 24 25 unsigned numOut = _bindInfo.GetNum_Bonds_and_PackStreams(); 26 _SrcOut_to_DestIn.ClearAndSetSize(numOut); 27 // _DestIn_to_SrcOut.ClearAndSetSize(numOut); 28 29 UInt32 destIn = 0; 30 UInt32 destOut = 0; 31 32 for (unsigned i = _bindInfo.Coders.Size(); i != 0;) 33 { 34 i--; 35 36 const NCoderMixer2::CCoderStreamsInfo &coder = _bindInfo.Coders[i]; 37 38 numIn--; 39 numOut -= coder.NumStreams; 40 41 _SrcIn_to_DestOut[numIn] = destOut; 42 _DestOut_to_SrcIn[destOut] = numIn; 43 44 destOut++; 45 46 for (UInt32 j = 0; j < coder.NumStreams; j++, destIn++) 47 { 48 UInt32 index = numOut + j; 49 _SrcOut_to_DestIn[index] = destIn; 50 // _DestIn_to_SrcOut[destIn] = index; 51 } 52 } 53 } 54 55 void CEncoder::SetFolder(CFolder &folder) 56 { 57 folder.Bonds.SetSize(_bindInfo.Bonds.Size()); 58 59 unsigned i; 60 61 for (i = 0; i < _bindInfo.Bonds.Size(); i++) 62 { 63 CBond &fb = folder.Bonds[i]; 64 const NCoderMixer2::CBond &mixerBond = _bindInfo.Bonds[_bindInfo.Bonds.Size() - 1 - i]; 65 fb.PackIndex = _SrcOut_to_DestIn[mixerBond.PackIndex]; 66 fb.UnpackIndex = _SrcIn_to_DestOut[mixerBond.UnpackIndex]; 67 } 68 69 folder.Coders.SetSize(_bindInfo.Coders.Size()); 70 71 for (i = 0; i < _bindInfo.Coders.Size(); i++) 72 { 73 CCoderInfo &coderInfo = folder.Coders[i]; 74 const NCoderMixer2::CCoderStreamsInfo &coderStreamsInfo = _bindInfo.Coders[_bindInfo.Coders.Size() - 1 - i]; 75 76 coderInfo.NumStreams = coderStreamsInfo.NumStreams; 77 coderInfo.MethodID = _decompressionMethods[i]; 78 // we don't free coderInfo.Props here. So coderInfo.Props can be non-empty. 79 } 80 81 folder.PackStreams.SetSize(_bindInfo.PackStreams.Size()); 82 83 for (i = 0; i < _bindInfo.PackStreams.Size(); i++) 84 folder.PackStreams[i] = _SrcOut_to_DestIn[_bindInfo.PackStreams[i]]; 85 } 86 87 88 89 static HRESULT SetCoderProps2(const CProps &props, const UInt64 *dataSizeReduce, IUnknown *coder) 90 { 91 CMyComPtr<ICompressSetCoderProperties> setCoderProperties; 92 coder->QueryInterface(IID_ICompressSetCoderProperties, (void **)&setCoderProperties); 93 if (setCoderProperties) 94 return props.SetCoderProps(setCoderProperties, dataSizeReduce); 95 return props.AreThereNonOptionalProps() ? E_INVALIDARG : S_OK; 96 } 97 98 99 100 void CMtEncMultiProgress::Init(ICompressProgressInfo *progress) 101 { 102 _progress = progress; 103 OutSize = 0; 104 } 105 106 STDMETHODIMP CMtEncMultiProgress::SetRatioInfo(const UInt64 *inSize, const UInt64 * /* outSize */) 107 { 108 UInt64 outSize2; 109 { 110 #ifndef _7ZIP_ST 111 NWindows::NSynchronization::CCriticalSectionLock lock(CriticalSection); 112 #endif 113 outSize2 = OutSize; 114 } 115 116 if (_progress) 117 return _progress->SetRatioInfo(inSize, &outSize2); 118 119 return S_OK; 120 } 121 122 123 124 HRESULT CEncoder::CreateMixerCoder( 125 DECL_EXTERNAL_CODECS_LOC_VARS 126 const UInt64 *inSizeForReduce) 127 { 128 #ifdef USE_MIXER_MT 129 #ifdef USE_MIXER_ST 130 if (_options.MultiThreadMixer) 131 #endif 132 { 133 _mixerMT = new NCoderMixer2::CMixerMT(true); 134 _mixerRef = _mixerMT; 135 _mixer = _mixerMT; 136 } 137 #ifdef USE_MIXER_ST 138 else 139 #endif 140 #endif 141 { 142 #ifdef USE_MIXER_ST 143 _mixerST = new NCoderMixer2::CMixerST(true); 144 _mixerRef = _mixerST; 145 _mixer = _mixerST; 146 #endif 147 } 148 149 RINOK(_mixer->SetBindInfo(_bindInfo)); 150 151 FOR_VECTOR (m, _options.Methods) 152 { 153 const CMethodFull &methodFull = _options.Methods[m]; 154 155 CCreatedCoder cod; 156 157 if (methodFull.CodecIndex >= 0) 158 { 159 RINOK(CreateCoder_Index( 160 EXTERNAL_CODECS_LOC_VARS 161 methodFull.CodecIndex, true, cod)); 162 } 163 else 164 { 165 RINOK(CreateCoder_Id( 166 EXTERNAL_CODECS_LOC_VARS 167 methodFull.Id, true, cod)); 168 } 169 170 if (cod.NumStreams != methodFull.NumStreams) 171 return E_FAIL; 172 if (!cod.Coder && !cod.Coder2) 173 return E_FAIL; 174 175 CMyComPtr<IUnknown> encoderCommon = cod.Coder ? (IUnknown *)cod.Coder : (IUnknown *)cod.Coder2; 176 177 #ifndef _7ZIP_ST 178 { 179 CMyComPtr<ICompressSetCoderMt> setCoderMt; 180 encoderCommon.QueryInterface(IID_ICompressSetCoderMt, &setCoderMt); 181 if (setCoderMt) 182 { 183 RINOK(setCoderMt->SetNumberOfThreads(_options.NumThreads)); 184 } 185 } 186 #endif 187 188 RINOK(SetCoderProps2(methodFull, inSizeForReduce, encoderCommon)); 189 190 /* 191 CMyComPtr<ICryptoResetSalt> resetSalt; 192 encoderCommon.QueryInterface(IID_ICryptoResetSalt, (void **)&resetSalt); 193 if (resetSalt) 194 { 195 resetSalt->ResetSalt(); 196 } 197 */ 198 199 // now there is no codec that uses another external codec 200 /* 201 #ifdef EXTERNAL_CODECS 202 CMyComPtr<ISetCompressCodecsInfo> setCompressCodecsInfo; 203 encoderCommon.QueryInterface(IID_ISetCompressCodecsInfo, (void **)&setCompressCodecsInfo); 204 if (setCompressCodecsInfo) 205 { 206 // we must use g_ExternalCodecs also 207 RINOK(setCompressCodecsInfo->SetCompressCodecsInfo(__externalCodecs->GetCodecs)); 208 } 209 #endif 210 */ 211 212 CMyComPtr<ICryptoSetPassword> cryptoSetPassword; 213 encoderCommon.QueryInterface(IID_ICryptoSetPassword, &cryptoSetPassword); 214 215 if (cryptoSetPassword) 216 { 217 const unsigned sizeInBytes = _options.Password.Len() * 2; 218 CByteBuffer buffer(sizeInBytes); 219 for (unsigned i = 0; i < _options.Password.Len(); i++) 220 { 221 wchar_t c = _options.Password[i]; 222 ((Byte *)buffer)[i * 2] = (Byte)c; 223 ((Byte *)buffer)[i * 2 + 1] = (Byte)(c >> 8); 224 } 225 RINOK(cryptoSetPassword->CryptoSetPassword((const Byte *)buffer, (UInt32)sizeInBytes)); 226 } 227 228 _mixer->AddCoder(cod); 229 } 230 return S_OK; 231 } 232 233 234 235 class CSequentialOutTempBufferImp2: 236 public ISequentialOutStream, 237 public CMyUnknownImp 238 { 239 CInOutTempBuffer *_buf; 240 public: 241 CMtEncMultiProgress *_mtProgresSpec; 242 243 CSequentialOutTempBufferImp2(): _buf(0), _mtProgresSpec(NULL) {} 244 void Init(CInOutTempBuffer *buffer) { _buf = buffer; } 245 MY_UNKNOWN_IMP1(ISequentialOutStream) 246 247 STDMETHOD(Write)(const void *data, UInt32 size, UInt32 *processedSize); 248 }; 249 250 STDMETHODIMP CSequentialOutTempBufferImp2::Write(const void *data, UInt32 size, UInt32 *processed) 251 { 252 if (!_buf->Write(data, size)) 253 { 254 if (processed) 255 *processed = 0; 256 return E_FAIL; 257 } 258 if (processed) 259 *processed = size; 260 if (_mtProgresSpec) 261 _mtProgresSpec->AddOutSize(size); 262 return S_OK; 263 } 264 265 266 class CSequentialOutMtNotify: 267 public ISequentialOutStream, 268 public CMyUnknownImp 269 { 270 public: 271 CMyComPtr<ISequentialOutStream> _stream; 272 CMtEncMultiProgress *_mtProgresSpec; 273 274 CSequentialOutMtNotify(): _mtProgresSpec(NULL) {} 275 MY_UNKNOWN_IMP1(ISequentialOutStream) 276 277 STDMETHOD(Write)(const void *data, UInt32 size, UInt32 *processedSize); 278 }; 279 280 STDMETHODIMP CSequentialOutMtNotify::Write(const void *data, UInt32 size, UInt32 *processed) 281 { 282 UInt32 realProcessed = 0; 283 HRESULT res = _stream->Write(data, size, &realProcessed); 284 if (processed) 285 *processed = realProcessed; 286 if (_mtProgresSpec) 287 _mtProgresSpec->AddOutSize(size); 288 return res; 289 } 290 291 292 293 HRESULT CEncoder::Encode( 294 DECL_EXTERNAL_CODECS_LOC_VARS 295 ISequentialInStream *inStream, 296 // const UInt64 *inStreamSize, 297 const UInt64 *inSizeForReduce, 298 CFolder &folderItem, 299 CRecordVector<UInt64> &coderUnpackSizes, 300 UInt64 &unpackSize, 301 ISequentialOutStream *outStream, 302 CRecordVector<UInt64> &packSizes, 303 ICompressProgressInfo *compressProgress) 304 { 305 RINOK(EncoderConstr()); 306 307 if (!_mixerRef) 308 { 309 RINOK(CreateMixerCoder(EXTERNAL_CODECS_LOC_VARS inSizeForReduce)); 310 } 311 312 _mixer->ReInit(); 313 314 CMtEncMultiProgress *mtProgressSpec = NULL; 315 CMyComPtr<ICompressProgressInfo> mtProgress; 316 317 CSequentialOutMtNotify *mtOutStreamNotifySpec = NULL; 318 CMyComPtr<ISequentialOutStream> mtOutStreamNotify; 319 320 CObjectVector<CInOutTempBuffer> inOutTempBuffers; 321 CObjectVector<CSequentialOutTempBufferImp2 *> tempBufferSpecs; 322 CObjectVector<CMyComPtr<ISequentialOutStream> > tempBuffers; 323 324 unsigned numMethods = _bindInfo.Coders.Size(); 325 326 unsigned i; 327 328 for (i = 1; i < _bindInfo.PackStreams.Size(); i++) 329 { 330 CInOutTempBuffer &iotb = inOutTempBuffers.AddNew(); 331 iotb.Create(); 332 iotb.InitWriting(); 333 } 334 335 for (i = 1; i < _bindInfo.PackStreams.Size(); i++) 336 { 337 CSequentialOutTempBufferImp2 *tempBufferSpec = new CSequentialOutTempBufferImp2; 338 CMyComPtr<ISequentialOutStream> tempBuffer = tempBufferSpec; 339 tempBufferSpec->Init(&inOutTempBuffers[i - 1]); 340 tempBuffers.Add(tempBuffer); 341 tempBufferSpecs.Add(tempBufferSpec); 342 } 343 344 for (i = 0; i < numMethods; i++) 345 _mixer->SetCoderInfo(i, NULL, NULL, false); 346 347 348 /* inStreamSize can be used by BCJ2 to set optimal range of conversion. 349 But current BCJ2 encoder uses also another way to check exact size of current file. 350 So inStreamSize is not required. */ 351 352 /* 353 if (inStreamSize) 354 _mixer->SetCoderInfo(_bindInfo.UnpackCoder, inStreamSize, NULL); 355 */ 356 357 358 CSequentialInStreamSizeCount2 *inStreamSizeCountSpec = new CSequentialInStreamSizeCount2; 359 CMyComPtr<ISequentialInStream> inStreamSizeCount = inStreamSizeCountSpec; 360 361 CSequentialOutStreamSizeCount *outStreamSizeCountSpec = NULL; 362 CMyComPtr<ISequentialOutStream> outStreamSizeCount; 363 364 inStreamSizeCountSpec->Init(inStream); 365 366 ISequentialInStream *inStreamPointer = inStreamSizeCount; 367 CRecordVector<ISequentialOutStream *> outStreamPointers; 368 369 SetFolder(folderItem); 370 371 for (i = 0; i < numMethods; i++) 372 { 373 IUnknown *coder = _mixer->GetCoder(i).GetUnknown(); 374 375 CMyComPtr<ICryptoResetInitVector> resetInitVector; 376 coder->QueryInterface(IID_ICryptoResetInitVector, (void **)&resetInitVector); 377 if (resetInitVector) 378 { 379 resetInitVector->ResetInitVector(); 380 } 381 382 { 383 CMyComPtr<ICompressSetCoderPropertiesOpt> optProps; 384 coder->QueryInterface(IID_ICompressSetCoderPropertiesOpt, (void **)&optProps); 385 if (optProps) 386 { 387 PROPID propID = NCoderPropID::kExpectedDataSize; 388 NWindows::NCOM::CPropVariant prop = (UInt64)unpackSize; 389 RINOK(optProps->SetCoderPropertiesOpt(&propID, &prop, 1)); 390 } 391 } 392 393 CMyComPtr<ICompressWriteCoderProperties> writeCoderProperties; 394 coder->QueryInterface(IID_ICompressWriteCoderProperties, (void **)&writeCoderProperties); 395 396 CByteBuffer &props = folderItem.Coders[numMethods - 1 - i].Props; 397 398 if (writeCoderProperties) 399 { 400 CDynBufSeqOutStream *outStreamSpec = new CDynBufSeqOutStream; 401 CMyComPtr<ISequentialOutStream> dynOutStream(outStreamSpec); 402 outStreamSpec->Init(); 403 RINOK(writeCoderProperties->WriteCoderProperties(dynOutStream)); 404 outStreamSpec->CopyToBuffer(props); 405 } 406 else 407 props.Free(); 408 } 409 410 _mixer->SelectMainCoder(false); 411 UInt32 mainCoder = _mixer->MainCoderIndex; 412 413 bool useMtProgress = false; 414 if (!_mixer->Is_PackSize_Correct_for_Coder(mainCoder)) 415 { 416 #ifdef _7ZIP_ST 417 if (!_mixer->IsThere_ExternalCoder_in_PackTree(mainCoder)) 418 #endif 419 useMtProgress = true; 420 } 421 422 if (useMtProgress) 423 { 424 mtProgressSpec = new CMtEncMultiProgress; 425 mtProgress = mtProgressSpec; 426 mtProgressSpec->Init(compressProgress); 427 428 mtOutStreamNotifySpec = new CSequentialOutMtNotify; 429 mtOutStreamNotify = mtOutStreamNotifySpec; 430 mtOutStreamNotifySpec->_stream = outStream; 431 mtOutStreamNotifySpec->_mtProgresSpec = mtProgressSpec; 432 433 FOR_VECTOR(t, tempBufferSpecs) 434 { 435 tempBufferSpecs[t]->_mtProgresSpec = mtProgressSpec; 436 } 437 } 438 439 440 if (_bindInfo.PackStreams.Size() != 0) 441 { 442 outStreamSizeCountSpec = new CSequentialOutStreamSizeCount; 443 outStreamSizeCount = outStreamSizeCountSpec; 444 outStreamSizeCountSpec->SetStream(mtOutStreamNotify ? (ISequentialOutStream *)mtOutStreamNotify : outStream); 445 outStreamSizeCountSpec->Init(); 446 outStreamPointers.Add(outStreamSizeCount); 447 } 448 449 for (i = 1; i < _bindInfo.PackStreams.Size(); i++) 450 outStreamPointers.Add(tempBuffers[i - 1]); 451 452 bool dataAfterEnd_Error; 453 454 RINOK(_mixer->Code( 455 &inStreamPointer, 456 &outStreamPointers.Front(), 457 mtProgress ? (ICompressProgressInfo *)mtProgress : compressProgress, dataAfterEnd_Error)); 458 459 if (_bindInfo.PackStreams.Size() != 0) 460 packSizes.Add(outStreamSizeCountSpec->GetSize()); 461 462 for (i = 1; i < _bindInfo.PackStreams.Size(); i++) 463 { 464 CInOutTempBuffer &inOutTempBuffer = inOutTempBuffers[i - 1]; 465 RINOK(inOutTempBuffer.WriteToStream(outStream)); 466 packSizes.Add(inOutTempBuffer.GetDataSize()); 467 } 468 469 unpackSize = 0; 470 471 for (i = 0; i < _bindInfo.Coders.Size(); i++) 472 { 473 int bond = _bindInfo.FindBond_for_UnpackStream(_DestOut_to_SrcIn[i]); 474 UInt64 streamSize; 475 if (bond < 0) 476 { 477 streamSize = inStreamSizeCountSpec->GetSize(); 478 unpackSize = streamSize; 479 } 480 else 481 streamSize = _mixer->GetBondStreamSize(bond); 482 coderUnpackSizes.Add(streamSize); 483 } 484 485 return S_OK; 486 } 487 488 489 CEncoder::CEncoder(const CCompressionMethodMode &options): 490 _constructed(false) 491 { 492 if (options.IsEmpty()) 493 throw 1; 494 495 _options = options; 496 497 #ifdef USE_MIXER_ST 498 _mixerST = NULL; 499 #endif 500 501 #ifdef USE_MIXER_MT 502 _mixerMT = NULL; 503 #endif 504 505 _mixer = NULL; 506 } 507 508 509 HRESULT CEncoder::EncoderConstr() 510 { 511 if (_constructed) 512 return S_OK; 513 if (_options.Methods.IsEmpty()) 514 { 515 // it has only password method; 516 if (!_options.PasswordIsDefined) 517 throw 1; 518 if (!_options.Bonds.IsEmpty()) 519 throw 1; 520 521 CMethodFull method; 522 method.Id = k_AES; 523 method.NumStreams = 1; 524 _options.Methods.Add(method); 525 526 NCoderMixer2::CCoderStreamsInfo coderStreamsInfo; 527 coderStreamsInfo.NumStreams = 1; 528 _bindInfo.Coders.Add(coderStreamsInfo); 529 530 _bindInfo.PackStreams.Add(0); 531 _bindInfo.UnpackCoder = 0; 532 } 533 else 534 { 535 536 UInt32 numOutStreams = 0; 537 unsigned i; 538 539 for (i = 0; i < _options.Methods.Size(); i++) 540 { 541 const CMethodFull &methodFull = _options.Methods[i]; 542 NCoderMixer2::CCoderStreamsInfo cod; 543 544 cod.NumStreams = methodFull.NumStreams; 545 546 if (_options.Bonds.IsEmpty()) 547 { 548 // if there are no bonds in options, we create bonds via first streams of coders 549 if (i != _options.Methods.Size() - 1) 550 { 551 NCoderMixer2::CBond bond; 552 bond.PackIndex = numOutStreams; 553 bond.UnpackIndex = i + 1; // it's next coder 554 _bindInfo.Bonds.Add(bond); 555 } 556 else if (cod.NumStreams != 0) 557 _bindInfo.PackStreams.Insert(0, numOutStreams); 558 559 for (UInt32 j = 1; j < cod.NumStreams; j++) 560 _bindInfo.PackStreams.Add(numOutStreams + j); 561 } 562 563 numOutStreams += cod.NumStreams; 564 565 _bindInfo.Coders.Add(cod); 566 } 567 568 if (!_options.Bonds.IsEmpty()) 569 { 570 for (i = 0; i < _options.Bonds.Size(); i++) 571 { 572 NCoderMixer2::CBond mixerBond; 573 const CBond2 &bond = _options.Bonds[i]; 574 if (bond.InCoder >= _bindInfo.Coders.Size() 575 || bond.OutCoder >= _bindInfo.Coders.Size() 576 || bond.OutStream >= _bindInfo.Coders[bond.OutCoder].NumStreams) 577 return E_INVALIDARG; 578 mixerBond.PackIndex = _bindInfo.GetStream_for_Coder(bond.OutCoder) + bond.OutStream; 579 mixerBond.UnpackIndex = bond.InCoder; 580 _bindInfo.Bonds.Add(mixerBond); 581 } 582 583 for (i = 0; i < numOutStreams; i++) 584 if (_bindInfo.FindBond_for_PackStream(i) == -1) 585 _bindInfo.PackStreams.Add(i); 586 } 587 588 if (!_bindInfo.SetUnpackCoder()) 589 return E_INVALIDARG; 590 591 if (!_bindInfo.CalcMapsAndCheck()) 592 return E_INVALIDARG; 593 594 if (_bindInfo.PackStreams.Size() != 1) 595 { 596 /* main_PackStream is pack stream of main path of coders tree. 597 We find main_PackStream, and place to start of list of out streams. 598 It allows to use more optimal memory usage for temp buffers, 599 if main_PackStream is largest stream. */ 600 601 UInt32 ci = _bindInfo.UnpackCoder; 602 603 for (;;) 604 { 605 if (_bindInfo.Coders[ci].NumStreams == 0) 606 break; 607 608 UInt32 outIndex = _bindInfo.Coder_to_Stream[ci]; 609 int bond = _bindInfo.FindBond_for_PackStream(outIndex); 610 if (bond >= 0) 611 { 612 ci = _bindInfo.Bonds[bond].UnpackIndex; 613 continue; 614 } 615 616 int si = _bindInfo.FindStream_in_PackStreams(outIndex); 617 if (si >= 0) 618 _bindInfo.PackStreams.MoveToFront(si); 619 break; 620 } 621 } 622 623 if (_options.PasswordIsDefined) 624 { 625 unsigned numCryptoStreams = _bindInfo.PackStreams.Size(); 626 627 unsigned numInStreams = _bindInfo.Coders.Size(); 628 629 for (i = 0; i < numCryptoStreams; i++) 630 { 631 NCoderMixer2::CBond bond; 632 bond.UnpackIndex = numInStreams + i; 633 bond.PackIndex = _bindInfo.PackStreams[i]; 634 _bindInfo.Bonds.Add(bond); 635 } 636 _bindInfo.PackStreams.Clear(); 637 638 /* 639 if (numCryptoStreams == 0) 640 numCryptoStreams = 1; 641 */ 642 643 for (i = 0; i < numCryptoStreams; i++) 644 { 645 CMethodFull method; 646 method.NumStreams = 1; 647 method.Id = k_AES; 648 _options.Methods.Add(method); 649 650 NCoderMixer2::CCoderStreamsInfo cod; 651 cod.NumStreams = 1; 652 _bindInfo.Coders.Add(cod); 653 654 _bindInfo.PackStreams.Add(numOutStreams++); 655 } 656 } 657 658 } 659 660 for (unsigned i = _options.Methods.Size(); i != 0;) 661 _decompressionMethods.Add(_options.Methods[--i].Id); 662 663 if (_bindInfo.Coders.Size() > 16) 664 return E_INVALIDARG; 665 if (_bindInfo.GetNum_Bonds_and_PackStreams() > 16) 666 return E_INVALIDARG; 667 668 if (!_bindInfo.CalcMapsAndCheck()) 669 return E_INVALIDARG; 670 671 InitBindConv(); 672 _constructed = true; 673 return S_OK; 674 } 675 676 CEncoder::~CEncoder() {} 677 678 }} 679