// 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 #include #include #include #include #include #include #include #include #include #include "zlib.h" #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_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); // 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* size); #define C4GroupMagic1 0x1e #define C4GroupMagic2 0x8c struct list_GroupEntryList; typedef struct C4GroupEntryData_t { C4GroupEntryCore core; bool freeData; 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; } 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) 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; const char* path; const char* subPath; C4GroupEntryData root; C4GroupEntryData realRoot; CleanUpJobList* cleanupJobs; struct { int32_t code; const char* method; const char* causer; ErrorFormatterData formatter; char* lastFormattedMessage; } error; }; typedef enum { Take, Copy, Reference } CC4Group_MemoryManagement; static const C4GroupEntryData* cc4group_getEntryByPath(const CC4Group* const this, const char* const entryPath); static const C4GroupEntryData* cc4group_getDirectoryByPath(CC4Group* const this, const char* const entryPath); static const C4GroupEntryData* cc4group_getFileByPath(CC4Group* const this, const char* const entryPath); static const C4GroupEntryData* cc4group_getFileOrDirectoryByPath(CC4Group* const this, const char* const entryPath); static char* cc4group_strerrorFormatter(int32_t const code, const char* const method, const char* const causer, void* const data) { (void)data; char* message; asprintf(&message, "%s: %s: %s (%d)", method, causer, strerror(code), code); return message; } static char* cc4group_zerrorFormatter(int32_t const code, const char* const method, const char* const causer, void* const data) { // restore errno for the case that code is Z_ERRNO errno = (size_t)data; char* message; asprintf(&message, "%s: %s: %s (%d)", method, causer, zError(code), code); 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; asprintf(&message, "%s: %s", method, causer); 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"); } #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 buildChildren(CC4Group* const this, C4GroupEntryData* const entry, size_t const dataSize) { C4GroupHeader* header = entry->header; 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, .freeData = false, .children = NULL, .parent = entry})->value; 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.freeData && entry->value.data != NULL) { free(entry->value.data); } if(entry->value.core.Directory) { 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 mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); } typedef struct { void* addr; size_t size; } MunmapData; static void cc4group_unmapTmpMemoryFile(MunmapData* data) { if(munmap(data->addr, data->size) == -1) { fprintf(stderr, "WARNING: munmap: Unmapping tempory file failed: %s\n", strerror(errno)); } free(data); } static void* cc4group_createTmpMemoryFile(CC4Group* const this, const size_t size, CC4Group_CleanupJob* cleanupJob) { void* ret; #define TMP_FILE "cc4group.tmp" int tmpFile = open(TMP_FILE, O_CREAT | O_RDWR | O_TRUNC | O_EXCL, 0600); if(tmpFile == -1) { SET_ERRNO_ERROR("open: Opening tmp file \"" TMP_FILE "\""); return NULL; } if(unlink(TMP_FILE) == -1) { fprintf(stderr, "WARNING: unlink: Failed to delete tmp file \"" TMP_FILE "\". Manual deletion is required: %s\n", strerror(errno)); } #undef TMP_FILE ret = cc4group_mapSizedWriteFd(this, tmpFile, size); if(ret == MAP_FAILED) { // error message is set in the method ret = NULL; } else { MunmapData* unmapData = malloc(sizeof(MunmapData)); if(unmapData == NULL) { fprintf(stderr, "ERROR: allocating memory for cleanup data: %s\n", strerror(errno)); } *unmapData = (MunmapData){ret, size}; *cleanupJob = (CC4Group_CleanupJob){(CC4Group_CleanupFunc)cc4group_unmapTmpMemoryFile, unmapData}; } if(close(tmpFile) == -1) { fprintf(stderr, "WARNING: close: Closing tmp file failed: %s\n", strerror(errno)); if(ret != NULL) { cleanupJob->func(cleanupJob->data); } return NULL; } return ret; } static void* cc4group_createTmpMemoryMalloc(CC4Group* const this, const size_t size, CC4Group_CleanupJob* cleanupJob) { (void)this; // error checking is left out intentionally. it is the job of the calling function void* ret = malloc(size); *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* 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 void cc4group_uncompressGroup(CC4Group* const this) { // declare variables here already for proper cleanup in error cases uint8_t* retData = NULL; C4GroupHeader* header = NULL; uint8_t* mappedFile = MAP_FAILED; uint8_t* mappedTmpFile = NULL; bool inflateStarted = false; size_t currentSize = 0; CC4Group_CleanupJob tmpCleanup; char* path; char* slash = NULL; this->path = path = strdup(this->path); AddCleanupJob(free, (void*)this->path); int file = -1; for(;;) { file = open(this->path, O_RDONLY); if(file != -1) { break; } if(errno != ENOTDIR) { SET_ERRNO_ERROR("open: Opening group file"); goto ret; } char* oldSlash = slash; slash = strrchr(path, '/'); if(oldSlash != NULL) { *oldSlash = '/'; } if(slash != NULL && slash != path) { *slash = '\0'; this->subPath = slash + 1; } } 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 = mmap(NULL, size, PROT_READ | PROT_WRITE, 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; } if((mappedFile[0] != C4GroupMagic1 && mappedFile[0] != 0x1f) || (mappedFile[1] != C4GroupMagic2 && mappedFile[1] != 0x8b)) { SET_MESSAGE_ERROR("The file is not a valid group file. Magic bytes don't match."); goto ret; } mappedFile[0] = 0x1f; mappedFile[1] = 0x8b; z_stream strm = { .zalloc = NULL, .zfree = NULL, .opaque = NULL, .next_in = mappedFile, .avail_in = size }; int ret = inflateInit2(&strm, 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; } strm.next_out = (Bytef*)header; strm.avail_out = currentSize; ret = inflate(&strm, Z_SYNC_FLUSH); if(ret != Z_OK && (ret != Z_STREAM_END || strm.avail_in != 0)) { 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) { SET_MALFORMED_MESSAGE_ERROR("The group is empty but the gzip stream has not ended yet."); goto ret; } retData = (void*)header; header = NULL; AddCleanupJob(free, header); goto ret; } if(header->Ver1 != 1 || header->Ver2 > 2) { SET_MESSAGE_ERROR("Unsupported group version. Only versions 1.0 up to 1.2 are supported."); goto ret; } if(memcmp(C4GroupId, header->id, sizeof(header->id)) != 0) { SET_MALFORMED_MESSAGE_ERROR("The group id does not match."); goto ret; } // make sure the maker is null terminated header->Maker[C4GroupMaxMaker + 1] = '\0'; 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)); strm.next_out = (Bytef*)cores; strm.avail_out = sizeof(C4GroupEntryCore) * header->Entries; ret = inflate(&strm, Z_SYNC_FLUSH); if(ret != Z_OK) { 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; mappedTmpFile = cc4group_tmpMemoryStrategy(this, currentSize, &tmpCleanup); uint8_t* data = (uint8_t*)(mappedTmpFile) + sizeof(C4GroupHeader) + sizeof(C4GroupEntryCore) * header->Entries; // write alredy decompressed header and cores into the file memcpy(mappedTmpFile, header, data - mappedTmpFile); strm.next_out = data; strm.avail_out = uncompressedSize; ret = inflate(&strm, Z_SYNC_FLUSH); if(ret != Z_STREAM_END || strm.avail_in != 0 || strm.avail_out != 0) { SET_ZERROR_ERROR("inflate: inflating group contents", ret); goto ret; } retData = mappedTmpFile; ret: if(header != NULL) { free(header); } if(inflateStarted) { inflateEnd(&strm); } if(mappedFile != MAP_FAILED) { if(munmap(mappedFile, size) == -1) { fprintf(stderr, "WARNING: munmap: Unmapping the group file failed: %s\n", strerror(errno)); } } if(retData == NULL && mappedTmpFile != NULL) { tmpCleanup.func(tmpCleanup.data); } else if(mappedTmpFile != NULL) { CleanUpJobListPrepend(this->cleanupJobs, tmpCleanup); } this->uncompressedData = retData; this->uncompressedSize = currentSize; } static bool cc4group_buildGroupHierarchy(CC4Group* const this) { this->root.data = this->uncompressedData; 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 void cc4group_init(CC4Group* const this) { assert(this); this->uncompressedData = NULL; this->uncompressedSize = 0; this->path = ""; this->subPath = ""; this->root.data = NULL; this->root.freeData = false; this->root.parent = NULL; this->root.children = NULL; this->root.core.Directory = true; 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; } static void cc4group_setMakerRecursively(const C4GroupEntryData* groupEntry, const char* const maker) { ForeachGroupEntry(groupEntry->children) { if(entry->value.core.Directory) { C4GroupHeader_setMaker(entry->value.header, maker); cc4group_setMakerRecursively(&entry->value, maker); } } } 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; if(path == NULL || *path == '\0') { entry = &this->root; } else { entry = cc4group_getDirectoryByPath(this, path); if(entry == NULL) { return false; } } C4GroupHeader_setMaker(entry->header, maker); if(recursive) { cc4group_setMakerRecursively(entry, maker); } return true; } static void cc4group_setOfficialRecursively(const C4GroupEntryData* groupEntry, bool const official) { ForeachGroupEntry(groupEntry->children) { if(entry->value.core.Directory) { C4GroupHeader_setOfficial(entry->value.header, official); cc4group_setOfficialRecursively(&entry->value, official); } } } static bool cc4group_setOfficial(CC4Group* const this, bool const official, const char* const path, bool const recursive) { assert(this); const C4GroupEntryData* entry; if(path == NULL || *path == '\0') { entry = &this->root; } else { entry = cc4group_getDirectoryByPath(this, path); if(entry == NULL) { return false; } } C4GroupHeader_setOfficial(entry->header, official); if(recursive) { cc4group_setOfficialRecursively(entry, official); } return true; } static void cc4group_setCreationRecursively(const C4GroupEntryData* groupEntry, int32_t const creation) { ForeachGroupEntry(groupEntry->children) { entry->value.core.Modified = creation; if(entry->value.core.Directory) { C4GroupHeader_setCreation(entry->value.header, creation); cc4group_setCreationRecursively(&entry->value, creation); } } } static bool cc4group_setCreation(CC4Group* const this, int32_t const creation, const char* const path, bool const recursive) { assert(this); C4GroupEntryData* entry; if(path == NULL || *path == '\0') { entry = &this->root; } else { entry = (C4GroupEntryData*)cc4group_getFileOrDirectoryByPath(this, path); if(entry == NULL) { return false; } } entry->core.Modified = creation; if(entry->core.Directory) { C4GroupHeader_setCreation(entry->header, creation); if(recursive) { cc4group_setCreationRecursively(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)); 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_openExisting(CC4Group* const this, const char* const path) { assert(this); // only if the group is still empty assert(this->root.children == NULL); this->path = path; cc4group_uncompressGroup(this); if(this->uncompressedData == NULL) { return false; } cc4group_buildGroupHierarchy(this); if(*this->subPath != '\0') { this->realRoot = this->root; const C4GroupEntryData* subRoot = cc4group_getDirectoryByPath(this, this->subPath); if(subRoot == NULL) { return false; } this->root = *subRoot; } return true; } static void cc4group_delete(CC4Group* const this) { assert(this); ForeachCleanupJob(this->cleanupJobs) { job->value.func(job->value.data); } CleanUpJobListDestroy(this->cleanupJobs); if(this->error.lastFormattedMessage != NULL) { free(this->error.lastFormattedMessage); } 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_CREAT | O_EXCL, root->core.Executable ? 0755 : 0644); if(file == -1) { SET_ERRNO_ERROR("open: Creating target file"); return false; } write(file, root->data, root->core.Size); if(close(file) == -1) { fprintf(stderr, "WARNING: close: Closing the extracted file \"%s\" failed: %s\n", targetPath, strerror(errno)); } struct timeval tv[2] = {{.tv_usec = 0, .tv_sec = root->core.Modified}, {.tv_usec = 0, .tv_sec = root->core.Modified}}; if(utimes(targetPath, tv) == -1) { fprintf(stderr, "WARNING: utimes: Setting modification time for \"%s\" failed: %s\n", targetPath, strerror(errno)); } return true; } static bool cc4group_extractChildren(CC4Group* const this, const C4GroupEntryData* const root, const char* const targetPath) { 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(*targetPath != '\0') { strcat(tmpPath, "/"); } strcat(tmpPath, root->core.FileName); bool success = true; if(root->core.Directory) { assert(root->children); if(mkdir(tmpPath, 0755) == -1 /*&& errno != EEXIST*/) { SET_ERRNO_ERROR("mkdir: Creating target directory"); success = false; goto ret; } ForeachGroupEntry(root->children) { if(!cc4group_extractChildren(this, &entry->value, tmpPath)) { success = false; goto ret; } } 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); } // does not set any error static const C4GroupEntryData* cc4group_getEntryByPath(const CC4Group* const this, const char* const entryPath) { assert(this); assert(this->root.children); assert(entryPath); if(*entryPath == '\0') { return &this->root; } const C4GroupEntryData* currentParent = &this->root; char* path = strdup(entryPath); 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) { currentParent = NULL; break; } bool found = false; ForeachGroupEntry(currentParent->children) { if(strcmp(entry->value.core.FileName, part) == 0) { currentParent = &entry->value; found = true; break; } } if(!found) { currentParent = NULL; break; } } free(path); return currentParent; } // sets error static const C4GroupEntryData* cc4group_getDirectoryByPath(CC4Group* const this, const char* const entryPath) { // asserts are in cc4group_getEntryByPath const C4GroupEntryData* entry = cc4group_getEntryByPath(this, entryPath); if(entry == NULL) { SET_MESSAGE_ERROR("The dirctory 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) { // asserts are in cc4group_getEntryByPath const C4GroupEntryData* entry = cc4group_getEntryByPath(this, entryPath); 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) { // asserts are in cc4group_getEntryByPath const C4GroupEntryData* entry = cc4group_getEntryByPath(this, entryPath); 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(entryPath); assert(targetPath); const C4GroupEntryData* entry = cc4group_getFileOrDirectoryByPath(this, entryPath); if(entry == NULL) { return false; } return cc4group_extractChildren(this, entry, targetPath); } static bool cc4group_getEntryData(CC4Group* const this, const char* const entryPath, const void** const data, size_t* size) { assert(this); assert(entryPath); assert(data); assert(size); const C4GroupEntryData* entry = cc4group_getFileByPath(this, entryPath); if(entry == NULL) { return false; } *data = entry->data; *size = entry->core.Size; return true; } static void cc4group_setTmpMemoryStrategy(const CC4Group_TmpMemoryStrategy strategy) { cc4group_tmpMemoryStrategy = strategy; } 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(C4GroupEntryData* const entryData) { if(entryData->core.Directory) { size_t sum = sizeof(C4GroupHeader) + GroupEntryListSize(entryData->children) * sizeof(C4GroupEntryCore); ForeachGroupEntry(entryData->children) { sum += cc4group_calculateEntrySizes(&entry->value); } return entryData->core.Size = sum; } else { return entryData->core.Size; } } static void cc4group_calculateEntryCRC(C4GroupEntryData* const groupEntry) { if(groupEntry->core.Directory) { uint32_t crc = 0; ForeachGroupEntry(groupEntry->children) { cc4group_calculateEntryCRC(&entry->value); crc ^= entry->value.core.CRC; } groupEntry->core.HasCRC = C4GroupEntryCore_ContentsFileNameCRC; groupEntry->core.CRC = crc; } else if(groupEntry->core.HasCRC != C4GroupEntryCore_ContentsFileNameCRC) { uint32_t crc = crc32(0, groupEntry->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_writeEntriesToGzFile(CC4Group* const this, C4GroupEntryData* const entryData, gzFile file) { C4GroupHeader header = *(C4GroupHeader*)entryData->data; header.Entries = GroupEntryListSize(entryData->children); header.Ver1 = 1; header.Ver2 = 2; if(header.Creation == 0) { header.Creation = time(NULL); } memScrambleHeader((uint8_t*)&header); if(gzwrite(file, &header, sizeof(header)) == 0) { int code; gzerror(file, &code); SET_ZERROR_ERROR("gzwrite: writing a group header", code); return false; } size_t offset = 0; ForeachGroupEntry(entryData->children) { entry->value.core.Offset = offset; entry->value.core.Packed = 1; if(gzwrite(file, &entry->value.core, sizeof(entry->value.core)) == 0) { int code; gzerror(file, &code); SET_ZERROR_ERROR("gzwrite: writing a group entry core", code); return false; } offset += entry->value.core.Size; } ForeachGroupEntry(entryData->children) { if(entry->value.core.Directory) { cc4group_writeEntriesToGzFile(this, &entry->value, file); } else if(entry->value.core.Size > 0) { if(gzwrite(file, entry->value.data, entry->value.core.Size) == 0) { int code; gzerror(file, &code); SET_ZERROR_ERROR("gzwrite: writing entry data", code); return false; } } } return true; } static bool cc4group_saveIt(CC4Group* const this, const char* const path, bool const overwrite) { assert(this); bool success = false; int file = open(path, O_WRONLY | O_CREAT | (overwrite ? O_TRUNC : O_EXCL), 0644); if(file == -1) { SET_ERRNO_ERROR("open: creating the target group file"); return false; } int fdKeep = dup(file); if(fdKeep == -1) { SET_ERRNO_ERROR("dup: duplicating the opened group fd"); return false; } gzFile gzfile = gzdopen(file, "wb"); if(gzfile == NULL) { SET_ERRNO_ERROR("gzdopen: Opening the target group file"); goto ret; } file = -1; cc4group_calculateEntrySizes(&this->root); cc4group_calculateEntryCRC(&this->root); success = cc4group_writeEntriesToGzFile(this, &this->root, gzfile); ret: if(file != -1) { if(close(file) == -1) { fprintf(stderr, "WARNING: close: Closing the target group file failed: %s\n", strerror(errno)); } } if(gzfile != NULL) { int ret = gzclose(gzfile); if(ret != Z_OK) { fprintf(stderr, "WARNING: gzclose: Closing the gzFile failed: %s\n", zError(ret)); } } if(!success) { if(unlink(path) == -1) { fprintf(stderr, "WARNING: unlink: Deleting the incomplete group file failed: %s\n", strerror(errno)); } } else { if(lseek(fdKeep, 0, SEEK_SET) != 0) { fprintf(stderr, "WARNING: lseek: Seeking to the group magic bytes failed: %s\n", strerror(errno)); } else { static uint8_t magic[2] = {C4GroupMagic1, C4GroupMagic2}; if(write(fdKeep, magic, 2) != 2) { fprintf(stderr, "WARNING: write: Writing the correct group magic bytes failed: %s\n", strerror(errno)); } } } if(fdKeep != -1) { if(close(fdKeep) == -1) { fprintf(stderr, "WARNING: close: Closing the dup'ed target group file failed: %s\n", strerror(errno)); } } return success; } static bool cc4group_save(CC4Group* const this, const char* const path) { return cc4group_saveIt(this, path, false); } static bool cc4group_saveOverwrite(CC4Group* const this, const char* const path) { return cc4group_saveIt(this, path, true); } static void cc4group_getEntryInfoForEntry(const C4GroupEntryData* const entry, CC4Group_EntryInfo* const info) { *info = (CC4Group_EntryInfo){ .fileName = entry->core.FileName, .modified = entry->core.Directory ? entry->header->Creation : entry->core.Modified, .author = entry->core.Directory ? entry->header->Maker : entry->parent->header->Maker, .size = entry->core.Directory ? (sizeof(C4GroupHeader) + entry->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 = C4GroupHeader_isOfficial(entry->core.Directory ? entry->header: entry->parent->header) }; } static bool cc4group_getEntryInfo(CC4Group* const this, const char* const path, CC4Group_EntryInfo* const info) { assert(this); assert(path); assert(info); const C4GroupEntryData* entry = cc4group_getFileOrDirectoryByPath(this, path); if(entry == NULL) { return false; } cc4group_getEntryInfoForEntry(entry, info); return true; } static bool cc4group_getEntryInfos(CC4Group* const this, const char* const path, CC4Group_EntryInfo** const infos, size_t* size) { assert(this); assert(infos); assert(size); const C4GroupEntryData* node; if(path != NULL && *path != '\0') { const C4GroupEntryData* entry = cc4group_getDirectoryByPath(this, path); if(entry == NULL) { return false; } node = entry; } else { node = &this->root; } *size = GroupEntryListSize(node->children); CC4Group_EntryInfo* myInfos = malloc(*size * sizeof(CC4Group_EntryInfo)); if(myInfos == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for group entry infos"); return false; } *infos = myInfos; ForeachGroupEntry(node->children) { cc4group_getEntryInfoForEntry(&entry->value, myInfos++); } 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** parentEntry, GroupEntryListEntry** childEntry) { const C4GroupEntryData* parent; char* myPath = strdup(path); const char* parentPath; char* entryName = cc4group_splitParentAndChildPaths(myPath, &parentPath); parent = cc4group_getDirectoryByPath(this, parentPath); if(parent == NULL) { free(myPath); return false; } *parentEntry = parent; GroupEntryListEntry* child = NULL; ForeachGroupEntry(parent->children) { if(strcmp(entry->value.core.FileName, entryName) == 0) { child = entry; break; } } free(myPath); if(child == NULL) { SET_MESSAGE_ERROR("The desired file was not found in the group file"); return false; } *childEntry = child; return true; } static bool cc4group_deleteEntry(CC4Group* const this, const char* const path, bool const recursive) { assert(this); assert(path); const C4GroupEntryData* parent; GroupEntryListEntry* deleteEntry = NULL; if(!cc4group_getParentAndChildEntries(this, path, &parent, &deleteEntry)) { return false; } 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; } deleteChildren(deleteEntry->value.children); } GroupEntryListRemove(parent->children, deleteEntry); parent->header->Entries = GroupEntryListSize(parent->children); return true; } static C4GroupEntryData* cc4group_addEntryToDirectory(C4GroupEntryData* const directory, const C4GroupEntryData* const entry) { assert(directory->core.Directory); // TODO? Implement engine-optimized order sorting here? C4GroupEntryData* newEntry = &GroupEntryListAppend(directory->children, *entry)->value; newEntry->parent = directory; directory->header->Entries = GroupEntryListSize(directory->children); return newEntry; } static bool cc4group_renameEntry(CC4Group* const this, const char* const oldPath, const char* const newPath) { assert(this); assert(oldPath); assert(newPath); if(cc4group_getEntryByPath(this, newPath) != NULL) { SET_MESSAGE_ERROR("The desired target path already exists"); return false; } const C4GroupEntryData* oldParent; GroupEntryListEntry* entry = NULL; if(!cc4group_getParentAndChildEntries(this, oldPath, &oldParent, &entry)) { return false; } char* targetPath = strdup(newPath); const char* targetDirectory; char* targetName = cc4group_splitParentAndChildPaths(targetPath, &targetDirectory); C4GroupEntryData* newParent = (C4GroupEntryData*)cc4group_getDirectoryByPath(this, targetDirectory); if(newParent == NULL) { free(targetPath); return false; } C4GroupEntryCore_setFileName(&entry->value.core, targetName); cc4group_addEntryToDirectory(newParent, &entry->value); GroupEntryListRemove(oldParent->children, entry); free(targetPath); return true; } static C4GroupEntryData* cc4group_createEntry(CC4Group* const this, const char* const path) { if(cc4group_getEntryByPath(this, path) != NULL) { SET_MESSAGE_ERROR("The desired target path already exists"); return NULL; } char* targetPath = strdup(path); const char* targetDirectory; char* targetName = cc4group_splitParentAndChildPaths(targetPath, &targetDirectory); C4GroupEntryData* parent = (C4GroupEntryData*)cc4group_getDirectoryByPath(this, targetDirectory); if(parent == NULL) { free(targetPath); return NULL; } C4GroupEntryData entry; entry.data = NULL; entry.freeData = false; entry.children = GroupEntryListNew(); C4GroupEntryCore_init(&entry.core); C4GroupEntryCore_setFileName(&entry.core, targetName); free(targetPath); return cc4group_addEntryToDirectory(parent, &entry); } static bool cc4group_createDirectory(CC4Group* const this, const char* const path) { assert(this); assert(path); C4GroupEntryData* entry = cc4group_createEntry(this, path); if(entry == NULL) { return false; } C4GroupHeader* header = malloc(sizeof(C4GroupHeader)); if(header == NULL) { SET_ERRNO_ERROR("malloc: allocating the group header for the new directory"); return 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; return true; } static bool cc4group_createFile(CC4Group* const this, const char* const path) { assert(this); assert(path); C4GroupEntryData* entry = cc4group_createEntry(this, path); if(entry == NULL) { return false; } return true; } static bool cc4group_setEntryData(CC4Group* const this, const char* const entryPath, const void* const data, size_t const size, int const memoryManagement) { assert(this); assert(entryPath); C4GroupEntryData* entry = (C4GroupEntryData*)cc4group_getFileByPath(this, entryPath); if(entry == NULL) { return false; } if(entry->freeData && entry->data != NULL) { free(entry->data); } if(data != NULL && size != 0) { if(memoryManagement == Copy) { entry->data = malloc(size); if(entry->data == NULL) { SET_ERRNO_ERROR("malloc: allocating memory for copying the file data"); return false; } memcpy(entry->data, data, size); } else { entry->data = (void*)data; } entry->core.Size = size; entry->freeData = memoryManagement != Reference; } else { entry->data = NULL; entry->core.Size = 0; entry->freeData = false; } 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; if(path == NULL || *path == '\0') { entry = &this->root; } else { entry = (C4GroupEntryData*)cc4group_getFileByPath(this, path); if(entry == NULL) { return false; } } entry->core.Executable = executable ? 1 : 0; return true; } CC4Group_API cc4group = { .new = cc4group_new, .create = cc4group_create, .delete = cc4group_delete, .openExisting = cc4group_openExisting, .save = cc4group_save, .saveOverwrite = cc4group_saveOverwrite, .extractAll = cc4group_extractAll, .extractSingle = cc4group_extractSingle, .getEntryData = cc4group_getEntryData, .setTmpMemoryStrategy = cc4group_setTmpMemoryStrategy, .TmpMemoryStrategies = { .Memory = cc4group_createTmpMemoryMalloc, .File = cc4group_createTmpMemoryFile, .Auto = cc4group_createTmpMemoryAuto }, .MemoryManagement = { .Take = Take, .Copy = Copy, .Reference = Reference }, .getErrorMessage = cc4group_getErrorMessage, .getErrorCode = cc4group_getErrorCode, .getErrorMethod = cc4group_getErrorMethod, .getErrorCauser = cc4group_getErrorCauser, .setMaker = cc4group_setMaker, .setCreation = cc4group_setCreation, .setOfficial = cc4group_setOfficial, .setExecutable = cc4group_setExecutable, .getEntryInfo = cc4group_getEntryInfo, .getEntryInfos = cc4group_getEntryInfos, .deleteEntry = cc4group_deleteEntry, .renameEntry = cc4group_renameEntry, .createDirectory = cc4group_createDirectory, .createFile = cc4group_createFile, .setEntryData = cc4group_setEntryData };