Branch data Line data Source code
1 : : #include "rational.h"
2 : : #include <errno.h>
3 : : #include <limits.h>
4 : : #include <stdint.h>
5 : : #include <stdarg.h>
6 : : #include <stdlib.h>
7 : : #include <stdio.h>
8 : : #include <string.h>
9 : :
10 : : // Global flag to manage output of all logging messages
11 : : // in an application and its default value
12 : : _Atomic LOGMODES rational_logger_mode = REGULAR;
13 : :
14 : : /* Keep only the logger's optional reference weak. Program implementations of
15 : : the REMEMBER callback remain strong symbols */
16 : : extern void rational_remember(
17 : : const char *,
18 : : const int) __attribute__((weak));
19 : :
20 : : /**
21 : : * @brief Converts LOGMODES bit flags to their string representation
22 : : *
23 : : * @details This function takes a combination of LOGMODES flags and converts them
24 : : * into a human-readable string representation where individual flags
25 : : * are separated by " | ". For example, (VERBOSE | SILENT) will be
26 : : * converted to "VERBOSE | SILENT"
27 : : *
28 : : * @param mode Combination of LOGMODES flags
29 : : * @return char* Pointer to static string containing flag names
30 : : *
31 : : * @note The function uses a static buffer which means:
32 : : * 1. No memory allocation/deallocation is needed
33 : : * 2. The buffer contents will be overwritten on next function call
34 : : * 3. The function is not thread-safe
35 : : * 4. The returned pointer should not be freed
36 : : *
37 : : * @warning Maximum resulting string length is limited to 256 characters
38 : : */
39 : 2246 : char *rational_reconvert(LOGMODES mode)
40 : : {
41 : : /* Static buffer to store the resulting string */
42 : : static char buffer[MAX_CHARACTERS];
43 : 2246 : buffer[0] = '\0'; /* Initialize buffer as empty string */
44 : :
45 : : /* Flag to track if we're adding the first item (for | separator) */
46 : 2246 : int first = 1;
47 : :
48 : : /* Define mapping between flag values and their string representations
49 : : * The array is terminated with {0, NULL} for easy iteration
50 : : */
51 : : static const struct {
52 : : LOGMODES flag; /* Flag value from LOGMODES constants */
53 : : const char *name; /* String representation of the flag */
54 : : } mapping[] = {
55 : : {REGULAR,"REGULAR"},
56 : : {VERBOSE,"VERBOSE"},
57 : : {TESTING,"TESTING"},
58 : : {ERROR,"ERROR"},
59 : : {SILENT,"SILENT"},
60 : : {UNDECOR,"UNDECOR"},
61 : : {REMEMBER,"REMEMBER"},
62 : : {VISIBLE_IN_SILENT,"VISIBLE_IN_SILENT"},
63 : : {0,NULL} /* Terminator element */
64 : : };
65 : :
66 : : /* Iterate through all possible flags */
67 [ + + ]: 20214 : for(int i = 0; mapping[i].name != NULL; i++)
68 : : {
69 : : /* Check if current flag is set in mode using bitwise AND */
70 [ + + ]: 17968 : if(mode & mapping[i].flag)
71 : : {
72 : : /* Add separator before all elements except the first one */
73 [ + + ]: 2262 : if(!first)
74 : : {
75 : 17 : strcat(buffer," | ");
76 : : }
77 : :
78 : : /* Add flag name to the result string */
79 : 2262 : strcat(buffer,mapping[i].name);
80 : :
81 : : /* Clear first flag as we've added an element */
82 : 2262 : first = 0;
83 : : }
84 : : }
85 : :
86 : 2246 : return buffer;
87 : : }
88 : :
89 : : /**
90 : : *
91 : : * @brief Format current date and time in ISO format
92 : : * @param time_string Pointer to the destination buffer that receives the timestamp.
93 : : * @param buffer_size Size of the destination buffer in bytes.
94 : : * @return Return SUCCESS on success, FAILURE on error (buffer contents will be empty on failure).
95 : : *
96 : : */
97 : 796 : static Return logger_show_time(
98 : : char *time_string,
99 : : size_t buffer_size)
100 : : {
101 : : /* Status returned by this function through provide()
102 : : Default value assumes successful completion */
103 : 796 : Return status = SUCCESS;
104 : :
105 : : struct timeval current_time;
106 : : struct tm local_time;
107 : :
108 [ + + ]: 796 : if(gettimeofday(¤t_time,NULL) != 0)
109 : : {
110 : 1 : time_string[0] = '\0';
111 : 1 : status = FAILURE;
112 : : }
113 : :
114 [ + + ]: 796 : if(SUCCESS == status)
115 : : {
116 [ + + ]: 795 : if(localtime_r(¤t_time.tv_sec,&local_time) == NULL)
117 : : {
118 : 1 : time_string[0] = '\0';
119 : 1 : status = FAILURE;
120 : : }
121 : : }
122 : :
123 [ + + ]: 796 : if(SUCCESS == status)
124 : : {
125 : 794 : const int milliseconds = (int)(current_time.tv_usec / 1000);
126 : :
127 : 794 : if(snprintf(time_string,
128 : : buffer_size,
129 : : "%04d-%02d-%02d %02d:%02d:%02d:%03d",
130 : 794 : local_time.tm_year + 1900,
131 [ + + ]: 794 : local_time.tm_mon + 1,
132 : : local_time.tm_mday,
133 : : local_time.tm_hour,
134 : : local_time.tm_min,
135 : : local_time.tm_sec,
136 : : milliseconds) < 0)
137 : : {
138 : 2 : time_string[0] = '\0';
139 : 2 : status = FAILURE;
140 : : }
141 : : }
142 : :
143 : 796 : return(status);
144 : : }
145 : :
146 : : /**
147 : : * @brief Append one printf payload while preserving the caller's errno
148 : : *
149 : : * @param[in,out] line Allocated destination line
150 : : * @param[in,out] line_len Current destination length
151 : : * @param[in] fmt Printf format string
152 : : * @param args Arguments associated with @p fmt
153 : : * @return true when the complete payload was appended, otherwise false
154 : : */
155 : : __attribute__((format(printf,3,0)))
156 : 76895 : static bool logger_line_append_va(
157 : : char **line,
158 : : int *line_len,
159 : : const char *fmt,
160 : : va_list args)
161 : : {
162 [ + - + - : 76895 : if(line == NULL || line_len == NULL || *line_len < 0 || fmt == NULL)
+ - - + ]
163 : : {
164 : 0 : return(false);
165 : : }
166 : :
167 : 76895 : const int format_errno = errno;
168 : : va_list args_copy;
169 : 76895 : va_copy(args_copy,args);
170 : 76895 : errno = format_errno;
171 : 76895 : const int needed = vsnprintf(NULL,0,fmt,args_copy);
172 : 76895 : va_end(args_copy);
173 : 76895 : errno = format_errno;
174 : :
175 [ + + - + ]: 76895 : if(needed < 0 || needed > INT_MAX - *line_len)
176 : : {
177 : 12 : return(false);
178 : : }
179 : :
180 : 76883 : const size_t new_len = (size_t)(*line_len) + (size_t)needed;
181 : 76883 : char *tmp = realloc(*line,new_len + 1);
182 : :
183 [ + + ]: 76883 : if(tmp == NULL)
184 : : {
185 : 11 : return(false);
186 : : }
187 : :
188 : 76872 : *line = tmp;
189 : :
190 : : va_list args_copy2;
191 : 76872 : va_copy(args_copy2,args);
192 : 76872 : errno = format_errno;
193 : 76872 : const int written = vsnprintf(*line + *line_len,(size_t)needed + 1U,fmt,args_copy2);
194 : 76872 : va_end(args_copy2);
195 : 76872 : errno = format_errno;
196 : :
197 [ - + ]: 76872 : if(written != needed)
198 : : {
199 : 0 : (*line)[*line_len] = '\0';
200 : 0 : return(false);
201 : : }
202 : :
203 : 76872 : *line_len = (int)new_len;
204 : 76872 : return(true);
205 : : }
206 : :
207 : : /**
208 : : * @brief Append one variadic printf payload to a log line
209 : : *
210 : : * @param[in,out] line Allocated destination line
211 : : * @param[in,out] line_len Current destination length
212 : : * @param[in] fmt Printf format string
213 : : * @return true when the complete payload was appended, otherwise false
214 : : */
215 : : __attribute__((format(printf,3,4)))
216 : 33989 : static bool logger_line_append(
217 : : char **line,
218 : : int *line_len,
219 : : const char *fmt,
220 : : ...)
221 : : {
222 : : va_list args;
223 : 33989 : va_start(args,fmt);
224 : 33989 : const bool was_appended = logger_line_append_va(line,line_len,fmt,args);
225 : 33989 : va_end(args);
226 : :
227 : 33989 : return(was_appended);
228 : : }
229 : :
230 : : /**
231 : : * @brief Append one byte as a visible hexadecimal escape
232 : : *
233 : : * @param[in,out] line Destination buffer with enough free space
234 : : * @param[in,out] line_len Current destination length in bytes
235 : : * @param[in] byte Byte to append as \xNN
236 : : */
237 : 36 : static void logger_line_append_hex_escape(
238 : : char *line,
239 : : size_t *line_len,
240 : : unsigned char byte)
241 : : {
242 : : /*
243 : : * Use an explicit digit table instead of sprintf().
244 : : * This helper is used from the logger cleanup path, so it should not depend
245 : : * on formatting functions or temporary buffers just to render one byte
246 : : */
247 : : static const char hex_digits[] = "0123456789ABCDEF";
248 : :
249 : : /*
250 : : * Write the escape directly into the caller-owned output buffer.
251 : : * The sanitizer allocates enough room before calling this helper, and
252 : : * line_len is advanced after every written character so the next append can
253 : : * continue from the correct position
254 : : */
255 : 36 : line[(*line_len)++] = '\\';
256 : 36 : line[(*line_len)++] = 'x';
257 : 36 : line[(*line_len)++] = hex_digits[byte >> 4U];
258 : 36 : line[(*line_len)++] = hex_digits[byte & 0x0FU];
259 : 36 : }
260 : :
261 : : /**
262 : : * @brief Decode one UTF-8 sequence from a bounded byte range
263 : : *
264 : : * @details
265 : : * The logger uses this small decoder instead of locale-dependent multibyte
266 : : * conversion because the process locale is not guaranteed to be initialized
267 : : * before a log line is printed. The function accepts only shortest-form UTF-8,
268 : : * rejects surrogate code points, and rejects values outside the Unicode range
269 : : *
270 : : * @param[in] line Source byte range
271 : : * @param[in] line_len Number of bytes available at @p line
272 : : * @param[out] codepoint Decoded Unicode code point
273 : : * @return Number of bytes consumed, or 0 when the input is not valid UTF-8
274 : : */
275 : 8552 : static size_t logger_line_decode_utf8(
276 : : const char *line,
277 : : size_t line_len,
278 : : uint32_t *codepoint)
279 : : {
280 : : /*
281 : : * Refuse invalid input before looking at the first byte.
282 : : * Returning 0 tells the caller to treat the current byte as unsafe and
283 : : * print it as a visible escape instead of trusting it as text
284 : : */
285 [ + - + - : 8552 : if(line == NULL || codepoint == NULL || line_len == 0U)
- + ]
286 : : {
287 : 0 : return(0U);
288 : : }
289 : :
290 : 8552 : const unsigned char first_byte = (unsigned char)line[0];
291 : :
292 : : /*
293 : : * ASCII is a single-byte subset of UTF-8.
294 : : * Decoding it here keeps the rest of the function focused on multibyte
295 : : * sequences and lets the caller apply its own ASCII control-character policy
296 : : */
297 [ - + ]: 8552 : if(first_byte < 0x80U)
298 : : {
299 : 0 : *codepoint = (uint32_t)first_byte;
300 : 0 : return(1U);
301 : : }
302 : :
303 : : /*
304 : : * Classify the leading byte and prepare the partial code point.
305 : : * The first byte tells us how many continuation bytes must follow and also
306 : : * provides the high bits of the decoded Unicode value
307 : : */
308 : 8552 : size_t expected_len = 0U;
309 : 8552 : uint32_t decoded_codepoint = 0U;
310 : 8552 : uint32_t lowest_codepoint = 0U;
311 : :
312 [ + + + + ]: 8552 : if(first_byte >= 0xC2U && first_byte <= 0xDFU)
313 : : {
314 : 7417 : expected_len = 2U;
315 : 7417 : decoded_codepoint = (uint32_t)(first_byte & 0x1FU);
316 : 7417 : lowest_codepoint = 0x80U;
317 [ + + + + ]: 1135 : } else if(first_byte >= 0xE0U && first_byte <= 0xEFU){
318 : 1107 : expected_len = 3U;
319 : 1107 : decoded_codepoint = (uint32_t)(first_byte & 0x0FU);
320 : 1107 : lowest_codepoint = 0x800U;
321 [ + + + - ]: 28 : } else if(first_byte >= 0xF0U && first_byte <= 0xF4U){
322 : 6 : expected_len = 4U;
323 : 6 : decoded_codepoint = (uint32_t)(first_byte & 0x07U);
324 : 6 : lowest_codepoint = 0x10000U;
325 : : } else {
326 : : /*
327 : : * Bytes outside these leading-byte ranges cannot start a valid UTF-8
328 : : * sequence. This includes continuation bytes seen without a starter,
329 : : * obsolete overlong starters, and values beyond the Unicode limit
330 : : */
331 : 22 : return(0U);
332 : : }
333 : :
334 : : /*
335 : : * A valid sequence must be complete inside the provided byte range.
336 : : * The logger works with bounded buffers, so an incomplete trailing sequence
337 : : * is escaped byte by byte rather than reading past the formatted line
338 : : */
339 [ + + ]: 8530 : if(line_len < expected_len)
340 : : {
341 : 2 : return(0U);
342 : : }
343 : :
344 : : /*
345 : : * Every byte after the leading byte must have the UTF-8 continuation shape
346 : : * 10xxxxxx. While checking that shape, assemble the final code point by
347 : : * shifting in the six payload bits carried by each continuation byte
348 : : */
349 [ + + ]: 18165 : for(size_t i = 1U; i < expected_len; i++)
350 : : {
351 : 9643 : const unsigned char continuation_byte = (unsigned char)line[i];
352 : :
353 [ + + ]: 9643 : if((continuation_byte & 0xC0U) != 0x80U)
354 : : {
355 : 6 : return(0U);
356 : : }
357 : :
358 : 9637 : decoded_codepoint = (decoded_codepoint << 6U) | (uint32_t)(continuation_byte & 0x3FU);
359 : : }
360 : :
361 : : /*
362 : : * Reject overlong encodings.
363 : : * UTF-8 has exactly one shortest byte representation for each code point,
364 : : * and accepting longer aliases would let unsafe bytes hide behind another
365 : : * spelling of the same character
366 : : */
367 [ - + ]: 8522 : if(decoded_codepoint < lowest_codepoint)
368 : : {
369 : 0 : return(0U);
370 : : }
371 : :
372 : : /*
373 : : * UTF-16 surrogate values are not Unicode scalar values.
374 : : * They are invalid in UTF-8 text, so the logger escapes their original bytes
375 : : * instead of copying them into terminal output
376 : : */
377 [ + + + + ]: 8522 : if(decoded_codepoint >= 0xD800U && decoded_codepoint <= 0xDFFFU)
378 : : {
379 : 2 : return(0U);
380 : : }
381 : :
382 : : /*
383 : : * Unicode ends at U+10FFFF.
384 : : * Anything above that value is invalid input and must be shown as escaped
385 : : * bytes, not as trusted text
386 : : */
387 [ + + ]: 8520 : if(decoded_codepoint > 0x10FFFFU)
388 : : {
389 : 2 : return(0U);
390 : : }
391 : :
392 : : /*
393 : : * At this point the byte sequence is well-formed UTF-8.
394 : : * Return both the decoded code point and the number of bytes consumed so the
395 : : * sanitizer can decide whether the character is safe to copy
396 : : */
397 : 8518 : *codepoint = decoded_codepoint;
398 : 8518 : return(expected_len);
399 : : }
400 : :
401 : : /**
402 : : * @brief Measure or write text with unsafe terminal bytes escaped
403 : : *
404 : : * @details
405 : : * Each unsafe byte becomes a four-character \xNN escape. Passing NULL as the
406 : : * output buffer measures the result without writing or allocating memory.
407 : : * To write the result, pass a separate buffer with at least the measured size
408 : : * and the same unchanged input. The function does not append a terminator
409 : : *
410 : : * @param[in] input Source bytes, including any embedded null bytes
411 : : * @param[in] input_len Number of bytes available at @p input
412 : : * @param[out] output Destination buffer, or NULL to measure only
413 : : * @return Result length excluding the terminator, or SIZE_MAX when the result
414 : : * cannot fit in an int or leave room for a terminator in size_t
415 : : */
416 : 42899 : static size_t logger_line_sanitize_text(
417 : : const char *input,
418 : : size_t input_len,
419 : : char *output)
420 : : {
421 : : /*
422 : : * input_position walks through the original formatted line.
423 : : * output_len tracks the sanitized length in both measurement and writing
424 : : * modes, and grows faster than the input when bytes are escaped
425 : : */
426 : 42899 : size_t input_position = 0U;
427 : 42899 : size_t output_len = 0U;
428 : :
429 : : /*
430 : : * Consume one ASCII byte or one complete UTF-8 sequence per loop.
431 : : * The loop never trusts a multibyte sequence until it has been decoded and
432 : : * checked, so damaged input cannot leak raw terminal controls into output
433 : : */
434 [ + + ]: 2059208 : while(input_position < input_len)
435 : : {
436 : 2016309 : const unsigned char byte = (unsigned char)input[input_position];
437 : 2016309 : size_t consumed_length = 1U;
438 : 2016309 : bool escape_required = false;
439 : :
440 : : /* Printable ASCII and the permitted layout characters keep their bytes.
441 : : All other ASCII controls, including embedded null bytes, need escapes */
442 [ + + ]: 2016309 : if(byte < 0x80U)
443 : : {
444 : : /*
445 : : * ASCII printable characters are safe to copy.
446 : : * Newline, carriage return, and tab are preserved because log lines
447 : : * legitimately use them for layout. Every other control byte remains
448 : : * visible instead of controlling a terminal
449 : : */
450 [ + + + + : 2007757 : if(byte != '\n' && byte != '\r' && byte != '\t'
+ + ]
451 [ + + + + ]: 1968225 : && (byte < 0x20U || byte == 0x7FU))
452 : : {
453 : : /*
454 : : * Other ASCII control bytes and DEL are not safe terminal text.
455 : : * Showing them as \xNN makes the byte visible to the user while
456 : : * preventing the terminal from treating it as an action
457 : : */
458 : 35 : escape_required = true;
459 : : }
460 : : } else {
461 : : /*
462 : : * Non-ASCII input must first prove that it is valid UTF-8.
463 : : * The decoder is intentionally local and locale-independent so logger
464 : : * safety does not depend on whether setlocale() has already run
465 : : */
466 : 8552 : uint32_t codepoint = 0U;
467 : 8552 : const size_t decoded_len = logger_line_decode_utf8(input + input_position,
468 : : input_len - input_position,&codepoint);
469 : :
470 : : /*
471 : : * Invalid UTF-8 is escaped one byte at a time.
472 : : * This preserves every original byte in a readable form and then retries
473 : : * from the next byte, which helps recover cleanly after a malformed prefix
474 : : */
475 [ + + ]: 8552 : if(decoded_len == 0U)
476 : : {
477 : 34 : escape_required = true;
478 : : } else {
479 : : /* Consume a complete validated UTF-8 sequence. Its bytes are either
480 : : retained together or individually escaped for a C1 control */
481 : 8518 : consumed_length = decoded_len;
482 : :
483 : : /*
484 : : * C1 controls are dangerous even when encoded as valid UTF-8.
485 : : * U+0090, for example, is a terminal control-string introducer on some
486 : : * terminals, so the original bytes are escaped instead of being copied
487 : : */
488 [ + - + + ]: 8518 : if(codepoint >= 0x80U && codepoint <= 0x9FU)
489 : : {
490 : 2 : escape_required = true;
491 : : }
492 : : }
493 : : }
494 : :
495 : : /*
496 : : * Escaping one input byte as \xNN needs four output bytes.
497 : : * A fragment consumes at most four input bytes, so its size can safely be
498 : : * multiplied by four. Check the total before adding the fragment so both
499 : : * the final int length and the size_t allocation with a terminator fit
500 : : */
501 : 2016309 : size_t fragment_length = consumed_length;
502 : :
503 [ + + ]: 2016309 : if(escape_required == true)
504 : : {
505 : 71 : fragment_length *= 4U;
506 : : }
507 : :
508 [ + - ]: 2016309 : if(fragment_length > (size_t)INT_MAX - output_len
509 [ - + ]: 2016309 : || fragment_length > SIZE_MAX - output_len - 1U)
510 : : {
511 : 0 : return(SIZE_MAX);
512 : : }
513 : :
514 : : /* Measurement and writing use the same fragment lengths. Only the write
515 : : pass touches the output buffer, which the caller has already sized */
516 [ + + ]: 2016309 : if(output != NULL)
517 : : {
518 [ + + ]: 583 : if(escape_required == true)
519 : : {
520 : 35 : size_t write_position = output_len;
521 : :
522 [ + + ]: 71 : for(size_t i = 0U; i < consumed_length; i++)
523 : : {
524 : 36 : logger_line_append_hex_escape(output,&write_position,
525 : 36 : (unsigned char)input[input_position + i]);
526 : : }
527 : : } else {
528 : : /*
529 : : * Printable ASCII, permitted layout bytes, and valid non-C1 UTF-8
530 : : * are copied unchanged. This keeps ordinary international file
531 : : * names and messages readable instead of escaping safe characters
532 : : */
533 : 548 : memcpy(output + output_len,input + input_position,consumed_length);
534 : : }
535 : : }
536 : :
537 : 2016309 : output_len += fragment_length;
538 : 2016309 : input_position += consumed_length;
539 : : }
540 : :
541 : 42899 : return(output_len);
542 : : }
543 : :
544 : : /**
545 : : * @brief Escape bytes that can disrupt terminal output
546 : : *
547 : : * @details
548 : : * The logger receives an already formatted line, so this layer cannot tell
549 : : * whether bytes came from a file name, a database path, an error message, or
550 : : * fixed application text. The filter therefore treats the complete log line as
551 : : * terminal output and escapes only byte patterns that are unsafe to write as
552 : : * text. Plain printable ASCII, newline, carriage return, tab, and valid non-C1
553 : : * UTF-8 multibyte sequences are preserved. Invalid multibyte input and raw ESC
554 : : * bytes are escaped byte by byte so malformed file names remain visible without
555 : : * being interpreted by the terminal. Markup is interpreted only after this
556 : : * function, so every terminal sequence present at this stage is untrusted.
557 : : * Unicode C1 controls are escaped even when their UTF-8 representation is
558 : : * otherwise valid. A sizing pass retains the input buffer when no bytes need
559 : : * escaping; otherwise, an exactly sized buffer receives the sanitized text.
560 : : * Allocation failures or unrepresentable result lengths suppress the line
561 : : *
562 : : * @param[in,out] line Pointer to the allocated line buffer
563 : : * @param[in,out] line_len Current line length in bytes, updated on success
564 : : */
565 : 58929 : static void logger_line_sanitize_for_terminal(
566 : : char **line,
567 : : int *line_len)
568 : : {
569 : : /*
570 : : * Sanitization requires an allocated buffer and a positive byte count.
571 : : * Missing pointers and empty lines return immediately without accessing
572 : : * the buffer contents
573 : : */
574 [ + - + + : 58929 : if(line == NULL || *line == NULL || line_len == NULL || *line_len <= 0)
+ - + + ]
575 : : {
576 : 16037 : return;
577 : : }
578 : :
579 : 42892 : const size_t input_len = (size_t)*line_len;
580 : :
581 : : /* Measure the result, counting four characters for each escaped byte.
582 : : Every other byte keeps its original size, including valid non-C1 UTF-8 */
583 : 42892 : const size_t sanitized_length = logger_line_sanitize_text(*line,input_len,NULL);
584 : :
585 [ - + ]: 42892 : if(sanitized_length == SIZE_MAX)
586 : : {
587 : 0 : free(*line);
588 : 0 : *line = NULL;
589 : 0 : *line_len = 0;
590 : 0 : return;
591 : : }
592 : :
593 : : /* Fully checked text without unsafe bytes already has the required form.
594 : : Keep its allocation and avoid copying the unchanged message */
595 [ + + ]: 42892 : if(sanitized_length == input_len)
596 : : {
597 : 42884 : return;
598 : : }
599 : :
600 : : /*
601 : : * Allocate exactly the input length plus the growth from escaped bytes.
602 : : * The extra byte is for a terminator because the logger stores text in a C
603 : : * string buffer even though fwrite() uses the explicit byte length
604 : : */
605 : 8 : char *sanitized_line = malloc(sanitized_length + 1U);
606 : :
607 [ + + ]: 8 : if(sanitized_line == NULL)
608 : : {
609 : : /* Suppress the line instead of allowing untrusted ESC bytes to bypass
610 : : sanitization when memory is exhausted */
611 : 1 : free(*line);
612 : 1 : *line = NULL;
613 : 1 : *line_len = 0;
614 : 1 : return;
615 : : }
616 : :
617 : : /* The input is unchanged, so writing produces exactly the measured length */
618 : 7 : (void)logger_line_sanitize_text(*line,input_len,sanitized_line);
619 : :
620 : : /*
621 : : * Replace the original formatted line with the sanitized version.
622 : : * Trusted decoration sequences are inserted only after this step
623 : : */
624 : 7 : sanitized_line[sanitized_length] = '\0';
625 : 7 : free(*line);
626 : 7 : *line = sanitized_line;
627 : 7 : *line_len = (int)sanitized_length;
628 : : }
629 : :
630 : : /**
631 : : * @brief Build the common log prefix and append one payload
632 : : *
633 : : * @details A single logger-mode snapshot controls the entire line. The verbose
634 : : * prefix is appended as one formatted fragment. An append failure
635 : : * stops assembly, leaving the caller responsible for freeing the line
636 : : *
637 : : * @param[in,out] line Allocated output line
638 : : * @param[in,out] line_len Current output length
639 : : * @param level Requested logger modes
640 : : * @param[in] filename Source file name
641 : : * @param line_number Source line number
642 : : * @param[in] funcname Source function name
643 : : * @param[in] fmt Original printf format
644 : : * @param args Arguments associated with @p fmt
645 : : * @param format_errno errno value visible to `%m` conversions
646 : : * @return true when the complete line was built, otherwise false
647 : : */
648 : : __attribute__((format(printf,7,0)))
649 : 58929 : static bool logger_line(
650 : : char **line,
651 : : int *line_len,
652 : : const LOGMODES level,
653 : : const char *const filename,
654 : : size_t line_number,
655 : : const char *const funcname,
656 : : const char *fmt,
657 : : va_list args,
658 : : int format_errno)
659 : : {
660 [ + - - + ]: 58929 : if(line == NULL || line_len == NULL)
661 : : {
662 : 0 : return(false);
663 : : }
664 : :
665 : : /* Use one atomic snapshot so all prefix and payload checks use the same
666 : : mode even when another thread updates the shared setting */
667 : 58929 : const LOGMODES logger_mode = atomic_load_explicit(&rational_logger_mode,memory_order_relaxed);
668 : 58929 : bool payload_is_visible = false;
669 : :
670 [ + + ]: 58929 : if(logger_mode & SILENT)
671 : : {
672 [ + + ]: 4752 : if(level & VISIBLE_IN_SILENT)
673 : : {
674 : 40 : payload_is_visible = true;
675 : : }
676 : : } else {
677 [ + + + + : 54177 : if(!(level & UNDECOR) && (level & TESTING) && (logger_mode & TESTING))
+ + ]
678 : : {
679 : : // Print out the word "TESTING:"
680 [ + + ]: 32567 : if(logger_line_append(line,line_len,"TESTING:") == false)
681 : : {
682 : 4 : return(false);
683 : : }
684 : : }
685 : :
686 [ + + + + : 54173 : if(!(level & UNDECOR) && (level & (VERBOSE|ERROR)) && (logger_mode & VERBOSE))
+ + ]
687 : : {
688 : : char time_string[sizeof "2011-10-18 07:07:09:000"];
689 : 796 : (void)logger_show_time(time_string,sizeof(time_string));
690 : :
691 : : // Print out current time
692 : 796 : if(logger_line_append(line,line_len,"%s %s:%03zu:%s:",time_string,
693 : : // Print out the source file name
694 : : filename,
695 : : // Print out line number in source file
696 : : line_number,
697 : : // Print out name of the function itself
698 [ + + ]: 796 : funcname) == false)
699 : : {
700 : 2 : return(false);
701 : : }
702 : : }
703 : :
704 [ + + + + : 54171 : if(!(level & UNDECOR) && (level & ERROR) && (logger_mode & (REGULAR | ERROR)))
+ + ]
705 : : {
706 : : // Print out error prefix
707 [ + + ]: 36 : if(logger_line_append(line,line_len,"ERROR: ") == false)
708 : : {
709 : 2 : return(false);
710 : : }
711 : :
712 [ + + + + : 54135 : } else if(!(level & UNDECOR) && (level & ERROR) && (logger_mode & (TESTING | VERBOSE))){
+ + ]
713 : : // Print out the word "ERROR:"
714 [ + + ]: 590 : if(logger_line_append(line,line_len,"ERROR:") == false)
715 : : {
716 : 2 : return(false);
717 : : }
718 : : }
719 : :
720 [ + + + + ]: 54167 : if(level & ERROR && logger_mode & ERROR)
721 : : {
722 : : // Print out other arguments
723 : 2 : payload_is_visible = true;
724 : :
725 [ + + + + ]: 54165 : } else if(level & (REGULAR|ERROR) && logger_mode & REGULAR){
726 : : // Print out other arguments
727 : 2471 : payload_is_visible = true;
728 : :
729 [ + + + + ]: 51694 : } else if(level & (VERBOSE|ERROR) && logger_mode & VERBOSE){
730 : : // Print out other arguments
731 : 1195 : payload_is_visible = true;
732 : :
733 [ + + + + ]: 50499 : } else if(level & (TESTING|ERROR) && logger_mode & TESTING){
734 : : // Print out other arguments
735 : 39198 : payload_is_visible = true;
736 : : }
737 : : }
738 : :
739 [ + + ]: 58919 : if(payload_is_visible == true)
740 : : {
741 : 42906 : errno = format_errno;
742 : :
743 [ + + ]: 42906 : if(logger_line_append_va(line,line_len,fmt,args) == false)
744 : : {
745 : 13 : return(false);
746 : : }
747 : :
748 : 42893 : errno = format_errno;
749 : : }
750 : :
751 : 58906 : return(true);
752 : : }
753 : :
754 : : /**
755 : : * @brief Build and print a formatted log line with file, line, and function metadata
756 : : *
757 : : * @details Literal markers are replaced after printf formatting and sanitization.
758 : : * Colored REMEMBER messages use a plain copy for the callback, delivered
759 : : * before terminal output. Other messages are transformed in place.
760 : : * Allocation or replacement failures suppress the entire message
761 : : */
762 : : __attribute__((format(printf,5,6))) // Without this we will get warning
763 : 58929 : void rational_logger(
764 : : const LOGMODES level,
765 : : const char *const filename,
766 : : size_t line,
767 : : const char *const funcname,
768 : : const char *fmt,
769 : : ...)
770 : : {
771 : 58929 : const int format_errno = errno;
772 : 58929 : char *logger_line_text = NULL;
773 : 58929 : char *remember_line_text = NULL;
774 : 58929 : int line_len = 0;
775 : :
776 : : va_list args;
777 : 58929 : va_start(args,fmt);
778 : 58929 : bool line_is_complete = logger_line(&logger_line_text,
779 : : &line_len,
780 : : level,
781 : : filename,
782 : : line,
783 : : funcname,
784 : : fmt,
785 : : args,
786 : : format_errno);
787 : 58929 : va_end(args);
788 : :
789 [ + + ]: 58929 : if(line_is_complete == false)
790 : : {
791 : 23 : free(logger_line_text);
792 : 23 : logger_line_text = NULL;
793 : 23 : line_len = 0;
794 : : }
795 : :
796 : 58929 : logger_line_sanitize_for_terminal(&logger_line_text,&line_len);
797 : :
798 [ + + ]: 58929 : if(logger_line_text != NULL)
799 : : {
800 : 42892 : size_t output_length = (size_t)line_len;
801 : 42892 : size_t remember_length = output_length;
802 [ + + + - ]: 42892 : const bool remember_is_requested = (level & REMEMBER) && rational_remember;
803 : :
804 [ + + ]: 42892 : if(strstr(logger_line_text,"@{") != NULL)
805 : : {
806 : 1374 : const bool styles_enabled = rational_color_is_enabled(stdout);
807 : :
808 [ + + + + ]: 1374 : if(remember_is_requested == true && styles_enabled == true)
809 : : {
810 : 50 : remember_line_text = strdup(logger_line_text);
811 : 50 : line_is_complete = remember_line_text != NULL;
812 : :
813 [ + - ]: 50 : if(line_is_complete == true)
814 : : {
815 : 50 : line_is_complete = rational_logger_markup_replace(
816 : : &remember_line_text,&remember_length,false);
817 : : }
818 : : }
819 : :
820 [ + - ]: 1374 : if(line_is_complete == true)
821 : : {
822 : 1374 : line_is_complete = rational_logger_markup_replace(
823 : : &logger_line_text,&output_length,styles_enabled);
824 : : }
825 : : }
826 : :
827 [ + - ]: 42892 : if(line_is_complete == true)
828 : : {
829 [ + + ]: 42892 : if(remember_is_requested == true)
830 : : {
831 [ + + + - ]: 192 : if(remember_line_text != NULL && remember_length > 0U)
832 : : {
833 : 50 : rational_remember(remember_line_text,(int)remember_length);
834 [ + - + + ]: 142 : } else if(remember_line_text == NULL && output_length > 0U){
835 : 141 : rational_remember(logger_line_text,(int)output_length);
836 : : }
837 : : }
838 : :
839 : 42892 : flockfile(stdout);
840 : 42892 : (void)fwrite(logger_line_text,sizeof(char),output_length,stdout);
841 : 42892 : funlockfile(stdout);
842 : : }
843 : : }
844 : :
845 : 58929 : free(remember_line_text);
846 : 58929 : free(logger_line_text);
847 : 58929 : errno = format_errno;
848 : 58929 : }
849 : :
850 : : #ifdef TEST
851 : : /**
852 : : * @brief Display the plain text received by the demonstration's REMEMBER callback
853 : : *
854 : : * @details The callback prints the supplied text directly, without calling slog().
855 : : * Its output precedes the logger's styled output of the same message
856 : : *
857 : : * @param[in] message Plain message supplied by the logger
858 : : * @param message_length Number of message bytes to print
859 : : */
860 : : void rational_remember(
861 : : const char *message,
862 : : const int message_length)
863 : : {
864 : : /* Show the callback's exact payload without applying formatting or markup */
865 : : printf("REMEMBER callback (plain): ");
866 : : (void)fwrite(message,sizeof(char),(size_t)message_length,stdout);
867 : : }
868 : :
869 : : /**
870 : : * @brief Demonstrate logger modes, text styling, sanitization, and REMEMBER output
871 : : *
872 : : * @details Prints labeled examples for visual inspection. Automatic styling uses
873 : : * the current output stream and environment. Explicit always examples
874 : : * emit terminal sequences even when output is redirected
875 : : *
876 : : * @return 0 after printing the demonstration
877 : : */
878 : : int main(void)
879 : : {
880 : : printf("All available combinations:\n");
881 : : printf("%s\n",rational_convert(REGULAR));
882 : : printf("%s\n",rational_convert(VERBOSE));
883 : : printf("%s\n",rational_convert(TESTING));
884 : : printf("%s\n",rational_convert(SILENT));
885 : : printf("%s\n",rational_convert(REGULAR|VERBOSE));
886 : : printf("%s\n",rational_convert(REGULAR|TESTING));
887 : : printf("%s\n",rational_convert(VERBOSE|TESTING));
888 : : printf("%s\n",rational_convert(REGULAR|VERBOSE|TESTING));
889 : : printf("%s\n",rational_convert(ERROR));
890 : : printf("%s\n",rational_convert(UNDECOR));
891 : : printf("%s\n",rational_convert(EVERY|UNDECOR));
892 : : printf("%s\n",rational_convert(ERROR|UNDECOR));
893 : : printf("%s\n",rational_convert(VISIBLE_IN_SILENT));
894 : :
895 : : /* Test REGULAR mode combinations */
896 : : rational_logger_mode = REGULAR;
897 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
898 : : printf("1. Must print:"); slog(REGULAR,"true"); printf("\n");
899 : : printf("2. Won't print:"); slog(VERBOSE,"but printed!"); printf("\n");
900 : : printf("3. Won't print:"); slog(TESTING,"but printed!"); printf("\n");
901 : : printf("4. Must print:"); slog(ERROR,"true"); printf("\n");
902 : :
903 : : /* Test VERBOSE mode combinations */
904 : : rational_logger_mode = VERBOSE;
905 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
906 : : printf("5. Won't print:"); slog(REGULAR,"but printed!"); printf("\n");
907 : : printf("6. Must print:"); slog(VERBOSE,"true"); printf("\n");
908 : : printf("7. Won't print:"); slog(TESTING,"but printed!"); printf("\n");
909 : : printf("8. Must print:"); slog(ERROR,"true"); printf("\n");
910 : :
911 : : /* Test TESTING mode combinations */
912 : : rational_logger_mode = TESTING;
913 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
914 : : printf("9. Won't print:"); slog(REGULAR,"but printed!"); printf("\n");
915 : : printf("10. Won't print:"); slog(VERBOSE,"but printed!"); printf("\n");
916 : : printf("11. Must print:"); slog(TESTING,"true"); printf("\n");
917 : : printf("12. Must print:"); slog(ERROR,"true"); printf("\n");
918 : :
919 : : /* Test SILENT mode combinations */
920 : : rational_logger_mode = SILENT;
921 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
922 : : printf("13. Won't print:"); slog(REGULAR,"but printed!"); printf("\n");
923 : : printf("14. Won't print:"); slog(VERBOSE,"but printed!"); printf("\n");
924 : : printf("15. Won't print:"); slog(TESTING,"but printed!"); printf("\n");
925 : : printf("16. Won't print:"); slog(ERROR,"but printed!"); printf("\n");
926 : :
927 : : /* Test REGULAR|VERBOSE combinations */
928 : : rational_logger_mode = REGULAR|VERBOSE;
929 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
930 : : printf("17. Must print:"); slog(REGULAR,"true"); printf("\n");
931 : : printf("18. Must print:"); slog(VERBOSE,"true"); printf("\n");
932 : : printf("19. Won't print:"); slog(TESTING,"but printed!"); printf("\n");
933 : : printf("20. Must print:"); slog(ERROR,"true"); printf("\n");
934 : :
935 : : /* Test REGULAR|TESTING combinations */
936 : : rational_logger_mode = REGULAR|TESTING;
937 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
938 : : printf("21. Must print:"); slog(REGULAR,"true"); printf("\n");
939 : : printf("22. Won't print:"); slog(VERBOSE,"but printed!"); printf("\n");
940 : : printf("23. Must print:"); slog(TESTING,"true"); printf("\n");
941 : : printf("24. Must print:"); slog(ERROR,"true"); printf("\n");
942 : :
943 : : /* Test VERBOSE|TESTING combinations */
944 : : rational_logger_mode = VERBOSE|TESTING;
945 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
946 : : printf("25. Won't print:"); slog(REGULAR,"but printed!"); printf("\n");
947 : : printf("26. Must print:"); slog(VERBOSE,"true"); printf("\n");
948 : : printf("27. Must print:"); slog(TESTING,"true"); printf("\n");
949 : : printf("28. Must print:"); slog(ERROR,"true"); printf("\n");
950 : :
951 : : /* Test REGULAR|VERBOSE|TESTING combinations */
952 : : rational_logger_mode = REGULAR|VERBOSE|TESTING;
953 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
954 : : printf("29. Must print:"); slog(REGULAR,"true"); printf("\n");
955 : : printf("30. Must print:"); slog(VERBOSE,"true"); printf("\n");
956 : : printf("31. Must print:"); slog(TESTING,"true"); printf("\n");
957 : : printf("32. Must print:"); slog(ERROR,"true"); printf("\n");
958 : :
959 : : /* Test ERROR mode combinations */
960 : : rational_logger_mode = ERROR;
961 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
962 : : printf("33. Won't print:"); slog(REGULAR,"but printed!"); printf("\n");
963 : : printf("34. Won't print:"); slog(VERBOSE,"but printed!"); printf("\n");
964 : : printf("35. Won't print:"); slog(TESTING,"but printed!"); printf("\n");
965 : : printf("36. Must print:"); slog(ERROR,"true"); printf("\n");
966 : :
967 : : /*
968 : : * Test UNDECOR flag: suppress logger prefixes (TESTING:, time/file/line/func, ERROR:)
969 : : * The output between the '|' markers should contain only the message payload.
970 : : */
971 : :
972 : : rational_logger_mode = EVERY|ERROR;
973 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
974 : : printf("37. Must print no prefixes:|"); slog(EVERY|UNDECOR,"true"); printf("|\n");
975 : : printf("38. Must print no ERROR prefix:|"); slog(ERROR|UNDECOR,"true"); printf("|\n");
976 : :
977 : : rational_logger_mode = VERBOSE;
978 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
979 : : printf("39. Must print no time/file/line/func:|"); slog(VERBOSE|UNDECOR,"true"); printf("|\n");
980 : :
981 : : rational_logger_mode = TESTING;
982 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
983 : : printf("40. Must print no TESTING prefix:|"); slog(TESTING|UNDECOR,"true"); printf("|\n");
984 : :
985 : : rational_logger_mode = REGULAR;
986 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
987 : : printf("41. Must not print (VERBOSE not enabled):|"); slog(VERBOSE|UNDECOR,"but printed!"); printf("|\n");
988 : :
989 : : rational_logger_mode = SILENT;
990 : : printf("Mode: %s\n",rational_reconvert(rational_logger_mode));
991 : : printf("42. Must print in SILENT without prefixes:|"); slog(EVERY|VISIBLE_IN_SILENT,"true"); printf("|\n");
992 : : printf("43. Must print no ERROR prefix in SILENT:|"); slog(ERROR|VISIBLE_IN_SILENT,"true"); printf("|\n");
993 : :
994 : : /* Compare the same message under each color policy without changing the
995 : : environment. Only auto depends on stdout, NO_COLOR, and TERM */
996 : : rational_logger_mode = REGULAR;
997 : : printf("\nColor modes: auto follows stdout, NO_COLOR, and TERM\n");
998 : : printf("The always examples emit ANSI sequences even when output is redirected\n");
999 : : rational_color_mode = COLOR_MODE_AUTO;
1000 : : printf("44. auto: ");
1001 : : slog(REGULAR|UNDECOR,"@{bold}@{red}styled text@{reset}, plain text\n");
1002 : : rational_color_mode = COLOR_MODE_ALWAYS;
1003 : : printf("45. always: ");
1004 : : slog(REGULAR|UNDECOR,"@{bold}@{red}styled text@{reset}, plain text\n");
1005 : : rational_color_mode = COLOR_MODE_NEVER;
1006 : : printf("46. never: ");
1007 : : slog(REGULAR|UNDECOR,"@{bold}@{red}styled text@{reset}, plain text\n");
1008 : :
1009 : : /* Force styling for the palette so every supported color can be inspected.
1010 : : Each styled fragment supplies its own explicit reset marker */
1011 : : rational_color_mode = COLOR_MODE_ALWAYS;
1012 : : printf("\nColors and text styles (always)\n");
1013 : : slog(REGULAR|UNDECOR,"47. Colors: "
1014 : : "@{black}black@{reset} @{gray}gray@{reset} @{red}red@{reset} "
1015 : : "@{green}green@{reset} @{yellow}yellow@{reset} @{blue}blue@{reset} "
1016 : : "@{magenta}magenta@{reset} @{cyan}cyan@{reset} @{white}white@{reset}\n");
1017 : : slog(REGULAR|UNDECOR,"48. Weight: @{bold}bold@{reset}, normal after reset\n");
1018 : : slog(REGULAR|UNDECOR,"49. Bold colors: "
1019 : : "@{boldblack}black@{reset} @{boldred}red@{reset} "
1020 : : "@{boldgreen}green@{reset} @{boldyellow}yellow@{reset} "
1021 : : "@{boldblue}blue@{reset} @{boldmagenta}magenta@{reset} "
1022 : : "@{boldcyan}cyan@{reset} @{boldwhite}white@{reset}\n");
1023 : : slog(REGULAR|UNDECOR,"50. Combined markers: @{bold}@{red}bold red@{reset}, plain text\n");
1024 : :
1025 : : /* Markers are interpreted after printf arguments have been formatted.
1026 : : Unknown markers are ordinary text and remain visible */
1027 : : printf("\nFormatted arguments and unknown markers (always)\n");
1028 : : slog(REGULAR|UNDECOR,"51. Markup inside %%s: %s; number: %d\n",
1029 : : "@{green}green argument@{reset}",7);
1030 : : slog(REGULAR|UNDECOR,"52. Unknown marker stays visible: @{unknown}\n");
1031 : :
1032 : : /* Sanitization preserves ordinary UTF-8 and makes raw ESC bytes visible.
1033 : : Raw terminal sequences from arguments are not trusted styling markers */
1034 : : printf("\nTerminal-safe text (always)\n");
1035 : : slog(REGULAR|UNDECOR,"53. UTF-8 stays readable: Привет 日本語 😀\n");
1036 : : slog(REGULAR|UNDECOR,"54. Raw ESC stays visible: %s\n",
1037 : : "\033[31mnot colored\033[0m");
1038 : :
1039 : : /* The demonstration callback prints the plain message before the logger
1040 : : writes its styled version, making both forms visible one after the other */
1041 : : printf("\nREMEMBER (always): plain callback output, then styled logger output\n");
1042 : : slog(REGULAR|UNDECOR|REMEMBER,"55. @{bold}@{green}Remembered message %d@{reset}\n",7);
1043 : :
1044 : : return 0;
1045 : : }
1046 : : #endif
|