ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
log/format.c
Go to the documentation of this file.
1
7#include <ascii-chat/log/format.h>
8#include <ascii-chat/log/colorize.h>
9#include <ascii-chat/util/string.h>
10#include <ascii-chat/util/utf8.h>
11#include <ascii-chat/util/time.h>
12#include <ascii-chat/util/path.h>
13#include <ascii-chat/common.h>
14#include <ascii-chat/log/logging.h>
15#include <stdio.h>
16#include <stdlib.h>
17#include <string.h>
18#include <ctype.h>
19#include <time.h>
20
21/* ============================================================================
22 * Default Format String (mode-aware)
23 * ============================================================================ */
24
25/* ============================================================================
26 * Format Parser Implementation
27 * ============================================================================ */
28
42static log_template_t *parse_format_string(const char *format_str, bool console_only) {
43 if (!format_str) {
44 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid format string: %s", format_str);
45 return NULL;
46 }
47
48 if (!utf8_is_valid(format_str)) {
49 SET_ERRNO(ERROR_INVALID_STATE, "Invalid UTF-8 in log format string");
50 return NULL;
51 }
52
53 log_template_t *result = SAFE_CALLOC(1, sizeof(log_template_t), log_template_t *);
54 result->original = SAFE_MALLOC(strlen(format_str) + 1, char *);
55 strcpy(result->original, format_str);
56 result->console_only = console_only;
57
58 /* Pre-allocate spec array (worst case: every char is a specifier) */
59 result->specs = SAFE_CALLOC(strlen(format_str) + 1, sizeof(log_format_spec_t), log_format_spec_t *);
60 if (!result->specs) {
61 SAFE_FREE(result->original);
62 SAFE_FREE(result);
63 return NULL;
64 }
65
66 const char *p = format_str;
67 size_t spec_idx = 0;
68
69 while (*p) {
70 /* Check for escape sequences first */
71 if (*p == '\\' && *(p + 1)) {
72 char next = *(p + 1);
73
74 if (next == 'n') {
75 /* \n - platform-aware newline */
76 result->specs[spec_idx].type = LOG_FORMAT_NEWLINE;
77 spec_idx++;
78 p += 2;
79 } else if (next == '\\') {
80 /* \\ - escaped backslash (output single \‍) */
81 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
82 result->specs[spec_idx].literal = SAFE_MALLOC(2, char *);
83 if (!result->specs[spec_idx].literal) {
84 goto cleanup;
85 }
86 result->specs[spec_idx].literal[0] = '\\';
87 result->specs[spec_idx].literal[1] = '\0';
88 result->specs[spec_idx].literal_len = 1;
89 spec_idx++;
90 p += 2;
91 } else {
92 /* Invalid escape sequence - treat backslash as literal */
93 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
94 result->specs[spec_idx].literal = SAFE_MALLOC(2, char *);
95 if (!result->specs[spec_idx].literal) {
96 goto cleanup;
97 }
98 result->specs[spec_idx].literal[0] = '\\';
99 result->specs[spec_idx].literal[1] = '\0';
100 result->specs[spec_idx].literal_len = 1;
101 spec_idx++;
102 p++;
103 }
104 } else if (*p == '%' && *(p + 1)) {
105 if (*(p + 1) == '%') {
106 /* %% - escaped percent (output single %) */
107 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
108 result->specs[spec_idx].literal = SAFE_MALLOC(2, char *);
109 if (!result->specs[spec_idx].literal) {
110 goto cleanup;
111 }
112 result->specs[spec_idx].literal[0] = '%';
113 result->specs[spec_idx].literal[1] = '\0';
114 result->specs[spec_idx].literal_len = 1;
115 spec_idx++;
116 p += 2;
117 } else {
118 /* Start of format specifier */
119 p++;
120
121 if (strncmp(p, "time(", 5) == 0) {
122 /* Parse %time(format) */
123 p += 5;
124 const char *fmt_start = p;
125 const char *fmt_end = strchr(p, ')');
126 if (!fmt_end) {
127 /* Parse error: unterminated time format */
128 log_error("Invalid %%time format: missing closing )");
129 goto cleanup;
130 }
131
132 result->specs[spec_idx].type = LOG_FORMAT_TIME;
133 size_t fmt_len = fmt_end - fmt_start;
134 result->specs[spec_idx].literal = SAFE_MALLOC(fmt_len + 1, char *);
135 if (!result->specs[spec_idx].literal) {
136 goto cleanup;
137 }
138 memcpy(result->specs[spec_idx].literal, fmt_start, fmt_len);
139 result->specs[spec_idx].literal[fmt_len] = '\0';
140 result->specs[spec_idx].literal_len = fmt_len;
141
142 spec_idx++;
143 p = fmt_end + 1;
144 } else if (strncmp(p, "level_aligned", 13) == 0) {
145 result->specs[spec_idx].type = LOG_FORMAT_LEVEL_ALIGNED;
146 spec_idx++;
147 p += 13;
148 } else if (strncmp(p, "level", 5) == 0) {
149 result->specs[spec_idx].type = LOG_FORMAT_LEVEL;
150 spec_idx++;
151 p += 5;
152 } else if (strncmp(p, "file_relative", 13) == 0) {
153 result->specs[spec_idx].type = LOG_FORMAT_FILE_RELATIVE;
154 spec_idx++;
155 p += 13;
156 } else if (strncmp(p, "file", 4) == 0) {
157 result->specs[spec_idx].type = LOG_FORMAT_FILE;
158 spec_idx++;
159 p += 4;
160 } else if (strncmp(p, "line", 4) == 0) {
161 result->specs[spec_idx].type = LOG_FORMAT_LINE;
162 spec_idx++;
163 p += 4;
164 } else if (strncmp(p, "func", 4) == 0) {
165 result->specs[spec_idx].type = LOG_FORMAT_FUNC;
166 spec_idx++;
167 p += 4;
168 } else if (strncmp(p, "tid", 3) == 0) {
169 result->specs[spec_idx].type = LOG_FORMAT_TID;
170 spec_idx++;
171 p += 3;
172 } else if (strncmp(p, "colored_message", 15) == 0) {
173 result->specs[spec_idx].type = LOG_FORMAT_COLORED_MESSAGE;
174 spec_idx++;
175 p += 15;
176 } else if (strncmp(p, "ms", 2) == 0) {
177 result->specs[spec_idx].type = LOG_FORMAT_MICROSECONDS;
178 spec_idx++;
179 p += 2;
180 } else if (strncmp(p, "ns", 2) == 0) {
181 result->specs[spec_idx].type = LOG_FORMAT_NANOSECONDS;
182 spec_idx++;
183 p += 2;
184 } else if (strncmp(p, "message", 7) == 0) {
185 result->specs[spec_idx].type = LOG_FORMAT_MESSAGE;
186 spec_idx++;
187 p += 7;
188 } else if (strncmp(p, "colorlog_level_string_to_color", 30) == 0) {
189 result->specs[spec_idx].type = LOG_FORMAT_COLORLOG_LEVEL;
190 spec_idx++;
191 p += 30;
192 } else if (strncmp(p, "color(", 6) == 0) {
193 /* Parse %color(LEVEL, content) */
194 p += 6; /* Skip "color(" */
195
196 /* Find the matching closing paren */
197 int paren_depth = 1;
198 const char *color_start = p;
199 while (*p && paren_depth > 0) {
200 if (*p == '(')
201 paren_depth++;
202 else if (*p == ')')
203 paren_depth--;
204 if (paren_depth > 0)
205 p++;
206 }
207
208 if (!*p || paren_depth != 0) {
209 /* Parse error: unterminated color format */
210 log_error("Invalid %%color format: missing closing )");
211 goto cleanup;
212 }
213
214 size_t color_arg_len = p - color_start;
215
216 /* Store as "LEVEL|content" for later parsing */
217 result->specs[spec_idx].type = LOG_FORMAT_COLOR;
218 result->specs[spec_idx].literal = SAFE_MALLOC(color_arg_len + 1, char *);
219 if (!result->specs[spec_idx].literal) {
220 goto cleanup;
221 }
222 memcpy(result->specs[spec_idx].literal, color_start, color_arg_len);
223 result->specs[spec_idx].literal[color_arg_len] = '\0';
224 result->specs[spec_idx].literal_len = color_arg_len;
225
226 spec_idx++;
227 p++; /* Skip closing paren */
228 } else {
229 /* Unknown ascii-chat specifier - treat as strftime format code and pass to strftime
230 * This allows strftime formats like %H, %M, %S, %A, %B, etc. to work
231 * without needing custom parsing for each one. strftime will handle validation. */
232 const char *fmt_start = p;
233 size_t fmt_len = 0;
234
235 /* Collect the format code (usually 1-2 chars, but allow flexibility) */
236 while (*p && *p != '%' && *p != '\\' && fmt_len < 8) {
237 p++;
238 fmt_len++;
239 }
240
241 if (fmt_len == 0) {
242 log_error("Empty format specifier: %%");
243 goto cleanup;
244 }
245
246 result->specs[spec_idx].type = LOG_FORMAT_STRFTIME_CODE;
247 result->specs[spec_idx].literal = SAFE_MALLOC(fmt_len + 1, char *);
248 if (!result->specs[spec_idx].literal) {
249 goto cleanup;
250 }
251 memcpy(result->specs[spec_idx].literal, fmt_start, fmt_len);
252 result->specs[spec_idx].literal[fmt_len] = '\0';
253 result->specs[spec_idx].literal_len = fmt_len;
254
255 spec_idx++;
256 /* p already advanced in the while loop above */
257 }
258 }
259 } else {
260 /* Literal text until next escape or specifier */
261 const char *text_start = p;
262 while (*p && *p != '\\' && *p != '%') {
263 p++;
264 }
265
266 result->specs[spec_idx].type = LOG_FORMAT_LITERAL;
267 size_t text_len = p - text_start;
268 result->specs[spec_idx].literal = SAFE_MALLOC(text_len + 1, char *);
269 if (!result->specs[spec_idx].literal) {
270 goto cleanup;
271 }
272
273 /* Safe memcpy for UTF-8 literal text (already validated) */
274 memcpy(result->specs[spec_idx].literal, text_start, text_len);
275 result->specs[spec_idx].literal[text_len] = '\0';
276 result->specs[spec_idx].literal_len = text_len;
277 spec_idx++;
278 }
279 }
280
281 result->spec_count = spec_idx;
282 return result;
283
284cleanup:
285 log_template_free(result);
286 return NULL;
287}
288
289log_template_t *log_template_parse(const char *format_str, bool console_only) {
290 return parse_format_string(format_str, console_only);
291}
292
293void log_template_free(log_template_t *format) {
294 if (!format) {
295 SET_ERRNO(ERROR_INVALID_PARAM, "null format");
296 return;
297 }
298
299 if (format->specs) {
300 for (size_t i = 0; i < format->spec_count; i++) {
301 if (format->specs[i].literal) {
302 SAFE_FREE(format->specs[i].literal);
303 }
304 }
305 SAFE_FREE(format->specs);
306 }
307
308 if (format->original) {
309 SAFE_FREE(format->original);
310 }
311
312 SAFE_FREE(format);
313}
314
315/* ============================================================================
316 * Format Application Implementation
317 * ============================================================================ */
318
327// get_level_string_padded is declared in logging.h and defined in logging.c
328
335static const char *get_level_string(log_level_t level) {
336 switch (level) {
337 case LOG_DEV:
338 return "DEV";
339 case LOG_DEBUG:
340 return "DEBUG";
341 case LOG_INFO:
342 return "INFO";
343 case LOG_WARN:
344 return "WARN";
345 case LOG_ERROR:
346 return "ERROR";
347 case LOG_FATAL:
348 return "FATAL";
349 default:
350 return "";
351 }
352}
353
366static log_color_t parse_color_level(const char *level_name, log_level_t current_level) {
367 if (!level_name) {
368 return LOG_COLOR_RESET;
369 }
370
371 /* Special case: "*" means use the current log level */
372 if (strcmp(level_name, "*") == 0) {
373 switch (current_level) {
374 case LOG_DEV:
375 return LOG_COLOR_DEV;
376 case LOG_DEBUG:
377 return LOG_COLOR_DEBUG;
378 case LOG_INFO:
379 return LOG_COLOR_INFO;
380 case LOG_WARN:
381 return LOG_COLOR_WARN;
382 case LOG_ERROR:
383 return LOG_COLOR_ERROR;
384 case LOG_FATAL:
385 return LOG_COLOR_FATAL;
386 default:
387 return LOG_COLOR_RESET;
388 }
389 }
390
391 /* Log level names */
392 if (strcmp(level_name, "DEV") == 0) {
393 return LOG_COLOR_DEV;
394 } else if (strcmp(level_name, "DEBUG") == 0) {
395 return LOG_COLOR_DEBUG;
396 } else if (strcmp(level_name, "INFO") == 0) {
397 return LOG_COLOR_INFO;
398 } else if (strcmp(level_name, "WARN") == 0) {
399 return LOG_COLOR_WARN;
400 } else if (strcmp(level_name, "ERROR") == 0) {
401 return LOG_COLOR_ERROR;
402 } else if (strcmp(level_name, "FATAL") == 0) {
403 return LOG_COLOR_FATAL;
404 }
405
406 /* Literal color names */
407 if (strcmp(level_name, "GREY") == 0 || strcmp(level_name, "GRAY") == 0) {
408 return LOG_COLOR_GREY;
409 }
410
411 return LOG_COLOR_RESET;
412}
413
434static int render_format_content(const char *content, char *buf, size_t buf_size, log_level_t level,
435 const char *timestamp, const char *file, int line, const char *func, uint64_t tid,
436 const char *message, bool use_colors, uint64_t time_nanoseconds) {
437 if (!content || !buf || buf_size == 0) {
438 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments: content=%p, buf=%p, buf_size=%zu", content, buf, buf_size);
439 return -1;
440 }
441
442 /* Parse content as a format string and apply it */
443 log_template_t *content_format = log_template_parse(content, false);
444 if (!content_format) {
445 return -1;
446 }
447
448 int written = log_template_apply(content_format, buf, buf_size, level, timestamp, file, line, func, tid, message,
449 use_colors, time_nanoseconds);
450 log_template_free(content_format);
451
452 return written;
453}
454
455int log_template_apply(const log_template_t *format, char *buf, size_t buf_size, log_level_t level,
456 const char *timestamp, const char *file, int line, const char *func, uint64_t tid,
457 const char *message, bool use_colors, uint64_t time_nanoseconds) {
458 if (!format || !buf || buf_size == 0) {
459 return -1;
460 }
461
462 int total_written = 0;
463 char *p = buf;
464 size_t remaining = buf_size - 1; /* Reserve space for null terminator */
465
466 (void)timestamp; /* May be used in future; for now, LOG_FORMAT_TIME uses custom formatting */
467
468 for (size_t i = 0; i < format->spec_count; i++) {
469 const log_format_spec_t *spec = &format->specs[i];
470 int written = 0;
471
472 switch (spec->type) {
473 case LOG_FORMAT_LITERAL:
474 written = safe_snprintf(p, remaining + 1, "%s", spec->literal ? spec->literal : "");
475 break;
476
477 case LOG_FORMAT_TIME:
478 /* Format time using custom time formatter */
479 if (spec->literal) {
480 written = time_format_now(spec->literal, p, remaining + 1);
481 if (written <= 0) {
482 log_debug("time_format_now failed for format: %s", spec->literal);
483 written = 0;
484 }
485 }
486 break;
487
488 case LOG_FORMAT_LEVEL:
489 /* Log level as string (DEV, DEBUG, INFO, WARN, ERROR, FATAL) */
490 written = safe_snprintf(p, remaining + 1, "%s", get_level_string(level));
491 break;
492
493 case LOG_FORMAT_LEVEL_ALIGNED:
494 /* Log level padded to exactly 5 characters (prevents truncation to [DEBU]/[ERRO]) */
495 written = safe_snprintf(p, remaining + 1, "%s", get_level_string_padded(level));
496 break;
497
498 case LOG_FORMAT_FILE:
499 if (file) {
500 written = safe_snprintf(p, remaining + 1, "%s", file);
501 }
502 break;
503
504 case LOG_FORMAT_FILE_RELATIVE:
505 if (file) {
506 const char *rel_file = extract_project_relative_path(file);
507 written = safe_snprintf(p, remaining + 1, "%s", rel_file);
508 }
509 break;
510
511 case LOG_FORMAT_LINE:
512 if (line > 0) {
513 written = safe_snprintf(p, remaining + 1, "%d", line);
514 }
515 break;
516
517 case LOG_FORMAT_FUNC:
518 if (func) {
519 written = safe_snprintf(p, remaining + 1, "%s", func);
520 }
521 break;
522
523 case LOG_FORMAT_TID:
524 written = safe_snprintf(p, remaining + 1, "%llu", (unsigned long long)tid);
525 break;
526
527 case LOG_FORMAT_MICROSECONDS: {
528 /* Extract microseconds from nanoseconds (ns_value % 1_000_000_000 / 1000) */
529 long nanoseconds = (long)(time_nanoseconds % NS_PER_SEC_INT);
530 long microseconds = nanoseconds / 1000;
531 if (microseconds < 0)
532 microseconds = 0;
533 if (microseconds > 999999)
534 microseconds = 999999;
535 written = safe_snprintf(p, remaining + 1, "%06ld", microseconds);
536 break;
537 }
538
539 case LOG_FORMAT_NANOSECONDS: {
540 /* Extract nanoseconds component (ns_value % 1_000_000_000) */
541 long nanoseconds = (long)(time_nanoseconds % NS_PER_SEC_INT);
542 if (nanoseconds < 0)
543 nanoseconds = 0;
544 if (nanoseconds > 999999999)
545 nanoseconds = 999999999;
546 written = safe_snprintf(p, remaining + 1, "%09ld", nanoseconds);
547 break;
548 }
549
550 case LOG_FORMAT_STRFTIME_CODE: {
551 /* strftime format code (like %H, %M, %S, %A, %B, etc.)
552 * Let strftime handle all the parsing and validation */
553 if (spec->literal) {
554 /* Construct format string with % prefix */
555 size_t fmt_len = spec->literal_len + 1;
556 char *format_str = SAFE_MALLOC(fmt_len + 1, char *);
557 if (format_str) {
558 format_str[0] = '%';
559 memcpy(format_str + 1, spec->literal, spec->literal_len);
560 format_str[fmt_len] = '\0';
561 written = time_format_now(format_str, p, remaining + 1);
562 if (written <= 0) {
563 log_debug("time_format_now failed for format code: %s", format_str);
564 written = 0;
565 }
566 SAFE_FREE(format_str);
567 }
568 }
569 break;
570 }
571
572 case LOG_FORMAT_MESSAGE:
573 if (message) {
574 written = safe_snprintf(p, remaining + 1, "%s", message);
575 }
576 break;
577
578 case LOG_FORMAT_COLORED_MESSAGE: {
579 /* Apply colorize_log_message() for number/unit/hex highlighting (keeps text white) */
580 if (message) {
581 const char *colorized_msg = colorize_log_message(message);
582 written = safe_snprintf(p, remaining + 1, "%s", colorized_msg);
583 }
584 break;
585 }
586
587 case LOG_FORMAT_COLORLOG_LEVEL:
588 /* Color code for the log level (placeholder for future color support) */
589 (void)use_colors; /* Suppress unused parameter warning */
590 written = 0;
591 break;
592
593 case LOG_FORMAT_COLOR: {
594 /* Parse and apply %color(LEVEL, content)
595 * literal stores "LEVEL,content" where LEVEL is the color level name (or "*" for current level)
596 * and content is a format string to render and colorize */
597 if (!spec->literal || spec->literal_len == 0) {
598 written = 0;
599 break;
600 }
601
602 /* Find the comma separating LEVEL and content */
603 const char *comma_pos = strchr(spec->literal, ',');
604 if (!comma_pos) {
605 /* Invalid format - no comma found */
606 log_debug("log_template_apply: %%color format missing comma in: %s", spec->literal);
607 written = 0;
608 break;
609 }
610
611 /* Extract level name (before comma) */
612 size_t level_len = comma_pos - spec->literal;
613 char level_name[32];
614 if (level_len >= sizeof(level_name)) {
615 /* Level name too long */
616 written = 0;
617 break;
618 }
619 memcpy(level_name, spec->literal, level_len);
620 level_name[level_len] = '\0';
621
622 /* Parse level name to log_color_t (pass current level for "*" support) */
623 log_color_t color = parse_color_level(level_name, level);
624
625 /* Extract content (after comma), skip leading whitespace */
626 const char *content_start = comma_pos + 1;
627 while (*content_start == ' ' || *content_start == '\t') {
628 content_start++;
629 }
630
631 /* Render content recursively */
632 char content_buf[512];
633 int content_len = render_format_content(content_start, content_buf, sizeof(content_buf), level, timestamp, file,
634 line, func, tid, message, use_colors, time_nanoseconds);
635
636 if (content_len < 0 || content_len >= (int)sizeof(content_buf)) {
637 written = 0;
638 break;
639 }
640
641 /* Apply color to rendered content using colored_string */
642 const char *colored_content = colored_string(color, content_buf);
643
644 /* Copy colored content to output buffer */
645 written = safe_snprintf(p, remaining + 1, "%s", colored_content);
646 break;
647 }
648
649 case LOG_FORMAT_NEWLINE:
650 /* Platform-aware newline */
651#ifdef _WIN32
652 written = safe_snprintf(p, remaining + 1, "\r\n");
653#else
654 written = safe_snprintf(p, remaining + 1, "\n");
655#endif
656 break;
657
658 default:
659 break;
660 }
661
662 if (written < 0) {
663 /* snprintf error */
664 return -1;
665 }
666
667 if ((size_t)written > remaining) {
668 /* Buffer overflow prevention */
669 log_debug("log_template_apply: buffer overflow prevented");
670 return -1;
671 }
672
673 p += written;
674 remaining -= written;
675 total_written += written;
676 }
677
678 *p = '\0';
679 return total_written;
680}
const char * colorize_log_message(const char *message)
Colorize a log message for terminal output.
Definition colorize.c:513
int log_template_apply(const log_template_t *format, char *buf, size_t buf_size, log_level_t level, const char *timestamp, const char *file, int line, const char *func, uint64_t tid, const char *message, bool use_colors, uint64_t time_nanoseconds)
Definition log/format.c:455
void log_template_free(log_template_t *format)
Definition log/format.c:293
log_template_t * log_template_parse(const char *format_str, bool console_only)
Definition log/format.c:289
const char * get_level_string_padded(log_level_t level)
Get padded level string for consistent alignment.
const char * extract_project_relative_path(const char *file)
Definition path.c:410
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
bool utf8_is_valid(const char *str)
Definition utf8.c:158
const char * colored_string(log_color_t color, const char *text)
int time_format_now(const char *format_str, char *buf, size_t buf_size)
Definition util/time.c:558