aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/beaker_globals.c13
-rw-r--r--src/beaker_globals.h18
-rw-r--r--src/http.c216
-rw-r--r--src/routing.c226
-rw-r--r--src/server.c181
-rw-r--r--src/template.c961
6 files changed, 1615 insertions, 0 deletions
diff --git a/src/beaker_globals.c b/src/beaker_globals.c
new file mode 100644
index 0000000..2ea5ab6
--- /dev/null
+++ b/src/beaker_globals.c
@@ -0,0 +1,13 @@
+#include "beaker_globals.h"
+
+RouteHandler handlers[MAX_HANDLERS];
+
+int handler_count = 0;
+
+int current_client_socket = -1;
+
+Cookie cookies_to_set[MAX_COOKIES];
+
+int cookies_to_set_count = 0;
+
+char current_request_buffer[BUFFER_SIZE]; \ No newline at end of file
diff --git a/src/beaker_globals.h b/src/beaker_globals.h
new file mode 100644
index 0000000..94aabdf
--- /dev/null
+++ b/src/beaker_globals.h
@@ -0,0 +1,18 @@
+#ifndef BEAKER_GLOBALS_H
+#define BEAKER_GLOBALS_H
+
+#include "../beaker.h"
+
+extern RouteHandler handlers[MAX_HANDLERS];
+
+extern int handler_count;
+
+extern int current_client_socket;
+
+extern Cookie cookies_to_set[MAX_COOKIES];
+
+extern int cookies_to_set_count;
+
+extern char current_request_buffer[BUFFER_SIZE];
+
+#endif \ No newline at end of file
diff --git a/src/http.c b/src/http.c
new file mode 100644
index 0000000..349f66f
--- /dev/null
+++ b/src/http.c
@@ -0,0 +1,216 @@
+#include "../beaker.h"
+#include "beaker_globals.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+static void build_cookie_headers(char *cookie_headers_buffer,
+ size_t buffer_size) {
+
+ cookie_headers_buffer[0] = '\0';
+
+ for (int i = 0; i < cookies_to_set_count; i++) {
+ char single_cookie_header[MAX_VALUE_LEN * 2];
+
+ snprintf(single_cookie_header, sizeof(single_cookie_header),
+ "Set-Cookie: %s=%s", cookies_to_set[i].name,
+ cookies_to_set[i].value);
+
+ if (strlen(cookies_to_set[i].expires) > 0) {
+ strcat(single_cookie_header, "; Expires=");
+ strcat(single_cookie_header, cookies_to_set[i].expires);
+ }
+
+ if (strlen(cookies_to_set[i].path) > 0) {
+ strcat(single_cookie_header, "; Path=");
+ strcat(single_cookie_header, cookies_to_set[i].path);
+ }
+
+ if (cookies_to_set[i].http_only) {
+ strcat(single_cookie_header, "; HttpOnly");
+ }
+
+ if (cookies_to_set[i].secure) {
+ strcat(single_cookie_header, "; Secure");
+ }
+
+ strcat(single_cookie_header, "\r\n");
+
+ strncat(cookie_headers_buffer, single_cookie_header,
+ buffer_size - strlen(cookie_headers_buffer) - 1);
+ }
+}
+
+void send_response(const char *html) {
+
+ if (current_client_socket == -1) {
+ fprintf(stderr, "[ERROR] send_response: No client socket set. Cannot send response.\n");
+ return;
+ }
+
+ char http_response_header[BUFFER_SIZE * 2];
+ int content_length = strlen(html);
+ char cookie_headers[BUFFER_SIZE];
+
+ build_cookie_headers(cookie_headers, sizeof(cookie_headers));
+
+ snprintf(http_response_header, sizeof(http_response_header),
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html; charset=UTF-8\r\n"
+ "Content-Length: %d\r\n"
+ "%s"
+ "Connection: close\r\n"
+ "\r\n",
+ content_length, cookie_headers);
+
+ if (send(current_client_socket, http_response_header,
+ strlen(http_response_header), 0) < 0) {
+ perror("Error sending HTTP header");
+ fprintf(stderr, "[ERROR] send_response: Failed to send HTTP header.\n");
+ return;
+ }
+
+ if (send(current_client_socket, html, content_length, 0) < 0) {
+ perror("Error sending HTML body");
+ fprintf(stderr, "[ERROR] send_response: Failed to send HTML body.\n");
+ return;
+ }
+
+ cookies_to_set_count = 0;
+}
+
+void send_redirect(const char *location) {
+
+ if (current_client_socket == -1) {
+ fprintf(stderr, "[ERROR] send_redirect: No client socket set. Cannot send redirect.\n");
+ return;
+ }
+
+ char http_response_header[BUFFER_SIZE];
+
+ snprintf(http_response_header, sizeof(http_response_header),
+ "HTTP/1.1 302 Found\r\n"
+ "Location: %s\r\n"
+ "Connection: close\r\n"
+ "\r\n",
+ location);
+
+ if (send(current_client_socket, http_response_header,
+ strlen(http_response_header), 0) < 0) {
+ perror("Error sending redirect header");
+ fprintf(stderr, "[ERROR] send_redirect: Failed to send redirect header.\n");
+ return;
+ }
+
+ cookies_to_set_count = 0;
+}
+
+void set_cookie(const char *name, const char *value, const char *expires,
+ const char *path, bool http_only, bool secure) {
+
+ if (cookies_to_set_count >= MAX_COOKIES) {
+ fprintf(stderr,
+ "[WARNING] set_cookie: Maximum number of cookies to set reached. Cannot set cookie '%s'.\n",
+ name);
+ return;
+ }
+
+ Cookie *new_cookie = &cookies_to_set[cookies_to_set_count];
+
+ strncpy(new_cookie->name, name, MAX_KEY_LEN - 1);
+ new_cookie->name[MAX_KEY_LEN - 1] = '\0';
+
+ strncpy(new_cookie->value, value, MAX_VALUE_LEN - 1);
+ new_cookie->value[MAX_VALUE_LEN - 1] = '\0';
+
+ if (expires && strlen(expires) > 0) {
+ strncpy(new_cookie->expires, expires, MAX_VALUE_LEN - 1);
+ new_cookie->expires[MAX_VALUE_LEN - 1] = '\0';
+ } else {
+ new_cookie->expires[0] = '\0';
+ }
+
+ if (path && strlen(path) > 0) {
+ strncpy(new_cookie->path, path, MAX_KEY_LEN - 1);
+ new_cookie->path[MAX_KEY_LEN - 1] = '\0';
+ } else {
+ new_cookie->path[0] = '\0';
+ }
+
+ new_cookie->http_only = http_only;
+ new_cookie->secure = secure;
+
+ cookies_to_set_count++;
+}
+
+char *get_cookie(const char *cookie_name) {
+
+ char *cookie_header_start = strstr(current_request_buffer, "\r\nCookie: ");
+ if (cookie_header_start == NULL) {
+ return NULL;
+ }
+
+ cookie_header_start += strlen("\r\nCookie: ");
+
+ char *cookie_header_end = strstr(cookie_header_start, "\r\n");
+ if (cookie_header_end == NULL) {
+
+ cookie_header_end = (char *)(current_request_buffer + strlen(current_request_buffer));
+ }
+
+ size_t cookie_str_len = cookie_header_end - cookie_header_start;
+ char *cookie_str = (char *)malloc(cookie_str_len + 1);
+ if (cookie_str == NULL) {
+ perror("Failed to allocate memory for raw cookie string");
+ fprintf(stderr, "[ERROR] get_cookie: Allocation failed for cookie_str.\n");
+ return NULL;
+ }
+
+ strncpy(cookie_str, cookie_header_start, cookie_str_len);
+ cookie_str[cookie_str_len] = '\0';
+
+ char *cookie_str_copy = strdup(cookie_str);
+ if (cookie_str_copy == NULL) {
+ perror("Failed to duplicate cookie string for strtok_r");
+ fprintf(stderr, "[ERROR] get_cookie: Duplication failed for cookie_str_copy.\n");
+ free(cookie_str);
+ return NULL;
+ }
+
+ char *token;
+ char *saveptr_cookie;
+
+ token = strtok_r(cookie_str_copy, ";", &saveptr_cookie);
+ while (token != NULL) {
+
+ while (*token == ' ') {
+ token++;
+ }
+
+ char *equals_sign = strchr(token, '=');
+ if (equals_sign != NULL) {
+ size_t name_len = equals_sign - token;
+
+ if (name_len == strlen(cookie_name) &&
+ strncmp(token, cookie_name, name_len) == 0) {
+
+ char *cookie_value = strdup(equals_sign + 1);
+ if (cookie_value == NULL) {
+ perror("Failed to duplicate cookie value");
+ fprintf(stderr, "[ERROR] get_cookie: Allocation failed for cookie_value.\n");
+ }
+ free(cookie_str);
+ free(cookie_str_copy);
+ return cookie_value;
+ }
+ }
+
+ token = strtok_r(NULL, ";", &saveptr_cookie);
+ }
+
+ free(cookie_str);
+ free(cookie_str_copy);
+ return NULL;
+} \ No newline at end of file
diff --git a/src/routing.c b/src/routing.c
new file mode 100644
index 0000000..156e843
--- /dev/null
+++ b/src/routing.c
@@ -0,0 +1,226 @@
+#include "../beaker.h"
+#include "beaker_globals.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+
+void set_handler(const char *path, RequestHandler handler) {
+
+ if (handler_count < MAX_HANDLERS) {
+
+ strncpy(handlers[handler_count].path, path, MAX_PATH_LEN - 1);
+ handlers[handler_count].path[MAX_PATH_LEN - 1] = '\0';
+
+ handlers[handler_count].handler = handler;
+
+ handler_count++;
+ } else {
+
+ fprintf(stderr,
+ "[WARNING] set_handler: Maximum number of handlers reached. Cannot register handler for '%s'.\n",
+ path);
+ }
+}
+
+char *parse_request_url(const char *request_line, UrlParams *params) {
+ char method[16];
+ char path_with_query_buffer[MAX_PATH_LEN];
+ char http_version[16];
+
+ if (sscanf(request_line, "%15s %255s %15s", method, path_with_query_buffer,
+ http_version) != 3) {
+ fprintf(stderr, "[ERROR] parse_request_url: Malformed request line: '%s'\n",
+ request_line);
+ return NULL;
+ }
+
+ params->count = 0;
+
+ char *working_url_copy = strdup(path_with_query_buffer);
+ if (working_url_copy == NULL) {
+ perror("Failed to allocate memory for working URL copy");
+ return NULL;
+ }
+
+ char *query_start = strchr(working_url_copy, '?');
+ char *path_only_for_return;
+
+ if (query_start != NULL) {
+ *query_start = '\0';
+ path_only_for_return = working_url_copy;
+ char *query_string = query_start + 1;
+
+ char *token;
+ char *saveptr_query;
+
+ token = strtok_r(query_string, "&", &saveptr_query);
+ while (token != NULL && params->count < MAX_URL_PARAMS) {
+ char *equals_sign = strchr(token, '=');
+ if (equals_sign != NULL) {
+ size_t key_len = equals_sign - token;
+
+ strncpy(params->params[params->count].key, token, key_len);
+ params->params[params->count].key[key_len] = '\0';
+ strncpy(params->params[params->count].value, equals_sign + 1,
+ MAX_VALUE_LEN - 1);
+ params->params[params->count].value[MAX_VALUE_LEN - 1] = '\0';
+ params->count++;
+ }
+ token = strtok_r(NULL, "&", &saveptr_query);
+ }
+ } else {
+
+ path_only_for_return = working_url_copy;
+ }
+
+ char *final_requested_path = strdup(path_only_for_return);
+ if (final_requested_path == NULL) {
+ perror("Failed to allocate final requested path");
+ free(working_url_copy);
+ return NULL;
+ }
+
+ char *segment;
+ int segment_index = 0;
+ char *saveptr_path;
+
+ segment = strtok_r(working_url_copy, "/", &saveptr_path);
+ while (segment != NULL) {
+
+ if (segment_index > 0) {
+ if (params->count < MAX_URL_PARAMS) {
+ char param_key[MAX_KEY_LEN];
+
+ snprintf(param_key, sizeof(param_key), "param%d", segment_index - 1);
+
+ strncpy(params->params[params->count].key, param_key, MAX_KEY_LEN - 1);
+ params->params[params->count].key[MAX_KEY_LEN - 1] = '\0';
+ strncpy(params->params[params->count].value, segment,
+ MAX_VALUE_LEN - 1);
+ params->params[params->count].value[MAX_VALUE_LEN - 1] = '\0';
+ params->count++;
+ } else {
+ fprintf(stderr,
+ "[WARNING] parse_request_url: Max URL parameters reached. Skipping path segment '%s'.\n",
+ segment);
+ }
+ }
+ segment_index++;
+ segment = strtok_r(NULL, "/", &saveptr_path);
+ }
+
+ free(working_url_copy);
+
+ return final_requested_path;
+}
+
+const char *get_mime_type(const char *file_path) {
+
+ const char *ext = strrchr(file_path, '.');
+ if (!ext) {
+ return "application/octet-stream";
+ }
+ ext++;
+
+ if (strcmp(ext, "html") == 0 || strcmp(ext, "htm") == 0)
+ return "text/html";
+ if (strcmp(ext, "css") == 0)
+ return "text/css";
+ if (strcmp(ext, "js") == 0)
+ return "application/javascript";
+ if (strcmp(ext, "json") == 0)
+ return "application/json";
+ if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0)
+ return "image/jpeg";
+ if (strcmp(ext, "png") == 0)
+ return "image/png";
+ if (strcmp(ext, "gif") == 0)
+ return "image/gif";
+ if (strcmp(ext, "ico") == 0)
+ return "image/x-icon";
+ if (strcmp(ext, "svg") == 0)
+ return "image/svg+xml";
+ if (strcmp(ext, "pdf") == 0)
+ return "application/pdf";
+ if (strcmp(ext, "txt") == 0)
+ return "text/plain";
+
+ return "application/octet-stream";
+}
+
+bool serve_static_file(const char *request_path_relative_to_static) {
+ char full_static_path[MAX_PATH_LEN];
+
+ if (strstr(request_path_relative_to_static, "..") != NULL) {
+ fprintf(stderr, "[SECURITY] Attempted directory traversal: %s\n",
+ request_path_relative_to_static);
+
+ const char *forbidden_response =
+ "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n";
+ send(current_client_socket, forbidden_response, strlen(forbidden_response), 0);
+ return true;
+ }
+
+ snprintf(full_static_path, sizeof(full_static_path), "%s%s", STATIC_DIR,
+ request_path_relative_to_static);
+
+ FILE *fp = fopen(full_static_path, "rb");
+ if (fp == NULL) {
+ fprintf(stderr,
+ "[ERROR] serve_static_file: File '%s' not found or could not be opened. %s\n",
+ full_static_path, strerror(errno));
+ return false;
+ }
+
+ struct stat st;
+
+ if (fstat(fileno(fp), &st) < 0) {
+ perror("fstat error");
+ fprintf(stderr, "[ERROR] serve_static_file: fstat failed for '%s'.\n",
+ full_static_path);
+ fclose(fp);
+
+ const char *server_error_response =
+ "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n";
+ send(current_client_socket, server_error_response, strlen(server_error_response), 0);
+ return true;
+ }
+
+ long file_size = st.st_size;
+ const char *mime_type = get_mime_type(full_static_path);
+
+ char http_header[BUFFER_SIZE];
+
+ snprintf(http_header, sizeof(http_header),
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: %s\r\n"
+ "Content-Length: %ld\r\n"
+ "Connection: close\r\n"
+ "\r\n",
+ mime_type, file_size);
+
+ if (send(current_client_socket, http_header, strlen(http_header), 0) < 0) {
+ perror("Error sending static file header");
+ fprintf(stderr, "[ERROR] serve_static_file: Failed to send header for '%s'.\n", full_static_path);
+ fclose(fp);
+ return true;
+ }
+
+ char file_buffer[BUFFER_SIZE];
+ size_t bytes_read;
+
+ while ((bytes_read = fread(file_buffer, 1, sizeof(file_buffer), fp)) > 0) {
+ if (send(current_client_socket, file_buffer, bytes_read, 0) < 0) {
+ perror("Error sending static file content");
+ fprintf(stderr, "[ERROR] serve_static_file: Failed to send content for '%s'.\n", full_static_path);
+ break;
+ }
+ }
+
+ fclose(fp);
+ return true;
+} \ No newline at end of file
diff --git a/src/server.c b/src/server.c
new file mode 100644
index 0000000..e6a460a
--- /dev/null
+++ b/src/server.c
@@ -0,0 +1,181 @@
+#include "../beaker.h"
+#include "beaker_globals.h"
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+static int initialize_server_socket(const char *ip, int port, int *server_fd_out,
+ struct sockaddr_in *address_out) {
+
+ if ((*server_fd_out = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
+ perror("socket failed");
+ fprintf(stderr, "[ERROR] initialize_server_socket: Failed to create socket.\n");
+ return -1;
+ }
+
+ int opt = 1;
+ if (setsockopt(*server_fd_out, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
+ sizeof(opt))) {
+ perror("setsockopt failed");
+ fprintf(stderr, "[ERROR] initialize_server_socket: Failed to set socket options.\n");
+ close(*server_fd_out);
+ return -1;
+ }
+
+ address_out->sin_family = AF_INET;
+ address_out->sin_addr.s_addr = inet_addr(ip);
+ address_out->sin_port = htons(port);
+
+ if (bind(*server_fd_out, (struct sockaddr *)address_out, sizeof(*address_out)) < 0) {
+ perror("bind failed");
+ fprintf(stderr, "[ERROR] initialize_server_socket: Failed to bind socket to %s:%d.\n", ip, port);
+ close(*server_fd_out);
+ return -1;
+ }
+
+ if (listen(*server_fd_out, 10) < 0) {
+ perror("listen failed");
+ fprintf(stderr, "[ERROR] initialize_server_socket: Failed to listen on socket.\n");
+ close(*server_fd_out);
+ return -1;
+ }
+
+ printf("Beaker server listening on %s:%d\n", ip, port);
+ return 0;
+}
+
+static void handle_client_connection(int new_socket) {
+ current_client_socket = new_socket;
+ char buffer[BUFFER_SIZE] = {0};
+
+ ssize_t bytes_read = read(new_socket, buffer, BUFFER_SIZE - 1);
+ if (bytes_read < 0) {
+ perror("read failed");
+ fprintf(stderr, "[ERROR] handle_client_connection: Failed to read from client socket.\n");
+ close(new_socket);
+ return;
+ }
+ buffer[bytes_read] = '\0';
+
+ strncpy(current_request_buffer, buffer, BUFFER_SIZE - 1);
+ current_request_buffer[BUFFER_SIZE - 1] = '\0';
+
+ char request_line[MAX_PATH_LEN + 64];
+ char *first_line_end = strstr(buffer, "\r\n");
+
+ if (first_line_end == NULL) {
+ fprintf(stderr, "[ERROR] handle_client_connection: Invalid HTTP request: No CRLF found.\n");
+ const char *bad_request = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n";
+ send(new_socket, bad_request, strlen(bad_request), 0);
+ close(new_socket);
+ return;
+ }
+ size_t request_line_len = first_line_end - buffer;
+ if (request_line_len >= sizeof(request_line)) {
+ fprintf(stderr, "[ERROR] handle_client_connection: Request line too long.\n");
+ const char *bad_request = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n";
+ send(new_socket, bad_request, strlen(bad_request), 0);
+ close(new_socket);
+ return;
+ }
+ strncpy(request_line, buffer, request_line_len);
+ request_line[request_line_len] = '\0';
+
+ UrlParams request_params;
+ char *requested_path = parse_request_url(request_line, &request_params);
+
+ if (requested_path == NULL) {
+ fprintf(stderr, "[ERROR] handle_client_connection: Could not parse request path. Sending 400 Bad Request.\n");
+ const char *bad_request = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n";
+ send(new_socket, bad_request, strlen(bad_request), 0);
+ close(new_socket);
+ return;
+ }
+ printf("Accessing: %s\n", requested_path);
+
+ bool handled = false;
+
+ if (strncmp(requested_path, "/static/", strlen("/static/")) == 0) {
+
+ if (serve_static_file(requested_path + strlen("/static/"))) {
+ handled = true;
+ }
+ }
+
+ if (!handled) {
+ int best_match_handler_index = -1;
+ size_t best_match_len = 0;
+
+ for (int i = 0; i < handler_count; i++) {
+ size_t handler_path_len = strlen(handlers[i].path);
+
+ if (strncmp(requested_path, handlers[i].path, handler_path_len) == 0) {
+
+ if (handler_path_len == strlen(requested_path) ||
+ requested_path[handler_path_len] == '/') {
+
+ if (handler_path_len > best_match_len) {
+ best_match_len = handler_path_len;
+ best_match_handler_index = i;
+ }
+ }
+ }
+ }
+
+ if (best_match_handler_index != -1) {
+ handlers[best_match_handler_index].handler(&request_params);
+ handled = true;
+ }
+ }
+
+ if (!handled) {
+ fprintf(stderr,
+ "[WARNING] handle_client_connection: No handler or static file found for path '%s'. Sending 404 Not Found.\n",
+ requested_path);
+ const char *not_found_html = "<h1>404 Not Found</h1><p>The requested URL "
+ "was not located on this server.</p>";
+ char not_found_response[BUFFER_SIZE];
+ snprintf(not_found_response, sizeof(not_found_response),
+ "HTTP/1.1 404 Not Found\r\n"
+ "Content-Type: text/html; charset=UTF-8\r\n"
+ "Content-Length: %zu\r\n"
+ "Connection: close\r\n"
+ "\r\n%s",
+ strlen(not_found_html), not_found_html);
+ send(new_socket, not_found_response, strlen(not_found_response), 0);
+ }
+
+ free(requested_path);
+ close(new_socket);
+ current_client_socket = -1;
+}
+
+int beaker_run(const char *ip, int port) {
+ int server_fd;
+ struct sockaddr_in address;
+ int addrlen = sizeof(address);
+
+ if (initialize_server_socket(ip, port, &server_fd, &address) != 0) {
+ return -1;
+ }
+
+ while (true) {
+ int new_socket;
+
+ if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
+ (socklen_t *)&addrlen)) < 0) {
+ perror("accept failed");
+ fprintf(stderr, "[ERROR] beaker_run: Failed to accept connection.\n");
+ continue;
+ }
+
+ handle_client_connection(new_socket);
+ }
+
+ close(server_fd);
+ return 0;
+} \ No newline at end of file
diff --git a/src/template.c b/src/template.c
new file mode 100644
index 0000000..494b011
--- /dev/null
+++ b/src/template.c
@@ -0,0 +1,961 @@
+#include "../beaker.h"
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static char *render_template_segment(const char *template_segment,
+ TemplateContext *ctx);
+
+static ContextVar *find_context_var(TemplateContext *ctx, const char *key) {
+ if (ctx == NULL || key == NULL) {
+ fprintf(stderr,
+ "[ERROR] find_context_var: Invalid NULL input (ctx or key).\n");
+ return NULL;
+ }
+ for (int i = 0; i < ctx->count; i++) {
+ if (strcmp(ctx->vars[i].key, key) == 0) {
+ return &ctx->vars[i];
+ }
+ }
+ return NULL;
+}
+
+static void free_string_array_var(ContextVar *var) {
+ if (var == NULL || var->value.string_array_data.values == NULL) {
+ return;
+ }
+ for (int i = 0; i < var->value.string_array_data.count; i++) {
+ if (var->value.string_array_data.values[i] != NULL) {
+ free(var->value.string_array_data.values[i]);
+ var->value.string_array_data.values[i] = NULL;
+ }
+ }
+ free(var->value.string_array_data.values);
+ var->value.string_array_data.values = NULL;
+ var->value.string_array_data.count = 0;
+}
+
+static void free_string_2d_array_var(ContextVar *var) {
+ if (var == NULL || var->value.string_2d_array_data.values == NULL) {
+ return;
+ }
+ for (int i = 0; i < var->value.string_2d_array_data.outer_count; i++) {
+ if (var->value.string_2d_array_data.values[i] != NULL) {
+ for (int j = 0; j < var->value.string_2d_array_data.inner_counts[i];
+ j++) {
+ if (var->value.string_2d_array_data.values[i][j] != NULL) {
+ free(var->value.string_2d_array_data.values[i][j]);
+ var->value.string_2d_array_data.values[i][j] = NULL;
+ }
+ }
+ free(var->value.string_2d_array_data.values[i]);
+ var->value.string_2d_array_data.values[i] = NULL;
+ }
+ }
+ free(var->value.string_2d_array_data.values);
+ var->value.string_2d_array_data.values = NULL;
+
+ if (var->value.string_2d_array_data.inner_counts != NULL) {
+ free(var->value.string_2d_array_data.inner_counts);
+ var->value.string_2d_array_data.inner_counts = NULL;
+ }
+ var->value.string_2d_array_data.outer_count = 0;
+}
+
+TemplateContext new_context() {
+ TemplateContext ctx;
+ ctx.count = 0;
+ return ctx;
+}
+
+void context_set(TemplateContext *ctx, const char *key, const char *value) {
+ if (ctx == NULL || key == NULL || value == NULL) {
+ fprintf(stderr,
+ "[ERROR] context_set: Invalid NULL input (ctx, key, or value).\n");
+ return;
+ }
+
+ ContextVar *var = find_context_var(ctx, key);
+ if (var != NULL) {
+
+ if (var->type == CONTEXT_TYPE_STRING_ARRAY) {
+ free_string_array_var(var);
+ } else if (var->type == CONTEXT_TYPE_STRING_2D_ARRAY) {
+ free_string_2d_array_var(var);
+ }
+
+ strncpy(var->value.string_val, value, MAX_VALUE_LEN - 1);
+ var->value.string_val[MAX_VALUE_LEN - 1] = '\0';
+ var->type = CONTEXT_TYPE_STRING;
+ return;
+ }
+
+ if (ctx->count < MAX_CONTEXT_VARS) {
+ ContextVar *new_var = &ctx->vars[ctx->count];
+ strncpy(new_var->key, key, MAX_KEY_LEN - 1);
+ new_var->key[MAX_KEY_LEN - 1] = '\0';
+ strncpy(new_var->value.string_val, value, MAX_VALUE_LEN - 1);
+ new_var->value.string_val[MAX_VALUE_LEN - 1] = '\0';
+ new_var->type = CONTEXT_TYPE_STRING;
+ ctx->count++;
+ } else {
+ fprintf(stderr,
+ "[WARNING] context_set: TemplateContext is full. Cannot add key "
+ "'%s'.\n",
+ key);
+ }
+}
+
+void context_set_string_array(TemplateContext *ctx, const char *key,
+ char *values[], int count) {
+ if (ctx == NULL || key == NULL || values == NULL) {
+ fprintf(stderr, "[ERROR] context_set_string_array: Invalid NULL input "
+ "(ctx, key, or values).\n");
+ return;
+ }
+ if (count < 0 || count > MAX_OUTER_ARRAY_ITEMS) {
+ fprintf(stderr,
+ "[ERROR] context_set_string_array: Invalid count %d for string "
+ "array context '%s'. Max %d allowed.\n",
+ count, key, MAX_OUTER_ARRAY_ITEMS);
+ return;
+ }
+
+ ContextVar *var = find_context_var(ctx, key);
+ if (var != NULL) {
+
+ if (var->type == CONTEXT_TYPE_STRING_ARRAY) {
+ free_string_array_var(var);
+ } else if (var->type == CONTEXT_TYPE_STRING_2D_ARRAY) {
+ free_string_2d_array_var(var);
+ }
+ } else {
+
+ if (ctx->count < MAX_CONTEXT_VARS) {
+ var = &ctx->vars[ctx->count];
+ strncpy(var->key, key, MAX_KEY_LEN - 1);
+ var->key[MAX_KEY_LEN - 1] = '\0';
+ ctx->count++;
+ } else {
+ fprintf(stderr,
+ "[WARNING] context_set_string_array: TemplateContext is full. "
+ "Cannot add string array key '%s'.\n",
+ key);
+ return;
+ }
+ }
+
+ var->type = CONTEXT_TYPE_STRING_ARRAY;
+ var->value.string_array_data.values = (char **)malloc(sizeof(char *) * count);
+ if (var->value.string_array_data.values == NULL) {
+ perror("Failed to allocate memory for string array pointers");
+ fprintf(stderr,
+ "[ERROR] context_set_string_array: Allocation failed for values "
+ "for key '%s'.\n",
+ key);
+ return;
+ }
+ var->value.string_array_data.count = count;
+
+ for (int i = 0; i < count; i++) {
+ if (values[i] == NULL) {
+ fprintf(stderr,
+ "[ERROR] context_set_string_array: NULL value at index %d for "
+ "key '%s'. Skipping.\n",
+ i, key);
+ var->value.string_array_data.values[i] = NULL;
+ continue;
+ }
+ var->value.string_array_data.values[i] = strdup(values[i]);
+ if (var->value.string_array_data.values[i] == NULL) {
+ perror("Failed to duplicate string for string array context");
+ fprintf(stderr,
+ "[ERROR] context_set_string_array: Failed to duplicate value for "
+ "item %d of key '%s'.\n",
+ i, key);
+
+ for (int j = 0; j < i; j++) {
+ if (var->value.string_array_data.values[j] != NULL) {
+ free(var->value.string_array_data.values[j]);
+ }
+ }
+ free(var->value.string_array_data.values);
+ var->value.string_array_data.values = NULL;
+ var->value.string_array_data.count = 0;
+ return;
+ }
+ }
+}
+
+void context_set_array_of_arrays(TemplateContext *ctx, const char *key,
+ char **values_2d[], int outer_count,
+ int inner_counts[]) {
+ if (ctx == NULL || key == NULL || values_2d == NULL || inner_counts == NULL) {
+ fprintf(stderr, "[ERROR] context_set_array_of_arrays: Invalid NULL input "
+ "(ctx, key, values_2d, or inner_counts).\n");
+ return;
+ }
+ if (outer_count < 0 || outer_count > MAX_OUTER_ARRAY_ITEMS) {
+ fprintf(stderr,
+ "[ERROR] context_set_array_of_arrays: Invalid outer count %d for "
+ "array of arrays context '%s'. Max %d allowed.\n",
+ outer_count, key, MAX_OUTER_ARRAY_ITEMS);
+ return;
+ }
+
+ ContextVar *var = find_context_var(ctx, key);
+ if (var != NULL) {
+
+ if (var->type == CONTEXT_TYPE_STRING_ARRAY) {
+ free_string_array_var(var);
+ } else if (var->type == CONTEXT_TYPE_STRING_2D_ARRAY) {
+ free_string_2d_array_var(var);
+ }
+ } else {
+
+ if (ctx->count < MAX_CONTEXT_VARS) {
+ var = &ctx->vars[ctx->count];
+ strncpy(var->key, key, MAX_KEY_LEN - 1);
+ var->key[MAX_KEY_LEN - 1] = '\0';
+ ctx->count++;
+ } else {
+ fprintf(stderr,
+ "[WARNING] context_set_array_of_arrays: TemplateContext is full. "
+ "Cannot add array of arrays key '%s'.\n",
+ key);
+ return;
+ }
+ }
+
+ var->type = CONTEXT_TYPE_STRING_2D_ARRAY;
+ var->value.string_2d_array_data.values =
+ (char ***)malloc(sizeof(char **) * outer_count);
+ var->value.string_2d_array_data.inner_counts =
+ (int *)malloc(sizeof(int) * outer_count);
+
+ if (var->value.string_2d_array_data.values == NULL ||
+ var->value.string_2d_array_data.inner_counts == NULL) {
+ perror("Failed to allocate memory for 2D string array pointers or counts");
+ fprintf(stderr,
+ "[ERROR] context_set_array_of_arrays: Allocation failed for key "
+ "'%s'.\n",
+ key);
+ free(var->value.string_2d_array_data.values);
+ free(var->value.string_2d_array_data.inner_counts);
+ var->value.string_2d_array_data.values = NULL;
+ var->value.string_2d_array_data.inner_counts = NULL;
+ return;
+ }
+ var->value.string_2d_array_data.outer_count = outer_count;
+
+ for (int i = 0; i < outer_count; i++) {
+ int current_inner_count = inner_counts[i];
+ if (current_inner_count < 0 ||
+ current_inner_count > MAX_INNER_ARRAY_ITEMS) {
+ fprintf(stderr,
+ "[ERROR] context_set_array_of_arrays: Invalid inner count %d for "
+ "item %d in 2D array '%s'. Max %d allowed.\n",
+ current_inner_count, i, key, MAX_INNER_ARRAY_ITEMS);
+
+ for (int k = 0; k < i; k++) {
+ if (var->value.string_2d_array_data.values[k] != NULL) {
+ for (int l = 0; l < var->value.string_2d_array_data.inner_counts[k];
+ l++) {
+ if (var->value.string_2d_array_data.values[k][l] != NULL) {
+ free(var->value.string_2d_array_data.values[k][l]);
+ }
+ }
+ free(var->value.string_2d_array_data.values[k]);
+ }
+ }
+ free(var->value.string_2d_array_data.values);
+ free(var->value.string_2d_array_data.inner_counts);
+ var->value.string_2d_array_data.values = NULL;
+ var->value.string_2d_array_data.inner_counts = NULL;
+ return;
+ }
+
+ var->value.string_2d_array_data.values[i] =
+ (char **)malloc(sizeof(char *) * current_inner_count);
+ if (var->value.string_2d_array_data.values[i] == NULL) {
+ perror("Failed to allocate memory for inner string array pointers");
+ fprintf(stderr,
+ "[ERROR] context_set_array_of_arrays: Allocation failed for "
+ "inner array %d of key '%s'.\n",
+ i, key);
+
+ for (int k = 0; k < i; k++) {
+ if (var->value.string_2d_array_data.values[k] != NULL) {
+ for (int l = 0; l < var->value.string_2d_array_data.inner_counts[k];
+ l++) {
+ if (var->value.string_2d_array_data.values[k][l] != NULL) {
+ free(var->value.string_2d_array_data.values[k][l]);
+ }
+ }
+ free(var->value.string_2d_array_data.values[k]);
+ }
+ }
+ free(var->value.string_2d_array_data.values);
+ free(var->value.string_2d_array_data.inner_counts);
+ var->value.string_2d_array_data.values = NULL;
+ var->value.string_2d_array_data.inner_counts = NULL;
+ return;
+ }
+ var->value.string_2d_array_data.inner_counts[i] = current_inner_count;
+
+ for (int j = 0; j < current_inner_count; j++) {
+ if (values_2d[i][j] == NULL) {
+ fprintf(stderr,
+ "[ERROR] context_set_array_of_arrays: NULL value at index "
+ "[%d][%d] for key '%s'. Skipping.\n",
+ i, j, key);
+ var->value.string_2d_array_data.values[i][j] = NULL;
+ continue;
+ }
+ var->value.string_2d_array_data.values[i][j] = strdup(values_2d[i][j]);
+ if (var->value.string_2d_array_data.values[i][j] == NULL) {
+ perror("Failed to duplicate string for inner array context");
+ fprintf(stderr,
+ "[ERROR] context_set_array_of_arrays: Failed to duplicate "
+ "value for item [%d][%d] of key '%s'.\n",
+ i, j, key);
+
+ for (int k = 0; k <= i; k++) {
+ if (var->value.string_2d_array_data.values[k] != NULL) {
+ for (int l = 0;
+ l <
+ (k == i ? j : var->value.string_2d_array_data.inner_counts[k]);
+ l++) {
+ if (var->value.string_2d_array_data.values[k][l] != NULL) {
+ free(var->value.string_2d_array_data.values[k][l]);
+ }
+ }
+ free(var->value.string_2d_array_data.values[k]);
+ }
+ }
+ free(var->value.string_2d_array_data.values);
+ free(var->value.string_2d_array_data.inner_counts);
+ var->value.string_2d_array_data.values = NULL;
+ var->value.string_2d_array_data.inner_counts = NULL;
+ return;
+ }
+ }
+ }
+}
+
+void free_context(TemplateContext *ctx) {
+ if (ctx == NULL) {
+ return;
+ }
+ for (int i = 0; i < ctx->count; i++) {
+ if (ctx->vars[i].type == CONTEXT_TYPE_STRING_ARRAY) {
+ free_string_array_var(&ctx->vars[i]);
+ } else if (ctx->vars[i].type == CONTEXT_TYPE_STRING_2D_ARRAY) {
+ free_string_2d_array_var(&ctx->vars[i]);
+ }
+ }
+ ctx->count = 0;
+}
+
+static char *html_escape(const char *input) {
+ if (input == NULL) {
+ return strdup("");
+ }
+
+ size_t input_len = strlen(input);
+
+ size_t estimated_len = input_len * 5 + 1;
+ char *output = (char *)malloc(estimated_len);
+ if (output == NULL) {
+ perror("Failed to allocate memory for HTML escape output");
+ return NULL;
+ }
+ output[0] = '\0';
+ size_t current_output_len = 0;
+
+ for (size_t i = 0; i < input_len; i++) {
+ const char *replacement = NULL;
+
+ switch (input[i]) {
+ case '&':
+ replacement = "&amp;";
+ break;
+ case '<':
+ replacement = "&lt;";
+ break;
+ case '>':
+ replacement = "&gt;";
+ break;
+ case '"':
+ replacement = "&quot;";
+ break;
+ case '\'':
+ replacement = "&apos;";
+ break;
+ default:
+ break;
+ }
+
+ if (replacement) {
+ size_t repl_len = strlen(replacement);
+
+ if (current_output_len + repl_len + 1 > estimated_len) {
+ estimated_len = (current_output_len + repl_len + 1) * 2;
+ char *new_output = (char *)realloc(output, estimated_len);
+ if (new_output == NULL) {
+ perror("Failed to reallocate memory for HTML escape output");
+ free(output);
+ return NULL;
+ }
+ output = new_output;
+ }
+ strcat(output, replacement);
+ current_output_len += repl_len;
+ } else {
+
+ if (current_output_len + 1 + 1 > estimated_len) {
+ estimated_len = (current_output_len + 1 + 1) * 2;
+ char *new_output = (char *)realloc(output, estimated_len);
+ if (new_output == NULL) {
+ perror("Failed to reallocate memory for HTML escape output");
+ free(output);
+ return NULL;
+ }
+ output = new_output;
+ }
+ output[current_output_len++] = input[i];
+ output[current_output_len] = '\0';
+ }
+ }
+ return output;
+}
+
+static void append_to_buffer(char **buffer, size_t *current_len,
+ size_t *max_len, const char *str_to_add) {
+ if (str_to_add == NULL) {
+ fprintf(stderr, "[WARNING] append_to_buffer: Attempted to append NULL "
+ "string. Skipping.\n");
+ return;
+ }
+
+ size_t add_len = strlen(str_to_add);
+
+ if (*current_len + add_len + 1 > *max_len) {
+ *max_len = (*current_len + add_len + 1) * 2;
+
+ if (*max_len < BUFFER_SIZE * 2) {
+ *max_len = BUFFER_SIZE * 2;
+ }
+
+ char *new_buffer = (char *)realloc(*buffer, *max_len);
+ if (new_buffer == NULL) {
+ perror("Failed to reallocate buffer for template rendering");
+ fprintf(stderr,
+ "[ERROR] append_to_buffer: Reallocation failed (requested %zu "
+ "bytes).\n",
+ *max_len);
+ free(*buffer);
+ *buffer = NULL;
+
+ exit(EXIT_FAILURE);
+ }
+ *buffer = new_buffer;
+ }
+
+ strcat(*buffer, str_to_add);
+ *current_len += add_len;
+}
+
+static char *parse_indexed_tag(const char *tag_content, int *index_val) {
+ *index_val = -1;
+
+ char *open_bracket = strchr(tag_content, '[');
+ if (open_bracket == NULL) {
+
+ char *key_name = strdup(tag_content);
+ if (key_name == NULL) {
+ perror("Failed to duplicate tag content for simple key");
+ fprintf(stderr, "[ERROR] parse_indexed_tag: strdup failed for '%s'.\n",
+ tag_content);
+ }
+ return key_name;
+ }
+
+ char *close_bracket = strchr(open_bracket, ']');
+ if (close_bracket == NULL) {
+ fprintf(stderr,
+ "[ERROR] parse_indexed_tag: Unclosed bracket in tag '%s'. "
+ "Returning raw tag content.\n",
+ tag_content);
+
+ char *key_name = strdup(tag_content);
+ if (key_name == NULL) {
+ perror("Failed to duplicate malformed tag content");
+ }
+ return key_name;
+ }
+
+ size_t key_len = open_bracket - tag_content;
+ char *key_name = (char *)malloc(key_len + 1);
+ if (key_name == NULL) {
+ perror("Failed to allocate memory for key_name in parse_indexed_tag");
+ fprintf(stderr,
+ "[ERROR] parse_indexed_tag: Allocation failed for key_name.\n");
+ return NULL;
+ }
+ strncpy(key_name, tag_content, key_len);
+ key_name[key_len] = '\0';
+
+ size_t index_str_len = close_bracket - (open_bracket + 1);
+ char *index_str = (char *)malloc(index_str_len + 1);
+ if (index_str == NULL) {
+ perror("Failed to allocate memory for index_str in parse_indexed_tag");
+ fprintf(stderr,
+ "[ERROR] parse_indexed_tag: Allocation failed for index_str.\n");
+ free(key_name);
+ return NULL;
+ }
+ strncpy(index_str, open_bracket + 1, index_str_len);
+ index_str[index_str_len] = '\0';
+
+ *index_val = atoi(index_str);
+ free(index_str);
+
+ return key_name;
+}
+
+static char *render_template_segment(const char *template_segment,
+ TemplateContext *ctx) {
+ size_t initial_max_len = BUFFER_SIZE;
+ char *rendered_buffer = (char *)malloc(initial_max_len);
+ if (rendered_buffer == NULL) {
+ perror("Failed to allocate initial buffer for template segment rendering");
+ fprintf(stderr,
+ "[ERROR] render_template_segment: Failed to allocate initial %zu "
+ "bytes for rendered_buffer.\n",
+ initial_max_len);
+ return NULL;
+ }
+ rendered_buffer[0] = '\0';
+ size_t current_len = 0;
+ size_t max_len = initial_max_len;
+
+ const char *current_pos = template_segment;
+ const char *start_tag;
+
+ while ((start_tag = strstr(current_pos, "{{")) != NULL) {
+
+ size_t text_len = start_tag - current_pos;
+ if (text_len > 0) {
+ char *text_before_tag = (char *)malloc(text_len + 1);
+ if (text_before_tag == NULL) {
+ perror("Failed to allocate memory for text_before_tag");
+ fprintf(stderr, "[ERROR] render_template_segment: Allocation failed "
+ "for text_before_tag.\n");
+ free(rendered_buffer);
+ return NULL;
+ }
+ strncpy(text_before_tag, current_pos, text_len);
+ text_before_tag[text_len] = '\0';
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ text_before_tag);
+ free(text_before_tag);
+ }
+
+ const char *end_tag = strstr(start_tag, "}}");
+ if (end_tag == NULL) {
+ fprintf(stderr, "[ERROR] render_template_segment: Unclosed '{{' tag. "
+ "Appending remaining template content as-is.\n");
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, start_tag);
+ return rendered_buffer;
+ }
+
+ size_t tag_content_len = end_tag - (start_tag + 2);
+ char *tag_content_raw = (char *)malloc(tag_content_len + 1);
+ if (tag_content_raw == NULL) {
+ perror("Failed to allocate memory for tag_content_raw");
+ fprintf(stderr, "[ERROR] render_template_segment: Allocation failed for "
+ "tag_content_raw.\n");
+ free(rendered_buffer);
+ return NULL;
+ }
+ strncpy(tag_content_raw, start_tag + 2, tag_content_len);
+ tag_content_raw[tag_content_len] = '\0';
+
+ char *trimmed_tag_content = tag_content_raw;
+ while (*trimmed_tag_content != '\0' &&
+ (*trimmed_tag_content == ' ' || *trimmed_tag_content == '\t' ||
+ *trimmed_tag_content == '\n' || *trimmed_tag_content == '\r')) {
+ trimmed_tag_content++;
+ }
+
+ if (*trimmed_tag_content == '\0') {
+ free(tag_content_raw);
+ current_pos = end_tag + 2;
+ continue;
+ } else {
+ char *end_trimmed = trimmed_tag_content + strlen(trimmed_tag_content) - 1;
+ while (end_trimmed >= trimmed_tag_content &&
+ (*end_trimmed == ' ' || *end_trimmed == '\t' ||
+ *end_trimmed == '\n' || *end_trimmed == '\r')) {
+ }
+ *(end_trimmed + 1) = '\0';
+ }
+
+ if (strncmp(trimmed_tag_content, "for ", 4) == 0) {
+ char loop_var[MAX_KEY_LEN];
+ char list_var[MAX_KEY_LEN];
+ if (sscanf(trimmed_tag_content, "for %s in %s", loop_var, list_var) ==
+ 2) {
+ ContextVar *list_ctx_var = find_context_var(ctx, list_var);
+
+ const char *loop_end_tag = strstr(end_tag + 2, "{{endfor}}");
+ if (loop_end_tag == NULL) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ trimmed_tag_content);
+ free(tag_content_raw);
+ current_pos = end_tag + 2;
+ continue;
+ }
+
+ if (list_ctx_var != NULL) {
+
+ bool is_empty = false;
+ if (list_ctx_var->type == CONTEXT_TYPE_STRING_2D_ARRAY) {
+ is_empty =
+ (list_ctx_var->value.string_2d_array_data.outer_count == 0);
+ } else if (list_ctx_var->type == CONTEXT_TYPE_STRING_ARRAY) {
+ is_empty = (list_ctx_var->value.string_array_data.count == 0);
+ } else {
+
+ is_empty = true;
+ }
+
+ if (is_empty) {
+ current_pos = loop_end_tag + strlen("{{endfor}}");
+ free(tag_content_raw);
+ continue;
+ }
+
+ const char *loop_inner_start = end_tag + 2;
+ size_t loop_inner_len = loop_end_tag - loop_inner_start;
+ char *loop_inner_template = (char *)malloc(loop_inner_len + 1);
+ if (loop_inner_template == NULL) {
+ perror("Failed to allocate memory for loop_inner_template");
+ fprintf(stderr, "[ERROR] render_template_segment: Allocation "
+ "failed for loop_inner_template.\n");
+ free(rendered_buffer);
+ free(tag_content_raw);
+ return NULL;
+ }
+ strncpy(loop_inner_template, loop_inner_start, loop_inner_len);
+ loop_inner_template[loop_inner_len] = '\0';
+
+ if (list_ctx_var->type == CONTEXT_TYPE_STRING_2D_ARRAY) {
+ for (int i = 0;
+ i < list_ctx_var->value.string_2d_array_data.outer_count;
+ i++) {
+ TemplateContext loop_ctx = new_context();
+
+ for (int j = 0; j < ctx->count; j++) {
+ if (ctx->vars[j].type == CONTEXT_TYPE_STRING) {
+ context_set(&loop_ctx, ctx->vars[j].key,
+ ctx->vars[j].value.string_val);
+ }
+ }
+
+ context_set_string_array(
+ &loop_ctx, loop_var,
+ list_ctx_var->value.string_2d_array_data.values[i],
+ list_ctx_var->value.string_2d_array_data.inner_counts[i]);
+
+ char *rendered_loop_item =
+ render_template_segment(loop_inner_template, &loop_ctx);
+ if (rendered_loop_item) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ rendered_loop_item);
+ free(rendered_loop_item);
+ }
+ free_context(&loop_ctx);
+ }
+ } else if (list_ctx_var->type == CONTEXT_TYPE_STRING_ARRAY) {
+ for (int i = 0; i < list_ctx_var->value.string_array_data.count;
+ i++) {
+ TemplateContext loop_ctx = new_context();
+
+ for (int j = 0; j < ctx->count; j++) {
+ if (ctx->vars[j].type == CONTEXT_TYPE_STRING) {
+ context_set(&loop_ctx, ctx->vars[j].key,
+ ctx->vars[j].value.string_val);
+ }
+ }
+
+ context_set(&loop_ctx, loop_var,
+ list_ctx_var->value.string_array_data.values[i]);
+
+ char *rendered_loop_item =
+ render_template_segment(loop_inner_template, &loop_ctx);
+ if (rendered_loop_item) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ rendered_loop_item);
+ free(rendered_loop_item);
+ }
+ free_context(&loop_ctx);
+ }
+ } else {
+ fprintf(
+ stderr,
+ "[IGNORE] [ERROR] render_template_segment: List variable '%s' "
+ "(type %d) is not an iterable array type for 'for' loop.\n",
+ list_var, list_ctx_var->type);
+ }
+ free(loop_inner_template);
+ current_pos = loop_end_tag + strlen("{{endfor}}");
+ free(tag_content_raw);
+ continue;
+ } else {
+
+ fprintf(stderr,
+ "[IGNORE] [ERROR] render_template_segment: List variable "
+ "'%s' not found for 'for' loop. Skipping loop block.\n",
+ list_var);
+ current_pos = loop_end_tag + strlen("{{endfor}}");
+ free(tag_content_raw);
+ continue;
+ }
+ } else {
+ fprintf(stderr,
+ "[ERROR] render_template_segment: Malformed 'for' loop tag: "
+ "'%s'. Expected 'for var in list'. Appending loop tag as-is.\n",
+ trimmed_tag_content);
+ }
+ }
+
+ else if (strcmp(trimmed_tag_content, "endfor") == 0) {
+ fprintf(stderr, "[WARNING] render_template_segment: '{{endfor}}' without "
+ "matching '{{for}}'. Appending endfor tag as-is.\n");
+ }
+
+ else if (strncmp(trimmed_tag_content, "include ", 8) == 0) {
+ const char *filename_start = trimmed_tag_content + strlen("include ");
+ if (*filename_start == '"') {
+ filename_start++;
+ char *filename_end = strchr(filename_start, '"');
+ if (filename_end != NULL) {
+ size_t filename_len = filename_end - filename_start;
+ char *included_filename = (char *)malloc(filename_len + 1);
+ if (included_filename == NULL) {
+ perror("Failed to allocate memory for included filename");
+ fprintf(stderr, "[ERROR] render_template_segment: Allocation "
+ "failed for included_filename.\n");
+ free(rendered_buffer);
+ free(tag_content_raw);
+ return NULL;
+ }
+ strncpy(included_filename, filename_start, filename_len);
+ included_filename[filename_len] = '\0';
+
+ char *included_html = render_template(included_filename, ctx);
+ if (included_html) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ included_html);
+ free(included_html);
+ } else {
+ fprintf(stderr,
+ "[WARNING] render_template_segment: Failed to render "
+ "included template '%s'.\n",
+ included_filename);
+
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ "<!-- Failed to include ");
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ included_filename);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, " -->");
+ }
+ free(included_filename);
+ current_pos = end_tag + 2;
+ free(tag_content_raw);
+ continue;
+ } else {
+ fprintf(stderr,
+ "[ERROR] render_template_segment: Malformed include tag "
+ "'%s'. Missing closing quote.\n",
+ trimmed_tag_content);
+
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "{{");
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ trimmed_tag_content);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "}}");
+ free(tag_content_raw);
+
+ current_pos = end_tag + 2;
+
+ continue;
+
+ }
+ } else {
+ fprintf(stderr,
+ "[ERROR] render_template_segment: Malformed include tag '%s'. "
+ "Expected quoted filename.\n",
+ trimmed_tag_content);
+
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "{{");
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ trimmed_tag_content);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "}}");
+ }
+ }
+
+ else {
+ bool is_safe = false;
+ char *processing_tag_content = strdup(trimmed_tag_content);
+ if (processing_tag_content == NULL) {
+ perror("Failed to duplicate tag content for processing");
+ free(rendered_buffer);
+ free(tag_content_raw);
+ return NULL;
+ }
+
+ char *safe_flag_pos_in_processing =
+ strstr(processing_tag_content, "|safe");
+ if (safe_flag_pos_in_processing != NULL) {
+ is_safe = true;
+ *safe_flag_pos_in_processing = '\0';
+ }
+
+ int index_val = -1;
+ char *key_name = parse_indexed_tag(processing_tag_content, &index_val);
+ free(processing_tag_content);
+
+ if (key_name == NULL) {
+ fprintf(stderr,
+ "[ERROR] render_template_segment: parse_indexed_tag failed for "
+ "'%s'.\n",
+ trimmed_tag_content);
+ free(rendered_buffer);
+ free(tag_content_raw);
+ return NULL;
+ }
+
+ const char *value_to_append = NULL;
+ ContextVar *var = find_context_var(ctx, key_name);
+
+ if (var != NULL) {
+ if (index_val == -1) {
+ if (var->type == CONTEXT_TYPE_STRING) {
+ value_to_append = var->value.string_val;
+ } else {
+ fprintf(
+ stderr,
+ "[WARNING] render_template_segment: Variable '%s' is not a "
+ "simple string (type %d). Not rendering as simple string.\n",
+ key_name, var->type);
+
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "{{");
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ trimmed_tag_content);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "}}");
+ }
+ } else {
+ if (var->type == CONTEXT_TYPE_STRING_ARRAY) {
+ if (index_val >= 0 &&
+ index_val < var->value.string_array_data.count) {
+ value_to_append = var->value.string_array_data.values[index_val];
+ } else {
+ fprintf(stderr,
+ "[ERROR] render_template_segment: Index %d out of bounds "
+ "for '%s' (count %d). Appending tag as-is.\n",
+ index_val, key_name, var->value.string_array_data.count);
+ }
+ } else {
+ fprintf(
+ stderr,
+ "[ERROR] render_template_segment: Variable '%s' (type %d) is "
+ "not a string array for indexed access. Appending tag as-is.\n",
+ key_name, var->type);
+ }
+ }
+ } else {
+ fprintf(stderr,
+ "[WARNING] render_template_segment: Key '%s' not found in "
+ "context. Appending tag as-is.\n",
+ key_name);
+ }
+
+ if (value_to_append != NULL) {
+ if (is_safe) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ value_to_append);
+ } else {
+ char *escaped_value = html_escape(value_to_append);
+ if (escaped_value) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ escaped_value);
+ free(escaped_value);
+ } else {
+ fprintf(stderr,
+ "[ERROR] render_template_segment: Failed to HTML escape "
+ "value for key '%s'. Appending raw.\n",
+ key_name);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ value_to_append);
+ }
+ }
+ }
+
+ free(key_name);
+
+ free(tag_content_raw);
+
+ current_pos = end_tag + 2;
+
+ continue;
+
+ }
+
+ free(tag_content_raw);
+ current_pos = end_tag + 2;
+ }
+
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, current_pos);
+ return rendered_buffer;
+}
+
+char *render_template(const char *template_file, TemplateContext *ctx) {
+ char full_path[MAX_PATH_LEN];
+
+ snprintf(full_path, sizeof(full_path), "%s%s", TEMPLATES_DIR, template_file);
+
+ FILE *fp = fopen(full_path, "r");
+ if (fp == NULL) {
+ perror("Error opening template file");
+ fprintf(stderr,
+ "[ERROR] render_template: Could not open template '%s'. %s\n",
+ full_path, strerror(errno));
+ return NULL;
+ }
+
+ fseek(fp, 0, SEEK_END);
+ long file_size = ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+
+ char *template_content = (char *)malloc(file_size + 1);
+ if (template_content == NULL) {
+ perror("Error allocating memory for template content");
+ fprintf(stderr,
+ "[ERROR] render_template: Failed to allocate %ld bytes for "
+ "template content.\n",
+ file_size + 1);
+ fclose(fp);
+ return NULL;
+ }
+ fread(template_content, 1, file_size, fp);
+ template_content[file_size] = '\0';
+ fclose(fp);
+
+ char *rendered_html = render_template_segment(template_content, ctx);
+ free(template_content);
+ return rendered_html;
+} \ No newline at end of file