summaryrefslogtreecommitdiffstats
path: root/src/cc4group.c
diff options
context:
space:
mode:
authorMarkus Mittendrein <git@maxmitti.tk>2018-08-15 22:46:40 +0200
committerMarkus Mittendrein <git@maxmitti.tk>2018-08-15 23:15:15 +0200
commit0d1ae015fef8e15442dafa61b9c8d929ce467969 (patch)
treea8d56fd12f5c5366e1f6fa3ecc20c78c0202f747 /src/cc4group.c
parente04f662a42c75cb202684e7254d3b7600e6e1756 (diff)
downloadcc4group-0d1ae015fef8e15442dafa61b9c8d929ce467969.tar.gz
cc4group-0d1ae015fef8e15442dafa61b9c8d929ce467969.zip
Refactor code into a library and implement basic file management methods
Diffstat (limited to 'src/cc4group.c')
-rw-r--r--src/cc4group.c1620
1 files changed, 1620 insertions, 0 deletions
diff --git a/src/cc4group.c b/src/cc4group.c
new file mode 100644
index 0000000..043efda
--- /dev/null
+++ b/src/cc4group.c
@@ -0,0 +1,1620 @@
+// TODO? Error handling of lists?
+
+#define _POSIX_C_SOURCE 200809L
+#define _GNU_SOURCE
+
+#include "cc4group.h"
+#include "c4groupheader.h"
+#include "c4groupentrycore.h"
+#include "GenericList.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/time.h>
+#include <time.h>
+#include <assert.h>
+
+#include <stdio.h>
+
+#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(const 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;
+ 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;
+};
+
+static const C4GroupEntryData* cc4group_getEntryByPath(const 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, .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.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*)((void*)(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 = (void*)(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.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_setRecursiveMaker(const C4GroupEntryData* groupEntry, const char* const maker)
+{
+ ForeachGroupEntry(groupEntry->children)
+ {
+ if(entry->value.core.Directory)
+ {
+ C4GroupHeader_setMaker(entry->value.header, maker);
+ cc4group_setRecursiveMaker(&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_getEntryByPath(this, path);
+
+ if(entry == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired target directory does not exist");
+ return false;
+ }
+ else if(!entry->core.Directory)
+ {
+ SET_MESSAGE_ERROR("The desired target is not a directory");
+ return false;
+ }
+ }
+
+ C4GroupHeader_setMaker(entry->header, maker);
+ if(recursive)
+ {
+ cc4group_setRecursiveMaker(entry, maker);
+ }
+ return true;
+}
+
+static void cc4group_setRecursiveOfficial(const C4GroupEntryData* groupEntry, bool const official)
+{
+ ForeachGroupEntry(groupEntry->children)
+ {
+ if(entry->value.core.Directory)
+ {
+ C4GroupHeader_setOfficial(entry->value.header, official);
+ cc4group_setRecursiveOfficial(&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_getEntryByPath(this, path);
+
+ if(entry == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired target directory does not exist");
+ return false;
+ }
+ else if(!entry->core.Directory)
+ {
+ SET_MESSAGE_ERROR("The desired target is not a directory");
+ return false;
+ }
+ }
+
+ C4GroupHeader_setOfficial(entry->header, official);
+ if(recursive)
+ {
+ cc4group_setRecursiveOfficial(entry, official);
+ }
+ return true;
+}
+
+static void cc4group_setRecursiveCreation(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_setRecursiveCreation(&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_getEntryByPath(this, path);
+
+ if(entry == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired target file or directory does not exist");
+ return false;
+ }
+ }
+
+ entry->core.Modified = creation;
+
+ if(entry->core.Directory)
+ {
+ C4GroupHeader_setCreation(entry->header, creation);
+ if(recursive)
+ {
+ cc4group_setRecursiveCreation(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_getEntryByPath(this, this->subPath);
+
+ if(subRoot == NULL || !subRoot->core.Directory)
+ {
+ SET_MESSAGE_ERROR("The desired subgroup does not exist as a child in the mother group");
+ 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);
+}
+
+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;
+}
+
+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_getEntryByPath(this, entryPath);
+ if(entry == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired file was not found in the group file");
+ return false;
+ }
+
+ return cc4group_extractChildren(this, entry, targetPath);
+}
+
+static bool cc4group_getEntryData(const 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_getEntryByPath(this, entryPath);
+ if(entry == NULL || entry->core.Directory)
+ {
+ 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_getEntryByPath(this, path);
+ if(entry == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired file was not found in the group file");
+ 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_getEntryByPath(this, path);
+ if(entry == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired subgroup was not found in the group file");
+ return false;
+ }
+
+ if(!entry->core.Directory)
+ {
+ SET_MESSAGE_ERROR("The desired subgroup is a file instead of a group");
+ 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, 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);
+ char* parentPath;
+ char* entryName = cc4group_splitParentAndChildPaths(myPath, &parentPath);
+
+ parent = cc4group_getEntryByPath(this, parentPath);
+
+ if(parent == NULL)
+ {
+ SET_MESSAGE_ERROR("The containing directory of the desired file was not found in the group file");
+ 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(CC4Group* const this, C4GroupEntryData* const directory, const C4GroupEntryData* const entry)
+{
+ assert(this);
+ 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);
+ char* targetDirectory;
+ char* targetName = cc4group_splitParentAndChildPaths(targetPath, &targetDirectory);
+
+ C4GroupEntryData* newParent = (C4GroupEntryData*)cc4group_getEntryByPath(this, targetDirectory);
+ if(newParent == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired target directory does not exist");
+ free(targetPath);
+ return false;
+ }
+
+ C4GroupEntryCore_setFileName(&entry->value.core, targetName);
+ cc4group_addEntryToDirectory(this, 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);
+ char* targetDirectory;
+ char* targetName = cc4group_splitParentAndChildPaths(targetPath, &targetDirectory);
+
+ C4GroupEntryData* parent = (C4GroupEntryData*)cc4group_getEntryByPath(this, targetDirectory);
+ if(parent == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired containing target directory does not exist");
+ free(targetPath);
+ return NULL;
+ }
+
+ if(!parent->core.Directory)
+ {
+ SET_MESSAGE_ERROR("The desired containing target directory is a file");
+ free(targetPath);
+ return NULL;
+ }
+
+ C4GroupEntryData entry;
+
+ entry.data = NULL;
+ entry.children = GroupEntryListNew();
+
+ C4GroupEntryCore_init(&entry.core);
+ C4GroupEntryCore_setFileName(&entry.core, targetName);
+
+ free(targetPath);
+
+ return cc4group_addEntryToDirectory(this, 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, void* const data, size_t const size)
+{
+ assert(this);
+ assert(path);
+
+ C4GroupEntryData* entry = cc4group_createEntry(this, path);
+
+ if(entry == NULL)
+ {
+ return false;
+ }
+
+ if(size != 0 && data != NULL)
+ {
+ entry->core.Size = size;
+ entry->data = data;
+
+ AddCleanupJob(free, data);
+ }
+
+ return true;
+}
+
+static bool cc4group_setEntryData(const CC4Group* const this, const char* const entryPath, void* const data, size_t const size)
+{
+ assert(this);
+ assert(entryPath);
+
+ C4GroupEntryData* entry = (C4GroupEntryData*)cc4group_getEntryByPath(this, entryPath);
+ if(entry == NULL || entry->core.Directory)
+ {
+ return false;
+ }
+
+ if(data != NULL && size != 0)
+ {
+ entry->data = data;
+ entry->core.Size = size;
+
+ AddCleanupJob(free, data);
+ }
+ else
+ {
+ entry->data = NULL;
+ entry->core.Size = 0;
+ }
+
+ 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_getEntryByPath(this, path);
+
+ if(entry == NULL)
+ {
+ SET_MESSAGE_ERROR("The desired target file does not exist");
+ return false;
+ }
+ else if(entry->core.Directory)
+ {
+ SET_MESSAGE_ERROR("The desired target is not a file");
+ 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
+ },
+
+ .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
+};