/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2009-2016, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ #include "C4Include.h" #include "c4group/C4Update.h" #include "C4Version.h" #include "c4group/C4Components.h" #include "c4group/C4Group.h" #include "lib/C4Log.h" C4Config *GetCfg(); #ifdef _WIN32 #include "platform/C4windowswrapper.h" #include #endif // helper bool C4Group_CopyEntry(C4Group *pFrom, C4Group *pTo, const char *strItemName) { // read entry char *pData; size_t iSize; if (!pFrom->LoadEntry(strItemName, &pData, &iSize)) return false; if (!pTo->Add(strItemName, pData, iSize, false, true)) return false; return true; } bool C4Group_ApplyUpdate(C4Group &hGroup, unsigned long ParentProcessID) { // Wait for parent process to terminate (so we can safely replace the executable) #ifdef _WIN32 if(ParentProcessID) { HANDLE ParentProcess = OpenProcess(SYNCHRONIZE, FALSE, ParentProcessID); if(ParentProcess) { // If we couldn't find a handle then either // a) the process terminated already, which is great. // b) OpenProcess() failed, which is not so great. But let's still try to do // the update. printf("Waiting for parent process to terminate..."); DWORD res = WaitForSingleObject(ParentProcess, 10000); if(res == WAIT_TIMEOUT) fprintf(stderr, "Parent process did not terminate after 10 seconds. Continuing..."); } } #else // We could use waitpid on Unix, but we don't need that functionality there anyway... #endif // Process object update group (GRPUP_Entries.txt found) C4UpdatePackage Upd; if (hGroup.FindEntry(C4CFN_UpdateEntries)) if (Upd.Load(&hGroup)) { // Do update check first (ensure packet has everything it needs in order to perfom the update) int iRes = Upd.Check(&hGroup); switch (iRes) { // Bad version - checks against version of the applying executable (major version must match, minor version must be equal or higher) case C4UPD_CHK_BAD_VERSION: fprintf(stderr, "This update %s can only be applied using version %d.%d.%d.%d or higher.\n", Upd.Name, Upd.RequireVersion[0], Upd.RequireVersion[1], Upd.RequireVersion[2], Upd.RequireVersion[3]); return false; // Target not found: keep going case C4UPD_CHK_NO_SOURCE: fprintf(stderr, "Target %s for update %s not found. Ignoring.\n", Upd.DestPath, Upd.Name); return true; // Target mismatch: abort updating case C4UPD_CHK_BAD_SOURCE: fprintf(stderr, "Target %s incorrect version for update %s. Ignoring.\n", Upd.DestPath, Upd.Name); return true; // Target already updated: keep going case C4UPD_CHK_ALREADY_UPDATED: fprintf(stderr,"Target %s already up-to-date at %s.\n", Upd.DestPath, Upd.Name); return true; // Ok to perform update case C4UPD_CHK_OK: printf("Updating %s to %s... ", Upd.DestPath, Upd.Name); // Make sure the user sees the message while the work is in progress fflush(stdout); // Execute update if (Upd.Execute(&hGroup)) { printf("Ok\n"); return true; } else { printf("Failed\n"); return false; } // Unknown return value from update default: fprintf(stderr,"Unknown error while updating.\n"); return false; } } // Process binary update group (AutoUpdate.txt found, additional binary files found) if (hGroup.EntryCount(C4CFN_UpdateCore)) if (hGroup.EntryCount() - hGroup.EntryCount(C4CFN_UpdateCore) - hGroup.EntryCount("*.ocu") > 0) { // Notice: AutoUpdate.txt is currently not processed... char strEntry[_MAX_FNAME + 1] = ""; StdStrBuf strList; printf("Updating binaries...\n"); hGroup.ResetSearch(); // Look for binaries while (hGroup.FindNextEntry("*", strEntry)) // Accept everything except *.ocu, AutoUpdate.txt, and c4group.exe (which is assumed not to work under Windows) if (!WildcardMatch("*.ocu", strEntry) && !WildcardMatch(C4CFN_UpdateCore, strEntry) && !WildcardMatch("c4group.exe", strEntry)) { strList += strEntry; strList += ";"; } // Extract binaries to current working directory if (!hGroup.Extract(strList.getData())) return false; // If extracted file is a group, explode it (this is meant for Clonk.app on Mac) for (int i = 0; SGetModule(strList.getData(), i, strEntry); i++) if (C4Group_IsGroup(strEntry)) { printf("Exploding: %s\n", strEntry); if (!C4Group_ExplodeDirectory(strEntry)) return false; } } // Process any child updates (*.ocu) if (hGroup.FindEntry("*.ocu")) { // Process all children char strEntry[_MAX_FNAME + 1] = ""; C4Group hChild; hGroup.ResetSearch(); while (hGroup.FindNextEntry("*.ocu", strEntry)) if (hChild.OpenAsChild(&hGroup, strEntry)) { bool ok = C4Group_ApplyUpdate(hChild, 0); hChild.Close(); // Failure on child update if (!ok) return false; } } // Success return true; } // *** C4GroupEx class C4GroupEx : public C4Group { public: // some funcs to alter internal values of groups. // Needed to create byte-correct updated files void SetHead(C4Group &rByGrp) { // Cheat away the protection C4GroupHeader *pHdr = &static_cast(rByGrp).Head; // save Entries int Entries = Head.Entries; // copy memcpy(&Head, pHdr, sizeof(Head)); // restore Head.Entries = Entries; } bool HeadIdentical(C4Group &rByGrp, bool fLax) { // Cheat away the protection while avoiding // gcc strict aliasing violation warnings. intptr_t iGroup = (intptr_t) &rByGrp; C4GroupHeader *pHdr = &((C4GroupEx*) iGroup)->Head; // overwrite entries field int Entries = Head.Entries; Head.Entries = pHdr->Entries; // compare bool fIdentical = !memcmp(&Head, pHdr, sizeof(C4GroupHeader)); // restore field values Head.Entries = Entries; // okay return fIdentical; } C4GroupEntryCore SavedCore; void SaveEntryCore(C4Group &rByGrp, const char *szEntry) { C4GroupEntryCore *pCore = ((C4GroupEx &)rByGrp).GetEntry(szEntry); // copy core SavedCore.Executable = pCore->Executable; } void SetSavedEntryCore(const char *szEntry) { C4GroupEntryCore *pCore = GetEntry(szEntry); // copy core pCore->Executable = SavedCore.Executable; } void SetNoSort(const char *szEntry) { C4GroupEntry *pEntry = GetEntry(szEntry); if (pEntry) pEntry->NoSort = true; } // close without header update bool Close(bool fHeaderUpdate) { if (fHeaderUpdate) return C4Group::Close(); else { bool fSuccess = Save(false); Clear(); return fSuccess; } } }; // *** C4UpdatePackageCore void C4UpdatePackageCore::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(toC4CArr(RequireVersion), "RequireVersion")); pComp->Value(mkNamingAdapt(toC4CStr(Name), "Name", "")); pComp->Value(mkNamingAdapt(toC4CStr(DestPath), "DestPath", "")); pComp->Value(mkNamingAdapt(GrpUpdate, "GrpUpdate", 0)); pComp->Value(mkNamingAdapt(UpGrpCnt, "TargetCount", 0)); pComp->Value(mkNamingAdapt(toC4CArrU(GrpChks1), "GrpChks1")); pComp->Value(mkNamingAdapt(GrpChks2, "GrpChks2", 0u)); } bool C4UpdatePackageCore::Load(C4Group &hGroup) { // Load from group StdStrBuf Source; if (!hGroup.LoadEntryString(C4CFN_UpdateCore,&Source)) return false; try { // Compile data CompileFromBuf(mkNamingAdapt(*this, "Update"), Source); } catch (StdCompiler::Exception *pExc) { delete pExc; return false; } return true; } bool C4UpdatePackageCore::Save(C4Group &hGroup) { try { // decompile data StdStrBuf Core = DecompileToBuf(mkNamingAdapt(*this, "Update")); char *stupid_buffer = new char[Core.getLength() + 1]; memcpy(stupid_buffer, Core.getMData(), Core.getLength() + 1); // add to group return hGroup.Add(C4CFN_UpdateCore, stupid_buffer, Core.getLength(), false, true); } catch (StdCompiler::Exception * pExc) { delete pExc; return false; } } // *** C4UpdatePackage bool C4UpdatePackage::Load(C4Group *pGroup) { // read update core StdStrBuf Source; if (!pGroup->LoadEntryString(C4CFN_UpdateCore,&Source)) return false; try { // Compile data CompileFromBuf(mkNamingAdapt(*this, "Update"), Source); } catch (StdCompiler::Exception *pExc) { StdStrBuf Name = pGroup->GetFullName() + DirSep + C4CFN_UpdateCore; WriteLog("ERROR: %s (in %s)", pExc->Msg.getData(), Name.getData()); delete pExc; return false; } return true; } // #define UPDATE_DEBUG bool C4UpdatePackage::Execute(C4Group *pGroup) { // search target C4GroupEx TargetGrp; char strTarget[_MAX_PATH]; SCopy(DestPath, strTarget, _MAX_PATH); char *p = strTarget, *lp = strTarget; while ((p = strchr(p + 1, '\\'))) { *p = 0; if (!*(p + 1)) break; if (!SEqual(lp, "..")) { if (TargetGrp.Open(strTarget)) { // packed? bool fPacked = TargetGrp.IsPacked(); // Close Group TargetGrp.Close(true); if (fPacked) // Unpack C4Group_UnpackDirectory(strTarget); } else { // GrpUpdate -> file must exist if (GrpUpdate) return false; // create dir CreatePath(strTarget); } } *p = '\\'; lp = p + 1; } // try to open it if (!TargetGrp.Open(strTarget, !GrpUpdate)) return false; // check if the update is allowed if (GrpUpdate) { // check checksum uint32_t iCRC32; if (!GetFileCRC(TargetGrp.GetFullName().getData(), &iCRC32)) return false; int i = 0; for (; i < UpGrpCnt; i++) if (iCRC32 == GrpChks1[i]) break; if (i >= UpGrpCnt) return false; } else { // only allow Extra.ocg-Updates if (!SEqual2(DestPath, "Extra.ocg")) return false; } // update children char ItemFileName[_MAX_PATH]; pGroup->ResetSearch(); while (pGroup->FindNextEntry("*", ItemFileName)) if (!SEqual(ItemFileName, C4CFN_UpdateCore) && !SEqual(ItemFileName, C4CFN_UpdateEntries)) DoUpdate(pGroup, &TargetGrp, ItemFileName); // do GrpUpdate if (GrpUpdate) DoGrpUpdate(pGroup, &TargetGrp); // close the group TargetGrp.Close(false); if (GrpUpdate) { // check the result uint32_t iResChks; if (!GetFileCRC(strTarget, &iResChks)) return false; if (iResChks != GrpChks2) { #ifdef UPDATE_DEBUG char *pData; int iSize; CStdFile MyFile; MyFile.Load(strTarget, (BYTE **)&pData, &iSize, 0, true); MyFile.Create("DiesesDingIstMist.txt", false); MyFile.Write(pData, iSize); MyFile.Close(); #endif return false; } } return true; } bool C4UpdatePackage::Optimize(C4Group *pGroup, const char *strTarget) { // Open target group C4GroupEx TargetGrp; if (!TargetGrp.Open(strTarget)) return false; // Both groups must be packed if (!pGroup->IsPacked() || !TargetGrp.IsPacked()) { TargetGrp.Close(false); return false; } // update children char ItemFileName[_MAX_PATH]; pGroup->ResetSearch(); while (pGroup->FindNextEntry("*", ItemFileName)) if (!SEqual(ItemFileName, C4CFN_UpdateCore) && !SEqual(ItemFileName, C4CFN_UpdateEntries)) Optimize(pGroup, &TargetGrp, ItemFileName); // set header if (TargetGrp.HeadIdentical(*pGroup, true)) TargetGrp.SetHead(*pGroup); // save TargetGrp.Close(false); // okay return true; } int C4UpdatePackage::Check(C4Group *pGroup) { // Version requirement is set if (RequireVersion[0]) { // Engine and game version must match (rest ignored) if ((C4XVER1 != RequireVersion[0]) || (C4XVER2 != RequireVersion[1])) return C4UPD_CHK_BAD_VERSION; } // only group updates have any special needs if (!GrpUpdate) return C4UPD_CHK_OK; // check source file C4Group TargetGrp; if (!TargetGrp.Open(DestPath)) return C4UPD_CHK_NO_SOURCE; if (!TargetGrp.IsPacked()) return C4UPD_CHK_BAD_SOURCE; TargetGrp.Close(); // check source crc uint32_t iCRC32; if (!GetFileCRC(DestPath, &iCRC32)) return C4UPD_CHK_BAD_SOURCE; // equal to destination group? if (iCRC32 == GrpChks2) // so there's nothing to do return C4UPD_CHK_ALREADY_UPDATED; // check if it's one of our registered sources int i = 0; for (; i < UpGrpCnt; i++) if (iCRC32 == GrpChks1[i]) break; if (i >= UpGrpCnt) return C4UPD_CHK_BAD_SOURCE; // ok return C4UPD_CHK_OK; } bool C4UpdatePackage::DoUpdate(C4Group *pGrpFrom, C4GroupEx *pGrpTo, const char *strFileName) { // group file? C4Group ItemGroupFrom; if (ItemGroupFrom.OpenAsChild(pGrpFrom, strFileName)) { // try to open target group C4GroupEx ItemGroupTo; char strTempGroup[_MAX_PATH+1]; strTempGroup[0] = 0; if (!ItemGroupTo.OpenAsChild(pGrpTo, strFileName, false, true)) return false; // update children char ItemFileName[_MAX_PATH]; ItemGroupFrom.ResetSearch(); while (ItemGroupFrom.FindNextEntry("*", ItemFileName)) if (!SEqual(ItemFileName, C4CFN_UpdateCore) && !SEqual(ItemFileName, C4CFN_UpdateEntries)) DoUpdate(&ItemGroupFrom, &ItemGroupTo, ItemFileName); if (GrpUpdate) { DoGrpUpdate(&ItemGroupFrom, &ItemGroupTo); // write group (do not change any headers set by DoGrpUpdate!) ItemGroupTo.Close(false); // set core (C4Group::Save overwrites it) pGrpTo->SaveEntryCore(*pGrpFrom, strFileName); pGrpTo->SetSavedEntryCore(strFileName); // flag as no-resort pGrpTo->SetNoSort(strFileName); } else { // write group ItemGroupTo.Close(true); // temporary group? if (strTempGroup[0]) if (!pGrpTo->Move(strTempGroup, strFileName)) return false; } } else { #ifdef _WIN32 OutputDebugString(FormatString("updating %s\\%s\n", pGrpTo->GetFullName().getData(), strFileName).GetWideChar()); #elif defined(_DEBUG) printf("updating %s\\%s\n", pGrpTo->GetFullName().getData(), strFileName); #endif if (!C4Group_CopyEntry(pGrpFrom, pGrpTo, strFileName)) return false; // set core pGrpTo->SaveEntryCore(*pGrpFrom, strFileName); pGrpTo->SetSavedEntryCore(strFileName); } // ok return true; } bool C4UpdatePackage::DoGrpUpdate(C4Group *pUpdateData, C4GroupEx *pGrpTo) { char *pData; // sort entries if (pUpdateData->LoadEntry(C4CFN_UpdateEntries, &pData, NULL, 1)) { // delete all entries that do not appear in the entries list char strItemName[_MAX_FNAME+1], strItemName2[_MAX_FNAME+1]; pGrpTo->ResetSearch(); while (pGrpTo->FindNextEntry("*", strItemName)) { bool fGotIt = false; for (int i = 0; (fGotIt = SCopySegment(pData, i, strItemName2, '|', _MAX_FNAME)); i++) { // remove separator char *pSep = strchr(strItemName2, '='); if (pSep) *pSep = '\0'; // in list? if (SEqual(strItemName, strItemName2)) break; } if (!fGotIt) pGrpTo->DeleteEntry(strItemName); } // set entry times, set sort list char strSortList[32767] = ""; for (int i = 0; SCopySegment(pData, i, strItemName, '|', _MAX_FNAME); i++) { // strip checksum/time (if given) char *pTime = strchr(strItemName, '='); if (pTime) *pTime = '\0'; // copy to sort list SAppend(strItemName, strSortList); SAppendChar('|', strSortList); } // sort by list pGrpTo->Sort(strSortList); delete[] pData; } // copy header from update group pGrpTo->SetHead(*pUpdateData); // ok return true; } bool C4UpdatePackage::Optimize(C4Group *pGrpFrom, C4GroupEx *pGrpTo, const char *strFileName) { // group file? C4Group ItemGroupFrom; if (!ItemGroupFrom.OpenAsChild(pGrpFrom, strFileName)) return true; // try to open target group C4GroupEx ItemGroupTo; if (!ItemGroupTo.OpenAsChild(pGrpTo, strFileName)) return true; // update children char ItemFileName[_MAX_PATH]; ItemGroupFrom.ResetSearch(); while (ItemGroupFrom.FindNextEntry("*", ItemFileName)) Optimize(&ItemGroupFrom, &ItemGroupTo, ItemFileName); // set head if (ItemGroupTo.HeadIdentical(ItemGroupFrom, true)) ItemGroupTo.SetHead(ItemGroupFrom); // write group (do not change any headers set by DoGrpUpdate!) ItemGroupTo.Close(false); // set core (C4Group::Save overwrites it) pGrpTo->SaveEntryCore(*pGrpFrom, strFileName); pGrpTo->SetSavedEntryCore(strFileName); return true; } void MemScramble(BYTE *, int); bool C4UpdatePackage::MakeUpdate(const char *strFile1, const char *strFile2, const char *strUpdateFile, const char *strName) { #ifdef UPDATE_DEBUG char *pData; int iSize; CStdFile MyFile; MyFile.Load(strFile2, (BYTE **)&pData, &iSize, 0, true); MyFile.Create("SoIstRichtig.txt", false); MyFile.Write(pData, iSize); MyFile.Close(); MemScramble((BYTE *)pData, iSize); MyFile.Create("UndSoAuch.txt", false); MyFile.Write(pData, iSize); MyFile.Close(); #endif // open Log if (!Log.Create("Update.log")) return false; // begin message WriteLog("Source: %s\nTarget: %s\nOutput: %s\n\n", strFile1, strFile2, strUpdateFile); // open both groups C4Group Group1, Group2; if (!Group1.Open(strFile1)) { WriteLog("Error: could not open %s!\n", strFile1); return false; } if (!Group2.Open(strFile2)) { WriteLog("Error: could not open %s!\n", strFile2); return false; } // All groups to be compared need to be packed if (!Group1.IsPacked()) { WriteLog("Error: source group %s not packed!\n", strFile1); return false; } if (!Group2.IsPacked()) { WriteLog("Error: target group %s not packed!\n", strFile2); return false; } if (Group1.HasPackedMother()) { WriteLog("Error: source group %s must not have a packed mother group!\n", strFile1); return false; } if (Group2.HasPackedMother()) { WriteLog("Error: target group %s must not have a packed mother group!\n", strFile2); return false; } // create/open update-group C4GroupEx UpGroup; if (!UpGroup.Open(strUpdateFile, true)) { WriteLog("Error: could not open %s!\n", strUpdateFile); return false; } // may be continued update-file -> try to load core UpGrpCnt = 0; bool fContinued = C4UpdatePackageCore::Load(UpGroup); // save crc2 for later check unsigned int iOldChks2 = GrpChks2; // create core info if (strName) SCopy(strName, Name, C4MaxName); else sprintf(Name, "%s Update", GetFilename(strFile1)); SCopy(strFile1, DestPath, _MAX_PATH); GrpUpdate = true; if (!GetFileCRC(strFile1, &GrpChks1[UpGrpCnt])) { WriteLog("Error: could not calc checksum for %s!\n", strFile1); return false; } if (!GetFileCRC(strFile2, &GrpChks2)) { WriteLog("Error: could not calc checksum for %s!\n", strFile2); return false; } if (fContinued) { // continuation check: GrpChks2 matches? if (GrpChks2 != iOldChks2) // that would mess up the update result... { WriteLog("Error: could not add to update package - target groups don't match (checksum error)\n"); return false; } // already supported by this update? int i = 0; for (; i < UpGrpCnt; i++) if (GrpChks1[UpGrpCnt] == GrpChks1[i]) break; if (i < UpGrpCnt) { WriteLog("This update already supports the version of the source file.\n"); return false; } } UpGrpCnt++; // save core if (!C4UpdatePackageCore::Save(UpGroup)) { WriteLog("Could not save update package core!\n"); return false; } // compare groups, create update bool fModified = false; bool fSuccess = MkUp(&Group1, &Group2, &UpGroup, &fModified); // close (save) it UpGroup.Close(false); // error? if (!fSuccess) { WriteLog("Update package not created.\n"); EraseItem(strUpdateFile); return false; } WriteLog("Update package created.\n"); return true; } extern char C4Group_TempPath[_MAX_PATH+1]; bool C4UpdatePackage::MkUp(C4Group *pGrp1, C4Group *pGrp2, C4GroupEx *pUpGrp, bool *fModified) { // (CAUTION: pGrp1 may be NULL - that means that there is no counterpart for Grp2 // in the base group) // compare headers if (!pGrp1 || pGrp1->EntryCRC32() != pGrp2->EntryCRC32()) *fModified = true; // set header pUpGrp->SetHead(*pGrp2); // compare entries char strItemName[_MAX_PATH], strItemName2[_MAX_PATH]; StdStrBuf EntryList; strItemName[0] = strItemName2[0] = 0; pGrp2->ResetSearch(); if (!*fModified) pGrp1->ResetSearch(); int iChangedEntries = 0; while (pGrp2->FindNextEntry("*", strItemName, NULL, !! strItemName[0])) { // add to entry list if (!!EntryList) EntryList.AppendChar('|'); EntryList.AppendFormat("%s=%d", strItemName, pGrp2->EntryCRC32(strItemName)); // no modification detected yet? then check order if (!*fModified) { if (!pGrp1->FindNextEntry("*", strItemName2, NULL, !! strItemName2[0])) *fModified = true; else if (!SEqual(strItemName, strItemName2)) *fModified = true; } // TODO: write DeleteEntries.txt // a child group? C4GroupEx ChildGrp2; if (ChildGrp2.OpenAsChild(pGrp2, strItemName)) { // open in Grp1 C4Group *pChildGrp1 = new C4GroupEx(); if (!pGrp1 || !pChildGrp1->OpenAsChild(pGrp1, strItemName)) { delete pChildGrp1; pChildGrp1 = NULL; } // open group for update data C4GroupEx UpdGroup; char strTempGroupName[_MAX_FNAME + 1]; strTempGroupName[0] = 0; if (!UpdGroup.OpenAsChild(pUpGrp, strItemName)) { // create new group (may be temporary) if (C4Group_TempPath[0]) { SCopy(C4Group_TempPath,strTempGroupName,_MAX_FNAME); SAppend("~upd",strTempGroupName,_MAX_FNAME); } else SCopy("~upd",strTempGroupName,_MAX_FNAME); MakeTempFilename(strTempGroupName); if (!UpdGroup.Open(strTempGroupName, true)) { delete pChildGrp1; WriteLog("Error: could not create temp group\n"); return false; } } // do nested MkUp-search bool Modified = false; bool fSuccess = MkUp(pChildGrp1, &ChildGrp2, &UpdGroup, &Modified); // sort & close extern const char ** C4Group_SortList; UpdGroup.SortByList(C4Group_SortList, ChildGrp2.GetName()); UpdGroup.Close(false); // check entry crcs if (!pGrp1 || (pGrp1->EntryCRC32(strItemName) != pGrp2->EntryCRC32(strItemName))) Modified = true; // add group (if modified) if (fSuccess && Modified) { if (strTempGroupName[0]) if (!pUpGrp->Move(strTempGroupName, strItemName)) { WriteLog("Error: could not add modified group\n"); return false; } // copy core pUpGrp->SaveEntryCore(*pGrp2, strItemName); pUpGrp->SetSavedEntryCore(strItemName); // got a modification in a subgroup *fModified = true; iChangedEntries++; } else // delete group (do not remove groups that existed before!) if (strTempGroupName[0]) if (!EraseItem(strTempGroupName)) { WriteLog("Error: could not delete temporary directory\n"); return false; } delete pChildGrp1; } else { // compare them (size & crc32) if (!pGrp1 || pGrp1->EntrySize(strItemName) != pGrp2->EntrySize(strItemName) || pGrp1->EntryCRC32(strItemName) != pGrp2->EntryCRC32(strItemName)) { bool fCopied = false; // save core (EntryCRC32 might set additional fields) pUpGrp->SaveEntryCore(*pGrp2, strItemName); // already in update grp? if (pUpGrp->EntrySize(strItemName) != pGrp2->EntrySize(strItemName) || pUpGrp->EntryCRC32(strItemName) != pGrp2->EntryCRC32(strItemName)) { // copy it if (!C4Group_CopyEntry(pGrp2, pUpGrp, strItemName)) { WriteLog("Error: could not add changed entry to update group\n"); return false; } // set entry core pUpGrp->SetSavedEntryCore(strItemName); // modified... *fModified = true; fCopied = true; } iChangedEntries++; WriteLog("%s\\%s: update%s\n", pGrp2->GetFullName().getData(), strItemName, fCopied ? "" : " (already in group)"); } } } // write entries list (always) if (!pUpGrp->Add(C4CFN_UpdateEntries, EntryList, false, true)) { WriteLog("Error: could not save entry list!"); return false; } if (iChangedEntries > 0) WriteLog("%s: %d/%d changed (%s)\n", pGrp2->GetFullName().getData(), iChangedEntries, pGrp2->EntryCount(), *fModified ? "update" : "skip"); // success return true; } void C4UpdatePackage::WriteLog(const char *strMsg, ...) { va_list arglist; va_start(arglist, strMsg); char strOutp[1024]; vsprintf(strOutp, strMsg, arglist); Log.Write(strOutp, strlen(strOutp)); Log.Flush(); ::Log(strOutp); }