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 : : }
|