1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (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/* Handles group files */
18
19/* Needs to be compiles as Objective C++ on OS X */
20
21#include <C4Group.h>
22
23#include <C4Components.h>
24#include <C4InputValidation.h>
25#include "StdConfig.h"
26
27#ifdef C4ENGINE
28#include "C4Log.h"
29#endif
30
31#ifdef _WIN32
32#include <sys/utime.h>
33#include <shellapi.h>
34#else
35#include <sys/types.h>
36#include <sys/stat.h>
37#include <utime.h>
38#include <fcntl.h>
39#include <unistd.h>
40#endif
41
42#include <StdSha1.h>
43#include <fcntl.h>
44
45#include <cstring>
46#include <print>
47
48// File Sort Lists
49
50const char *C4CFN_FLS[] =
51{
52 C4CFN_System, C4FLS_System,
53 C4CFN_Mouse, C4FLS_Mouse,
54 C4CFN_Keyboard, C4FLS_Keyboard,
55 C4CFN_Easy, C4FLS_Easy,
56 C4CFN_Material, C4FLS_Material,
57 C4CFN_Graphics, C4FLS_Graphics,
58 "Western.c4f", C4FLS_Western, // hardcoded stuff for foldermap
59 C4CFN_DefFiles, C4FLS_Def,
60 C4CFN_PlayerFiles, C4FLS_Player,
61 C4CFN_ObjectInfoFiles, C4FLS_Object,
62 C4CFN_ScenarioFiles, C4FLS_Scenario,
63 C4CFN_FolderFiles, C4FLS_Folder,
64 C4CFN_ScenarioSections, C4FLS_Section,
65 C4CFN_Music, C4FLS_Music,
66 nullptr, nullptr
67};
68
69#ifndef NDEBUG
70char *szCurrAccessedEntry = nullptr;
71int iC4GroupRewindFilePtrNoWarn = 0;
72#endif
73
74#ifdef C4ENGINE
75#ifndef NDEBUG
76//#define C4GROUP_DUMP_ACCESS
77#endif
78#endif
79
80// Global C4Group_Functions
81
82char C4Group_Maker[C4GroupMaxMaker + 1] = "";
83char C4Group_Passwords[CFG_MaxString + 1] = "";
84char C4Group_TempPath[_MAX_PATH + 1] = "";
85char C4Group_Ignore[_MAX_PATH + 1] = "cvs;Thumbs.db";
86const char **C4Group_SortList = nullptr;
87time_t C4Group_AssumeTimeOffset = 0;
88bool(*C4Group_ProcessCallback)(const char *, int) = nullptr;
89
90void C4Group_SetProcessCallback(bool(*fnCallback)(const char *, int))
91{
92 C4Group_ProcessCallback = fnCallback;
93}
94
95void C4Group_SetSortList(const char **ppSortList)
96{
97 C4Group_SortList = ppSortList;
98}
99
100void C4Group_SetMaker(const char *szMaker)
101{
102 if (!szMaker) C4Group_Maker[0] = 0;
103 else SCopy(szSource: szMaker, sTarget: C4Group_Maker, iMaxL: C4GroupMaxMaker);
104}
105
106void C4Group_SetTempPath(const char *szPath)
107{
108 if (!szPath || !szPath[0]) C4Group_TempPath[0] = 0;
109 else { SCopy(szSource: szPath, sTarget: C4Group_TempPath, _MAX_PATH); AppendBackslash(szFileName: C4Group_TempPath); }
110}
111
112const char *C4Group_GetTempPath()
113{
114 return C4Group_TempPath;
115}
116
117bool C4Group_TestIgnore(const char *szFilename)
118{
119 const auto fileName = GetFilename(path: szFilename);
120 return !SEqual(szStr1: fileName, szStr2: ".legacyclonk") && (*fileName == '.' || SIsModule(szList: C4Group_Ignore, szString: GetFilename(path: szFilename)));
121}
122
123bool C4Group_IsGroup(const char *szFilename)
124{
125 C4Group hGroup; if (hGroup.Open(szGroupName: szFilename)) { hGroup.Close(); return true; }
126 return false;
127}
128
129bool C4Group_CopyItem(const char *szSource, const char *szTarget1, bool fNoSort, bool fResetAttributes)
130{
131 // Parameter check
132 char szTarget[_MAX_PATH + 1]; SCopy(szSource: szTarget1, sTarget: szTarget, _MAX_PATH);
133 if (!szSource || !szSource[0] || !szTarget[0]) return false;
134
135 // Backslash terminator indicates target is a path only (append filename)
136 if (szTarget[SLen(sptr: szTarget) - 1] == DirectorySeparator) SAppend(szSource: GetFilename(path: szSource), szTarget);
137
138 // Check for identical source and target
139 // Note that attributes aren't reset here
140 if (ItemIdentical(szFilename1: szSource, szFilename2: szTarget)) return true;
141
142 // Source and target are simple items
143 if (ItemExists(szItemName: szSource) && CreateItem(szItemname: szTarget)) return CopyItem(szSource, szTarget, fResetAttributes);
144
145 // For items within groups, attribute resetting isn't needed, because packing/unpacking will kill all
146 // attributes anyway
147
148 // Source & target
149 C4Group hSourceParent, hTargetParent;
150 char szSourceParentPath[_MAX_PATH + 1], szTargetParentPath[_MAX_PATH + 1];
151 GetParentPath(szFilename: szSource, szBuffer: szSourceParentPath); GetParentPath(szFilename: szTarget, szBuffer: szTargetParentPath);
152
153 // Temp filename
154 char szTempFilename[_MAX_PATH + 1];
155 SCopy(szSource: C4Group_TempPath, sTarget: szTempFilename, _MAX_PATH);
156 SAppend(szSource: GetFilename(path: szSource), szTarget: szTempFilename);
157 MakeTempFilename(szFileName: szTempFilename);
158
159 // Extract source to temp file
160 if (!hSourceParent.Open(szGroupName: szSourceParentPath)
161 || !hSourceParent.Extract(szFiles: GetFilename(path: szSource), szExtractTo: szTempFilename)
162 || !hSourceParent.Close()) return false;
163
164 // Move temp file to target
165 if (!hTargetParent.Open(szGroupName: szTargetParentPath)
166 || !hTargetParent.SetNoSort(fNoSort)
167 || !hTargetParent.Move(szFile: szTempFilename, szAddAs: GetFilename(path: szTarget))
168 || !hTargetParent.Close())
169 {
170 EraseItem(szItemName: szTempFilename); return false;
171 }
172
173 return true;
174}
175
176bool C4Group_MoveItem(const char *szSource, const char *szTarget1, bool fNoSort)
177{
178 // Parameter check
179 char szTarget[_MAX_PATH + 1]; SCopy(szSource: szTarget1, sTarget: szTarget, _MAX_PATH);
180 if (!szSource || !szSource[0] || !szTarget[0]) return false;
181
182 // Backslash terminator indicates target is a path only (append filename)
183 if (szTarget[SLen(sptr: szTarget) - 1] == DirectorySeparator) SAppend(szSource: GetFilename(path: szSource), szTarget);
184
185 // Check for identical source and target
186 if (ItemIdentical(szFilename1: szSource, szFilename2: szTarget)) return true;
187
188 // Source and target are simple items
189 if (ItemExists(szItemName: szSource) && CreateItem(szItemname: szTarget))
190 {
191 // erase test file, because it may block moving a directory
192 EraseItem(szItemName: szTarget);
193 return MoveItem(szSource, szTarget);
194 }
195
196 // Source & target
197 C4Group hSourceParent, hTargetParent;
198 char szSourceParentPath[_MAX_PATH + 1], szTargetParentPath[_MAX_PATH + 1];
199 GetParentPath(szFilename: szSource, szBuffer: szSourceParentPath); GetParentPath(szFilename: szTarget, szBuffer: szTargetParentPath);
200
201 // Temp filename
202 char szTempFilename[_MAX_PATH + 1];
203 SCopy(szSource: C4Group_TempPath, sTarget: szTempFilename, _MAX_PATH);
204 SAppend(szSource: GetFilename(path: szSource), szTarget: szTempFilename);
205 MakeTempFilename(szFileName: szTempFilename);
206
207 // Extract source to temp file
208 if (!hSourceParent.Open(szGroupName: szSourceParentPath)
209 || !hSourceParent.Extract(szFiles: GetFilename(path: szSource), szExtractTo: szTempFilename)
210 || !hSourceParent.Close()) return false;
211
212 // Move temp file to target
213 if (!hTargetParent.Open(szGroupName: szTargetParentPath)
214 || !hTargetParent.SetNoSort(fNoSort)
215 || !hTargetParent.Move(szFile: szTempFilename, szAddAs: GetFilename(path: szTarget))
216 || !hTargetParent.Close())
217 {
218 EraseItem(szItemName: szTempFilename); return false;
219 }
220
221 // Delete original file
222 if (!hSourceParent.Open(szGroupName: szSourceParentPath)
223 || !hSourceParent.DeleteEntry(szFilename: GetFilename(path: szSource))
224 || !hSourceParent.Close()) return false;
225
226 return true;
227}
228
229bool C4Group_DeleteItem(const char *szItem, bool fRecycle)
230{
231 // Parameter check
232 if (!szItem || !szItem[0]) return false;
233
234 // simple item?
235 if (ItemExists(szItemName: szItem))
236 {
237 if (fRecycle)
238 return EraseItemSafe(szFilename: szItem);
239 else
240 return EraseItem(szItemName: szItem);
241 }
242
243 // delete from parent
244 C4Group hParent;
245 char szParentPath[_MAX_PATH + 1];
246 GetParentPath(szFilename: szItem, szBuffer: szParentPath);
247
248 // Delete original file
249 if (!hParent.Open(szGroupName: szParentPath)
250 || !hParent.DeleteEntry(szFilename: GetFilename(path: szItem), fRecycle)
251 || !hParent.Close()) return false;
252
253 return true;
254}
255
256bool C4Group_PackDirectoryTo(const char *szFilename, const char *szFilenameTo, const bool overwriteTarget)
257{
258 // Check file type
259 if (!DirectoryExists(szFileName: szFilename)) return false;
260 // Target mustn't exist
261 if (!overwriteTarget && FileExists(szFileName: szFilenameTo)) return false;
262 // Ignore
263 if (C4Group_TestIgnore(szFilename))
264 return true;
265 // Process message
266 if (C4Group_ProcessCallback)
267 C4Group_ProcessCallback(szFilename, 0);
268 // Create group file
269 C4Group hGroup;
270 if (!hGroup.Open(szGroupName: szFilenameTo, fCreate: true, flags: overwriteTarget ? C4Group::OpenFlags::Overwrite : C4Group::OpenFlags::None))
271 return false;
272 // Add folder contents to group
273 DirectoryIterator i(szFilename);
274 for (; *i; i++)
275 {
276 // Ignore
277 if (C4Group_TestIgnore(szFilename: *i))
278 continue;
279 // Must pack?
280 if (DirectoryExists(szFileName: *i))
281 {
282 // Find temporary filename
283 char szTempFilename[_MAX_PATH + 1];
284 // At C4Group temp path
285 SCopy(szSource: C4Group_TempPath, sTarget: szTempFilename, _MAX_PATH);
286 SAppend(szSource: GetFilename(path: *i), szTarget: szTempFilename, _MAX_PATH);
287 // Make temporary filename
288 MakeTempFilename(szFileName: szTempFilename);
289 // Pack and move into group
290 if (!C4Group_PackDirectoryTo(szFilename: *i, szFilenameTo: szTempFilename)) break;
291 if (!hGroup.Move(szFile: szTempFilename, szAddAs: GetFilename(path: *i)))
292 {
293 EraseFile(szFileName: szTempFilename);
294 break;
295 }
296 }
297 // Add normally otherwise
298 else if (!hGroup.Add(szFile: *i, szAddAs: nullptr))
299 break;
300 }
301 // Something went wrong?
302 if (*i)
303 {
304 // Close group and remove temporary file
305 hGroup.Close();
306 EraseItem(szItemName: szFilenameTo);
307 return false;
308 }
309 // Reset iterator
310 i.Reset();
311 // Close group
312 hGroup.SortByList(ppSortList: C4Group_SortList, szFilename);
313 if (!hGroup.Close())
314 return false;
315 // Done
316 return true;
317}
318
319bool C4Group_PackDirectory(const char *szFilename)
320{
321 // Make temporary filename
322 char szTempFilename[_MAX_PATH + 1];
323 SCopy(szSource: szFilename, sTarget: szTempFilename, _MAX_PATH);
324 MakeTempFilename(szFileName: szTempFilename);
325 // Pack directory
326 if (!C4Group_PackDirectoryTo(szFilename, szFilenameTo: szTempFilename))
327 return false;
328 // Rename folder
329 char szTempFilename2[_MAX_PATH + 1];
330 SCopy(szSource: szFilename, sTarget: szTempFilename2, _MAX_PATH);
331 MakeTempFilename(szFileName: szTempFilename2);
332 if (!RenameFile(szFileName: szFilename, szNewFileName: szTempFilename2))
333 return false;
334 // Name group file
335 if (!RenameFile(szFileName: szTempFilename, szNewFileName: szFilename))
336 return false;
337 // Last: Delete folder
338 return EraseDirectory(szDirName: szTempFilename2);
339}
340
341bool C4Group_UnpackDirectory(const char *szFilename)
342{
343 // Already unpacked: success
344 if (DirectoryExists(szFileName: szFilename)) return true;
345
346 // Not a real file: unpack parent directory first
347 char szParentFilename[_MAX_PATH + 1];
348 if (!FileExists(szFileName: szFilename))
349 if (GetParentPath(szFilename, szBuffer: szParentFilename))
350 if (!C4Group_UnpackDirectory(szFilename: szParentFilename))
351 return false;
352
353 // Open group
354 C4Group hGroup;
355 if (!hGroup.Open(szGroupName: szFilename)) return false;
356
357 // Process message
358 if (C4Group_ProcessCallback)
359 C4Group_ProcessCallback(szFilename, 0);
360
361 // Create target directory
362 char szFoldername[_MAX_PATH + 1];
363 SCopy(szSource: szFilename, sTarget: szFoldername, _MAX_PATH);
364 MakeTempFilename(szFileName: szFoldername);
365 if (!MakeDirectory(pathname: szFoldername, nullptr)) { hGroup.Close(); return false; }
366
367 // Extract files to folder
368 if (!hGroup.Extract(szFiles: "*", szExtractTo: szFoldername)) { hGroup.Close(); return false; }
369
370 // Close group
371 hGroup.Close();
372
373 // Rename group file
374 char szTempFilename[_MAX_PATH + 1];
375 SCopy(szSource: szFilename, sTarget: szTempFilename, _MAX_PATH);
376 MakeTempFilename(szFileName: szTempFilename);
377 if (!RenameFile(szFileName: szFilename, szNewFileName: szTempFilename)) return false;
378
379 // Rename target directory
380 if (!RenameFile(szFileName: szFoldername, szNewFileName: szFilename)) return false;
381
382 // Delete renamed group file
383 return EraseItem(szItemName: szTempFilename);
384}
385
386bool C4Group_ExplodeDirectory(const char *szFilename)
387{
388 // Ignore
389 if (C4Group_TestIgnore(szFilename)) return true;
390
391 // Unpack this directory
392 if (!C4Group_UnpackDirectory(szFilename)) return false;
393
394 // Explode all children
395 ForEachFile(szDirName: szFilename, fnCallback: C4Group_ExplodeDirectory);
396
397 // Success
398 return true;
399}
400
401bool C4Group_ReadFile(const char *szFile, char **pData, size_t *iSize)
402{
403 // security
404 if (!szFile || !pData) return false;
405 // get mother path & file name
406 char szPath[_MAX_PATH + 1];
407 GetParentPath(szFilename: szFile, szBuffer: szPath);
408 const char *pFileName = GetFilename(path: szFile);
409 // open mother group
410 C4Group MotherGroup;
411 if (!MotherGroup.Open(szGroupName: szPath)) return false;
412 // access the file
413 size_t iFileSize;
414 if (!MotherGroup.AccessEntry(szWildCard: pFileName, iSize: &iFileSize)) return false;
415 // create buffer
416 *pData = new char[iFileSize];
417 // read it
418 if (!MotherGroup.Read(pBuffer: *pData, iSize: iFileSize)) { delete[] *pData; *pData = nullptr; return false; }
419 // ok
420 MotherGroup.Close();
421 if (iSize) *iSize = iFileSize;
422 return true;
423}
424
425bool C4Group_GetFileCRC(const char *szFilename, uint32_t *pCRC32)
426{
427 if (!pCRC32) return false;
428 // doesn't exist physically?
429 char szPath[_MAX_PATH + 1]; bool fTemporary = false;
430 if (FileExists(szFileName: szFilename))
431 SCopy(szSource: szFilename, sTarget: szPath, _MAX_PATH);
432 else
433 {
434 // Expect file to be packed: Extract to temporary
435 SCopy(szSource: GetFilename(path: szFilename), sTarget: szPath, _MAX_PATH);
436 MakeTempFilename(szFileName: szPath);
437 if (!C4Group_CopyItem(szSource: szFilename, szTarget1: szPath)) return false;
438 fTemporary = true;
439 }
440 // open file
441 CStdFile File;
442 if (!File.Open(szFileName: szPath))
443 return false;
444 // calculcate CRC
445 uint32_t iCRC32 = 0;
446 for (;;)
447 {
448 // read a chunk of data
449 uint8_t szData[CStdFileBufSize]; size_t iSize = 0;
450 if (!File.Read(pBuffer: szData, iSize: CStdFileBufSize, ipFSize: &iSize))
451 if (!iSize)
452 break;
453 // update CRC
454 iCRC32 = crc32(crc: iCRC32, buf: szData, len: checked_cast<unsigned int>(from: iSize));
455 }
456 // close file
457 File.Close();
458 // okay
459 *pCRC32 = iCRC32;
460
461 if (fTemporary)
462 {
463 EraseFile(szFileName: szPath);
464 }
465 return true;
466}
467
468bool C4Group_GetFileContentsCRC(const char *szFilename, uint32_t *pCRC32)
469{
470 C4Group hGroup;
471 if (hGroup.Open(szGroupName: szFilename))
472 {
473 *pCRC32 = hGroup.EntryCRC32();
474 hGroup.Close();
475 return true;
476 }
477
478 return false;
479}
480
481bool C4Group_GetFileSHA1(const char *szFilename, uint8_t *pSHA1)
482{
483 if (!pSHA1) return false;
484 // doesn't exist physically?
485 char szPath[_MAX_PATH + 1]; bool fTemporary = false;
486 if (FileExists(szFileName: szFilename))
487 SCopy(szSource: szFilename, sTarget: szPath, _MAX_PATH);
488 else
489 {
490 // Expect file to be packed: Extract to temporary
491 SCopy(szSource: GetFilename(path: szFilename), sTarget: szPath, _MAX_PATH);
492 MakeTempFilename(szFileName: szPath);
493 if (!C4Group_CopyItem(szSource: szFilename, szTarget1: szPath)) return false;
494 fTemporary = true;
495 }
496 // open file
497 CStdFile File;
498 if (!File.Open(szFileName: szPath))
499 return false;
500 // calculcate CRC
501 StdSha1 sha1;
502 for (;;)
503 {
504 // read a chunk of data
505 uint8_t szData[CStdFileBufSize]; size_t iSize = 0;
506 if (!File.Read(pBuffer: szData, iSize: CStdFileBufSize, ipFSize: &iSize))
507 if (!iSize)
508 break;
509 // update CRC
510 sha1.Update(buffer: szData, len: iSize);
511 }
512 // close file
513 File.Close();
514
515 if (fTemporary)
516 {
517 EraseFile(szFileName: szPath);
518 }
519
520 // finish calculation
521 sha1.GetHash(result: pSHA1);
522 return true;
523}
524
525void MemScramble(uint8_t *bypBuffer, int iSize)
526{
527 int cnt; uint8_t temp;
528 // XOR deface
529 for (cnt = 0; cnt < iSize; cnt++)
530 bypBuffer[cnt] ^= 237;
531 // byte swap
532 for (cnt = 0; cnt + 2 < iSize; cnt += 3)
533 {
534 temp = bypBuffer[cnt];
535 bypBuffer[cnt] = bypBuffer[cnt + 2];
536 bypBuffer[cnt + 2] = temp;
537 }
538}
539
540// C4Group
541
542void C4GroupHeader::Init()
543{
544 SCopy(C4GroupFileID, sTarget: id, iMaxL: sizeof(id) - 1);
545 Ver1 = C4GroupFileVer1; Ver2 = C4GroupFileVer2;
546 Entries = 0;
547 SCopy(szSource: "New C4Group", sTarget: Maker, iMaxL: C4GroupMaxMaker);
548 Password[0] = 0;
549}
550
551C4GroupEntry::~C4GroupEntry()
552{
553 if (HoldBuffer)
554 if (bpMemBuf)
555 {
556 if (BufferIsStdbuf)
557 StdBuf::DeletePointer(data: bpMemBuf);
558 else
559 delete[] bpMemBuf;
560 }
561}
562
563#ifdef _WIN32
564
565void C4GroupEntry::Set(const DirectoryIterator &iter, const char *szPath)
566{
567 *this = {};
568 SCopy(iter.fdt.name, FileName, _MAX_FNAME);
569 Size = iter.fdt.size;
570 Time = static_cast<uint32_t>(iter.fdt.time_create);
571 SCopy(*iter, DiskPath, _MAX_PATH - 1);
572 Status = C4GRES_OnDisk;
573 Packed = false;
574 ChildGroup = false;
575 // Notice folder entries are not checked for ChildGroup status.
576 // This would cause extreme performance loss and be good for
577 // use in entry list display only.
578}
579
580#else
581
582void C4GroupEntry::Set(const DirectoryIterator &iter, const char *path)
583{
584 *this = {};
585 SCopy(szSource: GetFilename(path: *iter), sTarget: FileName, _MAX_FNAME);
586 SCopy(szSource: *iter, sTarget: DiskPath, _MAX_PATH - 1);
587 struct stat buf;
588 if (!stat(file: DiskPath, buf: &buf))
589 {
590 Size = buf.st_size;
591 Time = buf.st_mtime;
592 }
593 else
594 Size = 0;
595 Status = C4GRES_OnDisk;
596 Packed = false;
597 ChildGroup = false;
598 // Notice folder entries are not checked for ChildGroup status.
599 // This would cause extreme performance loss and be good for
600 // use in entry list display only.
601}
602
603#endif
604
605C4Group::C4Group()
606{
607 Init();
608 StdOutput = false;
609 fnProcessCallback = nullptr;
610 MadeOriginal = false;
611 NoSort = false;
612}
613
614void C4Group::Init()
615{
616 // General
617 Status = GRPF_Inactive;
618 FileName[0] = 0;
619 // Child status
620 Mother = nullptr;
621 ExclusiveChild = false;
622 // File only
623 FilePtr = 0;
624 EntryOffset = 0;
625 Modified = false;
626 Head.Init();
627 FirstEntry = nullptr;
628 SearchPtr = nullptr;
629 // Folder only
630 FolderSearch.Reset();
631 // Error status
632 SCopy(szSource: "No Error", sTarget: ErrorString, iMaxL: C4GroupMaxError);
633}
634
635C4Group::~C4Group()
636{
637 Clear();
638}
639
640bool C4Group::Error(const char *szStatus)
641{
642 SCopy(szSource: szStatus, sTarget: ErrorString, iMaxL: C4GroupMaxError);
643 return false;
644}
645
646const char *C4Group::GetError()
647{
648 return ErrorString;
649}
650
651void C4Group::SetStdOutput(bool fStatus)
652{
653 StdOutput = fStatus;
654}
655
656bool C4Group::Open(const char *szGroupName, bool fCreate, const OpenFlags openFlags)
657{
658 if (!szGroupName) return Error(szStatus: "Open: Null filename");
659 if (!szGroupName[0]) return Error(szStatus: "Open: Empty filename");
660
661 char szGroupNameN[_MAX_FNAME];
662 SCopy(szSource: szGroupName, sTarget: szGroupNameN, _MAX_FNAME);
663 // Convert to native path
664 SReplaceChar(str: szGroupNameN, fc: '\\', DirectorySeparator);
665
666 // Real reference
667 if (!(fCreate && (openFlags & OpenFlags::Overwrite) == OpenFlags::Overwrite) && FileExists(szFileName: szGroupNameN))
668 {
669 // Init
670 Init();
671 // Open group or folder
672 return OpenReal(szGroupName: szGroupNameN);
673 }
674
675 // If requested, try creating a new group file
676 if (fCreate)
677 {
678 CStdFile temp;
679 if (temp.Create(szFileName: szGroupNameN, fCompressed: false))
680 {
681 // Temporary file has been created
682 temp.Close();
683 // Init
684 Init();
685 Status = GRPF_File; Modified = true;
686 SCopy(szSource: szGroupNameN, sTarget: FileName, _MAX_FNAME);
687 return true;
688 }
689 }
690
691 // While not a real reference (child group), trace back to mother group or folder.
692 // Open mother and child in exclusive mode.
693 char szRealGroup[_MAX_FNAME];
694 SCopy(szSource: szGroupNameN, sTarget: szRealGroup, _MAX_FNAME);
695 do
696 {
697 if (!TruncatePath(szPath: szRealGroup)) return Error(szStatus: "Open: File not found");
698 } while (!FileExists(szFileName: szRealGroup));
699
700 // Open mother and child in exclusive mode
701 auto *const mother = new C4Group;
702 mother->SetStdOutput(StdOutput);
703
704 if (!mother->Open(szGroupName: szRealGroup))
705 {
706 Clear(); return Error(szStatus: "Open: Cannot open mother");
707 }
708
709 if (!OpenAsChild(pMother: mother, szEntryName: szGroupNameN + SLen(sptr: szRealGroup) + 1, fExclusive: true))
710 {
711 Clear(); return Error(szStatus: "Open:: Cannot open as child");
712 }
713
714 // Success
715 return true;
716}
717
718bool C4Group::OpenReal(const char *szFilename)
719{
720 // Get original filename
721 if (!szFilename) return false;
722 SCopy(szSource: szFilename, sTarget: FileName, _MAX_FNAME);
723 MakeOriginalFilename(szFilename: FileName);
724
725 // Folder
726 if (DirectoryExists(szFileName: FileName))
727 {
728 // Ignore
729 if (C4Group_TestIgnore(szFilename))
730 return false;
731 // OpenReal: Simply set status and return
732 Status = GRPF_Folder;
733 SCopy(szSource: "Open directory", sTarget: Head.Maker, iMaxL: C4GroupMaxMaker);
734 ResetSearch();
735 // Success
736 return true;
737 }
738
739 // File: Try reading header and entries
740 if (OpenRealGrpFile())
741 {
742 Status = GRPF_File;
743 ResetSearch();
744 return true;
745 }
746 else
747 return false;
748
749 return Error(szStatus: "OpenReal: Not a valid group");
750}
751
752bool C4Group::OpenRealGrpFile()
753
754{
755 int cnt, file_entries;
756 C4GroupEntryCore corebuf;
757
758 // Open StdFile
759 if (!StdFile.Open(szFileName: FileName, fCompressed: true)) return Error(szStatus: "OpenRealGrpFile: Cannot open standard file");
760
761 // Read header
762 if (!StdFile.Read(pBuffer: reinterpret_cast<uint8_t *>(&Head), iSize: sizeof(C4GroupHeader))) return Error(szStatus: "OpenRealGrpFile: Error reading header");
763 MemScramble(bypBuffer: reinterpret_cast<uint8_t *>(&Head), iSize: sizeof(C4GroupHeader));
764 EntryOffset += sizeof(C4GroupHeader);
765
766 // Check Header
767 if (!SEqual(szStr1: Head.id, C4GroupFileID)
768 || (Head.Ver1 != C4GroupFileVer1) || (Head.Ver2 > C4GroupFileVer2))
769 return Error(szStatus: "OpenRealGrpFile: Invalid header");
770
771 // Read Entries
772 file_entries = Head.Entries;
773 Head.Entries = 0; // Reset, will be recounted by AddEntry
774 for (cnt = 0; cnt < file_entries; cnt++)
775 {
776 if (!StdFile.Read(pBuffer: reinterpret_cast<uint8_t *>(&corebuf), iSize: sizeof(C4GroupEntryCore))) return Error(szStatus: "OpenRealGrpFile: Error reading entries");
777 C4InVal::ValidateFilename(szFilename: corebuf.FileName); // filename validation: Prevent overwriting of user stuff by malicuous groups
778 EntryOffset += sizeof(C4GroupEntryCore);
779 if (!AddEntry(status: C4GRES_InGroup, childgroup: !!corebuf.ChildGroup,
780 fname: corebuf.FileName, size: corebuf.Size, time: corebuf.Time,
781 cCRC: corebuf.HasCRC, iCRC: corebuf.CRC, entryname: corebuf.FileName,
782 membuf: nullptr, fDeleteOnDisk: false, fHoldBuffer: false,
783 fExecutable: !!corebuf.Executable))
784 return Error(szStatus: "OpenRealGrpFile: Cannot add entry");
785 }
786
787 return true;
788}
789
790bool C4Group::AddEntry(int status,
791 bool childgroup,
792 const char *fname,
793 size_t size,
794 time_t time,
795 char cCRC,
796 unsigned int iCRC,
797 const char *entryname,
798 uint8_t *membuf,
799 bool fDeleteOnDisk,
800 bool fHoldBuffer,
801 bool fExecutable,
802 bool fBufferIsStdbuf)
803{
804 // Folder: add file to folder immediately
805 if (Status == GRPF_Folder)
806 {
807 // Close open StdFile
808 StdFile.Close();
809
810 // Get path to target folder file
811 char tfname[_MAX_FNAME];
812 SCopy(szSource: FileName, sTarget: tfname, _MAX_FNAME);
813 AppendBackslash(szFileName: tfname);
814 if (entryname) SAppend(szSource: entryname, szTarget: tfname);
815 else SAppend(szSource: GetFilename(path: fname), szTarget: tfname);
816
817 switch (status)
818 {
819 case C4GRES_OnDisk: // Copy/move file to folder
820 return (CopyItem(szSource: fname, szTarget: tfname) && (!fDeleteOnDisk || EraseItem(szItemName: fname)));
821
822 case C4GRES_InMemory: // Save buffer to file in folder
823 CStdFile hFile;
824 bool fOkay = false;
825 if (hFile.Create(szFileName: tfname, fCompressed: !!childgroup))
826 fOkay = !!hFile.Write(pBuffer: membuf, iSize: size);
827 hFile.Close();
828
829 if (fHoldBuffer) if (fBufferIsStdbuf) StdBuf::DeletePointer(data: membuf); else delete[] membuf;
830
831 return fOkay;
832
833 // InGrp & Deleted ignored
834 }
835
836 return Error(szStatus: "Add to folder: Invalid request");
837 }
838
839 // Group file: add to virtual entry list
840
841 C4GroupEntry *nentry, *lentry, *centry;
842
843 // Delete existing entries of same name
844 centry = GetEntry(szName: GetFilename(path: entryname ? entryname : fname));
845 if (centry) { centry->Status = C4GRES_Deleted; Head.Entries--; }
846
847 // Allocate memory for new entry
848 nentry = new C4GroupEntry;
849 // if (!(nentry = new C4GroupEntry)) return false; ...theoretically, delete Hold buffer here - why?
850
851 // Find end of list
852 for (lentry = FirstEntry; lentry && lentry->Next; lentry = lentry->Next);
853
854 // Init entry core data
855 if (entryname) SCopy(szSource: entryname, sTarget: nentry->FileName, _MAX_FNAME);
856 else SCopy(szSource: GetFilename(path: fname), sTarget: nentry->FileName, _MAX_FNAME);
857 nentry->Size = checked_cast<int32_t>(from: size);
858 nentry->Time = static_cast<uint32_t>(time + C4Group_AssumeTimeOffset);
859 nentry->ChildGroup = childgroup;
860 nentry->Offset = 0;
861 nentry->HasCRC = cCRC;
862 nentry->CRC = iCRC;
863 nentry->Executable = fExecutable;
864 nentry->DeleteOnDisk = fDeleteOnDisk;
865 nentry->HoldBuffer = fHoldBuffer;
866 nentry->BufferIsStdbuf = fBufferIsStdbuf;
867 if (lentry) nentry->Offset = lentry->Offset + lentry->Size;
868
869 // Init list entry data
870 SCopy(szSource: fname, sTarget: nentry->DiskPath, _MAX_FNAME);
871 nentry->Status = status;
872 nentry->bpMemBuf = membuf;
873 nentry->Next = nullptr;
874 nentry->NoSort = NoSort;
875
876 // Append entry to list
877 if (lentry) lentry->Next = nentry;
878 else FirstEntry = nentry;
879
880 // Increase virtual file count of group
881 Head.Entries++;
882
883 return true;
884}
885
886C4GroupEntry *C4Group::GetEntry(const char *szName)
887{
888 if (Status == GRPF_Folder) return nullptr;
889 C4GroupEntry *centry;
890 for (centry = FirstEntry; centry; centry = centry->Next)
891 if (centry->Status != C4GRES_Deleted)
892 if (WildcardMatch(szFName1: szName, szFName2: centry->FileName))
893 return centry;
894 return nullptr;
895}
896
897bool C4Group::Close()
898{
899 C4GroupEntry *centry;
900 bool fRewrite = false;
901
902 if (Status == GRPF_Inactive) return false;
903
904 // Folder: just close
905 if (Status == GRPF_Folder)
906 {
907 CloseExclusiveMother(); Clear(); return true;
908 }
909
910 // Rewrite check
911 for (centry = FirstEntry; centry; centry = centry->Next)
912 if (centry->Status != C4GRES_InGroup)
913 fRewrite = true;
914 if (Modified) fRewrite = true;
915
916 // No rewrite: just close
917 if (!fRewrite)
918 {
919 CloseExclusiveMother(); Clear(); return true;
920 }
921
922 if (StdOutput) std::println(fmt: "Writing group file...");
923
924 // Set new version
925 Head.Ver1 = C4GroupFileVer1;
926 Head.Ver2 = C4GroupFileVer2;
927
928 // Creation stamp
929 Head.Creation = static_cast<int32_t>(time(timer: nullptr));
930
931 // Lose original on any save unless made in this session
932 if (!MadeOriginal) Head.Original = 0;
933
934 // Automatic maker
935 if (C4Group_Maker[0]) SCopy(szSource: C4Group_Maker, sTarget: Head.Maker, iMaxL: C4GroupMaxMaker);
936
937 // Automatic sort
938 SortByList(ppSortList: C4Group_SortList);
939
940 // Calculate all missing checksums
941 EntryCRC32(szWildCard: nullptr);
942
943 // Save group contents to disk
944 bool fSuccess = Save(fReOpen: false);
945
946 // Close exclusive mother
947 CloseExclusiveMother();
948
949 // Close file
950 Clear();
951
952 return !!fSuccess;
953}
954
955bool C4Group::Save(bool fReOpen)
956{
957 int cscore;
958 C4GroupEntryCore *save_core;
959 C4GroupEntry *centry;
960 char szTempFileName[_MAX_FNAME + 1], szGrpFileName[_MAX_FNAME + 1];
961
962 // Create temporary core list with new actual offsets to be saved
963 save_core = new C4GroupEntryCore[Head.Entries];
964 cscore = 0;
965 for (centry = FirstEntry; centry; centry = centry->Next)
966 if (centry->Status != C4GRES_Deleted)
967 {
968 save_core[cscore] = *centry;
969 // Make actual offset
970 save_core[cscore].Offset = 0;
971 if (cscore > 0) save_core[cscore].Offset = save_core[cscore - 1].Offset + save_core[cscore - 1].Size;
972 cscore++;
973 }
974
975 // Create target temp file (in working directory!)
976 SCopy(szSource: FileName, sTarget: szGrpFileName, _MAX_FNAME);
977 SCopy(szSource: GetFilename(path: FileName), sTarget: szTempFileName, _MAX_FNAME);
978 MakeTempFilename(szFileName: szTempFileName);
979 // (Temp file must not have the same name as the group.)
980 if (SEqual(szStr1: szTempFileName, szStr2: szGrpFileName))
981 {
982 SAppend(szSource: ".tmp", szTarget: szTempFileName); // Add a second temp extension
983 MakeTempFilename(szFileName: szTempFileName);
984 }
985
986 // Create the new (temp) group file
987 CStdFile tfile;
988 if (!tfile.Create(szFileName: szTempFileName, fCompressed: true))
989 {
990 delete[] save_core; return Error(szStatus: "Close: ...");
991 }
992
993 // Save header and core list
994 C4GroupHeader headbuf = Head;
995 MemScramble(bypBuffer: reinterpret_cast<uint8_t *>(&headbuf), iSize: sizeof(C4GroupHeader));
996 if (!tfile.Write(pBuffer: reinterpret_cast<uint8_t *>(&headbuf), iSize: sizeof(C4GroupHeader))
997 || !tfile.Write(pBuffer: reinterpret_cast<uint8_t *>(save_core), iSize: Head.Entries * sizeof(C4GroupEntryCore)))
998 {
999 tfile.Close(); delete[] save_core; return Error(szStatus: "Close: ...");
1000 }
1001 delete[] save_core;
1002
1003 // Save Entries to temp file
1004 int iTotalSize = 0, iSizeDone = 0;
1005 for (centry = FirstEntry; centry; centry = centry->Next) iTotalSize += centry->Size;
1006 for (centry = FirstEntry; centry; centry = centry->Next)
1007 if (AppendEntry2StdFile(centry, stdfile&: tfile))
1008 {
1009 iSizeDone += centry->Size; if (iTotalSize && fnProcessCallback) fnProcessCallback(centry->FileName, 100 * iSizeDone / iTotalSize);
1010 }
1011 else
1012 {
1013 tfile.Close(); return false;
1014 }
1015 tfile.Close();
1016
1017 // Child: move temp file to mother
1018 if (Mother)
1019 {
1020 if (!Mother->Move(szFile: szTempFileName, szAddAs: GetFilename(path: FileName)))
1021 {
1022 CloseExclusiveMother(); Clear(); return Error(szStatus: "Close: Cannot move rewritten child temp file to mother");
1023 }
1024 StdFile.Close();
1025 return true;
1026 }
1027
1028 // Clear (close file)
1029 Clear();
1030
1031 // Delete old group file, rename new file
1032 if (!EraseFile(szFileName: szGrpFileName))
1033 return Error(szStatus: "Close: Cannot erase temp file");
1034 if (!RenameFile(szFileName: szTempFileName, szNewFileName: szGrpFileName))
1035 return Error(szStatus: "Close: Cannot rename group file");
1036
1037 // Should reopen the file?
1038 if (fReOpen)
1039 OpenReal(szFilename: szGrpFileName);
1040
1041 return true;
1042}
1043
1044void C4Group::Default()
1045{
1046 FirstEntry = nullptr;
1047 StdFile.Default();
1048 Mother = nullptr;
1049 ExclusiveChild = 0;
1050 Init();
1051}
1052
1053void C4Group::Clear()
1054{
1055 // Delete entries
1056 C4GroupEntry *next;
1057 while (FirstEntry)
1058 {
1059 next = FirstEntry->Next;
1060 delete FirstEntry;
1061 FirstEntry = next;
1062 }
1063 // Close std file
1064 StdFile.Close();
1065 // Delete mother
1066 if (Mother && ExclusiveChild)
1067 {
1068 delete Mother;
1069 Mother = nullptr;
1070 }
1071 // Reset
1072 Init();
1073}
1074
1075bool C4Group::AppendEntry2StdFile(C4GroupEntry *centry, CStdFile &hTarget)
1076{
1077 CStdFile hSource;
1078 long csize;
1079 uint8_t fbuf;
1080
1081 switch (centry->Status)
1082 {
1083 case C4GRES_InGroup: // Copy from group to std file
1084 if (!SetFilePtr(centry->Offset))
1085 return Error(szStatus: "AE2S: Cannot set file pointer");
1086 for (csize = centry->Size; csize > 0; csize--)
1087 {
1088 if (!Read(pBuffer: &fbuf, iSize: 1))
1089 return Error(szStatus: "AE2S: Cannot read entry from group file");
1090 if (!hTarget.Write(pBuffer: &fbuf, iSize: 1))
1091 return Error(szStatus: "AE2S: Cannot write to target file");
1092 }
1093 break;
1094
1095 case C4GRES_OnDisk: // Copy/move from disk item to std file
1096 {
1097 char szFileSource[_MAX_FNAME + 1];
1098 SCopy(szSource: centry->DiskPath, sTarget: szFileSource, _MAX_FNAME);
1099
1100 // Disk item is a directory
1101 if (DirectoryExists(szFileName: centry->DiskPath))
1102 return Error(szStatus: "AE2S: Cannot add directory to group file");
1103
1104 // Resort group if neccessary
1105 // (The group might be renamed by adding, forcing a resort)
1106 bool fTempFile = false;
1107 if (centry->ChildGroup)
1108 if (!centry->NoSort)
1109 if (!SEqual(szStr1: GetFilename(path: szFileSource), szStr2: centry->FileName))
1110 {
1111 // copy group
1112 MakeTempFilename(szFileName: szFileSource);
1113 if (!CopyItem(szSource: centry->DiskPath, szTarget: szFileSource))
1114 return Error(szStatus: "AE2S: Cannot copy item");
1115 // open group and resort
1116 C4Group SortGrp;
1117 if (!SortGrp.Open(szGroupName: szFileSource))
1118 return Error(szStatus: "AE2S: Cannot open group");
1119 if (!SortGrp.SortByList(ppSortList: C4Group_SortList, szFilename: centry->FileName))
1120 return Error(szStatus: "AE2S: Cannot resort group");
1121 fTempFile = true;
1122 // close group (won't be saved if the sort didn't change)
1123 SortGrp.Close();
1124 }
1125
1126 // Append disk source to target file
1127 if (!hSource.Open(szFileName: szFileSource, fCompressed: !!centry->ChildGroup))
1128 return Error(szStatus: "AE2S: Cannot open on-disk file");
1129 for (csize = centry->Size; csize > 0; csize--)
1130 {
1131 if (!hSource.Read(pBuffer: &fbuf, iSize: 1))
1132 {
1133 hSource.Close(); return Error(szStatus: "AE2S: Cannot read on-disk file");
1134 }
1135 if (!hTarget.Write(pBuffer: &fbuf, iSize: 1))
1136 {
1137 hSource.Close(); return Error(szStatus: "AE2S: Cannot write to target file");
1138 }
1139 }
1140 hSource.Close();
1141
1142 // Erase temp file
1143 if (fTempFile)
1144 EraseItem(szItemName: szFileSource);
1145 // Erase disk source if requested
1146 if (centry->DeleteOnDisk)
1147 EraseItem(szItemName: centry->DiskPath);
1148
1149 break;
1150 }
1151
1152 case C4GRES_InMemory: // Copy from mem to std file
1153 if (!centry->bpMemBuf) return Error(szStatus: "AE2S: no buffer");
1154 if (!hTarget.Write(pBuffer: centry->bpMemBuf, iSize: centry->Size)) return Error(szStatus: "AE2S: writing error");
1155 break;
1156
1157 case C4GRES_Deleted: // Don't save
1158 break;
1159
1160 default: // Unknown file status
1161 return Error(szStatus: "AE2S: Unknown file status");
1162 }
1163
1164 return true;
1165}
1166
1167void C4Group::ResetSearch()
1168{
1169 switch (Status)
1170 {
1171 case GRPF_Folder:
1172 SearchPtr = nullptr;
1173 FolderSearch.Reset(dirname: FileName);
1174 if (*FolderSearch)
1175 {
1176 FolderSearchEntry.Set(iter: FolderSearch, path: FileName);
1177 SearchPtr = &FolderSearchEntry;
1178 }
1179 break;
1180 case GRPF_File:
1181 SearchPtr = FirstEntry;
1182 break;
1183 }
1184}
1185
1186C4GroupEntry *C4Group::GetNextFolderEntry()
1187{
1188 if (*++FolderSearch)
1189 {
1190 FolderSearchEntry.Set(iter: FolderSearch, path: FileName);
1191 return &FolderSearchEntry;
1192 }
1193 else
1194 {
1195 return nullptr;
1196 }
1197}
1198
1199C4GroupEntry *C4Group::SearchNextEntry(const char *szName)
1200{
1201 // Wildcard "*.*" is expected to find all files: substitute correct wildcard "*"
1202 if (SEqual(szStr1: szName, szStr2: "*.*"))
1203 szName = "*";
1204 // Search by group type
1205 C4GroupEntry *pEntry;
1206 switch (Status)
1207 {
1208 case GRPF_File:
1209 for (pEntry = SearchPtr; pEntry; pEntry = pEntry->Next)
1210 if (pEntry->Status != C4GRES_Deleted)
1211 if (WildcardMatch(szFName1: szName, szFName2: pEntry->FileName))
1212 {
1213 SearchPtr = pEntry->Next;
1214 return pEntry;
1215 }
1216 break;
1217
1218 case GRPF_Folder:
1219 for (pEntry = SearchPtr; pEntry; pEntry = GetNextFolderEntry())
1220 if (WildcardMatch(szFName1: szName, szFName2: pEntry->FileName))
1221 if (!C4Group_TestIgnore(szFilename: pEntry->FileName))
1222 {
1223 LastFolderSearchEntry = (*pEntry);
1224 pEntry = &LastFolderSearchEntry;
1225 SearchPtr = GetNextFolderEntry();
1226 return pEntry;
1227 }
1228 break;
1229 }
1230 // No entry found: reset search pointer
1231 SearchPtr = nullptr;
1232 return nullptr;
1233}
1234
1235bool C4Group::SetFilePtr(size_t iOffset)
1236{
1237 if (Status == GRPF_Folder)
1238 return Error(szStatus: "SetFilePtr not implemented for Folders");
1239
1240 // ensure mother is at correct pos
1241 if (Mother && !Mother->EnsureChildFilePtr(pChild: this))
1242 return false;
1243
1244 // Rewind if necessary
1245 if (FilePtr > iOffset)
1246 if (!RewindFilePtr()) return false;
1247
1248 // Advance to target pointer
1249 if (FilePtr < iOffset)
1250 if (!AdvanceFilePtr(iOffset: iOffset - FilePtr)) return false;
1251
1252 return true;
1253}
1254
1255bool C4Group::Advance(size_t iOffset)
1256{
1257 if (Status == GRPF_Folder) return !!StdFile.Advance(iOffset);
1258 // FIXME: reading the file one byte at a time sounds just slow.
1259 uint8_t buf;
1260 for (; iOffset > 0; iOffset--)
1261 if (!Read(pBuffer: &buf, iSize: 1)) return false;
1262 return true;
1263}
1264
1265bool C4Group::Read(void *pBuffer, size_t iSize)
1266{
1267 switch (Status)
1268 {
1269 case GRPF_File:
1270 // Child group: read from mother group
1271 if (Mother)
1272 {
1273 if (!Mother->Read(pBuffer, iSize))
1274 {
1275 RewindFilePtr(); return Error(szStatus: "Read:");
1276 }
1277 }
1278 // Regular group: read from standard file
1279 else
1280 {
1281 if (!StdFile.Read(pBuffer, iSize))
1282 {
1283 RewindFilePtr(); return Error(szStatus: "Read:");
1284 }
1285 }
1286 FilePtr += iSize;
1287 break;
1288 case GRPF_Folder:
1289 if (!StdFile.Read(pBuffer, iSize)) return Error(szStatus: "Read: Error reading from folder contents");
1290 break;
1291 }
1292
1293 return true;
1294}
1295
1296bool C4Group::AdvanceFilePtr(size_t iOffset, C4Group *pByChild)
1297{
1298 // Child group file: pass command to mother
1299 if ((Status == GRPF_File) && Mother)
1300 {
1301 // Ensure mother file ptr for it may have been moved by foreign access to mother
1302 if (!Mother->EnsureChildFilePtr(pChild: this))
1303 return false;
1304
1305 if (!Mother->AdvanceFilePtr(iOffset, pByChild: this))
1306 return false;
1307 }
1308 // Regular group
1309 else if (Status == GRPF_File)
1310 {
1311 if (!StdFile.Advance(iOffset))
1312 return false;
1313 }
1314 // Open folder
1315 else
1316 {
1317 if (!StdFile.Advance(iOffset))
1318 return false;
1319 }
1320
1321 // Advanced
1322 FilePtr += iOffset;
1323
1324 return true;
1325}
1326
1327bool C4Group::RewindFilePtr()
1328{
1329#ifndef NDEBUG
1330#ifdef C4ENGINE
1331 if (szCurrAccessedEntry && !iC4GroupRewindFilePtrNoWarn)
1332 {
1333 LogNTr(spdlog::level::debug, "C4Group::RewindFilePtr() for {} ({})", szCurrAccessedEntry ? szCurrAccessedEntry : "???", +FileName);
1334 szCurrAccessedEntry = nullptr;
1335 }
1336#endif
1337#endif
1338
1339 // Child group file: pass command to mother
1340 if ((Status == GRPF_File) && Mother)
1341 {
1342 if (!Mother->SetFilePtr2Entry(szName: FileName, pByChild: this)) // Set to group file start
1343 return false;
1344 if (!Mother->AdvanceFilePtr(iOffset: EntryOffset, pByChild: this)) // Advance data offset
1345 return false;
1346 }
1347 // Regular group or open folder: rewind standard file
1348 else
1349 {
1350 if (!StdFile.Rewind()) // Set to group file start
1351 return false;
1352 if (!StdFile.Advance(iOffset: EntryOffset)) // Advance data offset
1353 return false;
1354 }
1355
1356 FilePtr = 0;
1357
1358 return true;
1359}
1360
1361bool C4Group::View(const char *szFiles)
1362{
1363 C4GroupEntry *centry;
1364 int fcount = 0, bcount = 0; // Virtual counts
1365 std::size_t maxFilenameLength{0};
1366
1367 if (!StdOutput) return false;
1368
1369 // Calculate group file crc
1370 uint32_t crc = 0;
1371 C4Group_GetFileCRC(szFilename: GetFullName().getData(), pCRC32: &crc);
1372
1373 // Display list
1374 ResetSearch();
1375 while (centry = SearchNextEntry(szName: szFiles))
1376 {
1377 fcount++;
1378 bcount += centry->Size;
1379 maxFilenameLength = std::max(a: maxFilenameLength, b: std::strlen(s: centry->FileName));
1380 }
1381
1382 std::println(fmt: "Maker: {} Creation: {} {}\n\rVersion: {}.{} CRC: {} ({:X})",
1383 args: GetMaker(),
1384 args&: Head.Creation,
1385 args: GetOriginal() ? "Original" : "",
1386 args&: Head.Ver1, args&: Head.Ver2,
1387 args&: crc, args&: crc);
1388 ResetSearch();
1389 while (centry = SearchNextEntry(szName: szFiles))
1390 {
1391 // convert centry->Time into time_t for localtime
1392 time_t cur_time = centry->Time;
1393 tm *pcoretm = localtime(timer: &cur_time);
1394 tm coretm;
1395 if (pcoretm) coretm = *pcoretm; else std::print(fmt: "(invalid timestamp) ");
1396 centry->Time = static_cast<int32_t>(cur_time);
1397
1398 std::println(fmt: "{:>{}} {:8} Bytes {:02}.{:02}.{:02} {:02}:{:02}:{:02} {}{:08X} {}", args&: centry->FileName, args&: maxFilenameLength,
1399 args&: centry->Size,
1400 args&: coretm.tm_mday, args: coretm.tm_mon + 1, args: coretm.tm_year % 100,
1401 args&: coretm.tm_hour, args&: coretm.tm_min, args&: coretm.tm_sec,
1402 args: centry->HasCRC ? ((centry->HasCRC == C4GECS_New) ? "!" : "~") : " ",
1403 args: centry->HasCRC ? centry->CRC : 0,
1404 args: centry->ChildGroup ? "(Group)" : (centry->Executable ? "(Executable)" : ""));
1405 }
1406 std::println(fmt: "{} Entries, {} Bytes", args&: fcount, args&: bcount);
1407
1408 return true;
1409}
1410
1411bool C4Group::Merge(const char *szFolders)
1412{
1413 bool fMove = true;
1414
1415 if (StdOutput) std::println(fmt: "{}...", args: fMove ? "Moving" : "Adding");
1416
1417 // Add files & directories
1418 char szFileName[_MAX_FNAME + 1];
1419 int iFileCount = 0;
1420 DirectoryIterator i;
1421
1422 // Process segmented path & search wildcards
1423 char cSeparator = (SCharCount(cTarget: ';', szInStr: szFolders) ? ';' : '|');
1424 for (int cseg = 0; SCopySegment(fstr: szFolders, segn: cseg, tstr: szFileName, sepa: cSeparator, _MAX_FNAME); cseg++)
1425 {
1426 i.Reset(dirname: szFileName);
1427 while (*i)
1428 {
1429 // File count
1430 iFileCount++;
1431 // Process output & callback
1432 if (StdOutput) std::println(fmt: "{}", args: GetFilename(path: *i));
1433 if (fnProcessCallback)
1434 fnProcessCallback(GetFilename(path: *i), 0); // cbytes/tbytes
1435 // AddEntryOnDisk
1436 AddEntryOnDisk(szFilename: *i, szAddAs: nullptr, fMove);
1437 ++i;
1438 }
1439 }
1440
1441 if (StdOutput) std::println(fmt: "{} file(s) {}.", args&: iFileCount, args: fMove ? "moved" : "added");
1442
1443 return true;
1444}
1445
1446bool C4Group::AddEntryOnDisk(const char *szFilename,
1447 const char *szAddAs,
1448 bool fMove)
1449{
1450 // Do not process yourself
1451 if (ItemIdentical(szFilename1: szFilename, szFilename2: FileName)) return true;
1452
1453 // File is a directory: copy to temp path, pack, and add packed file
1454 if (DirectoryExists(szFileName: szFilename))
1455 {
1456 // Ignore
1457 if (C4Group_TestIgnore(szFilename)) return true;
1458 // Temp filename
1459 char szTempFilename[_MAX_PATH + 1];
1460 if (C4Group_TempPath[0]) { SCopy(szSource: C4Group_TempPath, sTarget: szTempFilename, _MAX_PATH); SAppend(szSource: GetFilename(path: szFilename), szTarget: szTempFilename, _MAX_PATH); }
1461 else SCopy(szSource: szFilename, sTarget: szTempFilename, _MAX_PATH);
1462 MakeTempFilename(szFileName: szTempFilename);
1463 // Copy or move item to temp file (moved items might be killed if later process fails)
1464 if (fMove) { if (!MoveItem(szSource: szFilename, szTarget: szTempFilename)) return Error(szStatus: "AddEntryOnDisk: Move failure"); }
1465 else { if (!CopyItem(szSource: szFilename, szTarget: szTempFilename)) return Error(szStatus: "AddEntryOnDisk: Copy failure"); }
1466 // Pack temp file
1467 if (!C4Group_PackDirectory(szFilename: szTempFilename)) return Error(szStatus: "AddEntryOnDisk: Pack directory failure");
1468 // Add temp file
1469 if (!szAddAs) szAddAs = GetFilename(path: szFilename);
1470 szFilename = szTempFilename;
1471 fMove = true;
1472 }
1473
1474 // Determine size
1475 bool fIsGroup = !!C4Group_IsGroup(szFilename);
1476 const auto iSize = fIsGroup ? UncompressedFileSize(szFileName: szFilename) : FileSize(fname: szFilename);
1477
1478 // Determine executable bit (linux only)
1479 bool fExecutable = false;
1480#ifdef __linux__
1481 fExecutable = (access(name: szFilename, X_OK) == 0);
1482#endif
1483
1484 // AddEntry
1485 return AddEntry(status: C4GRES_OnDisk,
1486 childgroup: fIsGroup,
1487 fname: szFilename,
1488 size: iSize,
1489 time: FileTime(fname: szFilename),
1490 cCRC: false, iCRC: 0,
1491 entryname: szAddAs,
1492 membuf: nullptr,
1493 fDeleteOnDisk: fMove,
1494 fHoldBuffer: false,
1495 fExecutable);
1496}
1497
1498bool C4Group::Add(const char *szFile, const char *szAddAs)
1499{
1500 bool fMove = false;
1501
1502 if (StdOutput) std::println(fmt: "{} {} as {}...", args: fMove ? "Moving" : "Adding", args: GetFilename(path: szFile), args&: szAddAs);
1503
1504 return AddEntryOnDisk(szFilename: szFile, szAddAs, fMove);
1505}
1506
1507bool C4Group::Move(const char *szFile, const char *szAddAs)
1508{
1509 bool fMove = true;
1510
1511 if (StdOutput) std::println(fmt: "{} {} as {}...", args: fMove ? "Moving" : "Adding", args: GetFilename(path: szFile), args&: szAddAs);
1512
1513 return AddEntryOnDisk(szFilename: szFile, szAddAs, fMove);
1514}
1515
1516bool C4Group::Delete(const char *szFiles, bool fRecursive)
1517{
1518 int fcount = 0;
1519 C4GroupEntry *tentry;
1520
1521 // Segmented file specs
1522 if (SCharCount(cTarget: ';', szInStr: szFiles) || SCharCount(cTarget: '|', szInStr: szFiles))
1523 {
1524 char cSeparator = (SCharCount(cTarget: ';', szInStr: szFiles) ? ';' : '|');
1525 bool success = true;
1526 char filespec[_MAX_FNAME + 1];
1527 for (int cseg = 0; SCopySegment(fstr: szFiles, segn: cseg, tstr: filespec, sepa: cSeparator, _MAX_FNAME); cseg++)
1528 if (!Delete(szFiles: filespec, fRecursive))
1529 success = false;
1530 return success; // Would be nicer to return the file count and add up all counts from recursive actions...
1531 }
1532
1533 // Delete all matching Entries
1534 ResetSearch();
1535 while ((tentry = SearchNextEntry(szName: szFiles)))
1536 {
1537 // StdOutput
1538 if (StdOutput) std::println(fmt: "{}", args&: tentry->FileName);
1539 if (!DeleteEntry(szFilename: tentry->FileName))
1540 return Error(szStatus: "Delete: Could not delete entry");
1541 fcount++;
1542 }
1543
1544 // Recursive: process sub groups
1545 if (fRecursive)
1546 {
1547 C4Group hChild;
1548 ResetSearch();
1549 while ((tentry = SearchNextEntry(szName: "*")))
1550 if (tentry->ChildGroup)
1551 if (hChild.OpenAsChild(pMother: this, szEntryName: tentry->FileName))
1552 {
1553 hChild.SetStdOutput(StdOutput);
1554 hChild.Delete(szFiles, fRecursive);
1555 hChild.Close();
1556 }
1557 }
1558
1559 // StdOutput
1560 if (StdOutput)
1561 std::println(fmt: "{} file(s) deleted.", args&: fcount);
1562
1563 return true; // Would be nicer to return the file count and add up all counts from recursive actions...
1564}
1565
1566// delete item to the recycle bin
1567bool EraseItemSafe(const char *szFilename)
1568{
1569#ifdef _WIN32
1570 char Filename[_MAX_PATH + 1];
1571 SCopy(szFilename, Filename, _MAX_PATH);
1572 Filename[SLen(Filename) + 1] = 0;
1573 SHFILEOPSTRUCTA shs;
1574 shs.hwnd = 0;
1575 shs.wFunc = FO_DELETE;
1576 shs.pFrom = Filename;
1577 shs.pTo = nullptr;
1578 shs.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
1579 shs.fAnyOperationsAborted = false;
1580 shs.hNameMappings = 0;
1581 shs.lpszProgressTitle = nullptr;
1582 return !SHFileOperationA(&shs);
1583#elif defined(USE_SDL_MAINLOOP) && defined(C4ENGINE) && defined(__APPLE__)
1584 bool sendFileToTrash(const char *filename);
1585 return sendFileToTrash(szFilename);
1586#else
1587 return false;
1588#endif
1589}
1590
1591bool C4Group::DeleteEntry(const char *szFilename, bool fRecycle)
1592{
1593 switch (Status)
1594 {
1595 case GRPF_File:
1596 // Get entry
1597 C4GroupEntry *pEntry;
1598 if (!(pEntry = GetEntry(szName: szFilename))) return false;
1599 // Delete moved source files
1600 if (pEntry->Status == C4GRES_OnDisk)
1601 if (pEntry->DeleteOnDisk)
1602 {
1603 EraseItem(szItemName: pEntry->DiskPath);
1604 }
1605 // (moved buffers are deleted by ~C4GroupEntry)
1606 // Delete status and update virtual file count
1607 pEntry->Status = C4GRES_Deleted;
1608 Head.Entries--;
1609 break;
1610 case GRPF_Folder:
1611 StdFile.Close();
1612 char szPath[_MAX_FNAME + 1];
1613 FormatWithNull(buf&: szPath, fmt: "{}" DirSep "{}", args: +FileName, args&: szFilename);
1614
1615 if (fRecycle)
1616 {
1617 if (!EraseItemSafe(szFilename: szPath)) return false;
1618 }
1619 else
1620 {
1621 if (!EraseItem(szItemName: szPath)) return false;
1622 }
1623 break;
1624 }
1625 return true;
1626}
1627
1628bool C4Group::Rename(const char *szFile, const char *szNewName)
1629{
1630 if (StdOutput) std::println(fmt: "Renaming {} to {}...", args&: szFile, args&: szNewName);
1631
1632 switch (Status)
1633 {
1634 case GRPF_File:
1635 // Get entry
1636 C4GroupEntry *pEntry;
1637 if (!(pEntry = GetEntry(szName: szFile))) return Error(szStatus: "Rename: File not found");
1638 // Check double name
1639 if (GetEntry(szName: szNewName) && !SEqualNoCase(szStr1: szNewName, szStr2: szFile)) return Error(szStatus: "Rename: File exists already");
1640 // Rename
1641 SCopy(szSource: szNewName, sTarget: pEntry->FileName, _MAX_FNAME);
1642 Modified = true;
1643 break;
1644 case GRPF_Folder:
1645 StdFile.Close();
1646 char path[_MAX_FNAME + 1]; SCopy(szSource: FileName, sTarget: path, _MAX_PATH - 1);
1647 AppendBackslash(szFileName: path); SAppend(szSource: szFile, szTarget: path, _MAX_PATH);
1648 char path2[_MAX_FNAME + 1]; SCopy(szSource: FileName, sTarget: path2, _MAX_PATH - 1);
1649 AppendBackslash(szFileName: path2); SAppend(szSource: szNewName, szTarget: path2, _MAX_PATH);
1650 if (!RenameFile(szFileName: path, szNewFileName: path2)) return Error(szStatus: "Rename: Failure");
1651 break;
1652 }
1653
1654 return true;
1655}
1656
1657bool C4Group_IsExcluded(const char *szFile, const char *szExcludeList)
1658{
1659 // No file or no exclude list
1660 if (!szFile || !szFile[0] || !szExcludeList || !szExcludeList[0]) return false;
1661 // Process segmented exclude list
1662 char cSeparator = (SCharCount(cTarget: ';', szInStr: szExcludeList) ? ';' : '|');
1663 char szSegment[_MAX_PATH + 1];
1664 for (int i = 0; SCopySegment(fstr: szExcludeList, segn: i, tstr: szSegment, sepa: cSeparator, _MAX_PATH); i++)
1665 if (WildcardMatch(szFName1: szSegment, szFName2: GetFilename(path: szFile)))
1666 return true;
1667 // No match
1668 return false;
1669}
1670
1671bool C4Group::Extract(const char *szFiles, const char *szExtractTo, const char *szExclude)
1672{
1673 // StdOutput
1674 if (StdOutput)
1675 {
1676 std::print(fmt: "Extracting");
1677 if (szExtractTo) std::print(fmt: " to {}", args&: szExtractTo);
1678 std::println(fmt: "...");
1679 }
1680
1681 int fcount = 0;
1682 int cbytes, tbytes;
1683 C4GroupEntry *tentry;
1684
1685 cbytes = 0; tbytes = EntrySize();
1686
1687 // Process segmented list
1688 char cSeparator = (SCharCount(cTarget: ';', szInStr: szFiles) ? ';' : '|');
1689 char szFileName[_MAX_PATH + 1];
1690 for (int cseg = 0; SCopySegment(fstr: szFiles, segn: cseg, tstr: szFileName, sepa: cSeparator, _MAX_PATH); cseg++)
1691 {
1692 // Search all entries
1693 ResetSearch();
1694 while (tentry = SearchNextEntry(szName: szFileName))
1695 {
1696 // skip?
1697 if (C4Group_IsExcluded(szFile: tentry->FileName, szExcludeList: szExclude)) continue;
1698 // Process data & output
1699 if (StdOutput) std::println(fmt: "{}", args: +tentry->FileName);
1700 cbytes += tentry->Size;
1701 if (fnProcessCallback)
1702 fnProcessCallback(tentry->FileName, 100 * cbytes / (std::max)(a: tbytes, b: 1));
1703
1704 // Extract
1705 if (!ExtractEntry(szFilename: tentry->FileName, szExtractTo))
1706 return Error(szStatus: "Extract: Could not extract entry");
1707
1708 fcount++;
1709 }
1710 }
1711
1712 if (StdOutput) std::println(fmt: "{} file(s) extracted.", args&: fcount);
1713
1714 return true;
1715}
1716
1717bool C4Group::ExtractEntry(const char *szFilename, const char *szExtractTo)
1718{
1719 CStdFile tfile;
1720 CStdFile hDummy;
1721 char szTempFName[_MAX_FNAME + 1], szTargetFName[_MAX_FNAME + 1];
1722
1723 // Target file name
1724 if (szExtractTo)
1725 {
1726 SCopy(szSource: szExtractTo, sTarget: szTargetFName, _MAX_FNAME - 1);
1727 if (DirectoryExists(szFileName: szTargetFName))
1728 {
1729 AppendBackslash(szFileName: szTargetFName);
1730 SAppend(szSource: szFilename, szTarget: szTargetFName, _MAX_FNAME);
1731 }
1732 }
1733 else
1734 SCopy(szSource: szFilename, sTarget: szTargetFName, _MAX_FNAME);
1735
1736 // Extract
1737 switch (Status)
1738 {
1739 case GRPF_File: // Copy entry to target
1740 // Get entry
1741 C4GroupEntry *pEntry;
1742 if (!(pEntry = GetEntry(szName: szFilename))) return false;
1743 // Create dummy file to reserve target file name
1744 hDummy.Save(szFileName: szTargetFName, bpBuf: reinterpret_cast<const unsigned char *>("Dummy"), iSize: 5, fCompressed: false, executable: pEntry->Executable, exclusive: true);
1745 // Make temp target file name
1746 SCopy(szSource: szTargetFName, sTarget: szTempFName, _MAX_FNAME);
1747 MakeTempFilename(szFileName: szTempFName);
1748 // Create temp target file
1749 if (!tfile.Create(szFileName: szTempFName, fCompressed: !!pEntry->ChildGroup, fExecutable: !!pEntry->Executable))
1750 return Error(szStatus: "Extract: Cannot create target file");
1751 // Write entry file to temp target file
1752 if (!AppendEntry2StdFile(centry: pEntry, hTarget&: tfile))
1753 {
1754 // Failure: close and erase temp target file
1755 tfile.Close();
1756 EraseItem(szItemName: szTempFName);
1757 // Also erase reservation target file
1758 EraseItem(szItemName: szTargetFName);
1759 // Failure
1760 return false;
1761 }
1762 // Close target file
1763 tfile.Close();
1764 // Make temp file to original file
1765 if (!EraseItem(szItemName: szTargetFName))
1766 return Error(szStatus: "Extract: Cannot erase temporary file");
1767 if (!RenameItem(szItemName: szTempFName, szNewItemName: szTargetFName))
1768 return Error(szStatus: "Extract: Cannot rename temporary file");
1769 // Set output file time
1770#ifdef _WIN32
1771 _utimbuf tftime;
1772 tftime.actime = pEntry->Time;
1773 tftime.modtime = pEntry->Time;
1774 _utime(szTargetFName, &tftime);
1775#else
1776 utimbuf tftime;
1777 tftime.actime = pEntry->Time;
1778 tftime.modtime = pEntry->Time;
1779 utime(file: szTargetFName, file_times: &tftime);
1780#endif
1781 break;
1782 case GRPF_Folder: // Copy item from folder to target
1783 char szPath[_MAX_FNAME + 1];
1784 FormatWithNull(buf&: szPath, fmt: "{}" DirSep "{}", args: +FileName, args&: szFilename);
1785 if (!CopyItem(szSource: szPath, szTarget: szTargetFName))
1786 return Error(szStatus: "ExtractEntry: Cannot copy item");
1787 break;
1788 }
1789 return true;
1790}
1791
1792bool C4Group::OpenAsChild(C4Group *pMother,
1793 const char *szEntryName, bool fExclusive)
1794{
1795 if (!pMother) return Error(szStatus: "OpenAsChild: No mother specified");
1796
1797 if (SCharCount(cTarget: '*', szInStr: szEntryName)) return Error(szStatus: "OpenAsChild: No wildcards allowed");
1798
1799 // Open nested child group check: If szEntryName is a reference to
1800 // a nested group, open the first mother (in specified mode), then open the child
1801 // in exclusive mode
1802
1803 if (SCharCount(DirectorySeparator, szInStr: szEntryName))
1804 {
1805 char mothername[_MAX_FNAME + 1];
1806 SCopyUntil(szSource: szEntryName, sTarget: mothername, DirectorySeparator, _MAX_FNAME);
1807
1808 C4Group *pMother2;
1809 pMother2 = new C4Group;
1810 pMother2->SetStdOutput(StdOutput);
1811 if (!pMother2->OpenAsChild(pMother, szEntryName: mothername, fExclusive))
1812 {
1813 delete pMother2;
1814 return Error(szStatus: "OpenAsChild: Cannot open mother");
1815 }
1816 return OpenAsChild(pMother: pMother2, szEntryName: szEntryName + SLen(sptr: mothername) + 1, fExclusive: true);
1817 }
1818
1819 // Init
1820 SCopy(szSource: szEntryName, sTarget: FileName, _MAX_FNAME);
1821 Mother = pMother;
1822 ExclusiveChild = fExclusive;
1823
1824 // Folder: Simply set status and return
1825 char path[_MAX_FNAME + 1];
1826 SCopy(szSource: GetFullName().getData(), sTarget: path, _MAX_FNAME);
1827 if (DirectoryExists(szFileName: path))
1828 {
1829 SCopy(szSource: path, sTarget: FileName, _MAX_FNAME);
1830 SCopy(szSource: "Open directory", sTarget: Head.Maker, iMaxL: C4GroupMaxMaker);
1831 Status = GRPF_Folder;
1832 ResetSearch();
1833 return true;
1834 }
1835
1836 // Get original entry name
1837 C4GroupEntry *centry;
1838 if (centry = Mother->GetEntry(szName: FileName))
1839 SCopy(szSource: centry->FileName, sTarget: FileName, _MAX_PATH);
1840
1841 // Access entry in mother group
1842 size_t iSize;
1843 if (!Mother->AccessEntry(szWildCard: FileName, iSize: &iSize))
1844 {
1845 CloseExclusiveMother(); Clear(); return Error(szStatus: "OpenAsChild: Entry not in mother group");
1846 }
1847
1848 // Child Group?
1849 if (centry && !centry->ChildGroup)
1850 {
1851 CloseExclusiveMother(); Clear(); return Error(szStatus: "OpenAsChild: Is not a child group");
1852 }
1853
1854 // Read header
1855 // Do not do size checks for packed subgroups of unpacked groups (there will be no entry),
1856 // because that would be the PACKED size which can actually be smaller than sizeof(C4GroupHeader)!
1857 if (iSize < sizeof(C4GroupHeader) && centry)
1858 {
1859 CloseExclusiveMother(); Clear(); return Error(szStatus: "OpenAsChild: Entry too small");
1860 }
1861 if (!Mother->Read(pBuffer: &Head, iSize: sizeof(C4GroupHeader)))
1862 {
1863 CloseExclusiveMother(); Clear(); return Error(szStatus: "OpenAsChild: Entry reading error");
1864 }
1865 MemScramble(bypBuffer: reinterpret_cast<uint8_t *>(&Head), iSize: sizeof(C4GroupHeader));
1866 EntryOffset += sizeof(C4GroupHeader);
1867
1868 // Check Header
1869 if (!SEqual(szStr1: Head.id, C4GroupFileID)
1870 || (Head.Ver1 != C4GroupFileVer1) || (Head.Ver2 > C4GroupFileVer2))
1871 {
1872 CloseExclusiveMother(); Clear(); return Error(szStatus: "OpenAsChild: Invalid Header");
1873 }
1874
1875 // Read Entries
1876 C4GroupEntryCore corebuf;
1877 int file_entries = Head.Entries;
1878 Head.Entries = 0; // Reset, will be recounted by AddEntry
1879 for (int cnt = 0; cnt < file_entries; cnt++)
1880 {
1881 if (!Mother->Read(pBuffer: &corebuf, iSize: sizeof(C4GroupEntryCore)))
1882 {
1883 CloseExclusiveMother(); Clear(); return Error(szStatus: "OpenAsChild: Entry reading error");
1884 }
1885 EntryOffset += sizeof(C4GroupEntryCore);
1886 if (!AddEntry(status: C4GRES_InGroup, childgroup: !!corebuf.ChildGroup,
1887 fname: corebuf.FileName, size: corebuf.Size, time: corebuf.Time,
1888 cCRC: corebuf.HasCRC, iCRC: corebuf.CRC,
1889 entryname: nullptr, membuf: nullptr, fDeleteOnDisk: false, fHoldBuffer: false,
1890 fExecutable: !!corebuf.Executable))
1891 {
1892 CloseExclusiveMother(); Clear(); return Error(szStatus: "OpenAsChild: Insufficient memory");
1893 }
1894 }
1895
1896 ResetSearch();
1897
1898 // File
1899 Status = GRPF_File;
1900
1901 // save position in mother group
1902 if (centry) MotherOffset = centry->Offset;
1903
1904 return true;
1905}
1906
1907bool C4Group::AccessEntry(const char *szWildCard,
1908 size_t *iSize, char *sFileName,
1909 bool *fChild)
1910{
1911#ifdef C4GROUP_DUMP_ACCESS
1912 LogNTr(spdlog::level::debug, "Group access in {}: {}", GetFullName().getData(), szWildCard);
1913#endif
1914 char fname[_MAX_FNAME + 1];
1915 if (!FindEntry(szWildCard, sFileName: fname, iSize: &iCurrFileSize, fChild))
1916 return false;
1917#ifndef NDEBUG
1918 szCurrAccessedEntry = fname;
1919#endif
1920 bool fResult = SetFilePtr2Entry(szName: fname);
1921#ifndef NDEBUG
1922 szCurrAccessedEntry = nullptr;
1923#endif
1924 if (!fResult) return false;
1925 if (sFileName) SCopy(szSource: fname, sTarget: sFileName);
1926 if (iSize) *iSize = iCurrFileSize;
1927 return true;
1928}
1929
1930bool C4Group::AccessNextEntry(const char *szWildCard,
1931 size_t *iSize, char *sFileName,
1932 bool *fChild)
1933{
1934 char fname[_MAX_FNAME + 1];
1935 if (!FindNextEntry(szWildCard, sFileName: fname, iSize: &iCurrFileSize, fChild)) return false;
1936#ifndef NDEBUG
1937 szCurrAccessedEntry = fname;
1938#endif
1939 bool fResult = SetFilePtr2Entry(szName: fname);
1940#ifndef NDEBUG
1941 szCurrAccessedEntry = nullptr;
1942#endif
1943 if (!fResult) return false;
1944 if (sFileName) SCopy(szSource: fname, sTarget: sFileName);
1945 if (iSize) *iSize = iCurrFileSize;
1946 return true;
1947}
1948
1949bool C4Group::SetFilePtr2Entry(const char *szName, C4Group *pByChild)
1950{
1951 switch (Status)
1952 {
1953 case GRPF_File:
1954 C4GroupEntry *centry;
1955 if (!(centry = GetEntry(szName))) return false;
1956 if (centry->Status != C4GRES_InGroup) return false;
1957 return SetFilePtr(static_cast<size_t>(centry->Offset));
1958
1959 case GRPF_Folder:
1960 StdFile.Close();
1961 char path[_MAX_FNAME + 1]; SCopy(szSource: FileName, sTarget: path, _MAX_FNAME);
1962 AppendBackslash(szFileName: path); SAppend(szSource: szName, szTarget: path);
1963 bool childgroup = C4Group_IsGroup(szFilename: path);
1964 bool fSuccess = StdFile.Open(szFileName: path, fCompressed: !!childgroup);
1965 return fSuccess;
1966 }
1967 return false;
1968}
1969
1970bool C4Group::FindEntry(const char *szWildCard, char *sFileName, size_t *iSize, bool *fChild)
1971{
1972 ResetSearch();
1973 return FindNextEntry(szWildCard, sFileName, iSize, fChild);
1974}
1975
1976bool C4Group::FindNextEntry(const char *szWildCard,
1977 char *sFileName,
1978 size_t *iSize,
1979 bool *fChild,
1980 bool fStartAtFilename)
1981{
1982 C4GroupEntry *centry;
1983 if (!szWildCard) return false;
1984
1985 // Reset search to specified position
1986 if (fStartAtFilename) FindEntry(szWildCard: sFileName);
1987
1988 if (!(centry = SearchNextEntry(szName: szWildCard))) return false;
1989 if (sFileName) SCopy(szSource: centry->FileName, sTarget: sFileName);
1990 if (iSize) *iSize = centry->Size;
1991 if (fChild) *fChild = !!centry->ChildGroup;
1992 return true;
1993}
1994
1995#ifdef _WIN32
1996
1997bool C4Group::Add(const char *szFiles)
1998{
1999 bool fMove = false;
2000
2001 if (StdOutput) std::println("{}...", fMove ? "Moving" : "Adding");
2002
2003 // Add files & directories
2004 char szFileName[_MAX_FNAME + 1];
2005 int iFileCount = 0;
2006 long lAttrib = 0x037; // _A_ALL
2007 struct _finddata_t fdt; intptr_t fdthnd;
2008
2009 // Process segmented path & search wildcards
2010 char cSeparator = (SCharCount(';', szFiles) ? ';' : '|');
2011 for (int cseg = 0; SCopySegment(szFiles, cseg, szFileName, cSeparator, _MAX_FNAME); cseg++)
2012 if ((fdthnd = _findfirst((char *)szFileName, &fdt)) >= 0)
2013 {
2014 do
2015 {
2016 if (fdt.attrib & lAttrib)
2017 {
2018 // ignore
2019 if (fdt.name[0] == '.') continue;
2020 // Compose item path
2021 SCopy(szFiles, szFileName, _MAX_FNAME); *GetFilename(szFileName) = 0;
2022 SAppend(fdt.name, szFileName, _MAX_FNAME);
2023 // File count
2024 iFileCount++;
2025 // Process output & callback
2026 if (StdOutput) std::println("{}", GetFilename(szFileName));
2027 if (fnProcessCallback) fnProcessCallback(GetFilename(szFileName), 0); // cbytes/tbytes
2028 // AddEntryOnDisk
2029 AddEntryOnDisk(szFileName, nullptr, fMove);
2030 }
2031 } while (_findnext(fdthnd, &fdt) == 0);
2032 _findclose(fdthnd);
2033 }
2034
2035 if (StdOutput) std::println("{} file(s) {}.", iFileCount, fMove ? "moved" : "added");
2036
2037 return true;
2038}
2039
2040bool C4Group::Move(const char *szFiles)
2041{
2042 bool fMove = true;
2043
2044 if (StdOutput) std::println("{}...", fMove ? "Moving" : "Adding");
2045
2046 // Add files & directories
2047 char szFileName[_MAX_FNAME + 1];
2048 int iFileCount = 0;
2049 long lAttrib = 0x037; // _A_ALL
2050 struct _finddata_t fdt; intptr_t fdthnd;
2051
2052 // Process segmented path & search wildcards
2053 char cSeparator = (SCharCount(';', szFiles) ? ';' : '|');
2054 for (int cseg = 0; SCopySegment(szFiles, cseg, szFileName, cSeparator, _MAX_FNAME); cseg++)
2055 if ((fdthnd = _findfirst((char *)szFileName, &fdt)) >= 0)
2056 {
2057 do
2058 {
2059 if (fdt.attrib & lAttrib)
2060 {
2061 // ignore
2062 if (fdt.name[0] == '.') continue;
2063 // Compose item path
2064 SCopy(szFiles, szFileName, _MAX_FNAME); *GetFilename(szFileName) = 0;
2065 SAppend(fdt.name, szFileName, _MAX_FNAME);
2066 // File count
2067 iFileCount++;
2068 // Process output & callback
2069 if (StdOutput) std::println("{}", GetFilename(szFileName));
2070 if (fnProcessCallback) fnProcessCallback(GetFilename(szFileName), 0); // cbytes/tbytes
2071 // AddEntryOnDisk
2072 AddEntryOnDisk(szFileName, nullptr, fMove);
2073 }
2074 } while (_findnext(fdthnd, &fdt) == 0);
2075 _findclose(fdthnd);
2076 }
2077
2078 if (StdOutput) std::println("{} file(s) {}.", iFileCount, fMove ? "moved" : "added");
2079
2080 return true;
2081}
2082
2083#endif
2084
2085bool C4Group::Add(const char *szName, void *pBuffer, size_t iSize, bool fChild, bool fHoldBuffer, time_t iTime, bool fExecutable)
2086{
2087 return AddEntry(status: C4GRES_InMemory,
2088 childgroup: fChild,
2089 fname: szName,
2090 size: iSize,
2091 time: iTime ? iTime : time(timer: nullptr),
2092 cCRC: false,
2093 iCRC: 0,
2094 entryname: szName,
2095 membuf: reinterpret_cast<uint8_t *>(pBuffer),
2096 fDeleteOnDisk: false,
2097 fHoldBuffer,
2098 fExecutable);
2099}
2100
2101bool C4Group::Add(const char *szName, StdBuf &pBuffer, bool fChild, bool fHoldBuffer, time_t iTime, bool fExecutable)
2102{
2103 if (!AddEntry(status: C4GRES_InMemory,
2104 childgroup: fChild,
2105 fname: szName,
2106 size: pBuffer.getSize(),
2107 time: iTime ? iTime : time(timer: nullptr),
2108 cCRC: false,
2109 iCRC: 0,
2110 entryname: szName,
2111 membuf: const_cast<uint8_t *>(reinterpret_cast<const uint8_t *>(pBuffer.getData())),
2112 fDeleteOnDisk: false,
2113 fHoldBuffer,
2114 fExecutable,
2115 fBufferIsStdbuf: true)) return false;
2116 // Pointer is now owned and released by C4Group!
2117 if (fHoldBuffer) pBuffer.GrabPointer();
2118 return true;
2119}
2120
2121bool C4Group::Add(const char *szName, StdStrBuf &pBuffer, bool fChild, bool fHoldBuffer, time_t iTime, bool fExecutable)
2122{
2123 if (!AddEntry(status: C4GRES_InMemory,
2124 childgroup: fChild,
2125 fname: szName,
2126 size: pBuffer.getLength(),
2127 time: iTime ? iTime : time(timer: nullptr),
2128 cCRC: false,
2129 iCRC: 0,
2130 entryname: szName,
2131 membuf: const_cast<uint8_t *>(reinterpret_cast<const uint8_t *>(pBuffer.getData())),
2132 fDeleteOnDisk: false,
2133 fHoldBuffer,
2134 fExecutable,
2135 fBufferIsStdbuf: true)) return false;
2136 // Pointer is now owned and released by C4Group!
2137 if (fHoldBuffer) pBuffer.GrabPointer();
2138 return true;
2139}
2140
2141const char *C4Group::GetName()
2142{
2143 return FileName;
2144}
2145
2146int C4Group::EntryCount(const char *szWildCard)
2147{
2148 int fcount;
2149 C4GroupEntry *tentry;
2150 // All files if no wildcard
2151 if (!szWildCard) szWildCard = "*";
2152 // Match wildcard
2153 ResetSearch(); fcount = 0;
2154 while (tentry = SearchNextEntry(szName: szWildCard)) fcount++;
2155 return fcount;
2156}
2157
2158int C4Group::EntrySize(const char *szWildCard)
2159{
2160 int fsize;
2161 C4GroupEntry *tentry;
2162 // All files if no wildcard
2163 if (!szWildCard) szWildCard = "*";
2164 // Match wildcard
2165 ResetSearch(); fsize = 0;
2166 while (tentry = SearchNextEntry(szName: szWildCard))
2167 fsize += tentry->Size;
2168 return fsize;
2169}
2170
2171unsigned int C4Group::EntryCRC32(const char *szWildCard)
2172{
2173 if (!szWildCard) szWildCard = "*";
2174 // iterate thorugh child
2175 C4GroupEntry *pEntry; unsigned int iCRC = 0;
2176 ResetSearch();
2177 while (pEntry = SearchNextEntry(szName: szWildCard))
2178 {
2179 if (!CalcCRC32(pEntry)) return false;
2180 iCRC ^= pEntry->CRC;
2181 }
2182 // return
2183 return iCRC;
2184}
2185
2186uint32_t C4Group::EntryTime(const char *szFilename)
2187{
2188 uint32_t iTime = 0;
2189 switch (Status)
2190 {
2191 case GRPF_File:
2192 C4GroupEntry *pEntry; pEntry = GetEntry(szName: szFilename);
2193 if (pEntry) iTime = pEntry->Time;
2194 break;
2195 case GRPF_Folder:
2196 char szPath[_MAX_FNAME + 1];
2197 FormatWithNull(buf&: szPath, fmt: "{}" DirSep "{}", args: +FileName, args&: szFilename);
2198 iTime = FileTime(fname: szPath);
2199 break;
2200 }
2201 return iTime;
2202}
2203
2204bool C4Group::LoadEntry(const char *szEntryName, char **lpbpBuf, size_t *ipSize, int iAppendZeros)
2205{
2206 size_t size;
2207
2208 // Access entry, allocate buffer, read data
2209 (*lpbpBuf) = nullptr; if (ipSize) *ipSize = 0;
2210 if (!AccessEntry(szWildCard: szEntryName, iSize: &size)) return Error(szStatus: "LoadEntry: Not found");
2211
2212 *lpbpBuf = new char[size + iAppendZeros];
2213 if (!Read(pBuffer: *lpbpBuf, iSize: size))
2214 {
2215 delete[] (*lpbpBuf); *lpbpBuf = nullptr;
2216 return Error(szStatus: "LoadEntry: Reading error");
2217 }
2218
2219 if (ipSize) *ipSize = size;
2220
2221 if (iAppendZeros) std::fill_n(first: *lpbpBuf + size, n: iAppendZeros, value: 0);
2222
2223 return true;
2224}
2225
2226bool C4Group::LoadEntry(const char *szEntryName, StdBuf &Buf)
2227{
2228 size_t size;
2229 // Access entry, allocate buffer, read data
2230 if (!AccessEntry(szWildCard: szEntryName, iSize: &size)) return Error(szStatus: "LoadEntry: Not found");
2231 // Allocate memory
2232 Buf.New(inSize: size);
2233 // Load data
2234 if (!Read(pBuffer: Buf.getMData(), iSize: size))
2235 {
2236 Buf.Clear();
2237 return Error(szStatus: "LoadEntry: Reading error");
2238 }
2239 // ok
2240 return true;
2241}
2242
2243bool C4Group::LoadEntryString(const char *szEntryName, StdStrBuf &Buf)
2244{
2245 size_t size;
2246 // Access entry, allocate buffer, read data
2247 if (!AccessEntry(szWildCard: szEntryName, iSize: &size)) return Error(szStatus: "LoadEntry: Not found");
2248 // Allocate memory
2249 Buf.SetLength(size);
2250 // other parts crash when they get a zero length buffer, so fail here
2251 if (!size) return false;
2252 // Load data
2253 if (!Read(pBuffer: Buf.getMData(), iSize: size))
2254 {
2255 Buf.Clear();
2256 return Error(szStatus: "LoadEntry: Reading error");
2257 }
2258 // ok
2259 return true;
2260}
2261
2262void C4Group::SetMaker(const char *szMaker)
2263{
2264 if (!SEqual(szStr1: szMaker, szStr2: Head.Maker)) Modified = true;
2265 SCopy(szSource: szMaker, sTarget: Head.Maker, iMaxL: C4GroupMaxMaker);
2266}
2267
2268const char *C4Group::GetMaker()
2269{
2270 return Head.Maker;
2271}
2272
2273const char *C4Group::GetPassword()
2274{
2275 return Head.Password;
2276}
2277
2278int SortRank(const char *szElement, const char *szSortList)
2279{
2280 int cnt;
2281 char csegment[_MAX_FNAME + 1];
2282
2283 for (cnt = 0; SCopySegment(fstr: szSortList, segn: cnt, tstr: csegment, sepa: '|', _MAX_FNAME); cnt++)
2284 if (WildcardMatch(szFName1: csegment, szFName2: szElement))
2285 return (SCharCount(cTarget: '|', szInStr: szSortList) + 1) - cnt;
2286
2287 return 0;
2288}
2289
2290bool C4Group::Sort(const char *szSortList)
2291{
2292 bool fBubble;
2293 C4GroupEntry *centry, *prev, *next, *nextnext;
2294
2295 if (!szSortList || !szSortList[0]) return false;
2296
2297 if (StdOutput) std::println(fmt: "Sorting...");
2298
2299 do
2300 {
2301 fBubble = false;
2302
2303 for (prev = nullptr, centry = FirstEntry; centry; prev = centry, centry = next)
2304 if (next = centry->Next)
2305 {
2306 // primary sort by file list
2307 int iS1 = SortRank(szElement: centry->FileName, szSortList);
2308 int iS2 = SortRank(szElement: next->FileName, szSortList);
2309 if (iS1 > iS2) continue;
2310 // secondary sort by filename
2311 if (iS1 == iS2)
2312 if (stricmp(s1: centry->FileName, s2: next->FileName) <= 0) continue;
2313 // wrong order: Swap!
2314 nextnext = next->Next;
2315 if (prev) prev->Next = next;
2316 else FirstEntry = next;
2317 next->Next = centry;
2318 centry->Next = nextnext;
2319 next = nextnext;
2320
2321 fBubble = true;
2322 Modified = true;
2323 }
2324 } while (fBubble);
2325
2326 return true;
2327}
2328
2329C4Group *C4Group::GetMother()
2330{
2331 return Mother;
2332}
2333
2334int C4Group::GetStatus()
2335{
2336 return Status;
2337}
2338
2339bool C4Group::CloseExclusiveMother()
2340{
2341 if (Mother && ExclusiveChild)
2342 {
2343 Mother->Close();
2344 delete Mother;
2345 Mother = nullptr;
2346 return true;
2347 }
2348 return false;
2349}
2350
2351int32_t C4Group::GetCreation()
2352{
2353 return Head.Creation;
2354}
2355
2356bool C4Group::SortByList(const char **ppSortList, const char *szFilename)
2357{
2358 // No sort list specified
2359 if (!ppSortList) return false;
2360 // No group name specified, use own
2361 if (!szFilename) szFilename = FileName;
2362 szFilename = GetFilename(path: szFilename);
2363 // Find matching filename entry in sort list
2364 const char **ppListEntry;
2365 for (ppListEntry = ppSortList; *ppListEntry; ppListEntry += 2)
2366 if (WildcardMatch(szFName1: *ppListEntry, szFName2: szFilename))
2367 break;
2368 // Sort by sort list entry
2369 if (*ppListEntry && *(ppListEntry + 1))
2370 Sort(szSortList: *(ppListEntry + 1));
2371 // Success
2372 return true;
2373}
2374
2375bool C4Group::EnsureChildFilePtr(C4Group *pChild)
2376{
2377 // group file
2378 if (Status == GRPF_File)
2379 {
2380 if (Mother && !Mother->EnsureChildFilePtr(pChild: this))
2381 return false;
2382 // check if FilePtr has to be moved
2383 if (FilePtr != pChild->MotherOffset + pChild->EntryOffset + pChild->FilePtr)
2384 // move it to the position the child thinks it is
2385 if (!SetFilePtr(pChild->MotherOffset + pChild->EntryOffset + pChild->FilePtr))
2386 return false;
2387 // ok
2388 return true;
2389 }
2390
2391 // Open standard file is not the child file ...or StdFile ptr does not match pChild->FilePtr
2392 char szChildPath[_MAX_PATH + 1];
2393 FormatWithNull(buf&: szChildPath, fmt: "{}" DirSep "{}", args: +FileName, args: GetFilename(path: pChild->FileName));
2394 if (!ItemIdentical(szFilename1: StdFile.Name, szFilename2: szChildPath))
2395 {
2396 // Reopen correct child stdfile
2397 if (!SetFilePtr2Entry(szName: GetFilename(path: pChild->FileName)))
2398 return false;
2399 // Advance to child's old file ptr
2400 if (!AdvanceFilePtr(iOffset: pChild->EntryOffset + pChild->FilePtr))
2401 return false;
2402 }
2403
2404 // Looks okay
2405 return true;
2406}
2407
2408StdStrBuf C4Group::GetFullName() const
2409{
2410 char str[_MAX_PATH + 1]; *str = '\0';
2411 char sep[] = "/"; sep[0] = DirectorySeparator;
2412 for (const C4Group *pGroup = this; pGroup; pGroup = pGroup->Mother)
2413 {
2414 if (*str) SInsert(szString: str, szInsert: sep, iPosition: 0, _MAX_PATH);
2415 SInsert(szString: str, szInsert: pGroup->FileName, iPosition: 0, _MAX_PATH);
2416 if (pGroup->Status == GRPF_Folder) break; // Folder is assumed to have full path
2417 }
2418 StdStrBuf sResult; sResult.Copy(pnData: str);
2419 return sResult;
2420}
2421
2422void C4Group::MakeOriginal(bool fOriginal)
2423{
2424 Modified = true;
2425 if (fOriginal) { Head.Original = 1234567; MadeOriginal = true; }
2426 else { Head.Original = 0; MadeOriginal = false; }
2427}
2428
2429bool C4Group::GetOriginal()
2430{
2431 return (Head.Original == 1234567);
2432}
2433
2434bool C4Group::CalcCRC32(C4GroupEntry *pEntry)
2435{
2436 // checksum already calculated?
2437 if (pEntry->HasCRC == C4GECS_New)
2438 return true;
2439 // child group?
2440 if (pEntry->ChildGroup || (pEntry->Status == C4GRES_OnDisk && (DirectoryExists(szFileName: pEntry->DiskPath) || C4Group_IsGroup(szFilename: pEntry->DiskPath))))
2441 {
2442 // open
2443 C4Group Child;
2444 switch (pEntry->Status)
2445 {
2446 case C4GRES_InGroup:
2447 if (!Child.OpenAsChild(pMother: this, szEntryName: pEntry->FileName))
2448 return 0;
2449 break;
2450 case C4GRES_OnDisk:
2451 if (!Child.Open(szGroupName: pEntry->DiskPath))
2452 return 0;
2453 break;
2454 default:
2455 return 0;
2456 }
2457 // get checksum
2458 pEntry->CRC = Child.EntryCRC32();
2459 }
2460 else if (!pEntry->Size)
2461 pEntry->CRC = 0;
2462 else
2463 {
2464 // file checksum already calculated?
2465 if (pEntry->HasCRC != C4GECS_Old)
2466 {
2467 uint8_t *pData = nullptr; bool fOwnData; CStdFile f;
2468 // get data
2469 switch (pEntry->Status)
2470 {
2471 case C4GRES_InGroup:
2472 // create buffer
2473 pData = new uint8_t[pEntry->Size]; fOwnData = true;
2474 // go to entry
2475 if (!SetFilePtr2Entry(szName: pEntry->FileName)) { delete[] pData; return false; }
2476 // read
2477 if (!Read(pBuffer: pData, iSize: pEntry->Size)) { delete[] pData; return false; }
2478 break;
2479 case C4GRES_OnDisk:
2480 // create buffer
2481 pData = new uint8_t[pEntry->Size]; fOwnData = true;
2482 // open
2483 if (!f.Open(szFileName: pEntry->DiskPath)) { delete[] pData; return false; }
2484 // read
2485 if (!f.Read(pBuffer: pData, iSize: pEntry->Size)) { delete[] pData; return false; }
2486 break;
2487 case C4GRES_InMemory:
2488 // set
2489 pData = pEntry->bpMemBuf; fOwnData = false;
2490 break;
2491 default:
2492 return false;
2493 }
2494 if (!pData) return false;
2495 // calc crc
2496 pEntry->CRC = crc32(crc: 0, buf: pData, len: pEntry->Size);
2497 // discard buffer
2498 if (fOwnData) delete[] pData;
2499 }
2500 // add file name
2501 pEntry->CRC = crc32(crc: pEntry->CRC, buf: reinterpret_cast<uint8_t *>(pEntry->FileName), len: checked_cast<unsigned int>(from: SLen(sptr: pEntry->FileName)));
2502 }
2503 // set flag
2504 pEntry->HasCRC = C4GECS_New;
2505 // ok
2506 return true;
2507}
2508
2509bool C4Group::OpenChild(const char *strEntry)
2510{
2511 // hack: The seach-handle would be closed twice otherwise
2512 FolderSearch.Reset();
2513 // Create a memory copy of ourselves
2514 C4Group *pOurselves = new C4Group;
2515 *pOurselves = *this;
2516
2517 // Open a child from the memory copy
2518 C4Group hChild;
2519 if (!hChild.OpenAsChild(pMother: pOurselves, szEntryName: strEntry, fExclusive: false))
2520 {
2521 // Silently delete our memory copy
2522 pOurselves->Default(); delete pOurselves;
2523 return false;
2524 }
2525
2526 // hack: The seach-handle would be closed twice otherwise
2527 FolderSearch.Reset();
2528 hChild.FolderSearch.Reset();
2529
2530 // We now become our own child
2531 *this = hChild;
2532
2533 // Make ourselves exclusive (until we hit our memory copy parent)
2534 for (C4Group *pGroup = this; pGroup != pOurselves; pGroup = pGroup->Mother)
2535 pGroup->ExclusiveChild = true;
2536
2537 // Reset the temporary child variable so it doesn't delete anything
2538 hChild.Default();
2539
2540 // Yeehaw
2541 return true;
2542}
2543
2544bool C4Group::OpenMother()
2545{
2546 // This only works if we are an exclusive child
2547 if (!Mother || !ExclusiveChild) return false;
2548
2549 // Store a pointer to our mother
2550 C4Group *pMother = Mother;
2551
2552 // Clear ourselves without deleting our mother
2553 ExclusiveChild = false;
2554 Clear();
2555
2556 // hack: The seach-handle would be closed twice otherwise
2557 pMother->FolderSearch.Reset();
2558 FolderSearch.Reset();
2559 // We now become our own mother (whoa!)
2560 *this = *pMother;
2561
2562 // Now silently delete our former mother
2563 pMother->Default();
2564 delete pMother;
2565
2566 // Yeehaw
2567 return true;
2568}
2569
2570#ifndef NDEBUG
2571void C4Group::PrintInternals(const char *szIndent)
2572{
2573 if (!szIndent) szIndent = "";
2574 std::println("{}Head.id: '{}'", szIndent, +Head.id);
2575 std::println("{}Head.Ver1: {}", szIndent, Head.Ver1);
2576 std::println("{}Head.Ver2: {}", szIndent, Head.Ver2);
2577 std::println("{}Head.Entries: {}", szIndent, Head.Entries);
2578 std::println("{}Head.Maker: '{}'", szIndent, +Head.Maker);
2579 std::println("{}Head.Creation: {}", szIndent, Head.Creation);
2580 std::println("{}Head.Original: {}", szIndent, Head.Original);
2581 for (C4GroupEntry *p = FirstEntry; p; p = p->Next)
2582 {
2583 std::println("{}Entry '{}':", szIndent, +p->FileName);
2584 std::println("{} Packed: {}", szIndent, p->Packed);
2585 std::println("{} ChildGroup: {}", szIndent, p->ChildGroup);
2586 std::println("{} Size: {}", szIndent, p->Size);
2587 std::println("{} __Unused: {}", szIndent, p->__Unused);
2588 std::println("{} Offset: {}", szIndent, p->Offset);
2589 std::println("{} Time: {}", szIndent, p->Time);
2590 std::println("{} HasCRC: {}", szIndent, p->HasCRC);
2591 std::println("{} CRC: {:08X}", szIndent, p->CRC);
2592 if (p->ChildGroup)
2593 {
2594 C4Group hChildGroup;
2595 if (hChildGroup.OpenAsChild(this, p->FileName))
2596 hChildGroup.PrintInternals(std::format("{}{}", szIndent, " ").c_str());
2597 }
2598 }
2599}
2600#endif
2601