Files
omnisearch/vendor/beaker/src/template.c
T

1488 lines
48 KiB
C

#include "../beaker.h"
#include "beaker_globals.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) {
beaker_log("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;
}
static void free_context_var(ContextVar *var) {
if (var == NULL) {
return;
}
switch (var->type) {
case CONTEXT_TYPE_STRING:
break;
case CONTEXT_TYPE_STRING_ARRAY:
free_string_array_var(var);
break;
case CONTEXT_TYPE_STRING_2D_ARRAY:
free_string_2d_array_var(var);
break;
}
}
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) {
beaker_log("ERROR",
"context_set: Invalid NULL input (ctx, key, or value).\n");
return;
}
ContextVar *var = find_context_var(ctx, key);
if (var != NULL) {
free_context_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 {
beaker_log("WARN",
"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) {
beaker_log("ERROR", "context_set_string_array: Invalid NULL input "
"(ctx, key, or values).\n");
return;
}
if (count < 0 || count > MAX_OUTER_ARRAY_ITEMS) {
beaker_log("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) {
free_context_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 {
beaker_log("WARN",
"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) {
beaker_log_errno_format(
"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) {
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) {
beaker_log_errno_format(
"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) {
beaker_log("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) {
beaker_log("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) {
free_context_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 {
beaker_log("WARN",
"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) {
beaker_log_errno_format(
"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) {
beaker_log("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) {
beaker_log_errno_format(
"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) {
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) {
beaker_log_errno_format(
"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++) {
free_context_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) {
beaker_log_errno("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) {
beaker_log_errno(
"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) {
beaker_log_errno(
"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) {
beaker_log("WARN", "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) {
beaker_log_errno_format(
"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;
const char *open_bracket = strchr(tag_content, '[');
if (open_bracket == NULL) {
char *key_name = strdup(tag_content);
if (key_name == NULL) {
beaker_log_errno_format(
"ERROR", "parse_indexed_tag: strdup failed for '%s'.\n", tag_content);
}
return key_name;
}
const char *close_bracket = strchr(open_bracket, ']');
if (close_bracket == NULL) {
beaker_log("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) {
beaker_log_errno("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) {
beaker_log_errno_format(
"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) {
beaker_log_errno_format(
"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;
}
typedef enum {
CONDITION_NONE,
CONDITION_TRUTHY,
CONDITION_EXISTS,
CONDITION_EQUAL,
CONDITION_NOT_EQUAL,
} ConditionType;
typedef struct {
ConditionType type;
char var_name[MAX_KEY_LEN];
char compare_value[MAX_VALUE_LEN];
int compare_index;
bool negate;
} Condition;
static const char *resolve_compare_value(const Condition *cond,
TemplateContext *ctx,
char compare_buf[MAX_VALUE_LEN]) {
const char *compare_value = cond->compare_value;
ContextVar *compare_var = find_context_var(ctx, cond->compare_value);
if (cond->compare_index >= 0) {
if (compare_var != NULL && compare_var->type == CONTEXT_TYPE_STRING_ARRAY &&
cond->compare_index < compare_var->value.string_array_data.count) {
compare_value =
compare_var->value.string_array_data.values[cond->compare_index];
}
} else if (compare_var != NULL && compare_var->type == CONTEXT_TYPE_STRING) {
strncpy(compare_buf, compare_var->value.string_val, MAX_VALUE_LEN - 1);
compare_buf[MAX_VALUE_LEN - 1] = '\0';
compare_value = compare_buf;
}
return compare_value;
}
static bool evaluate_condition(const Condition *cond, TemplateContext *ctx) {
if (cond == NULL || cond->type == CONDITION_NONE) {
return false;
}
ContextVar *var = find_context_var(ctx, cond->var_name);
const char *var_value = NULL;
if (var == NULL) {
if (cond->type == CONDITION_TRUTHY) {
return false;
}
var_value = "";
} else if (var->type == CONTEXT_TYPE_STRING_ARRAY &&
cond->compare_index >= 0) {
if (cond->compare_index >= 0 &&
cond->compare_index < var->value.string_array_data.count) {
var_value = var->value.string_array_data.values[cond->compare_index];
} else {
var_value = "";
}
} else if (var->type == CONTEXT_TYPE_STRING) {
var_value = var->value.string_val;
} else {
if (cond->type == CONDITION_TRUTHY) {
return false;
}
var_value = "";
}
switch (cond->type) {
case CONDITION_TRUTHY:
return (var_value != NULL && strlen(var_value) > 0);
case CONDITION_EXISTS: {
bool exists = (var != NULL);
return cond->negate ? !exists : exists;
}
case CONDITION_EQUAL:
case CONDITION_NOT_EQUAL: {
char compare_buf[MAX_VALUE_LEN];
const char *compare_value = resolve_compare_value(cond, ctx, compare_buf);
bool values_equal = strcmp(var_value, compare_value) == 0;
return cond->type == CONDITION_EQUAL ? values_equal : !values_equal;
}
default:
return false;
}
}
static Condition parse_condition(const char *condition_str) {
Condition cond;
cond.type = CONDITION_NONE;
cond.var_name[0] = '\0';
cond.compare_value[0] = '\0';
cond.compare_index = -1;
cond.negate = false;
if (condition_str == NULL || *condition_str == '\0') {
return cond;
}
const char *eq_pos = strstr(condition_str, "==");
const char *neq_pos = strstr(condition_str, "!=");
if (eq_pos != NULL && neq_pos != NULL) {
if (eq_pos < neq_pos) {
neq_pos = NULL;
} else {
eq_pos = NULL;
}
}
if (eq_pos != NULL) {
size_t var_len = eq_pos - condition_str;
if (var_len >= MAX_KEY_LEN) {
var_len = MAX_KEY_LEN - 1;
}
strncpy(cond.var_name, condition_str, var_len);
cond.var_name[var_len] = '\0';
const char *value_start = eq_pos + 2;
while (*value_start == ' ' || *value_start == '\t') {
value_start++;
}
if (*value_start == '"') {
value_start++;
const char *value_end = strchr(value_start, '"');
if (value_end != NULL) {
size_t val_len = value_end - value_start;
if (val_len >= MAX_VALUE_LEN) {
val_len = MAX_VALUE_LEN - 1;
}
strncpy(cond.compare_value, value_start, val_len);
cond.compare_value[val_len] = '\0';
}
} else {
size_t val_len = strlen(value_start);
if (val_len >= MAX_VALUE_LEN) {
val_len = MAX_VALUE_LEN - 1;
}
strncpy(cond.compare_value, value_start, val_len);
cond.compare_value[val_len] = '\0';
}
cond.type = CONDITION_EQUAL;
} else if (neq_pos != NULL) {
size_t var_len = neq_pos - condition_str;
if (var_len >= MAX_KEY_LEN) {
var_len = MAX_KEY_LEN - 1;
}
strncpy(cond.var_name, condition_str, var_len);
cond.var_name[var_len] = '\0';
const char *value_start = neq_pos + 2;
while (*value_start == ' ' || *value_start == '\t') {
value_start++;
}
if (*value_start == '"') {
value_start++;
const char *value_end = strchr(value_start, '"');
if (value_end != NULL) {
size_t val_len = value_end - value_start;
if (val_len >= MAX_VALUE_LEN) {
val_len = MAX_VALUE_LEN - 1;
}
strncpy(cond.compare_value, value_start, val_len);
cond.compare_value[val_len] = '\0';
}
} else {
size_t val_len = strlen(value_start);
if (val_len >= MAX_VALUE_LEN) {
val_len = MAX_VALUE_LEN - 1;
}
strncpy(cond.compare_value, value_start, val_len);
cond.compare_value[val_len] = '\0';
}
cond.type = CONDITION_NOT_EQUAL;
} else if (strncmp(condition_str, "exists ", 7) == 0) {
const char *var_start = condition_str + 7;
while (*var_start == ' ' || *var_start == '\t') {
var_start++;
}
size_t var_len = strlen(var_start);
if (var_len >= MAX_KEY_LEN) {
var_len = MAX_KEY_LEN - 1;
}
strncpy(cond.var_name, var_start, var_len);
cond.var_name[var_len] = '\0';
cond.type = CONDITION_EXISTS;
} else if (strncmp(condition_str, "not exists ", 11) == 0) {
const char *var_start = condition_str + 11;
while (*var_start == ' ' || *var_start == '\t') {
var_start++;
}
size_t var_len = strlen(var_start);
if (var_len >= MAX_KEY_LEN) {
var_len = MAX_KEY_LEN - 1;
}
strncpy(cond.var_name, var_start, var_len);
cond.var_name[var_len] = '\0';
cond.type = CONDITION_EXISTS;
cond.negate = true;
} else {
size_t var_len = strlen(condition_str);
if (var_len >= MAX_KEY_LEN) {
var_len = MAX_KEY_LEN - 1;
}
strncpy(cond.var_name, condition_str, var_len);
cond.var_name[var_len] = '\0';
cond.type = CONDITION_TRUTHY;
}
char *p = cond.var_name;
char *end = cond.var_name + strlen(cond.var_name) - 1;
while (p < end && (*end == ' ' || *end == '\t')) {
*end = '\0';
end--;
}
char *var_bracket = strchr(cond.var_name, '[');
if (var_bracket != NULL) {
char *close_bracket = strchr(var_bracket, ']');
if (close_bracket != NULL) {
*var_bracket = '\0';
cond.compare_index = atoi(var_bracket + 1);
}
}
char *bracket = strchr(cond.compare_value, '[');
if (bracket != NULL) {
char *close_bracket = strchr(bracket, ']');
if (close_bracket != NULL) {
*bracket = '\0';
cond.compare_index = atoi(bracket + 1);
}
}
return cond;
}
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) {
beaker_log_errno_format(
"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) {
beaker_log_errno_format("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) {
beaker_log("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) {
beaker_log_errno_format("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--;
}
*(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) {
beaker_log_errno_format("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 {
beaker_log(
"DEBUG",
"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 {
beaker_log("DEBUG",
"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 {
beaker_log(
"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) {
beaker_log("WARN", "render_template_segment: '{{endfor}}' without "
"matching '{{for}}'. Appending endfor tag as-is.\n");
}
else if (strncmp(trimmed_tag_content, "if ", 3) == 0) {
const char *condition_str = trimmed_tag_content + 3;
Condition cond = parse_condition(condition_str);
const char *else_tag = NULL;
const char *elif_tag = NULL;
const char *endif_tag = NULL;
int if_depth = 1;
const char *search_pos = end_tag + 2;
while (*search_pos != '\0' && if_depth > 0) {
const char *next_if = strstr(search_pos, "{{if ");
const char *next_else = strstr(search_pos, "{{else}}");
const char *next_elif = strstr(search_pos, "{{elif ");
const char *next_endif = strstr(search_pos, "{{endif}}");
const char *candidates[4] = {next_if, next_else, next_elif, next_endif};
const char *earliest = NULL;
int earliest_idx = -1;
for (int i = 0; i < 4; i++) {
if (candidates[i] != NULL) {
if (earliest == NULL || candidates[i] < earliest) {
earliest = candidates[i];
earliest_idx = i;
}
}
}
if (earliest == NULL) {
break;
}
if (earliest_idx == 0) {
if_depth++;
search_pos = earliest + strlen("{{if ");
continue;
} else if (earliest_idx == 1) {
else_tag = earliest;
search_pos = earliest + strlen("{{else}}");
continue;
} else if (earliest_idx == 2) {
if (elif_tag == NULL) {
elif_tag = earliest;
}
search_pos = earliest + strlen("{{elif ");
const char *elif_end = strstr(search_pos, "}}");
if (elif_end != NULL) {
search_pos = elif_end + 2;
}
continue;
} else {
if_depth--;
if (if_depth == 0) {
endif_tag = earliest;
}
search_pos = earliest + strlen("{{endif}}");
continue;
}
}
if (endif_tag == NULL) {
beaker_log("ERROR", "render_template_segment: Unclosed '{{if}}' tag. "
"Skipping if block.\n");
current_pos = end_tag + 2;
free(tag_content_raw);
continue;
}
if (endif_tag == NULL) {
beaker_log("ERROR", "render_template_segment: Unclosed '{{if}}' tag. "
"Skipping if block.\n");
current_pos = end_tag + 2;
free(tag_content_raw);
continue;
}
bool condition_met = evaluate_condition(&cond, ctx);
const char *block_start = end_tag + 2;
const char *block_end = NULL;
if (condition_met) {
if (elif_tag != NULL && elif_tag < endif_tag) {
block_end = elif_tag;
} else if (else_tag != NULL) {
block_end = else_tag;
} else {
block_end = endif_tag;
}
} else {
bool elif_matched = false;
if (elif_tag != NULL && elif_tag < endif_tag) {
const char *test_elif = elif_tag;
while (test_elif != NULL && test_elif < endif_tag) {
const char *elif_cond_start = test_elif + strlen("{{elif ");
const char *elif_cond_end = strstr(elif_cond_start, "}}");
if (elif_cond_end == NULL || elif_cond_end >= endif_tag) {
break;
}
size_t elif_cond_len = elif_cond_end - elif_cond_start;
char *elif_cond_str = (char *)malloc(elif_cond_len + 1);
if (elif_cond_str != NULL) {
strncpy(elif_cond_str, elif_cond_start, elif_cond_len);
elif_cond_str[elif_cond_len] = '\0';
Condition elif_cond = parse_condition(elif_cond_str);
if (evaluate_condition(&elif_cond, ctx)) {
block_start = test_elif + strlen("{{elif ");
const char *elif_end = strstr(block_start, "}}");
if (elif_end != NULL) {
block_start = elif_end + 2;
}
block_end = else_tag ? else_tag : endif_tag;
elif_matched = true;
free(elif_cond_str);
break;
}
free(elif_cond_str);
}
test_elif = strstr(test_elif + strlen("{{elif "), "{{elif ");
if (test_elif == NULL || test_elif >= endif_tag) {
break;
}
}
}
if (!elif_matched) {
if (else_tag && else_tag < endif_tag) {
block_start = else_tag + strlen("{{else}}");
block_end = endif_tag;
} else {
block_end = endif_tag;
block_start = endif_tag;
}
}
}
size_t block_len = block_end - block_start;
if (block_len > 0) {
char *block_template = (char *)malloc(block_len + 1);
if (block_template != NULL) {
strncpy(block_template, block_start, block_len);
block_template[block_len] = '\0';
char *rendered_block = render_template_segment(block_template, ctx);
if (rendered_block != NULL) {
append_to_buffer(&rendered_buffer, &current_len, &max_len,
rendered_block);
free(rendered_block);
}
free(block_template);
}
}
current_pos = endif_tag + strlen("{{endif}}");
free(tag_content_raw);
continue;
}
else if (strcmp(trimmed_tag_content, "elif ") == 0 ||
strcmp(trimmed_tag_content, "elif") == 0) {
beaker_log("WARN", "render_template_segment: '{{elif}}' without "
"matching '{{if}}'. Appending elif tag as-is.\n");
}
else if (strcmp(trimmed_tag_content, "else") == 0) {
beaker_log("WARN", "render_template_segment: '{{else}}' without "
"matching '{{if}}'. Appending else tag as-is.\n");
}
else if (strcmp(trimmed_tag_content, "endif") == 0) {
beaker_log("WARN", "render_template_segment: '{{endif}}' without "
"matching '{{if}}'. Appending endif 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++;
const 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) {
beaker_log_errno_format("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';
if (strstr(included_filename, "..") != NULL ||
strchr(included_filename, '/') != NULL) {
beaker_log("SECURITY",
"render_template_segment: Path traversal attempt in "
"include: %s\n",
included_filename);
free(included_filename);
append_to_buffer(
&rendered_buffer, &current_len, &max_len,
"<!-- Include blocked: path traversal not allowed -->");
current_pos = end_tag + 2;
free(tag_content_raw);
continue;
}
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 {
beaker_log("WARN",
"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 {
beaker_log("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 {
beaker_log("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 if (strncmp(trimmed_tag_content, "l(\"", 3) == 0) {
const char *key_start = trimmed_tag_content + 3;
const char *key_end = strstr(key_start, "\")");
if (key_end == NULL) {
beaker_log("ERROR",
"render_template_segment: Malformed l() tag '%s'. "
"Expected l(\"key\"). Appending as-is.\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;
}
size_t key_len = key_end - key_start;
char l10n_key[MAX_KEY_LEN];
if (key_len >= MAX_KEY_LEN)
key_len = MAX_KEY_LEN - 1;
strncpy(l10n_key, key_start, key_len);
l10n_key[key_len] = '\0';
bool is_safe = false;
const char *after_close = key_end + 2;
if (strncmp(after_close, "|safe", 5) == 0) {
is_safe = true;
}
const char *value_to_append = NULL;
ContextVar *locale_var = find_context_var(ctx, "__locale");
if (locale_var != NULL && locale_var->type == CONTEXT_TYPE_STRING) {
const char *current_locale_id = locale_var->value.string_val;
for (int i = 0; i < locale_count; i++) {
if (strcmp(locales[i].meta.id, current_locale_id) == 0) {
for (int j = 0; j < locales[i].key_count; j++) {
if (strcmp(locales[i].keys[j].key, l10n_key) == 0) {
value_to_append = locales[i].keys[j].value;
break;
}
}
break;
}
}
}
if (value_to_append != NULL) {
if (is_safe) {
append_to_buffer(&rendered_buffer, &current_len, &max_len,
value_to_append);
} else {
char *escaped = html_escape(value_to_append);
if (escaped) {
append_to_buffer(&rendered_buffer, &current_len, &max_len, escaped);
free(escaped);
} else {
append_to_buffer(&rendered_buffer, &current_len, &max_len,
value_to_append);
}
}
} else {
beaker_log("WARN",
"render_template_segment: l10n key '%s' not found "
"for current locale. Appending key as-is.\n",
l10n_key);
append_to_buffer(&rendered_buffer, &current_len, &max_len, l10n_key);
}
free(tag_content_raw);
current_pos = end_tag + 2;
continue;
}
else {
bool is_safe = false;
char *processing_tag_content = strdup(trimmed_tag_content);
if (processing_tag_content == NULL) {
beaker_log_errno("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) {
beaker_log("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 {
beaker_log(
"WARN",
"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 {
beaker_log("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 {
beaker_log(
"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 {
beaker_log("WARN",
"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 {
beaker_log("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];
if (strstr(template_file, "..") != NULL) {
beaker_log("SECURITY", "render_template: Path traversal attempt: %s\n",
template_file);
return NULL;
}
snprintf(full_path, sizeof(full_path), "%s%s", TEMPLATES_DIR, template_file);
FILE *fp = fopen(full_path, "r");
if (fp == NULL) {
beaker_log_errno_format(
"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);
if (file_size < 0) {
beaker_log_errno_format(
"ERROR", "render_template: Failed to get size of '%s'.\n", full_path);
fclose(fp);
return NULL;
}
fseek(fp, 0, SEEK_SET);
if (file_size == 0) {
char *empty_result = render_template_segment("", ctx);
fclose(fp);
return empty_result;
}
char *template_content = (char *)malloc((size_t)file_size + 1);
if (template_content == NULL) {
beaker_log_errno_format("ERROR",
"render_template: Failed to allocate %ld bytes for "
"template content.\n",
file_size + 1);
fclose(fp);
return NULL;
}
size_t bytes_read = fread(template_content, 1, (size_t)file_size, fp);
if (bytes_read != (size_t)file_size) {
beaker_log_errno_format(
"ERROR", "render_template: Failed to read complete template '%s'.\n",
full_path);
free(template_content);
fclose(fp);
return NULL;
}
template_content[file_size] = '\0';
fclose(fp);
char *rendered_html = render_template_segment(template_content, ctx);
free(template_content);
return rendered_html;
}