aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJan200101 <sentrycraft123@gmail.com>2022-07-17 22:13:03 +0200
committerJan200101 <sentrycraft123@gmail.com>2022-07-17 22:13:19 +0200
commit0690e13a39ccfc6e37f1ed6eb5e82032c76d3a34 (patch)
tree21bde442ee6a3c7c166b76e1e2282d977e550a56 /src
downloadcgci-0690e13a39ccfc6e37f1ed6eb5e82032c76d3a34.tar.gz
cgci-0690e13a39ccfc6e37f1ed6eb5e82032c76d3a34.zip
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt34
-rw-r--r--src/build.c116
-rw-r--r--src/build.h6
-rw-r--r--src/cgci.c53
-rw-r--r--src/cgci.h7
-rw-r--r--src/config.c158
-rw-r--r--src/config.h52
-rw-r--r--src/context.c67
-rw-r--r--src/context.h32
-rw-r--r--src/env.c12
-rw-r--r--src/env.h6
-rw-r--r--src/fs.c141
-rw-r--r--src/fs.h9
-rw-r--r--src/parser.c312
-rw-r--r--src/parser.h8
-rw-r--r--src/ui.c390
-rw-r--r--src/ui.h26
17 files changed, 1429 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..9f7e945
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,34 @@
+
+
+list(APPEND
+ SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/build.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/build.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/cgci.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/cgci.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/config.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/config.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/context.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/context.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/env.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/env.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/fs.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/fs.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/parser.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/parser.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/ui.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/ui.h
+)
+
+set(CFLAGS
+ -Wall -Wextra -pedantic
+ -Wconversion -Wshadow -Wstrict-aliasing
+ -Winit-self -Wcast-align -Wpointer-arith
+ -Wmissing-declarations -Wmissing-include-dirs
+ -Wno-unused-parameter -Wuninitialized
+)
+
+set(CMAKE_EXECUTABLE_SUFFIX ".cgi")
+add_executable(${CMAKE_PROJECT_NAME} ${SOURCES})
+
+target_compile_options(${CMAKE_PROJECT_NAME} PUBLIC ${CFLAGS})
diff --git a/src/build.c b/src/build.c
new file mode 100644
index 0000000..4c7667d
--- /dev/null
+++ b/src/build.c
@@ -0,0 +1,116 @@
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include "config.h"
+#include "fs.h"
+#include "build.h"
+
+static void write_build(char* project, char* build_id, struct build_t* build)
+{
+ char* build_path = build_dir(project, build_id);
+ if (!build_path)
+ return;
+
+ if (!isDir(build_path))
+ makeDir(build_path);
+
+ size_t build_size = strlen(build_path);
+ build_path = realloc(build_path, (build_size + 11 + 1) * sizeof(char));
+
+ FILE* fd;
+
+ strcat(build_path, "/timestamp");
+ fd = fopen(build_path, "wb");
+ if (fd)
+ {
+ fwrite(&build->timestamp, sizeof(time_t), 1, fd);
+ fclose(fd);
+ }
+ build_path[build_size] = '\0';
+
+ strcat(build_path, "/completion");
+ fd = fopen(build_path, "wb");
+ if (fd)
+ {
+ fwrite(&build->completion, sizeof(build->completion), 1, fd);
+ fclose(fd);
+ }
+ build_path[build_size] = '\0';
+
+ strcat(build_path, "/status");
+ fd = fopen(build_path, "wb");
+ if (fd)
+ {
+ fwrite(&build->status, sizeof(build->status), 1, fd);
+ fclose(fd);
+ }
+ build_path[build_size] = '\0';
+
+ free(build_path);
+
+}
+
+void create_build()
+{
+ if (!current_project)
+ return;
+
+ int build_id = 0;
+
+ if (current_project->build_count > 0)
+ build_id = atoi(current_project->builds[0].name)+1;
+
+ if (build_id <= 0)
+ build_id = 1;
+
+ struct build_t build;
+ build.name = NULL;
+ build.timestamp = time(NULL);
+ build.completion = 0;
+ build.status = STATUS_INPROGRESS;
+
+ size_t name_len = 1;
+ int temp = build_id;
+
+ while (temp > 10)
+ {
+ temp /= 10;
+ name_len++;
+ }
+
+ build.name = malloc((name_len + 1) * sizeof(char));
+ sprintf(build.name, "%d", build_id);
+
+ write_build(current_project->name, build.name, &build);
+
+ if (!fork())
+ {
+ char* build_path = build_dir(current_project->name, build.name);
+ if (build_path)
+ {
+ makeDir(build_path);
+ build_path = realloc(build_path, (strlen(build_path) + 4 + 1) * sizeof(char));
+ strcat(build_path, "/log");
+
+ freopen(build_path, "w", stdout);
+ freopen(build_path, "w", stderr);
+
+ free(build_path);
+ }
+
+ int status = system(current_project->script_path);
+ if (status)
+ build.status = STATUS_FAILURE;
+ else
+ build.status = STATUS_SUCCESS;
+
+ build.completion = time(NULL);
+
+ write_build(current_project->name, build.name, &build);
+ exit(status);
+ }
+} \ No newline at end of file
diff --git a/src/build.h b/src/build.h
new file mode 100644
index 0000000..01205ec
--- /dev/null
+++ b/src/build.h
@@ -0,0 +1,6 @@
+#ifndef BUILD_H
+#define BUILD_H
+
+void create_build();
+
+#endif \ No newline at end of file
diff --git a/src/cgci.c b/src/cgci.c
new file mode 100644
index 0000000..cf2f70e
--- /dev/null
+++ b/src/cgci.c
@@ -0,0 +1,53 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <assert.h>
+#include <errno.h>
+
+#include "config.h"
+#include "env.h"
+#include "fs.h"
+#include "parser.h"
+#include "context.h"
+#include "ui.h"
+#include "cgci.h"
+
+void init()
+{
+ unsetenv("HOME");
+ unsetenv("USER");
+
+ init_context();
+ init_config();
+}
+
+void deinit()
+{
+ deinit_context();
+ deinit_config();
+}
+
+int main(int argc, char **argv, char** envp)
+{
+ if (argc > 1)
+ {
+ argv_to_path(argc, argv);
+ }
+
+ init();
+
+ if (context.project && !strcmp(context.project, "assets"))
+ {
+ print_asset(context.action);
+ }
+ else
+ {
+ print_html();
+ }
+
+ deinit();
+
+ return 0;
+}
diff --git a/src/cgci.h b/src/cgci.h
new file mode 100644
index 0000000..c7958dd
--- /dev/null
+++ b/src/cgci.h
@@ -0,0 +1,7 @@
+#ifndef CGCI_H
+#define CGCI_H
+
+void init();
+void deinit();
+
+#endif \ No newline at end of file
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..02ad9e3
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,158 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+
+#include "parser.h"
+#include "context.h"
+#include "fs.h"
+#include "config.h"
+
+const char* build_string[] = {
+ "Unknown",
+ "In Progress",
+ "Success",
+ "Failure"
+};
+
+const char* build_class[] = {
+ "unknown",
+ "in-progress",
+ "success",
+ "failure"
+};
+
+struct config_t config;
+struct project_t* current_project = NULL;
+struct build_t* current_build = NULL;
+
+
+void init_config()
+{
+ config.cache_dir = NULL;
+ config.token = NULL;
+ config.projects = malloc(1);
+ config.project_count = 0;
+
+ parse_config();
+
+ if (context.project)
+ {
+ for (size_t i = 0; i < config.project_count; ++i)
+ {
+ if (!strcmp(context.project, config.projects[i].name))
+ {
+ current_project = &config.projects[i];
+ break;
+ }
+ }
+ }
+
+ if (context.index)
+ {
+ for (size_t i = 0; i < config.project_count; ++i)
+ {
+ for (size_t j = 0; j < config.projects[i].build_count; ++j)
+ {
+ if (!strcmp(context.index, config.projects[i].builds[j].name))
+ {
+ current_build = &config.projects[i].builds[j];
+ break;
+ }
+ }
+ }
+ }
+}
+
+void deinit_config()
+{
+ if (config.cache_dir)
+ free(config.cache_dir);
+
+ if (config.token)
+ free(config.token);
+
+ if (config.projects)
+ {
+ for (size_t i = 0; i < config.project_count; ++i)
+ {
+ if (config.projects[i].name)
+ free(config.projects[i].name);
+
+ if (config.projects[i].script_path)
+ free(config.projects[i].script_path);
+
+ if (config.projects[i].description)
+ free(config.projects[i].description);
+
+ if (config.projects[i].builds)
+ {
+ for (size_t j = 0; j < config.projects[i].build_count; ++j)
+ {
+ if (config.projects[i].builds[j].name)
+ free(config.projects[i].builds[j].name);
+
+ if (config.projects[i].builds[j].log)
+ free(config.projects[i].builds[j].log);
+ }
+
+ free(config.projects[i].builds);
+ }
+ }
+ free(config.projects);
+ }
+}
+
+char* cache_dir()
+{
+ char* dir = "cache/"NAME;
+ if (isDir(dir))
+ return strdup(dir);
+
+ dir = CACHE_DIR;
+
+ if (!isDir(CACHE_DIR) && makeDir(CACHE_DIR))
+ {
+ return NULL;
+ }
+
+ return strdup(dir);
+}
+
+char* project_dir(char* project)
+{
+ if (!project)
+ return NULL;
+
+ char* cache = cache_dir();
+
+ if (!cache)
+ return cache;
+
+ size_t size = strlen(cache) + 1 + strlen(project) ;
+ cache = realloc(cache, size+1);
+ strncat(cache, "/", size);
+ strncat(cache, project, size);
+ cache[size] = '\0';
+
+ return cache;
+}
+
+char* build_dir(char* project, char* build)
+{
+ if (!project || !build)
+ return NULL;
+
+ char* project_path = project_dir(project);
+
+ if (!project_path)
+ return project_path;
+
+ size_t size = strlen(project_path) + 1 + strlen(build) ;
+ project_path = realloc(project_path, size+1);
+ strncat(project_path, "/", size);
+ strncat(project_path, build, size);
+ project_path[size] = '\0';
+
+ return project_path;
+} \ No newline at end of file
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..49daf77
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,52 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include <stddef.h>
+#include <time.h>
+
+enum build_status {
+ STATUS_UNKNOWN,
+ STATUS_INPROGRESS,
+ STATUS_SUCCESS,
+ STATUS_FAILURE
+};
+extern const char* build_string[];
+extern const char* build_class[];
+
+struct build_t {
+ char* name;
+ time_t timestamp;
+ time_t completion;
+ enum build_status status;
+ char* log;
+};
+
+struct project_t {
+ char* name;
+ char* script_path;
+ char* description;
+
+ struct build_t* builds;
+ size_t build_count;
+};
+
+struct config_t {
+ char* cache_dir;
+ char* token;
+ struct project_t* projects;
+ size_t project_count;
+};
+
+extern struct config_t config;
+extern struct project_t* current_project;
+extern struct build_t* current_build;
+
+void init_config();
+void deinit_config();
+void parse_config();
+
+char* cache_dir();
+char* project_dir(char* project);
+char* build_dir(char* project, char* build);
+
+#endif \ No newline at end of file
diff --git a/src/context.c b/src/context.c
new file mode 100644
index 0000000..4b99bea
--- /dev/null
+++ b/src/context.c
@@ -0,0 +1,67 @@
+#include "env.h"
+#include "parser.h"
+#include "context.h"
+
+struct context_t context;
+
+void init_context()
+{
+ context.document_root = getenv_default("DOCUMENT_ROOT", "/");
+ context.raw_path = getenv_default("PATH_INFO", "/");
+ context.path = malloc(1);
+ context.path_length = 0;
+
+ context.project = NULL;
+ context.action = NULL;
+ context.index = NULL;
+ context.extra = NULL;
+
+ context.token = NULL;
+
+ context.debug = 0;
+
+ parse_path(context.raw_path);
+ parse_query(getenv_default("QUERY_STRING", ""));
+
+ for (size_t i = 0; i < context.path_length; ++i)
+ {
+ switch(i)
+ {
+ case 0:
+ context.project = context.path[i];
+ break;
+
+ case 1:
+ context.action = context.path[i];
+ break;
+
+ case 2:
+ context.index = context.path[i];
+ break;
+
+ case 3:
+ context.extra = context.path[i];
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+void deinit_context()
+{
+ if (context.path)
+ {
+ for (size_t i = 0; i < context.path_length; ++i)
+ {
+ if (context.path[i])
+ free(context.path[i]);
+ }
+
+ free(context.path);
+
+ if (context.token)
+ free(context.token);
+ }
+}
diff --git a/src/context.h b/src/context.h
new file mode 100644
index 0000000..80c7e72
--- /dev/null
+++ b/src/context.h
@@ -0,0 +1,32 @@
+#ifndef CONTEXT_H
+#define CONTEXT_H
+
+#include <stdint.h>
+#include <stdlib.h>
+
+struct context_t {
+ char* document_root;
+
+ char* raw_path;
+
+ char** path; /* NULLABLE */
+ size_t path_length;
+
+ // simplistic values
+ // never allocated to
+ char* project;
+ char* action;
+ char* index;
+ char* extra;
+
+ char* token;
+
+ uint8_t debug;
+};
+
+extern struct context_t context;
+
+void init_context();
+void deinit_context();
+
+#endif \ No newline at end of file
diff --git a/src/env.c b/src/env.c
new file mode 100644
index 0000000..43bf8d7
--- /dev/null
+++ b/src/env.c
@@ -0,0 +1,12 @@
+#include <stdlib.h>
+#include "env.h"
+
+char* getenv_default(const char* name, char* default_val)
+{
+ char* val = getenv(name);
+ if (!val)
+ val = default_val;
+
+ return val;
+
+}
diff --git a/src/env.h b/src/env.h
new file mode 100644
index 0000000..45f99bb
--- /dev/null
+++ b/src/env.h
@@ -0,0 +1,6 @@
+#ifndef ENV_H
+#define ENV_H
+
+char* getenv_default(const char* name, char* default_val);
+
+#endif \ No newline at end of file
diff --git a/src/fs.c b/src/fs.c
new file mode 100644
index 0000000..a28c217
--- /dev/null
+++ b/src/fs.c
@@ -0,0 +1,141 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#ifdef _WIN32
+#include <windows.h>
+#include <shlwapi.h>
+#endif
+
+#include "fs.h"
+
+#ifdef _WIN32
+#define mkdir(path, perm) mkdir(path)
+#endif
+
+static struct stat getStat(const char* path)
+{
+ // fill with 0s by default in the case stat fails
+ struct stat sb = {0};
+
+ // the return value signifies if stat failes (e.g. file not found)
+ // unimportant for us if it fails it won't touch sb
+ stat(path, &sb);
+
+ return sb;
+}
+
+int isFile(const char* path)
+{
+ struct stat sb = getStat(path);
+
+#ifndef _WIN32
+ if (S_ISLNK(sb.st_mode))
+ {
+ char buf[PATH_MAX];
+ readlink(path, buf, sizeof(buf));
+
+ return isFile(buf);
+ }
+#endif
+ return S_ISREG(sb.st_mode);
+}
+
+int isDir(const char* path)
+{
+ struct stat sb = getStat(path);
+
+#ifndef _WIN32
+ if (S_ISLNK(sb.st_mode))
+ {
+ char buf[PATH_MAX];
+ readlink(path, buf, sizeof(buf));
+
+ return isDir(buf);
+ }
+#endif
+ return S_ISDIR(sb.st_mode);
+}
+
+int makeDir(const char* path)
+{
+ char pathcpy[PATH_MAX];
+ char *index;
+
+ strncpy(pathcpy, path, PATH_MAX-1); // make a mutable copy of the path
+
+ for(index = pathcpy+1; *index; ++index)
+ {
+
+ if (*index == '/')
+ {
+ *index = '\0';
+
+ if (mkdir(pathcpy, 0755) != 0)
+ {
+ if (errno != EEXIST)
+ return -1;
+ }
+
+ *index = '/';
+ }
+ }
+
+ return mkdir(path, 0755);
+}
+
+int removeDir(const char* path)
+{
+ DIR *d = opendir(path);
+ size_t path_len = strlen(path);
+ int r = -1;
+
+ if (d) {
+ struct dirent *p;
+
+ r = 0;
+ while (!r && (p = readdir(d))) {
+ char *buf;
+ size_t len;
+
+ // Skip the names "." and ".." as we don't want to recurse on them.
+ if (!strcmp(p->d_name, ".") || !strcmp(p->d_name, ".."))
+ continue;
+
+ len = path_len + strlen(p->d_name) + 2;
+ buf = malloc(len);
+
+ if (buf) {
+ struct stat statbuf = {0};
+
+ snprintf(buf, len, "%s/%s", path, p->d_name);
+ if (!stat(buf, &statbuf)) {
+ if (S_ISDIR(statbuf.st_mode))
+ r = removeDir(buf);
+#ifndef _WIN32
+ else if (S_ISLNK(statbuf.st_mode))
+ r = unlink(buf);
+#endif
+ else
+ r = remove(buf);
+ }
+ else // it is very likely that we found a dangling symlink which is not detected by stat
+ {
+ r = unlink(buf);
+ }
+ free(buf);
+ }
+ }
+ closedir(d);
+ }
+
+ if (!r)
+ r = rmdir(path);
+
+ return r;
+} \ No newline at end of file
diff --git a/src/fs.h b/src/fs.h
new file mode 100644
index 0000000..8f4de4b
--- /dev/null
+++ b/src/fs.h
@@ -0,0 +1,9 @@
+#ifndef FS_H
+#define FS_H
+
+int isFile(const char*);
+int isDir(const char*);
+int makeDir(const char*);
+int removeDir(const char*);
+
+#endif \ No newline at end of file
diff --git a/src/parser.c b/src/parser.c
new file mode 100644
index 0000000..052acb4
--- /dev/null
+++ b/src/parser.c
@@ -0,0 +1,312 @@
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <dirent.h>
+
+#include "config.h"
+#include "context.h"
+#include "fs.h"
+#include "parser.h"
+
+void argv_to_path(int argc, char** argv)
+{
+ char* arg_path = malloc(1);
+ size_t arg_path_length = 1;
+ *arg_path = '\0';
+
+ for (int i = 1; i < argc; ++i)
+ {
+ if (argv[i][0] != '\0')
+ {
+ arg_path_length += strlen(argv[i]) + 1;
+
+ arg_path = realloc(arg_path, sizeof(char) * arg_path_length);
+
+ strncat(arg_path, "/", arg_path_length - strlen(arg_path));
+ strncat(arg_path, argv[i], arg_path_length - strlen(arg_path));
+ }
+ }
+
+ if (arg_path_length)
+ {
+ setenv("PATH_INFO", arg_path, 1);
+ }
+ free(arg_path);
+}
+
+void parse_config()
+{
+ FILE* fd = fopen("/etc/cgcirc", "r");
+ if (!fd)
+ {
+ fd = fopen("cgcirc", "r");
+ if (!fd)
+ return;
+ }
+
+ fseek(fd, 0L, SEEK_END);
+ size_t file_size = (size_t)ftell(fd);
+ rewind(fd);
+
+ char* buf = malloc(file_size+1);
+ assert(buf);
+
+ fread(buf, sizeof(char*), file_size, fd);
+ buf[file_size] = '\0';
+
+ fclose(fd);
+
+ char* head = buf;
+ char* tail = head;
+
+ char* key = NULL;
+ char* value = NULL;
+
+ while(*tail)
+ {
+ assert((size_t)(tail - buf) <= file_size);
+
+ if (*head == '#')
+ {
+ while (*tail != '\0' && *tail != '\n')
+ {
+ assert((size_t)(tail - buf) <= file_size);
+ ++tail;
+ }
+ head = tail+1;;
+ continue;
+ }
+ else if (*tail == '=')
+ {
+ key = head;
+ *tail = '\0';
+ head = tail+1;;
+ }
+ else if (*tail == '\0' || *tail == '\n')
+ {
+ if (key)
+ {
+ value = head;
+ }
+ *tail = '\0';
+ head = tail+1;;
+ }
+
+ ++tail;
+
+ if (key && value)
+ {
+ if (!strcmp(key, "cache-dir"))
+ {
+ assert(!config.cache_dir);
+ config.cache_dir = strdup(value);
+ }
+ if (!strcmp(key, "token"))
+ {
+ assert(!config.token);
+ config.token = strdup(value);
+ }
+ else if (!strcmp(key, "project.name"))
+ {
+ ++config.project_count;
+
+ config.projects = realloc(config.projects, config.project_count * sizeof(struct project_t));
+ assert(config.projects);
+
+ config.projects[config.project_count-1].name = strdup(value);
+ config.projects[config.project_count-1].script_path = NULL;
+ config.projects[config.project_count-1].description = NULL;
+ }
+ else if (!strcmp(key, "project.script"))
+ {
+ config.projects[config.project_count-1].script_path = strdup(value);
+ }
+ else if (!strcmp(key, "project.description"))
+ {
+ config.projects[config.project_count-1].description = strdup(value);
+ }
+
+ key = NULL;
+ value = NULL;
+ }
+ }
+
+ char* project = NULL;
+ char* build = NULL;
+ for (size_t i = 0; i < config.project_count; ++i)
+ {
+ config.projects[i].build_count = 0;
+ config.projects[i].builds = NULL;
+
+ project = project_dir(config.projects[i].name);
+
+ if (!isDir(project))
+ {
+ free(project);
+ continue;
+ }
+
+ DIR *dir;
+ struct dirent *ent;
+
+ if ((dir = opendir(project)) != NULL)
+ {
+ while ((ent = readdir(dir)) != NULL)
+ {
+ if (ent->d_name[0] == '.') continue;
+
+ build = build_dir(config.projects[i].name, ent->d_name);
+ size_t build_size = strlen(build);
+ build = realloc(build, (build_size + 11 + 1) * sizeof(char));
+
+ config.projects[i].build_count++;
+ config.projects[i].builds = realloc(config.projects[i].builds, config.projects[i].build_count * sizeof(struct build_t));
+
+ config.projects[i].builds[config.projects[i].build_count-1].name = strdup(ent->d_name);
+ config.projects[i].builds[config.projects[i].build_count-1].timestamp = 0;
+ config.projects[i].builds[config.projects[i].build_count-1].completion = 0;
+ config.projects[i].builds[config.projects[i].build_count-1].status = STATUS_UNKNOWN;
+ config.projects[i].builds[config.projects[i].build_count-1].log = NULL;
+
+ strcat(build, "/timestamp");
+ fd = fopen(build, "rb");
+ if (fd)
+ {
+ fread(&config.projects[i].builds[config.projects[i].build_count-1].timestamp, sizeof(time_t), 1, fd);
+ fclose(fd);
+ }
+ build[build_size] = '\0';
+
+ strcat(build, "/completion");
+ fd = fopen(build, "rb");
+ if (fd)
+ {
+ fread(&config.projects[i].builds[config.projects[i].build_count-1].completion, sizeof(time_t), 1, fd);
+ fclose(fd);
+ }
+ build[build_size] = '\0';
+
+ strcat(build, "/status");
+ fd = fopen(build, "rb");
+ if (fd)
+ {
+ fread(&config.projects[i].builds[config.projects[i].build_count-1].status, sizeof(enum build_status), 1, fd);
+ fclose(fd);
+ }
+ build[build_size] = '\0';
+
+ strcat(build, "/log");
+ fd = fopen(build, "rb");
+ if (fd)
+ {
+ fseek(fd, 0, SEEK_END);
+ size_t size = (size_t)ftell(fd);
+ rewind(fd);
+
+ config.projects[i].builds[config.projects[i].build_count-1].log = malloc(size + 1);
+ fread(config.projects[i].builds[config.projects[i].build_count-1].log, sizeof(char), size, fd);
+ fclose(fd);
+ config.projects[i].builds[config.projects[i].build_count-1].log[size] = '\0';
+ }
+ build[build_size] = '\0';
+
+ free(build);
+ }
+ closedir(dir);
+ }
+
+ // sort the builds
+ struct build_t t;
+ for (size_t j = 0; j < config.projects[i].build_count; ++j)
+ {
+ for (size_t k = 0; k < config.projects[i].build_count; ++k)
+ {
+ struct build_t* a = &config.projects[i].builds[j];
+ struct build_t* b = &config.projects[i].builds[k];
+
+ if (atoi(a->name) > atoi(b->name))
+ {
+ t = *a;
+ *a = *b;
+ *b = t;
+ }
+ }
+ }
+
+ free(project);
+ }
+
+ free(buf);
+}
+
+void parse_path(const char* path)
+{
+ size_t path_len = strlen(path) + 1;
+ const char* const orig_path = path;
+
+ if (path[0] != '/')
+ return;
+
+ const char* head = ++path;
+
+ while(*path)
+ {
+ assert(orig_path+path_len >= path);
+ ++path;
+ if (*path == '/' || *path == '\0')
+ {
+ assert(path > head);
+ size_t len = (size_t)(path - head) + 1;
+
+ if (!len)
+ continue;
+
+ ++context.path_length;
+
+ context.path = realloc(context.path, sizeof(char*) * context.path_length);
+ assert(context.path);
+
+ context.path[context.path_length-1] = malloc(len * sizeof(char));
+ strncpy(context.path[context.path_length-1], head, len-1);
+ context.path[context.path_length-1][len-1] = '\0';
+
+ head = path+1;
+ }
+ }
+}
+
+void parse_query(const char* query_arg)
+{
+ char* query = strdup(query_arg);
+ char* head = query;
+ char* end = head + strlen(query);
+
+ char* key = head;
+ char* value = NULL;
+
+ while (head <= end)
+ {
+ ++head;
+ if (!value && *head == '=')
+ {
+ *head = '\0';
+ value = head+1;
+ }
+
+ if (*head == '&' || *head == '\0')
+ {
+ *head = '\0';
+
+ if (!strcmp(key, "debug"))
+ {
+ context.debug = 1;
+ }
+ else if (!strcmp(key, "token"))
+ {
+ context.token = strdup(value);
+ }
+ }
+ }
+
+ free(query);
+}
diff --git a/src/parser.h b/src/parser.h
new file mode 100644
index 0000000..e5d5465
--- /dev/null
+++ b/src/parser.h
@@ -0,0 +1,8 @@
+#ifndef PARSER_H
+#define PARSER_H
+
+void argv_to_path(int, char**);
+void parse_path(const char*);
+void parse_query(const char*);
+
+#endif \ No newline at end of file
diff --git a/src/ui.c b/src/ui.c
new file mode 100644
index 0000000..25c810e
--- /dev/null
+++ b/src/ui.c
@@ -0,0 +1,390 @@
+#include <stdio.h>
+#include <string.h>
+#include <dirent.h>
+#include <stdlib.h>
+
+#include "config.h"
+#include "context.h"
+#include "build.h"
+#include "ui.h"
+
+#define CONTENT_TYPE_FORMAT "Content-Type: %s;\n\n"
+#define TEXT_HTML "text/html"
+#define TEXT_CSS "text/css"
+#define TEXT_PLAIN "text/plain"
+
+void print_html()
+{
+ if (current_project && current_project->name)
+ {
+ if (context.action)
+ {
+ if (!strcmp(context.action, "builds") && context.index)
+ {
+ if (context.extra)
+ {
+ printf(CONTENT_TYPE_FORMAT, TEXT_PLAIN);
+ printf("%s", current_build->log);
+ }
+ else
+ {
+ printf(CONTENT_TYPE_FORMAT, TEXT_HTML);
+ printf(HTML_START);
+ print_head();
+ print_title();
+ print_build_nav();
+ print_build_info();
+ printf(HTML_END);
+ }
+ }
+ else if (!strcmp(context.action, "trigger"))
+ {
+ if (context.token && config.token && !strcmp(context.token, config.token))
+ {
+ create_build();
+
+ printf(CONTENT_TYPE_FORMAT, TEXT_HTML);
+ printf(HTML_START);
+ print_head();
+ printf(HTML_END);
+ }
+ else
+ {
+ printf(CONTENT_TYPE_FORMAT, TEXT_HTML);
+ printf(HTML_START);
+ print_head();
+ print_title();
+ print_build_nav();
+ print_build_trigger();
+ printf(HTML_END);
+ }
+ }
+ }
+ else
+ {
+ printf(CONTENT_TYPE_FORMAT, TEXT_HTML);
+ printf(HTML_START);
+ print_head();
+ print_title();
+ print_build_nav();
+ print_build_list();
+ printf(HTML_END);
+ }
+ }
+ else
+ {
+ printf(CONTENT_TYPE_FORMAT, TEXT_HTML);
+ printf(HTML_START);
+ print_head();
+ print_title();
+ print_project_nav();
+ print_project_list();
+ printf(HTML_END);
+ }
+}
+
+void print_head()
+{
+ printf("<head>");
+
+ printf("<meta charset=\"utf-8\">");
+
+ printf("<title>CGCI");
+ if (current_project && current_project->name)
+ {
+ printf(" : %s", current_project->name);
+ }
+ printf("</title>");
+
+ if (current_project && current_project->name
+ && context.action && !strcmp(context.action, "trigger")
+ && context.token && config.token && !strcmp(context.token, config.token))
+ printf("<meta http-equiv=\"refresh\" content=\"0; url=/%s\"/>", current_project->name);
+
+ printf("<link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/base.css\"/>");
+ printf("</head>");
+}
+
+void print_title()
+{
+ printf("<h1 class=\"title\">");
+
+ printf("<a %s>CGCI</a>", context.project != NULL ? "href=\"/\"" : "");
+
+ if (context.project)
+ {
+ printf(" : <a href=\"/%s\">%s</a>", context.project, context.project);
+ if (context.action)
+ {
+ printf(" : %s", context.action);
+ }
+ }
+ printf("</h1>");
+}
+
+void print_build_nav()
+{
+ printf(
+ "<table class=\"tabs\">"
+ "<tbody>"
+ "<tr>"
+ "<td>"
+ "<a href=\"/%s\" class=\"active\">Builds</a>"
+ "</td>",
+ current_project->name
+ );
+
+ if (config.token && current_project->script_path && strlen(current_project->script_path))
+ {
+ printf(
+ "<td class=\"align-right\">"
+ "<a href=\"/%s/trigger\">Trigger Build</a>"
+ "</td>",
+ current_project->name
+ );
+ }
+
+ printf(
+ "</tr>"
+ "</tbody>"
+ "</table>"
+ );
+}
+
+void print_build_info()
+{
+ // YYYY-MM-DD HH:MM
+ char time[18];
+ strftime(time, sizeof(time), "%Y-%m-%d %H:%M", localtime(&current_build->timestamp));
+ char buildtime[18];
+ strftime(buildtime, sizeof(buildtime), "%Y-%m-%d %H:%M", localtime(&current_build->completion));
+
+ char* log_lines = current_build->log + strlen(current_build->log);
+ int line_count = 20;
+
+ while (line_count && log_lines > current_build->log)
+ {
+ --log_lines;
+
+ if (*log_lines == '\n')
+ --line_count;
+ }
+
+ printf(
+ "<table class=\"build-info\">"
+ "<tbody>"
+ "<tr>"
+ "<td>Build ID</td>"
+ "<td>%s</td>"
+ "</tr>"
+ "<tr>"
+ "<td>Build Date</td>"
+ "<td>%s</td>"
+ "</tr>"
+ "<tr>"
+ "<td>Completion Time</td>"
+ "<td>%s</td>"
+ "</tr>"
+ "<tr>"
+ "<td>Status</td>"
+ "<td class=\"%s\">%s</td>"
+ "</tr>"
+ "<tr>"
+ "<td>Log</td>"
+ "<td>"
+ "<a href=\"/%s/builds/%s/log\">Raw</a>"
+ "</td>"
+ "</tr>"
+ "<!--<tr>"
+ "<td>Artifact</td>"
+ "<td>"
+ "<a href=\"/polecat/builds/1/artifact\">Download</a>"
+ "</td>"
+ "</tr>-->"
+ "</tbody>"
+ "</table>"
+ "<pre>%s</pre>",
+ current_build->name, time, current_build->completion ? buildtime : "",
+ build_class[current_build->status], build_string[current_build->status],
+ current_project->name, current_build->name,
+ log_lines
+ );
+}
+
+void print_build_trigger()
+{
+ printf(
+ "<form>"
+ "<label>Trigger Build</label><br>"
+ "%s"
+ "<input name=\"token\" type=\"password\" placeholder=\"token\" value=\"%s\" required><br>"
+ "<input type=\"submit\" value=\"submit\">"
+ "</form>",
+ context.token ? "Invalid token" : "",
+ context.token ? context.token : ""
+ );
+}
+
+void print_build_list()
+{
+ printf(
+ "<table class=\"content\">"
+ "<thead>"
+ "<tr>"
+ "<th>Build ID</th>"
+ "<th>Build Date</th>"
+ "<th>Completion Time</th>"
+ "<th>Status</th>"
+ "</tr>"
+ "</thead>"
+ "<tbody>"
+ );
+
+ for (size_t i = 0; i < current_project->build_count; ++i)
+ {
+ struct build_t* build = &current_project->builds[i];
+
+ // YYYY-MM-DD HH:MM
+ char time[18];
+ strftime(time, sizeof(time), "%Y-%m-%d %H:%M", localtime(&build->timestamp));
+ char buildtime[255] = "";
+ strdifftime(build->completion, build->timestamp, buildtime, sizeof(buildtime));
+
+ printf(
+ "<tr>"
+ "<td>"
+ "<a href=\"/%s/builds/%s\">%s</a>"
+ "</td>"
+ "<td>%s</td>"
+ "<td>%s</td>"
+ "<td class=\"%s\">%s</td>"
+ "</tr>",
+ context.project, build->name, build->name,
+ time, buildtime,
+ build_class[build->status], build_string[build->status]
+ );
+ }
+
+ printf(
+ "</tbody>"
+ "</table>"
+ );
+}
+
+void print_project_nav()
+{
+ printf(
+ "<table class=\"tabs\">"
+ "<tbody>"
+ "<tr>"
+ "<td>"
+ "<a href=\"/\" class=\"active\">Projects</a>"
+ "</td>"
+ "</tr>"
+ "</tbody>"
+ "</table>"
+ );
+}
+
+void print_project_list()
+{
+ printf(
+ "<table class=\"content\">"
+ "<thead>"
+ "<tr>"
+ "<th>Name</th>"
+ "<th>Description</th>"
+ "<th>Last Build Date</th>"
+ "<th>Last Completion Time</th>"
+ "<th>Last Build Status</th>"
+ "</tr>"
+ "</thead>"
+ "<tbody>"
+ );
+
+ for (size_t i = 0; i < config.project_count; ++i)
+ {
+ struct project_t* project = &config.projects[i];
+
+ const char* class = "unknown";
+ const char* status = "Unknown";
+ char time[18] = "never";
+ char buildtime[255] = "";
+ if (project->build_count > 0)
+ {
+ class = build_class[project->builds[0].status];
+ status = build_string[project->builds[0].status];
+ strftime(time, sizeof(time), "%Y-%m-%d %H:%M", localtime(&project->builds[0].timestamp));
+ strdifftime(project->builds[0].completion, project->builds[0].timestamp, buildtime, sizeof(buildtime));
+ }
+
+ printf(
+ "<tr>"
+ "<td>"
+ "<a href=\"/%s\">%s</a>"
+ "</td>"
+ "<td>%s</td>"
+ "<td>%s</td>"
+ "<td>%s</td>"
+ "<td class=\"%s\">%s</td>"
+ "</tr>",
+ project->name, project->name,
+ project->description,
+ time, buildtime,
+ class, status
+ );
+ }
+ printf(
+ "</tbody>"
+ "</table>"
+ );
+}
+
+void print_asset(const char* file)
+{
+ if (!file)
+ return;
+
+ printf(CONTENT_TYPE_FORMAT, TEXT_CSS);
+
+ FILE* fd = fopen(file, "r");
+
+ if (!fd)
+ return;
+
+ char buf[255+1];
+
+ while (!feof(fd))
+ {
+ fread(buf, sizeof(buf)-1, sizeof(buf[0]), fd);
+ buf[sizeof(buf)-1] = '\0';
+
+ printf("%s", buf);
+ }
+}
+
+void strdifftime(time_t time1, time_t time0, char* str, size_t size)
+{
+ if (!size)
+ return;
+
+ double diff = difftime(time1, time0);
+
+ if (diff > 0)
+ {
+ *str = '\0';
+
+ int seconds = (int)diff % 60;
+ int minutes = (int)(diff / 60);
+ int hours = minutes / 60;
+
+ if (hours)
+ snprintf(str+strlen(str), size, "%i hours ", hours);
+
+ if (minutes)
+ snprintf(str+strlen(str), size, "%i minutes ", minutes);
+
+ if (seconds)
+ snprintf(str+strlen(str), size, "%i seconds ", seconds);
+ }
+} \ No newline at end of file
diff --git a/src/ui.h b/src/ui.h
new file mode 100644
index 0000000..06b559b
--- /dev/null
+++ b/src/ui.h
@@ -0,0 +1,26 @@
+#ifndef UI_H
+#define UI_H
+
+#include <time.h>
+#include <stdint.h>
+
+#define HTML_START "<!DOCTYPE html><html>"
+#define HTML_END "</html>"
+
+void print_html();
+void print_head();
+
+void print_body();
+void print_title();
+void print_build_nav();
+void print_build_info();
+void print_build_trigger();
+void print_build_list();
+void print_project_nav();
+void print_project_list();
+
+void print_asset(const char*);
+
+void strdifftime(time_t, time_t, char*, size_t);
+
+#endif