LCOV - code coverage report
Current view: top level - libs/rational/src - rational_logger_markup.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 96.2 % 53 51
Test Date: 2026-10-20 14:27:43 Functions: 100.0 % 2 2
Branches: 76.5 % 34 26

             Branch data     Line data    Source code
       1                 :             : /**
       2                 :             :  * @file
       3                 :             :  * @brief Literal substring replacement for formatted logger messages
       4                 :             :  */
       5                 :             : 
       6                 :             : #include "rational.h"
       7                 :             : 
       8                 :             : #include <stdint.h>
       9                 :             : #include <stdlib.h>
      10                 :             : #include <string.h>
      11                 :             : 
      12                 :             : /**
      13                 :             :  * @brief Replace every non-overlapping occurrence of a literal substring
      14                 :             :  *
      15                 :             :  * @details Counts matches and calculates the resulting byte length before
      16                 :             :  *          changing the text. Shorter or equal replacements reuse the existing
      17                 :             :  *          buffer. Longer replacements resize it once and move the source to
      18                 :             :  *          the right, leaving room for a forward copy into the same allocation.
      19                 :             :  *          Replacement bytes are not searched again during this call.
      20                 :             :  *          The caller must supply the exact text length, and the search and
      21                 :             :  *          replacement strings must not point into the text buffer
      22                 :             :  *
      23                 :             :  * @param[in,out] text Pointer to an owned, realloc-compatible, null-terminated
      24                 :             :  *                    text buffer; updated if resizing changes its address
      25                 :             :  * @param[in,out] text_length Exact byte length, excluding the null terminator;
      26                 :             :  *                           updated to the resulting length on success
      27                 :             :  * @param[in] search Nonempty literal substring to replace
      28                 :             :  * @param[in] replacement Literal replacement, which may be empty
      29                 :             :  * @return true on success; false for invalid arguments, size overflow, or an
      30                 :             :  *         allocation failure, leaving the original buffer and length unchanged
      31                 :             :  */
      32                 :       28491 : bool rational_logger_replace_all(
      33                 :             :         char       **text,
      34                 :             :         size_t     *text_length,
      35                 :             :         const char *search,
      36                 :             :         const char *replacement)
      37                 :             : {
      38                 :             :         /* Require valid argument pointers and a nonempty search string. An empty
      39                 :             :            search would not advance the matching loop. A length of SIZE_MAX leaves
      40                 :             :            no room for the terminating null byte, so reject it before arithmetic */
      41   [ +  -  +  -  :       28491 :         if(text == NULL || *text == NULL || text_length == NULL
                   +  - ]
      42   [ +  -  +  -  :       28491 :                 || search == NULL || replacement == NULL || search[0] == '\0'
                   +  + ]
      43         [ -  + ]:       28490 :                 || *text_length == SIZE_MAX)
      44                 :             :         {
      45                 :           1 :                 return(false);
      46                 :             :         }
      47                 :             : 
      48                 :             :         /* Measure the two literal strings once and retain the input byte length.
      49                 :             :            These lengths determine how much each match grows or shrinks the text */
      50                 :       28490 :         const size_t search_length = strlen(search);
      51                 :       28490 :         const size_t replacement_length = strlen(replacement);
      52                 :       28490 :         const size_t source_length = *text_length;
      53                 :       28490 :         size_t match_count = 0U;
      54                 :       28490 :         const char *match = *text;
      55                 :             : 
      56                 :             :         /* Count matches without changing the text. Continue after the complete
      57                 :             :            matched substring so overlapping occurrences are not counted twice */
      58         [ +  + ]:       31437 :         while((match = strstr(match,search)) != NULL)
      59                 :             :         {
      60                 :        2947 :                 match_count++;
      61                 :        2947 :                 match += search_length;
      62                 :             :         }
      63                 :             : 
      64                 :             :         /* No matches is a successful result that requires neither copying nor
      65                 :             :            resizing. Leave the caller's buffer pointer and byte length unchanged */
      66         [ +  + ]:       28490 :         if(match_count == 0U)
      67                 :             :         {
      68                 :       25601 :                 return(true);
      69                 :             :         }
      70                 :             : 
      71                 :             :         /* Start with the input length and adjust it by the total size difference
      72                 :             :            contributed by all matches. This length excludes the null terminator */
      73                 :        2889 :         size_t result_length = source_length;
      74                 :             : 
      75                 :             :         /* A longer replacement adds the same number of bytes for every match.
      76                 :             :            Reserve space for all of that growth before modifying the text */
      77         [ +  + ]:        2889 :         if(replacement_length > search_length)
      78                 :             :         {
      79                 :           4 :                 const size_t growth = replacement_length - search_length;
      80                 :             : 
      81                 :             :                 /* Reserve one byte for the terminator and check the maximum safe
      82                 :             :                    match count by division before multiplying. This prevents overflow
      83                 :             :                    in both the resulting length and the allocation size */
      84         [ -  + ]:           4 :                 if(match_count > (SIZE_MAX - source_length - 1U) / growth)
      85                 :             :                 {
      86                 :           0 :                         return(false);
      87                 :             :                 }
      88                 :             : 
      89                 :             :                 /* Request the complete output size in one realloc call. Keep its
      90                 :             :                    result separate from the caller's pointer until success is known */
      91                 :           4 :                 result_length += match_count * growth;
      92                 :           4 :                 char *resized_text = realloc(*text,result_length + 1U);
      93                 :             : 
      94                 :             :                 /* A failed realloc leaves the original allocation valid. Return
      95                 :             :                    without changing the caller's pointer, text, or recorded length */
      96         [ +  + ]:           4 :                 if(resized_text == NULL)
      97                 :             :                 {
      98                 :           1 :                         return(false);
      99                 :             :                 }
     100                 :             : 
     101                 :             :                 /* Publish the successful allocation, which may have a new address */
     102                 :           3 :                 *text = resized_text;
     103                 :             :         } else {
     104                 :             :                 /* Subtract the bytes removed by all matches. Equal-length replacements
     105                 :             :                    subtract zero. Keep the allocated capacity and reuse the same buffer */
     106                 :        2885 :                 result_length -= match_count * (search_length - replacement_length);
     107                 :             :         }
     108                 :             : 
     109                 :             :         /* Read the source through one pointer and build the result through another.
     110                 :             :            Both pointers refer to regions within the caller's allocation */
     111                 :        2888 :         const char *source = *text;
     112                 :        2888 :         char *destination = *text;
     113                 :             : 
     114                 :             :         /* Reserve expansion space ahead of the source so unread bytes stay intact.
     115                 :             :            Move the complete input, including its terminator, right by the total
     116                 :             :            growth. memmove handles overlapping regions, and the gap allows output
     117                 :             :            to be written forward without overwriting source bytes still needed */
     118         [ +  + ]:        2888 :         if(result_length > source_length)
     119                 :             :         {
     120                 :           3 :                 const size_t offset = result_length - source_length;
     121                 :           3 :                 memmove(*text + offset,*text,source_length + 1U);
     122                 :           3 :                 source += offset;
     123                 :             :         }
     124                 :             : 
     125                 :             :         /* Find each match in the unread source and write the resulting pieces
     126                 :             :            from left to right. Each iteration consumes one complete occurrence */
     127         [ +  + ]:        5832 :         while((match = strstr(source,search)) != NULL)
     128                 :             :         {
     129                 :             :                 /* Preserve the ordinary text before the match. The source and output
     130                 :             :                    regions can overlap, so copying this prefix requires memmove */
     131                 :        2944 :                 const size_t prefix_length = (size_t)(match - source);
     132                 :        2944 :                 memmove(destination,source,prefix_length);
     133                 :        2944 :                 destination += prefix_length;
     134                 :             : 
     135                 :             :                 /* Insert the replacement bytes and advance the write position. An
     136                 :             :                    empty replacement inserts nothing and therefore removes the match */
     137                 :        2944 :                 memcpy(destination,replacement,replacement_length);
     138                 :        2944 :                 destination += replacement_length;
     139                 :             : 
     140                 :             :                 /* Continue reading after the matched source substring. Inserted
     141                 :             :                    replacement bytes are outside the remaining search region */
     142                 :        2944 :                 source = match + search_length;
     143                 :             :         }
     144                 :             : 
     145                 :             :         /* Copy the remaining unmatched suffix together with its null terminator.
     146                 :             :            This also finishes the result when the final match ends the input */
     147                 :        2888 :         memmove(destination,source,strlen(source) + 1U);
     148                 :             : 
     149                 :             :         /* Publish the calculated byte length after the complete result is ready */
     150                 :        2888 :         *text_length = result_length;
     151                 :        2888 :         return(true);
     152                 :             : }
     153                 :             : 
     154                 :             : /**
     155                 :             :  * @brief Replace supported logger markers with terminal bytes or empty strings
     156                 :             :  *
     157                 :             :  * @details Applies literal replacements to formatted, sanitized text in table
     158                 :             :  *          order. Enabled styling replaces markers with terminal sequences;
     159                 :             :  *          disabled styling replaces them with empty strings. With styling
     160                 :             :  *          enabled, reset markers insert the terminal reset sequence.
     161                 :             :  *          The table's replacement strings are shorter than their markers,
     162                 :             :  *          so processing reuses the text buffer without allocating memory
     163                 :             :  *
     164                 :             :  * @param[in,out] text Pointer to the owned, null-terminated buffer containing
     165                 :             :  *                    formatted and sanitized text
     166                 :             :  * @param[in,out] text_length Exact byte length, excluding the null terminator;
     167                 :             :  *                           updated as markers are replaced
     168                 :             :  * @param styles_enabled true replaces markers with terminal bytes; false
     169                 :             :  *                       removes them. The caller decides whether styling is allowed
     170                 :             :  * @return true when all replacements succeed, including when no markers occur;
     171                 :             :  *         false stops processing without undoing earlier replacements
     172                 :             :  */
     173                 :        1424 : bool rational_logger_markup_replace(
     174                 :             :         char   **text,
     175                 :             :         size_t *text_length,
     176                 :             :         bool   styles_enabled)
     177                 :             : {
     178                 :             :         /* Pair each literal marker with its terminal sequence. The table holds
     179                 :             :            pointers to fixed strings in static storage for the program's lifetime.
     180                 :             :            Its const entries define the replacement rules shared by all calls */
     181                 :             :         static const struct {
     182                 :             :                 const char *marker;
     183                 :             :                 const char *replacement;
     184                 :             :         } decorations[] = {
     185                 :             :                 {"@{reset}",RESET},
     186                 :             :                 {"@{shorttab}",SHORTTAB},
     187                 :             :                 {"@{bold}",BOLD},
     188                 :             :                 {"@{black}",BLACK},
     189                 :             :                 {"@{gray}",GRAY},
     190                 :             :                 {"@{red}",RED},
     191                 :             :                 {"@{green}",GREEN},
     192                 :             :                 {"@{yellow}",YELLOW},
     193                 :             :                 {"@{blue}",BLUE},
     194                 :             :                 {"@{magenta}",MAGENTA},
     195                 :             :                 {"@{cyan}",CYAN},
     196                 :             :                 {"@{white}",WHITE},
     197                 :             :                 {"@{boldblack}",BOLDBLACK},
     198                 :             :                 {"@{boldred}",BOLDRED},
     199                 :             :                 {"@{boldgreen}",BOLDGREEN},
     200                 :             :                 {"@{boldyellow}",BOLDYELLOW},
     201                 :             :                 {"@{boldblue}",BOLDBLUE},
     202                 :             :                 {"@{boldmagenta}",BOLDMAGENTA},
     203                 :             :                 {"@{boldcyan}",BOLDCYAN},
     204                 :             :                 {"@{boldwhite}",BOLDWHITE}
     205                 :             :         };
     206                 :             : 
     207                 :             :         /* Process every rule in table order; each rule sees the text produced by
     208                 :             :            the preceding replacements. Dividing the array's byte size by one
     209                 :             :            entry's byte size gives the entry count used as the loop limit */
     210                 :        1424 :         for(size_t decoration_index = 0U;
     211         [ +  + ]:       29904 :                 decoration_index < sizeof(decorations) / sizeof(decorations[0]);
     212                 :       28480 :                 decoration_index++)
     213                 :             :         {
     214                 :             :                 /* An empty replacement removes the marker without inserting any bytes.
     215                 :             :                    Keep this value when styling is disabled, including for reset markers */
     216                 :       28480 :                 const char *replacement = "";
     217                 :             : 
     218                 :             :                 /* When styling is enabled, select this rule's terminal sequence.
     219                 :             :                    The styles_enabled argument supplies the caller's decision about
     220                 :             :                    whether terminal styling is allowed */
     221         [ +  + ]:       28480 :                 if(styles_enabled == true)
     222                 :             :                 {
     223                 :       25940 :                         replacement = decorations[decoration_index].replacement;
     224                 :             :                 }
     225                 :             : 
     226                 :             :                 /* Replace all occurrences of the current marker and update the text's
     227                 :             :                    byte length. The table's replacements fit in the existing buffer, so no
     228                 :             :                    allocation is needed. A missing marker leaves the text unchanged and
     229                 :             :                    counts as a successful replacement step */
     230                 :       56960 :                 if(rational_logger_replace_all(text,text_length,
     231         [ -  + ]:       28480 :                         decorations[decoration_index].marker,replacement) == false)
     232                 :             :                 {
     233                 :             :                         /* Stop at the first error instead of applying the remaining rules.
     234                 :             :                            Changes made by earlier iterations remain in the caller's buffer */
     235                 :           0 :                         return(false);
     236                 :             :                 }
     237                 :             :         }
     238                 :             : 
     239                 :             :         /* Every rule completed successfully, including rules with no matches.
     240                 :             :            The output parameters contain the processed text and its byte length */
     241                 :        1424 :         return(true);
     242                 :             : }
        

Generated by: LCOV version 2.0-1