openclonk/src/c4group/C4Group.cpp

2230 lines
58 KiB
C++

/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 1998-2000, Matthes Bender
* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
* Copyright (c) 2009-2016, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/
/* Handles group files */
/* Needs to be compilable as Objective C++ on OS X */
#include "C4Include.h"
#include "c4group/C4Group.h"
#include "c4group/C4Components.h"
#include "lib/C4InputValidation.h"
#include <zlib.h>
//------------------------------ File Sort Lists -------------------------------------------
const char *C4CFN_FLS[] =
{
C4CFN_System, C4FLS_System,
C4CFN_Material, C4FLS_Material,
C4CFN_Graphics, C4FLS_Graphics,
C4CFN_DefFiles, C4FLS_Def,
C4CFN_PlayerFiles, C4FLS_Player,
C4CFN_ObjectInfoFiles, C4FLS_Object,
C4CFN_ScenarioFiles, C4FLS_Scenario,
C4CFN_FolderFiles, C4FLS_Folder,
C4CFN_ScenarioSections, C4FLS_Section,
C4CFN_Sound, C4FLS_Sound,
C4CFN_Music, C4FLS_Music,
NULL, NULL
};
#ifdef _DEBUG
const char *szCurrAccessedEntry = NULL;
int iC4GroupRewindFilePtrNoWarn=0;
#endif
#ifdef _DEBUG
//#define C4GROUP_DUMP_ACCESS
#endif
//---------------------------- Global C4Group_Functions -------------------------------------------
char C4Group_TempPath[_MAX_PATH+1]="";
char C4Group_Ignore[_MAX_PATH+1]="cvs;CVS;Thumbs.db;.orig;.svn";
const char **C4Group_SortList = NULL;
bool (*C4Group_ProcessCallback)(const char *, int)=NULL;
void C4Group_SetProcessCallback(bool (*fnCallback)(const char *, int))
{
C4Group_ProcessCallback = fnCallback;
}
void C4Group_SetSortList(const char **ppSortList)
{
C4Group_SortList = ppSortList;
}
void C4Group_SetTempPath(const char *szPath)
{
if (!szPath || !szPath[0]) C4Group_TempPath[0]=0;
else { SCopy(szPath,C4Group_TempPath,_MAX_PATH); AppendBackslash(C4Group_TempPath); }
}
const char *C4Group_GetTempPath()
{
return C4Group_TempPath;
}
bool C4Group_TestIgnore(const char *szFilename)
{
if(!*szFilename) return true; //poke out empty strings
const char* name = GetFilename(szFilename);
return *name == '.' //no hidden files and the directory itself
|| name[strlen(name) - 1] == '~' //no temp files
|| SIsModule(C4Group_Ignore,name); //not on Blacklist
}
bool C4Group_IsGroup(const char *szFilename)
{
C4Group hGroup; if (hGroup.Open(szFilename)) { hGroup.Close(); return true; }
return false;
}
bool C4Group_CopyItem(const char *szSource, const char *szTarget1, bool fNoSort, bool fResetAttributes)
{
// Parameter check
if (!szSource || !szTarget1 || !szSource[0] || !szTarget1[0]) return false;
char szTarget[_MAX_PATH+1]; SCopy(szTarget1,szTarget,_MAX_PATH);
// Backslash terminator indicates target is a path only (append filename)
if (szTarget[SLen(szTarget)-1]==DirectorySeparator) SAppend(GetFilename(szSource),szTarget);
// Check for identical source and target
// Note that attributes aren't reset here
if (ItemIdentical(szSource,szTarget)) return true;
// Source and target are simple items
if (ItemExists(szSource) && CreateItem(szTarget)) return CopyItem(szSource,szTarget, fResetAttributes);
// For items within groups, attribute resetting isn't needed, because packing/unpacking will kill all
// attributes anyway
// Source & target
C4Group hSourceParent, hTargetParent;
char szSourceParentPath[_MAX_PATH+1],szTargetParentPath[_MAX_PATH+1];
GetParentPath(szSource,szSourceParentPath); GetParentPath(szTarget,szTargetParentPath);
// Temp filename
char szTempFilename[_MAX_PATH+1];
SCopy(C4Group_TempPath,szTempFilename,_MAX_PATH);
SAppend(GetFilename(szSource),szTempFilename);
MakeTempFilename(szTempFilename);
// Extract source to temp file
if ( !hSourceParent.Open(szSourceParentPath)
|| !hSourceParent.Extract(GetFilename(szSource),szTempFilename)
|| !hSourceParent.Close() ) return false;
// Move temp file to target
if ( !hTargetParent.Open(szTargetParentPath)
|| !hTargetParent.SetNoSort(fNoSort)
|| !hTargetParent.Move(szTempFilename, GetFilename(szTarget))
|| !hTargetParent.Close() ) { EraseItem(szTempFilename); return false; }
return true;
}
bool C4Group_MoveItem(const char *szSource, const char *szTarget1, bool fNoSort)
{
// Parameter check
if (!szSource || !szTarget1 || !szSource[0] || !szTarget1[0]) return false;
char szTarget[_MAX_PATH+1]; SCopy(szTarget1,szTarget,_MAX_PATH);
// Backslash terminator indicates target is a path only (append filename)
if (szTarget[SLen(szTarget)-1]==DirectorySeparator) SAppend(GetFilename(szSource),szTarget);
// Check for identical source and target
if (ItemIdentical(szSource,szTarget)) return true;
// Source and target are simple items
if (ItemExists(szSource) && CreateItem(szTarget))
{
// erase test file, because it may block moving a directory
EraseItem(szTarget);
return MoveItem(szSource,szTarget);
}
// Source & target
C4Group hSourceParent, hTargetParent;
char szSourceParentPath[_MAX_PATH+1],szTargetParentPath[_MAX_PATH+1];
GetParentPath(szSource,szSourceParentPath); GetParentPath(szTarget,szTargetParentPath);
// Temp filename
char szTempFilename[_MAX_PATH+1];
SCopy(C4Group_TempPath,szTempFilename,_MAX_PATH);
SAppend(GetFilename(szSource),szTempFilename);
MakeTempFilename(szTempFilename);
// Extract source to temp file
if ( !hSourceParent.Open(szSourceParentPath)
|| !hSourceParent.Extract(GetFilename(szSource),szTempFilename)
|| !hSourceParent.Close() ) return false;
// Move temp file to target
if ( !hTargetParent.Open(szTargetParentPath)
|| !hTargetParent.SetNoSort(fNoSort)
|| !hTargetParent.Move(szTempFilename, GetFilename(szTarget))
|| !hTargetParent.Close() ) { EraseItem(szTempFilename); return false; }
// Delete original file
if ( !hSourceParent.Open(szSourceParentPath)
|| !hSourceParent.DeleteEntry(GetFilename(szSource))
|| !hSourceParent.Close() ) return false;
return true;
}
bool C4Group_DeleteItem(const char *szItem, bool fRecycle)
{
// Parameter check
if (!szItem || !szItem[0]) return false;
// simple item?
if (ItemExists(szItem))
{
if (fRecycle)
return EraseItemSafe(szItem);
else
return EraseItem(szItem);
}
// delete from parent
C4Group hParent;
char szParentPath[_MAX_PATH+1];
GetParentPath(szItem,szParentPath);
// Delete original file
if ( !hParent.Open(szParentPath)
|| !hParent.DeleteEntry(GetFilename(szItem), fRecycle)
|| !hParent.Close() ) return false;
return true;
}
bool C4Group_PackDirectoryTo(const char *szFilename, const char *szFilenameTo)
{
// Check file type
if (!DirectoryExists(szFilename)) return false;
// Target mustn't exist
if (FileExists(szFilenameTo)) return false;
// Ignore
if (C4Group_TestIgnore(szFilename))
return true;
// Process message
if (C4Group_ProcessCallback)
C4Group_ProcessCallback(szFilename,0);
// Create group file
C4Group hGroup;
if (!hGroup.Open(szFilenameTo,true))
return false;
// Add folder contents to group
DirectoryIterator i(szFilename);
for (; *i; i++)
{
// Ignore
if (C4Group_TestIgnore(*i))
continue;
// Must pack?
if (DirectoryExists(*i))
{
// Find temporary filename
char szTempFilename[_MAX_PATH+1];
// At C4Group temp path
SCopy(C4Group_TempPath, szTempFilename, _MAX_PATH);
SAppend(GetFilename(*i), szTempFilename, _MAX_PATH);
// Make temporary filename
MakeTempFilename(szTempFilename);
// Pack and move into group
if ( !C4Group_PackDirectoryTo(*i, szTempFilename)) break;
if (!hGroup.Move(szTempFilename, GetFilename(*i)))
{
EraseFile(szTempFilename);
break;
}
}
// Add normally otherwise
else if (!hGroup.Add(*i, NULL))
break;
}
// Something went wrong?
if (*i)
{
// Close group and remove temporary file
hGroup.Close();
EraseItem(szFilenameTo);
return false;
}
// Reset iterator
i.Reset();
// Close group
hGroup.SortByList(C4Group_SortList,szFilename);
if (!hGroup.Close())
return false;
// Done
return true;
}
bool C4Group_PackDirectory(const char *szFilename)
{
// Make temporary filename
char szTempFilename[_MAX_PATH+1];
SCopy(szFilename, szTempFilename, _MAX_PATH);
MakeTempFilename(szTempFilename);
// Pack directory
if (!C4Group_PackDirectoryTo(szFilename, szTempFilename))
return false;
// Rename folder
char szTempFilename2[_MAX_PATH+1];
SCopy(szFilename, szTempFilename2, _MAX_PATH);
MakeTempFilename(szTempFilename2);
if (!RenameFile(szFilename, szTempFilename2))
return false;
// Name group file
if (!RenameFile(szTempFilename,szFilename))
return false;
// Last: Delete folder
return EraseDirectory(szTempFilename2);
}
bool C4Group_UnpackDirectory(const char *szFilename)
{
// Already unpacked: success
if (DirectoryExists(szFilename)) return true;
// Not a real file: unpack parent directory first
char szParentFilename[_MAX_PATH+1];
if (!FileExists(szFilename))
if (GetParentPath(szFilename,szParentFilename))
if (!C4Group_UnpackDirectory(szParentFilename))
return false;
// Open group
C4Group hGroup;
if (!hGroup.Open(szFilename)) return false;
// Process message
if (C4Group_ProcessCallback)
C4Group_ProcessCallback(szFilename,0);
// Create target directory
char szFoldername[_MAX_PATH+1];
SCopy(szFilename,szFoldername,_MAX_PATH);
MakeTempFilename(szFoldername);
if (!CreatePath(szFoldername)) { hGroup.Close(); return false; }
// Extract files to folder
if (!hGroup.Extract("*",szFoldername)) { hGroup.Close(); return false; }
// Close group
hGroup.Close();
// Rename group file
char szTempFilename[_MAX_PATH+1];
SCopy(szFilename,szTempFilename,_MAX_PATH);
MakeTempFilename(szTempFilename);
if (!RenameFile(szFilename, szTempFilename)) return false;
// Rename target directory
if (!RenameFile(szFoldername,szFilename)) return false;
// Delete renamed group file
return EraseItem(szTempFilename);
}
bool C4Group_ExplodeDirectory(const char *szFilename)
{
// Ignore
if (C4Group_TestIgnore(szFilename)) return true;
// Unpack this directory
if (!C4Group_UnpackDirectory(szFilename)) return false;
// Explode all children
ForEachFile(szFilename,C4Group_ExplodeDirectory);
// Success
return true;
}
bool C4Group_ReadFile(const char *szFile, char **pData, size_t *iSize)
{
// security
if (!szFile || !pData) return false;
// get mother path & file name
char szPath[_MAX_PATH + 1];
GetParentPath(szFile, szPath);
const char *pFileName = GetFilename(szFile);
// open mother group
C4Group MotherGroup;
if (!MotherGroup.Open(szPath)) return false;
// access the file
size_t iFileSize;
if (!MotherGroup.AccessEntry(pFileName, &iFileSize)) return false;
// create buffer
*pData = new char [iFileSize];
// read it
if (!MotherGroup.Read(*pData, iFileSize)) { delete [] *pData; *pData = NULL; return false; }
// ok
MotherGroup.Close();
if (iSize) *iSize = iFileSize;
return true;
}
void MemScramble(BYTE *bypBuffer, int iSize)
{
int cnt; BYTE temp;
// XOR deface
for (cnt=0; cnt<iSize; cnt++)
bypBuffer[cnt] ^= 237;
// BYTE swap
for (cnt=0; cnt+2<iSize; cnt+=3)
{
temp = bypBuffer[cnt];
bypBuffer[cnt] = bypBuffer[cnt+2];
bypBuffer[cnt+2] = temp;
}
}
//---------------------------------- C4Group ---------------------------------------------
C4GroupEntry::~C4GroupEntry()
{
if (HoldBuffer)
if (bpMemBuf)
{
if (BufferIsStdbuf)
StdBuf::DeletePointer(bpMemBuf);
else
delete [] bpMemBuf;
}
}
void C4GroupEntry::Set(const DirectoryIterator &iter, const char * path)
{
InplaceReconstruct(this);
SCopy(GetFilename(*iter),FileName,_MAX_FNAME);
SCopy(*iter, DiskPath, _MAX_PATH-1);
Size = FileSize(*iter);
Status=C4GRES_OnDisk;
Packed=false;
ChildGroup=false;
// Notice folder entries are not checked for ChildGroup status.
// This would cause extreme performance loss and be good for
// use in entry list display only.
}
C4Group::C4Group()
{
Init();
StdOutput=false;
fnProcessCallback=NULL;
NoSort=false;
}
void C4Group::Init()
{
// General
Status=GRPF_Inactive;
FileName[0]=0;
// Child status
Mother=NULL;
ExclusiveChild=false;
// File only
FilePtr=0;
EntryOffset=0;
Modified=false;
InplaceReconstruct(&Head);
FirstEntry=NULL;
SearchPtr=NULL;
pInMemEntry=NULL; iInMemEntrySize=0u;
// Folder only
FolderSearch.Clear();
// Error status
SCopy("No Error",ErrorString,C4GroupMaxError);
}
C4Group::~C4Group()
{
Clear();
}
bool C4Group::Error(const char *szStatus)
{
SCopy(szStatus,ErrorString,C4GroupMaxError);
return false;
}
const char *C4Group::GetError()
{
return ErrorString;
}
void C4Group::SetStdOutput(bool fStatus)
{
StdOutput=fStatus;
}
bool C4Group::Open(const char *szGroupName, bool fCreate)
{
if (!szGroupName) return Error("Open: Null filename");
if (!szGroupName[0]) return Error("Open: Empty filename");
char szGroupNameN[_MAX_FNAME];
SCopy(szGroupName,szGroupNameN,_MAX_FNAME);
// Convert to native path
SReplaceChar(szGroupNameN, '\\', DirectorySeparator);
// Real reference
if (FileExists(szGroupNameN))
{
// Init
Init();
// Open group or folder
return OpenReal(szGroupNameN);
}
// If requested, try creating a new group file
if (fCreate)
{
CStdFile temp;
if (temp.Create(szGroupNameN,false))
{
// Temporary file has been created
temp.Close();
// Init
Init();
Status=GRPF_File; Modified=true;
SCopy(szGroupNameN,FileName,_MAX_FNAME);
return true;
}
}
// While not a real reference (child group), trace back to mother group or folder.
// Open mother and child in exclusive mode.
char szRealGroup[_MAX_FNAME];
SCopy(szGroupNameN,szRealGroup,_MAX_FNAME);
do
{ if (!TruncatePath(szRealGroup)) return Error(FormatString("Open(\"%s\"): File not found", szGroupNameN).getData()); }
while (!FileExists(szRealGroup));
// Open mother and child in exclusive mode
C4Group *pMother = new C4Group;
pMother->SetStdOutput(StdOutput);
if (!pMother->Open(szRealGroup))
{ Clear(); Error(pMother->ErrorString); delete pMother; return false; }
if (!OpenAsChild(pMother,szGroupNameN+SLen(szRealGroup)+1,true))
{ Clear(); return false; }
// Success
return true;
}
bool C4Group::OpenReal(const char *szFilename)
{
// Get original filename
if (!szFilename) return false;
SCopy(szFilename,FileName,_MAX_FNAME);
// Folder
if (DirectoryExists(FileName))
{
// Ignore
if (C4Group_TestIgnore(szFilename))
return Error(FormatString("OpenReal: filename '%s' ignored", szFilename).getData());
// OpenReal: Simply set status and return
Status=GRPF_Folder;
ResetSearch();
// Success
return true;
}
// File: Try reading header and entries
if (OpenRealGrpFile())
{
Status=GRPF_File;
ResetSearch();
return true;
}
else
return false;
return Error("OpenReal: Not a valid group");
}
bool C4Group::OpenRealGrpFile()
{
int cnt,file_entries;
C4GroupEntryCore corebuf;
// Open StdFile
if (!StdFile.Open(FileName,true)) return Error("OpenRealGrpFile: Cannot open standard file");
// Read header
if (!StdFile.Read((BYTE*)&Head,sizeof(C4GroupHeader))) return Error("OpenRealGrpFile: Error reading header");
MemScramble((BYTE*)&Head,sizeof(C4GroupHeader));
EntryOffset+=sizeof(C4GroupHeader);
// Check Header
if (!SEqual(Head.id,C4GroupFileID)
|| (Head.Ver1!=C4GroupFileVer1) || (Head.Ver2>C4GroupFileVer2))
return Error("OpenRealGrpFile: Invalid header");
// Read Entries
file_entries=Head.Entries;
Head.Entries=0; // Reset, will be recounted by AddEntry
for (cnt=0; cnt<file_entries; cnt++)
{
if (!StdFile.Read((BYTE*)&corebuf,sizeof(C4GroupEntryCore))) return Error("OpenRealGrpFile: Error reading entries");
// New C4Groups have filenames in UTF-8
StdStrBuf entryname(corebuf.FileName);
entryname.EnsureUnicode();
// Prevent overwriting of user stuff by malicuous groups
C4InVal::ValidateFilename(const_cast<char *>(entryname.getData()),entryname.getLength());
EntryOffset+=sizeof(C4GroupEntryCore);
if (!AddEntry(C4GroupEntry::C4GRES_InGroup,!!corebuf.ChildGroup,
corebuf.FileName,corebuf.Size,
entryname.getData(),
NULL, false, false,
!!corebuf.Executable))
return Error("OpenRealGrpFile: Cannot add entry");
}
return true;
}
bool C4Group::AddEntry(C4GroupEntry::EntryStatus status,
bool childgroup,
const char *fname,
long size,
const char *entryname,
BYTE *membuf,
bool fDeleteOnDisk,
bool fHoldBuffer,
bool fExecutable,
bool fBufferIsStdbuf)
{
// Folder: add file to folder immediately
if (Status==GRPF_Folder)
{
// Close open StdFile
StdFile.Close();
// Get path to target folder file
char tfname[_MAX_FNAME];
SCopy(FileName,tfname,_MAX_FNAME);
AppendBackslash(tfname);
if (entryname) SAppend(entryname,tfname);
else SAppend(GetFilename(fname),tfname);
switch (status)
{
case C4GroupEntry::C4GRES_OnDisk: // Copy/move file to folder
if (!CopyItem(fname, tfname))
return false;
// Reset directory iterator to reflect new file
ResetSearch(true);
if (fDeleteOnDisk && !EraseItem(fname))
return false;
return true;
case C4GroupEntry::C4GRES_InMemory: { // Save buffer to file in folder
CStdFile hFile;
bool fOkay = false;
if (hFile.Create(tfname, !!childgroup))
fOkay = !!hFile.Write(membuf,size);
hFile.Close();
ResetSearch(true);
if (fHoldBuffer) { if (fBufferIsStdbuf) StdBuf::DeletePointer(membuf); else delete [] membuf; }
return fOkay;
}
default: break; // InGrp & Deleted ignored
}
return Error("Add to folder: Invalid request");
}
// Group file: add to virtual entry list
C4GroupEntry *nentry,*lentry,*centry;
// Delete existing entries of same name
centry=GetEntry(GetFilename(entryname ? entryname : fname));
if (centry) { centry->Status = C4GroupEntry::C4GRES_Deleted; Head.Entries--; }
// Allocate memory for new entry
nentry = new C4GroupEntry;
// Find end of list
for (lentry=FirstEntry; lentry && lentry->Next; lentry=lentry->Next) {}
// Init entry core data
if (entryname) SCopy(entryname,nentry->FileName,_MAX_FNAME);
else SCopy(GetFilename(fname),nentry->FileName,_MAX_FNAME);
nentry->Size=size;
nentry->ChildGroup=childgroup;
nentry->Offset=0;
nentry->Executable=fExecutable;
nentry->DeleteOnDisk=fDeleteOnDisk;
nentry->HoldBuffer=fHoldBuffer;
nentry->BufferIsStdbuf=fBufferIsStdbuf;
if (lentry) nentry->Offset=lentry->Offset+lentry->Size;
// Init list entry data
SCopy(fname,nentry->DiskPath,_MAX_FNAME);
nentry->Status=status;
nentry->bpMemBuf=membuf;
nentry->Next=NULL;
nentry->NoSort = NoSort;
// Append entry to list
if (lentry) lentry->Next=nentry;
else FirstEntry=nentry;
// Increase virtual file count of group
Head.Entries++;
return true;
}
C4GroupEntry* C4Group::GetEntry(const char *szName)
{
if (Status==GRPF_Folder) return NULL;
C4GroupEntry *centry;
for (centry=FirstEntry; centry; centry=centry->Next)
if (centry->Status != C4GroupEntry::C4GRES_Deleted)
if (WildcardMatch(szName,centry->FileName))
return centry;
return NULL;
}
bool C4Group::Close()
{
C4GroupEntry *centry;
bool fRewrite=false;
if (Status==GRPF_Inactive) return false;
// Folder: just close
if (Status==GRPF_Folder)
{ CloseExclusiveMother(); Clear(); return true; }
// Rewrite check
for (centry=FirstEntry; centry; centry=centry->Next)
if (centry->Status != C4GroupEntry::C4GRES_InGroup)
fRewrite=true;
if (Modified) fRewrite=true;
// No rewrite: just close
if (!fRewrite)
{ CloseExclusiveMother(); Clear(); return true; }
if (StdOutput) printf("Writing group file...\n");
// Set new version
Head.Ver1=C4GroupFileVer1;
Head.Ver2=C4GroupFileVer2;
// Automatic sort
SortByList(C4Group_SortList);
// Save group contents to disk
bool fSuccess = Save(false);
// Close exclusive mother
CloseExclusiveMother();
// Close file
Clear();
return !!fSuccess;
}
bool C4Group::Save(bool fReOpen)
{
int cscore;
C4GroupEntryCore *save_core;
C4GroupEntry *centry;
char szTempFileName[_MAX_FNAME+1],szGrpFileName[_MAX_FNAME+1];
// Create temporary core list with new actual offsets to be saved
int32_t iContentsSize = 0;
save_core = new C4GroupEntryCore [Head.Entries];
cscore=0;
for (centry=FirstEntry; centry; centry=centry->Next)
if (centry->Status != C4GroupEntry::C4GRES_Deleted)
{
save_core[cscore]=(C4GroupEntryCore)*centry;
// Make actual offset
save_core[cscore].Offset = iContentsSize;
iContentsSize += centry->Size;
cscore++;
}
// Hold contents in memory?
bool fToMemory = !fReOpen && Mother && iContentsSize < C4GroupSwapThreshold;
if (!fToMemory)
{
// Create target temp file (in temp directory!)
SCopy(FileName,szGrpFileName,_MAX_FNAME);
if (C4Group_TempPath[0]) { SCopy(C4Group_TempPath,szTempFileName,_MAX_FNAME); SAppend(GetFilename(FileName),szTempFileName,_MAX_FNAME); }
else SCopy(FileName,szTempFileName,_MAX_FNAME);
MakeTempFilename(szTempFileName);
// (Temp file must not have the same name as the group.)
if (SEqual(szTempFileName,szGrpFileName))
{
SAppend(".tmp",szTempFileName); // Add a second temp extension
MakeTempFilename(szTempFileName);
}
}
// Create the new (temp) group file
CStdFile tfile;
if (!tfile.Create(szTempFileName,true,false,fToMemory))
{ delete [] save_core; return Error("Close: ..."); }
// Save header and core list
C4GroupHeader headbuf = Head;
MemScramble((BYTE*)&headbuf,sizeof(C4GroupHeader));
if (!tfile.Write((BYTE*)&headbuf,sizeof(C4GroupHeader))
|| !tfile.Write((BYTE*)save_core,Head.Entries*sizeof(C4GroupEntryCore)))
{ tfile.Close(); delete [] save_core; return Error("Close: ..."); }
delete [] save_core;
// Save Entries to temp file
int iTotalSize=0,iSizeDone=0;
for (centry=FirstEntry; centry; centry=centry->Next) iTotalSize+=centry->Size;
for (centry=FirstEntry; centry; centry=centry->Next)
if (AppendEntry2StdFile(centry,tfile))
{ iSizeDone+=centry->Size; if (iTotalSize && fnProcessCallback) fnProcessCallback(centry->FileName,100*iSizeDone/iTotalSize); }
else
{
tfile.Close(); return false;
}
// Write
StdBuf *pBuf;
tfile.Close(fToMemory ? &pBuf : NULL);
// Child: move temp file to mother
if (Mother)
{
if (fToMemory)
{
if (!Mother->Add(GetFilename(FileName), *pBuf, true, true))
{ delete pBuf; CloseExclusiveMother(); Clear(); return Error("Close: Cannot move rewritten child data to mother"); }
delete pBuf;
}
else
{
if (!Mother->Move(szTempFileName,GetFilename(FileName)))
{ CloseExclusiveMother(); Clear(); return Error("Close: Cannot move rewritten child temp file to mother"); }
}
Clear();
return true;
}
// Clear (close file)
Clear();
// Delete old group file, rename new file
if (!EraseFile(szGrpFileName))
return Error("Close: Cannot erase temp file");
if (!RenameFile(szTempFileName,szGrpFileName))
return Error("Close: Cannot rename group file");
// Should reopen the file?
if (fReOpen)
OpenReal(szGrpFileName);
return true;
}
void C4Group::Default()
{
FirstEntry = NULL;
StdFile.Default();
Mother = NULL;
ExclusiveChild = 0;
Init();
}
void C4Group::Clear()
{
// Delete entries
C4GroupEntry *next;
while (FirstEntry)
{
next=FirstEntry->Next;
delete FirstEntry;
FirstEntry=next;
}
// Close std file
StdFile.Close();
// Delete mother
if (Mother && ExclusiveChild)
{
delete Mother;
Mother=NULL;
}
// Reset
Init();
}
bool C4Group::AppendEntry2StdFile(C4GroupEntry *centry, CStdFile &hTarget)
{
CStdFile hSource;
long csize;
BYTE fbuf;
switch (centry->Status)
{
case C4GroupEntry::C4GRES_InGroup: // Copy from group to std file
if (!SetFilePtr(centry->Offset))
return Error("AE2S: Cannot set file pointer");
for (csize=centry->Size; csize>0; csize--)
{
if (!Read(&fbuf,1))
return Error("AE2S: Cannot read entry from group file");
if (!hTarget.Write(&fbuf,1))
return Error("AE2S: Cannot write to target file");
}
break;
case C4GroupEntry::C4GRES_OnDisk: // Copy/move from disk item to std file
{
char szFileSource[_MAX_FNAME+1];
SCopy(centry->DiskPath,szFileSource,_MAX_FNAME);
// Disk item is a directory
if (DirectoryExists(centry->DiskPath))
return Error("AE2S: Cannot add directory to group file");
// Resort group if neccessary
// (The group might be renamed by adding, forcing a resort)
bool fTempFile = false;
if (centry->ChildGroup)
if (!centry->NoSort)
if (!SEqual(GetFilename(szFileSource), centry->FileName))
{
// copy group
MakeTempFilename(szFileSource);
if (!CopyItem(centry->DiskPath, szFileSource))
return Error("AE2S: Cannot copy item");
// open group and resort
C4Group SortGrp;
if (!SortGrp.Open(szFileSource))
return Error("AE2S: Cannot open group");
if (!SortGrp.SortByList(C4Group_SortList, centry->FileName))
return Error("AE2S: Cannot resort group");
fTempFile = true;
// close group (won't be saved if the sort didn't change)
SortGrp.Close();
}
// Append disk source to target file
if (!hSource.Open(szFileSource, !!centry->ChildGroup))
return Error("AE2S: Cannot open on-disk file");
for (csize=centry->Size; csize>0; csize--)
{
if (!hSource.Read(&fbuf,1))
{ hSource.Close(); return Error("AE2S: Cannot read on-disk file"); }
if (!hTarget.Write(&fbuf,1))
{ hSource.Close(); return Error("AE2S: Cannot write to target file"); }
}
hSource.Close();
// Erase temp file
if (fTempFile)
EraseItem(szFileSource);
// Erase disk source if requested
if (centry->DeleteOnDisk)
EraseItem(centry->DiskPath);
break;
}
case C4GroupEntry::C4GRES_InMemory: // Copy from mem to std file
if (!centry->bpMemBuf) return Error("AE2S: no buffer");
if (!hTarget.Write(centry->bpMemBuf,centry->Size)) return Error("AE2S: writing error");
break;
case C4GroupEntry::C4GRES_Deleted: // Don't save
break;
default: // Unknown file status
return Error("AE2S: Unknown file status");
}
return true;
}
void C4Group::ResetSearch(bool reload_contents)
{
switch (Status)
{
case GRPF_Folder:
SearchPtr=NULL;
FolderSearch.Reset(FileName, reload_contents);
if (*FolderSearch)
{
FolderSearchEntry.Set(FolderSearch,FileName);
SearchPtr=&FolderSearchEntry;
}
break;
case GRPF_File:
SearchPtr=FirstEntry;
break;
default: break; // InGrp & Deleted ignored
}
}
C4GroupEntry* C4Group::GetNextFolderEntry()
{
if (*++FolderSearch)
{
FolderSearchEntry.Set(FolderSearch,FileName);
return &FolderSearchEntry;
}
else
{
return NULL;
}
}
C4GroupEntry* C4Group::SearchNextEntry(const char *szName)
{
// Wildcard "*.*" is expected to find all files: substitute correct wildcard "*"
if (SEqual(szName, "*.*"))
szName = "*";
// Search by group type
C4GroupEntry *pEntry;
switch (Status)
{
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case GRPF_File:
for (pEntry=SearchPtr; pEntry; pEntry=pEntry->Next)
if (pEntry->Status != C4GroupEntry::C4GRES_Deleted)
if (WildcardMatch(szName,pEntry->FileName))
{
SearchPtr=pEntry->Next;
return pEntry;
}
break;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case GRPF_Folder:
for (pEntry=SearchPtr; pEntry; pEntry=GetNextFolderEntry())
if (WildcardMatch(szName,pEntry->FileName))
if (!C4Group_TestIgnore(pEntry->FileName))
{
LastFolderSearchEntry=(*pEntry);
pEntry=&LastFolderSearchEntry;
SearchPtr=GetNextFolderEntry();
return pEntry;
}
break;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
default: break; // InGrp & Deleted ignored
}
// No entry found: reset search pointer
SearchPtr=NULL;
return NULL;
}
bool C4Group::SetFilePtr(int iOffset)
{
if (Status==GRPF_Folder)
return Error("SetFilePtr not implemented for Folders");
// ensure mother is at correct pos
if (Mother) Mother->EnsureChildFilePtr(this);
// Rewind if necessary
if (FilePtr>iOffset)
if (!RewindFilePtr()) return false;
// Advance to target pointer
if (FilePtr<iOffset)
if (!AdvanceFilePtr(iOffset-FilePtr)) return false;
return true;
}
bool C4Group::Advance(int iOffset)
{
assert(iOffset >= 0);
// cached advance
if (pInMemEntry)
{
if (iInMemEntrySize < size_t(iOffset)) return false;
iInMemEntrySize -= iOffset;
pInMemEntry += iOffset;
return true;
}
// uncached advance
if (Status == GRPF_Folder) return !!StdFile.Advance(iOffset);
// FIXME: reading the file one byte at a time sounds just slow.
BYTE buf;
for (; iOffset>0; iOffset--)
if (!Read(&buf,1)) return false;
return true;
}
bool C4Group::Read(void *pBuffer, size_t iSize)
{
// Access cached entry from memory?
if (pInMemEntry)
{
if (iInMemEntrySize < iSize) return Error("ReadCached:");
memcpy(pBuffer, pInMemEntry, iSize);
iInMemEntrySize -= iSize;
pInMemEntry += iSize;
return true;
}
// Not cached. Read from file.
switch (Status)
{
case GRPF_File:
// Child group: read from mother group
if (Mother)
{
if (!Mother->Read(pBuffer,iSize))
{ RewindFilePtr(); return Error("Read:"); }
}
// Regular group: read from standard file
else
{
if (!StdFile.Read(pBuffer,iSize))
{ RewindFilePtr(); return Error("Read:"); }
}
FilePtr+=iSize;
break;
case GRPF_Folder:
if (!StdFile.Read(pBuffer,iSize)) return Error("Read: Error reading from folder contents");
break;
default: break; // InGrp & Deleted ignored
}
return true;
}
bool C4Group::AdvanceFilePtr(int iOffset, C4Group *pByChild)
{
// Child group file: pass command to mother
if ((Status==GRPF_File) && Mother)
{
// Ensure mother file ptr for it may have been moved by foreign access to mother
if (!Mother->EnsureChildFilePtr(this))
return false;
if (!Mother->AdvanceFilePtr(iOffset,this))
return false;
}
// Regular group
else if (Status==GRPF_File)
{
if (!StdFile.Advance(iOffset))
return false;
}
// Open folder
else
{
if (!StdFile.Advance(iOffset))
return false;
}
// Advanced
FilePtr+=iOffset;
return true;
}
bool C4Group::RewindFilePtr()
{
#ifdef _DEBUG
if (szCurrAccessedEntry && !iC4GroupRewindFilePtrNoWarn)
{
LogF("C4Group::RewindFilePtr() for %s (%s) after %s", szCurrAccessedEntry ? szCurrAccessedEntry : "???", FileName, sPrevAccessedEntry.getLength() ? sPrevAccessedEntry.getData() : "???");
szCurrAccessedEntry=NULL;
}
#endif
// Child group file: pass command to mother
if ((Status==GRPF_File) && Mother)
{
if (!Mother->SetFilePtr2Entry(FileName,true)) // Set to group file start
return false;
if (!Mother->AdvanceFilePtr(EntryOffset,this)) // Advance data offset
return false;
}
// Regular group or open folder: rewind standard file
else
{
if (!StdFile.Rewind()) // Set to group file start
return false;
if (!StdFile.Advance(EntryOffset)) // Advance data offset
return false;
}
FilePtr=0;
return true;
}
bool C4Group::Merge(const char *szFolders)
{
bool fMove = true;
if (StdOutput) printf("%s...\n",fMove ? "Moving" : "Adding");
// Add files & directories
char szFileName[_MAX_FNAME+1];
int iFileCount = 0;
DirectoryIterator i;
// Process segmented path & search wildcards
char cSeparator = (SCharCount(';', szFolders) ? ';' : '|');
for (int cseg=0; SCopySegment(szFolders, cseg, szFileName, cSeparator); cseg++)
{
i.Reset(szFileName);
while (*i)
{
// File count
iFileCount++;
// Process output & callback
if (StdOutput) printf("%s\n",GetFilename(*i));
if (fnProcessCallback)
fnProcessCallback(GetFilename(*i),0); // cbytes/tbytes
// AddEntryOnDisk
AddEntryOnDisk(*i, NULL, fMove);
++i;
}
}
if (StdOutput) printf("%d file(s) %s.\n",iFileCount,fMove ? "moved" : "added");
return true;
}
bool C4Group::AddEntryOnDisk(const char *szFilename,
const char *szAddAs,
bool fMove)
{
// Do not process yourself
if (ItemIdentical(szFilename,FileName)) return true;
// File is a directory: copy to temp path, pack, and add packed file
if (DirectoryExists(szFilename))
{
// Ignore
if (C4Group_TestIgnore(szFilename)) return true;
// Temp filename
char szTempFilename[_MAX_PATH+1];
if (C4Group_TempPath[0]) { SCopy(C4Group_TempPath,szTempFilename,_MAX_PATH); SAppend(GetFilename(szFilename),szTempFilename,_MAX_PATH); }
else SCopy(szFilename,szTempFilename,_MAX_PATH);
MakeTempFilename(szTempFilename);
// Copy or move item to temp file (moved items might be killed if later process fails)
if (fMove) { if (!MoveItem(szFilename,szTempFilename)) return Error("AddEntryOnDisk: Move failure"); }
else { if (!CopyItem(szFilename,szTempFilename)) return Error("AddEntryOnDisk: Copy failure"); }
// Pack temp file
if (!C4Group_PackDirectory(szTempFilename)) return Error("AddEntryOnDisk: Pack directory failure");
// Add temp file
if (!szAddAs) szAddAs = GetFilename(szFilename);
szFilename = szTempFilename;
fMove = true;
}
// Determine size
bool fIsGroup = !!C4Group_IsGroup(szFilename);
int iSize = fIsGroup ? UncompressedFileSize(szFilename) : FileSize(szFilename);
// Determine executable bit (linux only)
bool fExecutable = false;
#ifdef __linux__
fExecutable = (access(szFilename, X_OK) == 0);
#endif
// AddEntry
return AddEntry(C4GroupEntry::C4GRES_OnDisk,
fIsGroup,
szFilename,
iSize,
szAddAs,
NULL,
fMove,
false,
fExecutable);
}
bool C4Group::Add(const char *szFile, const char *szAddAs)
{
bool fMove = false;
if (StdOutput) printf("%s %s as %s...\n",fMove ? "Moving" : "Adding",GetFilename(szFile),szAddAs);
return AddEntryOnDisk(szFile, szAddAs, fMove);
}
bool C4Group::Move(const char *szFile, const char *szAddAs)
{
bool fMove = true;
if (StdOutput) printf("%s %s as %s...\n",fMove ? "Moving" : "Adding",GetFilename(szFile),szAddAs);
return AddEntryOnDisk(szFile, szAddAs, fMove);
}
bool C4Group::Delete(const char *szFiles, bool fRecursive)
{
int fcount = 0;
C4GroupEntry *tentry;
// Segmented file specs
if (SCharCount(';', szFiles) || SCharCount('|', szFiles))
{
char cSeparator = (SCharCount(';', szFiles) ? ';' : '|');
bool success = true;
char filespec[_MAX_FNAME+1];
for (int cseg = 0; SCopySegment(szFiles, cseg, filespec, cSeparator, _MAX_FNAME); cseg++)
if (!Delete(filespec, fRecursive))
success=false;
return success; // Would be nicer to return the file count and add up all counts from recursive actions...
}
// Delete all matching Entries
ResetSearch();
while ((tentry = SearchNextEntry(szFiles)))
{
// StdOutput
if (StdOutput) printf("%s\n",tentry->FileName);
if (!DeleteEntry(tentry->FileName))
return Error("Delete: Could not delete entry");
fcount++;
}
// Recursive: process sub groups
if (fRecursive)
{
C4Group hChild;
ResetSearch();
while ((tentry = SearchNextEntry("*")))
if (tentry->ChildGroup)
if (hChild.OpenAsChild(this, tentry->FileName))
{
hChild.SetStdOutput(StdOutput);
hChild.Delete(szFiles, fRecursive);
hChild.Close();
}
}
// StdOutput
if (StdOutput)
printf("%d file(s) deleted.\n",fcount);
return true; // Would be nicer to return the file count and add up all counts from recursive actions...
}
bool C4Group::DeleteEntry(const char *szFilename, bool fRecycle)
{
switch (Status)
{
case GRPF_File:
// Get entry
C4GroupEntry *pEntry;
if (!(pEntry=GetEntry(szFilename))) return false;
// Delete moved source files
if (pEntry->Status == C4GroupEntry::C4GRES_OnDisk)
if (pEntry->DeleteOnDisk)
{
EraseItem(pEntry->DiskPath);
}
// (moved buffers are deleted by ~C4GroupEntry)
// Delete status and update virtual file count
pEntry->Status = C4GroupEntry::C4GRES_Deleted;
Head.Entries--;
break;
case GRPF_Folder:
StdFile.Close();
char szPath[_MAX_FNAME+1];
sprintf(szPath,"%s%c%s",FileName,DirectorySeparator,szFilename);
if (fRecycle)
{
if (!EraseItemSafe(szPath)) return false;
}
else
{
if (!EraseItem(szPath)) return false;
}
break;
// refresh file list
ResetSearch(true);
default: break; // InGrp & Deleted ignored
}
return true;
}
bool C4Group::Rename(const char *szFile, const char *szNewName)
{
if (StdOutput) printf("Renaming %s to %s...\n",szFile,szNewName);
switch (Status)
{
case GRPF_File:
// Get entry
C4GroupEntry *pEntry;
if (!(pEntry=GetEntry(szFile))) return Error("Rename: File not found");
// Check double name
if (GetEntry(szNewName) && !SEqualNoCase(szNewName, szFile)) return Error("Rename: File exists already");
// Rename
SCopy(szNewName,pEntry->FileName,_MAX_FNAME);
Modified=true;
break;
case GRPF_Folder:
StdFile.Close();
char path[_MAX_FNAME+1]; SCopy(FileName,path,_MAX_PATH-1);
AppendBackslash(path); SAppend(szFile,path,_MAX_PATH);
char path2[_MAX_FNAME+1]; SCopy(FileName,path2,_MAX_PATH-1);
AppendBackslash(path2); SAppend(szNewName,path2,_MAX_PATH);
if (!RenameFile(path,path2)) return Error("Rename: Failure");
// refresh file list
ResetSearch(true);
break;
default: break; // InGrp & Deleted ignored
}
return true;
}
bool C4Group_IsExcluded(const char *szFile, const char *szExcludeList)
{
// No file or no exclude list
if (!szFile || !szFile[0] || !szExcludeList || !szExcludeList[0]) return false;
// Process segmented exclude list
char cSeparator = (SCharCount(';', szExcludeList) ? ';' : '|');
char szSegment[_MAX_PATH + 1];
for (int i = 0; SCopySegment(szExcludeList, i, szSegment, cSeparator); i++)
if (WildcardMatch(szSegment, GetFilename(szFile)))
return true;
// No match
return false;
}
bool C4Group::Extract(const char *szFiles, const char *szExtractTo, const char *szExclude)
{
// StdOutput
if (StdOutput)
{
printf("Extracting");
if (szExtractTo) printf(" to %s",szExtractTo);
printf("...\n");
}
int fcount=0;
int cbytes,tbytes;
C4GroupEntry *tentry;
cbytes=0; tbytes=EntrySize();
// Process segmented list
char cSeparator = (SCharCount(';', szFiles) ? ';' : '|');
char szFileName[_MAX_PATH + 1];
for (int cseg=0; SCopySegment(szFiles, cseg, szFileName, cSeparator); cseg++)
{
// Search all entries
ResetSearch();
while ((tentry = SearchNextEntry(szFileName)))
{
// skip?
if (C4Group_IsExcluded(tentry->FileName, szExclude)) continue;
// Process data & output
if (StdOutput) printf("%s\n",tentry->FileName);
cbytes+=tentry->Size;
if (fnProcessCallback)
fnProcessCallback(tentry->FileName,100*cbytes/std::max(tbytes,1));
// Extract
if (!ExtractEntry(tentry->FileName,szExtractTo))
return Error("Extract: Could not extract entry");
fcount++;
}
}
if (StdOutput) printf("%d file(s) extracted.\n",fcount);
return true;
}
bool C4Group::ExtractEntry(const char *szFilename, const char *szExtractTo)
{
CStdFile tfile;
CStdFile hDummy;
char szTempFName[_MAX_FNAME+1],szTargetFName[_MAX_FNAME+1];
// Target file name
if (szExtractTo)
{
SCopy(szExtractTo,szTargetFName,_MAX_FNAME-1);
if (DirectoryExists(szTargetFName))
{
AppendBackslash(szTargetFName);
SAppend(szFilename,szTargetFName,_MAX_FNAME);
}
}
else
SCopy(szFilename,szTargetFName,_MAX_FNAME);
// Extract
switch (Status)
{
case GRPF_File: // Copy entry to target
// Get entry
C4GroupEntry *pEntry;
if (!(pEntry=GetEntry(szFilename))) return Error("Extract: Entry not found");
// Create dummy file to reserve target file name
hDummy.Create(szTargetFName,false);
hDummy.Write("Dummy",5);
hDummy.Close();
// Make temp target file name
SCopy(szTargetFName,szTempFName,_MAX_FNAME);
MakeTempFilename(szTempFName);
// Create temp target file
if (!tfile.Create(szTempFName, !!pEntry->ChildGroup, !!pEntry->Executable))
return Error("Extract: Cannot create target file");
// Write entry file to temp target file
if (!AppendEntry2StdFile(pEntry,tfile))
{
// Failure: close and erase temp target file
tfile.Close();
EraseItem(szTempFName);
// Also erase reservation target file
EraseItem(szTargetFName);
// Failure
return false;
}
// Close target file
tfile.Close();
// Make temp file to original file
if (!EraseItem(szTargetFName))
return Error("Extract: Cannot erase temporary file");
if (!RenameItem(szTempFName,szTargetFName))
return Error("Extract: Cannot rename temporary file");
break;
case GRPF_Folder: // Copy item from folder to target
char szPath[_MAX_FNAME+1];
sprintf(szPath,"%s%c%s",FileName,DirectorySeparator,szFilename);
if (!CopyItem(szPath,szTargetFName))
return Error("ExtractEntry: Cannot copy item");
break;
default: break; // InGrp & Deleted ignored
}
return true;
}
bool C4Group::OpenAsChild(C4Group *pMother,
const char *szEntryName, bool fExclusive, bool fCreate)
{
if (!pMother) return Error("OpenAsChild: No mother specified");
if (SCharCount('*',szEntryName)) return Error("OpenAsChild: No wildcards allowed");
// Open nested child group check: If szEntryName is a reference to
// a nested group, open the first mother (in specified mode), then open the child
// in exclusive mode
if (SCharCount(DirectorySeparator,szEntryName))
{
char mothername[_MAX_FNAME+1];
SCopyUntil(szEntryName,mothername,DirectorySeparator,_MAX_FNAME);
C4Group *pMother2;
pMother2 = new C4Group;
pMother2->SetStdOutput(StdOutput);
if (!pMother2->OpenAsChild(pMother, mothername, fExclusive))
{
delete pMother2;
return Error("OpenAsChild: Cannot open mother");
}
return OpenAsChild(pMother2, szEntryName + SLen(mothername) + 1, true);
}
// Init
Init();
SCopy(szEntryName,FileName,_MAX_FNAME);
Mother=pMother;
ExclusiveChild=fExclusive;
// Folder: Simply set status and return
char path[_MAX_FNAME+1];
SCopy( GetFullName().getData(), path, _MAX_FNAME);
if (DirectoryExists(path))
{
SCopy(path,FileName, _MAX_FNAME);
Status=GRPF_Folder;
ResetSearch();
return true;
}
// Get original entry name
C4GroupEntry *centry;
if ((centry = Mother->GetEntry(FileName)))
SCopy(centry->FileName,FileName,_MAX_PATH);
// Access entry in mother group
size_t iSize;
if ((!Mother->AccessEntry(FileName, &iSize, NULL, true)))
{
if (!fCreate)
{ CloseExclusiveMother(); Clear(); return Error("OpenAsChild: Entry not in mother group"); }
else
{
// Create - will be added to mother in Close()
Status=GRPF_File; Modified=true;
return true;
}
}
// Child Group?
if (centry && !centry->ChildGroup)
{ CloseExclusiveMother(); Clear(); return Error("OpenAsChild: Is not a child group"); }
// Read header
// Do not do size checks for packed subgroups of unpacked groups (there will be no entry),
// because that would be the PACKED size which can actually be smaller than sizeof(C4GroupHeader)!
if (iSize < sizeof(C4GroupHeader) && centry)
{ CloseExclusiveMother(); Clear(); return Error("OpenAsChild: Entry too small"); }
if (!Mother->Read(&Head,sizeof(C4GroupHeader)))
{ CloseExclusiveMother(); Clear(); return Error("OpenAsChild: Entry reading error"); }
MemScramble((BYTE*)&Head,sizeof(C4GroupHeader));
EntryOffset+=sizeof(C4GroupHeader);
// Check Header
if (!SEqual(Head.id,C4GroupFileID)
|| (Head.Ver1!=C4GroupFileVer1) || (Head.Ver2>C4GroupFileVer2))
{ CloseExclusiveMother(); Clear(); return Error("OpenAsChild: Invalid Header"); }
// Read Entries
C4GroupEntryCore corebuf;
int file_entries=Head.Entries;
Head.Entries=0; // Reset, will be recounted by AddEntry
for (int cnt=0; cnt<file_entries; cnt++)
{
if (!Mother->Read(&corebuf,sizeof(C4GroupEntryCore)))
{ CloseExclusiveMother(); Clear(); return Error("OpenAsChild: Entry reading error"); }
EntryOffset+=sizeof(C4GroupEntryCore);
if (!AddEntry(C4GroupEntry::C4GRES_InGroup, !!corebuf.ChildGroup,
corebuf.FileName,corebuf.Size,
NULL, NULL, false, false,
!!corebuf.Executable))
{ CloseExclusiveMother(); Clear(); return Error("OpenAsChild: Insufficient memory"); }
}
ResetSearch();
// File
Status=GRPF_File;
// save position in mother group
if (centry) MotherOffset = centry->Offset;
return true;
}
bool C4Group::AccessEntry(const char *szWildCard,
size_t *iSize, char *sFileName,
bool NeedsToBeAGroup)
{
#ifdef C4GROUP_DUMP_ACCESS
LogF("Group access in %s: %s", GetFullName().getData(), szWildCard);
#endif
StdStrBuf fname;
if (!FindEntry(szWildCard,&fname,&iCurrFileSize))
return false;
#ifdef _DEBUG
szCurrAccessedEntry = fname.getMData();
#endif
bool fResult = SetFilePtr2Entry(fname.getData(), NeedsToBeAGroup);
#ifdef _DEBUG
sPrevAccessedEntry.Copy(szCurrAccessedEntry);
szCurrAccessedEntry = NULL;
#endif
if (!fResult) return false;
if (sFileName) SCopy(fname.getData(),sFileName);
if (iSize) *iSize=iCurrFileSize;
return true;
}
bool C4Group::AccessNextEntry(const char *szWildCard,
size_t *iSize, char *sFileName,
bool fStartAtFilename)
{
char fname[_MAX_FNAME+1];
if (!FindNextEntry(szWildCard,fname,&iCurrFileSize,fStartAtFilename)) return false;
#ifdef _DEBUG
szCurrAccessedEntry = fname;
#endif
bool fResult = SetFilePtr2Entry(fname);
#ifdef _DEBUG
szCurrAccessedEntry = NULL;
#endif
if (!fResult) return false;
if (sFileName) SCopy(fname,sFileName);
if (iSize) *iSize=iCurrFileSize;
return true;
}
bool C4Group::SetFilePtr2Entry(const char *szName, bool NeedsToBeAGroup)
{
C4GroupEntry *centry = GetEntry(szName);
// Read cached entries directly from memory (except child groups. that is not supported.)
if (centry && centry->bpMemBuf && !NeedsToBeAGroup)
{
pInMemEntry = centry->bpMemBuf;
iInMemEntrySize = centry->Size;
return true;
}
else
{
pInMemEntry = NULL;
}
// Not cached. Access from disk.
switch (Status)
{
case GRPF_File:
if ((!centry) || (centry->Status != C4GroupEntry::C4GRES_InGroup)) return false;
return SetFilePtr(centry->Offset);
case GRPF_Folder: {
StdFile.Close();
char path[_MAX_FNAME+1]; SCopy(FileName,path,_MAX_FNAME);
AppendBackslash(path); SAppend(szName,path);
bool fSuccess = StdFile.Open(path, NeedsToBeAGroup);
return fSuccess;
}
default: break; // InGrp & Deleted ignored
}
return false;
}
bool C4Group::FindEntry(const char *szWildCard, StdStrBuf *sFileName, size_t *iSize)
{
ResetSearch();
return FindNextEntry(szWildCard,sFileName,iSize);
}
bool C4Group::FindNextEntry(const char *szWildCard,
StdStrBuf *sFileName,
size_t *iSize,
bool fStartAtFilename)
{
C4GroupEntry *centry;
if (!szWildCard) return false;
// Reset search to specified position
if (fStartAtFilename) FindEntry(sFileName->getData());
if (!(centry=SearchNextEntry(szWildCard))) return false;
if (sFileName) sFileName->Copy(centry->FileName);
if (iSize) *iSize=centry->Size;
return true;
}
bool C4Group::Add(const char *szName, void *pBuffer, int iSize, bool fChild, bool fHoldBuffer, bool fExecutable)
{
return AddEntry(C4GroupEntry::C4GRES_InMemory,
fChild,
szName,
iSize,
szName,
(BYTE*) pBuffer,
false,
fHoldBuffer,
fExecutable);
}
bool C4Group::Add(const char *szName, StdBuf &pBuffer, bool fChild, bool fHoldBuffer, bool fExecutable)
{
if (!AddEntry(C4GroupEntry::C4GRES_InMemory,
fChild,
szName,
pBuffer.getSize(),
szName,
(BYTE*) pBuffer.getMData(),
false,
fHoldBuffer,
fExecutable,
true)) return false;
// Pointer is now owned and released by C4Group!
if (fHoldBuffer) pBuffer.GrabPointer();
return true;
}
bool C4Group::Add(const char *szName, StdStrBuf &pBuffer, bool fChild, bool fHoldBuffer, bool fExecutable)
{
if (!AddEntry(C4GroupEntry::C4GRES_InMemory,
fChild,
szName,
pBuffer.getLength(),
szName,
(BYTE*) pBuffer.getMData(),
false,
fHoldBuffer,
fExecutable,
true)) return false;
// Pointer is now owned and released by C4Group!
if (fHoldBuffer) pBuffer.GrabPointer();
return true;
}
const char* C4Group::GetName()
{
return FileName;
}
int C4Group::EntryCount(const char *szWildCard)
{
int fcount;
C4GroupEntry *tentry;
// All files if no wildcard
if (!szWildCard) szWildCard="*";
// Match wildcard
ResetSearch(); fcount=0;
while ((tentry=SearchNextEntry(szWildCard))) fcount++;
return fcount;
}
size_t C4Group::EntrySize(const char *szWildCard)
{
int fsize;
C4GroupEntry *tentry;
// All files if no wildcard
if (!szWildCard) szWildCard="*";
// Match wildcard
ResetSearch(); fsize=0;
while ((tentry=SearchNextEntry(szWildCard)))
fsize+=tentry->Size;
return fsize;
}
unsigned int C4Group::EntryCRC32(const char *szWildCard)
{
if (!szWildCard) szWildCard="*";
// iterate thorugh child
C4GroupEntry *pEntry; unsigned int iCRC = 0;
ResetSearch();
while ((pEntry = SearchNextEntry(szWildCard)))
{
iCRC ^= CalcCRC32(pEntry);
}
// return
return iCRC;
}
bool C4Group::LoadEntry(const char *szEntryName, char **lpbpBuf, size_t *ipSize, int iAppendZeros)
{
size_t size;
// Access entry, allocate buffer, read data
(*lpbpBuf)=NULL; if (ipSize) *ipSize=0;
if (!AccessEntry(szEntryName,&size)) return Error("LoadEntry: Not found");
*lpbpBuf = new char[size+iAppendZeros];
if (!Read(*lpbpBuf,size))
{
delete [] (*lpbpBuf); *lpbpBuf = NULL;
return Error("LoadEntry: Reading error");
}
if (ipSize) *ipSize=size;
if (iAppendZeros)
ZeroMem( (*lpbpBuf)+size, iAppendZeros );
return true;
}
bool C4Group::LoadEntry(const char *szEntryName, StdBuf * Buf)
{
size_t size;
// Access entry, allocate buffer, read data
if (!AccessEntry(szEntryName,&size)) return Error("LoadEntry: Not found");
// Allocate memory
Buf->New(size);
// Load data
if (!Read(Buf->getMData(),size))
{
Buf->Clear();
return Error("LoadEntry: Reading error");
}
// ok
return true;
}
bool C4Group::LoadEntryString(const char *szEntryName, StdStrBuf *Buf)
{
size_t size;
// Access entry, allocate buffer, read data
if (!AccessEntry(szEntryName,&size)) return Error("LoadEntry: Not found");
// Allocate memory
Buf->SetLength(size);
// other parts crash when they get a zero length buffer, so fail here
if (!size) return false;
// Load data
if (!Read(Buf->getMData(),size))
{
Buf->Clear();
return Error("LoadEntry: Reading error");
}
// ok
return true;
}
int SortRank(const char *szElement, const char *szSortList)
{
int cnt;
char csegment[_MAX_FNAME+1];
for (cnt=0; SCopySegment(szSortList,cnt,csegment,'|',_MAX_FNAME); cnt++)
if (WildcardMatch(csegment,szElement))
return (SCharCount('|',szSortList)+1)-cnt;
return 0;
}
bool C4Group::Sort(const char *szSortList)
{
bool fBubble;
C4GroupEntry *centry,*prev,*next,*nextnext;
if (!szSortList || !szSortList[0]) return false;
if (StdOutput) printf("Sorting...\n");
do
{
fBubble=false;
for (prev=NULL,centry=FirstEntry; centry; prev=centry,centry=next)
if ((next=centry->Next))
{
// primary sort by file list
int iS1 = SortRank(centry->FileName,szSortList);
int iS2 = SortRank(next->FileName,szSortList);
if (iS1 > iS2) continue;
// secondary sort by filename
if (iS1 == iS2)
if (stricmp(centry->FileName, next->FileName) <= 0) continue;
// wrong order: Swap!
nextnext=next->Next;
if (prev) prev->Next=next;
else FirstEntry=next;
next->Next=centry;
centry->Next=nextnext;
next=nextnext;
fBubble=true;
Modified=true;
}
}
while (fBubble);
return true;
}
C4Group* C4Group::GetMother()
{
return Mother;
}
bool C4Group::CloseExclusiveMother()
{
if (Mother && ExclusiveChild)
{
Mother->Close();
delete Mother;
Mother=NULL;
return true;
}
return false;
}
bool C4Group::SortByList(const char **ppSortList, const char *szFilename)
{
// No sort list specified
if (!ppSortList) return false;
// No group name specified, use own
if (!szFilename) szFilename = FileName;
szFilename = GetFilename(szFilename);
// Find matching filename entry in sort list
const char **ppListEntry;
for (ppListEntry = ppSortList; *ppListEntry; ppListEntry+=2)
if (WildcardMatch( *ppListEntry, szFilename ))
break;
// Sort by sort list entry
if (*ppListEntry && *(ppListEntry+1))
Sort(*(ppListEntry+1));
// Success
return true;
}
void C4Group::ProcessOut(const char *szMessage, int iProcess)
{
if (fnProcessCallback) fnProcessCallback(szMessage,iProcess);
if (C4Group_ProcessCallback) C4Group_ProcessCallback(szMessage,iProcess);
}
bool C4Group::EnsureChildFilePtr(C4Group *pChild)
{
// group file
if (Status == GRPF_File)
{
// check if FilePtr has to be moved
if (FilePtr != pChild->MotherOffset + pChild->EntryOffset + pChild->FilePtr)
// move it to the position the child thinks it is
if (!SetFilePtr(pChild->MotherOffset + pChild->EntryOffset + pChild->FilePtr))
return false;
// ok
return true;
}
// Open standard file is not the child file ...or StdFile ptr does not match pChild->FilePtr
char szChildPath[_MAX_PATH+1]; sprintf(szChildPath,"%s%c%s",FileName,DirectorySeparator,GetFilename(pChild->FileName));
if ( !ItemIdentical( StdFile.Name, szChildPath ) )
{
// Reopen correct child stdfile
if ( !SetFilePtr2Entry( GetFilename(pChild->FileName), true ) )
return false;
// Advance to child's old file ptr
if ( !AdvanceFilePtr( pChild->EntryOffset + pChild->FilePtr ) )
return false;
}
// Looks okay
return true;
}
StdStrBuf C4Group::GetFullName() const
{
char str[_MAX_PATH+1]; *str='\0';
char sep[] = "/"; sep[0] = DirectorySeparator;
for (const C4Group *pGroup=this; pGroup; pGroup=pGroup->Mother)
{
if (*str) SInsert(str, sep, 0, _MAX_PATH);
// Avoid double slash
if (pGroup == this || SLen(pGroup->FileName) > 1 || pGroup->FileName[0] != '/')
SInsert(str, pGroup->FileName, 0, _MAX_PATH);
if (pGroup->Status == GRPF_Folder) break; // Folder is assumed to have full path
}
StdStrBuf sResult; sResult.Copy(str);
return sResult;
}
uint32_t C4Group::CalcCRC32(C4GroupEntry *pEntry)
{
uint32_t CRC;
// child group?
if (pEntry->ChildGroup || (pEntry->Status == C4GroupEntry::C4GRES_OnDisk && (DirectoryExists(pEntry->DiskPath) || C4Group_IsGroup(pEntry->DiskPath))))
{
// open
C4Group Child;
switch (pEntry->Status)
{
case C4GroupEntry::C4GRES_InGroup:
if (!Child.OpenAsChild(this, pEntry->FileName))
return 0;
break;
case C4GroupEntry::C4GRES_OnDisk:
if (!Child.Open(pEntry->DiskPath))
return 0;
break;
default:
return 0;
}
// get checksum
CRC = Child.EntryCRC32();
}
else if (!pEntry->Size)
CRC = 0;
else
{
BYTE *pData = NULL; bool fOwnData; CStdFile f;
// get data
switch (pEntry->Status)
{
case C4GroupEntry::C4GRES_InGroup:
// create buffer
pData = new BYTE [pEntry->Size]; fOwnData = true;
// go to entry
if (!SetFilePtr2Entry(pEntry->FileName)) { delete [] pData; return false; }
// read
if (!Read(pData, pEntry->Size)) { delete [] pData; return false; }
break;
case C4GroupEntry::C4GRES_OnDisk:
// create buffer
pData = new BYTE [pEntry->Size]; fOwnData = true;
// open
if (!f.Open(pEntry->DiskPath)) { delete [] pData; return false; }
// read
if (!f.Read(pData, pEntry->Size)) { delete [] pData; return false; }
break;
case C4GroupEntry::C4GRES_InMemory:
// set
pData = pEntry->bpMemBuf; fOwnData = false;
break;
default:
return false;
}
if (!pData) return false;
// calc crc
CRC = crc32(0, pData, pEntry->Size);
// discard buffer
if (fOwnData) delete [] pData;
// add file name
CRC = crc32(CRC, reinterpret_cast<BYTE *>(pEntry->FileName), SLen(pEntry->FileName));
}
// ok
return CRC;
}
bool C4Group::OpenChild(const char* strEntry)
{
// hack: The seach-handle would be closed twice otherwise
FolderSearch.Reset();
// Create a memory copy of ourselves
C4Group *pOurselves = new C4Group;
*pOurselves = *this;
// Open a child from the memory copy
C4Group hChild;
if (!hChild.OpenAsChild(pOurselves, strEntry, false))
{
// Silently delete our memory copy
pOurselves->Default(); delete pOurselves;
return false;
}
// hack: The seach-handle would be closed twice otherwise
FolderSearch.Reset();
hChild.FolderSearch.Reset();
// We now become our own child
*this = hChild;
// Make ourselves exclusive (until we hit our memory copy parent)
for (C4Group *pGroup = this; pGroup != pOurselves; pGroup = pGroup->Mother)
pGroup->ExclusiveChild = true;
// Reset the temporary child variable so it doesn't delete anything
hChild.Default();
// Yeehaw
return true;
}
bool C4Group::OpenMother()
{
// This only works if we are an exclusive child
if (!Mother || !ExclusiveChild) return false;
// Store a pointer to our mother
C4Group *pMother = Mother;
// Clear ourselves without deleting our mother
ExclusiveChild = false;
Clear();
// hack: The seach-handle would be closed twice otherwise
pMother->FolderSearch.Reset();
FolderSearch.Reset();
// We now become our own mother (whoa!)
*this = *pMother;
// Now silently delete our former mother
pMother->Default();
delete pMother;
// Yeehaw
return true;
}
int C4Group::PreCacheEntries(const char *szSearchPattern, bool cache_previous)
{
assert(szSearchPattern);
int result = 0;
// pre-load entries to memory. return number of loaded entries.
for (C4GroupEntry * p = FirstEntry; p; p = p->Next)
{
// is this to be cached?
if (!WildcardListMatch(szSearchPattern, p->FileName)) continue;
// if desired, cache all entries up to that one to allow rewind in unpacked memory
// (only makes sense for groups)
if (cache_previous && Status == GRPF_File)
{
for (C4GroupEntry * p_pre = FirstEntry; p_pre != p; p_pre = p_pre->Next)
if (p_pre->Offset >= FilePtr)
PreCacheEntry(p_pre);
}
// cache the given entry
PreCacheEntry(p);
}
return result;
}
void C4Group::PreCacheEntry(C4GroupEntry * p)
{
// skip some stuff that can not be cached or has already been cached
if (p->ChildGroup || p->bpMemBuf || !p->Size) return;
// now load it!
StdBuf buf;
if (!this->LoadEntry(p->FileName, &buf)) return;
p->HoldBuffer = true;
p->BufferIsStdbuf = true;
p->Size = buf.getSize(); // update size in case group changed on disk between calls
p->bpMemBuf = static_cast<BYTE *>(buf.GrabPointer());
}