ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
util/string.c
Go to the documentation of this file.
1
11#include <ascii-chat-deps/utf8proc/utf8proc.h>
12#include <ascii-chat/common.h>
16#include <ascii-chat/log/log.h>
17#include <string.h>
18#include <ctype.h>
19
20void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size) {
21 if (escape_char == NULL || str == NULL || !out_buffer || out_buffer_size == 0) {
22 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid string or escape character or output buffer");
23 return;
24 }
25
26 // Count how many characters need escaping
27 size_t escape_count = 0;
28 for (size_t i = 0; i < strlen(str); i++) {
29 for (size_t j = 0; j < strlen(escape_char); j++) {
30 if (str[i] == escape_char[j]) {
31 escape_count++;
32 break;
33 }
34 }
35 }
36
37 size_t total_len = strlen(str) + escape_count;
38 if (out_buffer_size < total_len) {
39 SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer is too small");
40 return;
41 }
42
43 size_t out_pos = 0;
44 for (size_t i = 0; i < strlen(str); i++) {
45 bool needs_escape = false;
46 for (size_t j = 0; j < strlen(escape_char); j++) {
47 if (str[i] == escape_char[j]) {
48 needs_escape = true;
49 break;
50 }
51 }
52
53 if (needs_escape) {
54 out_buffer[out_pos++] = '\\';
55 }
56 out_buffer[out_pos++] = str[i];
57 }
58
59 out_buffer[out_pos] = '\0';
60}
61
62bool validate_shell_safe(const char *str, const char *allowed_chars) {
63 if (!str) {
64 return false;
65 }
66
67 // Shell metacharacters that are dangerous
68 // ; & | $ ` \ " ' < > ( ) [ ] { } * ? ! ~ # @ space tab newline
69 const char *dangerous = ";|$`\\\"'<>()[]{}*?!~#@ \t\n\r";
70
71 // First pass: validate UTF-8 encoding
72 if (!utf8_is_valid(str)) {
73 return false;
74 }
75
76 // Second pass: validate each character for shell safety
77 // Convert to codepoints and check only ASCII range for dangerous characters
78 // (non-ASCII characters are allowed by default)
79 const uint8_t *p = (const uint8_t *)str;
80 while (*p) {
81 uint32_t codepoint;
82 int decode_len = utf8_decode(p, &codepoint);
83 if (decode_len < 0) {
84 return false; // Should not happen after utf8_is_valid check
85 }
86
87 // Only validate ASCII range where shell metacharacters exist
88 // Non-ASCII characters (codepoint > 127) are allowed
89 if (codepoint <= 127) {
90 unsigned char c = (unsigned char)codepoint;
91
92 // Allow ASCII alphanumeric characters
93 if (isalnum(c)) {
94 p += decode_len;
95 continue;
96 }
97
98 // Check if character is in allowed_chars list
99 if (allowed_chars) {
100 bool is_allowed = false;
101 for (size_t j = 0; allowed_chars[j] != '\0'; j++) {
102 if (c == (unsigned char)allowed_chars[j]) {
103 is_allowed = true;
104 break;
105 }
106 }
107 if (is_allowed) {
108 p += decode_len;
109 continue;
110 }
111 }
112
113 // Check if character is dangerous
114 bool is_dangerous = false;
115 for (size_t j = 0; dangerous[j] != '\0'; j++) {
116 if (c == (unsigned char)dangerous[j]) {
117 is_dangerous = true;
118 break;
119 }
120 }
121 if (is_dangerous) {
122 return false;
123 }
124 }
125
126 p += decode_len;
127 }
128
129 return true;
130}
131
132bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size) {
133 if (!str || !out_buffer || out_buffer_size == 0) {
134 return false;
135 }
136
137 // Calculate required size: each ' becomes '\'' (4 chars) instead of 1
138 // Worst case: all chars are quotes, so we need strlen(str) * 4 + 2 (for surrounding quotes)
139 size_t required_size = 2; // Start and end quotes
140 for (size_t i = 0; str[i] != '\0'; i++) {
141 if (str[i] == '\'') {
142 required_size += 4; // '\''
143 } else {
144 required_size += 1;
145 }
146 }
147
148 if (out_buffer_size < required_size) {
149 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for shell escaping");
150 return false;
151 }
152
153 size_t out_pos = 0;
154 out_buffer[out_pos++] = '\''; // Opening quote
155
156 for (size_t i = 0; str[i] != '\0'; i++) {
157 if (str[i] == '\'') {
158 // Replace ' with '\'' (close quote, escaped quote, open quote)
159 out_buffer[out_pos++] = '\'';
160 out_buffer[out_pos++] = '\\';
161 out_buffer[out_pos++] = '\'';
162 out_buffer[out_pos++] = '\'';
163 } else {
164 out_buffer[out_pos++] = str[i];
165 }
166 }
167
168 out_buffer[out_pos++] = '\''; // Closing quote
169 out_buffer[out_pos] = '\0';
170
171 return true;
172}
173
174bool escape_shell_double_quotes(const char *str, char *out_buffer, size_t out_buffer_size) {
175 if (!str || !out_buffer || out_buffer_size == 0) {
176 return false;
177 }
178
179 // Calculate required size: each " becomes \" (2 chars) instead of 1
180 // Each \ also needs escaping, so \\ becomes \\\\
181 // Worst case: all chars are quotes/backslashes, so we need strlen(str) * 2 + 2 (for surrounding quotes)
182 size_t required_size = 2; // Start and end quotes
183 for (size_t i = 0; str[i] != '\0'; i++) {
184 if (str[i] == '"' || str[i] == '\\') {
185 required_size += 2; // \" or '\\'
186 } else {
187 required_size += 1;
188 }
189 }
190
191 if (out_buffer_size < required_size) {
192 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for shell escaping");
193 return false;
194 }
195
196 size_t out_pos = 0;
197 out_buffer[out_pos++] = '"'; // Opening quote
198
199 for (size_t i = 0; str[i] != '\0'; i++) {
200 if (str[i] == '"') {
201 // Escape double quote: " becomes \"
202 out_buffer[out_pos++] = '\\';
203 out_buffer[out_pos++] = '"';
204 } else if (str[i] == '\\') {
205 // Escape backslash: \ becomes '\\'
206 out_buffer[out_pos++] = '\\';
207 out_buffer[out_pos++] = '\\';
208 } else {
209 out_buffer[out_pos++] = str[i];
210 }
211 }
212
213 out_buffer[out_pos++] = '"'; // Closing quote
214 out_buffer[out_pos] = '\0';
215
216 return true;
217}
218
219bool string_needs_shell_quoting(const char *str) {
220 if (!str || str[0] == '\0') {
221 return false; // Empty strings don't need quoting
222 }
223
224 // Check for characters that need shell quoting
225 for (size_t i = 0; str[i] != '\0'; i++) {
226 char c = str[i];
227
228 // Whitespace characters always need quoting
229 if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
230 return true;
231 }
232
233 // Shell special characters
234 if (c == '"' || c == '\'' || c == '$' || c == '`' || c == '\\' || c == '<' || c == '>' || c == '&' || c == ';' ||
235 c == '|' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '*' || c == '?' ||
236 c == '!' || c == '~' || c == '#' || c == '@') {
237 return true;
238 }
239 }
240
241 return false;
242}
243
244bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size) {
245 if (!path || !out_buffer || out_buffer_size == 0) {
246 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid path or output buffer");
247 return false;
248 }
249
250 // Check if quoting is needed
251 if (!string_needs_shell_quoting(path)) {
252 // No quoting needed, just copy the path
253 size_t path_len = strlen(path);
254 if (out_buffer_size <= path_len) {
255 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for path");
256 return false;
257 }
258 SAFE_STRNCPY(out_buffer, path, out_buffer_size);
259 return true;
260 }
261
262 // Quoting is needed, use platform-specific escaping
263#ifdef _WIN32
264 return escape_shell_double_quotes(path, out_buffer, out_buffer_size);
265#else
266 return escape_shell_single_quotes(path, out_buffer, out_buffer_size);
267#endif
268}
269
270// ============================================================================
271// String Formatting and Display
272// ============================================================================
273
274const char *colored_string(log_color_t color, const char *text) {
275#define COLORED_BUFFERS 4
276#define COLORED_BUFFER_SIZE 256
277 static char buffers[COLORED_BUFFERS][COLORED_BUFFER_SIZE];
278 static int buffer_idx = 0;
279
280 if (!text) {
281 return "";
282 }
283
284 // Check if we should use colors
285 // Use global flag that persists even after options cleanup
286 bool use_colors = false;
287
288 // Priority 1: If --color was explicitly passed, force colors
290 use_colors = true;
291 }
292 // Priority 2: If --color NOT explicitly passed, use terminal detection
293 else if (!g_color_flag_passed) {
294 use_colors = terminal_should_color_output(STDOUT_FILENO);
295 }
296 // Priority 3: If --color=false was explicitly passed, disable colors
297 // (use_colors stays false)
298
299 if (!use_colors) {
300 // No colors, just return the text directly
301 return text;
302 }
303
304 // Use rotating buffer to handle multiple calls in same fprintf
305 char *current_buf = buffers[buffer_idx];
306 buffer_idx = (buffer_idx + 1) % COLORED_BUFFERS;
307
308 const char *color_code = log_level_color(color);
309 const char *reset_code = log_level_color(LOG_COLOR_RESET);
310
311 // Ensure we never pass NULL to snprintf (handle uninitialized color system)
312 if (!color_code)
313 color_code = "";
314 if (!reset_code)
315 reset_code = "";
316
317 // Format into rotating static buffer: color_code + text + reset_code
318 safe_snprintf(current_buf, COLORED_BUFFER_SIZE, "%s%s%s", color_code, text, reset_code);
319 return current_buf;
320}
321
322void truncate_with_ellipsis(const char *input, char *output, size_t output_size, int max_width) {
323 if (!input || !output || output_size < 4) {
324 if (output && output_size > 0) {
325 output[0] = '\0';
326 }
327 return;
328 }
329
330 int content_width = display_width(input);
331
332 // Content fits — copy as-is
333 if (content_width <= max_width) {
334 size_t len = strlen(input);
335 if (len >= output_size) {
336 len = output_size - 1;
337 }
338 memcpy(output, input, len);
339 output[len] = '\0';
340 return;
341 }
342
343 // Need to truncate. Walk forward copying bytes while counting visible
344 // columns. ANSI escape sequences are copied whole (zero display width).
345 // UTF-8 multi-byte characters are decoded to get their display width
346 // (e.g., CJK = 2 columns, emoji = 2 columns) and copied as whole
347 // codepoints so we never split a multi-byte sequence.
348 const char *src = input;
349 const char *src_end = input + strlen(input);
350 char *dst = output;
351 char *dst_end = output + output_size - 10; // Reserve for reset + ellipsis + NUL
352 int cols = 0;
353 int target = max_width - 1; // Reserve 1 column for "…"
354
355 if (target < 0) {
356 target = 0;
357 }
358
359 while (src < src_end && dst < dst_end) {
360 if (*src == '\x1b') {
361 // Copy the entire escape sequence (zero display width)
362 const char *esc_end = ansi_skip_escape(src, src_end);
363 size_t esc_len = (size_t)(esc_end - src);
364 if (dst + esc_len < dst_end) {
365 memcpy(dst, src, esc_len);
366 dst += esc_len;
367 }
368 src = esc_end;
369 } else {
370 // Decode one UTF-8 codepoint to get its byte length and display width
371 utf8proc_int32_t codepoint;
372 utf8proc_ssize_t cp_len = utf8proc_iterate((const utf8proc_uint8_t *)src, src_end - src, &codepoint);
373 if (cp_len <= 0) {
374 // Invalid UTF-8 — copy single byte, count as 1 column
375 cp_len = 1;
376 codepoint = (unsigned char)*src;
377 }
378
379 int char_width = utf8proc_charwidth(codepoint);
380 if (char_width < 0) {
381 char_width = 0; // Control characters
382 }
383
384 if (cols + char_width > target) {
385 break;
386 }
387
388 // Copy the full codepoint bytes
389 if (dst + cp_len < dst_end) {
390 memcpy(dst, src, (size_t)cp_len);
391 dst += cp_len;
392 }
393 src += cp_len;
394 cols += char_width;
395 }
396 }
397
398 // Reset colors and append ellipsis (U+2026, 3 bytes)
399 size_t remaining = output_size - (size_t)(dst - output);
400 int n = snprintf(dst, remaining, "\033[0m…");
401 if (n > 0) {
402 dst += n;
403 }
404 *dst = '\0';
405}
406
407void strip_ansi_codes(const char *input, char *output, size_t output_size) {
408 if (!input || !output || output_size == 0) {
409 return;
410 }
411
412 size_t out_idx = 0;
413 size_t in_idx = 0;
414
415 while (input[in_idx] != '\0' && out_idx < output_size - 1) {
416 // Check for ANSI escape sequence: ESC[
417 // Standard ANSI codes use escape character (0x1B or \033) followed by [
418 if (input[in_idx] == '\033' && input[in_idx + 1] == '[') {
419 in_idx += 2; // Skip \033[
420
421 // Skip until we find the terminating letter (m is most common, also H, J, K, etc.)
422 // This handles patterns like: 38;5;74m, 0m, 38;2;255;0;0m, 2J, H, etc.
423 while (input[in_idx] != '\0') {
424 if ((input[in_idx] >= 'A' && input[in_idx] <= 'Z') || (input[in_idx] >= 'a' && input[in_idx] <= 'z')) {
425 in_idx++; // Skip the terminating letter
426 break;
427 }
428 in_idx++;
429 }
430 } else {
431 // Regular character (including [ in normal text), copy to output
432 output[out_idx++] = input[in_idx++];
433 }
434 }
435
436 output[out_idx] = '\0';
437}
ANSI escape sequence utilities and fast color code generation.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
const char * ansi_skip_escape(const char *ptr, const char *end)
Find the end of an ANSI CSI escape sequence.
Definition ansi.c:74
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:58
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
bool g_color_flag_passed
Was –color explicitly passed in command-line arguments? Set by options_init() before RCU is initializ...
Definition common.c:57
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER_OVERFLOW
const char * log_level_color(log_color_t color)
Get color string for a given color enum.
Definition log/log.c:1656
log_color_t
Color enum for logging - indexes into color arrays.
Definition log/log.h:130
@ LOG_COLOR_RESET
Definition log/log.h:138
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:156
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
Decode a UTF-8 sequence to a Unicode codepoint.
Definition utf8.c:18
void strip_ansi_codes(const char *input, char *output, size_t output_size)
Strip ANSI escape codes from a string.
int display_width(const char *text)
Calculate the visible display width of text with ANSI escape codes.
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)
bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
Escape a string for safe use in shell commands (single quotes)
bool string_needs_shell_quoting(const char *str)
Check if a string needs shell quoting (contains spaces or special chars)
bool validate_shell_safe(const char *str, const char *allowed_chars)
Validate that a string contains only safe characters for shell commands.
Definition util/string.c:62
void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size)
Escape ASCII characters in a string.
Definition util/string.c:20
bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size)
Escape a path for safe use in shell commands (auto-platform)
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.
🖥️ Display Width and Terminal Centering Utilities
📝 Logging API with multiple log levels and terminal output control
bool terminal_should_color_output(int fd)
Determine if color output should be used.
Cross-platform system functions interface for ascii-chat.
🖥️ Cross-platform terminal interface for ascii-chat
🔤 UTF-8 Encoding and Decoding Utilities
#define COLORED_BUFFERS
#define COLORED_BUFFER_SIZE
🔤 String Manipulation and Shell Escaping Utilities