1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17#include <C4Include.h>
18#include "C4Update.h"
19#include "C4Version.h"
20#include "C4Config.h"
21#include "C4Components.h"
22#include "C4Group.h"
23
24#include <format>
25#include <print>
26
27C4Config *GetCfg();
28
29#ifdef _WIN32
30#include <direct.h>
31#endif
32
33// helper
34bool C4Group_CopyEntry(C4Group *pFrom, C4Group *pTo, const char *strItemName)
35{
36 // read entry
37 char *pData; size_t iSize;
38 if (!pFrom->LoadEntry(szEntryName: strItemName, lpbpBuf: &pData, ipSize: &iSize))
39 return false;
40 // write entry (keep time)
41 int iEntryTime = pFrom->EntryTime(szFilename: strItemName);
42 if (!pTo->Add(szName: strItemName, pBuffer: pData, iSize, fChild: false, fHoldBuffer: true, iTime: iEntryTime))
43 return false;
44 return true;
45}
46
47bool C4Group_ApplyUpdate(C4Group &hGroup)
48{
49 // Process object update group (GRPUP_Entries.txt found)
50 C4UpdatePackage Upd;
51 if (hGroup.FindEntry(C4CFN_UpdateEntries))
52 if (Upd.Load(pGroup: &hGroup))
53 {
54 // Do update check first (ensure packet has everything it needs in order to perfom the update)
55 const auto result = Upd.Check(pGroup: &hGroup);
56 switch (result)
57 {
58 // Bad version - checks against version of the applying executable (major version must match, minor version must be equal or higher)
59 case C4UpdatePackage::CheckResult::BadVersion:
60 std::println(stderr, fmt: "This update {} can only be applied using version {}.{}.{}.{} or higher.", args: +Upd.Name, args&: Upd.RequireVersion[0], args&: Upd.RequireVersion[1], args&: Upd.RequireVersion[2], args&: Upd.RequireVersion[3]);
61 return false;
62 // Target not found: keep going
63 case C4UpdatePackage::CheckResult::NoSource:
64 std::println(stderr, fmt: "Target {} for update {} not found. Ignoring.", args: +Upd.DestPath, args: +Upd.Name);
65 return true;
66 // Target mismatch: abort updating
67 case C4UpdatePackage::CheckResult::BadSource:
68 std::println(stderr, fmt: "Target {} incorrect version for update {}. Ignoring.", args: +Upd.DestPath, args: +Upd.Name);
69 return true;
70 // Target already updated: keep going
71 case C4UpdatePackage::CheckResult::AlreadyUpdated:
72 std::println(stderr, fmt: "Target {} already up-to-date at {}.", args: +Upd.DestPath, args: +Upd.Name);
73 return true;
74 // Ok to perform update
75 case C4UpdatePackage::CheckResult::Ok:
76 std::print(fmt: "Updating {} to {}... ", args: +Upd.DestPath, args: +Upd.Name);
77 // Make sure the user sees the message while the work is in progress
78 fflush(stdout);
79 // Execute update
80 if (Upd.Execute(pGroup: &hGroup))
81 {
82 std::println(fmt: "Ok");
83 return true;
84 }
85 else
86 {
87 std::println(fmt: "Failed");
88 return false;
89 }
90 }
91 std::println(stderr, fmt: "Unknown error while updating.");
92 return false;
93 }
94
95 // Process binary update group (AutoUpdate.txt found, additional binary files found)
96 if (hGroup.EntryCount(C4CFN_UpdateCore))
97 if (hGroup.EntryCount() - hGroup.EntryCount(C4CFN_UpdateCore) - hGroup.EntryCount(szWildCard: "*.c4u") > 0)
98 {
99 // Notice: AutoUpdate.txt is currently not processed...
100 char strEntry[_MAX_FNAME + 1] = "";
101 StdStrBuf strList;
102 std::println(fmt: "Updating binaries...");
103 hGroup.ResetSearch();
104 // Look for binaries
105 while (hGroup.FindNextEntry(szWildCard: "*", sFileName: strEntry))
106 // Accept everything except *.c4u, AutoUpdate.txt, and c4group.exe (which is assumed not to work under Windows)
107 if (!WildcardMatch(szFName1: "*.c4u", szFName2: strEntry) && !WildcardMatch(C4CFN_UpdateCore, szFName2: strEntry) && !WildcardMatch(szFName1: "c4group.exe", szFName2: strEntry))
108 {
109 strList += strEntry; strList += ";";
110 }
111 // Extract binaries to current working directory
112 if (!hGroup.Extract(szFiles: strList.getData()))
113 return false;
114 // If extracted file is a group, explode it (this is meant for Clonk.app on Mac)
115 for (int i = 0; SGetModule(szList: strList.getData(), iIndex: i, sTarget: strEntry); i++)
116 if (C4Group_IsGroup(szFilename: strEntry))
117 {
118 std::println(fmt: "Exploding: {}", args: +strEntry);
119 if (!C4Group_ExplodeDirectory(szFilename: strEntry))
120 return false;
121 }
122 }
123
124 // Process any child updates (*.c4u)
125 if (hGroup.FindEntry(szWildCard: "*.c4u"))
126 {
127 // Process all children
128 char strEntry[_MAX_FNAME + 1] = "";
129 C4Group hChild;
130 hGroup.ResetSearch();
131 while (hGroup.FindNextEntry(szWildCard: "*.c4u", sFileName: strEntry))
132 if (hChild.OpenAsChild(pMother: &hGroup, szEntryName: strEntry))
133 {
134 bool ok = C4Group_ApplyUpdate(hGroup&: hChild);
135 hChild.Close();
136 // Failure on child update
137 if (!ok) return false;
138 }
139 }
140
141 // Success
142 return true;
143}
144
145// *** C4GroupEx
146class C4GroupEx : public C4Group
147{
148public:
149 // some funcs to alter internal values of groups.
150 // Needed to create byte-correct updated files
151
152 void SetHead(C4Group &rByGrp)
153 {
154 // Cheat away the protection
155 C4GroupHeader *pHdr = &static_cast<C4GroupEx &>(rByGrp).Head;
156 // save Entries
157 int Entries = Head.Entries;
158 // copy
159 memcpy(dest: &Head, src: pHdr, n: sizeof(Head));
160 // restore
161 Head.Entries = Entries;
162 }
163
164 bool HeadIdentical(C4Group &rByGrp, bool fLax)
165 {
166 // Cheat away the protection
167 C4GroupHeader *pHdr = &static_cast<C4GroupEx &>(rByGrp).Head;
168 // overwrite entries field
169 int Entries = Head.Entries;
170 Head.Entries = pHdr->Entries;
171 // overwrite creation field
172 int Creation = Head.Creation;
173 if (fLax) Head.Creation = pHdr->Creation;
174 // compare
175 bool fIdentical = !memcmp(s1: &Head, s2: pHdr, n: sizeof(C4GroupHeader));
176 // restore field values
177 Head.Entries = Entries;
178 Head.Creation = Creation;
179 // okay
180 return fIdentical;
181 }
182
183 C4GroupEntryCore SavedCore;
184 void SaveEntryCore(C4Group &rByGrp, const char *szEntry)
185 {
186 C4GroupEntryCore *pCore = (static_cast<C4GroupEx &>(rByGrp)).GetEntry(szName: szEntry);
187 // copy core
188 memcpy(dest: &SavedCore.Time, src: &pCore->Time, n: reinterpret_cast<char *>(&SavedCore) + sizeof(SavedCore) - reinterpret_cast<char *>(&SavedCore.Time));
189 }
190 void SetSavedEntryCore(const char *szEntry)
191 {
192 C4GroupEntryCore *pCore = GetEntry(szName: szEntry);
193 // copy core
194 memcpy(dest: &pCore->Time, src: &SavedCore.Time, n: reinterpret_cast<char *>(&SavedCore) + sizeof(SavedCore) - reinterpret_cast<char *>(&SavedCore.Time));
195 }
196
197 void SetEntryTime(const char *szEntry, int iEntryTime)
198 {
199 C4GroupEntryCore *pCore = GetEntry(szName: szEntry);
200 if (pCore) pCore->Time = iEntryTime;
201 }
202
203 void SetNoSort(const char *szEntry)
204 {
205 C4GroupEntry *pEntry = GetEntry(szName: szEntry);
206 if (pEntry) pEntry->NoSort = true;
207 }
208
209 // close without header update
210 bool Close(bool fHeaderUpdate)
211 {
212 if (fHeaderUpdate) return C4Group::Close(); else { bool fSuccess = Save(fReOpen: false); Clear(); return fSuccess; }
213 }
214};
215
216// *** C4UpdatePackageCore
217
218C4UpdatePackageCore::C4UpdatePackageCore() :
219 RequireVersion{}, Name{}, DestPath{}, GrpUpdate{}, UpGrpCnt{},
220 GrpChks1{}, GrpChks2{} {}
221
222void C4UpdatePackageCore::CompileFunc(StdCompiler *pComp)
223{
224 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: RequireVersion, default_: 0), szName: "RequireVersion"));
225 pComp->Value(rStruct: mkNamingAdapt(toC4CStr(Name), szName: "Name", rDefault: ""));
226 pComp->Value(rStruct: mkNamingAdapt(toC4CStr(DestPath), szName: "DestPath", rDefault: ""));
227 pComp->Value(rStruct: mkNamingAdapt(rValue&: GrpUpdate, szName: "GrpUpdate", rDefault: 0));
228 pComp->Value(rStruct: mkNamingAdapt(rValue&: UpGrpCnt, szName: "TargetCount", rDefault: 0));
229 pComp->Value(rStruct: mkNamingAdapt(rValue&: AllowMissingTarget, szName: "AllowMissingTarget", rDefault: false));
230 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: GrpChks1, default_: 0u), szName: "GrpChks1"));
231 pComp->Value(rStruct: mkNamingAdapt(rValue&: GrpChks2, szName: "GrpChks2", rDefault: 0u));
232 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: GrpContentsCRC1, default_: 0u), szName: "GrpContentsCRC1"));
233 pComp->Value(rStruct: mkNamingAdapt(rValue&: GrpContentsCRC2, szName: "GrpContentsCRC2", rDefault: 0u));
234}
235
236bool C4UpdatePackageCore::Load(C4Group &hGroup)
237{
238 // Load from group
239 StdStrBuf Source;
240 if (!hGroup.LoadEntryString(C4CFN_UpdateCore, Buf&: Source))
241 return false;
242 try
243 {
244 // Compile data
245 CompileFromBuf<StdCompilerINIRead>(TargetStruct: mkNamingAdapt(rValue&: *this, szName: "Update"), SrcBuf: Source);
246 }
247 catch (const StdCompiler::Exception &)
248 {
249 return false;
250 }
251 return true;
252}
253
254bool C4UpdatePackageCore::Save(C4Group &hGroup)
255{
256 try
257 {
258 // decompile data
259 std::string Core = DecompileToBuf<StdCompilerINIWrite>(SrcStruct: mkNamingAdapt(rValue&: *this, szName: "Update"));
260 char *stupid_buffer = new char[Core.size() + 1];
261 memcpy(dest: stupid_buffer, src: Core.c_str(), n: Core.size() + 1);
262 // add to group
263 return hGroup.Add(C4CFN_UpdateCore, pBuffer: stupid_buffer, iSize: Core.size(), fChild: false, fHoldBuffer: true);
264 }
265 catch (const StdCompiler::Exception &)
266 {
267 return false;
268 }
269}
270
271// *** C4UpdatePackage
272
273bool C4UpdatePackage::Load(C4Group *pGroup)
274{
275 // read update core
276 StdStrBuf Source;
277 if (!pGroup->LoadEntryString(C4CFN_UpdateCore, Buf&: Source))
278 return false;
279 try
280 {
281 // Compile data
282 CompileFromBuf<StdCompilerINIRead>(TargetStruct: mkNamingAdapt(rValue&: *this, szName: "Update"), SrcBuf: Source);
283 }
284 catch (const StdCompiler::Exception &e)
285 {
286 StdStrBuf Name = pGroup->GetFullName() + DirSep + C4CFN_UpdateCore;
287 WriteLog(fmt: "ERROR: {} (in {})", args: e.what(), args: Name.getData());
288 return false;
289 }
290 return true;
291}
292
293bool C4UpdatePackage::Execute(C4Group *pGroup)
294{
295 // search target
296 C4GroupEx TargetGrp;
297 char strTarget[_MAX_PATH]; SCopy(szSource: DestPath, sTarget: strTarget, _MAX_PATH);
298 char *p = strTarget, *lp = strTarget;
299 while (p = strchr(s: p + 1, c: '\\'))
300 {
301 *p = 0;
302 if (!*(p + 1)) break;
303 if (!SEqual(szStr1: lp, szStr2: ".."))
304 if (TargetGrp.Open(szGroupName: strTarget))
305 {
306 // packed?
307 bool fPacked = TargetGrp.IsPacked();
308 // maker check (someone might try to unpack directories w/o asking user)
309 if (fPacked)
310 if (!SEqual(szStr1: TargetGrp.GetMaker(), szStr2: pGroup->GetMaker()))
311 return false;
312 // Close Group
313 TargetGrp.Close(fHeaderUpdate: true);
314 if (fPacked)
315 // Unpack
316 C4Group_UnpackDirectory(szFilename: strTarget);
317 }
318 else
319 {
320 // GrpUpdate -> file must exist
321 if (GrpUpdate) return false;
322 // create dir
323 MakeDirectory(pathname: strTarget, nullptr);
324 }
325 *p = '\\'; lp = p + 1;
326 }
327
328 // try to open it
329 bool targetMissing{false};
330 if (!TargetGrp.Open(szGroupName: strTarget, fCreate: !GrpUpdate))
331 {
332 if (AllowMissingTarget && !ItemExists(szItemName: strTarget) && TargetGrp.Open(szGroupName: strTarget, fCreate: true))
333 {
334 targetMissing = true;
335 }
336 else
337 {
338 return false;
339 }
340 }
341
342 // check if the update is allowed
343 if (GrpUpdate)
344 {
345 if (!targetMissing)
346 {
347 // check checksum
348 uint32_t iContentsCRC32;
349 if (!C4Group_GetFileContentsCRC(szFilename: TargetGrp.GetFullName().getData(), pCRC32: &iContentsCRC32))
350 return false;
351 int i = 0;
352 for (; i < UpGrpCnt; i++)
353 if (GrpContentsCRC1[i] && iContentsCRC32 == GrpContentsCRC1[i])
354 break;
355 if (i >= UpGrpCnt)
356 {
357 uint32_t iCRC32;
358 if (!C4Group_GetFileCRC(szFilename: TargetGrp.GetFullName().getData(), pCRC32: &iCRC32))
359 return false;
360 int i = 0;
361 for (; i < UpGrpCnt; i++)
362 if (iCRC32 == GrpChks1[i])
363 break;
364 if (i >= UpGrpCnt)
365 return false;
366 }
367 }
368 }
369 else
370 {
371 // only allow Extra.c4g-Updates
372 if (!SEqual2(szStr1: DestPath, szStr2: "Extra.c4g"))
373 return false;
374 }
375
376 // update children
377 char ItemFileName[_MAX_PATH];
378 pGroup->ResetSearch();
379 while (pGroup->FindNextEntry(szWildCard: "*", sFileName: ItemFileName))
380 if (!SEqual(szStr1: ItemFileName, C4CFN_UpdateCore) && !SEqual(szStr1: ItemFileName, C4CFN_UpdateEntries))
381 DoUpdate(pGrpFrom: pGroup, pGrpTo: &TargetGrp, strFileName: ItemFileName);
382
383 // do GrpUpdate
384 if (GrpUpdate)
385 DoGrpUpdate(pUpdateData: pGroup, pGrpTo: &TargetGrp);
386
387 // close the group
388 TargetGrp.Close(fHeaderUpdate: false);
389
390 if (GrpUpdate)
391 {
392 // check the result
393 uint32_t iResChks, iResContentsChks;
394 if (!C4Group_GetFileCRC(szFilename: strTarget, pCRC32: &iResChks))
395 return false;
396 if (!C4Group_GetFileContentsCRC(szFilename: strTarget, pCRC32: &iResContentsChks))
397 return false;
398 if ((!GrpContentsCRC2 || GrpContentsCRC2 != iResContentsChks) && iResChks != GrpChks2)
399 {
400 return false;
401 }
402 }
403
404 return true;
405}
406
407bool C4UpdatePackage::Optimize(C4Group *pGroup, const char *strTarget)
408{
409 // Open target group
410 C4GroupEx TargetGrp;
411 if (!TargetGrp.Open(szGroupName: strTarget))
412 return false;
413
414 // Both groups must be packed
415 if (!pGroup->IsPacked() || !TargetGrp.IsPacked())
416 {
417 TargetGrp.Close(fHeaderUpdate: false);
418 return false;
419 }
420
421 // update children
422 char ItemFileName[_MAX_PATH];
423 pGroup->ResetSearch();
424 while (pGroup->FindNextEntry(szWildCard: "*", sFileName: ItemFileName))
425 if (!SEqual(szStr1: ItemFileName, C4CFN_UpdateCore) && !SEqual(szStr1: ItemFileName, C4CFN_UpdateEntries))
426 Optimize(pGrpFrom: pGroup, pGrpTo: &TargetGrp, strFileName: ItemFileName);
427
428 // set header
429 if (TargetGrp.HeadIdentical(rByGrp&: *pGroup, fLax: true))
430 TargetGrp.SetHead(*pGroup);
431
432 // save
433 TargetGrp.Close(fHeaderUpdate: false);
434
435 // okay
436 return true;
437}
438
439C4UpdatePackage::CheckResult C4UpdatePackage::Check(C4Group *pGroup)
440{
441 // Version requirement is set
442 if (RequireVersion[0])
443 {
444 // Engine and game version must match (rest ignored)
445 if ((C4XVER1 != RequireVersion[0]) || (C4XVER2 != RequireVersion[1]))
446 return CheckResult::BadSource;
447 }
448
449 // only group updates have any special needs
450 if (!GrpUpdate) return CheckResult::Ok;
451
452 // check source file
453 C4Group TargetGrp;
454 if (!TargetGrp.Open(szGroupName: DestPath))
455 {
456 // make sure corrupted target files aren't overwritten
457 if (AllowMissingTarget && !ItemExists(szItemName: DestPath))
458 {
459 return CheckResult::Ok;
460 }
461 else
462 {
463 return CheckResult::NoSource;
464 }
465 }
466 if (!TargetGrp.IsPacked())
467 return CheckResult::BadSource;
468 TargetGrp.Close();
469
470 // check source crc
471 uint32_t iCRC32, iContentsCRC32;
472 if (!C4Group_GetFileContentsCRC(szFilename: DestPath, pCRC32: &iContentsCRC32))
473 return CheckResult::BadSource;
474
475 if (GrpContentsCRC2 && GrpContentsCRC2 == iContentsCRC32)
476 // so there's nothing to do
477 return CheckResult::AlreadyUpdated;
478
479 int i = 0;
480 for (; i < UpGrpCnt; i++)
481 if (GrpContentsCRC1[i] && iContentsCRC32 == GrpContentsCRC1[i])
482 break;
483
484 if (i >= UpGrpCnt)
485 {
486 if (!C4Group_GetFileCRC(szFilename: DestPath, pCRC32: &iCRC32))
487 return CheckResult::BadSource;
488 // equal to destination group?
489 if (iCRC32 == GrpChks2)
490 // so there's nothing to do
491 return CheckResult::AlreadyUpdated;
492 // check if it's one of our registered sources
493 int i = 0;
494 for (; i < UpGrpCnt; i++)
495 if (iCRC32 == GrpChks1[i])
496 break;
497 if (i >= UpGrpCnt)
498 return CheckResult::BadSource;
499 }
500
501 // ok
502 return CheckResult::Ok;
503}
504
505bool C4UpdatePackage::DoUpdate(C4Group *pGrpFrom, C4GroupEx *pGrpTo, const char *strFileName)
506{
507 // group file?
508 C4Group ItemGroupFrom;
509 if (ItemGroupFrom.OpenAsChild(pMother: pGrpFrom, szEntryName: strFileName))
510 {
511 // try to open target group
512 C4GroupEx ItemGroupTo;
513 char strTempGroup[_MAX_PATH + 1]; strTempGroup[0] = 0;
514 if (!ItemGroupTo.OpenAsChild(pMother: pGrpTo, szEntryName: strFileName))
515 {
516 // create (emtpy) temp dir
517 SCopy(szSource: GetCfg()->AtExePath(szFilename: "~tmp"), sTarget: strTempGroup, _MAX_PATH);
518 MakeTempFilename(szFileName: strTempGroup);
519 // open/create it
520 if (!ItemGroupTo.Open(szGroupName: strTempGroup, fCreate: true))
521 return false;
522 }
523 // update children
524 char ItemFileName[_MAX_PATH];
525 ItemGroupFrom.ResetSearch();
526 while (ItemGroupFrom.FindNextEntry(szWildCard: "*", sFileName: ItemFileName))
527 if (!SEqual(szStr1: ItemFileName, C4CFN_UpdateCore) && !SEqual(szStr1: ItemFileName, C4CFN_UpdateEntries))
528 DoUpdate(pGrpFrom: &ItemGroupFrom, pGrpTo: &ItemGroupTo, strFileName: ItemFileName);
529 // set maker (always)
530 ItemGroupTo.SetMaker(ItemGroupFrom.GetMaker());
531 if (GrpUpdate)
532 {
533 DoGrpUpdate(pUpdateData: &ItemGroupFrom, pGrpTo: &ItemGroupTo);
534 // write group (do not change any headers set by DoGrpUpdate!)
535 ItemGroupTo.Close(fHeaderUpdate: false);
536 // temporary group?
537 if (strTempGroup[0])
538 if (!pGrpTo->Move(szFile: strTempGroup, szAddAs: strFileName))
539 return false;
540 // set core (C4Group::Save overwrites it)
541 pGrpTo->SaveEntryCore(rByGrp&: *pGrpFrom, szEntry: strFileName);
542 pGrpTo->SetSavedEntryCore(strFileName);
543 // flag as no-resort
544 pGrpTo->SetNoSort(strFileName);
545 }
546 else
547 {
548 // write group
549 ItemGroupTo.Close(fHeaderUpdate: true);
550 // temporary group?
551 if (strTempGroup[0])
552 if (!pGrpTo->Move(szFile: strTempGroup, szAddAs: strFileName))
553 return false;
554 }
555 }
556 else
557 {
558 const std::string msg{std::format(fmt: "updating {}\\{}\n", args: pGrpTo->GetFullName().getData(), args&: strFileName)};
559#ifdef _MSC_VER
560 OutputDebugStringA(msg.c_str());
561#elif !defined(NDEBUG)
562 std::print("{}", msg);
563#endif
564 if (!C4Group_CopyEntry(pFrom: pGrpFrom, pTo: pGrpTo, strItemName: strFileName))
565 return false;
566 // set core
567 pGrpTo->SaveEntryCore(rByGrp&: *pGrpFrom, szEntry: strFileName);
568 pGrpTo->SetSavedEntryCore(strFileName);
569 }
570 // ok
571 return true;
572}
573
574bool C4UpdatePackage::DoGrpUpdate(C4Group *pUpdateData, C4GroupEx *pGrpTo)
575{
576 char *pData;
577 // sort entries
578 if (pUpdateData->LoadEntry(C4CFN_UpdateEntries, lpbpBuf: &pData, ipSize: nullptr, iAppendZeros: 1))
579 {
580 // delete all entries that do not appear in the entries list
581 char strItemName[_MAX_FNAME + 1], strItemName2[_MAX_FNAME + 1];
582 pGrpTo->ResetSearch();
583 while (pGrpTo->FindNextEntry(szWildCard: "*", sFileName: strItemName))
584 {
585 bool fGotIt = false;
586 for (int i = 0; fGotIt = SCopySegment(fstr: pData, segn: i, tstr: strItemName2, sepa: '|', _MAX_FNAME); i++)
587 {
588 // remove separator
589 char *pSep = strchr(s: strItemName2, c: '=');
590 if (pSep) *pSep = '\0';
591 // in list?
592 if (SEqual(szStr1: strItemName, szStr2: strItemName2))
593 break;
594 }
595 if (!fGotIt)
596 pGrpTo->DeleteEntry(szFilename: strItemName);
597 }
598 // set entry times, set sort list
599 char strSortList[32767] = "";
600 for (int i = 0; SCopySegment(fstr: pData, segn: i, tstr: strItemName, sepa: '|', _MAX_FNAME); i++)
601 {
602 // get time (if given)
603 char *pTime = strchr(s: strItemName, c: '=');
604 if (pTime) *pTime++ = '\0';
605 // set
606 if (pTime) pGrpTo->SetEntryTime(szEntry: strItemName, iEntryTime: atoi(nptr: pTime));
607 // update EntryCRC32. This will make updates to old groups invalid
608 // however, it's needed so updates will update the EntryCRC of *unchanged* files correctly
609 pGrpTo->EntryCRC32(szWildCard: strItemName);
610 // copy to sort list
611 SAppend(szSource: strItemName, szTarget: strSortList);
612 SAppendChar(cChar: '|', szStr: strSortList);
613 }
614 // sort by list
615 pGrpTo->Sort(szSortList: strSortList);
616 delete[] pData;
617 }
618 // copy header from update group
619 pGrpTo->SetHead(*pUpdateData);
620 // ok
621 return true;
622}
623
624bool C4UpdatePackage::Optimize(C4Group *pGrpFrom, C4GroupEx *pGrpTo, const char *strFileName)
625{
626 // group file?
627 C4Group ItemGroupFrom;
628 if (!ItemGroupFrom.OpenAsChild(pMother: pGrpFrom, szEntryName: strFileName))
629 return true;
630 // try to open target group
631 C4GroupEx ItemGroupTo;
632 char strTempGroup[_MAX_PATH + 1]; strTempGroup[0] = 0;
633 if (!ItemGroupTo.OpenAsChild(pMother: pGrpTo, szEntryName: strFileName))
634 return true;
635 // update children
636 char ItemFileName[_MAX_PATH];
637 ItemGroupFrom.ResetSearch();
638 while (ItemGroupFrom.FindNextEntry(szWildCard: "*", sFileName: ItemFileName))
639 Optimize(pGrpFrom: &ItemGroupFrom, pGrpTo: &ItemGroupTo, strFileName: ItemFileName);
640 // set head
641 if (ItemGroupTo.HeadIdentical(rByGrp&: ItemGroupFrom, fLax: true))
642 ItemGroupTo.SetHead(ItemGroupFrom);
643 // write group (do not change any headers set by DoGrpUpdate!)
644 ItemGroupTo.Close(fHeaderUpdate: false);
645 // set core (C4Group::Save overwrites it)
646 pGrpTo->SaveEntryCore(rByGrp&: *pGrpFrom, szEntry: strFileName);
647 pGrpTo->SetSavedEntryCore(strFileName);
648 return true;
649}
650
651bool C4UpdatePackage::MakeUpdate(const char *strFile1, const char *strFile2, const char *strUpdateFile, const char *strName, const bool allowMissingTarget)
652{
653 // open Log
654 if (!Log.Create(szFileName: "Update.log"))
655 return false;
656
657 // begin message
658 WriteLog(fmt: "Source: {}\nTarget: {}\nOutput: {}\n\n", args&: strFile1, args&: strFile2, args&: strUpdateFile);
659
660 // open both groups
661 C4Group Group1, Group2;
662 if (!Group1.Open(szGroupName: strFile1)) { WriteLog(fmt: "Error: could not open {}!\n", args&: strFile1); return false; }
663 if (!Group2.Open(szGroupName: strFile2)) { WriteLog(fmt: "Error: could not open {}!\n", args&: strFile2); return false; }
664
665 // All groups to be compared need to be packed
666 if (!Group1.IsPacked()) { WriteLog(fmt: "Error: source group {} not packed!\n", args&: strFile1); return false; }
667 if (!Group2.IsPacked()) { WriteLog(fmt: "Error: target group {} not packed!\n", args&: strFile2); return false; }
668 if (Group1.HasPackedMother()) { WriteLog(fmt: "Error: source group {} must not have a packed mother group!\n", args&: strFile1); return false; }
669 if (Group2.HasPackedMother()) { WriteLog(fmt: "Error: target group {} must not have a packed mother group!\n", args&: strFile2); return false; }
670
671 // create/open update-group
672 C4GroupEx UpGroup;
673 if (!UpGroup.Open(szGroupName: strUpdateFile, fCreate: true)) { WriteLog(fmt: "Error: could not open {}!\n", args&: strUpdateFile); return false; }
674
675 // may be continued update-file -> try to load core
676 UpGrpCnt = 0;
677 bool fContinued = C4UpdatePackageCore::Load(hGroup&: UpGroup);
678
679 // save crc2 for later check
680 unsigned int iOldChks2 = GrpChks2;
681 unsigned int iOldContentsChks2 = GrpContentsCRC2;
682
683 // create core info
684 if (strName)
685 SCopy(szSource: strName, sTarget: Name, iMaxL: C4MaxName);
686 else
687 FormatWithNull(buf&: Name, fmt: "{} Update", args: GetFilename(path: strFile1));
688 SCopy(szSource: strFile1, sTarget: DestPath, _MAX_PATH);
689 GrpUpdate = true;
690 if (!C4Group_GetFileCRC(szFilename: strFile1, pCRC32: &GrpChks1[UpGrpCnt]))
691 {
692 WriteLog(fmt: "Error: could not calc checksum for {}!\n", args&: strFile1); return false;
693 }
694 if (!C4Group_GetFileCRC(szFilename: strFile2, pCRC32: &GrpChks2))
695 {
696 WriteLog(fmt: "Error: could not calc checksum for {}!\n", args&: strFile2); return false;
697 }
698 if (!C4Group_GetFileContentsCRC(szFilename: strFile1, pCRC32: &GrpContentsCRC1[UpGrpCnt]))
699 {
700 WriteLog(fmt: "Error: could not calc contents checksum for {}!\n", args&: strFile1); return false;
701 }
702 if (!C4Group_GetFileContentsCRC(szFilename: strFile2, pCRC32: &GrpContentsCRC2))
703 {
704 WriteLog(fmt: "Error: could not calc contents checksum for {}!\n", args&: strFile2); return false;
705 }
706 if (fContinued)
707 {
708 // continuation check: GrpChks2 matches?
709 if (iOldContentsChks2 ? GrpContentsCRC2 != iOldContentsChks2 : GrpChks2 != iOldChks2)
710 // that would mess up the update result...
711 {
712 WriteLog(fmt: "Error: could not add to update package - target groups don't match (checksum error)\n"); return false;
713 }
714 // already supported by this update?
715 int i = 0;
716 for (; i < UpGrpCnt; i++)
717 if (GrpChks1[UpGrpCnt] == GrpChks1[i] || (GrpContentsCRC1[i] != 0 && GrpContentsCRC1[UpGrpCnt] != 0 && GrpContentsCRC1[i] == GrpContentsCRC1[UpGrpCnt]))
718 break;
719 if (i < UpGrpCnt)
720 {
721 WriteLog(fmt: "This update already supports the version of the source file.\n"); return false;
722 }
723 }
724
725 UpGrpCnt++;
726 AllowMissingTarget = allowMissingTarget;
727
728 // save core
729 if (!C4UpdatePackageCore::Save(hGroup&: UpGroup))
730 {
731 WriteLog(fmt: "Could not save update package core!\n"); return false;
732 }
733
734 // compare groups, create update
735 bool includeInUpdate{false};
736 bool fSuccess = MkUp(pGrp1: &Group1, pGrp2: &Group2, pUpGr: &UpGroup, includeInUpdate);
737 // close (save) it
738 UpGroup.Close(fHeaderUpdate: false);
739 // error?
740 if (!fSuccess)
741 {
742 WriteLog(fmt: "Update package not created.\n");
743 remove(filename: strUpdateFile);
744 return false;
745 }
746
747 WriteLog(fmt: "Update package created.\n");
748 return true;
749}
750
751bool C4UpdatePackage::MkUp(C4Group *pGrp1, C4Group *pGrp2, C4GroupEx *pUpGrp, bool &includeInUpdate)
752{
753 // (CAUTION: pGrp1 may be nullptr - that means that there is no counterpart for Grp2
754 // in the base group)
755
756 // compare headers
757 if (!pGrp1 ||
758 pGrp1->GetCreation() != pGrp2->GetCreation() ||
759 pGrp1->GetOriginal() != pGrp2->GetOriginal() ||
760 !SEqual(szStr1: pGrp1->GetMaker(), szStr2: pGrp2->GetMaker()) ||
761 !SEqual(szStr1: pGrp1->GetPassword(), szStr2: pGrp2->GetPassword()))
762 includeInUpdate = true;
763 // set header
764 pUpGrp->SetHead(*pGrp2);
765 // compare entries
766 char strItemName[_MAX_PATH], strItemName2[_MAX_PATH];
767 std::string entryList;
768 strItemName[0] = strItemName2[0] = 0;
769 pGrp2->ResetSearch(); if (!includeInUpdate) pGrp1->ResetSearch();
770 int iChangedEntries = 0;
771 while (pGrp2->FindNextEntry(szWildCard: "*", sFileName: strItemName, iSize: nullptr, fChild: nullptr, fStartAtFilename: !!strItemName[0]))
772 {
773 // add to entry list
774 if (!entryList.empty()) entryList += '|';
775 entryList += std::format(fmt: "{}={}", args&: strItemName, args: pGrp2->EntryTime(szFilename: strItemName));
776 // no modification detected yet? then check order
777 if (!AllowMissingTarget && !includeInUpdate)
778 {
779 if (!pGrp1->FindNextEntry(szWildCard: "*", sFileName: strItemName2, iSize: nullptr, fChild: nullptr, fStartAtFilename: !!strItemName2[0]))
780 includeInUpdate = true;
781 else if (!SEqual(szStr1: strItemName, szStr2: strItemName2))
782 includeInUpdate = true;
783 }
784
785 // TODO: write DeleteEntries.txt
786
787 // a child group?
788 C4GroupEx ChildGrp2;
789 if (ChildGrp2.OpenAsChild(pMother: pGrp2, szEntryName: strItemName))
790 {
791 // open in Grp1
792 C4Group *pChildGrp1 = new C4GroupEx();
793 if (!pGrp1 || !pChildGrp1->OpenAsChild(pMother: pGrp1, szEntryName: strItemName))
794 {
795 delete pChildGrp1; pChildGrp1 = nullptr;
796 }
797 // open group for update data
798 C4GroupEx UpdGroup; char strTempGroupName[_MAX_FNAME + 1];
799 strTempGroupName[0] = 0;
800 if (!UpdGroup.OpenAsChild(pMother: pUpGrp, szEntryName: strItemName))
801 {
802 // create new group (may be temporary)
803 SCopy(szSource: GetCfg()->AtTempPath(szFilename: "~upd"), sTarget: strTempGroupName, _MAX_FNAME);
804 MakeTempFilename(szFileName: strTempGroupName);
805 if (!UpdGroup.Open(szGroupName: strTempGroupName, fCreate: true)) { delete pChildGrp1; WriteLog(fmt: "Error: could not create temp group\n"); return false; }
806 }
807 // do nested MkUp-search
808 bool childIncludeInUpdate{false};
809 bool fSuccess = MkUp(pGrp1: pChildGrp1, pGrp2: &ChildGrp2, pUpGrp: &UpdGroup, includeInUpdate&: childIncludeInUpdate);
810 // sort & close
811 extern const char **C4Group_SortList;
812 UpdGroup.SortByList(ppSortList: C4Group_SortList, szFilename: ChildGrp2.GetName());
813 UpdGroup.Close(fHeaderUpdate: false);
814 // always add the entire group if missing targets are allowed
815 // otherwise check entry times
816 if (AllowMissingTarget || !pGrp1 || (pGrp1->EntryTime(szFilename: strItemName) != pGrp2->EntryTime(szFilename: strItemName)))
817 childIncludeInUpdate = true;
818 // add group (if modified)
819 if (fSuccess && childIncludeInUpdate)
820 {
821 if (strTempGroupName[0])
822 if (!pUpGrp->Move(szFile: strTempGroupName, szAddAs: strItemName))
823 {
824 WriteLog(fmt: "Error: could not add modified group\n");
825 return false;
826 }
827 // copy core
828 pUpGrp->SaveEntryCore(rByGrp&: *pGrp2, szEntry: strItemName);
829 pUpGrp->SetSavedEntryCore(strItemName);
830 // got a modification in a subgroup
831 includeInUpdate = true;
832 iChangedEntries++;
833 }
834 else
835 // delete group (do not remove groups that existed before!)
836 if (strTempGroupName[0])
837 if (remove(filename: strTempGroupName))
838 if (rmdir(path: strTempGroupName))
839 {
840 WriteLog(fmt: "Error: could not delete temporary directory\n"); return false;
841 }
842 delete pChildGrp1;
843 }
844 else
845 {
846 // compare them (size & crc32)
847 if (AllowMissingTarget ||
848 !pGrp1 ||
849 pGrp1->EntrySize(szWildCard: strItemName) != pGrp2->EntrySize(szWildCard: strItemName) ||
850 pGrp1->EntryCRC32(szWildCard: strItemName) != pGrp2->EntryCRC32(szWildCard: strItemName))
851 {
852 bool fCopied = false;
853
854 // save core (EntryCRC32 might set additional fields)
855 pUpGrp->SaveEntryCore(rByGrp&: *pGrp2, szEntry: strItemName);
856
857 // already in update grp?
858 if (AllowMissingTarget ||
859 pUpGrp->EntryTime(szFilename: strItemName) != pGrp2->EntryTime(szFilename: strItemName) ||
860 pUpGrp->EntrySize(szWildCard: strItemName) != pGrp2->EntrySize(szWildCard: strItemName) ||
861 pUpGrp->EntryCRC32(szWildCard: strItemName) != pGrp2->EntryCRC32(szWildCard: strItemName))
862 {
863 // copy it
864 if (!C4Group_CopyEntry(pFrom: pGrp2, pTo: pUpGrp, strItemName))
865 {
866 WriteLog(fmt: "Error: could not add changed entry to update group\n");
867 return false;
868 }
869 // set entry core
870 pUpGrp->SetSavedEntryCore(strItemName);
871 // modified...
872 includeInUpdate = true;
873 fCopied = true;
874 }
875 iChangedEntries++;
876
877 WriteLog(fmt: "{}\\{}: update{}\n", args: pGrp2->GetFullName().getData(), args&: strItemName, args: fCopied ? "" : " (already in group)");
878 }
879 }
880 }
881 // write entries list (always)
882 StdStrBuf buf{entryList.c_str(), entryList.size()};
883 if (!pUpGrp->Add(C4CFN_UpdateEntries, pBuffer&: buf, fChild: false, fHoldBuffer: true))
884 {
885 WriteLog(fmt: "Error: could not save entry list!");
886 return false;
887 }
888
889 if (iChangedEntries > 0)
890 WriteLog(fmt: "{}: {}/{} changed ({})\n", args: pGrp2->GetFullName().getData(), args&: iChangedEntries, args: pGrp2->EntryCount(), args: includeInUpdate ? "update" : "skip");
891
892 // success
893 return true;
894}
895