static HRESULT WriteVolumeHeader(COutArchive &archive, CFileItem &file, const CUpdateOptions &options) { CCoderInfo coder; coder.NumInStreams = coder.NumOutStreams = 1; coder.MethodID = k_Copy; CFolder folder; folder.Coders.Add(coder); folder.PackStreams.Add(0); CNum numUnPackStreams = 0; if (file.UnPackSize != 0) { file.IsFileCRCDefined = true; file.HasStream = true; numUnPackStreams++; } else { throw 1; file.IsFileCRCDefined = false; file.HasStream = false; } folder.UnPackSizes.Add(file.UnPackSize); CArchiveDatabase newDatabase; newDatabase.Files.Add(file); newDatabase.Folders.Add(folder); newDatabase.NumUnPackStreamsVector.Add(numUnPackStreams); newDatabase.PackSizes.Add(file.UnPackSize); newDatabase.PackCRCsDefined.Add(false); newDatabase.PackCRCs.Add(file.FileCRC); return archive.WriteDatabase(newDatabase, options.HeaderMethod, false, false); }
STDMETHODIMP CHandler::UpdateItems(ISequentialOutStream *outStream, UInt32 numItems, IArchiveUpdateCallback *updateCallback) { COM_TRY_BEGIN const CArchiveDatabaseEx *db = 0; #ifdef _7Z_VOL if (_volumes.Size() > 1) return E_FAIL; const CVolume *volume = 0; if (_volumes.Size() == 1) { volume = &_volumes.Front(); db = &volume->Database; } #else if (_inStream != 0) db = &_db; #endif CObjectVector<CUpdateItem> updateItems; for (UInt32 i = 0; i < numItems; i++) { Int32 newData, newProps; UInt32 indexInArchive; if (!updateCallback) return E_FAIL; RINOK(updateCallback->GetUpdateItemInfo(i, &newData, &newProps, &indexInArchive)); CUpdateItem ui; ui.NewProps = IntToBool(newProps); ui.NewData = IntToBool(newData); ui.IndexInArchive = indexInArchive; ui.IndexInClient = i; ui.IsAnti = false; ui.Size = 0; if (ui.IndexInArchive != -1) { if (db == 0 || ui.IndexInArchive >= db->Files.Size()) return E_INVALIDARG; const CFileItem &fi = db->Files[ui.IndexInArchive]; ui.Name = fi.Name; ui.IsDir = fi.IsDir; ui.Size = fi.Size; ui.IsAnti = db->IsItemAnti(ui.IndexInArchive); ui.CTimeDefined = db->CTime.GetItem(ui.IndexInArchive, ui.CTime); ui.ATimeDefined = db->ATime.GetItem(ui.IndexInArchive, ui.ATime); ui.MTimeDefined = db->MTime.GetItem(ui.IndexInArchive, ui.MTime); } if (ui.NewProps) { bool nameIsDefined; bool folderStatusIsDefined; { NCOM::CPropVariant prop; RINOK(updateCallback->GetProperty(i, kpidAttrib, &prop)); if (prop.vt == VT_EMPTY) ui.AttribDefined = false; else if (prop.vt != VT_UI4) return E_INVALIDARG; else { ui.Attrib = prop.ulVal; ui.AttribDefined = true; } } // we need MTime to sort files. RINOK(GetTime(updateCallback, i, WriteCTime, kpidCTime, ui.CTime, ui.CTimeDefined)); RINOK(GetTime(updateCallback, i, WriteATime, kpidATime, ui.ATime, ui.ATimeDefined)); RINOK(GetTime(updateCallback, i, true, kpidMTime, ui.MTime, ui.MTimeDefined)); { NCOM::CPropVariant prop; RINOK(updateCallback->GetProperty(i, kpidPath, &prop)); if (prop.vt == VT_EMPTY) nameIsDefined = false; else if (prop.vt != VT_BSTR) return E_INVALIDARG; else { ui.Name = NItemName::MakeLegalName(prop.bstrVal); nameIsDefined = true; } } { NCOM::CPropVariant prop; RINOK(updateCallback->GetProperty(i, kpidIsDir, &prop)); if (prop.vt == VT_EMPTY) folderStatusIsDefined = false; else if (prop.vt != VT_BOOL) return E_INVALIDARG; else { ui.IsDir = (prop.boolVal != VARIANT_FALSE); folderStatusIsDefined = true; } } { NCOM::CPropVariant prop; RINOK(updateCallback->GetProperty(i, kpidIsAnti, &prop)); if (prop.vt == VT_EMPTY) ui.IsAnti = false; else if (prop.vt != VT_BOOL) return E_INVALIDARG; else ui.IsAnti = (prop.boolVal != VARIANT_FALSE); } if (ui.IsAnti) { ui.AttribDefined = false; ui.CTimeDefined = false; ui.ATimeDefined = false; ui.MTimeDefined = false; ui.Size = 0; } if (!folderStatusIsDefined && ui.AttribDefined) ui.SetDirStatusFromAttrib(); } if (ui.NewData) { NCOM::CPropVariant prop; RINOK(updateCallback->GetProperty(i, kpidSize, &prop)); if (prop.vt != VT_UI8) return E_INVALIDARG; ui.Size = (UInt64)prop.uhVal.QuadPart; if (ui.Size != 0 && ui.IsAnti) return E_INVALIDARG; } updateItems.Add(ui); } CCompressionMethodMode methodMode, headerMethod; RINOK(SetCompressionMethod(methodMode, headerMethod)); #ifdef COMPRESS_MT methodMode.NumThreads = _numThreads; headerMethod.NumThreads = 1; #endif CMyComPtr<ICryptoGetTextPassword2> getPassword2; updateCallback->QueryInterface(IID_ICryptoGetTextPassword2, (void **)&getPassword2); if (getPassword2) { CMyComBSTR password; Int32 passwordIsDefined; RINOK(getPassword2->CryptoGetTextPassword2(&passwordIsDefined, &password)); methodMode.PasswordIsDefined = IntToBool(passwordIsDefined); if (methodMode.PasswordIsDefined) methodMode.Password = password; } else methodMode.PasswordIsDefined = false; bool compressMainHeader = _compressHeaders; // check it bool encryptHeaders = false; if (methodMode.PasswordIsDefined) { if (_encryptHeadersSpecified) encryptHeaders = _encryptHeaders; #ifndef _NO_CRYPTO else encryptHeaders = _passwordIsDefined; #endif compressMainHeader = true; if (encryptHeaders) { headerMethod.PasswordIsDefined = methodMode.PasswordIsDefined; headerMethod.Password = methodMode.Password; } } if (numItems < 2) compressMainHeader = false; CUpdateOptions options; options.Method = &methodMode; options.HeaderMethod = (_compressHeaders || encryptHeaders) ? &headerMethod : 0; options.UseFilters = _level != 0 && _autoFilter; options.MaxFilter = _level >= 8; options.HeaderOptions.CompressMainHeader = compressMainHeader; options.HeaderOptions.WriteCTime = WriteCTime; options.HeaderOptions.WriteATime = WriteATime; options.HeaderOptions.WriteMTime = WriteMTime; options.NumSolidFiles = _numSolidFiles; options.NumSolidBytes = _numSolidBytes; options.SolidExtension = _solidExtension; options.RemoveSfxBlock = _removeSfxBlock; options.VolumeMode = _volumeMode; COutArchive archive; CArchiveDatabase newDatabase; CMyComPtr<ICryptoGetTextPassword> getPassword; updateCallback->QueryInterface(IID_ICryptoGetTextPassword, (void **)&getPassword); HRESULT res = Update( EXTERNAL_CODECS_VARS #ifdef _7Z_VOL volume ? volume->Stream: 0, volume ? db : 0, #else _inStream, db, #endif updateItems, archive, newDatabase, outStream, updateCallback, options #ifndef _NO_CRYPTO , getPassword #endif ); RINOK(res); updateItems.ClearAndFree(); return archive.WriteDatabase(EXTERNAL_CODECS_VARS newDatabase, options.HeaderMethod, options.HeaderOptions); COM_TRY_END }
static HRESULT Update2( DECL_EXTERNAL_CODECS_LOC_VARS IInStream *inStream, const CArchiveDatabaseEx *database, const CObjectVector<CUpdateItem> &updateItems, ISequentialOutStream *seqOutStream, IArchiveUpdateCallback *updateCallback, const CUpdateOptions &options) { UInt64 numSolidFiles = options.NumSolidFiles; if (numSolidFiles == 0) numSolidFiles = 1; /* CMyComPtr<IOutStream> outStream; RINOK(seqOutStream->QueryInterface(IID_IOutStream, (void **)&outStream)); if (!outStream) return E_NOTIMPL; */ UInt64 startBlockSize = database != 0 ? database->ArchiveInfo.StartPosition: 0; if (startBlockSize > 0 && !options.RemoveSfxBlock) { RINOK(WriteRange(inStream, seqOutStream, 0, startBlockSize, NULL)); } CRecordVector<int> fileIndexToUpdateIndexMap; if (database != 0) { fileIndexToUpdateIndexMap.Reserve(database->Files.Size()); for (int i = 0; i < database->Files.Size(); i++) fileIndexToUpdateIndexMap.Add(-1); } int i; for(i = 0; i < updateItems.Size(); i++) { int index = updateItems[i].IndexInArchive; if (index != -1) fileIndexToUpdateIndexMap[index] = i; } CRecordVector<int> folderRefs; if (database != 0) { for(i = 0; i < database->Folders.Size(); i++) { CNum indexInFolder = 0; CNum numCopyItems = 0; CNum numUnPackStreams = database->NumUnPackStreamsVector[i]; for (CNum fileIndex = database->FolderStartFileIndex[i]; indexInFolder < numUnPackStreams; fileIndex++) { if (database->Files[fileIndex].HasStream) { indexInFolder++; int updateIndex = fileIndexToUpdateIndexMap[fileIndex]; if (updateIndex >= 0) if (!updateItems[updateIndex].NewData) numCopyItems++; } } if (numCopyItems != numUnPackStreams && numCopyItems != 0) return E_NOTIMPL; // It needs repacking !!! if (numCopyItems > 0) folderRefs.Add(i); } folderRefs.Sort(CompareFolderRefs, (void *)database); } CArchiveDatabase newDatabase; //////////////////////////// COutArchive archive; RINOK(archive.Create(seqOutStream, false)); RINOK(archive.SkeepPrefixArchiveHeader()); UInt64 complexity = 0; for(i = 0; i < folderRefs.Size(); i++) complexity += database->GetFolderFullPackSize(folderRefs[i]); UInt64 inSizeForReduce = 0; for(i = 0; i < updateItems.Size(); i++) { const CUpdateItem &updateItem = updateItems[i]; if (updateItem.NewData) { complexity += updateItem.Size; if (numSolidFiles == 1) { if (updateItem.Size > inSizeForReduce) inSizeForReduce = updateItem.Size; } else inSizeForReduce += updateItem.Size; } } RINOK(updateCallback->SetTotal(complexity)); complexity = 0; RINOK(updateCallback->SetCompleted(&complexity)); CLocalProgress *lps = new CLocalProgress; CMyComPtr<ICompressProgressInfo> progress = lps; lps->Init(updateCallback, true); ///////////////////////////////////////// // Write Copy Items for(i = 0; i < folderRefs.Size(); i++) { int folderIndex = folderRefs[i]; lps->ProgressOffset = complexity; UInt64 packSize = database->GetFolderFullPackSize(folderIndex); RINOK(WriteRange(inStream, archive.SeqStream, database->GetFolderStreamPos(folderIndex, 0), packSize, progress)); complexity += packSize; const CFolder &folder = database->Folders[folderIndex]; CNum startIndex = database->FolderStartPackStreamIndex[folderIndex]; for (int j = 0; j < folder.PackStreams.Size(); j++) { newDatabase.PackSizes.Add(database->PackSizes[startIndex + j]); // newDatabase.PackCRCsDefined.Add(database.PackCRCsDefined[startIndex + j]); // newDatabase.PackCRCs.Add(database.PackCRCs[startIndex + j]); } newDatabase.Folders.Add(folder); CNum numUnPackStreams = database->NumUnPackStreamsVector[folderIndex]; newDatabase.NumUnPackStreamsVector.Add(numUnPackStreams); CNum indexInFolder = 0; for (CNum fi = database->FolderStartFileIndex[folderIndex]; indexInFolder < numUnPackStreams; fi++) { CFileItem file = database->Files[fi]; if (file.HasStream) { indexInFolder++; int updateIndex = fileIndexToUpdateIndexMap[fi]; if (updateIndex >= 0) { const CUpdateItem &updateItem = updateItems[updateIndex]; if (updateItem.NewProperties) { CFileItem file2; FromUpdateItemToFileItem(updateItem, file2); file2.UnPackSize = file.UnPackSize; file2.FileCRC = file.FileCRC; file2.IsFileCRCDefined = file.IsFileCRCDefined; file2.HasStream = file.HasStream; file = file2; } } newDatabase.Files.Add(file); } } } ///////////////////////////////////////// // Compress New Files CObjectVector<CSolidGroup> groups; SplitFilesToGroups(*options.Method, options.UseFilters, options.MaxFilter, updateItems, groups); const UInt32 kMinReduceSize = (1 << 16); if (inSizeForReduce < kMinReduceSize) inSizeForReduce = kMinReduceSize; for (int groupIndex = 0; groupIndex < groups.Size(); groupIndex++) { const CSolidGroup &group = groups[groupIndex]; int numFiles = group.Indices.Size(); if (numFiles == 0) continue; CRecordVector<CRefItem> refItems; refItems.Reserve(numFiles); bool sortByType = (numSolidFiles > 1); for (i = 0; i < numFiles; i++) refItems.Add(CRefItem(group.Indices[i], updateItems[group.Indices[i]], sortByType)); refItems.Sort(CompareUpdateItems, (void *)&sortByType); CRecordVector<UInt32> indices; indices.Reserve(numFiles); for (i = 0; i < numFiles; i++) { UInt32 index = refItems[i].Index; indices.Add(index); /* const CUpdateItem &updateItem = updateItems[index]; CFileItem file; if (updateItem.NewProperties) FromUpdateItemToFileItem(updateItem, file); else file = database.Files[updateItem.IndexInArchive]; if (file.IsAnti || file.IsDirectory) return E_FAIL; newDatabase.Files.Add(file); */ } CEncoder encoder(group.Method); for (i = 0; i < numFiles;) { UInt64 totalSize = 0; int numSubFiles; UString prevExtension; for (numSubFiles = 0; i + numSubFiles < numFiles && numSubFiles < numSolidFiles; numSubFiles++) { const CUpdateItem &updateItem = updateItems[indices[i + numSubFiles]]; totalSize += updateItem.Size; if (totalSize > options.NumSolidBytes) break; if (options.SolidExtension) { UString ext = updateItem.GetExtension(); if (numSubFiles == 0) prevExtension = ext; else if (ext.CompareNoCase(prevExtension) != 0) break; } } if (numSubFiles < 1) numSubFiles = 1; CFolderInStream *inStreamSpec = new CFolderInStream; CMyComPtr<ISequentialInStream> solidInStream(inStreamSpec); inStreamSpec->Init(updateCallback, &indices[i], numSubFiles); CFolder folderItem; int startPackIndex = newDatabase.PackSizes.Size(); RINOK(encoder.Encode( EXTERNAL_CODECS_LOC_VARS solidInStream, NULL, &inSizeForReduce, folderItem, archive.SeqStream, newDatabase.PackSizes, progress)); for (; startPackIndex < newDatabase.PackSizes.Size(); startPackIndex++) lps->OutSize += newDatabase.PackSizes[startPackIndex]; lps->InSize += folderItem.GetUnPackSize(); // for() // newDatabase.PackCRCsDefined.Add(false); // newDatabase.PackCRCs.Add(0); newDatabase.Folders.Add(folderItem); CNum numUnPackStreams = 0; for (int subIndex = 0; subIndex < numSubFiles; subIndex++) { const CUpdateItem &updateItem = updateItems[indices[i + subIndex]]; CFileItem file; if (updateItem.NewProperties) FromUpdateItemToFileItem(updateItem, file); else file = database->Files[updateItem.IndexInArchive]; if (file.IsAnti || file.IsDirectory) return E_FAIL; /* CFileItem &file = newDatabase.Files[ startFileIndexInDatabase + i + subIndex]; */ if (!inStreamSpec->Processed[subIndex]) { continue; // file.Name += L".locked"; } file.FileCRC = inStreamSpec->CRCs[subIndex]; file.UnPackSize = inStreamSpec->Sizes[subIndex]; if (file.UnPackSize != 0) { file.IsFileCRCDefined = true; file.HasStream = true; numUnPackStreams++; } else { file.IsFileCRCDefined = false; file.HasStream = false; } newDatabase.Files.Add(file); } // numUnPackStreams = 0 is very bad case for locked files // v3.13 doesn't understand it. newDatabase.NumUnPackStreamsVector.Add(numUnPackStreams); i += numSubFiles; } } { ///////////////////////////////////////// // Write Empty Files & Folders CRecordVector<int> emptyRefs; for(i = 0; i < updateItems.Size(); i++) { const CUpdateItem &updateItem = updateItems[i]; if (updateItem.NewData) { if (updateItem.HasStream()) continue; } else if (updateItem.IndexInArchive != -1) if (database->Files[updateItem.IndexInArchive].HasStream) continue; emptyRefs.Add(i); } emptyRefs.Sort(CompareEmptyItems, (void *)&updateItems); for(i = 0; i < emptyRefs.Size(); i++) { const CUpdateItem &updateItem = updateItems[emptyRefs[i]]; CFileItem file; if (updateItem.NewProperties) FromUpdateItemToFileItem(updateItem, file); else file = database->Files[updateItem.IndexInArchive]; newDatabase.Files.Add(file); } } /* if (newDatabase.Files.Size() != updateItems.Size()) return E_FAIL; */ return archive.WriteDatabase(EXTERNAL_CODECS_LOC_VARS newDatabase, options.HeaderMethod, options.HeaderOptions); }
// Create a new 7z archive for each folder contained in the archive to be // exploded. HRESULT ExplodeArchives(CCodecs *codecs, const CIntVector &formatIndices, bool stdInMode, UStringVector &arcPaths, UStringVector &arcPathsFull, UString& outputPath, UInt64 maxDepth, UInt64 &numErrors) { int numArcs = arcPaths.Size(); for (int i = 0; i < numArcs; i++) { UString archivePath = arcPaths[i]; /*UString outputPath = arcPaths[i]; outputPath.Replace(L'\\', L'/'); // linux and windows consistent const UString archiveName = StripFile(outputPath); outputPath.Empty();*/ archivePath.Replace(L'\\', L'/'); // linux, windows and archive consistent outputPath.Replace(L'\\', L'/'); FixPathFormat(outputPath); const UString archiveName = GetFileFromPath(archivePath); g_StdOut << "Outputting into : " << outputPath << endl; UInt64 arcPackSize = 0; if (!stdInMode) { NFile::NFind::CFileInfoW fi; if (!fi.Find(archivePath) || fi.IsDir()) { SHOW_ERROR("is not a file."); continue; } arcPackSize = fi.Size; } g_StdOut << endl << "Exploding : " << archivePath << endl << endl; CArchiveLink archiveLink; COpenCallbackConsole openCallback; openCallback.OutStream = &g_StdOut; #ifndef _NO_CRYPTO openCallback.PasswordIsDefined = false; #endif HRESULT result = archiveLink.Open2(codecs, formatIndices, stdInMode, NULL, archivePath, &openCallback); if (result != S_OK) { if (result == E_ABORT) return result; g_StdOut << endl << "Error: " << archivePath << ": "; if (result == S_FALSE) g_StdOut << "Can not open file as archive"; else if (result == E_OUTOFMEMORY) g_StdOut << "Can't allocate required memory"; else g_StdOut << NError::MyFormatMessage(result); g_StdOut << endl; numErrors++; continue; } // remove other files names if multi-volume if (!stdInMode) { for (int v = 0; v < archiveLink.VolumePaths.Size(); v++) { int index = arcPathsFull.FindInSorted(archiveLink.VolumePaths[v]); if (index >= 0 && index > i) { arcPaths.Delete(index); arcPathsFull.Delete(index); numArcs = arcPaths.Size(); } } } // don't support multi volume because i have to reopen the stream if (archiveLink.VolumePaths.Size() != 1) { SHOW_ERROR("Exploding multi-volume archives isn't supported."); continue; } bool szArchive = true; for (int x = 0; x < archiveLink.Arcs.Size(); x++) { const UString szName = L"7z"; const CArc &arc = archiveLink.Arcs[x]; if (codecs->Formats[arc.FormatIndex].Name != szName) { szArchive = false; break; } } if (!szArchive) { SHOW_ERROR("Only 7z archives can be exploded."); continue; } // Only 7z is supported, and it's been checked using namespace NArchive::N7z; IInArchive* inArc = archiveLink.GetArchive(); CHandler* szHandler = (CHandler*)inArc; // not a fan of having to reopen the file.. CInFileStream* _inStream = new CInFileStream; CMyComPtr<CInFileStream> inStream(_inStream); if (!inStream->Open(archivePath)) { SHOW_ERROR("Cannot be opened for reading."); continue; } // Explode the archive into each folder CObjectVector<szExplodeData> exploded; szHandler->Explode(exploded, maxDepth); if (exploded.Size() == 0) { SHOW_ERROR("Empty archive!"); continue; } // should make a struct that gets passed to Explode.. // something like /* * struct a { * CArchiveDatabase newDatabase; * CRecordVector<UInt64> folderSizes, folderPositions * }; * CObjectVector<a> exploded; * szHandler->Explode(exploded); */ // Save each folder as a new 7z archive for (int x = 0; x < exploded.Size(); x++) { UString relativeFilePath; // relative to archive UString fileName; CArchiveDatabase& newDatabase = exploded[x].newDatabase; szExplodeData& explodeData = exploded[x]; // each exploded archive will only have a single folder. // no longer true. need to make sure the selected file // is the highest in the dir tree. could make 7zhandler // give us this info i guess. if (newDatabase.Files.Size() > 0) { relativeFilePath = newDatabase.Files[0].Name; if (!newDatabase.Files[0].IsDir) { fileName = GetFileFromPath(relativeFilePath); StripFile(relativeFilePath); } } //g_StdOut << "Relative path " << relativeFilePath << endl; //g_StdOut << "Archive " << archivePath << endl; UString folderOutPath = outputPath + relativeFilePath; if (relativeFilePath.Length() != 0) { bool b = NWindows::NFile::NDirectory::CreateComplexDirectory(folderOutPath); if (!b) g_StdOut << "Couldn't create directory " << folderOutPath << endl; //relativeFilePath.Insert(folderOutPath.Length(), L'/'); } std::wstringstream sstream; sstream << folderOutPath.GetBuffer(); if (newDatabase.Files.Size() == 1) // can use file names sstream << fileName.GetBuffer(); else // use folder as name sstream << archiveName.GetBuffer() << L"_folder_" << x; sstream << ".7z"; g_StdOut << "Saving as '" << sstream.str().c_str() << "'" << endl; COutFileStream* _outstream = new COutFileStream; CMyComPtr<COutFileStream> outstream(_outstream); outstream->Create(sstream.str().c_str(), true); COutArchive out; out.Create(outstream, false); out.SkipPrefixArchiveHeader(); for (int folderIndex = 0; folderIndex < newDatabase.Folders.Size(); folderIndex++) { UInt64 folderLen = explodeData.folderSizes[folderIndex]; UInt64 folderStartPackPos = explodeData.folderPositions[folderIndex]; // write actual data RINOK(WriteRange(inStream, out.SeqStream, folderStartPackPos, folderLen, NULL)); } CCompressionMethodMode method, headerMethod; szHandler->SetCompressionMethod(method, headerMethod); CHeaderOptions headerOptions; headerOptions.CompressMainHeader = true; out.WriteDatabase(newDatabase, &headerMethod, headerOptions); out.Close(); /*#ifdef ENV_UNIX // Create a symlink for each file in the folder. // This makes it seem as though each file is individually accessible. for (int fileIndex = 0; fileIndex < newDatabase.Files.Size(); fileIndex++) { AString oldfile, newfile; UString woldfile = sstream.str().c_str(); UString wnewfile = outputPath + relativeFilePath + newDatabase.Files[fileIndex].Name + L".7z"; ConvertUnicodeToUTF8(woldfile, oldfile); ConvertUnicodeToUTF8(wnewfile, newfile); const char* link_to = oldfile.GetBuffer(); const char* link_name = newfile.GetBuffer(); unlink(link_name);// should ask user //g_StdOut << "Creating symlink to '" << link_to << "' called '" << link_name << "'" << endl; int status = symlink(link_to, link_name); if (status == -1) { AString error = "Couldn't create symlink for '"; error += newfile; error += "'"; SHOW_ERROR(error); g_StdOut << "Error: " << errno << endl; } } #endif*/ } archiveLink.Close(); // not needed but oh well } return S_OK; }