ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
string.c
Go to the documentation of this file.
1
6
9#include <ascii-chat/common.h>
10#include <libplatform/system.h>
11#include <libplatform/terminal.h>
13#include <ascii-chat/log/log.h>
14#include <string.h>
15#include <ctype.h>
16
17#if 0 /* Shell escaping is provided by libplatform. */
18void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size) {
19 if (escape_char == NULL || str == NULL || !out_buffer || out_buffer_size == 0) {
20 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid string or escape character or output buffer");
21 return;
22 }
23
24 // Count how many characters need escaping
25 size_t escape_count = 0;
26 for (size_t i = 0; i < strlen(str); i++) {
27 for (size_t j = 0; j < strlen(escape_char); j++) {
28 if (str[i] == escape_char[j]) {
29 escape_count++;
30 break;
31 }
32 }
33 }
34
35 size_t total_len = strlen(str) + escape_count;
36 if (out_buffer_size < total_len) {
37 SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer is too small");
38 return;
39 }
40
41 size_t out_pos = 0;
42 for (size_t i = 0; i < strlen(str); i++) {
43 bool needs_escape = false;
44 for (size_t j = 0; j < strlen(escape_char); j++) {
45 if (str[i] == escape_char[j]) {
46 needs_escape = true;
47 break;
48 }
49 }
50
51 if (needs_escape) {
52 out_buffer[out_pos++] = '\\';
53 }
54 out_buffer[out_pos++] = str[i];
55 }
56
57 out_buffer[out_pos] = '\0';
58}
59
60bool validate_shell_safe(const char *str, const char *allowed_chars) {
61 if (!str) {
62 return false;
63 }
64
65 // Shell metacharacters that are dangerous
66 // ; & | $ ` \ " ' < > ( ) [ ] { } * ? ! ~ # @ space tab newline
67 const char *dangerous = ";|$`\\\"'<>()[]{}*?!~#@ \t\n\r";
68
69 // First pass: validate UTF-8 encoding
70 if (!utf8_is_valid(str)) {
71 return false;
72 }
73
74 // Second pass: validate each character for shell safety
75 // Convert to codepoints and check only ASCII range for dangerous characters
76 // (non-ASCII characters are allowed by default)
77 const uint8_t *p = (const uint8_t *)str;
78 while (*p) {
79 uint32_t codepoint;
80 int decode_len = utf8_decode(p, &codepoint);
81 if (decode_len < 0) {
82 return false; // Should not happen after utf8_is_valid check
83 }
84
85 // Only validate ASCII range where shell metacharacters exist
86 // Non-ASCII characters (codepoint > 127) are allowed
87 if (codepoint <= 127) {
88 unsigned char c = (unsigned char)codepoint;
89
90 // Allow ASCII alphanumeric characters
91 if (isalnum(c)) {
92 p += decode_len;
93 continue;
94 }
95
96 // Check if character is in allowed_chars list
97 if (allowed_chars) {
98 bool is_allowed = false;
99 for (size_t j = 0; allowed_chars[j] != '\0'; j++) {
100 if (c == (unsigned char)allowed_chars[j]) {
101 is_allowed = true;
102 break;
103 }
104 }
105 if (is_allowed) {
106 p += decode_len;
107 continue;
108 }
109 }
110
111 // Check if character is dangerous
112 bool is_dangerous = false;
113 for (size_t j = 0; dangerous[j] != '\0'; j++) {
114 if (c == (unsigned char)dangerous[j]) {
115 is_dangerous = true;
116 break;
117 }
118 }
119 if (is_dangerous) {
120 return false;
121 }
122 }
123
124 p += decode_len;
125 }
126
127 return true;
128}
129
130bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size) {
131 if (!str || !out_buffer || out_buffer_size == 0) {
132 return false;
133 }
134
135 // Calculate required size: each ' becomes '\'' (4 chars) instead of 1
136 // Worst case: all chars are quotes, so we need strlen(str) * 4 + 2 (for surrounding quotes)
137 size_t required_size = 2; // Start and end quotes
138 for (size_t i = 0; str[i] != '\0'; i++) {
139 if (str[i] == '\'') {
140 required_size += 4; // '\''
141 } else {
142 required_size += 1;
143 }
144 }
145
146 if (out_buffer_size < required_size) {
147 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for shell escaping");
148 return false;
149 }
150
151 size_t out_pos = 0;
152 out_buffer[out_pos++] = '\''; // Opening quote
153
154 for (size_t i = 0; str[i] != '\0'; i++) {
155 if (str[i] == '\'') {
156 // Replace ' with '\'' (close quote, escaped quote, open quote)
157 out_buffer[out_pos++] = '\'';
158 out_buffer[out_pos++] = '\\';
159 out_buffer[out_pos++] = '\'';
160 out_buffer[out_pos++] = '\'';
161 } else {
162 out_buffer[out_pos++] = str[i];
163 }
164 }
165
166 out_buffer[out_pos++] = '\''; // Closing quote
167 out_buffer[out_pos] = '\0';
168
169 return true;
170}
171
172bool escape_shell_double_quotes(const char *str, char *out_buffer, size_t out_buffer_size) {
173 if (!str || !out_buffer || out_buffer_size == 0) {
174 return false;
175 }
176
177 // Calculate required size: each " becomes \" (2 chars) instead of 1
178 // Each \ also needs escaping, so \\ becomes \\\\
179 // Worst case: all chars are quotes/backslashes, so we need strlen(str) * 2 + 2 (for surrounding quotes)
180 size_t required_size = 2; // Start and end quotes
181 for (size_t i = 0; str[i] != '\0'; i++) {
182 if (str[i] == '"' || str[i] == '\\') {
183 required_size += 2; // \" or '\\'
184 } else {
185 required_size += 1;
186 }
187 }
188
189 if (out_buffer_size < required_size) {
190 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for shell escaping");
191 return false;
192 }
193
194 size_t out_pos = 0;
195 out_buffer[out_pos++] = '"'; // Opening quote
196
197 for (size_t i = 0; str[i] != '\0'; i++) {
198 if (str[i] == '"') {
199 // Escape double quote: " becomes \"
200 out_buffer[out_pos++] = '\\';
201 out_buffer[out_pos++] = '"';
202 } else if (str[i] == '\\') {
203 // Escape backslash: \ becomes '\\'
204 out_buffer[out_pos++] = '\\';
205 out_buffer[out_pos++] = '\\';
206 } else {
207 out_buffer[out_pos++] = str[i];
208 }
209 }
210
211 out_buffer[out_pos++] = '"'; // Closing quote
212 out_buffer[out_pos] = '\0';
213
214 return true;
215}
216
217bool string_needs_shell_quoting(const char *str) {
218 if (!str || str[0] == '\0') {
219 return false; // Empty strings don't need quoting
220 }
221
222 // Check for characters that need shell quoting
223 for (size_t i = 0; str[i] != '\0'; i++) {
224 char c = str[i];
225
226 // Whitespace characters always need quoting
227 if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
228 return true;
229 }
230
231 // Shell special characters
232 if (c == '"' || c == '\'' || c == '$' || c == '`' || c == '\\' || c == '<' || c == '>' || c == '&' || c == ';' ||
233 c == '|' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '*' || c == '?' ||
234 c == '!' || c == '~' || c == '#' || c == '@') {
235 return true;
236 }
237 }
238
239 return false;
240}
241
242bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size) {
243 if (!path || !out_buffer || out_buffer_size == 0) {
244 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid path or output buffer");
245 return false;
246 }
247
248 // Check if quoting is needed
249 if (!string_needs_shell_quoting(path)) {
250 // No quoting needed, just copy the path
251 size_t path_len = strlen(path);
252 if (out_buffer_size <= path_len) {
253 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for path");
254 return false;
255 }
256 SAFE_STRNCPY(out_buffer, path, out_buffer_size);
257 return true;
258 }
259
260 // Quoting is needed, use platform-specific escaping
261#ifdef _WIN32
262 return escape_shell_double_quotes(path, out_buffer, out_buffer_size);
263#else
264 return escape_shell_single_quotes(path, out_buffer, out_buffer_size);
265#endif
266}
267#endif
268
269// ============================================================================
270// String Formatting and Display
271// ============================================================================
272
273static _Thread_local int g_colored_string_fd = STDOUT_FILENO;
275 int previous = g_colored_string_fd;
276 if (fd < 0) {
277 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid colored string output descriptor");
278 return previous;
279 }
280 g_colored_string_fd = fd;
281 return previous;
282}
283
284const char *colored_string(log_color_t color, const char *text) {
285#define COLORED_BUFFERS 4
286#define COLORED_BUFFER_SIZE 256
287 static _Thread_local char buffers[COLORED_BUFFERS][COLORED_BUFFER_SIZE];
288 static _Thread_local int buffer_idx = 0;
289
290 if (!text) {
291 return "";
292 }
293
294 // Check if we should use colors
295 // Use global flag that persists even after options cleanup
296 bool use_colors = false;
297
298 // Priority 1: If --color was explicitly passed, force colors
300 use_colors = true;
301 }
302 // Priority 2: If --color NOT explicitly passed, use terminal detection
303 else if (!g_color_flag_passed) {
304 use_colors = terminal_should_color_output(g_colored_string_fd, options_terminal_settings());
305 }
306 // Priority 3: If --color=false was explicitly passed, disable colors
307 // (use_colors stays false)
308
309 if (GET_OPTION(strip_ansi) || GET_OPTION(color_mode) == TERM_COLOR_NONE)
310 use_colors = false;
311 if (!use_colors) {
312 // No colors, just return the text directly
313 return text;
314 }
315
316 // Use rotating buffer to handle multiple calls in same fprintf
317 char *current_buf = buffers[buffer_idx];
318 buffer_idx = (buffer_idx + 1) % COLORED_BUFFERS;
319
320 const char *color_code = log_level_color_for_output(color, true);
321 const char *reset_code = log_level_color_for_output(LOG_COLOR_RESET, true);
322
323 // Ensure we never pass NULL to snprintf (handle uninitialized color system)
324 if (!color_code)
325 color_code = "";
326 if (!reset_code)
327 reset_code = "";
328
329 // Format into rotating static buffer: color_code + text + reset_code
330 safe_snprintf(current_buf, COLORED_BUFFER_SIZE, "%s%s%s", color_code, text, reset_code);
331 return current_buf;
332}
333
334void truncate_with_ellipsis_mode(const char *input, char *output, size_t output_size, int max_width, bool utf8) {
335 if (!output || output_size == 0) {
336 return;
337 }
338 output[0] = '\0';
339 if (!input || max_width <= 0) {
340 return;
341 }
342
343 size_t input_size = strlen(input);
344 size_t byte_limit = input_size < output_size - 1 ? input_size : output_size - 1;
345 size_t copy = utf8_prefix_bytes_for_width(input, byte_limit, max_width);
346 if (copy == input_size) {
347 memcpy(output, input, copy);
348 output[copy] = '\0';
349 return;
350 }
351
352 // Reserve the complete reset, terminal-appropriate ellipsis, and terminator before copying content.
353 static const char utf8_suffix[] = "\033[0m…";
354 static const char ascii_suffix[] = "\033[0m...";
355 const char *suffix = utf8 ? utf8_suffix : ascii_suffix;
356 int suffix_width = utf8 ? 1 : 3;
357 size_t suffix_size = strlen(suffix) + 1;
358 if (output_size < suffix_size || max_width < suffix_width) {
359 return;
360 }
361 byte_limit = output_size - suffix_size;
362 if (byte_limit > input_size) {
363 byte_limit = input_size;
364 }
365 copy = utf8_prefix_bytes_for_width(input, byte_limit, max_width - suffix_width);
366 memcpy(output, input, copy);
367 memcpy(output + copy, suffix, suffix_size);
368}
369
370void truncate_with_ellipsis(const char *input, char *output, size_t output_size, int max_width) {
371 truncate_with_ellipsis_mode(input, output, output_size, max_width, utf8_output_enabled());
372}
373
374void strip_ansi_codes(const char *input, char *output, size_t output_size) {
375 if (!input || !output || output_size == 0) {
376 return;
377 }
378
379 size_t out_idx = 0;
380 size_t in_idx = 0;
381
382 while (input[in_idx] != '\0' && out_idx < output_size - 1) {
383 // Check for ANSI escape sequence: ESC[
384 // Standard ANSI codes use escape character (0x1B or \033) followed by [
385 if (input[in_idx] == '\033' && input[in_idx + 1] == '[') {
386 in_idx += 2; // Skip \033[
387
388 // Skip until we find the terminating letter (m is most common, also H, J, K, etc.)
389 // This handles patterns like: 38;5;74m, 0m, 38;2;255;0;0m, 2J, H, etc.
390 while (input[in_idx] != '\0') {
391 if ((input[in_idx] >= 'A' && input[in_idx] <= 'Z') || (input[in_idx] >= 'a' && input[in_idx] <= 'z')) {
392 in_idx++; // Skip the terminating letter
393 break;
394 }
395 in_idx++;
396 }
397 } else {
398 // Regular character (including [ in normal text), copy to output
399 output[out_idx++] = input[in_idx++];
400 }
401 }
402
403 output[out_idx] = '\0';
404}
#define SET_ERRNO(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
int color_mode
Definition fft.c:25
bool g_color_flag_value
Value of –color flag (true if –color was in argv) Set by options_init() before RCU is initialized,...
Definition common.c:66
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
bool g_color_flag_passed
Was –color explicitly passed in command-line arguments? Set by options_init() before RCU is initializ...
Definition common.c:65
unsigned char uint8_t
Definition common.h:60
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER_OVERFLOW
const char * log_level_color_for_output(log_color_t color, bool enabled)
Definition log.c:1942
log_color_t
Color enum for logging - indexes into color arrays.
Definition log/log.h:147
@ LOG_COLOR_RESET
Definition log/log.h:155
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
#define options_terminal_settings()
Definition options.h:1296
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
Definition string.c:284
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:128
int colored_string_set_output_fd(int fd)
Definition string.c:274
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
Decode a UTF-8 sequence to a Unicode codepoint.
Definition utf8.c:27
void strip_ansi_codes(const char *input, char *output, size_t output_size)
Strip ANSI escape codes from a string.
Definition string.c:374
bool escape_shell_double_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
Escape a string for safe use in shell commands (double quotes, Windows-compatible).
void truncate_with_ellipsis_mode(const char *input, char *output, size_t output_size, int max_width, bool utf8)
Truncate a string to a maximum display width with a terminal-appropriate ellipsis.
Definition string.c:334
bool utf8_output_enabled(void)
Return whether terminal output should use UTF-8 according to –utf8.
Definition utf8.c:22
bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
Validate that a string contains only safe characters for shell commands.
void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size)
Escape ASCII characters in a string.
bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size)
Check if a string needs shell quoting (contains spaces or special chars).
void truncate_with_ellipsis(const char *input, char *output, size_t output_size, int max_width)
Truncate a string to a maximum display width, with ellipsis.
Definition string.c:370
📝 Logging API with multiple log levels and terminal output control
#define COLORED_BUFFERS
#define COLORED_BUFFER_SIZE
🔤 String Manipulation and Shell Escaping Utilities
size_t utf8_prefix_bytes_for_width(const char *str, size_t max_bytes, int max_width)
Return the byte length of a prefix fitting a terminal column limit.
Definition utf8.c:93
🔤 UTF-8 Encoding and Decoding Utilities