C · 29424 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 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) {
869 set_system_error(ERR_SYSTEM_CALL, "Failed to get terminal size");
870 return -1;
871 }
872
873 *width = ws.ws_col;
874 *height = ws.ws_row;
875
876 return 0;
877 }
878
879 void disable_echo(void) {
880 struct termios new_termios;
881
882 if (g_echo_disabled) return;
883
884 if (tcgetattr(STDIN_FILENO, &g_original_termios) != 0) {
885 return; /* Can't save original, don't disable echo */
886 }
887
888 new_termios = g_original_termios;
889 new_termios.c_lflag &= ~ECHO;
890
891 if (tcsetattr(STDIN_FILENO, TCSANOW, &new_termios) == 0) {
892 g_echo_disabled = true;
893 }
894 }
895
896 void enable_echo(void) {
897 if (!g_echo_disabled) return;
898
899 tcsetattr(STDIN_FILENO, TCSANOW, &g_original_termios);
900 g_echo_disabled = false;
901 }
902
903 /* Time utilities */
904
905 void get_current_time_string(char *buffer, size_t buffer_size) {
906 time_t now;
907 struct tm *tm_info;
908
909 if (!buffer || buffer_size == 0) return;
910
911 time(&now);
912 tm_info = localtime(&now);
913
914 if (tm_info) {
915 strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", tm_info);
916 } else {
917 strncpy(buffer, "UNKNOWN", buffer_size - 1);
918 buffer[buffer_size - 1] = '\0';
919 }
920 }
921
922 void get_timestamp_string(char *buffer, size_t buffer_size) {
923 time_t now;
924
925 if (!buffer || buffer_size == 0) return;
926
927 time(&now);
928 snprintf(buffer, buffer_size, "%ld", (long)now);
929 }
930
931 bool is_timestamp_expired(time_t timestamp, int max_age_seconds) {
932 time_t now;
933 time(&now);
934 return (now - timestamp) > max_age_seconds;
935 }
936
937 /* Comparison utilities */
938
939 int compare_strings(const void *a, const void *b) {
940 return strcmp(*(const char **)a, *(const char **)b);
941 }
942
943 int compare_accounts_by_id(const void *a, const void *b) {
944 const account_t *acc_a = (const account_t *)a;
945 const account_t *acc_b = (const account_t *)b;
946
947 if (acc_a->id < acc_b->id) return -1;
948 if (acc_a->id > acc_b->id) return 1;
949 return 0;
950 }
951
952 int compare_accounts_by_name(const void *a, const void *b) {
953 const account_t *acc_a = (const account_t *)a;
954 const account_t *acc_b = (const account_t *)b;
955
956 return strcmp(acc_a->name, acc_b->name);
957 }
958
959 /* Array utilities */
960
961 void sort_accounts(account_t *accounts, size_t count,
962 int (*compare)(const void *, const void *)) {
963 if (accounts && count > 1 && compare) {
964 qsort(accounts, count, sizeof(account_t), compare);
965 }
966 }
967
968 account_t *find_account_in_array(account_t *accounts, size_t count,
969 const char *identifier) {
970 if (!accounts || !identifier || count == 0) {
971 return NULL;
972 }
973
974 /* Try numeric ID first */
975 char *endptr;
976 unsigned long id = strtoul(identifier, &endptr, 10);
977 if (*endptr == '\0') {
978 /* It's a number - search by ID */
979 for (size_t i = 0; i < count; i++) {
980 if (accounts[i].id == (uint32_t)id) {
981 return &accounts[i];
982 }
983 }
984 }
985
986 /* Search by name or description */
987 for (size_t i = 0; i < count; i++) {
988 if (strstr(accounts[i].name, identifier) ||
989 strstr(accounts[i].description, identifier) ||
990 strcmp(accounts[i].email, identifier) == 0) {
991 return &accounts[i];
992 }
993 }
994
995 return NULL;
996 }
997
998 /* Memory utilities */
999
1000 void *safe_memset(void *ptr, int value, size_t size) {
1001 if (!ptr || size == 0) {
1002 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_memset");
1003 return NULL;
1004 }
1005
1006 return memset(ptr, value, size);
1007 }
1008
1009 void *safe_memcpy(void *dest, const void *src, size_t size) {
1010 if (!dest || !src || size == 0) {
1011 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_memcpy");
1012 return NULL;
1013 }
1014
1015 return memcpy(dest, src, size);
1016 }
1017
1018 int safe_mlock(void *ptr, size_t size) {
1019 #if defined(__linux__)
1020 if (!ptr || size == 0) {
1021 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_mlock");
1022 return -1;
1023 }
1024
1025 if (mlock(ptr, size) != 0) {
1026 set_system_error(ERR_SYSTEM_CALL, "Failed to lock memory");
1027 return -1;
1028 }
1029
1030 return 0;
1031 #else
1032 /* Not supported on this platform */
1033 (void)ptr;
1034 (void)size;
1035 return 0;
1036 #endif
1037 }
1038
1039 int safe_munlock(void *ptr, size_t size) {
1040 #if defined(__linux__)
1041 if (!ptr || size == 0) {
1042 set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_munlock");
1043 return -1;
1044 }
1045
1046 if (munlock(ptr, size) != 0) {
1047 set_system_error(ERR_SYSTEM_CALL, "Failed to unlock memory");
1048 return -1;
1049 }
1050
1051 return 0;
1052 #else
1053 /* Not supported on this platform */
1054 (void)ptr;
1055 (void)size;
1056 return 0;
1057 #endif
1058 }
1059
1060 /* Cleanup utilities */
1061
1062 void cleanup_temporary_files(void) {
1063 /* Implementation would clean up any temporary files created */
1064 log_debug("Cleaning up temporary files");
1065 }
1066
1067 int register_cleanup_handler(void (*handler)(void)) {
1068 if (!handler) {
1069 set_error(ERR_INVALID_ARGS, "NULL handler to register_cleanup_handler");
1070 return -1;
1071 }
1072
1073 if (g_cleanup_handler_count >= sizeof(g_cleanup_handlers) / sizeof(g_cleanup_handlers[0])) {
1074 set_error(ERR_INVALID_ARGS, "Too many cleanup handlers registered");
1075 return -1;
1076 }
1077
1078 g_cleanup_handlers[g_cleanup_handler_count++] = handler;
1079 return 0;
1080 }
1081
1082 /* Debug utilities */
1083
1084 void dump_account(const account_t *account) {
1085 if (!account) {
1086 log_debug("Account: NULL");
1087 return;
1088 }
1089
1090 log_debug("Account dump:");
1091 log_debug(" ID: %u", account->id);
1092 log_debug(" Name: %s", account->name);
1093 log_debug(" Email: %s", account->email);
1094 log_debug(" Description: %s", account->description);
1095 log_debug(" SSH enabled: %s", account->ssh_enabled ? "yes" : "no");
1096 log_debug(" SSH key: %s", account->ssh_key_path);
1097 log_debug(" GPG enabled: %s", account->gpg_enabled ? "yes" : "no");
1098 log_debug(" GPG signing: %s", account->gpg_signing_enabled ? "yes" : "no");
1099 log_debug(" GPG key: %s", account->gpg_key_id);
1100 }
1101
1102 void dump_config(const config_t *config) {
1103 if (!config) {
1104 log_debug("Config: NULL");
1105 return;
1106 }
1107
1108 log_debug("Config dump:");
1109 log_debug(" Default scope: %d", config->default_scope);
1110 log_debug(" Config path: %s", config->config_path);
1111 log_debug(" Verbose: %s", config->verbose ? "yes" : "no");
1112 log_debug(" Dry run: %s", config->dry_run ? "yes" : "no");
1113 log_debug(" Color output: %s", config->color_output ? "yes" : "no");
1114 }
1115
1116 void dump_context(const gitswitch_ctx_t *ctx) {
1117 if (!ctx) {
1118 log_debug("Context: NULL");
1119 return;
1120 }
1121
1122 log_debug("Context dump:");
1123 log_debug(" Account count: %zu", ctx->account_count);
1124 log_debug(" Current account: %s",
1125 ctx->current_account ? ctx->current_account->name : "none");
1126
1127 dump_config(&ctx->config);
1128
1129 for (size_t i = 0; i < ctx->account_count; i++) {
1130 dump_account(&ctx->accounts[i]);
1131 }
1132 }