C · 29691 bytes Raw Blame History
1 /* Utility functions and helpers with security focus
2 * Provides secure, validated utility functions for gitswitch-c
3 */
4
5 #define _GNU_SOURCE
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <unistd.h>
10 #include <sys/stat.h>
11 #include <sys/types.h>
12 #include <sys/wait.h>
13 #include <sys/ioctl.h>
14 #include <fcntl.h>
15 #include <errno.h>
16 #include <pwd.h>
17 #include <termios.h>
18 #include <time.h>
19 #include <ctype.h>
20 #include <signal.h>
21 #include <regex.h>
22
23 #if defined(__linux__)
24 #include <sys/mman.h>
25 #include <linux/random.h>
26 #include <sys/syscall.h>
27 #endif
28
29 #include "utils.h"
30 #include "error.h"
31
32 /* Static variables for terminal state management */
33 static struct termios g_original_termios;
34 static bool g_echo_disabled = false;
35
36 /* Cleanup handlers registry */
37 static void (*g_cleanup_handlers[16])(void);
38 static size_t g_cleanup_handler_count = 0;
39
40 /* String utilities */
41
42 char *trim_whitespace(char *str) {
43 char *end;
44
45 if (!str) return NULL;
46
47 /* Trim leading space */
48 while (isspace((unsigned char)*str)) str++;
49
50 /* All spaces? */
51 if (*str == '\0') return str;
52
53 /* Trim trailing space */
54 end = str + strlen(str) - 1;
55 while (end > str && isspace((unsigned char)*end)) end--;
56
57 /* Write new null terminator */
58 end[1] = '\0';
59
60 return str;
61 }
62
63 bool string_empty(const char *str) {
64 return !str || *str == '\0';
65 }
66
67 bool string_equals(const char *a, const char *b) {
68 if (!a && !b) return true;
69 if (!a || !b) return false;
70 return strcmp(a, b) == 0;
71 }
72
73 bool string_starts_with(const char *str, const char *prefix) {
74 if (!str || !prefix) return false;
75 return strncmp(str, prefix, strlen(prefix)) == 0;
76 }
77
78 bool string_ends_with(const char *str, const char *suffix) {
79 if (!str || !suffix) return false;
80
81 size_t str_len = strlen(str);
82 size_t suffix_len = strlen(suffix);
83
84 if (suffix_len > str_len) return false;
85
86 return strcmp(str + str_len - suffix_len, suffix) == 0;
87 }
88
89 int string_replace(char *str, size_t str_size, const char *old, const char *new) {
90 if (!str || !old || !new) {
91 set_error(ERR_INVALID_ARGS, "NULL arguments to string_replace");
92 return -1;
93 }
94
95 char *pos = strstr(str, old);
96 if (!pos) return 0; /* No replacement needed */
97
98 size_t old_len = strlen(old);
99 size_t new_len = strlen(new);
100 size_t str_len = strlen(str);
101
102 /* Check if replacement would overflow buffer */
103 if (str_len - old_len + new_len >= str_size) {
104 set_error(ERR_INVALID_ARGS, "String replacement would overflow buffer");
105 return -1;
106 }
107
108 /* Move the rest of the string */
109 memmove(pos + new_len, pos + old_len, strlen(pos + old_len) + 1);
110
111 /* Copy new string */
112 memcpy(pos, new, new_len);
113
114 return 1;
115 }
116
117 /* Path utilities */
118
119 int expand_path(const char *path, char *expanded_path, size_t path_size) {
120 if (!path || !expanded_path || path_size == 0) {
121 set_error(ERR_INVALID_ARGS, "Invalid arguments to expand_path");
122 return -1;
123 }
124
125 /* Handle tilde expansion */
126 if (path[0] == '~') {
127 char home_path[MAX_PATH_LEN];
128
129 if (get_home_directory(home_path, sizeof(home_path)) != 0) {
130 return -1;
131 }
132
133 /* Handle ~/path and ~/ cases */
134 const char *rest = (path[1] == '/') ? path + 2 : path + 1;
135
136 if (snprintf(expanded_path, path_size, "%s/%s", home_path, rest) >= (int)path_size) {
137 set_error(ERR_INVALID_ARGS, "Expanded path too long");
138 return -1;
139 }
140 } else {
141 /* Path doesn't need expansion */
142 if (strlen(path) >= path_size) {
143 set_error(ERR_INVALID_ARGS, "Path too long for buffer");
144 return -1;
145 }
146 strcpy(expanded_path, path);
147 }
148
149 return 0;
150 }
151
152 int get_home_directory(char *home_path, size_t path_size) {
153 const char *home = getenv("HOME");
154
155 if (!home) {
156 /* Fall back to password database */
157 struct passwd *pw = getpwuid(getuid());
158 if (!pw) {
159 set_system_error(ERR_SYSTEM_CALL, "Failed to get user home directory");
160 return -1;
161 }
162 home = pw->pw_dir;
163 }
164
165 if (strlen(home) >= path_size) {
166 set_error(ERR_INVALID_ARGS, "Home directory path too long");
167 return -1;
168 }
169
170 strcpy(home_path, home);
171 return 0;
172 }
173
174 int join_path(char *result, size_t result_size, const char *base, const char *component) {
175 if (!result || !base || !component || result_size == 0) {
176 set_error(ERR_INVALID_ARGS, "Invalid arguments to join_path");
177 return -1;
178 }
179
180 size_t base_len = strlen(base);
181 size_t comp_len = strlen(component);
182 bool needs_separator = (base_len > 0 && base[base_len - 1] != '/') &&
183 (comp_len > 0 && component[0] != '/');
184
185 size_t total_len = base_len + comp_len + (needs_separator ? 1 : 0);
186
187 if (total_len >= result_size) {
188 set_error(ERR_INVALID_ARGS, "Joined path too long for buffer");
189 return -1;
190 }
191
192 strcpy(result, base);
193 if (needs_separator) {
194 strcat(result, "/");
195 }
196 strcat(result, component);
197
198 return 0;
199 }
200
201 bool path_exists(const char *path) {
202 struct stat st;
203 return path && stat(path, &st) == 0;
204 }
205
206 bool is_directory(const char *path) {
207 struct stat st;
208 return path && stat(path, &st) == 0 && S_ISDIR(st.st_mode);
209 }
210
211 bool is_regular_file(const char *path) {
212 struct stat st;
213 return path && stat(path, &st) == 0 && S_ISREG(st.st_mode);
214 }
215
216 int create_directory_recursive(const char *path, mode_t mode) {
217 if (!path) {
218 set_error(ERR_INVALID_ARGS, "NULL path to create_directory_recursive");
219 return -1;
220 }
221
222 char temp_path[MAX_PATH_LEN];
223 char *p = NULL;
224 size_t len;
225
226 if ((size_t)snprintf(temp_path, sizeof(temp_path), "%s", path) >= sizeof(temp_path)) {
227 set_error(ERR_INVALID_ARGS, "Path too long");
228 return -1;
229 }
230
231 len = strlen(temp_path);
232 if (temp_path[len - 1] == '/') {
233 temp_path[len - 1] = '\0';
234 }
235
236 for (p = temp_path + 1; *p; p++) {
237 if (*p == '/') {
238 *p = '\0';
239 if (mkdir(temp_path, mode) != 0 && errno != EEXIST) {
240 set_system_error(ERR_FILE_IO, "Failed to create directory: %s", temp_path);
241 return -1;
242 }
243 *p = '/';
244 }
245 }
246
247 if (mkdir(temp_path, mode) != 0 && errno != EEXIST) {
248 set_system_error(ERR_FILE_IO, "Failed to create directory: %s", temp_path);
249 return -1;
250 }
251
252 return 0;
253 }
254
255 int get_file_permissions(const char *path, mode_t *mode) {
256 struct stat st;
257
258 if (!path || !mode) {
259 set_error(ERR_INVALID_ARGS, "Invalid arguments to get_file_permissions");
260 return -1;
261 }
262
263 if (stat(path, &st) != 0) {
264 set_system_error(ERR_FILE_IO, "Failed to stat file: %s", path);
265 return -1;
266 }
267
268 *mode = st.st_mode & 07777; /* Only permission bits */
269 return 0;
270 }
271
272 int set_file_permissions(const char *path, mode_t mode) {
273 if (!path) {
274 set_error(ERR_INVALID_ARGS, "NULL path to set_file_permissions");
275 return -1;
276 }
277
278 if (chmod(path, mode) != 0) {
279 set_system_error(ERR_PERMISSION_DENIED, "Failed to set permissions on: %s", path);
280 return -1;
281 }
282
283 return 0;
284 }
285
286 /* File utilities */
287
288 int read_file_to_string(const char *file_path, char *buffer, size_t buffer_size) {
289 FILE *file;
290 size_t bytes_read;
291
292 if (!file_path || !buffer || buffer_size == 0) {
293 set_error(ERR_INVALID_ARGS, "Invalid arguments to read_file_to_string");
294 return -1;
295 }
296
297 file = fopen(file_path, "r");
298 if (!file) {
299 set_system_error(ERR_FILE_IO, "Failed to open file for reading: %s", file_path);
300 return -1;
301 }
302
303 bytes_read = fread(buffer, 1, buffer_size - 1, file);
304 if (ferror(file)) {
305 set_system_error(ERR_FILE_IO, "Failed to read from file: %s", file_path);
306 fclose(file);
307 return -1;
308 }
309
310 buffer[bytes_read] = '\0';
311 fclose(file);
312
313 return (int)bytes_read;
314 }
315
316 int write_string_to_file(const char *file_path, const char *content, mode_t mode) {
317 FILE *file;
318 size_t content_len, bytes_written;
319
320 if (!file_path || !content) {
321 set_error(ERR_INVALID_ARGS, "Invalid arguments to write_string_to_file");
322 return -1;
323 }
324
325 file = fopen(file_path, "w");
326 if (!file) {
327 set_system_error(ERR_FILE_IO, "Failed to open file for writing: %s", file_path);
328 return -1;
329 }
330
331 content_len = strlen(content);
332 bytes_written = fwrite(content, 1, content_len, file);
333
334 if (bytes_written != content_len) {
335 set_system_error(ERR_FILE_IO, "Failed to write complete content to: %s", file_path);
336 fclose(file);
337 return -1;
338 }
339
340 fclose(file);
341
342 /* Set file permissions */
343 if (set_file_permissions(file_path, mode) != 0) {
344 return -1;
345 }
346
347 return 0;
348 }
349
350 int copy_file(const char *src_path, const char *dst_path) {
351 FILE *src, *dst;
352 char buffer[4096];
353 size_t bytes;
354 int result = 0;
355
356 if (!src_path || !dst_path) {
357 set_error(ERR_INVALID_ARGS, "Invalid arguments to copy_file");
358 return -1;
359 }
360
361 src = fopen(src_path, "rb");
362 if (!src) {
363 set_system_error(ERR_FILE_IO, "Failed to open source file: %s", src_path);
364 return -1;
365 }
366
367 dst = fopen(dst_path, "wb");
368 if (!dst) {
369 set_system_error(ERR_FILE_IO, "Failed to open destination file: %s", dst_path);
370 fclose(src);
371 return -1;
372 }
373
374 while ((bytes = fread(buffer, 1, sizeof(buffer), src)) > 0) {
375 if (fwrite(buffer, 1, bytes, dst) != bytes) {
376 set_system_error(ERR_FILE_IO, "Failed to write to destination file: %s", dst_path);
377 result = -1;
378 break;
379 }
380 }
381
382 if (ferror(src)) {
383 set_system_error(ERR_FILE_IO, "Error reading source file: %s", src_path);
384 result = -1;
385 }
386
387 fclose(src);
388 fclose(dst);
389
390 /* Copy permissions from source to destination */
391 if (result == 0) {
392 struct stat src_stat;
393 if (stat(src_path, &src_stat) == 0) {
394 chmod(dst_path, src_stat.st_mode);
395 }
396 }
397
398 return result;
399 }
400
401 int backup_file(const char *file_path, const char *backup_suffix) {
402 char backup_path[MAX_PATH_LEN];
403
404 if (!file_path || !backup_suffix) {
405 set_error(ERR_INVALID_ARGS, "Invalid arguments to backup_file");
406 return -1;
407 }
408
409 if ((size_t)snprintf(backup_path, sizeof(backup_path), "%s%s",
410 file_path, backup_suffix) >= sizeof(backup_path)) {
411 set_error(ERR_INVALID_ARGS, "Backup path too long");
412 return -1;
413 }
414
415 return copy_file(file_path, backup_path);
416 }
417
418 bool file_is_readable(const char *file_path) {
419 return file_path && access(file_path, R_OK) == 0;
420 }
421
422 bool file_is_writable(const char *file_path) {
423 return file_path && access(file_path, W_OK) == 0;
424 }
425
426 size_t get_file_size(const char *file_path) {
427 struct stat st;
428
429 if (!file_path || stat(file_path, &st) != 0) {
430 return 0;
431 }
432
433 return (size_t)st.st_size;
434 }
435
436 time_t get_file_mtime(const char *file_path) {
437 struct stat st;
438
439 if (!file_path || stat(file_path, &st) != 0) {
440 return 0;
441 }
442
443 return st.st_mtime;
444 }
445
446 /* Process utilities */
447
448 int execute_command(const char *command, char *output, size_t output_size) {
449 return execute_command_with_input(command, NULL, output, output_size);
450 }
451
452 int execute_command_with_input(const char *command, const char *input,
453 char *output, size_t output_size) {
454 FILE *pipe;
455 char *line = NULL;
456 size_t len = 0;
457 ssize_t read_len;
458 int status;
459
460 if (!command) {
461 set_error(ERR_INVALID_ARGS, "NULL command to execute_command_with_input");
462 return -1;
463 }
464
465 /* Clear output buffer */
466 if (output && output_size > 0) {
467 output[0] = '\0';
468 }
469
470 pipe = popen(command, "r");
471 if (!pipe) {
472 set_system_error(ERR_SYSTEM_CALL, "Failed to execute command: %s", command);
473 return -1;
474 }
475
476 /* Write input to command if provided */
477 if (input) {
478 /* Note: This is simplified - full implementation would need bidirectional pipes */
479 log_warning("Input to command not fully implemented yet");
480 }
481
482 /* Read output */
483 if (output && output_size > 0) {
484 size_t total_read = 0;
485
486 while ((read_len = getline(&line, &len, pipe)) != -1 &&
487 total_read < output_size - 1) {
488
489 size_t to_copy = (size_t)read_len;
490 if (total_read + to_copy >= output_size) {
491 to_copy = output_size - total_read - 1;
492 }
493
494 memcpy(output + total_read, line, to_copy);
495 total_read += to_copy;
496 }
497
498 output[total_read] = '\0';
499
500 /* Remove trailing newline if present */
501 if (total_read > 0 && output[total_read - 1] == '\n') {
502 output[total_read - 1] = '\0';
503 }
504 }
505
506 free(line);
507 status = pclose(pipe);
508
509 if (status == -1) {
510 set_system_error(ERR_SYSTEM_CALL, "pclose failed for command: %s", command);
511 return -1;
512 }
513
514 return WEXITSTATUS(status);
515 }
516
517 bool command_exists(const char *command) {
518 char test_command[256];
519 int result;
520
521 if (!command) return false;
522
523 if ((size_t)snprintf(test_command, sizeof(test_command),
524 "command -v %s >/dev/null 2>&1", command) >= sizeof(test_command)) {
525 return false;
526 }
527
528 result = system(test_command);
529 return result == 0;
530 }
531
532 pid_t start_background_process(const char *command, char *pidfile_path) {
533 pid_t pid;
534 FILE *pidfile;
535
536 if (!command) {
537 set_error(ERR_INVALID_ARGS, "NULL command to start_background_process");
538 return -1;
539 }
540
541 pid = fork();
542 if (pid == -1) {
543 set_system_error(ERR_SYSTEM_CALL, "Failed to fork process");
544 return -1;
545 }
546
547 if (pid == 0) {
548 /* Child process */
549 setsid(); /* Create new session */
550
551 /* Redirect standard streams */
552 if (!freopen("/dev/null", "r", stdin)) {
553 /* Failed to redirect stdin, but continue */
554 }
555 if (!freopen("/dev/null", "w", stdout)) {
556 /* Failed to redirect stdout, but continue */
557 }
558 if (!freopen("/dev/null", "w", stderr)) {
559 /* Failed to redirect stderr, but continue */
560 }
561
562 /* Execute command */
563 execl("/bin/sh", "sh", "-c", command, (char *)NULL);
564 _exit(127); /* If exec fails */
565 }
566
567 /* Parent process */
568 if (pidfile_path) {
569 pidfile = fopen(pidfile_path, "w");
570 if (pidfile) {
571 fprintf(pidfile, "%d\n", pid);
572 fclose(pidfile);
573 }
574 }
575
576 return pid;
577 }
578
579 int kill_process_by_pidfile(const char *pidfile_path) {
580 FILE *pidfile;
581 pid_t pid;
582
583 if (!pidfile_path) {
584 set_error(ERR_INVALID_ARGS, "NULL pidfile path");
585 return -1;
586 }
587
588 pidfile = fopen(pidfile_path, "r");
589 if (!pidfile) {
590 set_system_error(ERR_FILE_IO, "Failed to open pidfile: %s", pidfile_path);
591 return -1;
592 }
593
594 if (fscanf(pidfile, "%d", &pid) != 1) {
595 set_error(ERR_FILE_IO, "Failed to read PID from file: %s", pidfile_path);
596 fclose(pidfile);
597 return -1;
598 }
599
600 fclose(pidfile);
601
602 if (pid <= 0) {
603 set_error(ERR_INVALID_ARGS, "Invalid PID in file: %d", pid);
604 return -1;
605 }
606
607 if (kill(pid, SIGTERM) != 0) {
608 if (errno == ESRCH) {
609 /* Process doesn't exist - clean up pidfile */
610 unlink(pidfile_path);
611 return 0;
612 }
613 set_system_error(ERR_SYSTEM_CALL, "Failed to kill process %d", pid);
614 return -1;
615 }
616
617 /* Clean up pidfile */
618 unlink(pidfile_path);
619
620 return 0;
621 }
622
623 bool process_is_running(pid_t pid) {
624 if (pid <= 0) return false;
625 return kill(pid, 0) == 0;
626 }
627
628 /* Environment utilities */
629
630 int get_env_var(const char *name, char *buffer, size_t buffer_size) {
631 const char *value;
632
633 if (!name || !buffer || buffer_size == 0) {
634 set_error(ERR_INVALID_ARGS, "Invalid arguments to get_env_var");
635 return -1;
636 }
637
638 value = getenv(name);
639 if (!value) {
640 buffer[0] = '\0';
641 return 1; /* Not an error, just not found */
642 }
643
644 if (strlen(value) >= buffer_size) {
645 set_error(ERR_INVALID_ARGS, "Environment variable value too long");
646 return -1;
647 }
648
649 strcpy(buffer, value);
650 return 0;
651 }
652
653 int set_env_var(const char *name, const char *value, bool overwrite) {
654 if (!name || !value) {
655 set_error(ERR_INVALID_ARGS, "Invalid arguments to set_env_var");
656 return -1;
657 }
658
659 if (setenv(name, value, overwrite ? 1 : 0) != 0) {
660 set_system_error(ERR_SYSTEM_CALL, "Failed to set environment variable: %s", name);
661 return -1;
662 }
663
664 return 0;
665 }
666
667 int unset_env_var(const char *name) {
668 if (!name) {
669 set_error(ERR_INVALID_ARGS, "NULL name to unset_env_var");
670 return -1;
671 }
672
673 if (unsetenv(name) != 0) {
674 set_system_error(ERR_SYSTEM_CALL, "Failed to unset environment variable: %s", name);
675 return -1;
676 }
677
678 return 0;
679 }
680
681 /* Validation utilities */
682
683 bool validate_email(const char *email) {
684 regex_t regex;
685 int result;
686
687 if (!email || strlen(email) > MAX_EMAIL_LEN) {
688 return false;
689 }
690
691 /* Basic email regex - not RFC compliant but good enough for git configs */
692 const char *pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
693
694 result = regcomp(&regex, pattern, REG_EXTENDED);
695 if (result) return false;
696
697 result = regexec(&regex, email, 0, NULL, 0);
698 regfree(&regex);
699
700 return result == 0;
701 }
702
703 bool validate_name(const char *name) {
704 if (!name || strlen(name) == 0 || strlen(name) >= MAX_NAME_LEN) {
705 return false;
706 }
707
708 /* Name should contain at least one non-whitespace character */
709 for (const char *p = name; *p; p++) {
710 if (!isspace((unsigned char)*p)) {
711 return true;
712 }
713 }
714
715 return false;
716 }
717
718 bool validate_key_id(const char *key_id) {
719 if (!key_id || strlen(key_id) == 0 || strlen(key_id) >= MAX_KEY_ID_LEN) {
720 return false;
721 }
722
723 /* Key ID should be hexadecimal */
724 for (const char *p = key_id; *p; p++) {
725 if (!isxdigit((unsigned char)*p)) {
726 return false;
727 }
728 }
729
730 return true;
731 }
732
733 bool validate_file_path(const char *path) {
734 char expanded[MAX_PATH_LEN];
735
736 if (!path || strlen(path) == 0 || strlen(path) >= MAX_PATH_LEN) {
737 return false;
738 }
739
740 /* Expand path and check if it exists */
741 if (expand_path(path, expanded, sizeof(expanded)) != 0) {
742 return false;
743 }
744
745 return path_exists(expanded);
746 }
747
748 /* Security utilities */
749
750 void secure_zero_memory(void *ptr, size_t size) {
751 if (!ptr || size == 0) return;
752
753 /* Use explicit_bzero if available, otherwise volatile memset */
754 #ifdef __GLIBC__
755 explicit_bzero(ptr, size);
756 #else
757 volatile unsigned char *p = ptr;
758 while (size--) {
759 *p++ = 0;
760 }
761 #endif
762 }
763
764 int generate_random_string(char *buffer, size_t buffer_size, const char *charset) {
765 size_t charset_len;
766 size_t i;
767 FILE *urandom;
768
769 if (!buffer || buffer_size == 0 || !charset) {
770 set_error(ERR_INVALID_ARGS, "Invalid arguments to generate_random_string");
771 return -1;
772 }
773
774 charset_len = strlen(charset);
775 if (charset_len == 0) {
776 set_error(ERR_INVALID_ARGS, "Empty charset");
777 return -1;
778 }
779
780 urandom = fopen("/dev/urandom", "rb");
781 if (!urandom) {
782 set_system_error(ERR_FILE_IO, "Failed to open /dev/urandom");
783 return -1;
784 }
785
786 for (i = 0; i < buffer_size - 1; i++) {
787 unsigned char rand_byte;
788 if (fread(&rand_byte, 1, 1, urandom) != 1) {
789 set_system_error(ERR_FILE_IO, "Failed to read random data");
790 fclose(urandom);
791 return -1;
792 }
793 buffer[i] = charset[rand_byte % charset_len];
794 }
795
796 buffer[buffer_size - 1] = '\0';
797 fclose(urandom);
798
799 return 0;
800 }
801
802 bool check_file_permissions_safe(const char *file_path, mode_t expected_mode) {
803 mode_t actual_mode;
804
805 if (!file_path) return false;
806
807 if (get_file_permissions(file_path, &actual_mode) != 0) {
808 return false;
809 }
810
811 /* Check if permissions are as expected or more restrictive */
812 return (actual_mode & 07777) == expected_mode;
813 }
814
815 /* Configuration utilities */
816
817 int get_config_directory(char *config_dir, size_t dir_size) {
818 char home[MAX_PATH_LEN];
819
820 if (!config_dir || dir_size == 0) {
821 set_error(ERR_INVALID_ARGS, "Invalid arguments to get_config_directory");
822 return -1;
823 }
824
825 if (get_home_directory(home, sizeof(home)) != 0) {
826 return -1;
827 }
828
829 if (snprintf(config_dir, dir_size, "%s/%s", home, DEFAULT_CONFIG_DIR) >= (int)dir_size) {
830 set_error(ERR_INVALID_ARGS, "Config directory path too long");
831 return -1;
832 }
833
834 return 0;
835 }
836
837 int ensure_config_directory_exists(void) {
838 char config_dir[MAX_PATH_LEN];
839
840 if (get_config_directory(config_dir, sizeof(config_dir)) != 0) {
841 return -1;
842 }
843
844 if (!is_directory(config_dir)) {
845 if (create_directory_recursive(config_dir, PERM_USER_RWX) != 0) {
846 return -1;
847 }
848 log_info("Created config directory: %s", config_dir);
849 }
850
851 return 0;
852 }
853
854 /* Terminal utilities */
855
856 bool is_terminal(int fd) {
857 return isatty(fd) == 1;
858 }
859
860 int get_terminal_size(int *width, int *height) {
861 struct winsize ws;
862
863 if (!width || !height) {
864 set_error(ERR_INVALID_ARGS, "NULL arguments to get_terminal_size");
865 return -1;
866 }
867
868 /* Skip the ioctl when stdout isn't a terminal (piped, redirected,
869 * command-substituted). Return failure silently so callers fall back to
870 * their default width without spamming stderr on every invocation. */
871 if (!isatty(STDOUT_FILENO)) {
872 return -1;
873 }
874
875 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) {
876 set_system_error(ERR_SYSTEM_CALL, "Failed to get terminal size");
877 return -1;
878 }
879
880 *width = ws.ws_col;
881 *height = ws.ws_row;
882
883 return 0;
884 }
885
886 void disable_echo(void) {
887 struct termios new_termios;
888
889 if (g_echo_disabled) return;
890
891 if (tcgetattr(STDIN_FILENO, &g_original_termios) != 0) {
892 return; /* Can't save original, don't disable echo */
893 }
894
895 new_termios = g_original_termios;
896 new_termios.c_lflag &= ~ECHO;
897
898 if (tcsetattr(STDIN_FILENO, TCSANOW, &new_termios) == 0) {
899 g_echo_disabled = true;
900 }
901 }
902
903 void enable_echo(void) {
904 if (!g_echo_disabled) return;
905
906 tcsetattr(STDIN_FILENO, TCSANOW, &g_original_termios);
907 g_echo_disabled = false;
908 }
909
910 /* Time utilities */
911
912 void get_current_time_string(char *buffer, size_t buffer_size) {
913 time_t now;
914 struct tm *tm_info;
915
916 if (!buffer || buffer_size == 0) return;
917
918 time(&now);
919 tm_info = localtime(&now);
920
921 if (tm_info) {
922 strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", tm_info);
923 } else {
924 strncpy(buffer, "UNKNOWN", buffer_size - 1);
925 buffer[buffer_size - 1] = '\0';
926 }
927 }
928
929 void get_timestamp_string(char *buffer, size_t buffer_size) {
930 time_t now;
931
932 if (!buffer || buffer_size == 0) return;
933
934 time(&now);
935 snprintf(buffer, buffer_size, "%ld", (long)now);
936 }
937
938 bool is_timestamp_expired(time_t timestamp, int max_age_seconds) {
939 time_t now;
940 time(&now);
941 return (now - timestamp) > max_age_seconds;
942 }
943
944 /* Comparison utilities */
945
946 int compare_strings(const void *a, const void *b) {
947 return strcmp(*(const char **)a, *(const char **)b);
948 }
949
950 int compare_accounts_by_id(const void *a, const void *b) {
951 const account_t *acc_a = (const account_t *)a;
952 const account_t *acc_b = (const account_t *)b;
953
954 if (acc_a->id < acc_b->id) return -1;
955 if (acc_a->id > acc_b->id) return 1;
956 return 0;
957 }
958
959 int compare_accounts_by_name(const void *a, const void *b) {
960 const account_t *acc_a = (const account_t *)a;
961 const account_t *acc_b = (const account_t *)b;
962
963 return strcmp(acc_a->name, acc_b->name);
964 }
965
966 /* Array utilities */
967
968 void sort_accounts(account_t *accounts, size_t count,
969 int (*compare)(const void *, const void *)) {
970 if (accounts && count > 1 && compare) {
971 qsort(accounts, count, sizeof(account_t), compare);
972 }
973 }
974
975 account_t *find_account_in_array(account_t *accounts, size_t count,
976 const char *identifier) {
977 if (!accounts || !identifier || count == 0) {
978 return NULL;
979 }
980
981 /* Try numeric ID first */
982 char *endptr;
983 unsigned long id = strtoul(identifier, &endptr, 10);
984 if (*endptr == '\0') {
985 /* It's a number - search by ID */
986 for (size_t i = 0; i < count; i++) {
987 if (accounts[i].id == (uint32_t)id) {
988 return &accounts[i];
989 }
990 }
991 }
992
993 /* Search by name or description */
994 for (size_t i = 0; i < count; i++) {
995 if (strstr(accounts[i].name, identifier) ||
996 strstr(accounts[i].description, identifier) ||
997 strcmp(accounts[i].email, identifier) == 0) {
998 return &accounts[i];
999 }
1000 }
1001
1002 return NULL;
1003 }
1004
1005 /* Memory utilities */
1006
1007 void *safe_memset(void *ptr, int value, size_t size) {
1008 if (!ptr || size == 0) {
1009 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_memset");
1010 return NULL;
1011 }
1012
1013 return memset(ptr, value, size);
1014 }
1015
1016 void *safe_memcpy(void *dest, const void *src, size_t size) {
1017 if (!dest || !src || size == 0) {
1018 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_memcpy");
1019 return NULL;
1020 }
1021
1022 return memcpy(dest, src, size);
1023 }
1024
1025 int safe_mlock(void *ptr, size_t size) {
1026 #if defined(__linux__)
1027 if (!ptr || size == 0) {
1028 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_mlock");
1029 return -1;
1030 }
1031
1032 if (mlock(ptr, size) != 0) {
1033 set_system_error(ERR_SYSTEM_CALL, "Failed to lock memory");
1034 return -1;
1035 }
1036
1037 return 0;
1038 #else
1039 /* Not supported on this platform */
1040 (void)ptr;
1041 (void)size;
1042 return 0;
1043 #endif
1044 }
1045
1046 int safe_munlock(void *ptr, size_t size) {
1047 #if defined(__linux__)
1048 if (!ptr || size == 0) {
1049 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_munlock");
1050 return -1;
1051 }
1052
1053 if (munlock(ptr, size) != 0) {
1054 set_system_error(ERR_SYSTEM_CALL, "Failed to unlock memory");
1055 return -1;
1056 }
1057
1058 return 0;
1059 #else
1060 /* Not supported on this platform */
1061 (void)ptr;
1062 (void)size;
1063 return 0;
1064 #endif
1065 }
1066
1067 /* Cleanup utilities */
1068
1069 void cleanup_temporary_files(void) {
1070 /* Implementation would clean up any temporary files created */
1071 log_debug("Cleaning up temporary files");
1072 }
1073
1074 int register_cleanup_handler(void (*handler)(void)) {
1075 if (!handler) {
1076 set_error(ERR_INVALID_ARGS, "NULL handler to register_cleanup_handler");
1077 return -1;
1078 }
1079
1080 if (g_cleanup_handler_count >= sizeof(g_cleanup_handlers) / sizeof(g_cleanup_handlers[0])) {
1081 set_error(ERR_INVALID_ARGS, "Too many cleanup handlers registered");
1082 return -1;
1083 }
1084
1085 g_cleanup_handlers[g_cleanup_handler_count++] = handler;
1086 return 0;
1087 }
1088
1089 /* Debug utilities */
1090
1091 void dump_account(const account_t *account) {
1092 if (!account) {
1093 log_debug("Account: NULL");
1094 return;
1095 }
1096
1097 log_debug("Account dump:");
1098 log_debug(" ID: %u", account->id);
1099 log_debug(" Name: %s", account->name);
1100 log_debug(" Email: %s", account->email);
1101 log_debug(" Description: %s", account->description);
1102 log_debug(" SSH enabled: %s", account->ssh_enabled ? "yes" : "no");
1103 log_debug(" SSH key: %s", account->ssh_key_path);
1104 log_debug(" GPG enabled: %s", account->gpg_enabled ? "yes" : "no");
1105 log_debug(" GPG signing: %s", account->gpg_signing_enabled ? "yes" : "no");
1106 log_debug(" GPG key: %s", account->gpg_key_id);
1107 }
1108
1109 void dump_config(const config_t *config) {
1110 if (!config) {
1111 log_debug("Config: NULL");
1112 return;
1113 }
1114
1115 log_debug("Config dump:");
1116 log_debug(" Default scope: %d", config->default_scope);
1117 log_debug(" Config path: %s", config->config_path);
1118 log_debug(" Verbose: %s", config->verbose ? "yes" : "no");
1119 log_debug(" Dry run: %s", config->dry_run ? "yes" : "no");
1120 log_debug(" Color output: %s", config->color_output ? "yes" : "no");
1121 }
1122
1123 void dump_context(const gitswitch_ctx_t *ctx) {
1124 if (!ctx) {
1125 log_debug("Context: NULL");
1126 return;
1127 }
1128
1129 log_debug("Context dump:");
1130 log_debug(" Account count: %zu", ctx->account_count);
1131 log_debug(" Current account: %s",
1132 ctx->current_account ? ctx->current_account->name : "none");
1133
1134 dump_config(&ctx->config);
1135
1136 for (size_t i = 0; i < ctx->account_count; i++) {
1137 dump_account(&ctx->accounts[i]);
1138 }
1139 }