// TODO? Error handling of lists? #define _POSIX_C_SOURCE 200809L #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include "cc4group.h" #include "c4groupheader.h" #include "c4groupentrycore.h" #include "GenericList.h" #include "platform/platform.h" #include #include #include #include #include #include #include #include #include #include #include #define ZLIB_CONST #include #define SET_ERROR(errorCauser, errorCode, errorFormatter, data) do { this->error.code = errorCode; this->error.formatter.formatter = errorFormatter; this->error.causer = errorCauser; this->error.method = __func__; } while(0) #define SET_MESSAGE_ERROR(message) SET_ERROR(message, 1337, cc4group_messageFormatter, NULL) #define SET_EOD_ERROR(message) SET_MESSAGE_ERROR("Unexpected end of group data while " message) #define SET_MALFORMED_MESSAGE_ERROR(message) SET_MESSAGE_ERROR("The group file is malformed: " message) #define SET_ERRNO_ERROR(causer) SET_ERROR(causer, errno, cc4group_strerrorFormatter, NULL) #define SET_ZERROR_ERROR(causer, error) SET_ERROR(causer, error, cc4group_zerrorFormatter, (void*)errno) static bool cc4group_openExisting(CC4Group* const this, const char* const path); static void cc4group_delete(CC4Group* const this); static bool cc4group_openAsChildOn(CC4Group* const this, const char* const path, CC4Group* const child); // extraction to disk static bool cc4group_extractAll(CC4Group* const this, const char* const targetPath); static bool cc4group_extractSingle(CC4Group* const this, const char* const entryPath, const char* const targetPath); // the group owns the data pointed to. the pointer is valid until the group destructor is called static bool cc4group_getEntryData(CC4Group* const this, const char* const entryPath, const void** const data, size_t* const size); #define C4GroupMagic1 0x1e #define C4GroupMagic2 0x8c #define CC4GROUP_UNEXPECTED_EOD -100 #define CC4GROUP_MEM_ERROR -101 typedef enum { CC4Group_AllowedEntryTypes_File = 0x01, CC4Group_AllowedEntryTypes_Directory = 0x02, CC4Group_AllowedEntryTypes_HandleGroups = 0x04, CC4Group_AllowedEntryTypes_NoGroupHandling = CC4Group_AllowedEntryTypes_File | CC4Group_AllowedEntryTypes_Directory, CC4Group_AllowedEntryTypes_All = CC4Group_AllowedEntryTypes_File | CC4Group_AllowedEntryTypes_Directory | CC4Group_AllowedEntryTypes_HandleGroups } CC4Group_AllowedEntryTypes; struct list_GroupEntryList; typedef struct C4GroupEntryData_t { C4GroupEntryCore core; CC4Group_MemoryManagement memoryManagement; union { uint8_t* data; // if the entry is a directory, the beginning of the data contains the header C4GroupHeader* header; }; struct list_GroupEntryList* children; struct C4GroupEntryData_t* parent; size_t absolutePosition; const char* path; } C4GroupEntryData; LIST_AUTO(C4GroupEntryData, GroupEntryList) #define ForeachGroupEntry(list) LIST_FOREACH(GroupEntryList, list, entry) LIST_AUTO(CC4Group_CleanupJob, CleanUpJobList) #define ForeachCleanupJob(list) LIST_FOREACH(CleanUpJobList, list, job) LIST_AUTO(const CC4Group*, GroupList); #define ForeachGroup(list) LIST_FOREACH(GroupList, list, group) typedef char* (*ErrorFormatter)(int32_t const code, const char* const method, const char* const causer, void* const data); typedef struct { void* data; ErrorFormatter formatter; } ErrorFormatterData; struct CC4Group_t { uint8_t* uncompressedData; size_t uncompressedSize; C4GroupEntryData root; CleanUpJobList* cleanupJobs; struct { int32_t code; const char* method; const char* causer; ErrorFormatterData formatter; char* lastFormattedMessage; } error; size_t referenceCounter; CC4Group* parent; struct { struct { CC4Group_ReadCallback read; CC4Group_ReadSetupCallback setup; CC4Group_ReadSetupCallback deinit; CC4Group_MemoryManagement memoryManagement; void* arg; } callback; bool active; size_t position; z_stream gzStrm; const uint8_t* lastData; } readState; bool lazy; const char* path; const char* name; const char* pathInParent; }; typedef struct { CC4Group_WriteCallback callback; void* arg; size_t bufferSize; size_t position; void* buffer; bool magicDone; z_stream gzStream; } WriteCallback; static bool cc4group_applyMemoryManagementStart(CC4Group_MemoryManagement const management, const uint8_t** data, size_t size) { void* newData = management.start(data == NULL ? NULL : (void*)*data, size, management.arg); if(newData == NULL) { return false; } *data = newData; return true; } static void cc4group_applyMemoryManagementEnd(CC4Group_MemoryManagement const management, const uint8_t* data) { management.end((void*)data, management.arg); } static const uint8_t* cc4group_getOnlyEntryData(CC4Group* const this, const C4GroupEntryData* const entry); static const C4GroupEntryData* cc4group_getEntryByPath(CC4Group* const this, const char* const entryPath, bool const allowRoot, bool* const error, const C4GroupEntryData* const inParent); static const C4GroupEntryData* cc4group_getDirectoryByPath(CC4Group* const this, const char* const entryPath, bool const allowRoot, const C4GroupEntryData* const inParent); static const C4GroupEntryData* cc4group_getFileByPath(CC4Group* const this, const char* const entryPath, const C4GroupEntryData* const inParent); static const C4GroupEntryData* cc4group_getFileOrDirectoryByPath(CC4Group* const this, const char* const entryPath, bool const allowRoot, const C4GroupEntryData* const inParent); static GroupEntryList* cc4group_getChildren(CC4Group* const this, const C4GroupEntryData* const entry); static C4GroupHeader* cc4group_getHeader(CC4Group* const this, const C4GroupEntryData* const entry); static C4GroupEntryData* cc4group_createEntry(CC4Group* const this, const char* const path, C4GroupEntryData* const parent); static C4GroupEntryData* cc4group_createDirectory(CC4Group* const this, const char* const path, C4GroupEntryData* const parent); static void cc4group_unreference(CC4Group* const this); static bool cc4group_getParentAndChildEntries(CC4Group* const this, const char* const path, const C4GroupEntryData* const inParent, const C4GroupEntryData** parentEntry, GroupEntryListEntry** const childEntry); static char* cc4group_strerrorFormatter(int32_t const code, const char* const method, const char* const causer, void* const data) { (void)data; char* message; if(asprintf(&message, "%s: %s: %s (%d)", method, causer, strerror(code), code) == -1) { return NULL; } return message; } static char* cc4group_zerrorFormatter(int32_t const code, const char* const method, const char* const causer, void* const data) { int storedErrno = (size_t)data; // restore errno for the case that code is Z_ERRNO if(code == Z_ERRNO) { errno = storedErrno; } char* message; const char* errorString; if(code == CC4GROUP_MEM_ERROR) { errorString = ""; if(asprintf(&message, "%s: %s: Can't allocate memory for copying the input data: %s (%d)", method, causer, strerror(storedErrno), storedErrno) == -1) { return NULL; } return message; } if(code == CC4GROUP_UNEXPECTED_EOD) { errorString = "Unexpected end of group data"; } else if(code == Z_OK) { errorString = "Z_OK"; } else { errorString = zError(code); } if(asprintf(&message, "%s: %s: %s (%d)", method, causer, errorString, code) == -1) { return NULL; } return message; } static char* cc4group_messageFormatter(int32_t const code, const char* const method, const char* const causer, void* const data) { (void)code; (void)data; char* message; if(asprintf(&message, "%s: %s", method, causer) == -1) { return NULL; } return message; } static char* cc4group_noerrorFormatter(int32_t const code, const char* const method, const char* const causer, void* const data) { (void)code; (void)method; (void)causer; (void)data; return strdup("No Error"); } static void cc4group_printToStderrWarningCallback(const CC4Group* const this, const char* const format, ...) { (void)this; va_list ap; va_start(ap, format); fputs("WARNING: ", stderr); vfprintf(stderr, format, ap); fputs("\n", stderr); va_end (ap); } static void cc4group_copyErrorInformation(const CC4Group* const src, CC4Group* const dst) { dst->error.code = src->error.code; dst->error.formatter = src->error.formatter; dst->error.causer = src->error.causer; dst->error.method = src->error.method; } static const CC4Group_WarningCallback cc4group_defaultWarningCallback = cc4group_printToStderrWarningCallback; static CC4Group_WarningCallback cc4group_warn = cc4group_defaultWarningCallback; static const CC4Group_EntryNameMatchingCallback cc4group_defaultEntryNameMatchingCallback = strcmp; static CC4Group_EntryNameMatchingCallback cc4group_namecmp = cc4group_defaultEntryNameMatchingCallback; #define AddCleanUpJob(func, data) CleanUpJobListPrepend(this->cleanupJobs, (CC4Group_CleanupJob){(CC4Group_CleanupFunc)func, data}); static void memScrambleHeader(uint8_t* const data) { // XOR deface for(size_t i = 0; i < sizeof(C4GroupHeader); i++) data[i] ^= 237; // byte swap for(size_t i = 0; i + 2 < sizeof(C4GroupHeader); i += 3) { uint8_t temp = data[i]; data[i] = data[i + 2]; data[i + 2] = temp; } } static bool cc4group_checkHeader(CC4Group* const this, C4GroupHeader* const header) { if(header->Ver1 != 1 || header->Ver2 > 2) { SET_MESSAGE_ERROR("Unsupported group version. Only versions 1.0 up to 1.2 are supported."); return false; } if(memcmp(C4GroupId, header->id, sizeof(header->id)) != 0) { SET_MALFORMED_MESSAGE_ERROR("The group id does not match."); return false; } // make sure the maker is null terminated header->Maker[C4GroupMaxMaker + 1] = '\0'; return true; } static bool buildChildren(CC4Group* const this, C4GroupEntryData* const entry, size_t const dataSize) { // only if it's read already if(this->readState.position < entry->absolutePosition + sizeof(C4GroupHeader)) { entry->header = NULL; return true; } if(!cc4group_checkHeader(this, entry->header)) { return false; } C4GroupHeader* header = entry->header; // only if it's read already if(this->readState.position < entry->absolutePosition + sizeof(C4GroupHeader) + sizeof(C4GroupEntryCore) * header->Entries) { entry->children = NULL; return true; } entry->children = GroupEntryListNew(); uint8_t* data = entry->data + sizeof(C4GroupHeader); size_t childDataOffset = sizeof(C4GroupHeader) + sizeof(C4GroupEntryCore) * header->Entries; uint8_t* childData = entry->data + childDataOffset; for(size_t i = 0; i < (size_t)header->Entries; ++i) { C4GroupEntryCore* core = (C4GroupEntryCore*)(data + sizeof(C4GroupEntryCore) * i); if(core->Offset + core->Size + childDataOffset > dataSize) { SET_MALFORMED_MESSAGE_ERROR("The group entry dictionary contains invalid entries."); return false; } C4GroupEntryData* childEntry = &GroupEntryListAppend(entry->children, (C4GroupEntryData){.core = *core, .data = childData + core->Offset, .memoryManagement = cc4group.MemoryManagement.Reference, .children = NULL, .parent = entry, .absolutePosition = entry->absolutePosition + childDataOffset + core->Offset, .path = NULL})->value; if(this->readState.position < childEntry->absolutePosition + core->Size) { childEntry->data = NULL; } else if(core->Directory) { memScrambleHeader(childEntry->data); if(!buildChildren(this, childEntry, core->Size)) { return false; } } } return true; } static void deleteChildren(GroupEntryList* const entries) { ForeachGroupEntry(entries) { if(entry->value.data != NULL) { cc4group_applyMemoryManagementEnd(entry->value.memoryManagement, entry->value.data); } if(entry->value.path != NULL) { free((void*)entry->value.path); } // if they are not loaded yet, there is no need to delete them if(entry->value.core.Directory && entry->value.children != NULL) { deleteChildren(entry->value.children); } } GroupEntryListDestroy(entries); } static void* cc4group_mapSizedWriteFd(CC4Group* const this, int fd, size_t size) { // allocate file size // https://gist.github.com/marcetcheverry/991042 if(lseek(fd, size - 1, SEEK_SET) == -1) { SET_ERRNO_ERROR("lseek: seeking to the desired file size"); return MAP_FAILED; } if(write(fd, "", 1) == -1) { SET_ERRNO_ERROR("write: writing to the file's end"); return MAP_FAILED; } return cc4group_mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); } static const char tmpFileTemplate[] = "cc4grouptmpXXXXXX"; #define tmpFileNameLength (sizeof(tmpFileTemplate) / sizeof(tmpFileTemplate[0])) typedef struct { void* addr; size_t size; char fileName[tmpFileNameLength]; bool unlinkLater; const CC4Group* group; } MunmapData; // *fileNameTarget must be at least tmpFileNameLength long! static int cc4group_createTmpFile(CC4Group* const this, char* const fileNameTarget) { strncpy(fileNameTarget, tmpFileTemplate, tmpFileNameLength); int tmpFile = mkstemp(fileNameTarget); if(tmpFile == -1) { SET_ERRNO_ERROR("mkstemp: Creating and opening the tmp file"); fileNameTarget[0] = '\0'; } return tmpFile; } static void cc4group_tryWarnUnlink(const CC4Group* const this, const char* const fileName) { if(unlink(fileName) == -1) { cc4group_warn(this, "Removing file \"%s\" failed. Manual deletion is required", fileName); } } static void cc4group_unmapTmpMemoryFile(MunmapData* const data) { if(cc4group_munmap(data->addr, data->size) == -1) { cc4group_warn(data->group, "munmap: Unmapping tempory file failed: %s", strerror(errno)); } if(data->unlinkLater) { cc4group_tryWarnUnlink(data->group, data->fileName); } free(data); } static void* cc4group_createTmpMemoryFile(CC4Group* const this, const size_t size, CC4Group_CleanupJob* const cleanupJob) { MunmapData* unmapData = malloc(sizeof(MunmapData)); if(unmapData == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for cleanup data"); return NULL; } void* ret = NULL; int tmpFile = cc4group_createTmpFile(this, unmapData->fileName); if(tmpFile == -1) { goto ret; } unmapData->unlinkLater = unlink(unmapData->fileName) == -1; // in case it can't be deleted now (Windows -.-), delete it when cleaning up ret = cc4group_mapSizedWriteFd(this, tmpFile, size); if(ret == MAP_FAILED) { // error message is set in the method ret = NULL; } else { unmapData->addr = ret; unmapData->size = size; unmapData->group = this; *cleanupJob = (CC4Group_CleanupJob){.func = (CC4Group_CleanupFunc)cc4group_unmapTmpMemoryFile, .data = unmapData}; } ret: if(tmpFile != -1 && close(tmpFile) == -1) { cc4group_warn(this, "close: Closing tmp file failed: %s", strerror(errno)); } if(ret == NULL) { if(unmapData->fileName[0] != '\0' && unmapData->unlinkLater) { cc4group_tryWarnUnlink(this, unmapData->fileName); } free(unmapData); } return ret; } static void* cc4group_createTmpMemoryMalloc(CC4Group* const this, const size_t size, CC4Group_CleanupJob* const cleanupJob) { (void)this; void* ret = malloc(size); if(ret == NULL) { SET_ERRNO_ERROR("malloc: allocating tmp memory"); } else { *cleanupJob = (CC4Group_CleanupJob){free, ret}; } return ret; } // uses in memory for up to 500MB, although it falls back to tmp file if in memory fails static void* cc4group_createTmpMemoryAuto(CC4Group* const this, const size_t size, CC4Group_CleanupJob* const cleanupJob) { void* ret = NULL; if(size < 500 * 1024 * 1024) { ret = cc4group_createTmpMemoryMalloc(this, size, cleanupJob); } if(ret == NULL) { // error checking is left out intentionally. it is the job of the calling function ret = cc4group_createTmpMemoryFile(this, size, cleanupJob); } return ret; } static CC4Group_TmpMemoryStrategy cc4group_tmpMemoryStrategy = cc4group_createTmpMemoryAuto; static bool cc4group_buildGroupHierarchy(CC4Group* const this) { this->root.data = this->uncompressedData; this->root.absolutePosition = 0; bool ret = buildChildren(this, &this->root, this->uncompressedSize); this->root.core.Directory = 1; this->root.core.FileName[0] = '\0'; AddCleanUpJob(deleteChildren, this->root.children); return ret; } static bool cc4group_inflateFillOutput(z_stream* const strm, CC4Group_ReadCallback const callback, void* const callbackArg, CC4Group_MemoryManagement const memoryManagement, bool* const eof, const uint8_t** const lastData, int* const inflateRet) { int ret = Z_OK; while(strm->avail_out > 0) { if(strm->avail_in == 0) { if(*eof) { *inflateRet = CC4GROUP_UNEXPECTED_EOD; return false; } if(*lastData != NULL) { cc4group_applyMemoryManagementEnd(memoryManagement, *lastData); *lastData = NULL; } size_t readSize = 0; *eof = callback((const void**)lastData, &readSize, callbackArg); if(readSize == 0) { continue; } if(!cc4group_applyMemoryManagementStart(memoryManagement, lastData, readSize)) { *inflateRet = CC4GROUP_MEM_ERROR; return false; } strm->avail_in = readSize; strm->next_in = *lastData; } ret = inflate(strm, *eof ? Z_FINISH : Z_NO_FLUSH); if(ret != Z_OK && (ret != Z_STREAM_END || strm->avail_in != 0) && !(ret == Z_BUF_ERROR && strm->avail_out == 0)) { break; } } if(ret == Z_BUF_ERROR && strm->avail_out == 0) { return true; } *inflateRet = ret; if(ret == Z_OK) { return true; } if(ret == Z_STREAM_END && strm->avail_in == 0) { // the input should have really ended now // but some callbacks may need to be called again to return EOF if(!*eof) { size_t readSize = 0; *eof = callback((const void**)lastData, &readSize, callbackArg); } return *eof; } return false; } static void cc4group_setLazy(CC4Group* const this, const bool lazy) { assert(this->root.children == NULL); this->lazy = lazy; } static void cc4group_cleanupReadState(CC4Group* const this) { if(this->readState.lastData != NULL && this->lazy) { cc4group_applyMemoryManagementEnd(this->readState.callback.memoryManagement, this->readState.lastData); } // indicate end of prolonged use (for reference counting) cc4group_applyMemoryManagementEnd(this->readState.callback.memoryManagement, NULL); if(this->readState.callback.deinit != NULL) { if(!this->readState.callback.deinit(this->readState.callback.arg)) { cc4group_warn(this, "cc4group_cleanupReadState: the deinitCallback failed"); } } inflateEnd(&this->readState.gzStrm); this->readState.active = false; } static bool cc4group_uncompressUntilPosition(CC4Group* const this, size_t const position) { CC4Group* rootGroup = this; while(rootGroup->parent != NULL) { rootGroup = rootGroup->parent; } assert(rootGroup); assert(position <= rootGroup->uncompressedSize); if(rootGroup->readState.active && rootGroup->readState.position < position) { rootGroup->readState.gzStrm.avail_out = position - rootGroup->readState.position; bool eof = false; int inflateRet; if(!cc4group_inflateFillOutput(&rootGroup->readState.gzStrm, rootGroup->readState.callback.read, rootGroup->readState.callback.arg, rootGroup->readState.callback.memoryManagement, &eof, &rootGroup->readState.lastData, &inflateRet)) { SET_ZERROR_ERROR("inflate: inflating the group contents", inflateRet); cc4group_cleanupReadState(rootGroup); return false; } if(position == rootGroup->uncompressedSize) { cc4group_cleanupReadState(rootGroup); } rootGroup->readState.position = position; } // update for child groups if(this != rootGroup) { this->readState.position = position; } return true; } static bool cc4group_uncompressGroup(CC4Group* const this, CC4Group_ReadCallback const readCallback, void* const callbackArg, CC4Group_MemoryManagement const memoryManagement, CC4Group_ReadSetupCallback const initCallback, CC4Group_ReadSetupCallback const deinitCallback) { assert(this); assert(readCallback); assert(callbackArg); // only if the group is still empty assert(this->root.children == NULL); // declare variables here already for proper cleanup in error cases C4GroupHeader* header = NULL; bool inflateStarted = false; uint8_t* retData = NULL; size_t currentSize = 0; CC4Group_CleanupJob tmpCleanup; uint8_t* uncompressedData = NULL; if(initCallback != NULL) { if(!initCallback(callbackArg)) { SET_MESSAGE_ERROR("The initCallback failed"); return false; } } size_t totalReadSize = 0; const uint8_t* readDataAfterMagic = NULL; size_t readSize = 0; bool eof = false; uint8_t magic1 = 0, magic2 = 0; while(!eof && totalReadSize < 2) { if(this->readState.lastData != NULL) { cc4group_applyMemoryManagementEnd(memoryManagement, this->readState.lastData); } this->readState.lastData = NULL; readSize = 0; eof = readCallback((const void**)&this->readState.lastData, &readSize, callbackArg); if(readSize == 0) { continue; } if(!cc4group_applyMemoryManagementStart(memoryManagement, &this->readState.lastData, readSize)) { SET_ZERROR_ERROR("reading the group magic", CC4GROUP_MEM_ERROR); goto ret; } size_t newTotalReadSize = totalReadSize + readSize; if(totalReadSize < 1 && newTotalReadSize > 0) { magic1 = this->readState.lastData[0]; } if(totalReadSize < 2 && newTotalReadSize > 1) { magic2 = this->readState.lastData[1 - totalReadSize]; readDataAfterMagic = this->readState.lastData + (2 - totalReadSize); } totalReadSize = newTotalReadSize; } if(eof && totalReadSize < 2) { SET_EOD_ERROR("reading the group magic"); goto ret; } static const uint8_t magic[] = {0x1f, 0x8b}; if((magic1 != C4GroupMagic1 && magic1 != magic[0]) || (magic2 != C4GroupMagic2 && magic2 != magic[1])) { SET_MESSAGE_ERROR("The file is not a valid group file. Magic bytes don't match."); goto ret; } this->readState.gzStrm.zalloc = NULL; this->readState.gzStrm.zfree = NULL; this->readState.gzStrm.opaque = NULL; this->readState.gzStrm.next_in = magic; this->readState.gzStrm.avail_in = 2; int ret = inflateInit2(&this->readState.gzStrm, 15 + 16); // window size 15 + automatic gzip inflateStarted = true; if(ret != Z_OK) { SET_ZERROR_ERROR("inflateInit2", ret); goto ret; } currentSize = sizeof(C4GroupHeader); header = malloc(currentSize); if(header == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for group header"); goto ret; } this->readState.gzStrm.next_out = (Bytef*)header; this->readState.gzStrm.avail_out = currentSize; ret = inflate(&this->readState.gzStrm, Z_SYNC_FLUSH); if(ret != Z_OK && (ret != Z_STREAM_END || this->readState.gzStrm.avail_in != 0)) { SET_ZERROR_ERROR("inflate: inflating the magic", ret); goto ret; } if(totalReadSize > 2) { this->readState.gzStrm.next_in = readDataAfterMagic; this->readState.gzStrm.avail_in = totalReadSize - 2; } if(!cc4group_inflateFillOutput(&this->readState.gzStrm, readCallback, callbackArg, memoryManagement, &eof, &this->readState.lastData, &ret)) { SET_ZERROR_ERROR("inflate: inflating the group header", ret); goto ret; } memScrambleHeader((uint8_t*)header); if(header->Entries == 0) { // the group is empty // so the stream should have ended also if(ret != Z_STREAM_END || !eof || this->readState.gzStrm.avail_in > 0) { SET_MALFORMED_MESSAGE_ERROR("The group is empty but the gzip stream has not ended yet."); goto ret; } retData = (void*)header; AddCleanUpJob(free, header); header = NULL; goto ret; } if(!cc4group_checkHeader(this, header)) { goto ret; } currentSize += sizeof(C4GroupEntryCore) * header->Entries; header = realloc(header, currentSize); if(header == NULL) { SET_ERRNO_ERROR("realloc: allocating memory for group entry directory"); goto ret; } C4GroupEntryCore* cores = (C4GroupEntryCore*)((uint8_t*)(header) + sizeof(C4GroupHeader)); this->readState.gzStrm.next_out = (Bytef*)cores; this->readState.gzStrm.avail_out = sizeof(C4GroupEntryCore) * header->Entries; if(!cc4group_inflateFillOutput(&this->readState.gzStrm, readCallback, callbackArg, memoryManagement, &eof, &this->readState.lastData, &ret)) { SET_ZERROR_ERROR("inflate: inflating the group entry dictionary", ret); goto ret; } C4GroupEntryCore* last = cores + header->Entries - 1; size_t uncompressedSize = last->Offset + last->Size; currentSize += uncompressedSize; if(currentSize == 0) { SET_MESSAGE_ERROR("Bug: the computed size of the whole uncompressed group somehow turned out to be 0"); goto ret; } uncompressedData = cc4group_tmpMemoryStrategy(this, currentSize, &tmpCleanup); if(uncompressedData == NULL) { goto ret; } uint8_t* data = (uint8_t*)(uncompressedData) + sizeof(C4GroupHeader) + sizeof(C4GroupEntryCore) * header->Entries; // write alredy decompressed header and cores into the new tmp memory memcpy(uncompressedData, header, data - uncompressedData); this->readState.gzStrm.next_out = data; this->readState.gzStrm.avail_out = uncompressedSize; if(!this->lazy) { if(!cc4group_inflateFillOutput(&this->readState.gzStrm, readCallback, callbackArg, memoryManagement, &eof, &this->readState.lastData, &ret)) { SET_ZERROR_ERROR("inflate: inflating the group contents", ret); goto ret; } if(this->readState.gzStrm.avail_in > 0) { // NOTE: This may miss additional garbage data at the end if the read callback happens to read exactly the right amount of data SET_MALFORMED_MESSAGE_ERROR("The group contents are read completely but more data is left to read"); goto ret; } } retData = uncompressedData; ret: if(header != NULL) { free(header); } if(retData == NULL) { if(deinitCallback != NULL) { if(!deinitCallback(callbackArg)) { cc4group_warn(this, "cc4group_uncompressGroup: the deinitCallback failed"); } } if(this->readState.lastData != NULL) { cc4group_applyMemoryManagementEnd(memoryManagement, this->readState.lastData); } if(inflateStarted) { inflateEnd(&this->readState.gzStrm); } if(uncompressedData != NULL) { tmpCleanup.func(tmpCleanup.data); } } else { if(uncompressedData != NULL) { CleanUpJobListPrepend(this->cleanupJobs, tmpCleanup); } this->readState.position = this->readState.gzStrm.next_out - retData; this->readState.callback.setup = initCallback; this->readState.callback.read = readCallback; this->readState.callback.deinit = deinitCallback; this->readState.callback.arg = callbackArg; this->readState.callback.memoryManagement = memoryManagement; if(this->lazy) { this->readState.active = true; // indicate prolonged use (for reference counting) cc4group_applyMemoryManagementStart(memoryManagement, NULL, 0); } else { cc4group_cleanupReadState(this); } } this->uncompressedData = retData; this->uncompressedSize = currentSize; if(retData == NULL) { return false; } return cc4group_buildGroupHierarchy(this); } typedef struct { const void* data; size_t size; const CC4Group* group; } CompleteDataReadCallbackArg; static bool cc4group_completeDataReadCallback(const void** const data, size_t* const size, void* callbackArg) { CompleteDataReadCallbackArg* argData = callbackArg; *data = argData->data; *size = argData->size; return true; } typedef struct { void* arg; void* buffer; } ChunkedReadData; static bool cc4group_initChunkBufferCallback(void* const callbackArg) { #define CHUNK_SIZE 1024 * 1024 void* buffer = malloc(CHUNK_SIZE); if(buffer == NULL) { return false; } ((ChunkedReadData*)callbackArg)->buffer = buffer; return true; } static bool cc4group_deinitChunkBufferCallback(void* const callbackArg) { free(((ChunkedReadData*)callbackArg)->buffer); free(callbackArg); return true; } typedef struct { ChunkedReadData chunkedData; int fd; } FdReadData; static bool cc4group_readFdReadCallback(const void** const data, size_t* const size, void* const callbackArg) { FdReadData* arg = callbackArg; void* buffer = arg->chunkedData.buffer; ssize_t count = read(arg->fd, buffer, CHUNK_SIZE); if(count > 0) { *data = buffer; *size = count; return false; } return true; } static bool cc4group_readFilePointerReadCallback(const void** const data, size_t* const size, void* const callbackArg) { ChunkedReadData* arg = callbackArg; void* buffer = arg->buffer; size_t count = fread(buffer, 1, CHUNK_SIZE, arg->arg); if(count > 0) { *data = buffer; *size = count; return false; } return true; #undef CHUNK_SIZE } static bool cc4group_openMemory(CC4Group* const this, const void* const compressedData, size_t const size, CC4Group_MemoryManagement const memoryManagement) { assert(this); assert(compressedData); assert(size); CompleteDataReadCallbackArg data = {.data = compressedData, .size = size, .group = this}; return cc4group_uncompressGroup(this, cc4group_completeDataReadCallback, &data, memoryManagement, NULL, NULL); } static bool cc4group_cleanupMappedGroupFile(void* const arg) { CompleteDataReadCallbackArg* data = arg; if(cc4group_munmap((void*)data->data, data->size) == -1) { cc4group_warn(data->group, "munmap: Unmapping the group file failed: %s", strerror(errno)); } free(data); return true; } static bool cc4group_uncompressGroupFromFile(CC4Group* const this, const char* const path) { uint8_t* mappedFile = MAP_FAILED; bool success = false; char* slash = NULL; char* tmpPath = strdup(path); if(tmpPath == NULL) { SET_ERRNO_ERROR("strdup: duplicating the path"); return false; } char* subPath = NULL; int file = -1; for(;;) { file = open(tmpPath, O_RDONLY | O_BINARY); if(file != -1) { break; } if(errno != ENOTDIR) { SET_ERRNO_ERROR("open: Opening group file"); goto ret; } char* oldSlash = slash; slash = strrchr(tmpPath, '/'); if(oldSlash != NULL) { *oldSlash = '/'; } if(slash != NULL && slash != tmpPath) { *slash = '\0'; subPath = slash + 1; } } if(subPath != NULL && subPath[0] != '\0') { CC4Group* const parent = cc4group.new(); if(parent == NULL) { SET_ERRNO_ERROR("malloc: allocating the parent group"); goto ret; } cc4group.setLazy(parent, this->lazy); if(!cc4group.openExisting(parent, tmpPath)) { cc4group_copyErrorInformation(parent, this); this->error.method = "Nested cc4group.openExisting opening the parent group"; goto ret2; } if(this->name != NULL) { free((void*)this->name); this->name = NULL; } if(this->path != NULL) { free((void*)this->path); this->path = NULL; } if(!cc4group_openAsChildOn(parent, subPath, this)) { cc4group_copyErrorInformation(parent, this); this->error.method = "cc4group.openAsChild on parent group"; goto ret2; } success = true; ret2: cc4group.delete(parent); goto ret; } struct stat st; if(fstat(file, &st) == -1) { SET_ERRNO_ERROR("fstat: on the opened group file"); goto ret; } if(S_ISDIR(st.st_mode)) { SET_MESSAGE_ERROR("The specified group file is a directory"); goto ret; } off_t size = st.st_size; mappedFile = cc4group_mmap(NULL, size, PROT_READ, MAP_PRIVATE, file, 0); if(close(file) == -1) { SET_ERRNO_ERROR("close: Closing the group file"); goto ret; } if(mappedFile == MAP_FAILED) { SET_ERRNO_ERROR("mmap: Mapping the group file"); goto ret; } CompleteDataReadCallbackArg* data = malloc(sizeof(CompleteDataReadCallbackArg)); if(data == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for cleanup data failed"); goto ret; } *data = (CompleteDataReadCallbackArg){.data = mappedFile, .size = size, .group = this}; success = cc4group_uncompressGroup(this, cc4group_completeDataReadCallback, data, cc4group.MemoryManagement.Reference, NULL, cc4group_cleanupMappedGroupFile); mappedFile = MAP_FAILED; ret: if(mappedFile != MAP_FAILED) { if(cc4group_munmap(mappedFile, size) == -1) { cc4group_warn(this, "munmap: Unmapping the group file failed: %s", strerror(errno)); } } free(tmpPath); return success; } static void cc4group_init(CC4Group* const this) { assert(this); this->referenceCounter = 1; this->parent = NULL; this->lazy = true; this->path = NULL; this->name = NULL; this->pathInParent = NULL; this->uncompressedData = NULL; this->uncompressedSize = 0; this->root.data = NULL; this->root.memoryManagement = cc4group.MemoryManagement.Reference; this->root.parent = NULL; this->root.children = NULL; this->root.core.Directory = true; this->root.path = NULL; this->cleanupJobs = CleanUpJobListNew(); this->error.code = 0; this->error.method = NULL; this->error.formatter.data = NULL; this->error.formatter.formatter = cc4group_noerrorFormatter; this->error.lastFormattedMessage = NULL; this->readState.active = false; this->readState.lastData = NULL; } static bool cc4group_setMakerRecursively(CC4Group* const this, const C4GroupEntryData* groupEntry, const char* const maker) { GroupEntryList* children = cc4group_getChildren(this, groupEntry); if(children == NULL) { return false; } ForeachGroupEntry(children) { if(entry->value.core.Directory) { C4GroupHeader* header = cc4group_getHeader(this, &entry->value); if(header == NULL) { return false; } C4GroupHeader_setMaker(header, maker); if(!cc4group_setMakerRecursively(this, &entry->value, maker)) { return false; } } } return true; } static bool cc4group_setMaker(CC4Group* const this, const char* const maker, const char* const path, bool const recursive) { assert(this); assert(maker); const C4GroupEntryData* entry = cc4group_getDirectoryByPath(this, path, true, NULL); if(entry == NULL) { return false; } C4GroupHeader* header = cc4group_getHeader(this, entry); if(header == NULL) { return false; } C4GroupHeader_setMaker(header, maker); if(recursive) { return cc4group_setMakerRecursively(this, entry, maker); } return true; } static bool cc4group_setOfficialRecursively(CC4Group* const this, const C4GroupEntryData* groupEntry, bool const official) { GroupEntryList* children = cc4group_getChildren(this, groupEntry); if(children == NULL) { return false; } ForeachGroupEntry(children) { if(entry->value.core.Directory) { C4GroupHeader* header = cc4group_getHeader(this, &entry->value); if(header == NULL) { return false; } C4GroupHeader_setOfficial(header, official); if(!cc4group_setOfficialRecursively(this, &entry->value, official)) { return false; } } } return true; } static bool cc4group_setOfficial(CC4Group* const this, bool const official, const char* const path, bool const recursive) { assert(this); const C4GroupEntryData* entry = cc4group_getDirectoryByPath(this, path, true, NULL); if(entry == NULL) { return false; } C4GroupHeader* header = cc4group_getHeader(this, entry); if(header == NULL) { return false; } C4GroupHeader_setOfficial(header, official); if(recursive) { return cc4group_setOfficialRecursively(this, entry, official); } return true; } static bool cc4group_setCreationRecursively(CC4Group* const this, const C4GroupEntryData* groupEntry, int32_t const creation) { GroupEntryList* children = cc4group_getChildren(this, groupEntry); if(children == NULL) { return false; } ForeachGroupEntry(children) { entry->value.core.Modified = creation; if(entry->value.core.Directory) { C4GroupHeader* header = cc4group_getHeader(this, &entry->value); if(header == NULL) { return false; } C4GroupHeader_setCreation(header, creation); if(!cc4group_setCreationRecursively(this, &entry->value, creation)) { return false; } } } return true; } static bool cc4group_setCreation(CC4Group* const this, int32_t const creation, const char* const path, bool const recursive) { assert(this); C4GroupEntryData* entry = (C4GroupEntryData*)cc4group_getFileOrDirectoryByPath(this, path, true, NULL); if(entry == NULL) { return false; } entry->core.Modified = creation; if(entry->core.Directory) { C4GroupHeader* header = cc4group_getHeader(this, entry); if(header == NULL) { return false; } C4GroupHeader_setCreation(header, creation); if(recursive) { return cc4group_setCreationRecursively(this, entry, creation); } } return true; } static bool cc4group_initNewHeader(CC4Group* const this) { assert(this); this->root.header = malloc(sizeof(C4GroupHeader)); if(this->root.header == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for a new group header"); return false; } AddCleanUpJob(free, this->root.header); C4GroupHeader_init(this->root.header); return true; } static CC4Group* cc4group_new(void) { CC4Group* this = malloc(sizeof(CC4Group)); if(this != NULL) { cc4group_init(this); } return this; } static bool cc4group_create(CC4Group* const this) { assert(this); if(!cc4group_initNewHeader(this)) { return false; } this->root.children = GroupEntryListNew(); AddCleanUpJob(deleteChildren, this->root.children); return true; } static bool cc4group_openFd(CC4Group* const this, int const fd) { // assert is in cc4group_uncompressGroup FdReadData* arg = malloc(sizeof(FdReadData)); if(arg == NULL) { SET_ERRNO_ERROR("malloc: Allocating memory for temporary data"); return false; } arg->fd = fd; return cc4group_uncompressGroup(this, cc4group_readFdReadCallback, arg, cc4group.MemoryManagement.Reference, cc4group_initChunkBufferCallback, cc4group_deinitChunkBufferCallback); } static bool cc4group_openFilePointer(CC4Group* const this, FILE* const file) { // assert(this) is in cc4group_uncompressGroup assert(file); ChunkedReadData* arg = malloc(sizeof(ChunkedReadData)); if(arg == NULL) { SET_ERRNO_ERROR("malloc: Allocating memory for temporary data"); return false; } arg->arg = file; return cc4group_uncompressGroup(this, cc4group_readFilePointerReadCallback, arg, cc4group.MemoryManagement.Reference, cc4group_initChunkBufferCallback, cc4group_deinitChunkBufferCallback); } static bool cc4group_isDirectory(const char* const path) { struct stat st; return stat(path, &st) != -1 && S_ISDIR(st.st_mode); } static char* cc4group_pathCombine(const char* const firstPart, const char* secondPart) { if(firstPart == NULL) { assert(secondPart); return strdup(secondPart); } if(secondPart == NULL) { assert(firstPart); return strdup(firstPart); } if(*secondPart == '/') { ++secondPart; } size_t firstLen = strlen(firstPart); if(firstLen && firstPart[firstLen - 1] == '/') { --firstLen; } char* const result = malloc(firstLen + 1 + strlen(secondPart) + 1); // '/' and '\0' if(result != NULL) { strncpy(result, firstPart, firstLen); result[firstLen] = '/'; strcpy(result + firstLen + 1, secondPart); } return result; } static void* cc4group_memoryManagementOtherGroupStart(void* const data, size_t const size, void* const arg) { (void)data; (void)size; ++((CC4Group*)arg)->referenceCounter; return data; } static void cc4group_memoryManagementOtherGroupEnd(void* const data, void* const arg) { (void)data; cc4group_unreference(arg); } static const CC4Group_MemoryManagement_t otherGroupMemoryManagement = { .start = cc4group_memoryManagementOtherGroupStart, .end = cc4group_memoryManagementOtherGroupEnd, }; static bool cc4group_deleteEntryFromParent(CC4Group* const this, GroupEntryListEntry* const deleteEntry, const C4GroupEntryData* const parent, bool recursive) { if(deleteEntry->value.core.Directory) { if(!recursive) { SET_MESSAGE_ERROR("The desired entry is a subgroup but a non-recursive deletion was requested"); return false; } // if they are not loaded yet, there is no need to delete them if(deleteEntry->value.children != NULL) { deleteChildren(deleteEntry->value.children); } } if(deleteEntry->value.data != NULL) { cc4group_applyMemoryManagementEnd(deleteEntry->value.memoryManagement, deleteEntry->value.data); } if(deleteEntry->value.path) { free((void*)deleteEntry->value.path); } GroupEntryList* parentChildren = cc4group_getChildren(this, parent); assert(parentChildren); // if there was an error, it should have really been catched already in cc4group_getParentAndChildEntries GroupEntryListRemove(parentChildren, deleteEntry); parent->header->Entries = GroupEntryListSize(parentChildren); // header is loaded already for the children return true; } static bool cc4group_addOtherGroupContents(CC4Group* const this, C4GroupEntryData* const contentsRoot, CC4Group* const group, GroupEntryList* entries) { if(entries == NULL) { SET_MESSAGE_ERROR("Could not retrieve children list of other group."); return false; } CC4Group_MemoryManagement memoryMgmt = otherGroupMemoryManagement; memoryMgmt.arg = group; ForeachGroupEntry(entries) { C4GroupEntryData* newEntry; if(entry->value.core.Directory) { newEntry = cc4group_createDirectory(this, entry->value.core.FileName, contentsRoot); newEntry->children = GroupEntryListNew(); if(newEntry == NULL) { return false; } C4GroupHeader_setCreation(newEntry->header, entry->value.header->Creation); C4GroupHeader_setOfficial(newEntry->header, entry->value.header->Official); C4GroupHeader_setMaker(newEntry->header, entry->value.header->Maker); if(!cc4group_addOtherGroupContents(this, newEntry, group, cc4group_getChildren(group, &entry->value))) { return false; } } else { newEntry = cc4group_createEntry(this, entry->value.core.FileName, contentsRoot); if(newEntry == NULL) { return false; } if(entry->value.data != NULL) { newEntry->data = entry->value.data; if(!cc4group_applyMemoryManagementStart(memoryMgmt, (const uint8_t**)&newEntry->data, newEntry->core.Size)) { assert(!"This must not happen."); } newEntry->memoryManagement = memoryMgmt; } } newEntry->core.HasCRC = C4GroupEntryCore_NoCRC; newEntry->core.Executable = entry->value.core.Executable; newEntry->core.Modified = entry->value.core.Modified; newEntry->core.Size = entry->value.core.Size; } return true; } static C4GroupEntryData* cc4group_addGroup(CC4Group* const this, CC4Group* const group, const char* const targetPath, C4GroupEntryData* const parent) { assert(this); C4GroupEntryData* const contentsRoot = cc4group_createDirectory(this, targetPath, parent); if(!contentsRoot) { return NULL; } contentsRoot->children = GroupEntryListNew(); C4GroupHeader_setCreation(contentsRoot->header, group->root.header->Creation); C4GroupHeader_setOfficial(contentsRoot->header, group->root.header->Official); C4GroupHeader_setMaker(contentsRoot->header, group->root.header->Maker); contentsRoot->core.Size = group->uncompressedSize; if(cc4group_addOtherGroupContents(this, contentsRoot, group, group->root.children)) { return contentsRoot; } const char* errorMessage = cc4group.getErrorMessage(this); const C4GroupEntryData* deleteEntryparent; GroupEntryListEntry* deleteEntry; if(!cc4group_getParentAndChildEntries(this, targetPath, parent, &deleteEntryparent, &deleteEntry) && cc4group_deleteEntryFromParent(this, deleteEntry, deleteEntryparent, true)) { cc4group_warn(this, "Loading contents from \"%s\" into this group \"%s\" failed with error: %s\n" "Additionally, removing the directory which should have held the other group's contents failed. The error information stored in the group is about the failed deletion.", cc4group.getFullName(group), cc4group.getFullName(this), errorMessage); } return NULL; } // takes over memory management of filePath static C4GroupEntryData* cc4group_addFileFromDisk(CC4Group* const this, const char* const filePath, const char* const targetPath, C4GroupEntryData* const parent, bool handleGroup) { assert(this); assert(filePath); assert(targetPath); C4GroupEntryData* result = NULL; struct stat st; if(stat(filePath, &st) == -1) { SET_ERRNO_ERROR("stat: getting information of a contained file"); goto ret; } CC4Group* group = NULL; if(handleGroup) { group = cc4group_new(); cc4group_setLazy(group, false); if(!cc4group_openExisting(group, filePath)) { cc4group_unreference(group); } else { C4GroupEntryData* contentsRoot = cc4group_addGroup(this, group, targetPath, parent); if(contentsRoot != NULL) { free((void*)filePath); cc4group_unreference(group); return contentsRoot; } } } result = cc4group_createEntry(this, targetPath, parent); if(result == NULL) { goto ret; } if(!S_ISREG(st.st_mode)) { SET_MESSAGE_ERROR("Unsupported directory entry type found. Only regular files and directories are supported."); goto ret; } result->core.Executable = (st.st_mode & S_IXUSR) ? 1 : 0; result->core.Modified = st.st_mtime; result->core.Size = st.st_size; result->path = filePath; ret: if(result == NULL || result->path == NULL) { free((void*)filePath); } return result; } // takes over memory management of directoryPath static bool cc4group_addDirectoryFromDisk(CC4Group* const this, const char* directoryPath, const char* const targetPath, C4GroupEntryData* const parent) { assert(this); assert(directoryPath); bool success = false; if(!cc4group_isDirectory(directoryPath)) { SET_MESSAGE_ERROR("The specified path is not a directory"); goto ret; } C4GroupEntryData* existingDirectory = targetPath == NULL ? &this->root : (C4GroupEntryData*)cc4group_getDirectoryByPath(this, targetPath, false, parent); if(existingDirectory == NULL) { existingDirectory = cc4group_createDirectory(this, targetPath, parent); if(existingDirectory == NULL) { goto ret; } } struct stat st; if(stat(directoryPath, &st) == -1) { SET_ERRNO_ERROR("stat: getting information of a directory"); goto ret; } existingDirectory->core.Modified = st.st_mtime; existingDirectory->path = directoryPath; directoryPath = NULL; success = true; ret: if(directoryPath != NULL) { free((void*)directoryPath); } return success; } static bool cc4group_populateDirectoryFromDisk(CC4Group* const this, C4GroupEntryData* const entry) { assert(this); assert(entry); assert(entry->core.Directory); assert(entry->path); assert(!entry->children); entry->children = GroupEntryListNew(); DIR* dir = opendir(entry->path); if(dir == NULL) { SET_ERRNO_ERROR("opendir: Opening the directoryPath"); return false; } bool success = false; for(;;) { errno = 0; struct dirent* dirEntry = readdir(dir); if(dirEntry == NULL) { success = errno == 0; if(!success) { SET_ERRNO_ERROR("readdir: reading the next entry"); } break; } if(strcmp(dirEntry->d_name, ".") == 0 || strcmp(dirEntry->d_name, "..") == 0) { continue; } char* directoryEntryPath = cc4group_pathCombine(entry->path, dirEntry->d_name); if(directoryEntryPath == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for the temporary path of an entry in the directory"); break; } if(cc4group_isDirectory(directoryEntryPath)) { if(!cc4group_addDirectoryFromDisk(this, directoryEntryPath, dirEntry->d_name, entry)) { success = false; break; } } else { if(!cc4group_addFileFromDisk(this, directoryEntryPath, dirEntry->d_name, entry, true)) { success = false; break; } } } entry->header->Entries = GroupEntryListSize(entry->children); if(closedir(dir) == -1) { cc4group_warn(this, "closedir: Closing the directory stream \"%s\" failed: %s", entry->path, strerror(errno)); } return success; } static bool cc4group_openDirectory(CC4Group* const this, const char* const path) { assert(this); assert(path); if(!cc4group_isDirectory(path)) { SET_MESSAGE_ERROR("The specified path is not a directory"); return false; } this->root.core.Directory = 1; this->root.core.FileName[0] = '\0'; this->lazy = false; if(!cc4group_initNewHeader(this)) { return false; } if(cc4group_addDirectoryFromDisk(this, strdup(path), NULL, NULL) && cc4group_populateDirectoryFromDisk(this, &this->root)) { AddCleanUpJob(deleteChildren, this->root.children); return true; } else { return false; } } static bool cc4group_openExisting(CC4Group* const this, const char* const path) { assert(this); assert(path); if(strcmp(path, "-") == 0) { SET_BINARY(STDIN_FILENO); return cc4group_openFd(this, STDIN_FILENO); } this->path = cc4group_absolutePath(path); this->name = cc4group_basename(path); if(cc4group_isDirectory(path)) { return cc4group_openDirectory(this, path); } return cc4group_uncompressGroupFromFile(this, path); } static void cc4group_unreference(CC4Group* const this) { assert(this); if(--this->referenceCounter == 0) { cc4group_delete(this); } } static void cc4group_delete(CC4Group* const this) { assert(this); if(this->readState.active) { cc4group_cleanupReadState(this); } ForeachCleanupJob(this->cleanupJobs) { job->value.func(job->value.data); } CleanUpJobListDestroy(this->cleanupJobs); if(this->error.lastFormattedMessage != NULL) { free(this->error.lastFormattedMessage); } if(this->root.path != NULL) { free((void*)this->root.path); } if(this->path != NULL) { free((void*)this->path); } if(this->name != NULL) { free((void*)this->name); } if(this->pathInParent != NULL) { free((void*)this->pathInParent); } free(this); } static bool cc4group_extractEntry(CC4Group* const this, const C4GroupEntryData* const root, const char* const targetPath) { assert(root); assert(!root->core.Directory); int file = open(targetPath, O_WRONLY | O_BINARY | O_CREAT | O_EXCL, root->core.Executable ? 0755 : 0644); if(file == -1) { SET_ERRNO_ERROR("open: Creating target file"); return false; } const uint8_t* data = cc4group_getOnlyEntryData(this, root); if(data == NULL) { return false; } bool success = true; if(write(file, data, root->core.Size) != root->core.Size) { SET_ERRNO_ERROR("write: Writing to the target file"); success = false; } if(close(file) == -1) { cc4group_warn(this, "close: Closing the extracted file \"%s\" failed: %s", targetPath, strerror(errno)); } if(success) { struct utimbuf times = {.actime = root->core.Modified, .modtime = root->core.Modified}; if(utime(targetPath, ×) == -1) { cc4group_warn(this, "utime: Setting modification time for \"%s\" failed: %s", targetPath, strerror(errno)); } } return success; } static bool cc4group_extractChildren(CC4Group* const this, const C4GroupEntryData* const root, const char* const targetPath, bool const rename) { assert(root); char* tmpPath = malloc((strlen(targetPath) + 1 + strlen(root->core.FileName) + 1) * sizeof(*tmpPath)); // + 1 for '/' + 1 for '\0' if(tmpPath == NULL) { SET_ERRNO_ERROR("malloc: Allocating memory for target path"); return false; } strcpy(tmpPath, targetPath); if(!rename) { if(*targetPath != '\0') { strcat(tmpPath, "/"); } strcat(tmpPath, root->core.FileName); } bool success = true; if(root->core.Directory) { if(cc4group_mkdir(tmpPath, 0755) == -1 /*&& errno != EEXIST*/) { SET_ERRNO_ERROR("mkdir: Creating target directory"); success = false; goto ret; } GroupEntryList* children = cc4group_getChildren(this, root); if(children == NULL) { success = false; goto ret; } ForeachGroupEntry(children) { if(!cc4group_extractChildren(this, &entry->value, tmpPath, false)) { success = false; goto ret; } } struct utimbuf times = {.actime = root->header->Creation, .modtime = root->header->Creation}; if(utime(tmpPath, ×) == -1) { cc4group_warn(this, "utime: Setting modification time for \"%s\" failed: %s", tmpPath, strerror(errno)); } success = true; goto ret; } else { success = cc4group_extractEntry(this, root, tmpPath); goto ret; } ret: free(tmpPath); return success; } static bool cc4group_extractAll(CC4Group* const this, const char* const targetPath) { assert(this); return cc4group_extractChildren(this, &this->root, targetPath, false); } // sets *error to true if any error is set static GroupEntryListEntry* cc4group_getChildListEntryByPath(CC4Group* const this, const char* const entryPath, bool* const error, const C4GroupEntryData* const inParent) { assert(this); assert(this->root.children); assert(entryPath != NULL && *entryPath != '\0'); const C4GroupEntryData* currentParent = inParent ? inParent : &this->root; GroupEntryListEntry* listEntry = NULL; char* path = strdup(entryPath); if(path == NULL) { *error = true; SET_ERRNO_ERROR("strdup: duplicating the path"); return NULL; } for(char* tokenIn = path, *savePtr; ; tokenIn = NULL) { const char* part = strtok_r(tokenIn, "/", &savePtr); if(part == NULL || *part == '\0') { break; } else if(!currentParent->core.Directory) { listEntry = NULL; break; } GroupEntryList* children = cc4group_getChildren(this, currentParent); if(!children) { *error = true; listEntry = NULL; goto ret; } bool found = false; ForeachGroupEntry(children) { if(cc4group_namecmp(part, entry->value.core.FileName) == 0) { currentParent = &entry->value; listEntry = entry; found = true; break; } } if(!found) { listEntry = NULL; break; } } ret: free(path); return listEntry; } // sets *error to true if any error is set static const C4GroupEntryData* cc4group_getEntryByPath(CC4Group* const this, const char* const entryPath, bool const allowRoot, bool* const error, const C4GroupEntryData* const inParent) { assert(this); assert(this->root.children); if(entryPath == NULL || *entryPath == '\0') { if(allowRoot) { return inParent ? inParent : &this->root; } else { SET_MESSAGE_ERROR("The root directory is not allowed for this"); *error = true; return NULL; } } GroupEntryListEntry* listEntry = cc4group_getChildListEntryByPath(this, entryPath, error, inParent); if(*error || listEntry == NULL) { return NULL; } return &listEntry->value; } // sets error static const C4GroupEntryData* cc4group_getDirectoryByPath(CC4Group* const this, const char* const entryPath, bool const allowRoot, const C4GroupEntryData* const inParent) { // asserts are in cc4group_getEntryByPath bool error = false; const C4GroupEntryData* entry = cc4group_getEntryByPath(this, entryPath, allowRoot, &error, inParent); if(error) { return NULL; } if(entry == NULL) { SET_MESSAGE_ERROR("The directory was not found in the group file"); return NULL; } if(!entry->core.Directory) { SET_MESSAGE_ERROR("The specified path is a file, not a directory"); return NULL; } return entry; } // sets error static const C4GroupEntryData* cc4group_getFileByPath(CC4Group* const this, const char* const entryPath, const C4GroupEntryData* const inParent) { // asserts are in cc4group_getEntryByPath bool error = false; const C4GroupEntryData* entry = cc4group_getEntryByPath(this, entryPath, true, &error, inParent); if(error) { return NULL; } if(entry == NULL) { SET_MESSAGE_ERROR("The file was not found in the group file"); return NULL; } if(entry->core.Directory) { SET_MESSAGE_ERROR("The specified path is a directory, not a file"); return NULL; } return entry; } // sets error static const C4GroupEntryData* cc4group_getFileOrDirectoryByPath(CC4Group* const this, const char* const entryPath, bool const allowRoot, const C4GroupEntryData* const inParent) { // asserts are in cc4group_getEntryByPath bool error = false; const C4GroupEntryData* entry = cc4group_getEntryByPath(this, entryPath, allowRoot, &error, inParent); if(error) { return false; } if(entry == NULL) { SET_MESSAGE_ERROR("The file or directory was not found in the group file"); return NULL; } return entry; } static bool cc4group_extractSingle(CC4Group* const this, const char* const entryPath, const char* const targetPath) { assert(this); assert(targetPath); const C4GroupEntryData* entry = cc4group_getFileOrDirectoryByPath(this, entryPath, true, NULL); if(entry == NULL) { return false; } return cc4group_extractChildren(this, entry, targetPath, false); } static bool cc4group_extractSingleRename(CC4Group* const this, const char* const entryPath, const char* const targetPath) { assert(this); assert(targetPath); const C4GroupEntryData* entry = cc4group_getFileOrDirectoryByPath(this, entryPath, true, NULL); if(entry == NULL) { return false; } return cc4group_extractChildren(this, entry, targetPath, true); } static void* cc4group_mmappedFileManagementStart(void* const data, size_t const size, void* const arg) { (void)size; (void)arg; return data; } typedef struct { size_t size; int fd; const CC4Group* group; } CC4GroupMMappedFileManagementData; static void cc4group_mmappedFileManagementEnd(void* const data, void* const arg) { (void)data; CC4GroupMMappedFileManagementData* argData = arg; if(cc4group_munmap(data, argData->size) == -1) { cc4group_warn(argData->group, "munmap: unmapping a mmapped file failed: %s", strerror(errno)); } if(argData->fd != -1) { if(close(argData->fd) == -1) { cc4group_warn(argData->group, "close: closing a file after unmapping (and also directly after mapping) it failed: %s", strerror(errno)); } } free(argData); } static const uint8_t* cc4group_getOnlyEntryData(CC4Group* const this, const C4GroupEntryData* entry) { if(this->lazy && entry->path == NULL) { if(!entry->data && entry->core.Size > 0) { if(!cc4group_uncompressUntilPosition(this, entry->absolutePosition + entry->core.Size)) { return NULL; } ((C4GroupEntryData*)entry)->data = this->uncompressedData + entry->absolutePosition; } } else if(entry->path != NULL && entry->data == NULL) { int file = open(entry->path, O_RDONLY | O_BINARY); if(file == -1) { SET_ERRNO_ERROR("open: Opening the needed file"); return NULL; } void* mapped = cc4group_mmap(NULL, entry->core.Size, PROT_READ, MAP_PRIVATE, file, 0); if(mapped == MAP_FAILED) { SET_ERRNO_ERROR("mmap: Mapping the file to add/load from disk"); if(close(file) == -1) { cc4group_warn(this, "close: after mmapping the file \"%s\" failed, closing it failed too: %s", entry->path, strerror(errno)); } return NULL; } CC4GroupMMappedFileManagementData* management = malloc(sizeof(*management)); if(management == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for temporary memory management"); return NULL; } *management = (CC4GroupMMappedFileManagementData){ .size = entry->core.Size, .fd = -1, .group = this }; if(close(file) == -1) { management->fd = file; } ((C4GroupEntryData*)entry)->data = mapped; ((C4GroupEntryData*)entry)->memoryManagement = (CC4Group_MemoryManagement){ .start = cc4group_mmappedFileManagementStart, .end = cc4group_mmappedFileManagementEnd, .arg = management }; } return entry->data; } static C4GroupHeader* cc4group_getHeader(CC4Group* const this, const C4GroupEntryData* const entry) { assert(this); assert(entry); assert(entry->core.Directory); if(entry->header == NULL || (entry->path != NULL && entry->children == NULL)) { if(entry->path != NULL) { cc4group_populateDirectoryFromDisk(this, (C4GroupEntryData*)entry); return entry->header; } else { if(!cc4group_uncompressUntilPosition(this, entry->absolutePosition + sizeof(C4GroupHeader))) { return NULL; } } ((C4GroupEntryData*)entry)->data = this->uncompressedData + entry->absolutePosition; memScrambleHeader(entry->data); if(!cc4group_checkHeader(this, entry->header)) { return NULL; } } return entry->header; } static GroupEntryList* cc4group_getChildren(CC4Group* const this, const C4GroupEntryData* const entry) { if(entry->children == NULL) { if(cc4group_getHeader(this, entry) == NULL) { return NULL; } if(entry->path != NULL) { // should have been handled by getHeader if possible return entry->children; } else { if(!cc4group_uncompressUntilPosition(this, entry->absolutePosition + sizeof(C4GroupHeader) + entry->header->Entries * sizeof(C4GroupEntryCore))) { return NULL; } if(!buildChildren(this, (C4GroupEntryData*)entry, entry->core.Size)) { return NULL; } } } return entry->children; } static bool cc4group_getEntryData(CC4Group* const this, const char* const entryPath, const void** const data, size_t* const size) { assert(this); assert(data); assert(size); const C4GroupEntryData* entry = cc4group_getFileByPath(this, entryPath, NULL); if(entry == NULL) { return false; } *data = cc4group_getOnlyEntryData(this, entry); *size = entry->core.Size; return *data != NULL || *size == 0; } static void cc4group_setTmpMemoryStrategy(const CC4Group_TmpMemoryStrategy strategy) { cc4group_tmpMemoryStrategy = strategy; } static void cc4group_setWarningCallback(const CC4Group_WarningCallback warningCallback) { cc4group_warn = (warningCallback == NULL) ? cc4group_defaultWarningCallback : warningCallback; } static void cc4group_setEntryNameMatchingCallback(const CC4Group_EntryNameMatchingCallback matchingCallback) { cc4group_namecmp = (matchingCallback == NULL) ? cc4group_defaultEntryNameMatchingCallback : matchingCallback; } static const char* cc4group_getErrorMessage(CC4Group* const this) { assert(this); if(this->error.lastFormattedMessage != NULL) { free(this->error.lastFormattedMessage); } this->error.lastFormattedMessage = this->error.formatter.formatter(this->error.code, this->error.method, this->error.causer, this->error.formatter.data); return this->error.lastFormattedMessage; } static int32_t cc4group_getErrorCode(const CC4Group* const this) { assert(this); return this->error.code; } static const char* cc4group_getErrorMethod(const CC4Group* const this) { assert(this); return this->error.method; } static const char* cc4group_getErrorCauser(const CC4Group* const this) { assert(this); return this->error.causer; } static size_t cc4group_calculateEntrySizes(CC4Group* const this, C4GroupEntryData* const entryData) { if(entryData->core.Directory) { GroupEntryList* children = cc4group_getChildren(this, entryData); if(children == NULL) { return 0; } size_t sum = sizeof(C4GroupHeader) + GroupEntryListSize(children) * sizeof(C4GroupEntryCore); ForeachGroupEntry(children) { sum += cc4group_calculateEntrySizes(this, &entry->value); } return entryData->core.Size = sum; } else { return entryData->core.Size; } } static void cc4group_calculateEntryCRC(CC4Group* const this, C4GroupEntryData* const groupEntry) { if(groupEntry->core.Directory) { GroupEntryList* children = cc4group_getChildren(this, groupEntry); if(children == NULL) { return; } uint32_t crc = 0; ForeachGroupEntry(children) { cc4group_calculateEntryCRC(this, &entry->value); crc ^= entry->value.core.CRC; } groupEntry->core.HasCRC = C4GroupEntryCore_ContentsFileNameCRC; groupEntry->core.CRC = crc; } else if(groupEntry->core.HasCRC != C4GroupEntryCore_ContentsFileNameCRC) { const uint8_t* data = cc4group_getOnlyEntryData(this, groupEntry); if(data == NULL) { return; } uint32_t crc = crc32(0, data, groupEntry->core.Size); crc = crc32(crc, (uint8_t*)groupEntry->core.FileName, strlen(groupEntry->core.FileName)); groupEntry->core.HasCRC = C4GroupEntryCore_ContentsFileNameCRC; groupEntry->core.CRC = crc; } } static bool cc4group_flushBufferedWrite(WriteCallback* const callback) { bool success = true; if(callback->position > 0) { success = callback->callback(callback->buffer, callback->position, callback->arg); callback->position = 0; } return success; } static bool cc4group_bufferedWriteToCallback(WriteCallback* const callback, const void* const data, size_t size) { assert(callback); assert(data || size == 0); size_t dataPosition = 0; while(size > 0) { size_t amount = size - dataPosition; size_t bufferLeft = callback->bufferSize - callback->position; if(amount > bufferLeft) { amount = bufferLeft; } if(amount > 0) { memcpy(callback->buffer + callback->position, data + dataPosition, amount); callback->position += amount; dataPosition += amount; size -= amount; } assert(callback->position <= callback->bufferSize); if(callback->position == callback->bufferSize) { if(!cc4group_flushBufferedWrite(callback)) { return false; } } } return true; } static bool cc4group_deflateToCallback(WriteCallback* const callback, const void* const data, size_t const size, int const flushMode, int const expectedRet) { assert(callback); assert(data || size == 0); callback->gzStream.next_in = data; callback->gzStream.avail_in = size; int ret = Z_BUF_ERROR; // skip the gzip magic if not done yet if(!callback->magicDone) { static uint8_t magicDummy[2]; callback->gzStream.next_out = magicDummy; callback->gzStream.avail_out = 2; callback->magicDone = true; ret = deflate(&callback->gzStream, Z_NO_FLUSH); } while(ret == Z_BUF_ERROR || callback->gzStream.avail_in > 0 || (ret == Z_OK && flushMode == Z_FINISH)) { callback->gzStream.next_out = callback->buffer + callback->position; callback->gzStream.avail_out = callback->bufferSize - callback->position; ret = deflate(&callback->gzStream, flushMode); callback->position = callback->bufferSize - callback->gzStream.avail_out; if(callback->position == callback->bufferSize) { cc4group_flushBufferedWrite(callback); } if(ret == Z_STREAM_ERROR) { return false; } } assert(callback->gzStream.avail_in == 0); return ret == expectedRet; } static bool cc4group_writeEntries(CC4Group* const this, C4GroupEntryData* const entryData, WriteCallback* const callback) { const C4GroupHeader* origHeader = cc4group_getHeader(this, entryData); if(origHeader == NULL) { return false; } C4GroupHeader header = *origHeader; GroupEntryList* children = cc4group_getChildren(this, entryData); if(children == NULL) { return false; } header.Entries = GroupEntryListSize(children); header.Ver1 = 1; header.Ver2 = 2; if(header.Creation == 0) { header.Creation = time(NULL); } memScrambleHeader((uint8_t*)&header); if(!cc4group_deflateToCallback(callback, &header, sizeof(header), Z_NO_FLUSH, Z_OK)) { SET_MESSAGE_ERROR("Failed writing a group header"); return false; } size_t offset = 0; ForeachGroupEntry(children) { entry->value.core.Offset = offset; entry->value.core.Packed = 1; if(!cc4group_deflateToCallback(callback, &entry->value.core, sizeof(entry->value.core), Z_NO_FLUSH, Z_OK)) { SET_MESSAGE_ERROR("Failed writing a group entry core"); return false; } offset += entry->value.core.Size; } ForeachGroupEntry(children) { if(entry->value.core.Directory) { if(!cc4group_writeEntries(this, &entry->value, callback)) { // error is set in the recursive call return false; } } else if(entry->value.core.Size > 0) { const uint8_t* data = cc4group_getOnlyEntryData(this, &entry->value); if(data == NULL) { return false; } if(!cc4group_deflateToCallback(callback, data, entry->value.core.Size, Z_NO_FLUSH, Z_OK)) { SET_MESSAGE_ERROR("Failed writing entry data"); return false; } } } return true; } static bool cc4group_saveWithWriteCallback(CC4Group* const this, CC4Group_WriteCallback const writeCallback, void* const arg, size_t maxBlockSize) { assert(this); assert(writeCallback); if(maxBlockSize == 0) { maxBlockSize = 1024 * 1024; } WriteCallback callback = { .callback = writeCallback, .arg = arg, .bufferSize = maxBlockSize, .position = 0, .buffer = malloc(maxBlockSize), .magicDone = false, .gzStream = { .zalloc = NULL, .zfree = NULL, .opaque = NULL, .next_in = NULL, .avail_in = 0, } }; if(callback.buffer == NULL) { SET_ERRNO_ERROR("malloc: allocating a buffer of maxBlockSize"); return false; } bool success = false; bool deflateStarted = false; int ret = deflateInit2(&callback.gzStream, 9, Z_DEFLATED, 15 + 16, 9, Z_DEFAULT_STRATEGY); if(ret != Z_OK) { SET_ZERROR_ERROR("deflateInit2", ret); goto ret; } deflateStarted = true; // write the real c4group magic instead static const uint8_t magic[] = {C4GroupMagic1, C4GroupMagic2}; if(!cc4group_bufferedWriteToCallback(&callback, magic, 2)) { SET_MESSAGE_ERROR("Failed writing the group magic"); goto ret; } cc4group_calculateEntrySizes(this, &this->root); cc4group_calculateEntryCRC(this, &this->root); if(!cc4group_writeEntries(this, &this->root, &callback)) { // error is set in cc4group_writeEntries goto ret; } if(!cc4group_deflateToCallback(&callback, NULL, 0, Z_FINISH, Z_STREAM_END)) { SET_MESSAGE_ERROR("Failed finishing the deflate stream"); goto ret; } if(!cc4group_flushBufferedWrite(&callback)) { SET_MESSAGE_ERROR("Failed writing the remaining buffered data at the end of the group"); goto ret; } success = true; ret: if(deflateStarted) { deflateEnd(&callback.gzStream); } if(callback.buffer != NULL) { free(callback.buffer); } return success; } static bool cc4group_writeToFdCallback(const void* const data, size_t const size, void* const arg) { for(;;) { if(write(*((int*)arg), data, size) == -1) { if(errno != EINTR) { return false; } continue; } return true; } } static bool cc4group_writeToFilePointerCallback(const void* const data, size_t const size, void* const arg) { return fwrite(data, 1, size, arg) == size; } static bool cc4group_saveToFilePointer(CC4Group* const this, FILE* const file) { return cc4group_saveWithWriteCallback(this, cc4group_writeToFilePointerCallback, file, 0); } static bool cc4group_saveToFd(CC4Group* const this, int fd) { return cc4group_saveWithWriteCallback(this, cc4group_writeToFdCallback, &fd, 0); } static bool cc4group_saveAs(CC4Group* const this, const char* const path) { assert(this); assert(path); if(strcmp(path, "-") == 0) { SET_BINARY(STDOUT_FILENO); return cc4group_saveToFd(this, STDOUT_FILENO); } bool success = false; int file = open(path, O_WRONLY | O_BINARY | O_CREAT | O_EXCL, 0644); if(file == -1) { SET_ERRNO_ERROR("open: creating the target group file"); return false; } success = cc4group_saveWithWriteCallback(this, cc4group_writeToFdCallback, &file, 0); if(file != -1) { if(close(file) == -1) { cc4group_warn(this, "close: Closing the target group file failed: %s", strerror(errno)); } } if(!success) { if(unlink(path) == -1) { cc4group_warn(this, "unlink: Deleting the incomplete group file failed: %s", strerror(errno)); } } return success; } static bool cc4group_saveAsOverwrite(CC4Group* const this, const char* const path) { if(strcmp(path, "-") == 0) { return cc4group_saveAs(this, path); } char tmpFileName[tmpFileNameLength]; int tmpFile = cc4group_createTmpFile(this, tmpFileName); if(tmpFile == -1) { return false; } bool success = false; if(!cc4group_saveWithWriteCallback(this, cc4group_writeToFdCallback, &tmpFile, 0)) { goto ret; } if(rename(tmpFileName, path) == 0) { success = true; } else { SET_ERRNO_ERROR("rename: can't rename the temporary file to the target path"); } if(close(tmpFile) == -1) { cc4group_warn(this, "close: Closing the target group file failed: %s", strerror(errno)); } ret: if(!success) { if(unlink(tmpFileName) == -1) { cc4group_warn(this, "unlink: Deleting the incomplete temporary group file \"%s\" failed: %s", tmpFileName, strerror(errno)); } } return success; } static bool cc4group_save(CC4Group* const this) { assert(this); if(this->path == NULL) { SET_MESSAGE_ERROR("This group was not opened from an existing on disk group. cc4group.save can not be used."); return false; } if(this->parent != NULL) { SET_MESSAGE_ERROR("Refusing to save a group opened with openAsChild. cc4group.saveParent might be used."); return false; } return cc4group_saveAsOverwrite(this, this->path); } static bool cc4group_saveParent(CC4Group* const this) { CC4Group* root = this; while(root->parent) { root = root->parent; } return cc4group_save(root); } static bool cc4group_getEntryInfoForEntry(CC4Group* const this, const C4GroupEntryData* const entry, CC4Group_EntryInfo* const info, bool const lazy) { C4GroupHeader* header = NULL; if(entry->core.Directory) { header = entry->header; if(header == NULL && !((this->lazy || entry->path != NULL) && lazy)) { header = cc4group_getHeader(this, entry); if(header == NULL) { return false; } } } C4GroupHeader* headerOrParentHeader = header != NULL ? header : entry->parent != NULL ? entry->parent->header : NULL; *info = (CC4Group_EntryInfo){ .fileName = entry->core.FileName, .modified = header != NULL ? header->Creation : entry->core.Modified, .author = headerOrParentHeader != NULL ? headerOrParentHeader->Maker : NULL, .size = header != NULL ? (sizeof(C4GroupHeader) + header->Entries * sizeof(C4GroupEntryCore)) : (size_t)entry->core.Size, .totalSize = entry->core.Size, .executable = entry->core.Executable ? true : false, .directory = entry->core.Directory ? true : false, .official = headerOrParentHeader != NULL ? C4GroupHeader_isOfficial(headerOrParentHeader) : false }; return true; } static bool cc4group_getEntryInfo(CC4Group* const this, const char* const path, CC4Group_EntryInfo* const info, bool const lazy) { assert(this); assert(info); const C4GroupEntryData* entry = cc4group_getFileOrDirectoryByPath(this, path, true, NULL); if(entry == NULL) { return false; } return cc4group_getEntryInfoForEntry(this, entry, info, lazy); } static bool cc4group_getEntryInfos(CC4Group* const this, const char* const path, CC4Group_EntryInfo** const infos, size_t* size, bool const lazy) { assert(this); assert(infos); assert(size); const C4GroupEntryData* node; if(path != NULL && *path != '\0') { const C4GroupEntryData* entry = cc4group_getDirectoryByPath(this, path, true, NULL); if(entry == NULL) { return false; } node = entry; } else { node = &this->root; } GroupEntryList* children = cc4group_getChildren(this, node); if(children == NULL) { return false; } *size = GroupEntryListSize(children); CC4Group_EntryInfo* myInfos = malloc(*size * sizeof(CC4Group_EntryInfo)); if(myInfos == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for group entry infos"); return false; } CC4Group_EntryInfo* infosStart = myInfos; ForeachGroupEntry(children) { if(!cc4group_getEntryInfoForEntry(this, &entry->value, myInfos++, lazy)) { free(infosStart); return false; } } *infos = infosStart; return true; } // returns the child path and stores the parentPath in parentPath; modifies combinedPath static char* cc4group_splitParentAndChildPaths(char* const combinedPath, const char** const parentPath) { char* slash = strrchr(combinedPath, '/'); if(slash == NULL) { *parentPath = ""; return combinedPath; } else { *slash = '\0'; *parentPath = combinedPath; return slash + 1; } } static bool cc4group_getParentAndChildEntries(CC4Group* const this, const char* const path, const C4GroupEntryData* const inParent, const C4GroupEntryData** parentEntry, GroupEntryListEntry** const childEntry) { bool error = false; GroupEntryListEntry* listEntry = cc4group_getChildListEntryByPath(this, path, &error, inParent); if(error) { return false; } if(listEntry == NULL) { SET_MESSAGE_ERROR("The desired file was not found in the group file"); return false; } *childEntry = listEntry; *parentEntry = listEntry->value.parent; return true; } static bool cc4group_deleteEntry(CC4Group* const this, const char* const path, bool const recursive) { assert(this); const C4GroupEntryData* parent; GroupEntryListEntry* deleteEntry = NULL; if(!cc4group_getParentAndChildEntries(this, path, NULL, &parent, &deleteEntry)) { return false; } return cc4group_deleteEntryFromParent(this, deleteEntry, parent, recursive); } static C4GroupEntryData* cc4group_addEntryToDirectory(CC4Group* const this, C4GroupEntryData* const directory, const C4GroupEntryData* const entry) { assert(directory->core.Directory); // TODO? Implement engine-optimized order sorting here? GroupEntryList* directoryChildren = cc4group_getChildren(this, directory); if(directoryChildren == NULL) { return NULL; } C4GroupEntryData* newEntry = &GroupEntryListAppend(directoryChildren, *entry)->value; newEntry->parent = directory; directory->header->Entries = GroupEntryListSize(directoryChildren); // header is loaded already for the children return newEntry; } static bool cc4group_renameEntry(CC4Group* const this, const char* const oldPath, const char* const newPath) { assert(this); assert(newPath); bool error = false; if(cc4group_getEntryByPath(this, newPath, false, &error, NULL) != NULL) { SET_MESSAGE_ERROR("The desired target path already exists"); return false; } if(error) { return false; } const C4GroupEntryData* oldParent; GroupEntryListEntry* entry = NULL; if(!cc4group_getParentAndChildEntries(this, oldPath, NULL, &oldParent, &entry)) { return false; } char* targetPath = strdup(newPath); if(targetPath == NULL) { SET_ERRNO_ERROR("strdup: duplicating the new path"); return false; } const char* targetDirectory; char* targetName = cc4group_splitParentAndChildPaths(targetPath, &targetDirectory); C4GroupEntryData* newParent = (C4GroupEntryData*)cc4group_getDirectoryByPath(this, targetDirectory, true, NULL); if(newParent == NULL) { free(targetPath); return false; } C4GroupEntryCore_setFileName(&entry->value.core, targetName); if(cc4group_addEntryToDirectory(this, newParent, &entry->value) == NULL) { free(targetPath); return false; } assert(cc4group_getChildren(this, oldParent)); // again, should have been catched already with cc4group_getParentAndChildEntries GroupEntryListRemove(cc4group_getChildren(this, oldParent), entry); free(targetPath); return true; } static C4GroupEntryData* cc4group_createEntry(CC4Group* const this, const char* const path, C4GroupEntryData* inParent) { bool error = false; if(cc4group_getEntryByPath(this, path, false, &error, inParent) != NULL) { SET_MESSAGE_ERROR("The desired target path already exists"); return NULL; } if(error) { return NULL; } const char* targetName; C4GroupEntryData* parent; char* targetPath; if(inParent == NULL) { targetPath = strdup(path); if(targetPath == NULL) { SET_ERRNO_ERROR("strdup: duplicating the path"); return NULL; } const char* targetDirectory; targetName = cc4group_splitParentAndChildPaths(targetPath, &targetDirectory); parent = (C4GroupEntryData*)cc4group_getDirectoryByPath(this, targetDirectory, true, NULL); if(parent == NULL) { free(targetPath); return NULL; } } else { parent = inParent; targetName = path; } C4GroupEntryData entry; entry.data = NULL; entry.path = NULL; entry.memoryManagement = cc4group.MemoryManagement.Reference; C4GroupEntryCore_init(&entry.core); C4GroupEntryCore_setFileName(&entry.core, targetName); if(inParent == NULL) { free(targetPath); } return cc4group_addEntryToDirectory(this, parent, &entry); } static C4GroupEntryData* cc4group_createDirectory(CC4Group* const this, const char* const path, C4GroupEntryData* const parent) { assert(this); assert(path); C4GroupEntryData* entry = cc4group_createEntry(this, path, parent); if(entry == NULL) { return NULL; } C4GroupHeader* header = malloc(sizeof(C4GroupHeader)); if(header == NULL) { SET_ERRNO_ERROR("malloc: allocating the group header for the new directory"); return NULL; } entry->children = NULL; C4GroupHeader_init(header); C4GroupHeader_setOfficial(header, C4GroupHeader_isOfficial(entry->parent->header)); C4GroupHeader_setMaker(header, entry->parent->header->Maker); entry->header = header; entry->core.Directory = 1; entry->memoryManagement = cc4group.MemoryManagement.Take; return entry; } static bool cc4group_addFromDisk(CC4Group* const this, const char* const path, const char* const targetEntryPath, int const allowedEntryTypes) { CC4Group_AllowedEntryTypes type = cc4group_isDirectory(path) ? CC4Group_AllowedEntryTypes_Directory : CC4Group_AllowedEntryTypes_File; if(!(type & allowedEntryTypes)) { SET_MESSAGE_ERROR("The specified path doesn't match the specified allowed entry type"); return false; } switch(type) { case CC4Group_AllowedEntryTypes_File: return cc4group_addFileFromDisk(this, strdup(path), targetEntryPath, NULL, allowedEntryTypes & CC4Group_AllowedEntryTypes_HandleGroups) != NULL; case CC4Group_AllowedEntryTypes_Directory: return cc4group_addDirectoryFromDisk(this, strdup(path), targetEntryPath, NULL); case CC4Group_AllowedEntryTypes_All: case CC4Group_AllowedEntryTypes_NoGroupHandling: case CC4Group_AllowedEntryTypes_HandleGroups: assert(!"Invalid type determined for the path; This code should never be reached"); return false; } return false; } static bool cc4group_createEmptyDirectory(CC4Group* const this, const char* const path) { C4GroupEntryData* entry = cc4group_createDirectory(this, path, NULL); if(entry != NULL) { entry->children = GroupEntryListNew(); } return entry != NULL; } static bool cc4group_createFile(CC4Group* const this, const char* const path) { assert(this); assert(path); return cc4group_createEntry(this, path, NULL) != NULL; } static bool cc4group_setEntryData(CC4Group* const this, const char* const entryPath, const void* const data, size_t const size, CC4Group_MemoryManagement const memoryManagement) { assert(this); assert(entryPath); C4GroupEntryData* entry = (C4GroupEntryData*)cc4group_getFileByPath(this, entryPath, NULL); if(entry == NULL) { return false; } if(entry->data != NULL) { cc4group_applyMemoryManagementEnd(entry->memoryManagement, entry->data); entry->data = NULL; entry->core.Size = 0; entry->memoryManagement = cc4group.MemoryManagement.Reference; } if(entry->path != NULL) { free((void*)entry->path); entry->path = NULL; } if(data != NULL && size != 0) { const uint8_t* ownData = data; if(!cc4group_applyMemoryManagementStart(memoryManagement, &ownData, size)) { SET_ERRNO_ERROR("memoryManagement: allocating memory for copying the file data"); return false; } else { entry->data = (uint8_t*)ownData; } entry->core.Size = size; entry->memoryManagement = memoryManagement; } entry->core.HasCRC = C4GroupEntryCore_NoCRC; return true; } static bool cc4group_setExecutable(CC4Group* const this, bool const executable, const char* const path) { assert(this); C4GroupEntryData* entry = (C4GroupEntryData*)cc4group_getFileByPath(this, path, NULL); if(entry == NULL) { return false; } entry->core.Executable = executable ? 1 : 0; return true; } static bool cc4group_openAsChildOn(CC4Group* const this, const char* const path, CC4Group* const child) { assert(this); const C4GroupEntryData* entry = cc4group_getDirectoryByPath(this, path, false, NULL); if(entry == NULL) { return false; } GroupEntryList* children = cc4group_getChildren(this, entry); // this also loads the header of entry child->root = *entry; child->readState.position = this->readState.position; child->root.parent = NULL; child->root.core.FileName[0] = '\0'; child->root.children = children; child->uncompressedData = this->uncompressedData; assert(child->root.children); child->parent = this; child->name = strdup(entry->core.FileName); child->pathInParent = strdup(path); ++this->referenceCounter; CleanUpJobListAppend(child->cleanupJobs, (CC4Group_CleanupJob){.func = (CC4Group_CleanupFunc)cc4group_unreference, .data = this}); return true; } static CC4Group* cc4group_openAsChild(CC4Group* const this, const char* const path) { CC4Group* child = cc4group_new(); if(child == NULL) { SET_ERRNO_ERROR("malloc: allocating the child group object"); return NULL; } if(!cc4group_openAsChildOn(this, path, child)) { cc4group_delete(child); return NULL; } return child; } static bool cc4group_isChild(const CC4Group* const this) { return this->parent != NULL; } static bool cc4group_isPacked(const CC4Group* const this) { return this->root.path != NULL; } static const char* cc4group_getName(const CC4Group* const this) { return this->name; } static char* cc4group_getFullName(const CC4Group* const this) { if(!this->parent) { return strdup(this->path); } GroupList* groups = GroupListNew(); size_t len = 0; const CC4Group* child = this; while(child->parent) { GroupListPrepend(groups, child); len += strlen(child->pathInParent) + 1; child = child->parent; } char* result = NULL; if(!child->path) { goto ret; } len += strlen(child->path) + 1; result = malloc(sizeof(char) * len); if(result == NULL) { goto ret; } strcpy(result, child->path); ForeachGroup(groups) { strcat(result, "/"); strcat(result, group->value->pathInParent); } ret: GroupListDestroy(groups); return result; } static void* cc4group_memoryManagementTakeStart(void* const data, size_t const size, void* const arg) { (void)size; (void)arg; return data; } static void cc4group_memoryManagementTakeEnd(void* const data, void* const arg) { (void)arg; if(data != NULL) { free(data); } } static void* cc4group_memoryManagementCopyStart(void* const data, size_t const size, void* const arg) { if(data == NULL && size == 0) { return NULL; } (void)arg; uint8_t* copy = malloc(size); if(copy == NULL) { return NULL; } memcpy(copy, data, size); return copy; } static void cc4group_memoryManagementCopyEnd(void* const data, void* const arg) { (void)arg; if(data != NULL) { free(data); } } static void* cc4group_memoryManagementReferenceStart(void* const data, size_t const size, void* const arg) { (void)size; (void)arg; return data; } static void cc4group_memoryManagementReferenceEnd(void* const data, void* const arg) { (void)data; (void)arg; } CC4Group_API cc4group = { .AllowedEntryTypes = { .File = CC4Group_AllowedEntryTypes_File, .Directory = CC4Group_AllowedEntryTypes_Directory, .HandleGroups = CC4Group_AllowedEntryTypes_HandleGroups, .NoGroupHandling = CC4Group_AllowedEntryTypes_NoGroupHandling, .All = CC4Group_AllowedEntryTypes_All }, .MemoryManagement = { .Take = { .start = cc4group_memoryManagementTakeStart, .end = cc4group_memoryManagementTakeEnd, .arg = NULL }, .Copy = { .start = cc4group_memoryManagementCopyStart, .end = cc4group_memoryManagementCopyEnd, .arg = NULL }, .Reference = { .start = cc4group_memoryManagementReferenceStart, .end = cc4group_memoryManagementReferenceEnd, .arg = NULL } }, .TmpMemoryStrategies = { .Memory = cc4group_createTmpMemoryMalloc, .File = cc4group_createTmpMemoryFile, .Auto = cc4group_createTmpMemoryAuto }, .setTmpMemoryStrategy = cc4group_setTmpMemoryStrategy, .setWarningCallback = cc4group_setWarningCallback, .setEntryNameMatchingCallback = cc4group_setEntryNameMatchingCallback, .new = cc4group_new, .delete = cc4group_unreference, .setLazy = cc4group_setLazy, .create = cc4group_create, .openExisting = cc4group_openExisting, .openMemory = cc4group_openMemory, .openFd = cc4group_openFd, .openFilePointer = cc4group_openFilePointer, .openWithReadCallback = cc4group_uncompressGroup, .save = cc4group_save, .saveParent = cc4group_saveParent, .saveAs = cc4group_saveAs, .saveAsOverwrite = cc4group_saveAsOverwrite, .saveToFd = cc4group_saveToFd, .saveToFilePointer = cc4group_saveToFilePointer, .saveWithWriteCallback = cc4group_saveWithWriteCallback, .extractAll = cc4group_extractAll, .extractSingle = cc4group_extractSingle, .extractSingleRename = cc4group_extractSingleRename, .getEntryInfo = cc4group_getEntryInfo, .getEntryInfos = cc4group_getEntryInfos, .getEntryData = cc4group_getEntryData, .setEntryData = cc4group_setEntryData, .setMaker = cc4group_setMaker, .setCreation = cc4group_setCreation, .setOfficial = cc4group_setOfficial, .setExecutable = cc4group_setExecutable, .addFromDisk = cc4group_addFromDisk, .createDirectory = cc4group_createEmptyDirectory, .createFile = cc4group_createFile, .renameEntry = cc4group_renameEntry, .deleteEntry = cc4group_deleteEntry, .getErrorMessage = cc4group_getErrorMessage, .getErrorCode = cc4group_getErrorCode, .getErrorMethod = cc4group_getErrorMethod, .getErrorCauser = cc4group_getErrorCauser, .openAsChild = cc4group_openAsChild, .isChild = cc4group_isChild, .isPacked = cc4group_isPacked, .getName = cc4group_getName, .getFullName = cc4group_getFullName };