diff options
| -rw-r--r-- | beaker.h | 37 | ||||
| -rw-r--r-- | src/beaker_globals.c | 8 | ||||
| -rw-r--r-- | src/beaker_globals.h | 11 | ||||
| -rw-r--r-- | src/l10n.c | 278 | ||||
| -rw-r--r-- | src/server.c | 143 | ||||
| -rw-r--r-- | src/template.c | 128 |
6 files changed, 542 insertions, 63 deletions
@@ -20,6 +20,14 @@ #define TEMPLATES_DIR "templates/" #define STATIC_DIR "static/" +#define LOCALES_DIR "locales/" + +#define INITIAL_LOCALES_CAPACITY 8 +#define INITIAL_LOCALE_KEYS_CAPACITY 16 +#define MAX_LOCALES_HARD 1024 +#define MAX_LOCALE_KEYS_HARD 65536 +#define MAX_LOCALE_ID_LEN 64 +#define MAX_LOCALE_VALUE_LEN 512 typedef enum { CONTEXT_TYPE_STRING, @@ -68,6 +76,28 @@ typedef struct { bool secure; } Cookie; +typedef struct { + char id[MAX_LOCALE_ID_LEN]; + char name[MAX_VALUE_LEN]; + char direction[16]; +} LocaleMeta; + +typedef struct { + char key[MAX_KEY_LEN]; + char value[MAX_LOCALE_VALUE_LEN]; +} LocaleKV; + +typedef struct { + LocaleMeta meta; + LocaleKV *keys; + int key_count; + int key_capacity; +} Locale; + +typedef struct { + LocaleMeta meta; +} LocaleInfo; + typedef int (*RequestHandler)(UrlParams *params); typedef struct { @@ -85,6 +115,13 @@ void context_set_array_of_arrays(TemplateContext *ctx, const char *key, void free_context(TemplateContext *ctx); char *render_template(const char *template_file, TemplateContext *ctx); +int beaker_load_locales(void); +void beaker_set_locale(TemplateContext *ctx, const char *locale_id); +int beaker_get_all_locales(LocaleInfo *out, int max_count); +const LocaleMeta *beaker_get_locale_meta(const char *locale_id); +const char *beaker_get_locale_value(const char *locale_id, const char *key); +void beaker_free_locales(void); + void send_response(const char *html); void send_redirect(const char *location); void set_cookie(const char *name, const char *value, const char *expires, diff --git a/src/beaker_globals.c b/src/beaker_globals.c index 29c301e..1f81a5e 100644 --- a/src/beaker_globals.c +++ b/src/beaker_globals.c @@ -10,4 +10,10 @@ __thread Cookie cookies_to_set[MAX_COOKIES]; __thread int cookies_to_set_count = 0; -__thread char current_request_buffer[BUFFER_SIZE];
\ No newline at end of file +__thread char current_request_buffer[BUFFER_SIZE]; + +Locale *locales = NULL; + +int locale_count = 0; + +int locale_capacity = 0;
\ No newline at end of file diff --git a/src/beaker_globals.h b/src/beaker_globals.h index 7b727bc..7418808 100644 --- a/src/beaker_globals.h +++ b/src/beaker_globals.h @@ -1,7 +1,8 @@ -#ifndef BEAKER_GLOBALS_H +#ifndef BEAKER_GLOBALS_H #define BEAKER_GLOBALS_H -#include "../beaker.h" +#include "../beaker.h" +#include <stdatomic.h> extern RouteHandler handlers[MAX_HANDLERS]; @@ -15,4 +16,10 @@ extern __thread int cookies_to_set_count; extern __thread char current_request_buffer[BUFFER_SIZE]; +extern Locale *locales; + +extern int locale_count; + +extern int locale_capacity; + #endif
\ No newline at end of file diff --git a/src/l10n.c b/src/l10n.c new file mode 100644 index 0000000..4d8ac30 --- /dev/null +++ b/src/l10n.c @@ -0,0 +1,278 @@ +#include "../beaker.h" +#include "beaker_globals.h" +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +typedef enum { + SECTION_NONE, + SECTION_META, + SECTION_KEYS, +} IniSection; + +static char *trim(char *str) { + if (str == NULL) return NULL; + while (*str == ' ' || *str == '\t') str++; + if (*str == '\0') return str; + char *end = str + strlen(str) - 1; + while (end > str && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { + *end = '\0'; + end--; + } + return str; +} + +static void unquote_value(char *value) { + size_t len = strlen(value); + if (len >= 2 && value[0] == '"' && value[len - 1] == '"') { + memmove(value, value + 1, len - 2); + value[len - 2] = '\0'; + } +} + +static int safe_grow_capacity(int current, int hard_max) { + int new_cap; + if (current == 0) { + new_cap = (hard_max < INITIAL_LOCALE_KEYS_CAPACITY) ? hard_max : INITIAL_LOCALE_KEYS_CAPACITY; + } else { + new_cap = current * 2; + if (new_cap > hard_max) new_cap = hard_max; + } + return new_cap; +} + +static int parse_ini_file(const char *path, Locale *locale) { + FILE *fp = fopen(path, "r"); + if (fp == NULL) { + fprintf(stderr, "[ERROR] parse_ini_file: Could not open '%s': %s\n", + path, strerror(errno)); + return -1; + } + + memset(&locale->meta, 0, sizeof(locale->meta)); + locale->keys = NULL; + locale->key_count = 0; + locale->key_capacity = 0; + IniSection current_section = SECTION_NONE; + char line[BUFFER_SIZE]; + + while (fgets(line, sizeof(line), fp) != NULL) { + char *trimmed = trim(line); + + if (*trimmed == '\0' || *trimmed == ';' || *trimmed == '#') { + continue; + } + + if (*trimmed == '[') { + char *close = strchr(trimmed, ']'); + if (close == NULL) continue; + *close = '\0'; + char *section_name = trimmed + 1; + section_name = trim(section_name); + + if (strcmp(section_name, "Meta") == 0) { + current_section = SECTION_META; + } else if (strcmp(section_name, "Keys") == 0) { + current_section = SECTION_KEYS; + } else { + current_section = SECTION_NONE; + } + continue; + } + + char *eq = strchr(trimmed, '='); + if (eq == NULL) continue; + + *eq = '\0'; + char *key = trim(trimmed); + char *value = trim(eq + 1); + unquote_value(value); + + if (current_section == SECTION_META) { + if (strcmp(key, "Id") == 0) { + strncpy(locale->meta.id, value, MAX_LOCALE_ID_LEN - 1); + locale->meta.id[MAX_LOCALE_ID_LEN - 1] = '\0'; + } else if (strcmp(key, "Name") == 0) { + strncpy(locale->meta.name, value, MAX_VALUE_LEN - 1); + locale->meta.name[MAX_VALUE_LEN - 1] = '\0'; + } else if (strcmp(key, "Direction") == 0) { + strncpy(locale->meta.direction, value, sizeof(locale->meta.direction) - 1); + locale->meta.direction[sizeof(locale->meta.direction) - 1] = '\0'; + } + } else if (current_section == SECTION_KEYS) { + if (locale->key_count >= locale->key_capacity) { + int new_cap = safe_grow_capacity(locale->key_capacity, MAX_LOCALE_KEYS_HARD); + if (new_cap <= locale->key_count) { + fprintf(stderr, + "[WARNING] parse_ini_file: Hard key limit (%d) reached in '%s', " + "skipping key '%s'.\n", + MAX_LOCALE_KEYS_HARD, path, key); + continue; + } + LocaleKV *new_keys = realloc(locale->keys, (size_t)new_cap * sizeof(LocaleKV)); + if (new_keys == NULL) { + fprintf(stderr, + "[ERROR] parse_ini_file: Memory allocation failed for keys in '%s'.\n", + path); + continue; + } + locale->keys = new_keys; + locale->key_capacity = new_cap; + } + LocaleKV *kv = &locale->keys[locale->key_count]; + strncpy(kv->key, key, MAX_KEY_LEN - 1); + kv->key[MAX_KEY_LEN - 1] = '\0'; + strncpy(kv->value, value, MAX_LOCALE_VALUE_LEN - 1); + kv->value[MAX_LOCALE_VALUE_LEN - 1] = '\0'; + locale->key_count++; + } + } + + fclose(fp); + + if (locale->meta.id[0] == '\0') { + fprintf(stderr, + "[WARNING] parse_ini_file: No Id in [Meta] section of '%s'.\n", + path); + return -1; + } + + return 0; +} + +int beaker_load_locales(void) { + locale_count = 0; + + DIR *dir = opendir(LOCALES_DIR); + if (dir == NULL) { + fprintf(stderr, + "[ERROR] beaker_load_locales: Could not open directory '%s': %s\n", + LOCALES_DIR, strerror(errno)); + return 0; + } + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + const char *name = entry->d_name; + size_t name_len = strlen(name); + + if (name_len < 5 || strcmp(name + name_len - 4, ".ini") != 0) { + continue; + } + + if (locale_count >= locale_capacity) { + int new_cap = safe_grow_capacity(locale_capacity, MAX_LOCALES_HARD); + if (new_cap <= locale_count) { + fprintf(stderr, + "[WARNING] beaker_load_locales: Hard locale limit (%d) reached, " + "skipping '%s'.\n", + MAX_LOCALES_HARD, name); + break; + } + Locale *new_locales = realloc(locales, (size_t)new_cap * sizeof(Locale)); + if (new_locales == NULL) { + fprintf(stderr, + "[ERROR] beaker_load_locales: Memory allocation failed for locales.\n"); + break; + } + locales = new_locales; + locale_capacity = new_cap; + } + + char path[512]; + snprintf(path, sizeof(path), "%s%s", LOCALES_DIR, name); + + Locale *locale = &locales[locale_count]; + if (parse_ini_file(path, locale) == 0) { + fprintf(stderr, "[INFO] beaker_load_locales: Loaded locale '%s' from '%s'\n", + locale->meta.id, name); + locale_count++; + } else { + free(locale->keys); + locale->keys = NULL; + locale->key_count = 0; + locale->key_capacity = 0; + } + } + + closedir(dir); + return locale_count; +} + +void beaker_set_locale(TemplateContext *ctx, const char *locale_id) { + if (ctx == NULL || locale_id == NULL) { + fprintf(stderr, + "[ERROR] beaker_set_locale: Invalid NULL input (ctx or locale_id).\n"); + return; + } + + context_set(ctx, "__locale", locale_id); + + const LocaleMeta *meta = beaker_get_locale_meta(locale_id); + if (meta != NULL) { + context_set(ctx, "__locale_id", meta->id); + context_set(ctx, "__locale_name", meta->name); + context_set(ctx, "__locale_direction", meta->direction); + } else { + fprintf(stderr, + "[WARNING] beaker_set_locale: Locale '%s' not found. Context vars " + "not set.\n", + locale_id); + } +} + +int beaker_get_all_locales(LocaleInfo *out, int max_count) { + if (out == NULL || max_count <= 0) { + return 0; + } + + int count = (locale_count < max_count) ? locale_count : max_count; + for (int i = 0; i < count; i++) { + out[i].meta = locales[i].meta; + } + return count; +} + +const LocaleMeta *beaker_get_locale_meta(const char *locale_id) { + if (locale_id == NULL) { + return NULL; + } + for (int i = 0; i < locale_count; i++) { + if (strcmp(locales[i].meta.id, locale_id) == 0) { + return &locales[i].meta; + } + } + return NULL; +} + +const char *beaker_get_locale_value(const char *locale_id, const char *key) { + if (locale_id == NULL || key == NULL) { + return NULL; + } + for (int i = 0; i < locale_count; i++) { + if (strcmp(locales[i].meta.id, locale_id) == 0) { + for (int j = 0; j < locales[i].key_count; j++) { + if (strcmp(locales[i].keys[j].key, key) == 0) { + return locales[i].keys[j].value; + } + } + return NULL; + } + } + return NULL; +} + +void beaker_free_locales(void) { + for (int i = 0; i < locale_count; i++) { + free(locales[i].keys); + locales[i].keys = NULL; + locales[i].key_count = 0; + locales[i].key_capacity = 0; + } + free(locales); + locales = NULL; + locale_count = 0; + locale_capacity = 0; +} diff --git a/src/server.c b/src/server.c index caeb4bf..270be70 100644 --- a/src/server.c +++ b/src/server.c @@ -12,6 +12,9 @@ #include <unistd.h> #include <stdlib.h> #include <signal.h> +#include <fcntl.h> +#include <poll.h> +#include <errno.h> #define MAX_PENDING_CONNECTIONS 128 @@ -23,22 +26,28 @@ static void signal_handler(int sig) { } typedef struct { - int client_sockets[MAX_PENDING_CONNECTIONS]; - int count; - int front; - int rear; + _Atomic(size_t) sequence; + int socket; +} WorkSlot; + +typedef struct { + _Atomic(size_t) head; + _Atomic(size_t) tail; + _Atomic(int) shutdown; + WorkSlot slots[MAX_PENDING_CONNECTIONS]; pthread_mutex_t mutex; pthread_cond_t cond; - int shutdown; } WorkQueue; static WorkQueue g_work_queue; static void work_queue_init(WorkQueue *queue) { - queue->count = 0; - queue->front = 0; - queue->rear = 0; - queue->shutdown = 0; + atomic_store(&queue->head, 0); + atomic_store(&queue->tail, 0); + atomic_store(&queue->shutdown, 0); + for (int i = 0; i < MAX_PENDING_CONNECTIONS; i++) { + atomic_store(&queue->slots[i].sequence, (size_t)i); + } pthread_mutex_init(&queue->mutex, NULL); pthread_cond_init(&queue->cond, NULL); } @@ -49,40 +58,55 @@ static void work_queue_destroy(WorkQueue *queue) { } static int work_queue_push(WorkQueue *queue, int client_socket) { - pthread_mutex_lock(&queue->mutex); - - if (queue->count >= MAX_PENDING_CONNECTIONS) { - pthread_mutex_unlock(&queue->mutex); - return -1; + size_t tail = atomic_load(&queue->tail); + + for (;;) { + WorkSlot *slot = &queue->slots[tail % MAX_PENDING_CONNECTIONS]; + size_t seq = atomic_load(&slot->sequence); + intptr_t diff = (intptr_t)seq - (intptr_t)tail; + + if (diff == 0) { + if (atomic_compare_exchange_weak(&queue->tail, &tail, tail + 1)) { + slot->socket = client_socket; + atomic_store(&slot->sequence, tail + 1); + pthread_cond_signal(&queue->cond); + return 0; + } + } else if (diff < 0) { + return -1; + } else { + tail = atomic_load(&queue->tail); + } } - - queue->client_sockets[queue->rear] = client_socket; - queue->rear = (queue->rear + 1) % MAX_PENDING_CONNECTIONS; - queue->count++; - - pthread_cond_signal(&queue->cond); - pthread_mutex_unlock(&queue->mutex); - return 0; } static int work_queue_pop(WorkQueue *queue) { - pthread_mutex_lock(&queue->mutex); - - while (queue->count == 0 && !queue->shutdown) { - pthread_cond_wait(&queue->cond, &queue->mutex); - } - - if (queue->shutdown && queue->count == 0) { - pthread_mutex_unlock(&queue->mutex); - return -1; + size_t head = atomic_load(&queue->head); + + for (;;) { + if (atomic_load(&queue->shutdown)) return -1; + + WorkSlot *slot = &queue->slots[head % MAX_PENDING_CONNECTIONS]; + size_t seq = atomic_load(&slot->sequence); + intptr_t diff = (intptr_t)seq - (intptr_t)(head + 1); + + if (diff == 0) { + if (atomic_compare_exchange_weak(&queue->head, &head, head + 1)) { + int fd = slot->socket; + atomic_store(&slot->sequence, head + MAX_PENDING_CONNECTIONS); + return fd; + } + } else if (diff < 0) { + pthread_mutex_lock(&queue->mutex); + if (!atomic_load(&queue->shutdown) && atomic_load(&queue->head) == head) { + pthread_cond_wait(&queue->cond, &queue->mutex); + } + pthread_mutex_unlock(&queue->mutex); + head = atomic_load(&queue->head); + } else { + head = atomic_load(&queue->head); + } } - - int client_socket = queue->client_sockets[queue->front]; - queue->front = (queue->front + 1) % MAX_PENDING_CONNECTIONS; - queue->count--; - - pthread_mutex_unlock(&queue->mutex); - return client_socket; } static int get_optimal_thread_count(void) { @@ -149,6 +173,13 @@ static int initialize_server_socket(const char *ip, int port, int *server_fd_out return -1; } + int flags = fcntl(*server_fd_out, F_GETFL, 0); + if (flags < 0 || fcntl(*server_fd_out, F_SETFL, flags | O_NONBLOCK) < 0) { + perror("fcntl O_NONBLOCK failed"); + close(*server_fd_out); + return -1; + } + printf("Beaker server listening on %s:%d\n", ip, port); return 0; } @@ -295,32 +326,32 @@ void beaker_run_with_threads(const char *ip, int port, int num_workers) { printf("Beaker server started with %d worker threads\n", num_workers); - while (!g_shutdown_requested) { - int new_socket; + struct pollfd pfd = { .fd = server_fd, .events = POLLIN }; - if ((new_socket = accept(server_fd, (struct sockaddr *)&address, - (socklen_t *)&addrlen)) < 0) { - if (g_shutdown_requested) { - break; - } - perror("accept failed"); - fprintf(stderr, "[ERROR] beaker_run_with_threads: Failed to accept connection.\n"); - continue; + while (!g_shutdown_requested) { + int ret = poll(&pfd, 1, 1000); + if (ret < 0) { + if (errno == EINTR) continue; + perror("poll failed"); + break; } + if (ret == 0) continue; - if (work_queue_push(&g_work_queue, new_socket) < 0) { - fprintf(stderr, "[WARNING] Work queue full, closing connection\n"); - const char *busy_response = "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\n\r\n"; - send(new_socket, busy_response, strlen(busy_response), 0); - close(new_socket); + int new_socket; + while ((new_socket = accept(server_fd, (struct sockaddr *)&address, + (socklen_t *)&addrlen)) >= 0) { + if (work_queue_push(&g_work_queue, new_socket) < 0) { + fprintf(stderr, "[WARNING] Work queue full, closing connection\n"); + const char *busy_response = "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\n\r\n"; + send(new_socket, busy_response, strlen(busy_response), 0); + close(new_socket); + } } } printf("Shutting down server...\n"); - pthread_mutex_lock(&g_work_queue.mutex); - g_work_queue.shutdown = 1; - pthread_mutex_unlock(&g_work_queue.mutex); + atomic_store(&g_work_queue.shutdown, 1); pthread_cond_broadcast(&g_work_queue.cond); for (int i = 0; i < num_workers; i++) { diff --git a/src/template.c b/src/template.c index 76e8662..6255d1b 100644 --- a/src/template.c +++ b/src/template.c @@ -1,4 +1,5 @@ #include "../beaker.h" +#include "beaker_globals.h" #include <errno.h> #include <stdbool.h> #include <stdio.h> @@ -530,6 +531,7 @@ typedef struct { ConditionType type; char var_name[MAX_KEY_LEN]; char compare_value[MAX_VALUE_LEN]; + int compare_index; bool negate; } Condition; @@ -564,11 +566,45 @@ static bool evaluate_condition(const Condition *cond, TemplateContext *ctx) { return cond->negate ? !exists : exists; } - case CONDITION_EQUAL: - return (strcmp(var_value, cond->compare_value) == 0); + case CONDITION_EQUAL: { + const char *compare_value = cond->compare_value; + char compare_buf[MAX_VALUE_LEN]; + 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 >= 0 && + 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 (strcmp(var_value, compare_value) == 0); + } - case CONDITION_NOT_EQUAL: - return (strcmp(var_value, cond->compare_value) != 0); + case CONDITION_NOT_EQUAL: { + const char *compare_value = cond->compare_value; + char compare_buf[MAX_VALUE_LEN]; + 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 >= 0 && + 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 (strcmp(var_value, compare_value) != 0); + } default: return false; @@ -580,6 +616,7 @@ static Condition parse_condition(const char *condition_str) { 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') { @@ -707,6 +744,15 @@ static Condition parse_condition(const char *condition_str) { end--; } + 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; } @@ -1184,6 +1230,80 @@ static char *render_template_segment(const char *template_segment, } } + 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) { + fprintf(stderr, + "[ERROR] render_template_segment: Malformed l() tag '%s'. " + "Expected l(\"key\"). Appending as-is.\n", + trimmed_tag_content); + append_to_buffer(&rendered_buffer, ¤t_len, &max_len, "{{"); + append_to_buffer(&rendered_buffer, ¤t_len, &max_len, + trimmed_tag_content); + append_to_buffer(&rendered_buffer, ¤t_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, ¤t_len, &max_len, + value_to_append); + } else { + char *escaped = html_escape(value_to_append); + if (escaped) { + append_to_buffer(&rendered_buffer, ¤t_len, &max_len, escaped); + free(escaped); + } else { + append_to_buffer(&rendered_buffer, ¤t_len, &max_len, + value_to_append); + } + } + } else { + fprintf(stderr, + "[WARNING] render_template_segment: l10n key '%s' not found " + "for current locale. Appending key as-is.\n", + l10n_key); + append_to_buffer(&rendered_buffer, ¤t_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); |
