summaryrefslogtreecommitdiffstats
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
parente04f662a42c75cb202684e7254d3b7600e6e1756 (diff)
downloadcc4group-0d1ae015fef8e15442dafa61b9c8d929ce467969.tar.gz
cc4group-0d1ae015fef8e15442dafa61b9c8d929ce467969.zip
Refactor code into a library and implement basic file management methods
-rw-r--r--CC4Group.kdev43
-rw-r--r--CMakeLists.txt32
-rw-r--r--examples/c4cat.c128
-rw-r--r--examples/c4cat_dyn.c145
-rw-r--r--examples/c4copy.c36
-rw-r--r--examples/c4info.c45
-rw-r--r--examples/c4ls.c87
-rw-r--r--examples/c4mkdir.c49
-rw-r--r--examples/c4rm.c49
-rw-r--r--examples/c4touch.c61
-rw-r--r--examples/unc4group.c46
-rw-r--r--main.c376
-rw-r--r--src/GenericList.h (renamed from GenericList.h)0
-rw-r--r--src/c4groupentrycore.c24
-rw-r--r--src/c4groupentrycore.h26
-rw-r--r--src/c4groupheader.c40
-rw-r--r--src/c4groupheader.h27
-rw-r--r--src/cc4group.c1620
-rw-r--r--src/cc4group.h97
19 files changed, 2511 insertions, 380 deletions
diff --git a/CC4Group.kdev4 b/CC4Group.kdev4
deleted file mode 100644
index 063ba2a..0000000
--- a/CC4Group.kdev4
+++ /dev/null
@@ -1,3 +0,0 @@
-[Project]
-Name=CC4Group
-Manager=KDevCMakeManager
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b7bb5cc..47da632 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -10,6 +10,36 @@ endif()
find_package(ZLIB REQUIRED)
-add_executable(cc4group main.c)
+set(cc4group_SRC
+ src/cc4group.c
+ src/c4groupheader.c
+ src/c4groupentrycore.c
+)
+
+add_library(cc4group STATIC ${cc4group_SRC})
target_link_libraries(cc4group PRIVATE ZLIB::ZLIB)
target_include_directories(cc4group PRIVATE ZLIB::ZLIB)
+set_property(TARGET cc4group PROPERTY POSITION_INDEPENDENT_CODE ON)
+
+add_library(cc4group_dyn SHARED ${cc4group_SRC})
+target_link_libraries(cc4group_dyn PRIVATE ZLIB::ZLIB)
+target_include_directories(cc4group_dyn PRIVATE ZLIB::ZLIB)
+
+include_directories(src)
+
+function(example name)
+ add_executable(${name} examples/${name}.c)
+ target_link_libraries(${name} PRIVATE cc4group)
+endfunction()
+
+example(unc4group)
+example(c4cat)
+example(c4copy)
+example(c4info)
+example(c4ls)
+example(c4touch)
+example(c4rm)
+example(c4mkdir)
+
+add_executable(c4cat_dyn examples/c4cat_dyn.c)
+target_link_libraries(c4cat_dyn PRIVATE -ldl)
diff --git a/examples/c4cat.c b/examples/c4cat.c
new file mode 100644
index 0000000..04f51b6
--- /dev/null
+++ b/examples/c4cat.c
@@ -0,0 +1,128 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "cc4group.h"
+
+bool catNormalFile(const char* const path, const off_t size)
+{
+ int file = open(path, O_RDONLY);
+ if(file == -1)
+ {
+ fprintf(stderr, "ERROR: Reading \"%s\": %s\n", path, strerror(errno));
+ return false;
+ }
+
+ void* mappedFile = mmap(NULL, size, PROT_READ, MAP_PRIVATE, file, 0);
+
+ if(close(file) == -1)
+ {
+ fprintf(stderr, "ERROR: Closing \"%s\": %s\n", path, strerror(errno));
+ }
+
+ if(mappedFile == MAP_FAILED)
+ {
+ fprintf(stderr, "ERROR: Mapping \"%s\" for reading: %s\n", path, strerror(errno));
+ return false;
+ }
+
+ if(write(STDOUT_FILENO, mappedFile, size) != size)
+ {
+ fprintf(stderr, "ERROR: Writing file contents of \"%s\" to stdout: %s\n", path, strerror(errno));
+ }
+
+ if(munmap(mappedFile, size) == -1)
+ {
+ fprintf(stderr, "ERROR: Unmapping \"%s\": %s\n", path, strerror(errno));
+ }
+
+ return true;
+}
+
+bool catFileWithPathsAndSize(const char* const completePath, const char* const groupPath, off_t fileSize)
+{
+ if(strcmp(groupPath, completePath) == 0)
+ {
+ return catNormalFile(completePath, fileSize);
+ }
+
+ const char* pathInGroup = completePath + strlen(groupPath) + 1;
+ CC4Group* group = cc4group.new();
+ if(!cc4group.openExisting(group, groupPath))
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", groupPath, cc4group.getErrorMessage(group));
+ return false;
+ }
+
+ const void* data;
+ size_t size;
+ bool success = cc4group.getEntryData(group, pathInGroup, &data, &size);
+
+ if(success)
+ {
+ if(write(STDOUT_FILENO, data, size) != (ssize_t)size)
+ {
+ fprintf(stderr, "ERROR: Writing file contents of \"%s\" to stdout: %s\n", completePath, strerror(errno));
+ }
+ }
+ else
+ {
+ fprintf(stderr, "ERROR: Reading \"%s\" from \"%s\": %s\n", pathInGroup, groupPath, strerror(ENOENT));
+ }
+
+ cc4group.delete(group);
+
+ return success;
+}
+
+bool catFile(const char* const completePath)
+{
+ struct stat st;
+
+ char* groupPath = strdup(completePath);
+ char* slash;
+ for(slash = groupPath + strlen(groupPath); slash != NULL; slash = strrchr(groupPath, '/'))
+ {
+ *slash = '\0';
+ if(stat(groupPath, &st) == 0 && !S_ISDIR(st.st_mode))
+ {
+ break;
+ }
+ }
+
+ bool success;
+ if(slash == NULL)
+ {
+ fprintf(stderr, "ERROR: Reading \"%s\": %s\n", completePath, strerror(ENOENT));
+ success = false;
+ }
+ else
+ {
+ success = catFileWithPathsAndSize(completePath, groupPath, st.st_size);
+ }
+
+ free(groupPath);
+ return success;
+}
+
+int main(int argc, char* argv[])
+{
+ if(argc < 2)
+ {
+ fprintf(stderr, "USAGE: %s <entry path>...\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ bool success = true;
+ for(int i = 1; i < argc; ++i)
+ {
+ success &= catFile(argv[i]);
+ }
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/c4cat_dyn.c b/examples/c4cat_dyn.c
new file mode 100644
index 0000000..92da979
--- /dev/null
+++ b/examples/c4cat_dyn.c
@@ -0,0 +1,145 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <dlfcn.h>
+#include <assert.h>
+
+#define CC4GROUP_DYNAMIC_LOAD
+#include "cc4group.h"
+#undef CC4GROUP_DYNAMIC_LOAD
+
+CC4Group_API* cc4group_dyn;
+#define cc4group (*cc4group_dyn)
+
+bool catNormalFile(const char* const path, const off_t size)
+{
+ int file = open(path, O_RDONLY);
+ if(file == -1)
+ {
+ fprintf(stderr, "ERROR: Reading \"%s\": %s\n", path, strerror(errno));
+ return false;
+ }
+
+ void* mappedFile = mmap(NULL, size, PROT_READ, MAP_PRIVATE, file, 0);
+
+ if(close(file) == -1)
+ {
+ fprintf(stderr, "ERROR: Closing \"%s\": %s\n", path, strerror(errno));
+ }
+
+ if(mappedFile == MAP_FAILED)
+ {
+ fprintf(stderr, "ERROR: Mapping \"%s\" for reading: %s\n", path, strerror(errno));
+ return false;
+ }
+
+ if(write(STDOUT_FILENO, mappedFile, size) != size)
+ {
+ fprintf(stderr, "ERROR: Writing file contents of \"%s\" to stdout: %s\n", path, strerror(errno));
+ }
+
+ if(munmap(mappedFile, size) == -1)
+ {
+ fprintf(stderr, "ERROR: Unmapping \"%s\": %s\n", path, strerror(errno));
+ }
+
+ return true;
+}
+
+bool catFileWithPathsAndSize(const char* const completePath, const char* const groupPath, off_t fileSize)
+{
+ if(strcmp(groupPath, completePath) == 0)
+ {
+ return catNormalFile(completePath, fileSize);
+ }
+
+ const char* pathInGroup = completePath + strlen(groupPath) + 1;
+ CC4Group* group = cc4group.new();
+ if(!cc4group.openExisting(group, groupPath))
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", groupPath, cc4group.getErrorMessage(group));
+ return false;
+ }
+
+ const void* data;
+ size_t size;
+ bool success = cc4group.getEntryData(group, pathInGroup, &data, &size);
+
+ if(success)
+ {
+ if(write(STDOUT_FILENO, data, size) != (ssize_t)size)
+ {
+ fprintf(stderr, "ERROR: Writing file contents of \"%s\" to stdout: %s\n", completePath, strerror(errno));
+ }
+ }
+ else
+ {
+ fprintf(stderr, "ERROR: Reading \"%s\" from \"%s\": %s\n", pathInGroup, groupPath, strerror(ENOENT));
+ }
+
+ cc4group.delete(group);
+
+ return success;
+}
+
+bool catFile(const char* const completePath)
+{
+ struct stat st;
+
+ char* groupPath = strdup(completePath);
+ char* slash;
+ for(slash = groupPath + strlen(groupPath); slash != NULL; slash = strrchr(groupPath, '/'))
+ {
+ *slash = '\0';
+ if(stat(groupPath, &st) == 0 && !S_ISDIR(st.st_mode))
+ {
+ break;
+ }
+ }
+
+ bool success;
+ if(slash == NULL)
+ {
+ fprintf(stderr, "ERROR: Reading \"%s\": %s\n", completePath, strerror(ENOENT));
+ success = false;
+ }
+ else
+ {
+ success = catFileWithPathsAndSize(completePath, groupPath, st.st_size);
+ }
+
+ free(groupPath);
+ return success;
+}
+
+int main(int argc, char* argv[])
+{
+ void* dl = dlopen("./libcc4group_dyn.so", RTLD_LAZY);
+ assert(dl);
+
+ dlerror();
+ cc4group_dyn = dlsym(dl, "cc4group");
+ assert(!dlerror());
+
+ if(argc < 2)
+ {
+ fprintf(stderr, "USAGE: %s <entry path>...\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ bool success = true;
+ for(int i = 1; i < argc; ++i)
+ {
+ success &= catFile(argv[i]);
+ }
+
+ dlclose(dl);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/c4copy.c b/examples/c4copy.c
new file mode 100644
index 0000000..35e201b
--- /dev/null
+++ b/examples/c4copy.c
@@ -0,0 +1,36 @@
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "cc4group.h"
+
+int main(int argc, char* argv[])
+{
+ if(argc != 3)
+ {
+ fprintf(stderr, "USAGE: %s <group> <target>\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ bool success = true;
+
+ CC4Group* group = cc4group.new();
+ if(!cc4group.openExisting(group, argv[1]))
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", argv[1], cc4group.getErrorMessage(group));
+
+ success = false;
+ }
+ else
+ {
+ success = cc4group.save(group, argv[2]);
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not save group file to \"%s\": %s\n", argv[2], cc4group.getErrorMessage(group));
+ }
+ }
+
+ cc4group.delete(group);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/c4info.c b/examples/c4info.c
new file mode 100644
index 0000000..6fa63fe
--- /dev/null
+++ b/examples/c4info.c
@@ -0,0 +1,45 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+
+#include "cc4group.h"
+
+const char* formatTime(int32_t time)
+{
+ time_t tTime = time;
+ static char ret[128];
+ strftime(ret, sizeof(ret), "%c", localtime(&tTime));
+ return ret;
+}
+
+int main(int argc, char* argv[])
+{
+ if(argc != 2)
+ {
+ fprintf(stderr, "USAGE: %s <group>\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ CC4Group* group = cc4group.new();
+ bool success = cc4group.openExisting(group, argv[1]);
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", argv[1], cc4group.getErrorMessage(group));
+ }
+ else
+ {
+ CC4Group_EntryInfo info;
+ cc4group.getEntryInfo(group, "", &info);
+
+ puts(argv[1]);
+ printf("Created: %s\n", formatTime(info.modified));
+ printf("Author: %s\n", info.author);
+ if(info.official)
+ {
+ puts("Official");
+ }
+ }
+ cc4group.delete(group);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/c4ls.c b/examples/c4ls.c
new file mode 100644
index 0000000..4324d9b
--- /dev/null
+++ b/examples/c4ls.c
@@ -0,0 +1,87 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+#include <locale.h>
+
+#include "cc4group.h"
+
+const char* formatTime(int32_t time)
+{
+ time_t tTime = time;
+ static char ret[128];
+ strftime(ret, sizeof(ret), "%c", localtime(&tTime));
+ return ret;
+}
+
+const char* formatSize(size_t size)
+{
+ static char ret[128];
+ if(size < 1000)
+ {
+ snprintf(ret, 128, "%4zd", size);
+ return ret;
+ }
+
+ static const char prefixes[] = " KMGTPE";
+
+ const char* prefix = prefixes;
+ size_t fract = 0;
+
+ while(size >= 1000 && *(prefix + 1) != '\0')
+ {
+ ++prefix;
+ fract = size % 1000;
+ size /= 1000;
+ }
+
+ if(size < 10)
+ {
+ snprintf(ret, 128, "%1.1f%c", (float)size + (float)fract / 1000, *prefix);
+ }
+ else
+ {
+ snprintf(ret, 128, "%3zd%c", size, *prefix);
+ }
+
+ return ret;
+}
+
+int main(int argc, char* argv[])
+{
+ if(argc != 2 && argc != 3)
+ {
+ fprintf(stderr, "USAGE: %s <group> [subgroup]\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ CC4Group* group = cc4group.new();
+ bool success = cc4group.openExisting(group, argv[1]);
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", argv[1], cc4group.getErrorMessage(group));
+ }
+ else
+ {
+ CC4Group_EntryInfo* infos;
+ size_t entries;
+
+ success = cc4group.getEntryInfos(group, argc == 3 ? argv[2] : NULL, &infos, &entries);
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not list group entries: %s\n", cc4group.getErrorMessage(group));
+ }
+ else
+ {
+ for(size_t i = 0; i < entries; ++i)
+ {
+ printf("%c %s %s %s\t%s\n", infos[i].directory ? 'd' : infos[i].executable ? 'x' : ' ', formatTime(infos[i].modified), formatSize(infos[i].totalSize), infos[i].fileName, infos[i].author);
+ }
+
+ free(infos);
+ }
+ }
+ cc4group.delete(group);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/c4mkdir.c b/examples/c4mkdir.c
new file mode 100644
index 0000000..5677125
--- /dev/null
+++ b/examples/c4mkdir.c
@@ -0,0 +1,49 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "cc4group.h"
+
+int main(int argc, char* argv[])
+{
+ bool recursive = argc == 4 && strcmp(argv[1], "-r") == 0;
+
+ if(argc != 3 && !recursive)
+ {
+ fprintf(stderr, "USAGE: %s [-r] <group> <entry>\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ if(recursive)
+ {
+ ++argv;
+ }
+
+ CC4Group* group = cc4group.new();
+ bool success = cc4group.openExisting(group, argv[1]);
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", argv[1], cc4group.getErrorMessage(group));
+ }
+ else
+ {
+ success = cc4group.deleteEntry(group, argv[2], recursive);
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not delete group entry \"%s\": %s\n", argv[2], cc4group.getErrorMessage(group));
+ }
+ else
+ {
+ success = cc4group.saveOverwrite(group, argv[1]);
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not save group file \"%s\": %s\n", argv[2], cc4group.getErrorMessage(group));
+ }
+ }
+ }
+ cc4group.delete(group);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/c4rm.c b/examples/c4rm.c
new file mode 100644
index 0000000..5677125
--- /dev/null
+++ b/examples/c4rm.c
@@ -0,0 +1,49 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "cc4group.h"
+
+int main(int argc, char* argv[])
+{
+ bool recursive = argc == 4 && strcmp(argv[1], "-r") == 0;
+
+ if(argc != 3 && !recursive)
+ {
+ fprintf(stderr, "USAGE: %s [-r] <group> <entry>\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ if(recursive)
+ {
+ ++argv;
+ }
+
+ CC4Group* group = cc4group.new();
+ bool success = cc4group.openExisting(group, argv[1]);
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", argv[1], cc4group.getErrorMessage(group));
+ }
+ else
+ {
+ success = cc4group.deleteEntry(group, argv[2], recursive);
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not delete group entry \"%s\": %s\n", argv[2], cc4group.getErrorMessage(group));
+ }
+ else
+ {
+ success = cc4group.saveOverwrite(group, argv[1]);
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not save group file \"%s\": %s\n", argv[2], cc4group.getErrorMessage(group));
+ }
+ }
+ }
+ cc4group.delete(group);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/c4touch.c b/examples/c4touch.c
new file mode 100644
index 0000000..5d0000d
--- /dev/null
+++ b/examples/c4touch.c
@@ -0,0 +1,61 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "cc4group.h"
+
+int main(int argc, char* argv[])
+{
+ if(argc != 2 && argc != 3)
+ {
+ fprintf(stderr, "USAGE: %s <group> [author]\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ CC4Group* group = cc4group.new();
+ bool success, exists;
+ struct stat st;
+ if(stat(argv[1], &st) == 0)
+ {
+ success = cc4group.openExisting(group, argv[1]);
+ exists = true;
+ }
+ else
+ {
+ success = cc4group.create(group);
+ exists = false;
+ }
+ if(!success)
+ {
+ const char* error = cc4group.getErrorMessage(group);
+ if(exists)
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", argv[1], error);
+ }
+ else
+ {
+ fprintf(stderr, "ERROR: Can not initialize empty group file: %s\n", error);
+ }
+ }
+
+ if(success)
+ {
+ cc4group.setCreation(group, time(NULL), NULL, false);
+
+ if(argc == 3)
+ {
+ cc4group.setMaker(group, argv[2], NULL, true);
+ }
+
+ success = (exists ? cc4group.saveOverwrite : cc4group.save)(group, argv[1]);
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Can not save group file to \"%s\": %s\n", argv[1], cc4group.getErrorMessage(group));
+ }
+ }
+ cc4group.delete(group);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/examples/unc4group.c b/examples/unc4group.c
new file mode 100644
index 0000000..8d08eb3
--- /dev/null
+++ b/examples/unc4group.c
@@ -0,0 +1,46 @@
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "cc4group.h"
+
+int main(int argc, char* argv[])
+{
+ if(argc != 3 && argc != 4)
+ {
+ fprintf(stderr, "USAGE: %s <group> [group entry] <target>\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ CC4Group* group = cc4group.new();
+ if(!cc4group.openExisting(group, argv[1]))
+ {
+ fprintf(stderr, "ERROR: Can not open group file \"%s\": %s\n", argv[1], cc4group.getErrorMessage(group));
+
+ cc4group.delete(group);
+ return EXIT_FAILURE;
+ }
+
+ bool success;
+ if(argc == 3)
+ {
+ success = cc4group.extractAll(group, argv[2]);
+ }
+ else if(argc == 4)
+ {
+ success = cc4group.extractSingle(group, argv[2], argv[3]);
+ }
+ else
+ {
+ fprintf(stderr, "ERROR: Can't get here\n");
+ success = false;
+ }
+
+ if(!success)
+ {
+ fprintf(stderr, "ERROR: Extracting the desired file/group: %s\n", cc4group.getErrorMessage(group));
+ }
+
+ cc4group.delete(group);
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/main.c b/main.c
deleted file mode 100644
index b52e32d..0000000
--- a/main.c
+++ /dev/null
@@ -1,376 +0,0 @@
-#include <stdint.h>
-#include <stdlib.h>
-#include <stdio.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 <zlib.h>
-
-#include <assert.h>
-
-#include "GenericList.h"
-
-#define C4GroupMaxMaker 30
-#define C4GroupMaxPassword 30
-
-typedef struct {
- char id[25];
- uint8_t Reserved1[3];
- int32_t Ver1, Ver2;
- int32_t Entries;
- char Maker[C4GroupMaxMaker + 2];
- char Password[C4GroupMaxPassword + 2];
- int32_t Creation;
- int32_t Original;
- uint8_t Reserved2[92];
-} __attribute__((__packed__)) C4GroupHeader;
-
-typedef struct {
- char FileName[257];
- uint8_t Reserved1[3];
- int32_t Packed;
- int32_t Directory;
- int32_t Size;
- int32_t Reserved2;
- int32_t Offset;
- int32_t Modified;
- uint8_t HasCRC;
- uint32_t CRC;
- uint8_t Executable;
- uint8_t Reserved3[26];
-} __attribute__((__packed__)) C4GroupEntryCore;
-
-struct list_GroupEntryList;
-typedef struct {
- C4GroupEntryCore core;
- uint8_t* data;
- struct list_GroupEntryList* children;
-} C4GroupEntryData;
-
-LIST_AUTO(C4GroupEntryData, GroupEntryList)
-#define ForeachGroupEntry(list) LIST_FOREACH(GroupEntryList, list, entry)
-
-off_t fileSizeFd(int fd)
-{
- struct stat st;
-
- if(fstat(fd, &st) == -1)
- {
- return -1;
- }
-
- return st.st_size;
-}
-
-void memScrambleHeader(uint8_t* 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;
- }
-}
-
-void buildChildren(C4GroupEntryData* entry)
-{
- C4GroupHeader* header = (C4GroupHeader*)entry->data;
-
- entry->children = GroupEntryListNew();
-
- uint8_t* data = entry->data + sizeof(C4GroupHeader);
- uint8_t* childData = entry->data + sizeof(C4GroupHeader) + sizeof(C4GroupEntryCore) * header->Entries;
-
- for(size_t i = 0; i < (size_t)header->Entries; ++i)
- {
- C4GroupEntryCore* core = (C4GroupEntryCore*)(data + sizeof(C4GroupEntryCore) * i);
- C4GroupEntryData* childEntry = &GroupEntryListAppend(entry->children, (C4GroupEntryData){.core = *core, .data = childData + core->Offset, .children = NULL})->value;
-
- if(core->Directory)
- {
- memScrambleHeader(childEntry->data);
- buildChildren(childEntry);
- }
- }
-}
-
-const char* formatTime(int32_t time)
-{
- time_t tTime = time;
- static char ret[128];
- strftime(ret, sizeof(ret), "%c", localtime(&tTime));
- return ret;
-}
-
-void handleChildren(GroupEntryList* entries, size_t indent, const char* path)
-{
- size_t pathLen = strlen(path);
- char* targetPath = malloc(pathLen + 260 + 1);
-
- strcpy(targetPath, path);
- strcpy(targetPath + pathLen++, "/");
-
- if(mkdir(path, 0755) == -1)
- {
- fprintf(stderr, "ERROR: Creating target directory \"%s\": %s\n", path, strerror(errno));
- goto ret;
- }
-
- ForeachGroupEntry(entries)
- {
- for(size_t i = 0; i < indent; ++i)
- {
- putchar('\t');
- }
-
- strcpy(targetPath + pathLen, entry->value.core.FileName);
-
-// printf("%s\t%s\t%d B\n", targetPath, formatTime(entry->value.core.Modified), entry->value.core.Size);
-
- if(!entry->value.core.Directory)
- {
- int file = open(targetPath, O_WRONLY | O_CREAT, entry->value.core.Executable ? 0755 : 0644);
- if(file == -1)
- {
- fprintf(stderr, "ERROR: Creating target file \"%s\": %s\n", targetPath, strerror(errno));
- goto ret;
- }
- write(file, entry->value.data, entry->value.core.Size);
-
- if(close(file) == -1)
- {
- fprintf(stderr, "ERROR: Closing file \"%s\": %s\n", targetPath, strerror(errno));
- }
-
- struct timeval tv[2] = {{.tv_usec = 0, .tv_sec = entry->value.core.Modified}, {.tv_usec = 0, .tv_sec = entry->value.core.Modified}};
- if(utimes(targetPath, tv) == -1)
- {
- fprintf(stderr, "ERROR: Setting modification time for \"%s\": %s\n", targetPath, strerror(errno));
- }
- }
-
- if(entry->value.core.Directory)
- {
- handleChildren(entry->value.children, indent + 1, targetPath);
- }
- }
-
-ret:
- free(targetPath);
-}
-
-void deleteChildren(GroupEntryList* entries)
-{
- ForeachGroupEntry(entries)
- {
- if(entry->value.core.Directory)
- {
- deleteChildren(entry->value.children);
- }
- }
-
- GroupEntryListDestroy(entries);
-}
-
-int main(int argc, char* argv[])
-{
- if(argc != 3)
- {
- fprintf(stderr, "USAGE: %s <group> <target>\n", argv[0]);
- return EXIT_FAILURE;
- }
-
- int file = open(argv[1], O_RDONLY);
- if(file == -1)
- {
- fprintf(stderr, "ERROR: Opening file \"%s\": %s\n", argv[1], strerror(errno));
- return EXIT_FAILURE;
- }
-
- __off_t size = fileSizeFd(file);
- if(size == -1)
- {
- fprintf(stderr, "ERROR: Getting file size: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
- else
- {
- printf("Size: %ld\n", size);
- }
-
- uint8_t* mappedFile = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, file, 0);
-
- if(mappedFile == MAP_FAILED)
- {
- fprintf(stderr, "ERROR: Mapping file: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- if(close(file) == -1)
- {
- fprintf(stderr, "ERROR: Closing file: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- if((mappedFile[0] != 0x1e && mappedFile[0] != 0x1f) || (mappedFile[1] != 0x8c && mappedFile[1] != 0x8b))
- {
- fprintf(stderr, "ERROR: The file is not a valid group file. Magic bytes don't match.\n");
- return EXIT_FAILURE;
- }
-
- 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
-
- if(ret != Z_OK)
- {
- fprintf(stderr, "ERROR: inflateInit2: %s\n", zError(ret));
- return EXIT_FAILURE;
- }
-
- size_t currentSize = sizeof(C4GroupHeader);
- C4GroupHeader* header = malloc(currentSize);
-
- strm.next_out = (Bytef*)header;
- strm.avail_out = currentSize;
-
- ret = inflate(&strm, Z_SYNC_FLUSH);
- if(ret != Z_OK)
- {
- fprintf(stderr, "ERROR: inflating header: %s\n", zError(ret));
- return EXIT_FAILURE;
- }
-
- memScrambleHeader((uint8_t*)header);
-
- printf("Version %d.%d\n", header->Ver1, header->Ver2);
- printf("%d Entries\n", header->Entries);
- printf("%sOriginal\n", header->Original == 1234567 ? "" : "Not ");
- printf("Created %s\n", formatTime(header->Creation));
- puts(header->id);
- puts(header->Maker);
-
- assert(header->Ver1 == 1);
- assert(header->Ver2 == 2);
-
- currentSize += sizeof(C4GroupEntryCore) * header->Entries;
- header = realloc(header, currentSize);
- C4GroupEntryCore* cores = (C4GroupEntryCore*)((void*)(header) + sizeof(C4GroupHeader));
-
- GroupEntryList* entries = GroupEntryListNew();
-
- strm.next_out = (Bytef*)cores;
- strm.avail_out = sizeof(C4GroupEntryCore) * header->Entries;
-
- ret = inflate(&strm, Z_SYNC_FLUSH);
- if(ret != Z_OK)
- {
- fprintf(stderr, "ERROR: inflating header: %s\n", zError(ret));
- return EXIT_FAILURE;
- }
-
- C4GroupEntryCore* last = cores + header->Entries - 1;
- size_t uncompressedSize = last->Offset + last->Size;
-
- currentSize += uncompressedSize;
-
-#define TMP_FILE "cc4group.tmp"
- int tmpFile = open(TMP_FILE, O_CREAT | O_RDWR | O_TRUNC | O_EXCL, 0600);
- if(tmpFile == -1)
- {
- fprintf(stderr, "ERROR: Opening tmp file \"%s\": %s\n", "cc4group.tmp", strerror(errno));
- return EXIT_FAILURE;
- }
-
- if(unlink(TMP_FILE) == -1)
- {
- fprintf(stderr, "ERROR: Deleting tmp file \"%s\". Manual deletion is required: %s\n", TMP_FILE, strerror(errno));
- }
-#undef TMP_FILE
-
- // allocate file size
- // https://gist.github.com/marcetcheverry/991042
- if(lseek(tmpFile, currentSize - 1, SEEK_SET) == -1)
- {
- fprintf(stderr, "ERROR: Seeking the tmp file to the end: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- if(write(tmpFile, "", 1) == -1)
- {
- fprintf(stderr, "ERROR: Writing to the tmp file's end: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- uint8_t* mmappedHeader = mmap(NULL, currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, tmpFile, 0);
- if(mmappedHeader == MAP_FAILED)
- {
- fprintf(stderr, "ERROR: Mapping tmp file: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- if(close(tmpFile) == -1)
- {
- fprintf(stderr, "ERROR: Closing tmp file \"cc4group.tmp\": %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- uint8_t* data = (void*)(mmappedHeader) + sizeof(C4GroupHeader) + sizeof(C4GroupEntryCore) * header->Entries;
-
- // write alredy decompressed header and cores into the file
- memcpy(mmappedHeader, header, data - mmappedHeader);
-
- strm.next_out = data;
- strm.avail_out = uncompressedSize;
-
- ret = inflate(&strm, Z_SYNC_FLUSH);
- if(ret != Z_STREAM_END)
- {
- fprintf(stderr, "ERROR: inflating header: %s\n", zError(ret));
- return EXIT_FAILURE;
- }
-
- inflateEnd(&strm);
-
- if(munmap(mappedFile, size) == -1)
- {
- fprintf(stderr, "ERROR: Unmapping file: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- C4GroupEntryData root = {.data = (uint8_t*)mmappedHeader, .children = NULL};
- buildChildren(&root);
-
- handleChildren(root.children, 0, argv[2]);
-
- deleteChildren(entries);
-
- free(header);
-
- if(munmap(mmappedHeader, size) == -1)
- {
- fprintf(stderr, "ERROR: Unmapping tmp file: %s\n", strerror(errno));
- return EXIT_FAILURE;
- }
-
- return EXIT_SUCCESS;
-}
diff --git a/GenericList.h b/src/GenericList.h
index 0679bb7..0679bb7 100644
--- a/GenericList.h
+++ b/src/GenericList.h
diff --git a/src/c4groupentrycore.c b/src/c4groupentrycore.c
new file mode 100644
index 0000000..8b483cc
--- /dev/null
+++ b/src/c4groupentrycore.c
@@ -0,0 +1,24 @@
+#include "c4groupentrycore.h"
+#include <string.h>
+#include <time.h>
+
+void C4GroupEntryCore_init(C4GroupEntryCore* const this)
+{
+ memset(this->FileName, 0, sizeof(this->FileName));
+ memset(this->Reserved1, 0, sizeof(this->Reserved1));
+ this->Packed = 0;
+ this->Directory = 0;
+ this->Size = 0;
+ this->Reserved2 = 0;
+ this->Offset = 0;
+ this->Modified = time(NULL);
+ this->HasCRC = C4GroupEntryCore_NoCRC;
+ this->CRC = 0;
+ this->Executable = 0;
+ memset(this->Reserved3, 0, sizeof(this->Reserved3));
+}
+
+void C4GroupEntryCore_setFileName(C4GroupEntryCore* const this, const char* const fileName)
+{
+ strncpy(this->FileName, fileName, sizeof(this->FileName) - 1);
+}
diff --git a/src/c4groupentrycore.h b/src/c4groupentrycore.h
new file mode 100644
index 0000000..c77a5d1
--- /dev/null
+++ b/src/c4groupentrycore.h
@@ -0,0 +1,26 @@
+#pragma once
+#include <stdint.h>
+
+typedef enum {
+ C4GroupEntryCore_NoCRC = 0,
+ C4GroupEntryCore_ContentsCRC = 1,
+ C4GroupEntryCore_ContentsFileNameCRC = 2
+} C4GroupEntryCore_HasCRC;
+
+typedef struct {
+ char FileName[257];
+ uint8_t Reserved1[3];
+ int32_t Packed;
+ int32_t Directory;
+ int32_t Size;
+ int32_t Reserved2;
+ int32_t Offset;
+ int32_t Modified;
+ uint8_t HasCRC;
+ uint32_t CRC;
+ uint8_t Executable;
+ uint8_t Reserved3[26];
+} __attribute__((__packed__)) C4GroupEntryCore;
+
+void C4GroupEntryCore_init(C4GroupEntryCore* const this);
+void C4GroupEntryCore_setFileName(C4GroupEntryCore* const this, const char* const fileName);
diff --git a/src/c4groupheader.c b/src/c4groupheader.c
new file mode 100644
index 0000000..e06155c
--- /dev/null
+++ b/src/c4groupheader.c
@@ -0,0 +1,40 @@
+#include "c4groupheader.h"
+#include <string.h>
+#include <time.h>
+
+void C4GroupHeader_init(C4GroupHeader* const this)
+{
+ // id is not cleared because the id string exactly fits the size
+ strcpy(this->id, C4GroupId);
+ memset(this->Reserved1, 0, sizeof(this->Reserved1));
+ memset(this->Reserved2, 0, sizeof(this->Reserved2));
+ memset(this->Password, 0, sizeof(this->Password));
+
+ this->Ver1 = 1;
+ this->Ver2 = 2;
+ this->Entries = 0;
+ C4GroupHeader_setMaker(this, "New C4Group");
+ this->Maker[C4GroupMaxMaker + 1] = '\0';
+ C4GroupHeader_setCreation(this, time(NULL));
+ C4GroupHeader_setOfficial(this, false);
+}
+
+void C4GroupHeader_setMaker(C4GroupHeader* const this, const char* const maker)
+{
+ strncpy(this->Maker, maker, C4GroupMaxMaker + 1);
+}
+
+void C4GroupHeader_setCreation(C4GroupHeader* const this, int32_t const creation)
+{
+ this->Creation = creation;
+}
+
+void C4GroupHeader_setOfficial(C4GroupHeader* const this, const bool official)
+{
+ this->Official = official ? C4GroupOfficial : 0;
+}
+
+bool C4GroupHeader_isOfficial(const C4GroupHeader* const this)
+{
+ return this->Official == C4GroupOfficial;
+}
diff --git a/src/c4groupheader.h b/src/c4groupheader.h
new file mode 100644
index 0000000..1faa912
--- /dev/null
+++ b/src/c4groupheader.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <stdint.h>
+#include <stdbool.h>
+
+#define C4GroupMaxMaker 30
+#define C4GroupMaxPassword 30
+
+#define C4GroupId "RedWolf Design GrpFolder"
+#define C4GroupOfficial 1234567
+
+typedef struct {
+ char id[25];
+ uint8_t Reserved1[3];
+ int32_t Ver1, Ver2;
+ int32_t Entries;
+ char Maker[C4GroupMaxMaker + 2];
+ char Password[C4GroupMaxPassword + 2]; // also reserved
+ int32_t Creation;
+ int32_t Official;
+ uint8_t Reserved2[92];
+} __attribute__((__packed__)) C4GroupHeader;
+
+void C4GroupHeader_init(C4GroupHeader* const this);
+void C4GroupHeader_setMaker(C4GroupHeader* const this, const char* const maker);
+bool C4GroupHeader_isOfficial(const C4GroupHeader* const this);
+void C4GroupHeader_setOfficial(C4GroupHeader* const this, bool const official);
+void C4GroupHeader_setCreation(C4GroupHeader* const this, int32_t const creation);
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
+};
diff --git a/src/cc4group.h b/src/cc4group.h
new file mode 100644
index 0000000..ef3a55d
--- /dev/null
+++ b/src/cc4group.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#define this this_
+#define new new_
+#define delete delete_
+#endif
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+typedef struct {
+ const char* fileName;
+ int32_t modified;
+ const char* author;
+ size_t size;
+ size_t totalSize;
+ bool executable;
+ bool directory;
+ bool official;
+} CC4Group_EntryInfo;
+
+typedef struct CC4Group_t CC4Group;
+
+typedef void(*CC4Group_CleanupFunc)(void* data);
+typedef struct {
+ CC4Group_CleanupFunc func;
+ void* data;
+} CC4Group_CleanupJob;
+typedef void* (*CC4Group_TmpMemoryStrategy)(CC4Group* const this, const size_t size, CC4Group_CleanupJob* cleanupJob);
+
+typedef struct {
+ CC4Group* (*new)(void);
+ bool (*create)(CC4Group* const this);
+ void (*delete)(CC4Group* const this);
+
+ // only open an existing group on a frehsly created group object
+ bool (*openExisting)(CC4Group* const this, const char* const path);
+
+ bool (*save)(CC4Group* const this, const char* const path);
+ bool (*saveOverwrite)(CC4Group* const this, const char* const path);
+
+ // extraction to disk
+ bool (*extractAll)(CC4Group* const this, const char* const targetPath);
+ bool (*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
+ bool (*getEntryData)(const CC4Group* const this, const char* const entryPath, const void** const data, size_t* const size);
+
+ // the returned error message pointer is valid until the next call to getErrorMessage is issued or the group is destructed
+ const char* (*getErrorMessage)(CC4Group* const this);
+
+ int32_t (*getErrorCode)(const CC4Group* const this);
+
+ const char* (*getErrorMethod)(const CC4Group* const this);
+ const char* (*getErrorCauser)(const CC4Group* const this);
+
+ struct {
+ CC4Group_TmpMemoryStrategy Memory;
+ CC4Group_TmpMemoryStrategy File;
+ CC4Group_TmpMemoryStrategy Auto;
+ } const TmpMemoryStrategies;
+
+ void (*setTmpMemoryStrategy)(const CC4Group_TmpMemoryStrategy strategy);
+
+ // group metadata handling
+ bool (*setMaker)(CC4Group* const this, const char* const maker, const char* const path, bool const recursive);
+ bool (*setCreation)(CC4Group* const this, int32_t const creation, const char* const path, bool const recursive);
+ bool (*setOfficial)(CC4Group* const this, bool const official, const char* const path, bool const recursive);
+ bool (*setExecutable)(CC4Group* const this, bool const executable, const char* const path);
+
+ bool (*getEntryInfo)(CC4Group* const this, const char* const path, CC4Group_EntryInfo* const info);
+ bool (*getEntryInfos)(CC4Group* const this, const char* const path, CC4Group_EntryInfo** const infos, size_t* const size);
+
+ // modifying the group
+ bool (*deleteEntry)(CC4Group* const this, const char* const path, bool const recursive);
+ bool (*renameEntry)(CC4Group* const this, const char* const oldPath, const char* const newPath);
+ bool (*createDirectory)(CC4Group* const this, const char* const path);
+
+ // ownership of the data is taken by the group
+ bool (*createFile)(CC4Group* const this, const char* const path, void* const data, size_t const size);
+ // ownership of the data is taken by the group
+ bool (*setEntryData)(const CC4Group* const this, const char* const entryPath, void* const data, size_t const size);
+} const CC4Group_API;
+
+#ifndef CC4GROUP_DYNAMIC_LOAD
+extern CC4Group_API cc4group;
+#endif
+
+#ifdef __cplusplus
+}
+#undef this
+#undef new
+#undef delete
+#endif