tenseleyflow/gitswitch / 6fa4564

Browse files

source

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6fa45645fb89058eab83497c5f9ec9040c303373
Parents
56b2994
Tree
3a0df48

12 changed files

StatusFile+-
A src/accounts.c 873 0
A src/config.c 817 0
A src/display.c 765 0
A src/error.c 468 0
A src/git_ops.c 682 0
A src/gpg_manager.c 709 0
A src/main.c 398 0
A src/main_simple.c 132 0
A src/ssh_manager.c 829 0
A src/stubs.c 29 0
A src/toml_parser.c 1064 0
A src/utils.c 1126 0
src/accounts.cadded
@@ -0,0 +1,873 @@
1
+/* Account management and operations with comprehensive security validation
2
+ * Implements secure account switching and management for gitswitch-c
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <stdlib.h>
7
+#include <string.h>
8
+#include <ctype.h>
9
+#include <sys/stat.h>
10
+#include <unistd.h>
11
+
12
+#include "accounts.h"
13
+#include "config.h"
14
+#include "display.h"
15
+#include "error.h"
16
+#include "utils.h"
17
+#include "git_ops.h"
18
+#include "ssh_manager.h"
19
+#include "gpg_manager.h"
20
+
21
+/* Internal helper functions */
22
+static uint32_t get_next_available_id(const gitswitch_ctx_t *ctx);
23
+static int validate_ssh_key_security(const char *ssh_key_path);
24
+static int validate_gpg_key_availability(const char *gpg_key_id);
25
+static int test_ssh_key_functionality(const account_t *account);
26
+static int test_gpg_key_functionality(const account_t *account);
27
+
28
+/* Initialize accounts system */
29
+int accounts_init(gitswitch_ctx_t *ctx) {
30
+    if (!ctx) {
31
+        set_error(ERR_INVALID_ARGS, "NULL context to accounts_init");
32
+        return -1;
33
+    }
34
+    
35
+    /* Initialize account array */
36
+    memset(ctx->accounts, 0, sizeof(ctx->accounts));
37
+    ctx->account_count = 0;
38
+    ctx->current_account = NULL;
39
+    
40
+    log_debug("Accounts system initialized");
41
+    return 0;
42
+}
43
+
44
+/* Switch to specified account with SSH isolation and validation */
45
+int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
46
+    account_t *account;
47
+    
48
+    if (!ctx || !identifier) {
49
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to accounts_switch");
50
+        return -1;
51
+    }
52
+    
53
+    /* Find the account */
54
+    account = config_find_account(ctx, identifier);
55
+    if (!account) {
56
+        set_error(ERR_ACCOUNT_NOT_FOUND, "Account not found: %s", identifier);
57
+        return -1;
58
+    }
59
+    
60
+    /* Basic validation */
61
+    if (!validate_name(account->name) || !validate_email(account->email)) {
62
+        set_error(ERR_ACCOUNT_INVALID, "Account has invalid name or email");
63
+        return -1;
64
+    }
65
+    
66
+    /* Determine git scope - use account preference or context default */
67
+    git_scope_t scope = account->preferred_scope;
68
+    if (scope == GIT_SCOPE_LOCAL && !git_is_repository()) {
69
+        log_warning("Account prefers local scope, but not in git repository. Using global scope.");
70
+        scope = GIT_SCOPE_GLOBAL;
71
+    }
72
+    
73
+    /* Initialize git operations if not already done */
74
+    if (git_ops_init() != 0) {
75
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to initialize git operations");
76
+        return -1;
77
+    }
78
+    
79
+    /* If not in dry-run mode, actually set git configuration */
80
+    if (!ctx->config.dry_run) {
81
+        log_info("Setting git configuration for account: %s (%s scope)", 
82
+                 account->name, scope == GIT_SCOPE_LOCAL ? "local" : "global");
83
+                 
84
+        if (git_set_config(account, scope) != 0) {
85
+            set_error(ERR_GIT_CONFIG_FAILED, "Failed to set git configuration: %s", 
86
+                      get_last_error()->message);
87
+            return -1;
88
+        }
89
+        
90
+        /* Validate the configuration was set correctly */
91
+        if (git_test_config(account, scope) != 0) {
92
+            log_warning("Git configuration validation failed: %s", get_last_error()->message);
93
+            /* Don't fail completely, just warn */
94
+        }
95
+        
96
+        /* Handle SSH agent isolation if SSH is enabled */
97
+        if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
98
+            log_info("Setting up SSH isolation for account: %s", account->name);
99
+            
100
+            /* Initialize SSH manager with isolated agents */
101
+            ssh_config_t ssh_config = {0};
102
+            if (ssh_manager_init(&ssh_config, SSH_AGENT_ISOLATED) != 0) {
103
+                log_warning("Failed to initialize SSH manager: %s", get_last_error()->message);
104
+            } else {
105
+                /* Switch to account's SSH configuration */
106
+                if (ssh_switch_account(&ssh_config, account) != 0) {
107
+                    log_warning("Failed to switch SSH configuration: %s", get_last_error()->message);
108
+                    /* Clean up SSH manager on failure */
109
+                    ssh_manager_cleanup(&ssh_config);
110
+                } else {
111
+                    log_info("SSH isolation activated for account: %s", account->name);
112
+                    
113
+                    /* Test SSH connection if connection testing is available */
114
+                    if (strlen(account->ssh_host_alias) > 0) {
115
+                        if (ssh_test_connection(account, account->ssh_host_alias) == 0) {
116
+                            log_info("SSH connection test passed for %s", account->ssh_host_alias);
117
+                        } else {
118
+                            log_warning("SSH connection test failed for %s", account->ssh_host_alias);
119
+                        }
120
+                    } else {
121
+                        /* Test with default GitHub host */
122
+                        if (ssh_test_connection(account, "github.com") == 0) {
123
+                            log_info("SSH connection test passed for github.com");
124
+                        } else {
125
+                            log_debug("SSH connection test failed for github.com (this may be normal)");
126
+                        }
127
+                    }
128
+                }
129
+            }
130
+        }
131
+        
132
+        /* Handle GPG environment isolation if GPG is enabled */
133
+        if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
134
+            log_info("Setting up GPG isolation for account: %s", account->name);
135
+            
136
+            /* Initialize GPG manager with isolated environments */
137
+            gpg_config_t gpg_config = {0};
138
+            if (gpg_manager_init(&gpg_config, GPG_MODE_ISOLATED) != 0) {
139
+                log_warning("Failed to initialize GPG manager: %s", get_last_error()->message);
140
+            } else {
141
+                /* Switch to account's GPG configuration */
142
+                if (gpg_switch_account(&gpg_config, account) != 0) {
143
+                    log_warning("Failed to switch GPG configuration: %s", get_last_error()->message);
144
+                    /* Clean up GPG manager on failure */
145
+                    gpg_manager_cleanup(&gpg_config);
146
+                } else {
147
+                    log_info("GPG isolation activated for account: %s", account->name);
148
+                    
149
+                    /* Configure git GPG signing */
150
+                    if (gpg_configure_git_signing(&gpg_config, account, scope) != 0) {
151
+                        log_warning("Failed to configure git GPG signing: %s", get_last_error()->message);
152
+                    } else {
153
+                        log_info("Git GPG signing configured for account: %s", account->name);
154
+                    }
155
+                }
156
+            }
157
+        }
158
+    } else {
159
+        display_info("DRY RUN: Would set git configuration for %s", account->name);
160
+        if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
161
+            display_info("DRY RUN: Would activate SSH isolation for %s", account->ssh_key_path);
162
+        }
163
+        if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
164
+            display_info("DRY RUN: Would activate GPG isolation for key %s", account->gpg_key_id);
165
+        }
166
+    }
167
+    
168
+    /* Test SSH functionality if enabled (basic validation) */
169
+    if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
170
+        if (test_ssh_key_functionality(account) != 0) {
171
+            log_warning("SSH key test failed for account: %s", account->name);
172
+        }
173
+    }
174
+    
175
+    /* Test GPG functionality if enabled */
176
+    if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
177
+        if (test_gpg_key_functionality(account) != 0) {
178
+            log_warning("GPG key test failed for account: %s", account->name);
179
+        }
180
+    }
181
+    
182
+    /* Set as current account */
183
+    ctx->current_account = account;
184
+    
185
+    log_info("Successfully switched to account: %s (%s)", account->name, account->description);
186
+    return 0;
187
+}
188
+
189
+/* Add new account interactively with basic validation */
190
+int accounts_add_interactive(gitswitch_ctx_t *ctx) {
191
+    account_t new_account;
192
+    char input[512];
193
+    char expanded_path[MAX_PATH_LEN];
194
+    
195
+    if (!ctx) {
196
+        set_error(ERR_INVALID_ARGS, "NULL context to accounts_add_interactive");
197
+        return -1;
198
+    }
199
+    
200
+    if (ctx->account_count >= MAX_ACCOUNTS) {
201
+        set_error(ERR_ACCOUNT_EXISTS, "Maximum number of accounts reached: %d", MAX_ACCOUNTS);
202
+        return -1;
203
+    }
204
+    
205
+    /* Initialize new account */
206
+    memset(&new_account, 0, sizeof(new_account));
207
+    new_account.id = get_next_available_id(ctx);
208
+    new_account.preferred_scope = ctx->config.default_scope;
209
+    
210
+    printf("\n┌─────────────────────────────────────┐\n");
211
+    printf("│          Add New Account            │\n");
212
+    printf("└─────────────────────────────────────┘\n\n");
213
+    
214
+    /* Get account name */
215
+    do {
216
+        printf("Account Name: ");
217
+        fflush(stdout);
218
+        
219
+        if (!fgets(input, sizeof(input), stdin)) {
220
+            set_error(ERR_FILE_IO, "Failed to read account name");
221
+            return -1;
222
+        }
223
+        
224
+        input[strcspn(input, "\n")] = '\0';
225
+        trim_whitespace(input);
226
+        
227
+        if (!validate_name(input)) {
228
+            printf("[ERROR]: Invalid name. Please enter a non-empty name.\n");
229
+            continue;
230
+        }
231
+        
232
+        safe_strncpy(new_account.name, input, sizeof(new_account.name));
233
+        break;
234
+    } while (1);
235
+    
236
+    /* Get email address */
237
+    do {
238
+        printf("Email Address: ");
239
+        fflush(stdout);
240
+        
241
+        if (!fgets(input, sizeof(input), stdin)) {
242
+            set_error(ERR_FILE_IO, "Failed to read email address");
243
+            return -1;
244
+        }
245
+        
246
+        input[strcspn(input, "\n")] = '\0';
247
+        trim_whitespace(input);
248
+        
249
+        if (!validate_email(input)) {
250
+            printf("[ERROR]: Invalid email address format.\n");
251
+            continue;
252
+        }
253
+        
254
+        safe_strncpy(new_account.email, input, sizeof(new_account.email));
255
+        break;
256
+    } while (1);
257
+    
258
+    /* Get description */
259
+    printf("Description (optional): ");
260
+    fflush(stdout);
261
+    
262
+    if (fgets(input, sizeof(input), stdin)) {
263
+        input[strcspn(input, "\n")] = '\0';
264
+        trim_whitespace(input);
265
+        
266
+        if (strlen(input) > 0) {
267
+            safe_strncpy(new_account.description, input, sizeof(new_account.description));
268
+        } else {
269
+            safe_strncpy(new_account.description, new_account.name, sizeof(new_account.description));
270
+        }
271
+    } else {
272
+        safe_strncpy(new_account.description, new_account.name, sizeof(new_account.description));
273
+    }
274
+    
275
+    /* Get SSH key configuration */
276
+    printf("SSH Key Path (optional, press Enter to skip): ");
277
+    fflush(stdout);
278
+    
279
+    if (fgets(input, sizeof(input), stdin)) {
280
+        input[strcspn(input, "\n")] = '\0';
281
+        trim_whitespace(input);
282
+        
283
+        if (strlen(input) > 0) {
284
+            /* Expand and validate path */
285
+            if (expand_path(input, expanded_path, sizeof(expanded_path)) == 0) {
286
+                if (path_exists(expanded_path)) {
287
+                    if (validate_ssh_key_security(expanded_path) == 0) {
288
+                        safe_strncpy(new_account.ssh_key_path, expanded_path, sizeof(new_account.ssh_key_path));
289
+                        new_account.ssh_enabled = true;
290
+                        printf("[OK]: SSH key validated: %s\n", expanded_path);
291
+                        
292
+                        /* Optional SSH host alias */
293
+                        printf("SSH Host Alias (optional, e.g., github.com-work): ");
294
+                        fflush(stdout);
295
+                        
296
+                        if (fgets(input, sizeof(input), stdin)) {
297
+                            input[strcspn(input, "\n")] = '\0';
298
+                            trim_whitespace(input);
299
+                            
300
+                            if (strlen(input) > 0) {
301
+                                safe_strncpy(new_account.ssh_host_alias, input, sizeof(new_account.ssh_host_alias));
302
+                            }
303
+                        }
304
+                    } else {
305
+                        printf("[ERROR]: SSH key validation failed. Continuing without SSH key.\n");
306
+                    }
307
+                } else {
308
+                    printf("[ERROR]: SSH key file not found: %s\n", expanded_path);
309
+                }
310
+            } else {
311
+                printf("[ERROR]: Invalid SSH key path: %s\n", input);
312
+            }
313
+        }
314
+    }
315
+    
316
+    /* Get GPG key configuration */
317
+    printf("GPG Key ID (optional, press Enter to skip): ");
318
+    fflush(stdout);
319
+    
320
+    if (fgets(input, sizeof(input), stdin)) {
321
+        input[strcspn(input, "\n")] = '\0';
322
+        trim_whitespace(input);
323
+        
324
+        if (strlen(input) > 0) {
325
+            if (validate_key_id(input)) {
326
+                if (validate_gpg_key_availability(input) == 0) {
327
+                    safe_strncpy(new_account.gpg_key_id, input, sizeof(new_account.gpg_key_id));
328
+                    new_account.gpg_enabled = true;
329
+                    printf("[OK]: GPG key validated: %s\n", input);
330
+                    
331
+                    /* Ask about GPG signing */
332
+                    printf("Enable GPG signing for commits? (y/N): ");
333
+                    fflush(stdout);
334
+                    
335
+                    if (fgets(input, sizeof(input), stdin)) {
336
+                        input[strcspn(input, "\n")] = '\0';
337
+                        trim_whitespace(input);
338
+                        
339
+                        new_account.gpg_signing_enabled = (tolower(input[0]) == 'y');
340
+                    }
341
+                } else {
342
+                    printf("[ERROR]: GPG key validation failed. Continuing without GPG key.\n");
343
+                }
344
+            } else {
345
+                printf("[ERROR]: Invalid GPG key ID format: %s\n", input);
346
+            }
347
+        }
348
+    }
349
+    
350
+    /* Get preferred scope */
351
+    printf("Preferred Git Scope (local/global) [%s]: ", 
352
+           config_scope_to_string(new_account.preferred_scope));
353
+    fflush(stdout);
354
+    
355
+    if (fgets(input, sizeof(input), stdin)) {
356
+        input[strcspn(input, "\n")] = '\0';
357
+        trim_whitespace(input);
358
+        
359
+        if (strlen(input) > 0) {
360
+            new_account.preferred_scope = config_parse_scope(input);
361
+        }
362
+    }
363
+    
364
+    /* Basic validation */
365
+    if (!validate_name(new_account.name) || !validate_email(new_account.email)) {
366
+        printf("[ERROR]: Account validation failed: Invalid name or email\n");
367
+        return -1;
368
+    }
369
+    
370
+    /* Confirmation */
371
+    printf("\nAccount Summary:\n");
372
+    printf("   ID: %u\n", new_account.id);
373
+    printf("   Name: %s\n", new_account.name);
374
+    printf("   Email: %s\n", new_account.email);
375
+    printf("   Description: %s\n", new_account.description);
376
+    printf("   Scope: %s\n", config_scope_to_string(new_account.preferred_scope));
377
+    printf("   SSH: %s\n", new_account.ssh_enabled ? "[ENABLED]" : "[DISABLED]");
378
+    printf("   GPG: %s\n", new_account.gpg_enabled ? "[ENABLED]" : "[DISABLED]");
379
+    
380
+    printf("\nAdd this account? (y/N): ");
381
+    fflush(stdout);
382
+    
383
+    if (!fgets(input, sizeof(input), stdin)) {
384
+        set_error(ERR_FILE_IO, "Failed to read confirmation");
385
+        return -1;
386
+    }
387
+    
388
+    input[strcspn(input, "\n")] = '\0';
389
+    trim_whitespace(input);
390
+    
391
+    if (tolower(input[0]) != 'y') {
392
+        printf("Account creation cancelled.\n");
393
+        return -1;
394
+    }
395
+    
396
+    /* Add account to context */
397
+    if (config_add_account(ctx, &new_account) != 0) {
398
+        return -1;
399
+    }
400
+    
401
+    printf("[OK]: Account added successfully!\n");
402
+    return 0;
403
+}
404
+
405
+/* Remove account with confirmation and cleanup */
406
+int accounts_remove(gitswitch_ctx_t *ctx, const char *identifier) {
407
+    account_t *account;
408
+    char input[64];
409
+    
410
+    if (!ctx || !identifier) {
411
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to accounts_remove");
412
+        return -1;
413
+    }
414
+    
415
+    /* Find the account */
416
+    account = config_find_account(ctx, identifier);
417
+    if (!account) {
418
+        set_error(ERR_ACCOUNT_NOT_FOUND, "Account not found: %s", identifier);
419
+        return -1;
420
+    }
421
+    
422
+    /* Show account details */
423
+    printf("\nRemove Account\n");
424
+    printf("─────────────────\n");
425
+    printf("ID: %u\n", account->id);
426
+    printf("Name: %s\n", account->name);
427
+    printf("Email: %s\n", account->email);
428
+    printf("Description: %s\n", account->description);
429
+    
430
+    /* Confirmation */
431
+    printf("\n[WARN]: This will permanently remove the account from configuration.\n");
432
+    printf("Are you sure? (type 'yes' to confirm): ");
433
+    fflush(stdout);
434
+    
435
+    if (!fgets(input, sizeof(input), stdin)) {
436
+        set_error(ERR_FILE_IO, "Failed to read confirmation");
437
+        return -1;
438
+    }
439
+    
440
+    input[strcspn(input, "\n")] = '\0';
441
+    trim_whitespace(input);
442
+    
443
+    if (strcmp(input, "yes") != 0) {
444
+        printf("Account removal cancelled.\n");
445
+        return 0;
446
+    }
447
+    
448
+    /* Clear current account if it's the one being removed */
449
+    if (ctx->current_account == account) {
450
+        ctx->current_account = NULL;
451
+    }
452
+    
453
+    uint32_t account_id = account->id;
454
+    
455
+    /* Remove account */
456
+    if (config_remove_account(ctx, account_id) != 0) {
457
+        return -1;
458
+    }
459
+    
460
+    printf("[OK]: Account removed successfully.\n");
461
+    return 0;
462
+}
463
+
464
+/* List all configured accounts */
465
+int accounts_list(const gitswitch_ctx_t *ctx) {
466
+    if (!ctx) {
467
+        set_error(ERR_INVALID_ARGS, "NULL context to accounts_list");
468
+        return -1;
469
+    }
470
+    
471
+    if (ctx->account_count == 0) {
472
+        printf("\n[INFO]: No accounts configured.\n");
473
+        printf("Run 'gitswitch add' to create your first account.\n\n");
474
+        return 0;
475
+    }
476
+    
477
+    printf("\nConfigured Accounts (%zu total)\n", ctx->account_count);
478
+    printf("════════════════════════════════════════════════════════════════\n");
479
+    
480
+    for (size_t i = 0; i < ctx->account_count; i++) {
481
+        const account_t *account = &ctx->accounts[i];
482
+        bool is_current = (ctx->current_account == account);
483
+        
484
+        printf("%s [%u] %s\n", is_current ? "[CURRENT]" : "", account->id, account->name);
485
+        printf("     Email: %s\n", account->email);
486
+        printf("     Description: %s\n", account->description);
487
+        printf("     Scope: %s\n", config_scope_to_string(account->preferred_scope));
488
+        
489
+        if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
490
+            printf("     SSH Key: %s\n", account->ssh_key_path);
491
+            if (strlen(account->ssh_host_alias) > 0) {
492
+                printf("         Host: %s\n", account->ssh_host_alias);
493
+            }
494
+        } else {
495
+            printf("     SSH Key: Not configured\n");
496
+        }
497
+        
498
+        if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
499
+            printf("     GPG Key: %s %s\n", account->gpg_key_id,
500
+                   account->gpg_signing_enabled ? "(signing enabled)" : "(signing disabled)");
501
+        } else {
502
+            printf("     GPG Key: Not configured\n");
503
+        }
504
+        
505
+        if (i < ctx->account_count - 1) {
506
+            printf("\n");
507
+        }
508
+    }
509
+    
510
+    printf("════════════════════════════════════════════════════════════════\n\n");
511
+    
512
+    if (ctx->current_account) {
513
+        printf("Current: %s (%s)\n\n", ctx->current_account->name, ctx->current_account->description);
514
+    } else {
515
+        printf("No account currently active.\n\n");
516
+    }
517
+    
518
+    return 0;
519
+}
520
+
521
+/* Show current account status */
522
+int accounts_show_status(const gitswitch_ctx_t *ctx) {
523
+    if (!ctx) {
524
+        set_error(ERR_INVALID_ARGS, "NULL context to accounts_show_status");
525
+        return -1;
526
+    }
527
+    
528
+    printf("\nAccount Status\n");
529
+    printf("════════════════\n");
530
+    
531
+    if (ctx->current_account) {
532
+        const account_t *account = ctx->current_account;
533
+        
534
+        printf("Active Account: %s (ID: %u)\n", account->name, account->id);
535
+        printf("Email: %s\n", account->email);
536
+        printf("Description: %s\n", account->description);
537
+        printf("Preferred Scope: %s\n", config_scope_to_string(account->preferred_scope));
538
+        
539
+        /* SSH Status */
540
+        printf("\nSSH Configuration:\n");
541
+        if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
542
+            printf("  Status: [ENABLED]\n");
543
+            printf("  Key: %s\n", account->ssh_key_path);
544
+            
545
+            if (path_exists(account->ssh_key_path)) {
546
+                printf("  Key File: [FOUND]\n");
547
+                
548
+                mode_t key_mode;
549
+                if (get_file_permissions(account->ssh_key_path, &key_mode) == 0) {
550
+                    if ((key_mode & 077) == 0) {
551
+                        printf("  Permissions: [SECURE] (600)\n");
552
+                    } else {
553
+                        printf("  Permissions: [WARN] Insecure (%o)\n", key_mode & 0777);
554
+                    }
555
+                }
556
+            } else {
557
+                printf("  Key File: [NOT FOUND]\n");
558
+            }
559
+            
560
+            if (strlen(account->ssh_host_alias) > 0) {
561
+                printf("  Host Alias: %s\n", account->ssh_host_alias);
562
+            }
563
+        } else {
564
+            printf("  Status: [DISABLED]\n");
565
+        }
566
+        
567
+        /* GPG Status */
568
+        printf("\nGPG Configuration:\n");
569
+        if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
570
+            printf("  Status: [ENABLED]\n");
571
+            printf("  Key ID: %s\n", account->gpg_key_id);
572
+            printf("  Signing: %s\n", account->gpg_signing_enabled ? "[ENABLED]" : "[DISABLED]");
573
+        } else {
574
+            printf("  Status: [DISABLED]\n");
575
+        }
576
+        
577
+        /* Git Configuration Status */
578
+        printf("\nGit Configuration:\n");
579
+        git_current_config_t git_config;
580
+        if (git_get_current_config(&git_config) == 0) {
581
+            printf("  Current Name: %s\n", git_config.name);
582
+            printf("  Current Email: %s\n", git_config.email);
583
+            printf("  Configuration Scope: %s\n", 
584
+                   git_config.scope == GIT_SCOPE_LOCAL ? "local" : 
585
+                   git_config.scope == GIT_SCOPE_GLOBAL ? "global" : "system");
586
+            
587
+            /* Check if git config matches account */
588
+            if (strcmp(git_config.name, account->name) == 0 &&
589
+                strcmp(git_config.email, account->email) == 0) {
590
+                printf("  Match Status: [OK] Git config matches account\n");
591
+            } else {
592
+                printf("  Match Status: [WARN] Git config does not match account\n");
593
+                printf("    Expected: %s <%s>\n", account->name, account->email);
594
+                printf("    Current:  %s <%s>\n", git_config.name, git_config.email);
595
+            }
596
+            
597
+            /* GPG signing status */
598
+            if (strlen(git_config.signing_key) > 0) {
599
+                printf("  GPG Signing Key: %s\n", git_config.signing_key);
600
+                printf("  GPG Signing Enabled: %s\n", git_config.gpg_signing_enabled ? "[YES]" : "[NO]");
601
+            } else {
602
+                printf("  GPG Signing: [NOT CONFIGURED]\n");
603
+            }
604
+        } else {
605
+            printf("  Status: [NOT FOUND] No git configuration found\n");
606
+        }
607
+        
608
+        /* Repository context */
609
+        printf("\nRepository Context:\n");
610
+        if (git_is_repository()) {
611
+            char repo_root[MAX_PATH_LEN];
612
+            if (git_get_repo_root(repo_root, sizeof(repo_root)) == 0) {
613
+                printf("  Repository: [FOUND] %s\n", repo_root);
614
+            } else {
615
+                printf("  Repository: [REPOSITORY] Current directory is a git repository\n");
616
+            }
617
+        } else {
618
+            printf("  Repository: [NO REPOSITORY] Not in a git repository\n");
619
+        }
620
+        
621
+    } else {
622
+        printf("No account currently active.\n");
623
+        printf("Run 'gitswitch list' to see available accounts.\n");
624
+        printf("Run 'gitswitch <account>' to activate an account.\n");
625
+        
626
+        /* Show current git config even without active account */
627
+        printf("\nCurrent Git Configuration:\n");
628
+        git_current_config_t git_config;
629
+        if (git_get_current_config(&git_config) == 0) {
630
+            printf("  Name: %s\n", git_config.name);
631
+            printf("  Email: %s\n", git_config.email);
632
+            printf("  Scope: %s\n", 
633
+                   git_config.scope == GIT_SCOPE_LOCAL ? "local" : 
634
+                   git_config.scope == GIT_SCOPE_GLOBAL ? "global" : "system");
635
+        } else {
636
+            printf("  Status: [NOT FOUND] No git configuration found\n");
637
+        }
638
+        
639
+        /* Repository context */
640
+        printf("\nRepository Context:\n");
641
+        if (git_is_repository()) {
642
+            printf("  Repository: [REPOSITORY] Current directory is a git repository\n");
643
+        } else {
644
+            printf("  Repository: [NO REPOSITORY] Not in a git repository\n");
645
+        }
646
+    }
647
+    
648
+    printf("\n");
649
+    return 0;
650
+}
651
+
652
+/* Simple account validation for Phase 2 */
653
+int accounts_validate(const account_t *account) {
654
+    if (!account) {
655
+        set_error(ERR_INVALID_ARGS, "NULL account pointer");
656
+        return -1;
657
+    }
658
+    
659
+    /* Validate required fields */
660
+    if (!validate_name(account->name)) {
661
+        set_error(ERR_ACCOUNT_INVALID, "Invalid or empty account name");
662
+        return -1;
663
+    }
664
+    
665
+    if (!validate_email(account->email)) {
666
+        set_error(ERR_ACCOUNT_INVALID, "Invalid email address format");
667
+        return -1;
668
+    }
669
+    
670
+    /* Basic SSH validation if enabled */
671
+    if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
672
+        char expanded_path[MAX_PATH_LEN];
673
+        
674
+        if (expand_path(account->ssh_key_path, expanded_path, sizeof(expanded_path)) != 0) {
675
+            set_error(ERR_ACCOUNT_INVALID, "Invalid SSH key path: %s", account->ssh_key_path);
676
+            return -1;
677
+        }
678
+        
679
+        if (!path_exists(expanded_path)) {
680
+            set_error(ERR_ACCOUNT_INVALID, "SSH key file not found: %s", expanded_path);
681
+            return -1;
682
+        }
683
+    }
684
+    
685
+    /* Basic GPG validation if enabled */
686
+    if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
687
+        if (!validate_key_id(account->gpg_key_id)) {
688
+            set_error(ERR_ACCOUNT_INVALID, "Invalid GPG key ID format: %s", account->gpg_key_id);
689
+            return -1;
690
+        }
691
+    }
692
+    
693
+    return 0;
694
+}
695
+
696
+/* Get next available account ID */
697
+static uint32_t get_next_available_id(const gitswitch_ctx_t *ctx) {
698
+    uint32_t max_id = 0;
699
+    
700
+    if (!ctx) return 1;
701
+    
702
+    /* Find the highest existing ID */
703
+    for (size_t i = 0; i < ctx->account_count; i++) {
704
+        if (ctx->accounts[i].id > max_id) {
705
+            max_id = ctx->accounts[i].id;
706
+        }
707
+    }
708
+    
709
+    return max_id + 1;
710
+}
711
+
712
+/* Validate SSH key security */
713
+static int validate_ssh_key_security(const char *ssh_key_path) {
714
+    FILE *key_file;
715
+    char first_line[256];
716
+    mode_t file_mode;
717
+    
718
+    if (!ssh_key_path || !path_exists(ssh_key_path)) {
719
+        return -1;
720
+    }
721
+    
722
+    /* Check file permissions */
723
+    if (get_file_permissions(ssh_key_path, &file_mode) != 0) {
724
+        return -1;
725
+    }
726
+    
727
+    if ((file_mode & 077) != 0) {
728
+        log_warning("SSH key file has insecure permissions: %o", file_mode & 0777);
729
+        return -1;
730
+    }
731
+    
732
+    /* Check if it looks like a valid SSH key */
733
+    key_file = fopen(ssh_key_path, "r");
734
+    if (!key_file) {
735
+        return -1;
736
+    }
737
+    
738
+    if (fgets(first_line, sizeof(first_line), key_file)) {
739
+        /* Check for common SSH key formats */
740
+        if (!string_starts_with(first_line, "-----BEGIN OPENSSH PRIVATE KEY-----") &&
741
+            !string_starts_with(first_line, "-----BEGIN RSA PRIVATE KEY-----") &&
742
+            !string_starts_with(first_line, "-----BEGIN DSA PRIVATE KEY-----") &&
743
+            !string_starts_with(first_line, "-----BEGIN EC PRIVATE KEY-----") &&
744
+            !string_starts_with(first_line, "-----BEGIN SSH2 PRIVATE KEY-----")) {
745
+            fclose(key_file);
746
+            log_warning("SSH key file format not recognized");
747
+            return -1;
748
+        }
749
+    }
750
+    
751
+    fclose(key_file);
752
+    return 0;
753
+}
754
+
755
+/* Validate GPG key availability */
756
+static int validate_gpg_key_availability(const char *gpg_key_id) {
757
+    char command[256];
758
+    int result;
759
+    
760
+    if (!gpg_key_id) {
761
+        return -1;
762
+    }
763
+    
764
+    /* Try to find the key in the GPG keyring */
765
+    if (snprintf(command, sizeof(command), "gpg --list-secret-keys %s >/dev/null 2>&1", 
766
+                gpg_key_id) >= sizeof(command)) {
767
+        log_error("GPG command too long");
768
+        return -1;
769
+    }
770
+    
771
+    result = system(command);
772
+    if (result != 0) {
773
+        log_debug("GPG key %s not found in keyring", gpg_key_id);
774
+        return -1;
775
+    }
776
+    
777
+    return 0;
778
+}
779
+
780
+/* Test SSH key functionality */
781
+static int test_ssh_key_functionality(const account_t *account) {
782
+    /* This is a placeholder for SSH functionality testing
783
+     * In a full implementation, this would:
784
+     * 1. Start SSH agent if needed
785
+     * 2. Load the key into agent
786
+     * 3. Test connection to a known host
787
+     * 4. Verify authentication works
788
+     */
789
+    log_debug("SSH key functionality test for %s: %s", 
790
+              account->name, account->ssh_key_path);
791
+    
792
+    /* For now, just validate the key file exists and has correct permissions */
793
+    return validate_ssh_key_security(account->ssh_key_path);
794
+}
795
+
796
+/* Test GPG key functionality */
797
+static int test_gpg_key_functionality(const account_t *account) {
798
+    /* This is a placeholder for GPG functionality testing
799
+     * In a full implementation, this would:
800
+     * 1. Set up GPG environment
801
+     * 2. Test key can be used for signing
802
+     * 3. Verify key is not expired
803
+     * 4. Test signing a test message
804
+     */
805
+    log_debug("GPG key functionality test for %s: %s", 
806
+              account->name, account->gpg_key_id);
807
+    
808
+    /* For now, just check if key exists in keyring */
809
+    return validate_gpg_key_availability(account->gpg_key_id);
810
+}
811
+
812
+/* Run comprehensive health check on all accounts */
813
+int accounts_health_check(const gitswitch_ctx_t *ctx) {
814
+    bool all_healthy = true;
815
+    
816
+    if (!ctx) {
817
+        set_error(ERR_INVALID_ARGS, "NULL context to accounts_health_check");
818
+        return -1;
819
+    }
820
+    
821
+    printf("\nAccount Health Check\n");
822
+    printf("══════════════════════\n");
823
+    
824
+    if (ctx->account_count == 0) {
825
+        printf("[ERROR]: No accounts configured\n");
826
+        printf("   Run 'gitswitch add' to create your first account\n\n");
827
+        return -1;
828
+    }
829
+    
830
+    for (size_t i = 0; i < ctx->account_count; i++) {
831
+        const account_t *account = &ctx->accounts[i];
832
+        int validation_result = accounts_validate(account);
833
+        
834
+        printf("\n[%u] %s\n", account->id, account->name);
835
+        printf("────────────────────────\n");
836
+        
837
+        if (validation_result == 0) {
838
+            printf("[OK]: Account configuration valid\n");
839
+            
840
+            /* Test SSH if configured */
841
+            if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
842
+                if (test_ssh_key_functionality(account) == 0) {
843
+                    printf("[OK]: SSH key functional\n");
844
+                } else {
845
+                    printf("[ERROR]: SSH key issues detected\n");
846
+                    all_healthy = false;
847
+                }
848
+            }
849
+            
850
+            /* Test GPG if configured */
851
+            if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
852
+                if (test_gpg_key_functionality(account) == 0) {
853
+                    printf("[OK]: GPG key functional\n");
854
+                } else {
855
+                    printf("[ERROR]: GPG key issues detected\n");
856
+                    all_healthy = false;
857
+                }
858
+            }
859
+        } else {
860
+            printf("[ERROR]: Account validation failed\n");
861
+            all_healthy = false;
862
+        }
863
+    }
864
+    
865
+    printf("\n══════════════════════\n");
866
+    if (all_healthy) {
867
+        printf("[OK]: All accounts are healthy\n\n");
868
+        return 0;
869
+    } else {
870
+        printf("[ERROR]: Some accounts have issues\n\n");
871
+        return -1;
872
+    }
873
+}
src/config.cadded
@@ -0,0 +1,817 @@
1
+/* Configuration file management with comprehensive security validation
2
+ * Implements secure TOML-based configuration for gitswitch-c
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <stdlib.h>
7
+#include <string.h>
8
+#include <sys/stat.h>
9
+#include <time.h>
10
+#include <unistd.h>
11
+
12
+#include "config.h"
13
+#include "toml_parser.h"
14
+#include "error.h"
15
+#include "utils.h"
16
+
17
+/* Default configuration template with security-focused defaults */
18
+const char *default_config_template = 
19
+"# gitswitch-c Configuration File\n"
20
+"# This file contains sensitive information - ensure proper permissions (600)\n"
21
+"\n"
22
+"[settings]\n"
23
+"# Default scope for git configuration changes\n"
24
+"# Options: \"local\" (repository-specific) or \"global\" (user-wide)\n"
25
+"default_scope = \"local\"\n"
26
+"\n"
27
+"# Example account configuration\n"
28
+"# Uncomment and modify for your accounts\n"
29
+"\n"
30
+"#[accounts.1]\n"
31
+"#name = \"Your Name\"\n"
32
+"#email = \"your.email@example.com\"\n"
33
+"#description = \"Personal Account\"\n"
34
+"#preferred_scope = \"local\"\n"
35
+"#ssh_key = \"~/.ssh/id_ed25519_personal\"\n"
36
+"#gpg_key = \"1234567890ABCDEF\"\n"
37
+"#gpg_signing_enabled = true\n"
38
+"\n"
39
+"#[accounts.2]\n"
40
+"#name = \"Your Name\"\n"
41
+"#email = \"work@company.com\"\n"
42
+"#description = \"Work Account\"\n"
43
+"#preferred_scope = \"global\"\n"
44
+"#ssh_key = \"~/.ssh/id_rsa_work\"\n"
45
+"#gpg_key = \"ABCDEF1234567890\"\n"
46
+"#gpg_signing_enabled = true\n"
47
+"#ssh_host = \"github.com-work\"\n"
48
+"\n"
49
+"# Security Notes:\n"
50
+"# - SSH keys should have 600 permissions\n"
51
+"# - GPG keys should exist in your keyring\n"
52
+"# - This config file should have 600 permissions\n"
53
+"# - Use absolute paths or ~ expansion for key files\n";
54
+
55
+/* Internal helper functions */
56
+static int validate_config_file_security(const char *config_path);
57
+static int create_config_directory_secure(const char *config_dir);
58
+static int load_accounts_from_toml(gitswitch_ctx_t *ctx, const toml_document_t *doc);
59
+static int save_accounts_to_toml(const gitswitch_ctx_t *ctx, toml_document_t *doc);
60
+static int parse_account_id_from_section(const char *section_name, uint32_t *account_id);
61
+static int validate_account_security(const account_t *account);
62
+
63
+/* Initialize configuration system */
64
+int config_init(gitswitch_ctx_t *ctx) {
65
+    char config_path[MAX_PATH_LEN];
66
+    char config_dir[MAX_PATH_LEN];
67
+    
68
+    if (!ctx) {
69
+        set_error(ERR_INVALID_ARGS, "NULL context to config_init");
70
+        return -1;
71
+    }
72
+    
73
+    /* Initialize context */
74
+    memset(ctx, 0, sizeof(gitswitch_ctx_t));
75
+    ctx->config.default_scope = GIT_SCOPE_LOCAL;
76
+    ctx->config.verbose = false;
77
+    ctx->config.dry_run = false;
78
+    ctx->config.color_output = true;
79
+    
80
+    /* Get configuration directory path */
81
+    if (get_config_directory(config_dir, sizeof(config_dir)) != 0) {
82
+        return -1;
83
+    }
84
+    
85
+    /* Ensure config directory exists with secure permissions */
86
+    if (create_config_directory_secure(config_dir) != 0) {
87
+        return -1;
88
+    }
89
+    
90
+    /* Build config file path */
91
+    if (join_path(config_path, sizeof(config_path), config_dir, DEFAULT_CONFIG_FILE) != 0) {
92
+        return -1;
93
+    }
94
+    
95
+    /* Store config path in context */
96
+    safe_strncpy(ctx->config.config_path, config_path, sizeof(ctx->config.config_path));
97
+    
98
+    /* Load configuration if it exists */
99
+    if (path_exists(config_path)) {
100
+        log_info("Loading configuration from: %s", config_path);
101
+        return config_load(ctx, config_path);
102
+    } else {
103
+        log_info("Configuration file not found, will create default");
104
+        /* Don't automatically create - let user create when needed */
105
+        return 0;
106
+    }
107
+}
108
+
109
+/* Load configuration from TOML file */
110
+int config_load(gitswitch_ctx_t *ctx, const char *config_path) {
111
+    toml_document_t toml_doc;
112
+    char scope_str[32];
113
+    
114
+    if (!ctx || !config_path) {
115
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to config_load");
116
+        return -1;
117
+    }
118
+    
119
+    /* Validate file security before loading */
120
+    if (validate_config_file_security(config_path) != 0) {
121
+        return -1;
122
+    }
123
+    
124
+    /* Parse TOML configuration */
125
+    toml_init_document(&toml_doc);
126
+    if (toml_parse_file(config_path, &toml_doc) != 0) {
127
+        toml_cleanup_document(&toml_doc);
128
+        return -1;
129
+    }
130
+    
131
+    /* Load settings section */
132
+    if (toml_get_string(&toml_doc, "settings", "default_scope", 
133
+                        scope_str, sizeof(scope_str)) == 0) {
134
+        ctx->config.default_scope = config_parse_scope(scope_str);
135
+    } else {
136
+        log_warning("No default_scope found in settings, using local");
137
+        ctx->config.default_scope = GIT_SCOPE_LOCAL;
138
+    }
139
+    
140
+    /* Load accounts */
141
+    if (load_accounts_from_toml(ctx, &toml_doc) != 0) {
142
+        toml_cleanup_document(&toml_doc);
143
+        return -1;
144
+    }
145
+    
146
+    /* Store config path */
147
+    safe_strncpy(ctx->config.config_path, config_path, sizeof(ctx->config.config_path));
148
+    
149
+    toml_cleanup_document(&toml_doc);
150
+    
151
+    log_info("Configuration loaded successfully: %zu accounts", ctx->account_count);
152
+    return 0;
153
+}
154
+
155
+/* Save configuration to TOML file */
156
+int config_save(const gitswitch_ctx_t *ctx, const char *config_path) {
157
+    toml_document_t toml_doc;
158
+    char temp_path[MAX_PATH_LEN];
159
+    int result = -1;
160
+    
161
+    if (!ctx || !config_path) {
162
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to config_save");
163
+        return -1;
164
+    }
165
+    
166
+    /* Create backup if file exists */
167
+    if (path_exists(config_path)) {
168
+        if (config_backup(config_path) != 0) {
169
+            log_warning("Failed to create backup before saving config");
170
+        }
171
+    }
172
+    
173
+    /* Create temporary file path for atomic write */
174
+    if (snprintf(temp_path, sizeof(temp_path), "%s.tmp", config_path) >= sizeof(temp_path)) {
175
+        set_error(ERR_INVALID_ARGS, "Temporary file path too long");
176
+        return -1;
177
+    }
178
+    
179
+    /* Initialize TOML document */
180
+    toml_init_document(&toml_doc);
181
+    
182
+    /* Add settings section */
183
+    if (toml_set_string(&toml_doc, "settings", "default_scope", 
184
+                        config_scope_to_string(ctx->config.default_scope)) != 0) {
185
+        goto cleanup;
186
+    }
187
+    
188
+    /* Add accounts */
189
+    if (save_accounts_to_toml(ctx, &toml_doc) != 0) {
190
+        goto cleanup;
191
+    }
192
+    
193
+    /* Write to temporary file first */
194
+    if (toml_write_file(&toml_doc, temp_path) != 0) {
195
+        goto cleanup;
196
+    }
197
+    
198
+    /* Set secure permissions on temp file */
199
+    if (set_file_permissions(temp_path, PERM_USER_RW) != 0) {
200
+        unlink(temp_path);
201
+        goto cleanup;
202
+    }
203
+    
204
+    /* Atomic move from temp to final location */
205
+    if (rename(temp_path, config_path) != 0) {
206
+        set_system_error(ERR_CONFIG_WRITE_FAILED, 
207
+                        "Failed to move temporary config file to final location");
208
+        unlink(temp_path);
209
+        goto cleanup;
210
+    }
211
+    
212
+    log_info("Configuration saved successfully to: %s", config_path);
213
+    result = 0;
214
+    
215
+cleanup:
216
+    toml_cleanup_document(&toml_doc);
217
+    return result;
218
+}
219
+
220
+/* Create default configuration file */
221
+int config_create_default(const char *config_path) {
222
+    FILE *file;
223
+    char config_dir[MAX_PATH_LEN];
224
+    char *last_slash;
225
+    
226
+    if (!config_path) {
227
+        set_error(ERR_INVALID_ARGS, "NULL config path to config_create_default");
228
+        return -1;
229
+    }
230
+    
231
+    /* Extract directory from config path */
232
+    safe_strncpy(config_dir, config_path, sizeof(config_dir));
233
+    last_slash = strrchr(config_dir, '/');
234
+    if (last_slash) {
235
+        *last_slash = '\0';
236
+    }
237
+    
238
+    /* Ensure directory exists */
239
+    if (create_config_directory_secure(config_dir) != 0) {
240
+        return -1;
241
+    }
242
+    
243
+    /* Create file with secure permissions */
244
+    file = fopen(config_path, "w");
245
+    if (!file) {
246
+        set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to create config file: %s", config_path);
247
+        return -1;
248
+    }
249
+    
250
+    /* Write default template */
251
+    if (fwrite(default_config_template, 1, strlen(default_config_template), file) != 
252
+        strlen(default_config_template)) {
253
+        set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to write default config content");
254
+        fclose(file);
255
+        return -1;
256
+    }
257
+    
258
+    fclose(file);
259
+    
260
+    /* Set secure permissions */
261
+    if (set_file_permissions(config_path, PERM_USER_RW) != 0) {
262
+        return -1;
263
+    }
264
+    
265
+    log_info("Created default configuration file: %s", config_path);
266
+    return 0;
267
+}
268
+
269
+/* Validate configuration structure */
270
+int config_validate(const gitswitch_ctx_t *ctx) {
271
+    if (!ctx) {
272
+        set_error(ERR_INVALID_ARGS, "NULL context to config_validate");
273
+        return -1;
274
+    }
275
+    
276
+    /* Validate configuration file security */
277
+    if (path_exists(ctx->config.config_path)) {
278
+        if (validate_config_file_security(ctx->config.config_path) != 0) {
279
+            return -1;
280
+        }
281
+    }
282
+    
283
+    /* Validate each account */
284
+    for (size_t i = 0; i < ctx->account_count; i++) {
285
+        if (validate_account_security(&ctx->accounts[i]) != 0) {
286
+            set_error(ERR_ACCOUNT_INVALID, "Account %u failed security validation", 
287
+                      ctx->accounts[i].id);
288
+            return -1;
289
+        }
290
+    }
291
+    
292
+    log_debug("Configuration validation passed for %zu accounts", ctx->account_count);
293
+    return 0;
294
+}
295
+
296
+/* Get configuration file path */
297
+int config_get_path(char *path_buffer, size_t buffer_size) {
298
+    char config_dir[MAX_PATH_LEN];
299
+    
300
+    if (!path_buffer || buffer_size == 0) {
301
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to config_get_path");
302
+        return -1;
303
+    }
304
+    
305
+    /* Get config directory */
306
+    if (get_config_directory(config_dir, sizeof(config_dir)) != 0) {
307
+        return -1;
308
+    }
309
+    
310
+    /* Build full path */
311
+    return join_path(path_buffer, buffer_size, config_dir, DEFAULT_CONFIG_FILE);
312
+}
313
+
314
+/* Add new account to configuration */
315
+int config_add_account(gitswitch_ctx_t *ctx, const account_t *account) {
316
+    if (!ctx || !account) {
317
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to config_add_account");
318
+        return -1;
319
+    }
320
+    
321
+    if (ctx->account_count >= MAX_ACCOUNTS) {
322
+        set_error(ERR_ACCOUNT_EXISTS, "Maximum number of accounts reached: %d", MAX_ACCOUNTS);
323
+        return -1;
324
+    }
325
+    
326
+    /* Validate account security */
327
+    if (validate_account_security(account) != 0) {
328
+        return -1;
329
+    }
330
+    
331
+    /* Check for duplicate IDs */
332
+    for (size_t i = 0; i < ctx->account_count; i++) {
333
+        if (ctx->accounts[i].id == account->id) {
334
+            set_error(ERR_ACCOUNT_EXISTS, "Account with ID %u already exists", account->id);
335
+            return -1;
336
+        }
337
+    }
338
+    
339
+    /* Add account */
340
+    ctx->accounts[ctx->account_count] = *account;
341
+    ctx->account_count++;
342
+    
343
+    log_info("Added account: %s (%s)", account->name, account->description);
344
+    return 0;
345
+}
346
+
347
+/* Remove account from configuration */
348
+int config_remove_account(gitswitch_ctx_t *ctx, uint32_t account_id) {
349
+    size_t found_index = SIZE_MAX;
350
+    
351
+    if (!ctx) {
352
+        set_error(ERR_INVALID_ARGS, "NULL context to config_remove_account");
353
+        return -1;
354
+    }
355
+    
356
+    /* Find account */
357
+    for (size_t i = 0; i < ctx->account_count; i++) {
358
+        if (ctx->accounts[i].id == account_id) {
359
+            found_index = i;
360
+            break;
361
+        }
362
+    }
363
+    
364
+    if (found_index == SIZE_MAX) {
365
+        set_error(ERR_ACCOUNT_NOT_FOUND, "Account with ID %u not found", account_id);
366
+        return -1;
367
+    }
368
+    
369
+    /* Clear sensitive data before removing */
370
+    secure_zero_memory(&ctx->accounts[found_index], sizeof(account_t));
371
+    
372
+    /* Shift remaining accounts */
373
+    for (size_t i = found_index; i < ctx->account_count - 1; i++) {
374
+        ctx->accounts[i] = ctx->accounts[i + 1];
375
+    }
376
+    
377
+    ctx->account_count--;
378
+    
379
+    /* Clear the last slot */
380
+    memset(&ctx->accounts[ctx->account_count], 0, sizeof(account_t));
381
+    
382
+    log_info("Removed account with ID: %u", account_id);
383
+    return 0;
384
+}
385
+
386
+/* Update existing account */
387
+int config_update_account(gitswitch_ctx_t *ctx, const account_t *account) {
388
+    account_t *existing_account = NULL;
389
+    
390
+    if (!ctx || !account) {
391
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to config_update_account");
392
+        return -1;
393
+    }
394
+    
395
+    /* Find existing account */
396
+    for (size_t i = 0; i < ctx->account_count; i++) {
397
+        if (ctx->accounts[i].id == account->id) {
398
+            existing_account = &ctx->accounts[i];
399
+            break;
400
+        }
401
+    }
402
+    
403
+    if (!existing_account) {
404
+        set_error(ERR_ACCOUNT_NOT_FOUND, "Account with ID %u not found", account->id);
405
+        return -1;
406
+    }
407
+    
408
+    /* Validate new account data */
409
+    if (validate_account_security(account) != 0) {
410
+        return -1;
411
+    }
412
+    
413
+    /* Clear old sensitive data */
414
+    secure_zero_memory(existing_account, sizeof(account_t));
415
+    
416
+    /* Update with new data */
417
+    *existing_account = *account;
418
+    
419
+    log_info("Updated account: %s (%s)", account->name, account->description);
420
+    return 0;
421
+}
422
+
423
+/* Find account by ID or name/description */
424
+account_t *config_find_account(gitswitch_ctx_t *ctx, const char *identifier) {
425
+    char *endptr;
426
+    unsigned long account_id;
427
+    
428
+    if (!ctx || !identifier) {
429
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to config_find_account");
430
+        return NULL;
431
+    }
432
+    
433
+    /* Try to parse as numeric ID */
434
+    account_id = strtoul(identifier, &endptr, 10);
435
+    if (*endptr == '\0') {
436
+        /* It's a number - search by ID */
437
+        for (size_t i = 0; i < ctx->account_count; i++) {
438
+            if (ctx->accounts[i].id == (uint32_t)account_id) {
439
+                return &ctx->accounts[i];
440
+            }
441
+        }
442
+    } else {
443
+        /* Search by name, email, or description */
444
+        for (size_t i = 0; i < ctx->account_count; i++) {
445
+            if (strstr(ctx->accounts[i].name, identifier) ||
446
+                strstr(ctx->accounts[i].description, identifier) ||
447
+                strcmp(ctx->accounts[i].email, identifier) == 0) {
448
+                return &ctx->accounts[i];
449
+            }
450
+        }
451
+    }
452
+    
453
+    return NULL;
454
+}
455
+
456
+/* Parse git scope from string */
457
+git_scope_t config_parse_scope(const char *scope_str) {
458
+    if (!scope_str) return GIT_SCOPE_LOCAL;
459
+    
460
+    if (strcmp(scope_str, "global") == 0) {
461
+        return GIT_SCOPE_GLOBAL;
462
+    } else if (strcmp(scope_str, "system") == 0) {
463
+        return GIT_SCOPE_SYSTEM;
464
+    } else {
465
+        return GIT_SCOPE_LOCAL;
466
+    }
467
+}
468
+
469
+/* Convert git scope to string */
470
+const char *config_scope_to_string(git_scope_t scope) {
471
+    switch (scope) {
472
+        case GIT_SCOPE_GLOBAL: return "global";
473
+        case GIT_SCOPE_SYSTEM: return "system";
474
+        case GIT_SCOPE_LOCAL:
475
+        default:
476
+            return "local";
477
+    }
478
+}
479
+
480
+/* Backup configuration file with timestamp */
481
+int config_backup(const char *config_path) {
482
+    char backup_path[MAX_PATH_LEN];
483
+    char timestamp[32];
484
+    time_t now;
485
+    struct tm *tm_info;
486
+    
487
+    if (!config_path) {
488
+        set_error(ERR_INVALID_ARGS, "NULL config path to config_backup");
489
+        return -1;
490
+    }
491
+    
492
+    if (!path_exists(config_path)) {
493
+        log_debug("Config file does not exist, no backup needed");
494
+        return 0;
495
+    }
496
+    
497
+    /* Generate timestamp */
498
+    time(&now);
499
+    tm_info = localtime(&now);
500
+    if (tm_info) {
501
+        strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm_info);
502
+    } else {
503
+        snprintf(timestamp, sizeof(timestamp), "%ld", (long)now);
504
+    }
505
+    
506
+    /* Create backup path */
507
+    if (snprintf(backup_path, sizeof(backup_path), "%s.backup.%s", 
508
+                config_path, timestamp) >= sizeof(backup_path)) {
509
+        set_error(ERR_INVALID_ARGS, "Backup path too long");
510
+        return -1;
511
+    }
512
+    
513
+    /* Copy file */
514
+    if (copy_file(config_path, backup_path) != 0) {
515
+        return -1;
516
+    }
517
+    
518
+    /* Set secure permissions on backup */
519
+    if (set_file_permissions(backup_path, PERM_USER_RW) != 0) {
520
+        return -1;
521
+    }
522
+    
523
+    log_info("Created configuration backup: %s", backup_path);
524
+    return 0;
525
+}
526
+
527
+/* Internal helper functions implementation */
528
+
529
+/* Validate configuration file security */
530
+static int validate_config_file_security(const char *config_path) {
531
+    struct stat file_stat;
532
+    
533
+    if (stat(config_path, &file_stat) != 0) {
534
+        set_system_error(ERR_CONFIG_NOT_FOUND, "Cannot access config file: %s", config_path);
535
+        return -1;
536
+    }
537
+    
538
+    /* Check file permissions - must not be readable by group/others */
539
+    if (file_stat.st_mode & (S_IRGRP | S_IROTH | S_IWGRP | S_IWOTH)) {
540
+        set_error(ERR_PERMISSION_DENIED, 
541
+                  "Configuration file has unsafe permissions: %o (should be 600)", 
542
+                  file_stat.st_mode & 0777);
543
+        return -1;
544
+    }
545
+    
546
+    /* Check ownership - must be owned by current user */
547
+    if (file_stat.st_uid != getuid()) {
548
+        set_error(ERR_PERMISSION_DENIED, "Configuration file not owned by current user");
549
+        return -1;
550
+    }
551
+    
552
+    /* Check file size is reasonable */
553
+    if (file_stat.st_size > TOML_MAX_FILE_SIZE) {
554
+        set_error(ERR_CONFIG_INVALID, "Configuration file too large: %ld bytes", file_stat.st_size);
555
+        return -1;
556
+    }
557
+    
558
+    return 0;
559
+}
560
+
561
+/* Create config directory with secure permissions */
562
+static int create_config_directory_secure(const char *config_dir) {
563
+    if (!path_exists(config_dir)) {
564
+        if (create_directory_recursive(config_dir, PERM_USER_RWX) != 0) {
565
+            return -1;
566
+        }
567
+        log_info("Created configuration directory: %s", config_dir);
568
+    }
569
+    
570
+    /* Verify directory permissions */
571
+    mode_t dir_mode;
572
+    if (get_file_permissions(config_dir, &dir_mode) == 0) {
573
+        if ((dir_mode & 077) != 0) {
574
+            /* Directory has group/other permissions - fix it */
575
+            if (set_file_permissions(config_dir, PERM_USER_RWX) != 0) {
576
+                return -1;
577
+            }
578
+            log_warning("Fixed configuration directory permissions");
579
+        }
580
+    }
581
+    
582
+    return 0;
583
+}
584
+
585
+/* Load accounts from TOML document */
586
+static int load_accounts_from_toml(gitswitch_ctx_t *ctx, const toml_document_t *doc) {
587
+    char sections[TOML_MAX_SECTIONS][TOML_MAX_SECTION_LEN];
588
+    size_t section_count;
589
+    
590
+    if (toml_get_sections(doc, sections, TOML_MAX_SECTIONS, &section_count) != 0) {
591
+        set_error(ERR_CONFIG_INVALID, "Failed to get sections from TOML document");
592
+        return -1;
593
+    }
594
+    
595
+    ctx->account_count = 0;
596
+    
597
+    for (size_t i = 0; i < section_count; i++) {
598
+        if (string_starts_with(sections[i], "accounts.")) {
599
+            account_t account;
600
+            uint32_t account_id;
601
+            char temp_str[256];
602
+            bool temp_bool;
603
+            
604
+            /* Parse account ID from section name */
605
+            if (parse_account_id_from_section(sections[i], &account_id) != 0) {
606
+                log_warning("Invalid account section name: %s", sections[i]);
607
+                continue;
608
+            }
609
+            
610
+            /* Initialize account */
611
+            memset(&account, 0, sizeof(account));
612
+            account.id = account_id;
613
+            account.preferred_scope = GIT_SCOPE_LOCAL; /* Default */
614
+            
615
+            /* Load required fields */
616
+            if (toml_get_string(doc, sections[i], "name", account.name, sizeof(account.name)) != 0) {
617
+                log_error("Account %u missing required 'name' field", account_id);
618
+                continue;
619
+            }
620
+            
621
+            if (toml_get_string(doc, sections[i], "email", account.email, sizeof(account.email)) != 0) {
622
+                log_error("Account %u missing required 'email' field", account_id);
623
+                continue;
624
+            }
625
+            
626
+            /* Load optional fields */
627
+            if (toml_get_string(doc, sections[i], "description", 
628
+                               account.description, sizeof(account.description)) != 0) {
629
+                /* Use name as description if not provided */
630
+                safe_strncpy(account.description, account.name, sizeof(account.description));
631
+            }
632
+            
633
+            if (toml_get_string(doc, sections[i], "preferred_scope", temp_str, sizeof(temp_str)) == 0) {
634
+                account.preferred_scope = config_parse_scope(temp_str);
635
+            }
636
+            
637
+            /* SSH configuration */
638
+            if (toml_get_string(doc, sections[i], "ssh_key", 
639
+                               account.ssh_key_path, sizeof(account.ssh_key_path)) == 0 &&
640
+                strlen(account.ssh_key_path) > 0) {
641
+                account.ssh_enabled = true;
642
+                
643
+                /* Expand path if needed */
644
+                char expanded_path[MAX_PATH_LEN];
645
+                if (expand_path(account.ssh_key_path, expanded_path, sizeof(expanded_path)) == 0) {
646
+                    safe_strncpy(account.ssh_key_path, expanded_path, sizeof(account.ssh_key_path));
647
+                }
648
+                
649
+                /* Optional SSH host alias */
650
+                toml_get_string(doc, sections[i], "ssh_host", 
651
+                               account.ssh_host_alias, sizeof(account.ssh_host_alias));
652
+            }
653
+            
654
+            /* GPG configuration */
655
+            if (toml_get_string(doc, sections[i], "gpg_key", 
656
+                               account.gpg_key_id, sizeof(account.gpg_key_id)) == 0 &&
657
+                strlen(account.gpg_key_id) > 0) {
658
+                account.gpg_enabled = true;
659
+                
660
+                /* GPG signing preference */
661
+                if (toml_get_boolean(doc, sections[i], "gpg_signing_enabled", &temp_bool) == 0) {
662
+                    account.gpg_signing_enabled = temp_bool;
663
+                }
664
+            }
665
+            
666
+            /* Validate and add account */
667
+            if (validate_account_security(&account) == 0) {
668
+                if (ctx->account_count < MAX_ACCOUNTS) {
669
+                    ctx->accounts[ctx->account_count] = account;
670
+                    ctx->account_count++;
671
+                    log_debug("Loaded account: %s (%s)", account.name, account.description);
672
+                } else {
673
+                    log_error("Too many accounts, skipping account %u", account_id);
674
+                }
675
+            } else {
676
+                log_error("Account %u failed security validation", account_id);
677
+            }
678
+        }
679
+    }
680
+    
681
+    log_info("Loaded %zu accounts from configuration", ctx->account_count);
682
+    return 0;
683
+}
684
+
685
+/* Parse account ID from section name like "accounts.1" */
686
+static int parse_account_id_from_section(const char *section_name, uint32_t *account_id) {
687
+    const char *dot_pos;
688
+    char *endptr;
689
+    unsigned long parsed_id;
690
+    
691
+    if (!section_name || !account_id) return -1;
692
+    
693
+    dot_pos = strchr(section_name, '.');
694
+    if (!dot_pos || dot_pos == section_name + strlen(section_name) - 1) {
695
+        return -1;
696
+    }
697
+    
698
+    parsed_id = strtoul(dot_pos + 1, &endptr, 10);
699
+    if (*endptr != '\0' || parsed_id == 0 || parsed_id > UINT32_MAX) {
700
+        return -1;
701
+    }
702
+    
703
+    *account_id = (uint32_t)parsed_id;
704
+    return 0;
705
+}
706
+
707
+/* Validate account security */
708
+static int validate_account_security(const account_t *account) {
709
+    char expanded_path[MAX_PATH_LEN];
710
+    mode_t file_mode;
711
+    
712
+    if (!account) {
713
+        set_error(ERR_INVALID_ARGS, "NULL account to validate");
714
+        return -1;
715
+    }
716
+    
717
+    /* Validate required fields */
718
+    if (!validate_name(account->name)) {
719
+        set_error(ERR_ACCOUNT_INVALID, "Invalid account name: %s", account->name);
720
+        return -1;
721
+    }
722
+    
723
+    if (!validate_email(account->email)) {
724
+        set_error(ERR_ACCOUNT_INVALID, "Invalid email address: %s", account->email);
725
+        return -1;
726
+    }
727
+    
728
+    /* Validate SSH key if configured */
729
+    if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
730
+        if (expand_path(account->ssh_key_path, expanded_path, sizeof(expanded_path)) != 0) {
731
+            set_error(ERR_ACCOUNT_INVALID, "Invalid SSH key path: %s", account->ssh_key_path);
732
+            return -1;
733
+        }
734
+        
735
+        if (!path_exists(expanded_path)) {
736
+            set_error(ERR_ACCOUNT_INVALID, "SSH key file not found: %s", expanded_path);
737
+            return -1;
738
+        }
739
+        
740
+        /* Check SSH key file permissions - must be 600 */
741
+        if (get_file_permissions(expanded_path, &file_mode) == 0) {
742
+            if ((file_mode & 077) != 0) {
743
+                set_error(ERR_ACCOUNT_INVALID, 
744
+                          "SSH key file has unsafe permissions: %o (should be 600)", 
745
+                          file_mode & 0777);
746
+                return -1;
747
+            }
748
+        }
749
+    }
750
+    
751
+    /* Validate GPG key if configured */
752
+    if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
753
+        if (!validate_key_id(account->gpg_key_id)) {
754
+            set_error(ERR_ACCOUNT_INVALID, "Invalid GPG key ID: %s", account->gpg_key_id);
755
+            return -1;
756
+        }
757
+    }
758
+    
759
+    return 0;
760
+}
761
+
762
+/* Save accounts to TOML document */
763
+static int save_accounts_to_toml(const gitswitch_ctx_t *ctx, toml_document_t *doc) {
764
+    char section_name[64];
765
+    
766
+    if (!ctx || !doc) {
767
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to save_accounts_to_toml");
768
+        return -1;
769
+    }
770
+    
771
+    /* Save each account */
772
+    for (size_t i = 0; i < ctx->account_count; i++) {
773
+        const account_t *account = &ctx->accounts[i];
774
+        
775
+        /* Create section name */
776
+        if (snprintf(section_name, sizeof(section_name), "accounts.%u", account->id) >= sizeof(section_name)) {
777
+            set_error(ERR_ACCOUNT_INVALID, "Account ID too large: %u", account->id);
778
+            return -1;
779
+        }
780
+        
781
+        /* Save required fields */
782
+        if (toml_set_string(doc, section_name, "name", account->name) != 0) {
783
+            set_error(ERR_CONFIG_INVALID, "Failed to save account name");
784
+            return -1;
785
+        }
786
+        
787
+        if (toml_set_string(doc, section_name, "email", account->email) != 0) {
788
+            set_error(ERR_CONFIG_INVALID, "Failed to save account email");
789
+            return -1;
790
+        }
791
+        
792
+        /* Save optional fields */
793
+        if (strlen(account->description) > 0) {
794
+            toml_set_string(doc, section_name, "description", account->description);
795
+        }
796
+        
797
+        toml_set_string(doc, section_name, "preferred_scope", 
798
+                       config_scope_to_string(account->preferred_scope));
799
+        
800
+        /* Save SSH configuration */
801
+        if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
802
+            toml_set_string(doc, section_name, "ssh_key", account->ssh_key_path);
803
+            
804
+            if (strlen(account->ssh_host_alias) > 0) {
805
+                toml_set_string(doc, section_name, "ssh_host", account->ssh_host_alias);
806
+            }
807
+        }
808
+        
809
+        /* Save GPG configuration */
810
+        if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
811
+            toml_set_string(doc, section_name, "gpg_key", account->gpg_key_id);
812
+            toml_set_boolean(doc, section_name, "gpg_signing_enabled", account->gpg_signing_enabled);
813
+        }
814
+    }
815
+    
816
+    return 0;
817
+}
src/display.cadded
@@ -0,0 +1,765 @@
1
+/* Display and user interface functions
2
+ * Provides safe, accessible terminal output for gitswitch-c
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <stdlib.h>
7
+#include <string.h>
8
+#include <stdarg.h>
9
+#include <unistd.h>
10
+#include <ctype.h>
11
+#include <termios.h>
12
+
13
+#include "display.h"
14
+#include "utils.h"
15
+#include "error.h"
16
+#include "git_ops.h"
17
+
18
+/* Global display state */
19
+static bool g_color_enabled = false;
20
+static bool g_color_forced = false;
21
+static int g_terminal_width = 80;
22
+static int g_terminal_height = 24;
23
+
24
+/* Color support detection */
25
+static bool detect_color_support(void) {
26
+    const char *term = getenv("TERM");
27
+    const char *colorterm = getenv("COLORTERM");
28
+    
29
+    /* Force color if COLORTERM is set */
30
+    if (colorterm && *colorterm) {
31
+        return true;
32
+    }
33
+    
34
+    /* Check for common color-capable terminals */
35
+    if (term) {
36
+        if (strstr(term, "color") || 
37
+            strstr(term, "xterm") ||
38
+            strstr(term, "screen") ||
39
+            strstr(term, "tmux") ||
40
+            strcmp(term, "linux") == 0) {
41
+            return true;
42
+        }
43
+    }
44
+    
45
+    return false;
46
+}
47
+
48
+/* Initialize display system */
49
+int display_init(bool force_color, bool no_color) {
50
+    if (no_color) {
51
+        g_color_enabled = false;
52
+        g_color_forced = true;
53
+    } else if (force_color) {
54
+        g_color_enabled = true;
55
+        g_color_forced = true;
56
+    } else {
57
+        /* Auto-detect color support */
58
+        g_color_enabled = is_terminal(STDOUT_FILENO) && detect_color_support();
59
+        g_color_forced = false;
60
+    }
61
+    
62
+    /* Get terminal size */
63
+    if (get_terminal_size(&g_terminal_width, &g_terminal_height) != 0) {
64
+        /* Use defaults if we can't get size */
65
+        g_terminal_width = 80;
66
+        g_terminal_height = 24;
67
+    }
68
+    
69
+    log_debug("Display initialized: color=%s, size=%dx%d", 
70
+              g_color_enabled ? "enabled" : "disabled",
71
+              g_terminal_width, g_terminal_height);
72
+    
73
+    return 0;
74
+}
75
+
76
+/* Check if terminal supports color output */
77
+bool display_supports_color(void) {
78
+    return g_color_enabled;
79
+}
80
+
81
+/* Format and colorize text based on content type */
82
+const char *display_colorize(const char *text, const char *type) {
83
+    static char colored_buffer[512];
84
+    const char *color_code = "";
85
+    
86
+    if (!g_color_enabled || !text || !type) {
87
+        return text;
88
+    }
89
+    
90
+    /* Select color based on type */
91
+    if (strcmp(type, "success") == 0) {
92
+        color_code = COLOR_GREEN;
93
+    } else if (strcmp(type, "error") == 0) {
94
+        color_code = COLOR_RED;
95
+    } else if (strcmp(type, "warning") == 0) {
96
+        color_code = COLOR_YELLOW;
97
+    } else if (strcmp(type, "info") == 0) {
98
+        color_code = COLOR_BLUE;
99
+    } else if (strcmp(type, "header") == 0) {
100
+        color_code = COLOR_BOLD COLOR_CYAN;
101
+    } else if (strcmp(type, "current") == 0) {
102
+        color_code = COLOR_BOLD COLOR_GREEN;
103
+    } else if (strcmp(type, "inactive") == 0) {
104
+        color_code = COLOR_DIM;
105
+    } else {
106
+        return text; /* No coloring */
107
+    }
108
+    
109
+    snprintf(colored_buffer, sizeof(colored_buffer), 
110
+             "%s%s%s", color_code, text, COLOR_RESET);
111
+    
112
+    return colored_buffer;
113
+}
114
+
115
+/* Print formatted header with decorative border */
116
+void display_header(const char *title) {
117
+    int title_len, padding, total_width;
118
+    int i;
119
+    
120
+    if (!title) return;
121
+    
122
+    title_len = strlen(title);
123
+    total_width = (title_len + 4 > 40) ? title_len + 4 : 40;
124
+    if (total_width > g_terminal_width - 2) {
125
+        total_width = g_terminal_width - 2;
126
+    }
127
+    
128
+    padding = (total_width - title_len - 2) / 2;
129
+    
130
+    /* Top border */
131
+    printf("┌");
132
+    for (i = 0; i < total_width - 2; i++) {
133
+        printf("─");
134
+    }
135
+    printf("┐\n");
136
+    
137
+    /* Title line */
138
+    printf("│%s%*s%s%s%*s│\n", 
139
+           display_colorize("", "header"),
140
+           padding, "",
141
+           display_colorize(title, "header"),
142
+           COLOR_RESET,
143
+           total_width - title_len - padding - 2, "");
144
+    
145
+    /* Bottom border */
146
+    printf("└");
147
+    for (i = 0; i < total_width - 2; i++) {
148
+        printf("─");
149
+    }
150
+    printf("┘\n");
151
+}
152
+
153
+/* Print status message with appropriate color and icon */
154
+void display_status(const char *level, const char *message, ...) {
155
+    va_list args;
156
+    char formatted_message[1024];
157
+    const char *icon = "";
158
+    const char *color_type = "";
159
+    
160
+    if (!level || !message) return;
161
+    
162
+    /* Format the message */
163
+    va_start(args, message);
164
+    vsnprintf(formatted_message, sizeof(formatted_message), message, args);
165
+    va_end(args);
166
+    
167
+    /* Select icon and color based on level */
168
+    if (strcmp(level, "success") == 0) {
169
+        icon = STATUS_SUCCESS;
170
+        color_type = "success";
171
+    } else if (strcmp(level, "error") == 0) {
172
+        icon = STATUS_ERROR;
173
+        color_type = "error";
174
+    } else if (strcmp(level, "warning") == 0) {
175
+        icon = STATUS_WARNING;
176
+        color_type = "warning";
177
+    } else if (strcmp(level, "info") == 0) {
178
+        icon = STATUS_INFO;
179
+        color_type = "info";
180
+    } else {
181
+        icon = "-";
182
+        color_type = "info";
183
+    }
184
+    
185
+    printf("%s %s\n", 
186
+           display_colorize(icon, color_type),
187
+           display_colorize(formatted_message, color_type));
188
+}
189
+
190
+/* Print error message with context */
191
+void display_error(const char *context, const char *message, ...) {
192
+    va_list args;
193
+    char formatted_message[1024];
194
+    
195
+    if (!message) return;
196
+    
197
+    va_start(args, message);
198
+    vsnprintf(formatted_message, sizeof(formatted_message), message, args);
199
+    va_end(args);
200
+    
201
+    if (context) {
202
+        display_status("error", "%s: %s", context, formatted_message);
203
+    } else {
204
+        display_status("error", "%s", formatted_message);
205
+    }
206
+}
207
+
208
+/* Print warning message */
209
+void display_warning(const char *message, ...) {
210
+    va_list args;
211
+    char formatted_message[1024];
212
+    
213
+    if (!message) return;
214
+    
215
+    va_start(args, message);
216
+    vsnprintf(formatted_message, sizeof(formatted_message), message, args);
217
+    va_end(args);
218
+    
219
+    display_status("warning", "%s", formatted_message);
220
+}
221
+
222
+/* Print success message */
223
+void display_success(const char *message, ...) {
224
+    va_list args;
225
+    char formatted_message[1024];
226
+    
227
+    if (!message) return;
228
+    
229
+    va_start(args, message);
230
+    vsnprintf(formatted_message, sizeof(formatted_message), message, args);
231
+    va_end(args);
232
+    
233
+    display_status("success", "%s", formatted_message);
234
+}
235
+
236
+/* Print info message */
237
+void display_info(const char *message, ...) {
238
+    va_list args;
239
+    char formatted_message[1024];
240
+    
241
+    if (!message) return;
242
+    
243
+    va_start(args, message);
244
+    vsnprintf(formatted_message, sizeof(formatted_message), message, args);
245
+    va_end(args);
246
+    
247
+    display_status("info", "%s", formatted_message);
248
+}
249
+
250
+/* Format table with proper column alignment */
251
+void display_table_header(const char **headers, const int *widths, int columns) {
252
+    int i;
253
+    
254
+    if (!headers || !widths || columns <= 0) return;
255
+    
256
+    /* Print header row */
257
+    printf("│");
258
+    for (i = 0; i < columns; i++) {
259
+        printf(" %s%-*s%s │", 
260
+               display_colorize("", "header"),
261
+               widths[i] - 1, headers[i],
262
+               COLOR_RESET);
263
+    }
264
+    printf("\n");
265
+    
266
+    /* Print separator */
267
+    printf("├");
268
+    for (i = 0; i < columns; i++) {
269
+        int j;
270
+        for (j = 0; j < widths[i] + 1; j++) {
271
+            printf("─");
272
+        }
273
+        printf(i < columns - 1 ? "┼" : "┤");
274
+    }
275
+    printf("\n");
276
+}
277
+
278
+void display_table_row(const char **values, const int *widths, int columns) {
279
+    int i;
280
+    
281
+    if (!values || !widths || columns <= 0) return;
282
+    
283
+    printf("│");
284
+    for (i = 0; i < columns; i++) {
285
+        printf(" %-*s │", widths[i] - 1, values[i] ? values[i] : "");
286
+    }
287
+    printf("\n");
288
+}
289
+
290
+void display_table_separator(const int *widths, int columns) {
291
+    int i;
292
+    
293
+    if (!widths || columns <= 0) return;
294
+    
295
+    printf("└");
296
+    for (i = 0; i < columns; i++) {
297
+        int j;
298
+        for (j = 0; j < widths[i] + 1; j++) {
299
+            printf("─");
300
+        }
301
+        printf(i < columns - 1 ? "┴" : "┘");
302
+    }
303
+    printf("\n");
304
+}
305
+
306
+/* Print account information in formatted table */
307
+void display_account(const account_t *account, bool is_current) {
308
+    if (!account) return;
309
+    
310
+    const char *marker = is_current ? "→" : " ";
311
+    const char *color_type = is_current ? "current" : "inactive";
312
+    
313
+    printf("%s %s%3u%s │ %s%-20s%s │ %s%-30s%s │ %s%s%s\n",
314
+           display_colorize(marker, color_type),
315
+           display_colorize("", color_type), account->id, COLOR_RESET,
316
+           display_colorize("", color_type), account->name, COLOR_RESET,
317
+           display_colorize("", color_type), account->email, COLOR_RESET,
318
+           display_colorize("", color_type), account->description, COLOR_RESET);
319
+}
320
+
321
+/* Print accounts list in formatted table */
322
+void display_accounts_list(const gitswitch_ctx_t *ctx) {
323
+    const char *headers[] = {"", "ID", "Name", "Email", "Description"};
324
+    const int widths[] = {3, 5, 22, 32, 30};
325
+    size_t i;
326
+    
327
+    if (!ctx) return;
328
+    
329
+    if (ctx->account_count == 0) {
330
+        display_info("No accounts configured");
331
+        display_info("Run 'gitswitch add' to create your first account");
332
+        return;
333
+    }
334
+    
335
+    printf("\n");
336
+    display_header("Configured Accounts");
337
+    printf("\n");
338
+    
339
+    /* Print table */
340
+    display_table_header(headers, widths, 5);
341
+    
342
+    for (i = 0; i < ctx->account_count; i++) {
343
+        bool is_current = ctx->current_account && 
344
+                         ctx->current_account->id == ctx->accounts[i].id;
345
+        display_account(&ctx->accounts[i], is_current);
346
+    }
347
+    
348
+    display_table_separator(widths, 5);
349
+    printf("\n");
350
+}
351
+
352
+/* These functions will be implemented in later phases when we have git/ssh/gpg components
353
+
354
+void display_current_status(const git_current_config_t *config) { ... }
355
+void display_ssh_status(const ssh_config_t *ssh_config) { ... }
356
+void display_gpg_status(const gpg_config_t *gpg_config) { ... }
357
+void display_validation_results(const account_validation_t *validation) { ... }
358
+
359
+*/
360
+
361
+/* Print health check results */
362
+void display_health_check(const gitswitch_ctx_t *ctx) {
363
+    if (!ctx) return;
364
+    
365
+    printf("\n");
366
+    display_header("System Health Check");
367
+    printf("\n");
368
+    
369
+    /* Check git availability */
370
+    if (command_exists("git")) {
371
+        display_success("Git is available");
372
+    } else {
373
+        display_error("Git command not found", "Install git to use gitswitch");
374
+    }
375
+    
376
+    /* Check SSH agent */
377
+    if (command_exists("ssh-agent")) {
378
+        display_success("SSH agent is available");
379
+    } else {
380
+        display_warning("SSH agent not found - SSH key management may not work");
381
+    }
382
+    
383
+    /* Check GPG */
384
+    if (command_exists("gpg") || command_exists("gpg2")) {
385
+        display_success("GPG is available");
386
+    } else {
387
+        display_warning("GPG not found - GPG signing will not work");
388
+    }
389
+    
390
+    /* Check configuration */
391
+    char config_path[MAX_PATH_LEN];
392
+    if (get_config_directory(config_path, sizeof(config_path)) == 0) {
393
+        if (is_directory(config_path)) {
394
+            display_success("Configuration directory exists: %s", config_path);
395
+        } else {
396
+            display_warning("Configuration directory not found: %s", config_path);
397
+        }
398
+    }
399
+    
400
+    printf("\n");
401
+}
402
+
403
+/* Display interactive account selection menu */
404
+uint32_t display_account_menu(const gitswitch_ctx_t *ctx) {
405
+    char input[64];
406
+    char *endptr;
407
+    unsigned long selected_id;
408
+    
409
+    if (!ctx || ctx->account_count == 0) {
410
+        display_error("No accounts available", "");
411
+        return 0;
412
+    }
413
+    
414
+    display_accounts_list(ctx);
415
+    
416
+    printf("Select account (ID or name): ");
417
+    fflush(stdout);
418
+    
419
+    if (!fgets(input, sizeof(input), stdin)) {
420
+        display_error("Failed to read input", "");
421
+        return 0;
422
+    }
423
+    
424
+    /* Remove trailing newline */
425
+    input[strcspn(input, "\n")] = '\0';
426
+    trim_whitespace(input);
427
+    
428
+    if (string_empty(input)) {
429
+        return 0; /* User cancelled */
430
+    }
431
+    
432
+    /* Try to parse as number first */
433
+    selected_id = strtoul(input, &endptr, 10);
434
+    if (*endptr == '\0' && selected_id > 0) {
435
+        /* It's a valid number - verify it exists */
436
+        for (size_t i = 0; i < ctx->account_count; i++) {
437
+            if (ctx->accounts[i].id == (uint32_t)selected_id) {
438
+                return (uint32_t)selected_id;
439
+            }
440
+        }
441
+        display_error("Account ID not found", "%lu", selected_id);
442
+        return 0;
443
+    }
444
+    
445
+    /* Search by name/description */
446
+    account_t *found = find_account_in_array((account_t *)ctx->accounts, 
447
+                                            ctx->account_count, input);
448
+    if (found) {
449
+        return found->id;
450
+    }
451
+    
452
+    display_error("Account not found", "%s", input);
453
+    return 0;
454
+}
455
+
456
+/* Prompt user for account information during add/edit */
457
+int display_prompt_account_info(account_t *account, bool is_edit) {
458
+    char input[512];
459
+    char *trimmed;
460
+    
461
+    if (!account) {
462
+        set_error(ERR_INVALID_ARGS, "NULL account to display_prompt_account_info");
463
+        return -1;
464
+    }
465
+    
466
+    printf("\n");
467
+    display_header(is_edit ? "Edit Account" : "Add New Account");
468
+    printf("\n");
469
+    
470
+    /* Name */
471
+    printf("Name%s: ", is_edit ? " (current: " : "");
472
+    if (is_edit && account->name[0] != '\0') {
473
+        printf("%s): ", account->name);
474
+    }
475
+    fflush(stdout);
476
+    
477
+    if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
478
+        trimmed = trim_whitespace(input);
479
+        trimmed[strcspn(trimmed, "\n")] = '\0';
480
+        if (strlen(trimmed) > 0 && validate_name(trimmed)) {
481
+            safe_strncpy(account->name, trimmed, sizeof(account->name));
482
+        } else if (!is_edit) {
483
+            display_error("Invalid name", "Name cannot be empty");
484
+            return -1;
485
+        }
486
+    }
487
+    
488
+    /* Email */
489
+    printf("Email%s: ", is_edit ? " (current: " : "");
490
+    if (is_edit && account->email[0] != '\0') {
491
+        printf("%s): ", account->email);
492
+    }
493
+    fflush(stdout);
494
+    
495
+    if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
496
+        trimmed = trim_whitespace(input);
497
+        trimmed[strcspn(trimmed, "\n")] = '\0';
498
+        if (strlen(trimmed) > 0 && validate_email(trimmed)) {
499
+            safe_strncpy(account->email, trimmed, sizeof(account->email));
500
+        } else if (!is_edit) {
501
+            display_error("Invalid email", "Please enter a valid email address");
502
+            return -1;
503
+        }
504
+    }
505
+    
506
+    /* Description */
507
+    printf("Description%s: ", is_edit ? " (current: " : "");
508
+    if (is_edit && account->description[0] != '\0') {
509
+        printf("%s): ", account->description);
510
+    }
511
+    fflush(stdout);
512
+    
513
+    if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
514
+        trimmed = trim_whitespace(input);
515
+        trimmed[strcspn(trimmed, "\n")] = '\0';
516
+        if (strlen(trimmed) > 0) {
517
+            safe_strncpy(account->description, trimmed, sizeof(account->description));
518
+        }
519
+    }
520
+    
521
+    /* SSH Key Path */
522
+    printf("SSH Key Path (optional)%s: ", is_edit ? " (current: " : "");
523
+    if (is_edit && account->ssh_key_path[0] != '\0') {
524
+        printf("%s): ", account->ssh_key_path);
525
+    }
526
+    fflush(stdout);
527
+    
528
+    if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
529
+        trimmed = trim_whitespace(input);
530
+        trimmed[strcspn(trimmed, "\n")] = '\0';
531
+        if (strlen(trimmed) > 0) {
532
+            char expanded[MAX_PATH_LEN];
533
+            if (expand_path(trimmed, expanded, sizeof(expanded)) == 0 && 
534
+                path_exists(expanded)) {
535
+                safe_strncpy(account->ssh_key_path, expanded, sizeof(account->ssh_key_path));
536
+                account->ssh_enabled = true;
537
+            } else {
538
+                display_warning("SSH key file not found: %s", trimmed);
539
+                account->ssh_enabled = false;
540
+            }
541
+        }
542
+    }
543
+    
544
+    /* GPG Key ID */
545
+    printf("GPG Key ID (optional)%s: ", is_edit ? " (current: " : "");
546
+    if (is_edit && account->gpg_key_id[0] != '\0') {
547
+        printf("%s): ", account->gpg_key_id);
548
+    }
549
+    fflush(stdout);
550
+    
551
+    if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
552
+        trimmed = trim_whitespace(input);
553
+        trimmed[strcspn(trimmed, "\n")] = '\0';
554
+        if (strlen(trimmed) > 0 && validate_key_id(trimmed)) {
555
+            safe_strncpy(account->gpg_key_id, trimmed, sizeof(account->gpg_key_id));
556
+            account->gpg_enabled = true;
557
+            
558
+            /* Ask about GPG signing */
559
+            printf("Enable GPG signing? (y/N): ");
560
+            fflush(stdout);
561
+            if (fgets(input, sizeof(input), stdin)) {
562
+                trimmed = trim_whitespace(input);
563
+                account->gpg_signing_enabled = (tolower(trimmed[0]) == 'y');
564
+            }
565
+        }
566
+    }
567
+    
568
+    return 0;
569
+}
570
+
571
+/* Confirm dangerous operations */
572
+bool display_confirm(const char *message, ...) {
573
+    va_list args;
574
+    char formatted_message[1024];
575
+    char input[64];
576
+    char *trimmed;
577
+    
578
+    if (!message) return false;
579
+    
580
+    va_start(args, message);
581
+    vsnprintf(formatted_message, sizeof(formatted_message), message, args);
582
+    va_end(args);
583
+    
584
+    printf("%s %s (y/N): ", 
585
+           display_colorize(STATUS_WARNING, "warning"),
586
+           formatted_message);
587
+    fflush(stdout);
588
+    
589
+    if (!fgets(input, sizeof(input), stdin)) {
590
+        return false;
591
+    }
592
+    
593
+    trimmed = trim_whitespace(input);
594
+    return tolower(trimmed[0]) == 'y';
595
+}
596
+
597
+/* Display progress indicator for long operations */
598
+void display_progress(const char *operation, int percent) {
599
+    const int bar_width = 40;
600
+    int filled = (percent * bar_width) / 100;
601
+    int i;
602
+    
603
+    if (!operation) return;
604
+    
605
+    printf("\r%s [", operation);
606
+    
607
+    for (i = 0; i < bar_width; i++) {
608
+        if (i < filled) {
609
+            printf("█");
610
+        } else {
611
+            printf("░");
612
+        }
613
+    }
614
+    
615
+    printf("] %3d%%", percent);
616
+    fflush(stdout);
617
+    
618
+    if (percent >= 100) {
619
+        printf("\n");
620
+    }
621
+}
622
+
623
+/* Clear current line */
624
+void display_clear_line(void) {
625
+    printf("\r\033[K");
626
+    fflush(stdout);
627
+}
628
+
629
+/* Get user input with prompt and validation */
630
+int display_get_input(const char *prompt, char *buffer, size_t buffer_size,
631
+                      bool (*validator)(const char *)) {
632
+    char *trimmed;
633
+    
634
+    if (!prompt || !buffer || buffer_size == 0) {
635
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to display_get_input");
636
+        return -1;
637
+    }
638
+    
639
+    printf("%s: ", prompt);
640
+    fflush(stdout);
641
+    
642
+    if (!fgets(buffer, buffer_size, stdin)) {
643
+        set_error(ERR_FILE_IO, "Failed to read user input");
644
+        return -1;
645
+    }
646
+    
647
+    trimmed = trim_whitespace(buffer);
648
+    trimmed[strcspn(trimmed, "\n")] = '\0';
649
+    
650
+    /* Move trimmed string to start of buffer */
651
+    if (trimmed != buffer) {
652
+        memmove(buffer, trimmed, strlen(trimmed) + 1);
653
+    }
654
+    
655
+    /* Validate if validator provided */
656
+    if (validator && !validator(buffer)) {
657
+        set_error(ERR_INVALID_ARGS, "Input validation failed");
658
+        return -1;
659
+    }
660
+    
661
+    return 0;
662
+}
663
+
664
+/* Get password/sensitive input (hidden) */
665
+int display_get_password(const char *prompt, char *buffer, size_t buffer_size) {
666
+    char *trimmed;
667
+    
668
+    if (!prompt || !buffer || buffer_size == 0) {
669
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to display_get_password");
670
+        return -1;
671
+    }
672
+    
673
+    printf("%s: ", prompt);
674
+    fflush(stdout);
675
+    
676
+    /* Disable echo */
677
+    disable_echo();
678
+    
679
+    if (!fgets(buffer, buffer_size, stdin)) {
680
+        enable_echo();
681
+        printf("\n");
682
+        set_error(ERR_FILE_IO, "Failed to read password input");
683
+        return -1;
684
+    }
685
+    
686
+    /* Re-enable echo */
687
+    enable_echo();
688
+    printf("\n");
689
+    
690
+    trimmed = trim_whitespace(buffer);
691
+    trimmed[strcspn(trimmed, "\n")] = '\0';
692
+    
693
+    /* Move trimmed string to start of buffer */
694
+    if (trimmed != buffer) {
695
+        memmove(buffer, trimmed, strlen(trimmed) + 1);
696
+    }
697
+    
698
+    return 0;
699
+}
700
+
701
+/* Show help text */
702
+void display_help(const char *command) {
703
+    printf("\n");
704
+    display_header("gitswitch-c Help");
705
+    printf("\n");
706
+    
707
+    if (!command) {
708
+        /* General help */
709
+        printf("Usage: gitswitch [OPTIONS] [COMMAND] [ARGS]\n\n");
710
+        printf("Commands:\n");
711
+        printf("  list                 List all configured accounts\n");
712
+        printf("  switch <account>     Switch to specified account\n");
713
+        printf("  add                  Add new account interactively\n");
714
+        printf("  remove <account>     Remove specified account\n");
715
+        printf("  status               Show current git configuration\n");
716
+        printf("  doctor               Run system health checks\n\n");
717
+        printf("Options:\n");
718
+        printf("  --global             Set git config globally\n");
719
+        printf("  --local              Set git config locally (default)\n");
720
+        printf("  --no-ssh             Skip SSH key management\n");
721
+        printf("  --no-gpg             Skip GPG key management\n");
722
+        printf("  --dry-run            Show what would be done without executing\n");
723
+        printf("  --verbose            Enable verbose output\n");
724
+        printf("  --color              Force color output\n");
725
+        printf("  --no-color           Disable color output\n");
726
+        printf("  --help               Show this help message\n");
727
+        printf("  --version            Show version information\n\n");
728
+        printf("Examples:\n");
729
+        printf("  gitswitch                    # Interactive account selection\n");
730
+        printf("  gitswitch list               # List all accounts\n");
731
+        printf("  gitswitch switch 1           # Switch to account ID 1\n");
732
+        printf("  gitswitch switch work        # Switch to account matching 'work'\n");
733
+        printf("  gitswitch add                # Add new account\n");
734
+        printf("  gitswitch doctor             # Check system health\n");
735
+    }
736
+    
737
+    printf("\n");
738
+}
739
+
740
+/* Display version and build information */
741
+void display_version(void) {
742
+    printf("%s version %s\n", GITSWITCH_NAME, GITSWITCH_VERSION);
743
+    printf("Safe git identity switching with SSH/GPG isolation\n");
744
+    printf("Built with security and reliability in mind\n\n");
745
+    printf("Features:\n");
746
+    printf("- Isolated SSH agents per account\n");
747
+    printf("- Separate GPG environments\n");
748
+    printf("- Comprehensive validation\n");
749
+    printf("- Secure memory handling\n");
750
+}
751
+
752
+/* Print configuration file location and status */
753
+void display_config_info(const gitswitch_ctx_t *ctx) {
754
+    if (!ctx) return;
755
+    
756
+    printf("\n");
757
+    printf("Configuration: %s\n", ctx->config.config_path);
758
+    printf("Accounts:      %zu configured\n", ctx->account_count);
759
+    
760
+    if (path_exists(ctx->config.config_path)) {
761
+        printf("Status:        %s\n", display_colorize("exists", "success"));
762
+    } else {
763
+        printf("Status:        %s\n", display_colorize("not found", "warning"));
764
+    }
765
+}
src/error.cadded
@@ -0,0 +1,468 @@
1
+/* Error handling and logging utilities
2
+ * Provides comprehensive error tracking and safe logging for gitswitch-c
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <stdlib.h>
7
+#include <string.h>
8
+#include <stdarg.h>
9
+#include <time.h>
10
+#include <unistd.h>
11
+#include <errno.h>
12
+#include <sys/stat.h>
13
+#include <sys/mman.h>
14
+
15
+#include "error.h"
16
+#include "gitswitch.h"
17
+
18
+/* Global error context */
19
+error_context_t g_last_error = {0};
20
+
21
+/* Logging configuration */
22
+log_level_t g_log_level = LOG_LEVEL_INFO;
23
+FILE *g_log_file = NULL;
24
+bool g_log_to_stderr = true;
25
+
26
+/* Error code to string mapping */
27
+static const struct {
28
+    error_code_t code;
29
+    const char *message;
30
+} error_messages[] = {
31
+    {ERR_SUCCESS, "Success"},
32
+    {ERR_INVALID_ARGS, "Invalid arguments"},
33
+    {ERR_CONFIG_NOT_FOUND, "Configuration file not found"},
34
+    {ERR_CONFIG_INVALID, "Configuration file is invalid"},
35
+    {ERR_CONFIG_WRITE_FAILED, "Failed to write configuration"},
36
+    {ERR_ACCOUNT_NOT_FOUND, "Account not found"},
37
+    {ERR_ACCOUNT_INVALID, "Account configuration is invalid"},
38
+    {ERR_ACCOUNT_EXISTS, "Account already exists"},
39
+    {ERR_GIT_NOT_FOUND, "Git command not found"},
40
+    {ERR_GIT_CONFIG_FAILED, "Git configuration operation failed"},
41
+    {ERR_GIT_NOT_REPO, "Not a git repository"},
42
+    {ERR_GIT_CONFIG_NOT_FOUND, "Git configuration not found"},
43
+    {ERR_GIT_REPOSITORY_INVALID, "Git repository is invalid"},
44
+    {ERR_SSH_AGENT_FAILED, "SSH agent operation failed"},
45
+    {ERR_SSH_KEY_FAILED, "SSH key operation failed"},
46
+    {ERR_SSH_KEY_NOT_FOUND, "SSH key not found"},
47
+    {ERR_SSH_CONNECTION_FAILED, "SSH connection test failed"},
48
+    {ERR_SSH_NOT_FOUND, "SSH command not found"},
49
+    {ERR_SSH_AGENT_NOT_FOUND, "SSH agent command not found"},
50
+    {ERR_SSH_KEY_LOAD_FAILED, "Failed to load SSH key"},
51
+    {ERR_SSH_AGENT_START_FAILED, "Failed to start SSH agent"},
52
+    {ERR_SSH_KEY_INVALID, "SSH key file is invalid"},
53
+    {ERR_SSH_KEY_PERMISSIONS, "SSH key file has wrong permissions"},
54
+    {ERR_SSH_KEY_OWNERSHIP, "SSH key file has wrong ownership"},
55
+    {ERR_SSH_AGENT_SOCKET_INVALID, "SSH agent socket is invalid"},
56
+    {ERR_GPG_NOT_FOUND, "GPG command not found"},
57
+    {ERR_GPG_KEY_FAILED, "GPG key operation failed"},
58
+    {ERR_GPG_KEY_NOT_FOUND, "GPG key not found"},
59
+    {ERR_GPG_SIGNING_FAILED, "GPG signing operation failed"},
60
+    {ERR_MEMORY_ALLOCATION, "Memory allocation failed"},
61
+    {ERR_FILE_IO, "File I/O operation failed"},
62
+    {ERR_PERMISSION_DENIED, "Permission denied"},
63
+    {ERR_NETWORK_ERROR, "Network operation failed"},
64
+    {ERR_SYSTEM_CALL, "System call failed"},
65
+    {ERR_SYSTEM_REQUIREMENT, "System requirement not met"},
66
+    {ERR_SYSTEM_COMMAND_FAILED, "System command execution failed"},
67
+    {ERR_INVALID_PATH, "Invalid file path"},
68
+    {ERR_UNKNOWN, "Unknown error"}
69
+};
70
+
71
+/* Log level to string mapping */
72
+static const char* log_level_strings[] = {
73
+    "DEBUG", "INFO", "WARN", "ERROR", "CRIT"
74
+};
75
+
76
+/* Initialize error handling and logging system */
77
+int error_init(log_level_t level, const char *log_file_path) {
78
+    g_log_level = level;
79
+    
80
+    /* Close existing log file if open */
81
+    if (g_log_file && g_log_file != stderr) {
82
+        fclose(g_log_file);
83
+        g_log_file = NULL;
84
+    }
85
+    
86
+    /* Open new log file if specified */
87
+    if (log_file_path) {
88
+        g_log_file = fopen(log_file_path, "a");
89
+        if (!g_log_file) {
90
+            /* Fall back to stderr if file can't be opened */
91
+            g_log_file = stderr;
92
+            set_error(ERR_FILE_IO, "Failed to open log file: %s", log_file_path);
93
+            return -1;
94
+        }
95
+        /* Set log file to line buffered for immediate output */
96
+        setvbuf(g_log_file, NULL, _IOLBF, 0);
97
+    } else {
98
+        g_log_file = stderr;
99
+    }
100
+    
101
+    /* Clear any existing error */
102
+    clear_error();
103
+    
104
+    log_info("Error handling system initialized (level=%s)", 
105
+             log_level_strings[level]);
106
+    
107
+    return 0;
108
+}
109
+
110
+/* Cleanup error handling system */
111
+void error_cleanup(void) {
112
+    if (g_log_file && g_log_file != stderr) {
113
+        log_info("Error handling system shutting down");
114
+        fclose(g_log_file);
115
+        g_log_file = NULL;
116
+    }
117
+    
118
+    /* Clear error context */
119
+    clear_error();
120
+}
121
+
122
+/* Set error context with detailed information */
123
+void set_error_context(error_code_t code, const char *file, int line,
124
+                       const char *function, const char *fmt, ...) {
125
+    va_list args;
126
+    
127
+    /* Clear previous error */
128
+    memset(&g_last_error, 0, sizeof(g_last_error));
129
+    
130
+    g_last_error.code = code;
131
+    g_last_error.file = file;
132
+    g_last_error.line = line;
133
+    g_last_error.function = function;
134
+    g_last_error.system_errno = 0;
135
+    
136
+    /* Format the error message */
137
+    if (fmt) {
138
+        va_start(args, fmt);
139
+        vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
140
+        va_end(args);
141
+    } else {
142
+        strncpy(g_last_error.message, error_code_to_string(code), 
143
+                sizeof(g_last_error.message) - 1);
144
+    }
145
+    
146
+    /* Log the error */
147
+    log_error("Error set: %s (%s:%d in %s)", 
148
+              g_last_error.message, file, line, function);
149
+}
150
+
151
+/* Set error context including system errno */
152
+void set_system_error_context(error_code_t code, const char *file, int line,
153
+                              const char *function, const char *fmt, ...) {
154
+    va_list args;
155
+    int saved_errno = errno; /* Save errno before any other operations */
156
+    
157
+    /* Clear previous error */
158
+    memset(&g_last_error, 0, sizeof(g_last_error));
159
+    
160
+    g_last_error.code = code;
161
+    g_last_error.file = file;
162
+    g_last_error.line = line;
163
+    g_last_error.function = function;
164
+    g_last_error.system_errno = saved_errno;
165
+    
166
+    /* Format the error message */
167
+    if (fmt) {
168
+        va_start(args, fmt);
169
+        vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
170
+        va_end(args);
171
+    } else {
172
+        strncpy(g_last_error.message, error_code_to_string(code), 
173
+                sizeof(g_last_error.message) - 1);
174
+    }
175
+    
176
+    /* Add system error details */
177
+    if (saved_errno != 0) {
178
+        int msg_len = strlen(g_last_error.message);
179
+        snprintf(g_last_error.details, sizeof(g_last_error.details),
180
+                 "System error: %s (errno=%d)", strerror(saved_errno), saved_errno);
181
+        
182
+        /* Append system error to message if there's room */
183
+        if (msg_len < sizeof(g_last_error.message) - 50) {
184
+            snprintf(g_last_error.message + msg_len, 
185
+                    sizeof(g_last_error.message) - msg_len,
186
+                    " (%s)", strerror(saved_errno));
187
+        }
188
+    }
189
+    
190
+    /* Log the error with system details */
191
+    log_error("System error: %s [errno=%d: %s] (%s:%d in %s)", 
192
+              g_last_error.message, saved_errno, strerror(saved_errno),
193
+              file, line, function);
194
+}
195
+
196
+/* Get last error information */
197
+const error_context_t *get_last_error(void) {
198
+    return &g_last_error;
199
+}
200
+
201
+/* Clear last error */
202
+void clear_error(void) {
203
+    memset(&g_last_error, 0, sizeof(g_last_error));
204
+}
205
+
206
+/* Convert error code to human-readable string */
207
+const char *error_code_to_string(error_code_t code) {
208
+    for (size_t i = 0; i < sizeof(error_messages) / sizeof(error_messages[0]); i++) {
209
+        if (error_messages[i].code == code) {
210
+            return error_messages[i].message;
211
+        }
212
+    }
213
+    return "Unknown error code";
214
+}
215
+
216
+/* Log message with context information */
217
+void log_message(log_level_t level, const char *file, int line,
218
+                 const char *function, const char *fmt, ...) {
219
+    va_list args;
220
+    char timestamp[32];
221
+    char message[1024];
222
+    
223
+    /* Check if this level should be logged */
224
+    if (!should_log(level)) {
225
+        return;
226
+    }
227
+    
228
+    /* Format the message */
229
+    va_start(args, fmt);
230
+    vsnprintf(message, sizeof(message), fmt, args);
231
+    va_end(args);
232
+    
233
+    /* Get timestamp */
234
+    get_timestamp(timestamp, sizeof(timestamp));
235
+    
236
+    /* Format complete log entry */
237
+    const char *level_str = (level < LOG_LEVEL_CRITICAL) ? 
238
+        log_level_strings[level] : "UNKNOWN";
239
+    
240
+    /* Log to file/stderr */
241
+    if (g_log_file) {
242
+        fprintf(g_log_file, "[%s] %s %s:%d (%s) - %s\n",
243
+                timestamp, level_str, file, line, function, message);
244
+        fflush(g_log_file);
245
+    }
246
+    
247
+    /* Also log to stderr if enabled and not already logging there */
248
+    if (g_log_to_stderr && g_log_file != stderr) {
249
+        fprintf(stderr, "[%s] %s - %s\n", timestamp, level_str, message);
250
+    }
251
+}
252
+
253
+/* Set logging level */
254
+void set_log_level(log_level_t level) {
255
+    g_log_level = level;
256
+    log_info("Log level changed to %s", log_level_strings[level]);
257
+}
258
+
259
+/* Set log output file */
260
+int set_log_file(const char *file_path) {
261
+    FILE *new_file = NULL;
262
+    
263
+    if (file_path) {
264
+        new_file = fopen(file_path, "a");
265
+        if (!new_file) {
266
+            set_system_error(ERR_FILE_IO, "Failed to open log file: %s", file_path);
267
+            return -1;
268
+        }
269
+        setvbuf(new_file, NULL, _IOLBF, 0);
270
+    } else {
271
+        new_file = stderr;
272
+    }
273
+    
274
+    /* Close old file if it's not stderr */
275
+    if (g_log_file && g_log_file != stderr) {
276
+        fclose(g_log_file);
277
+    }
278
+    
279
+    g_log_file = new_file;
280
+    log_info("Log output changed to %s", file_path ? file_path : "stderr");
281
+    
282
+    return 0;
283
+}
284
+
285
+/* Enable/disable logging to stderr */
286
+void set_log_to_stderr(bool enable) {
287
+    g_log_to_stderr = enable;
288
+}
289
+
290
+/* Format error message for user display */
291
+void format_error_message(char *buffer, size_t buffer_size, 
292
+                          const error_context_t *error) {
293
+    if (!buffer || buffer_size == 0 || !error) {
294
+        return;
295
+    }
296
+    
297
+    if (error->system_errno != 0) {
298
+        snprintf(buffer, buffer_size, 
299
+                "Error: %s\nDetails: %s\nLocation: %s:%d in %s()",
300
+                error->message, error->details, 
301
+                error->file, error->line, error->function);
302
+    } else {
303
+        snprintf(buffer, buffer_size,
304
+                "Error: %s\nLocation: %s:%d in %s()",
305
+                error->message, error->file, error->line, error->function);
306
+    }
307
+}
308
+
309
+/* Print formatted error to stderr */
310
+void print_error(const char *prefix) {
311
+    char error_msg[2048];
312
+    
313
+    if (g_last_error.code == ERR_SUCCESS) {
314
+        return; /* No error to print */
315
+    }
316
+    
317
+    format_error_message(error_msg, sizeof(error_msg), &g_last_error);
318
+    
319
+    if (prefix) {
320
+        fprintf(stderr, "%s: %s\n", prefix, error_msg);
321
+    } else {
322
+        fprintf(stderr, "%s\n", error_msg);
323
+    }
324
+}
325
+
326
+/* Check if error level should be logged */
327
+bool should_log(log_level_t level) {
328
+    return level >= g_log_level;
329
+}
330
+
331
+/* Get current timestamp for logging */
332
+void get_timestamp(char *buffer, size_t buffer_size) {
333
+    time_t now;
334
+    struct tm *tm_info;
335
+    
336
+    if (!buffer || buffer_size == 0) {
337
+        return;
338
+    }
339
+    
340
+    time(&now);
341
+    tm_info = localtime(&now);
342
+    
343
+    if (tm_info) {
344
+        strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", tm_info);
345
+    } else {
346
+        strncpy(buffer, "UNKNOWN-TIME", buffer_size - 1);
347
+        buffer[buffer_size - 1] = '\0';
348
+    }
349
+}
350
+
351
+/* Safe string copy with error context */
352
+int safe_strncpy(char *dest, const char *src, size_t dest_size) {
353
+    if (!dest || !src || dest_size == 0) {
354
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_strncpy");
355
+        return -1;
356
+    }
357
+    
358
+    size_t src_len = strlen(src);
359
+    if (src_len >= dest_size) {
360
+        set_error(ERR_INVALID_ARGS, "String too long for destination buffer");
361
+        return -1;
362
+    }
363
+    
364
+    strncpy(dest, src, dest_size - 1);
365
+    dest[dest_size - 1] = '\0';
366
+    return 0;
367
+}
368
+
369
+/* Safe string concatenation with error context */
370
+int safe_strncat(char *dest, const char *src, size_t dest_size) {
371
+    if (!dest || !src || dest_size == 0) {
372
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_strncat");
373
+        return -1;
374
+    }
375
+    
376
+    size_t dest_len = strlen(dest);
377
+    size_t src_len = strlen(src);
378
+    
379
+    if (dest_len + src_len >= dest_size) {
380
+        set_error(ERR_INVALID_ARGS, "String concatenation would overflow buffer");
381
+        return -1;
382
+    }
383
+    
384
+    strncat(dest, src, dest_size - dest_len - 1);
385
+    return 0;
386
+}
387
+
388
+/* Safe snprintf with error context */
389
+int safe_snprintf(char *buffer, size_t buffer_size, const char *fmt, ...) {
390
+    va_list args;
391
+    int result;
392
+    
393
+    if (!buffer || !fmt || buffer_size == 0) {
394
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_snprintf");
395
+        return -1;
396
+    }
397
+    
398
+    va_start(args, fmt);
399
+    result = vsnprintf(buffer, buffer_size, fmt, args);
400
+    va_end(args);
401
+    
402
+    if (result < 0) {
403
+        set_error(ERR_INVALID_ARGS, "snprintf formatting error");
404
+        return -1;
405
+    }
406
+    
407
+    if ((size_t)result >= buffer_size) {
408
+        set_error(ERR_INVALID_ARGS, "snprintf output truncated");
409
+        return -1;
410
+    }
411
+    
412
+    return result;
413
+}
414
+
415
+/* Safe memory allocation with error context */
416
+void *safe_malloc(size_t size) {
417
+    void *ptr;
418
+    
419
+    if (size == 0) {
420
+        set_error(ERR_INVALID_ARGS, "Attempted to allocate zero bytes");
421
+        return NULL;
422
+    }
423
+    
424
+    ptr = malloc(size);
425
+    if (!ptr) {
426
+        set_system_error(ERR_MEMORY_ALLOCATION, "Failed to allocate %zu bytes", size);
427
+        return NULL;
428
+    }
429
+    
430
+    return ptr;
431
+}
432
+
433
+/* Safe calloc with error context */
434
+void *safe_calloc(size_t nmemb, size_t size) {
435
+    void *ptr;
436
+    
437
+    if (nmemb == 0 || size == 0) {
438
+        set_error(ERR_INVALID_ARGS, "Attempted to allocate zero elements");
439
+        return NULL;
440
+    }
441
+    
442
+    ptr = calloc(nmemb, size);
443
+    if (!ptr) {
444
+        set_system_error(ERR_MEMORY_ALLOCATION, 
445
+                        "Failed to allocate %zu elements of %zu bytes", nmemb, size);
446
+        return NULL;
447
+    }
448
+    
449
+    return ptr;
450
+}
451
+
452
+/* Safe realloc with error context */
453
+void *safe_realloc(void *ptr, size_t size) {
454
+    void *new_ptr;
455
+    
456
+    if (size == 0) {
457
+        set_error(ERR_INVALID_ARGS, "Attempted to reallocate to zero bytes");
458
+        return NULL;
459
+    }
460
+    
461
+    new_ptr = realloc(ptr, size);
462
+    if (!new_ptr) {
463
+        set_system_error(ERR_MEMORY_ALLOCATION, "Failed to reallocate to %zu bytes", size);
464
+        return NULL;
465
+    }
466
+    
467
+    return new_ptr;
468
+}
src/git_ops.cadded
@@ -0,0 +1,682 @@
1
+/* Git configuration operations with comprehensive validation and security
2
+ * Implements safe git configuration management for gitswitch-c
3
+ */
4
+
5
+#define _POSIX_C_SOURCE 200809L
6
+#include <stdio.h>
7
+#include <stdlib.h>
8
+#include <string.h>
9
+#include <unistd.h>
10
+#include <sys/wait.h>
11
+#include <sys/stat.h>
12
+
13
+#include "git_ops.h"
14
+#include "error.h"
15
+#include "utils.h"
16
+#include "display.h"
17
+
18
+/* Internal helper functions */
19
+static int execute_git_command(const char *args, char *output, size_t output_size);
20
+static int validate_git_installation(void);
21
+static int detect_repository_scope(git_scope_t *detected_scope);
22
+static bool is_valid_git_config_value(const char *value);
23
+static int backup_git_config_if_needed(git_scope_t scope);
24
+static int restore_git_config_if_needed(git_scope_t scope);
25
+
26
+/* Initialize git operations */
27
+int git_ops_init(void) {
28
+    log_debug("Initializing git operations");
29
+    
30
+    /* Validate git installation */
31
+    if (validate_git_installation() != 0) {
32
+        set_error(ERR_SYSTEM_REQUIREMENT, "Git validation failed");
33
+        return -1;
34
+    }
35
+    
36
+    log_info("Git operations initialized successfully");
37
+    return 0;
38
+}
39
+
40
+/* Set git configuration for account */
41
+int git_set_config(const account_t *account, git_scope_t scope) {
42
+    const char *scope_flag;
43
+    
44
+    if (!account) {
45
+        set_error(ERR_INVALID_ARGS, "NULL account to git_set_config");
46
+        return -1;
47
+    }
48
+    
49
+    /* Validate account data */
50
+    if (!validate_name(account->name)) {
51
+        set_error(ERR_ACCOUNT_INVALID, "Invalid account name for git config");
52
+        return -1;
53
+    }
54
+    
55
+    if (!validate_email(account->email)) {
56
+        set_error(ERR_ACCOUNT_INVALID, "Invalid account email for git config");
57
+        return -1;
58
+    }
59
+    
60
+    /* Get scope flag */
61
+    scope_flag = git_scope_to_flag(scope);
62
+    if (!scope_flag) {
63
+        set_error(ERR_INVALID_ARGS, "Invalid git scope");
64
+        return -1;
65
+    }
66
+    
67
+    /* If local scope, ensure we're in a git repository */
68
+    if (scope == GIT_SCOPE_LOCAL && !git_is_repository()) {
69
+        set_error(ERR_GIT_NOT_REPOSITORY, "Not in a git repository, cannot set local config");
70
+        return -1;
71
+    }
72
+    
73
+    /* Backup current configuration if requested */
74
+    if (backup_git_config_if_needed(scope) != 0) {
75
+        log_warning("Failed to backup git configuration");
76
+    }
77
+    
78
+    log_info("Setting git configuration for account: %s (%s scope)", account->name, scope_flag);
79
+    
80
+    /* Set user.name */
81
+    if (git_set_config_value(GIT_CONFIG_USER_NAME, account->name, scope) != 0) {
82
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to set user.name");
83
+        return -1;
84
+    }
85
+    
86
+    /* Set user.email */
87
+    if (git_set_config_value(GIT_CONFIG_USER_EMAIL, account->email, scope) != 0) {
88
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to set user.email");
89
+        return -1;
90
+    }
91
+    
92
+    /* Configure GPG if enabled */
93
+    if (account->gpg_enabled) {
94
+        if (git_configure_gpg(account, scope) != 0) {
95
+            log_warning("Failed to configure GPG for git");
96
+            /* Don't fail completely, GPG is optional */
97
+        }
98
+    } else {
99
+        /* Disable GPG signing */
100
+        git_unset_config_value(GIT_CONFIG_USER_SIGNINGKEY, scope);
101
+        git_set_config_value(GIT_CONFIG_COMMIT_GPGSIGN, "false", scope);
102
+    }
103
+    
104
+    /* Configure SSH if enabled */
105
+    if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
106
+        if (git_configure_ssh(account, scope) != 0) {
107
+            log_warning("Failed to configure SSH for git");
108
+            /* Don't fail completely, SSH config is optional */
109
+        }
110
+    } else {
111
+        /* Clear SSH configuration */
112
+        git_unset_config_value(GIT_CONFIG_CORE_SSHCOMMAND, scope);
113
+    }
114
+    
115
+    /* Verify configuration was set correctly */
116
+    git_current_config_t current_config;
117
+    if (git_get_current_config(&current_config) == 0) {
118
+        if (strcmp(current_config.name, account->name) != 0 ||
119
+            strcmp(current_config.email, account->email) != 0) {
120
+            set_error(ERR_GIT_CONFIG_FAILED, "Git configuration verification failed");
121
+            return -1;
122
+        }
123
+    }
124
+    
125
+    log_info("Git configuration set successfully for %s", account->name);
126
+    return 0;
127
+}
128
+
129
+/* Get current git configuration */
130
+int git_get_current_config(git_current_config_t *config) {
131
+    char name[MAX_NAME_LEN];
132
+    char email[MAX_EMAIL_LEN];
133
+    char signing_key[MAX_KEY_ID_LEN];
134
+    char gpg_sign[16];
135
+    
136
+    if (!config) {
137
+        set_error(ERR_INVALID_ARGS, "NULL config to git_get_current_config");
138
+        return -1;
139
+    }
140
+    
141
+    /* Initialize structure */
142
+    memset(config, 0, sizeof(git_current_config_t));
143
+    config->valid = false;
144
+    
145
+    /* Try to get user.name */
146
+    if (git_get_config_value(GIT_CONFIG_USER_NAME, name, sizeof(name), GIT_SCOPE_LOCAL) == 0) {
147
+        config->scope = GIT_SCOPE_LOCAL;
148
+    } else if (git_get_config_value(GIT_CONFIG_USER_NAME, name, sizeof(name), GIT_SCOPE_GLOBAL) == 0) {
149
+        config->scope = GIT_SCOPE_GLOBAL;
150
+    } else if (git_get_config_value(GIT_CONFIG_USER_NAME, name, sizeof(name), GIT_SCOPE_SYSTEM) == 0) {
151
+        config->scope = GIT_SCOPE_SYSTEM;
152
+    } else {
153
+        set_error(ERR_GIT_CONFIG_NOT_FOUND, "No git user.name configured");
154
+        return -1;
155
+    }
156
+    
157
+    /* Get user.email from same scope */
158
+    if (git_get_config_value(GIT_CONFIG_USER_EMAIL, email, sizeof(email), config->scope) != 0) {
159
+        set_error(ERR_GIT_CONFIG_NOT_FOUND, "No git user.email configured");
160
+        return -1;
161
+    }
162
+    
163
+    /* Copy basic configuration */
164
+    safe_strncpy(config->name, name, sizeof(config->name));
165
+    safe_strncpy(config->email, email, sizeof(config->email));
166
+    
167
+    /* Get GPG configuration if available */
168
+    if (git_get_config_value(GIT_CONFIG_USER_SIGNINGKEY, signing_key, sizeof(signing_key), config->scope) == 0) {
169
+        safe_strncpy(config->signing_key, signing_key, sizeof(config->signing_key));
170
+    }
171
+    
172
+    /* Check if GPG signing is enabled */
173
+    if (git_get_config_value(GIT_CONFIG_COMMIT_GPGSIGN, gpg_sign, sizeof(gpg_sign), config->scope) == 0) {
174
+        config->gpg_signing_enabled = (strcmp(gpg_sign, "true") == 0);
175
+    }
176
+    
177
+    config->valid = true;
178
+    return 0;
179
+}
180
+
181
+/* Clear git configuration */
182
+int git_clear_config(git_scope_t scope) {
183
+    const char *scope_flag;
184
+    
185
+    scope_flag = git_scope_to_flag(scope);
186
+    if (!scope_flag) {
187
+        set_error(ERR_INVALID_ARGS, "Invalid git scope");
188
+        return -1;
189
+    }
190
+    
191
+    log_info("Clearing git configuration (%s scope)", scope_flag);
192
+    
193
+    /* Clear basic user configuration */
194
+    git_unset_config_value(GIT_CONFIG_USER_NAME, scope);
195
+    git_unset_config_value(GIT_CONFIG_USER_EMAIL, scope);
196
+    
197
+    /* Clear GPG configuration */
198
+    git_unset_config_value(GIT_CONFIG_USER_SIGNINGKEY, scope);
199
+    git_unset_config_value(GIT_CONFIG_COMMIT_GPGSIGN, scope);
200
+    git_unset_config_value(GIT_CONFIG_GPG_PROGRAM, scope);
201
+    
202
+    /* Clear SSH configuration */
203
+    git_unset_config_value(GIT_CONFIG_CORE_SSHCOMMAND, scope);
204
+    
205
+    log_info("Git configuration cleared");
206
+    return 0;
207
+}
208
+
209
+/* Validate git repository */
210
+int git_validate_repository(void) {
211
+    char output[256];
212
+    
213
+    if (!git_is_repository()) {
214
+        set_error(ERR_GIT_NOT_REPOSITORY, "Current directory is not a git repository");
215
+        return -1;
216
+    }
217
+    
218
+    /* Check if repository is bare */
219
+    if (execute_git_command("rev-parse --is-bare-repository", output, sizeof(output)) == 0) {
220
+        trim_whitespace(output);
221
+        if (strcmp(output, "true") == 0) {
222
+            set_error(ERR_GIT_REPOSITORY_INVALID, "Repository is bare");
223
+            return -1;
224
+        }
225
+    }
226
+    
227
+    /* Check repository health - verify we can read HEAD */
228
+    if (execute_git_command("rev-parse --verify HEAD", output, sizeof(output)) != 0) {
229
+        /* This is OK for new repositories with no commits */
230
+        log_debug("Repository has no commits yet (new repository)");
231
+    }
232
+    
233
+    return 0;
234
+}
235
+
236
+/* Get git configuration scope */
237
+git_scope_t git_get_config_scope(const char *config_key) {
238
+    char value[512];
239
+    
240
+    if (!config_key) {
241
+        return GIT_SCOPE_GLOBAL; /* Default fallback */
242
+    }
243
+    
244
+    /* Try local scope first if we're in a repository */
245
+    if (git_is_repository()) {
246
+        if (git_get_config_value(config_key, value, sizeof(value), GIT_SCOPE_LOCAL) == 0) {
247
+            return GIT_SCOPE_LOCAL;
248
+        }
249
+    }
250
+    
251
+    /* Try global scope */
252
+    if (git_get_config_value(config_key, value, sizeof(value), GIT_SCOPE_GLOBAL) == 0) {
253
+        return GIT_SCOPE_GLOBAL;
254
+    }
255
+    
256
+    /* Try system scope */
257
+    if (git_get_config_value(config_key, value, sizeof(value), GIT_SCOPE_SYSTEM) == 0) {
258
+        return GIT_SCOPE_SYSTEM;
259
+    }
260
+    
261
+    /* Default to global if not found */
262
+    return GIT_SCOPE_GLOBAL;
263
+}
264
+
265
+/* Test git configuration */
266
+int git_test_config(const account_t *account, git_scope_t scope) {
267
+    git_current_config_t current_config;
268
+    (void)scope; /* Suppress unused parameter warning */
269
+    
270
+    if (!account) {
271
+        set_error(ERR_INVALID_ARGS, "NULL account to git_test_config");
272
+        return -1;
273
+    }
274
+    
275
+    log_info("Testing git configuration for account: %s", account->name);
276
+    
277
+    /* Get current configuration and verify it matches */
278
+    if (git_get_current_config(&current_config) != 0) {
279
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to read current git configuration");
280
+        return -1;
281
+    }
282
+    
283
+    if (strcmp(current_config.name, account->name) != 0) {
284
+        set_error(ERR_GIT_CONFIG_FAILED, "Git user.name does not match account: expected '%s', got '%s'",
285
+                  account->name, current_config.name);
286
+        return -1;
287
+    }
288
+    
289
+    if (strcmp(current_config.email, account->email) != 0) {
290
+        set_error(ERR_GIT_CONFIG_FAILED, "Git user.email does not match account: expected '%s', got '%s'",
291
+                  account->email, current_config.email);
292
+        return -1;
293
+    }
294
+    
295
+    /* Test GPG configuration if enabled */
296
+    if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
297
+        if (strlen(current_config.signing_key) == 0) {
298
+            set_error(ERR_GIT_CONFIG_FAILED, "GPG signing key not configured in git");
299
+            return -1;
300
+        }
301
+        
302
+        if (!current_config.gpg_signing_enabled) {
303
+            log_warning("GPG signing is configured but not enabled");
304
+        }
305
+        
306
+        /* Test GPG key availability */
307
+        char gpg_test[256];
308
+        snprintf(gpg_test, sizeof(gpg_test), "gpg --list-secret-keys '%s' >/dev/null 2>&1", 
309
+                 account->gpg_key_id);
310
+        if (system(gpg_test) != 0) {
311
+            set_error(ERR_GPG_KEY_NOT_FOUND, "GPG key not available: %s", account->gpg_key_id);
312
+            return -1;
313
+        }
314
+    }
315
+    
316
+    log_info("Git configuration test passed for %s", account->name);
317
+    return 0;
318
+}
319
+
320
+/* Set single git configuration value */
321
+int git_set_config_value(const char *key, const char *value, git_scope_t scope) {
322
+    char command[1024];
323
+    char output[256];
324
+    const char *scope_flag;
325
+    
326
+    if (!key || !value) {
327
+        set_error(ERR_INVALID_ARGS, "NULL key or value to git_set_config_value");
328
+        return -1;
329
+    }
330
+    
331
+    if (!is_valid_git_config_value(value)) {
332
+        set_error(ERR_INVALID_ARGS, "Invalid characters in git config value");
333
+        return -1;
334
+    }
335
+    
336
+    scope_flag = git_scope_to_flag(scope);
337
+    if (!scope_flag) {
338
+        set_error(ERR_INVALID_ARGS, "Invalid git scope");
339
+        return -1;
340
+    }
341
+    
342
+    /* Build git config command */
343
+    if (snprintf(command, sizeof(command), "config %s '%s' '%s'", 
344
+                 scope_flag, key, value) >= sizeof(command)) {
345
+        set_error(ERR_INVALID_ARGS, "Git config command too long");
346
+        return -1;
347
+    }
348
+    
349
+    log_debug("Setting git config: %s = %s (%s)", key, value, scope_flag);
350
+    
351
+    if (execute_git_command(command, output, sizeof(output)) != 0) {
352
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to set git config %s: %s", key, output);
353
+        return -1;
354
+    }
355
+    
356
+    return 0;
357
+}
358
+
359
+/* Get single git configuration value */
360
+int git_get_config_value(const char *key, char *value, size_t value_size, git_scope_t scope) {
361
+    char command[512];
362
+    char output[512];
363
+    const char *scope_flag;
364
+    
365
+    if (!key || !value || value_size == 0) {
366
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to git_get_config_value");
367
+        return -1;
368
+    }
369
+    
370
+    scope_flag = git_scope_to_flag(scope);
371
+    if (!scope_flag) {
372
+        set_error(ERR_INVALID_ARGS, "Invalid git scope");
373
+        return -1;
374
+    }
375
+    
376
+    /* Build git config command */
377
+    if (snprintf(command, sizeof(command), "config %s '%s'", scope_flag, key) >= sizeof(command)) {
378
+        set_error(ERR_INVALID_ARGS, "Git config command too long");
379
+        return -1;
380
+    }
381
+    
382
+    if (execute_git_command(command, output, sizeof(output)) != 0) {
383
+        /* Config value not found - this is not always an error */
384
+        value[0] = '\0';
385
+        return -1;
386
+    }
387
+    
388
+    /* Remove trailing newline */
389
+    trim_whitespace(output);
390
+    safe_strncpy(value, output, value_size);
391
+    
392
+    return 0;
393
+}
394
+
395
+/* Unset git configuration value */
396
+int git_unset_config_value(const char *key, git_scope_t scope) {
397
+    char command[512];
398
+    char output[256];
399
+    const char *scope_flag;
400
+    
401
+    if (!key) {
402
+        set_error(ERR_INVALID_ARGS, "NULL key to git_unset_config_value");
403
+        return -1;
404
+    }
405
+    
406
+    scope_flag = git_scope_to_flag(scope);
407
+    if (!scope_flag) {
408
+        set_error(ERR_INVALID_ARGS, "Invalid git scope");
409
+        return -1;
410
+    }
411
+    
412
+    /* Build git config unset command */
413
+    if (snprintf(command, sizeof(command), "config %s --unset '%s'", 
414
+                 scope_flag, key) >= sizeof(command)) {
415
+        set_error(ERR_INVALID_ARGS, "Git config command too long");
416
+        return -1;
417
+    }
418
+    
419
+    log_debug("Unsetting git config: %s (%s)", key, scope_flag);
420
+    
421
+    /* Execute command - ignore errors as key might not exist */
422
+    execute_git_command(command, output, sizeof(output));
423
+    
424
+    return 0;
425
+}
426
+
427
+/* List all git configuration values */
428
+int git_list_config(git_scope_t scope, char *output, size_t output_size) {
429
+    char command[256];
430
+    const char *scope_flag;
431
+    
432
+    if (!output || output_size == 0) {
433
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to git_list_config");
434
+        return -1;
435
+    }
436
+    
437
+    scope_flag = git_scope_to_flag(scope);
438
+    if (!scope_flag) {
439
+        set_error(ERR_INVALID_ARGS, "Invalid git scope");
440
+        return -1;
441
+    }
442
+    
443
+    /* Build git config list command */
444
+    if (snprintf(command, sizeof(command), "config %s --list", scope_flag) >= sizeof(command)) {
445
+        set_error(ERR_INVALID_ARGS, "Git config command too long");
446
+        return -1;
447
+    }
448
+    
449
+    if (execute_git_command(command, output, output_size) != 0) {
450
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to list git configuration");
451
+        return -1;
452
+    }
453
+    
454
+    return 0;
455
+}
456
+
457
+/* Configure SSH command for git operations */
458
+int git_configure_ssh(const account_t *account, git_scope_t scope) {
459
+    char ssh_command[MAX_PATH_LEN * 2];
460
+    char expanded_key_path[MAX_PATH_LEN];
461
+    
462
+    if (!account || !account->ssh_enabled || strlen(account->ssh_key_path) == 0) {
463
+        return 0; /* Nothing to configure */
464
+    }
465
+    
466
+    /* Expand SSH key path */
467
+    if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
468
+        set_error(ERR_INVALID_PATH, "Failed to expand SSH key path: %s", account->ssh_key_path);
469
+        return -1;
470
+    }
471
+    
472
+    /* Verify SSH key file exists and has correct permissions */
473
+    if (!path_exists(expanded_key_path)) {
474
+        set_error(ERR_SSH_KEY_NOT_FOUND, "SSH key file not found: %s", expanded_key_path);
475
+        return -1;
476
+    }
477
+    
478
+    /* Build SSH command with security options */
479
+    if (snprintf(ssh_command, sizeof(ssh_command),
480
+                 "ssh -i '%s' -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no",
481
+                 expanded_key_path) >= sizeof(ssh_command)) {
482
+        set_error(ERR_INVALID_ARGS, "SSH command too long");
483
+        return -1;
484
+    }
485
+    
486
+    log_debug("Configuring SSH command: %s", ssh_command);
487
+    
488
+    if (git_set_config_value(GIT_CONFIG_CORE_SSHCOMMAND, ssh_command, scope) != 0) {
489
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to set SSH command configuration");
490
+        return -1;
491
+    }
492
+    
493
+    return 0;
494
+}
495
+
496
+/* Configure GPG for git operations */
497
+int git_configure_gpg(const account_t *account, git_scope_t scope) {
498
+    if (!account || !account->gpg_enabled || strlen(account->gpg_key_id) == 0) {
499
+        return 0; /* Nothing to configure */
500
+    }
501
+    
502
+    log_debug("Configuring GPG signing key: %s", account->gpg_key_id);
503
+    
504
+    /* Set signing key */
505
+    if (git_set_config_value(GIT_CONFIG_USER_SIGNINGKEY, account->gpg_key_id, scope) != 0) {
506
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to set GPG signing key");
507
+        return -1;
508
+    }
509
+    
510
+    /* Enable/disable GPG signing */
511
+    if (git_set_config_value(GIT_CONFIG_COMMIT_GPGSIGN, 
512
+                            account->gpg_signing_enabled ? "true" : "false", scope) != 0) {
513
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to set GPG signing preference");
514
+        return -1;
515
+    }
516
+    
517
+    return 0;
518
+}
519
+
520
+/* Check if current directory is a git repository */
521
+bool git_is_repository(void) {
522
+    char output[256];
523
+    
524
+    /* Use git rev-parse --git-dir to check for repository */
525
+    if (execute_git_command("rev-parse --git-dir", output, sizeof(output)) == 0) {
526
+        return true;
527
+    }
528
+    
529
+    return false;
530
+}
531
+
532
+/* Get repository root directory */
533
+int git_get_repo_root(char *path, size_t path_size) {
534
+    char output[MAX_PATH_LEN];
535
+    
536
+    if (!path || path_size == 0) {
537
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to git_get_repo_root");
538
+        return -1;
539
+    }
540
+    
541
+    if (execute_git_command("rev-parse --show-toplevel", output, sizeof(output)) != 0) {
542
+        set_error(ERR_GIT_NOT_REPOSITORY, "Not in a git repository");
543
+        return -1;
544
+    }
545
+    
546
+    trim_whitespace(output);
547
+    safe_strncpy(path, output, path_size);
548
+    
549
+    return 0;
550
+}
551
+
552
+/* Convert scope enum to git config scope string */
553
+const char *git_scope_to_flag(git_scope_t scope) {
554
+    switch (scope) {
555
+        case GIT_SCOPE_LOCAL:  return "--local";
556
+        case GIT_SCOPE_GLOBAL: return "--global";
557
+        case GIT_SCOPE_SYSTEM: return "--system";
558
+        default: return NULL;
559
+    }
560
+}
561
+
562
+/* Internal helper functions */
563
+
564
+/* Execute git command and capture output */
565
+static int execute_git_command(const char *args, char *output, size_t output_size) {
566
+    char command[1024];
567
+    FILE *pipe;
568
+    
569
+    if (!args) {
570
+        return -1;
571
+    }
572
+    
573
+    /* Build full git command */
574
+    if (snprintf(command, sizeof(command), "git %s 2>&1", args) >= sizeof(command)) {
575
+        set_error(ERR_INVALID_ARGS, "Git command too long");
576
+        return -1;
577
+    }
578
+    
579
+    log_debug("Executing git command: %s", command);
580
+    
581
+    /* Execute command */
582
+    pipe = popen(command, "r");
583
+    if (!pipe) {
584
+        set_system_error(ERR_SYSTEM_COMMAND_FAILED, "Failed to execute git command");
585
+        return -1;
586
+    }
587
+    
588
+    /* Read output if buffer provided */
589
+    if (output && output_size > 0) {
590
+        if (!fgets(output, output_size, pipe)) {
591
+            output[0] = '\0';
592
+        }
593
+    }
594
+    
595
+    int exit_code = pclose(pipe);
596
+    if (exit_code != 0) {
597
+        log_debug("Git command failed with exit code: %d", exit_code);
598
+        return -1;
599
+    }
600
+    
601
+    return 0;
602
+}
603
+
604
+/* Validate git installation */
605
+static int validate_git_installation(void) {
606
+    char version_output[256];
607
+    
608
+    /* Check if git is available */
609
+    if (!command_exists("git")) {
610
+        set_error(ERR_SYSTEM_REQUIREMENT, "Git is not installed or not in PATH");
611
+        return -1;
612
+    }
613
+    
614
+    /* Get git version */
615
+    if (execute_git_command("--version", version_output, sizeof(version_output)) != 0) {
616
+        set_error(ERR_SYSTEM_REQUIREMENT, "Failed to get git version");
617
+        return -1;
618
+    }
619
+    
620
+    log_debug("Git version: %s", version_output);
621
+    
622
+    /* Basic version check - require git 2.0+ */
623
+    if (!strstr(version_output, "git version ")) {
624
+        set_error(ERR_SYSTEM_REQUIREMENT, "Unexpected git version output");
625
+        return -1;
626
+    }
627
+    
628
+    return 0;
629
+}
630
+
631
+/* Detect repository scope */
632
+static int detect_repository_scope(git_scope_t *detected_scope) {
633
+    if (!detected_scope) {
634
+        return -1;
635
+    }
636
+    
637
+    if (git_is_repository()) {
638
+        *detected_scope = GIT_SCOPE_LOCAL;
639
+    } else {
640
+        *detected_scope = GIT_SCOPE_GLOBAL;
641
+    }
642
+    
643
+    return 0;
644
+}
645
+
646
+/* Validate git config value for security */
647
+static bool is_valid_git_config_value(const char *value) {
648
+    if (!value) {
649
+        return false;
650
+    }
651
+    
652
+    /* Check for dangerous characters */
653
+    const char *dangerous_chars = ";|&`$(){}[]";
654
+    for (const char *p = dangerous_chars; *p; p++) {
655
+        if (strchr(value, *p)) {
656
+            return false;
657
+        }
658
+    }
659
+    
660
+    /* Check for control characters */
661
+    for (const char *p = value; *p; p++) {
662
+        if (*p < 32 && *p != '\t') {
663
+            return false;
664
+        }
665
+    }
666
+    
667
+    return true;
668
+}
669
+
670
+/* Backup git config if needed */
671
+static int backup_git_config_if_needed(git_scope_t scope) {
672
+    /* TODO: Implement config backup for safety */
673
+    (void)scope; /* Suppress unused parameter warning */
674
+    return 0;
675
+}
676
+
677
+/* Restore git config if needed */
678
+static int restore_git_config_if_needed(git_scope_t scope) {
679
+    /* TODO: Implement config restore */
680
+    (void)scope; /* Suppress unused parameter warning */
681
+    return 0;
682
+}
src/gpg_manager.cadded
@@ -0,0 +1,709 @@
1
+/* GPG key and environment management with comprehensive isolation and security
2
+ * Implements per-account GNUPGHOME environments to prevent GPG key mixing
3
+ */
4
+
5
+#define _POSIX_C_SOURCE 200809L
6
+#define _DEFAULT_SOURCE
7
+#include <stdio.h>
8
+#include <stdlib.h>
9
+#include <string.h>
10
+#include <unistd.h>
11
+#include <sys/wait.h>
12
+#include <sys/stat.h>
13
+#include <signal.h>
14
+#include <errno.h>
15
+#include <fcntl.h>
16
+
17
+#include "gpg_manager.h"
18
+#include "error.h"
19
+#include "utils.h"
20
+#include "display.h"
21
+#include "git_ops.h"
22
+
23
+/* Internal helper functions */
24
+static int create_isolated_gnupg_home_dir(const char *gnupg_home);
25
+static int execute_gpg_command_in_env(const gpg_config_t *gpg_config, 
26
+                                       const char *command, char *output, size_t output_size);
27
+static int copy_key_from_system_keyring(const gpg_config_t *gpg_config, const char *key_id);
28
+static int validate_gnupg_home_permissions(const char *gnupg_home);
29
+static int setup_gpg_agent_config(const char *gnupg_home);
30
+
31
+/* Initialize GPG manager with specified mode */
32
+int gpg_manager_init(gpg_config_t *gpg_config, gpg_mode_t mode) {
33
+    if (!gpg_config) {
34
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_manager_init");
35
+        return -1;
36
+    }
37
+    
38
+    log_debug("Initializing GPG manager with mode: %d", mode);
39
+    
40
+    /* Initialize GPG configuration */
41
+    memset(gpg_config, 0, sizeof(gpg_config_t));
42
+    gpg_config->mode = mode;
43
+    gpg_config->signing_enabled = false;
44
+    gpg_config->home_owned = false;
45
+    
46
+    /* Initialize based on mode */
47
+    switch (mode) {
48
+        case GPG_MODE_SYSTEM:
49
+            /* Use system GNUPGHOME */
50
+            log_debug("Using system GPG environment");
51
+            break;
52
+            
53
+        case GPG_MODE_ISOLATED:
54
+            /* Will create isolated GNUPGHOME per account */
55
+            log_debug("GPG manager initialized for isolated environments");
56
+            break;
57
+            
58
+        case GPG_MODE_SHARED:
59
+            /* Shared GNUPGHOME with key switching */
60
+            log_debug("GPG manager initialized for shared environment");
61
+            break;
62
+            
63
+        default:
64
+            set_error(ERR_INVALID_ARGS, "Invalid GPG mode: %d", mode);
65
+            return -1;
66
+    }
67
+    
68
+    /* Verify GPG is available */
69
+    if (!command_exists("gpg")) {
70
+        set_error(ERR_GPG_NOT_FOUND, "GPG command not found in PATH");
71
+        return -1;
72
+    }
73
+    
74
+    log_info("GPG manager initialized successfully");
75
+    return 0;
76
+}
77
+
78
+/* Cleanup GPG manager */
79
+void gpg_manager_cleanup(gpg_config_t *gpg_config) {
80
+    if (!gpg_config) {
81
+        return;
82
+    }
83
+    
84
+    log_debug("Cleaning up GPG manager");
85
+    
86
+    /* Clean up owned GNUPGHOME directory if needed */
87
+    if (gpg_config->home_owned && strlen(gpg_config->gnupg_home) > 0) {
88
+        log_debug("Cleaning up owned GNUPGHOME: %s", gpg_config->gnupg_home);
89
+        
90
+        /* Only remove if it's clearly our isolated directory */
91
+        if (strstr(gpg_config->gnupg_home, "gitswitch-gpg") != NULL) {
92
+            char command[512];
93
+            if (safe_snprintf(command, sizeof(command), "rm -rf '%s'", gpg_config->gnupg_home) == 0) {
94
+                if (system(command) != 0) {
95
+                    log_warning("Failed to remove GNUPGHOME directory: %s", gpg_config->gnupg_home);
96
+                } else {
97
+                    log_debug("Successfully removed GNUPGHOME: %s", gpg_config->gnupg_home);
98
+                }
99
+            }
100
+        }
101
+    }
102
+    
103
+    /* Clear configuration */
104
+    memset(gpg_config, 0, sizeof(gpg_config_t));
105
+    
106
+    log_debug("GPG manager cleanup completed");
107
+}
108
+
109
+/* Switch to account's GPG configuration with complete isolation */
110
+int gpg_switch_account(gpg_config_t *gpg_config, const account_t *account) {
111
+    if (!gpg_config || !account) {
112
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_switch_account");
113
+        return -1;
114
+    }
115
+    
116
+    /* Skip if GPG not enabled for account */
117
+    if (!account->gpg_enabled || strlen(account->gpg_key_id) == 0) {
118
+        log_debug("GPG not enabled for account: %s", account->name);
119
+        return 0;
120
+    }
121
+    
122
+    log_info("Switching to GPG configuration for account: %s", account->name);
123
+    log_debug("Account GPG key ID: %s", account->gpg_key_id);
124
+    
125
+    /* Handle different GPG modes */
126
+    switch (gpg_config->mode) {
127
+        case GPG_MODE_SYSTEM:
128
+            /* Just validate key exists in system keyring */
129
+            if (gpg_validate_key(gpg_config, account->gpg_key_id) != 0) {
130
+                set_error(ERR_GPG_KEY_NOT_FOUND, "GPG key not found in system keyring: %s", 
131
+                         account->gpg_key_id);
132
+                return -1;
133
+            }
134
+            break;
135
+            
136
+        case GPG_MODE_ISOLATED:
137
+            /* Create isolated GNUPGHOME for account */
138
+            if (gpg_create_isolated_home(gpg_config, account) != 0) {
139
+                set_error(ERR_GPG_KEY_FAILED, "Failed to create isolated GPG environment: %s", 
140
+                         get_last_error()->message);
141
+                return -1;
142
+            }
143
+            
144
+            /* Copy key from system keyring to isolated environment */
145
+            if (copy_key_from_system_keyring(gpg_config, account->gpg_key_id) != 0) {
146
+                log_warning("Failed to copy GPG key to isolated environment: %s", 
147
+                           get_last_error()->message);
148
+                /* Continue anyway - maybe key is already there */
149
+            }
150
+            
151
+            /* Validate key is available in isolated environment */
152
+            if (gpg_validate_key(gpg_config, account->gpg_key_id) != 0) {
153
+                set_error(ERR_GPG_KEY_NOT_FOUND, "GPG key not available in isolated environment: %s", 
154
+                         account->gpg_key_id);
155
+                return -1;
156
+            }
157
+            break;
158
+            
159
+        case GPG_MODE_SHARED:
160
+            /* Validate key exists and switch to it */
161
+            if (gpg_validate_key(gpg_config, account->gpg_key_id) != 0) {
162
+                set_error(ERR_GPG_KEY_NOT_FOUND, "GPG key not found: %s", account->gpg_key_id);
163
+                return -1;
164
+            }
165
+            break;
166
+            
167
+        default:
168
+            set_error(ERR_INVALID_ARGS, "Invalid GPG mode: %d", gpg_config->mode);
169
+            return -1;
170
+    }
171
+    
172
+    /* Update GPG configuration */
173
+    safe_strncpy(gpg_config->current_key_id, account->gpg_key_id, sizeof(gpg_config->current_key_id));
174
+    gpg_config->signing_enabled = account->gpg_signing_enabled;
175
+    
176
+    /* Set environment variable if using isolated mode */
177
+    if (gpg_config->mode == GPG_MODE_ISOLATED) {
178
+        if (gpg_set_environment(gpg_config) != 0) {
179
+            log_warning("Failed to set GPG environment variable: %s", get_last_error()->message);
180
+        }
181
+    }
182
+    
183
+    /* Test GPG signing if enabled */
184
+    if (account->gpg_signing_enabled) {
185
+        if (gpg_test_signing(gpg_config, account->gpg_key_id) != 0) {
186
+            log_warning("GPG signing test failed for key: %s", account->gpg_key_id);
187
+            /* Don't fail completely, just warn */
188
+        } else {
189
+            log_info("GPG signing test passed for key: %s", account->gpg_key_id);
190
+        }
191
+    }
192
+    
193
+    log_info("Successfully switched to GPG configuration for account: %s", account->name);
194
+    return 0;
195
+}
196
+
197
+/* Create isolated GNUPGHOME for account */
198
+int gpg_create_isolated_home(gpg_config_t *gpg_config, const account_t *account) {
199
+    char gnupg_base_dir[MAX_PATH_LEN];
200
+    char gnupg_home[MAX_PATH_LEN];
201
+    const char *home_dir;
202
+    const char *runtime_dir;
203
+    
204
+    if (!gpg_config || !account) {
205
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_create_isolated_home");
206
+        return -1;
207
+    }
208
+    
209
+    /* Determine base directory for isolated GNUPGHOME */
210
+    runtime_dir = getenv("XDG_RUNTIME_DIR");
211
+    home_dir = getenv("HOME");
212
+    
213
+    if (runtime_dir) {
214
+        /* Use XDG runtime directory if available */
215
+        if (safe_snprintf(gnupg_base_dir, sizeof(gnupg_base_dir), "%s/gitswitch-gpg", runtime_dir) != 0) {
216
+            set_error(ERR_INVALID_PATH, "GNUPG base directory path too long");
217
+            return -1;
218
+        }
219
+    } else if (home_dir) {
220
+        /* Fall back to home directory */
221
+        if (safe_snprintf(gnupg_base_dir, sizeof(gnupg_base_dir), "%s/.local/run/gitswitch-gpg", home_dir) != 0) {
222
+            set_error(ERR_INVALID_PATH, "GNUPG base directory path too long");
223
+            return -1;
224
+        }
225
+    } else {
226
+        /* Last resort: use /tmp */
227
+        if (safe_snprintf(gnupg_base_dir, sizeof(gnupg_base_dir), "/tmp/gitswitch-gpg-%d", getuid()) != 0) {
228
+            set_error(ERR_INVALID_PATH, "GNUPG base directory path too long");
229
+            return -1;
230
+        }
231
+    }
232
+    
233
+    /* Create base directory */
234
+    if (create_directory_recursive(gnupg_base_dir, 0700) != 0) {
235
+        set_error(ERR_FILE_IO, "Failed to create GPG base directory: %s", gnupg_base_dir);
236
+        return -1;
237
+    }
238
+    
239
+    /* Create account-specific GNUPGHOME */
240
+    if (safe_snprintf(gnupg_home, sizeof(gnupg_home), "%s/%s", gnupg_base_dir, account->name) != 0) {
241
+        set_error(ERR_INVALID_PATH, "GNUPGHOME path too long");
242
+        return -1;
243
+    }
244
+    
245
+    /* Create isolated GNUPGHOME directory */
246
+    if (create_isolated_gnupg_home_dir(gnupg_home) != 0) {
247
+        return -1;
248
+    }
249
+    
250
+    /* Set up GPG agent configuration */
251
+    if (setup_gpg_agent_config(gnupg_home) != 0) {
252
+        log_warning("Failed to set up GPG agent config: %s", get_last_error()->message);
253
+        /* Continue anyway */
254
+    }
255
+    
256
+    /* Update GPG configuration */
257
+    safe_strncpy(gpg_config->gnupg_home, gnupg_home, sizeof(gpg_config->gnupg_home));
258
+    gpg_config->home_owned = true;
259
+    
260
+    log_info("Created isolated GNUPGHOME: %s", gnupg_home);
261
+    return 0;
262
+}
263
+
264
+/* Import GPG key from file or keyserver */
265
+int gpg_import_key(gpg_config_t *gpg_config, const char *key_source) {
266
+    char command[512];
267
+    char output[1024];
268
+    int result;
269
+    
270
+    if (!gpg_config || !key_source) {
271
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_import_key");
272
+        return -1;
273
+    }
274
+    
275
+    log_debug("Importing GPG key from: %s", key_source);
276
+    
277
+    /* Check if key_source is a file or key ID */
278
+    if (path_exists(key_source)) {
279
+        /* Import from file */
280
+        if (safe_snprintf(command, sizeof(command), "gpg --import '%s'", key_source) != 0) {
281
+            set_error(ERR_INVALID_ARGS, "GPG import command too long");
282
+            return -1;
283
+        }
284
+    } else {
285
+        /* Import from keyserver */
286
+        if (safe_snprintf(command, sizeof(command), "gpg --keyserver hkps://keys.openpgp.org --recv-keys %s", 
287
+                         key_source) != 0) {
288
+            set_error(ERR_INVALID_ARGS, "GPG keyserver command too long");
289
+            return -1;
290
+        }
291
+    }
292
+    
293
+    /* Execute import command */
294
+    result = execute_gpg_command_in_env(gpg_config, command, output, sizeof(output));
295
+    if (result != 0) {
296
+        set_error(ERR_GPG_KEY_FAILED, "Failed to import GPG key: %s", output);
297
+        return -1;
298
+    }
299
+    
300
+    log_info("Successfully imported GPG key from: %s", key_source);
301
+    return 0;
302
+}
303
+
304
+/* Export GPG public key for backup/sharing */
305
+int gpg_export_public_key(gpg_config_t *gpg_config, const char *key_id, 
306
+                          char *output, size_t output_size) {
307
+    char command[256];
308
+    
309
+    if (!gpg_config || !key_id || !output || output_size == 0) {
310
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_export_public_key");
311
+        return -1;
312
+    }
313
+    
314
+    if (safe_snprintf(command, sizeof(command), "gpg --armor --export %s", key_id) != 0) {
315
+        set_error(ERR_INVALID_ARGS, "GPG export command too long");
316
+        return -1;
317
+    }
318
+    
319
+    return execute_gpg_command_in_env(gpg_config, command, output, output_size);
320
+}
321
+
322
+/* List available GPG keys */
323
+int gpg_list_keys(gpg_config_t *gpg_config, char *output, size_t output_size) {
324
+    if (!gpg_config || !output || output_size == 0) {
325
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_list_keys");
326
+        return -1;
327
+    }
328
+    
329
+    return execute_gpg_command_in_env(gpg_config, "gpg --list-keys --with-colons", output, output_size);
330
+}
331
+
332
+/* Validate GPG key exists and is usable */
333
+int gpg_validate_key(gpg_config_t *gpg_config, const char *key_id) {
334
+    char command[256];
335
+    char output[512];
336
+    int result;
337
+    
338
+    if (!gpg_config || !key_id) {
339
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_validate_key");
340
+        return -1;
341
+    }
342
+    
343
+    log_debug("Validating GPG key: %s", key_id);
344
+    
345
+    /* Check if key exists in keyring */
346
+    if (safe_snprintf(command, sizeof(command), "gpg --list-secret-keys %s", key_id) != 0) {
347
+        set_error(ERR_INVALID_ARGS, "GPG validation command too long");
348
+        return -1;
349
+    }
350
+    
351
+    result = execute_gpg_command_in_env(gpg_config, command, output, sizeof(output));
352
+    if (result != 0) {
353
+        set_error(ERR_GPG_KEY_NOT_FOUND, "GPG key not found: %s", key_id);
354
+        return -1;
355
+    }
356
+    
357
+    log_debug("GPG key validation passed: %s", key_id);
358
+    return 0;
359
+}
360
+
361
+/* Configure git GPG signing */
362
+int gpg_configure_git_signing(gpg_config_t *gpg_config, const account_t *account, git_scope_t scope) {
363
+    if (!gpg_config || !account) {
364
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_configure_git_signing");
365
+        return -1;
366
+    }
367
+    
368
+    /* Skip if GPG signing not enabled */
369
+    if (!account->gpg_signing_enabled) {
370
+        log_debug("GPG signing not enabled for account: %s", account->name);
371
+        
372
+        /* Disable git signing */
373
+        if (git_set_config_value("commit.gpgsign", "false", scope) != 0) {
374
+            log_warning("Failed to disable git GPG signing");
375
+        }
376
+        return 0;
377
+    }
378
+    
379
+    log_info("Configuring git GPG signing for account: %s", account->name);
380
+    
381
+    /* Set signing key */
382
+    if (git_set_config_value("user.signingkey", account->gpg_key_id, scope) != 0) {
383
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to set git signing key");
384
+        return -1;
385
+    }
386
+    
387
+    /* Enable GPG signing */
388
+    if (git_set_config_value("commit.gpgsign", "true", scope) != 0) {
389
+        set_error(ERR_GIT_CONFIG_FAILED, "Failed to enable git GPG signing");
390
+        return -1;
391
+    }
392
+    
393
+    /* Set GPG program if using isolated environment */
394
+    if (gpg_config->mode == GPG_MODE_ISOLATED && strlen(gpg_config->gnupg_home) > 0) {
395
+        char gpg_command[MAX_PATH_LEN + 50];
396
+        if (safe_snprintf(gpg_command, sizeof(gpg_command), "gpg --homedir '%s'", gpg_config->gnupg_home) == 0) {
397
+            if (git_set_config_value("gpg.program", gpg_command, scope) != 0) {
398
+                log_warning("Failed to set git GPG program");
399
+            }
400
+        }
401
+    }
402
+    
403
+    log_info("Git GPG signing configured successfully for account: %s", account->name);
404
+    return 0;
405
+}
406
+
407
+/* Test GPG signing by creating a test signature */
408
+int gpg_test_signing(gpg_config_t *gpg_config, const char *key_id) {
409
+    char command[512];
410
+    char output[1024];
411
+    int result;
412
+    
413
+    if (!gpg_config || !key_id) {
414
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_test_signing");
415
+        return -1;
416
+    }
417
+    
418
+    log_debug("Testing GPG signing with key: %s", key_id);
419
+    
420
+    /* Create test signature */
421
+    if (safe_snprintf(command, sizeof(command), 
422
+                     "echo 'GPG signing test' | gpg --clearsign --local-user %s", key_id) != 0) {
423
+        set_error(ERR_INVALID_ARGS, "GPG test command too long");
424
+        return -1;
425
+    }
426
+    
427
+    result = execute_gpg_command_in_env(gpg_config, command, output, sizeof(output));
428
+    if (result != 0) {
429
+        set_error(ERR_GPG_SIGNING_FAILED, "GPG signing test failed: %s", output);
430
+        return -1;
431
+    }
432
+    
433
+    /* Verify the signature contains expected content */
434
+    if (strstr(output, "BEGIN PGP SIGNED MESSAGE") == NULL) {
435
+        set_error(ERR_GPG_SIGNING_FAILED, "GPG signing test produced invalid output");
436
+        return -1;
437
+    }
438
+    
439
+    log_debug("GPG signing test passed for key: %s", key_id);
440
+    return 0;
441
+}
442
+
443
+/* Generate new GPG key for account */
444
+int gpg_generate_key(gpg_config_t *gpg_config, const account_t *account) {
445
+    char command[1024];
446
+    char key_params[512];
447
+    char output[2048];
448
+    int result;
449
+    
450
+    if (!gpg_config || !account) {
451
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_generate_key");
452
+        return -1;
453
+    }
454
+    
455
+    log_info("Generating new GPG key for account: %s", account->name);
456
+    
457
+    /* Create key generation parameters */
458
+    if (safe_snprintf(key_params, sizeof(key_params),
459
+                     "Key-Type: RSA\n"
460
+                     "Key-Length: 4096\n"
461
+                     "Subkey-Type: RSA\n"
462
+                     "Subkey-Length: 4096\n"
463
+                     "Name-Real: %s\n"
464
+                     "Name-Email: %s\n"
465
+                     "Expire-Date: 2y\n"
466
+                     "%%commit\n"
467
+                     "%%echo done\n",
468
+                     account->name, account->email) != 0) {
469
+        set_error(ERR_INVALID_ARGS, "GPG key parameters too long");
470
+        return -1;
471
+    }
472
+    
473
+    /* Generate key */
474
+    if (safe_snprintf(command, sizeof(command), "echo '%s' | gpg --batch --generate-key", key_params) != 0) {
475
+        set_error(ERR_INVALID_ARGS, "GPG generation command too long");
476
+        return -1;
477
+    }
478
+    
479
+    result = execute_gpg_command_in_env(gpg_config, command, output, sizeof(output));
480
+    if (result != 0) {
481
+        set_error(ERR_GPG_KEY_FAILED, "Failed to generate GPG key: %s", output);
482
+        return -1;
483
+    }
484
+    
485
+    log_info("Successfully generated GPG key for account: %s", account->name);
486
+    return 0;
487
+}
488
+
489
+/* Set environment variables for GPG operation */
490
+int gpg_set_environment(const gpg_config_t *gpg_config) {
491
+    if (!gpg_config) {
492
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to gpg_set_environment");
493
+        return -1;
494
+    }
495
+    
496
+    /* Set GNUPGHOME if using isolated mode */
497
+    if (gpg_config->mode == GPG_MODE_ISOLATED && strlen(gpg_config->gnupg_home) > 0) {
498
+        if (setenv("GNUPGHOME", gpg_config->gnupg_home, 1) != 0) {
499
+            set_system_error(ERR_SYSTEM_CALL, "Failed to set GNUPGHOME environment variable");
500
+            return -1;
501
+        }
502
+        
503
+        log_debug("Set GNUPGHOME environment variable: %s", gpg_config->gnupg_home);
504
+    }
505
+    
506
+    return 0;
507
+}
508
+
509
+/* Internal helper functions */
510
+
511
+/* Create isolated GNUPGHOME directory with proper permissions */
512
+static int create_isolated_gnupg_home_dir(const char *gnupg_home) {
513
+    if (!gnupg_home) {
514
+        set_error(ERR_INVALID_ARGS, "NULL gnupg_home path");
515
+        return -1;
516
+    }
517
+    
518
+    /* Create directory with 700 permissions */
519
+    if (create_directory_recursive(gnupg_home, 0700) != 0) {
520
+        set_error(ERR_FILE_IO, "Failed to create GNUPGHOME directory: %s", gnupg_home);
521
+        return -1;
522
+    }
523
+    
524
+    /* Validate permissions */
525
+    if (validate_gnupg_home_permissions(gnupg_home) != 0) {
526
+        return -1;
527
+    }
528
+    
529
+    log_debug("Created isolated GNUPGHOME directory: %s", gnupg_home);
530
+    return 0;
531
+}
532
+
533
+/* Execute GPG command in specified environment */
534
+static int execute_gpg_command_in_env(const gpg_config_t *gpg_config, 
535
+                                       const char *command, char *output, size_t output_size) {
536
+    char full_command[1024];
537
+    FILE *fp;
538
+    int status;
539
+    size_t bytes_read;
540
+    
541
+    if (!gpg_config || !command || !output || output_size == 0) {
542
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to execute_gpg_command_in_env");
543
+        return -1;
544
+    }
545
+    
546
+    /* Prepare command with GNUPGHOME if needed */
547
+    if (gpg_config->mode == GPG_MODE_ISOLATED && strlen(gpg_config->gnupg_home) > 0) {
548
+        if (safe_snprintf(full_command, sizeof(full_command), 
549
+                         "GNUPGHOME='%s' %s 2>&1", gpg_config->gnupg_home, command) != 0) {
550
+            set_error(ERR_INVALID_ARGS, "GPG command too long");
551
+            return -1;
552
+        }
553
+    } else {
554
+        if (safe_snprintf(full_command, sizeof(full_command), "%s 2>&1", command) != 0) {
555
+            set_error(ERR_INVALID_ARGS, "GPG command too long");
556
+            return -1;
557
+        }
558
+    }
559
+    
560
+    log_debug("Executing GPG command: %s", full_command);
561
+    
562
+    /* Execute command */
563
+    fp = popen(full_command, "r");
564
+    if (!fp) {
565
+        set_system_error(ERR_SYSTEM_COMMAND_FAILED, "Failed to execute GPG command");
566
+        return -1;
567
+    }
568
+    
569
+    /* Read output */
570
+    bytes_read = fread(output, 1, output_size - 1, fp);
571
+    output[bytes_read] = '\0';
572
+    
573
+    status = pclose(fp);
574
+    if (status != 0) {
575
+        log_debug("GPG command failed with status: %d, output: %s", status, output);
576
+        return -1;
577
+    }
578
+    
579
+    log_debug("GPG command completed successfully");
580
+    return 0;
581
+}
582
+
583
+/* Copy GPG key from system keyring to isolated environment */
584
+static int copy_key_from_system_keyring(const gpg_config_t *gpg_config, const char *key_id) {
585
+    char export_command[256];
586
+    char import_command[512];
587
+    char key_data[8192];
588
+    FILE *export_fp, *import_fp;
589
+    int export_status, import_status;
590
+    size_t bytes_read;
591
+    
592
+    if (!gpg_config || !key_id) {
593
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to copy_key_from_system_keyring");
594
+        return -1;
595
+    }
596
+    
597
+    log_debug("Copying GPG key from system keyring: %s", key_id);
598
+    
599
+    /* Export key from system keyring */
600
+    if (safe_snprintf(export_command, sizeof(export_command), 
601
+                     "gpg --armor --export-secret-keys %s 2>/dev/null", key_id) != 0) {
602
+        set_error(ERR_INVALID_ARGS, "Export command too long");
603
+        return -1;
604
+    }
605
+    
606
+    export_fp = popen(export_command, "r");
607
+    if (!export_fp) {
608
+        set_system_error(ERR_SYSTEM_COMMAND_FAILED, "Failed to export GPG key");
609
+        return -1;
610
+    }
611
+    
612
+    bytes_read = fread(key_data, 1, sizeof(key_data) - 1, export_fp);
613
+    key_data[bytes_read] = '\0';
614
+    
615
+    export_status = pclose(export_fp);
616
+    if (export_status != 0 || bytes_read == 0) {
617
+        set_error(ERR_GPG_KEY_NOT_FOUND, "Failed to export GPG key from system keyring");
618
+        return -1;
619
+    }
620
+    
621
+    /* Import key into isolated environment */
622
+    if (safe_snprintf(import_command, sizeof(import_command), 
623
+                     "GNUPGHOME='%s' gpg --batch --import 2>/dev/null", gpg_config->gnupg_home) != 0) {
624
+        set_error(ERR_INVALID_ARGS, "Import command too long");
625
+        return -1;
626
+    }
627
+    
628
+    import_fp = popen(import_command, "w");
629
+    if (!import_fp) {
630
+        set_system_error(ERR_SYSTEM_COMMAND_FAILED, "Failed to start GPG import");
631
+        return -1;
632
+    }
633
+    
634
+    fwrite(key_data, 1, bytes_read, import_fp);
635
+    import_status = pclose(import_fp);
636
+    
637
+    if (import_status != 0) {
638
+        set_error(ERR_GPG_KEY_FAILED, "Failed to import GPG key into isolated environment");
639
+        return -1;
640
+    }
641
+    
642
+    log_info("Successfully copied GPG key to isolated environment: %s", key_id);
643
+    return 0;
644
+}
645
+
646
+/* Validate GNUPGHOME directory permissions */
647
+static int validate_gnupg_home_permissions(const char *gnupg_home) {
648
+    mode_t dir_mode;
649
+    
650
+    if (!gnupg_home) {
651
+        set_error(ERR_INVALID_ARGS, "NULL gnupg_home path");
652
+        return -1;
653
+    }
654
+    
655
+    if (get_file_permissions(gnupg_home, &dir_mode) != 0) {
656
+        set_error(ERR_FILE_IO, "Failed to check GNUPGHOME permissions: %s", gnupg_home);
657
+        return -1;
658
+    }
659
+    
660
+    /* Check for 700 permissions */
661
+    if ((dir_mode & 0777) != 0700) {
662
+        set_error(ERR_PERMISSION_DENIED, "GNUPGHOME has insecure permissions: %o", dir_mode & 0777);
663
+        return -1;
664
+    }
665
+    
666
+    log_debug("GNUPGHOME permissions validated: %s", gnupg_home);
667
+    return 0;
668
+}
669
+
670
+/* Set up GPG agent configuration for isolated environment */
671
+static int setup_gpg_agent_config(const char *gnupg_home) {
672
+    char gpg_agent_conf_path[MAX_PATH_LEN];
673
+    FILE *conf_file;
674
+    
675
+    if (!gnupg_home) {
676
+        set_error(ERR_INVALID_ARGS, "NULL gnupg_home path");
677
+        return -1;
678
+    }
679
+    
680
+    /* Create gpg-agent.conf path */
681
+    if (safe_snprintf(gpg_agent_conf_path, sizeof(gpg_agent_conf_path), 
682
+                     "%s/gpg-agent.conf", gnupg_home) != 0) {
683
+        set_error(ERR_INVALID_PATH, "GPG agent config path too long");
684
+        return -1;
685
+    }
686
+    
687
+    /* Create basic gpg-agent.conf */
688
+    conf_file = fopen(gpg_agent_conf_path, "w");
689
+    if (!conf_file) {
690
+        set_system_error(ERR_FILE_IO, "Failed to create gpg-agent.conf");
691
+        return -1;
692
+    }
693
+    
694
+    fprintf(conf_file, "# GPG Agent configuration for gitswitch isolated environment\n");
695
+    fprintf(conf_file, "default-cache-ttl 3600\n");
696
+    fprintf(conf_file, "max-cache-ttl 7200\n");
697
+    fprintf(conf_file, "pinentry-program /usr/bin/pinentry-curses\n");
698
+    
699
+    fclose(conf_file);
700
+    
701
+    /* Set proper permissions */
702
+    if (chmod(gpg_agent_conf_path, 0600) != 0) {
703
+        set_system_error(ERR_PERMISSION_DENIED, "Failed to set gpg-agent.conf permissions");
704
+        return -1;
705
+    }
706
+    
707
+    log_debug("Created GPG agent configuration: %s", gpg_agent_conf_path);
708
+    return 0;
709
+}
src/main.cadded
@@ -0,0 +1,398 @@
1
+/* gitswitch-c: Safe git identity switching with SSH/GPG isolation
2
+ * Phase 2: Configuration Management - Full CLI with account management
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <stdlib.h>
7
+#include <string.h>
8
+#include <ctype.h>
9
+#include <getopt.h>
10
+#include <unistd.h>
11
+
12
+#include "gitswitch.h"
13
+#include "config.h"
14
+#include "accounts.h"
15
+#include "display.h"
16
+#include "error.h"
17
+#include "utils.h"
18
+
19
+static void print_usage(const char *prog_name) {
20
+    printf("Usage: %s [OPTIONS] [COMMAND] [ARGS]\n", prog_name);
21
+    printf("\nPhase 5: Complete Authentication Isolation\n");
22
+    printf("Safe git identity switching with actual git configuration management\n");
23
+    printf("\nCommands:\n");
24
+    printf("  add                  Add new account interactively\n");
25
+    printf("  list, ls             List all configured accounts\n");
26
+    printf("  remove <account>     Remove specified account\n");
27
+    printf("  status               Show current account status\n");
28
+    printf("  doctor, health       Run comprehensive health check\n");
29
+    printf("  config               Show configuration file information\n");
30
+    printf("  <account>            Switch to specified account\n");
31
+    printf("\nOptions:\n");
32
+    printf("  --global, -g         Use global git scope\n");
33
+    printf("  --local, -l          Use local git scope (default)\n");
34
+    printf("  --dry-run, -n        Show what would be done without executing\n");
35
+    printf("  --verbose, -V        Enable verbose output\n");
36
+    printf("  --debug, -d          Enable debug logging\n");
37
+    printf("  --color, -c          Force color output\n");
38
+    printf("  --no-color, -C       Disable color output\n");
39
+    printf("  --help, -h           Show this help message\n");
40
+    printf("  --version, -v        Show version information\n");
41
+    printf("\nExamples:\n");
42
+    printf("  %s add                    # Add new account interactively\n", prog_name);
43
+    printf("  %s list                   # List all accounts\n", prog_name);
44
+    printf("  %s 1                      # Switch to account ID 1\n", prog_name);
45
+    printf("  %s work                   # Switch to account matching 'work'\n", prog_name);
46
+    printf("  %s remove 2               # Remove account ID 2\n", prog_name);
47
+    printf("  %s doctor                 # Run health check\n", prog_name);
48
+    printf("\nPhase 3 Features:\n");
49
+    printf("- Secure TOML configuration management\n");
50
+    printf("- Interactive account creation with validation\n");
51
+    printf("- Comprehensive account health checking\n");
52
+    printf("- SSH/GPG key validation and security checks\n");
53
+    printf("- Atomic configuration file operations\n");
54
+    printf("- Safe file permission handling\n");
55
+    printf("- Actual git configuration switching\n");
56
+    printf("- Repository detection and scope management\n");
57
+    printf("- Git configuration validation and testing\n");
58
+}
59
+static void print_version(void) {
60
+    printf("%s version %s (Phase 5: Complete Authentication Isolation)\n", GITSWITCH_NAME, GITSWITCH_VERSION);
61
+    printf("Safe git identity switching with SSH/GPG isolation\n");
62
+    printf("Built with security and reliability in mind\n\n");
63
+    printf("Phase 2 Features [COMPLETE]:\n");
64
+    printf("- [DONE] Comprehensive error handling and logging\n");
65
+    printf("- [DONE] Security-focused utility functions\n");
66
+    printf("- [DONE] Terminal display with color support\n");
67
+    printf("- [DONE] Custom secure TOML parser\n");
68
+    printf("- [DONE] Configuration management with validation\n");
69
+    printf("- [DONE] Interactive account creation and management\n");
70
+    printf("- [DONE] SSH/GPG key validation and security checks\n");
71
+    printf("- [DONE] Comprehensive health checking system\n");
72
+    printf("- [DONE] Atomic file operations with backups\n");
73
+    printf("\nPhase 3 Features [COMPLETE]:\n");
74
+    printf("- [DONE] Git operations and configuration management\n");
75
+    printf("- [DONE] Repository detection and scope handling\n");
76
+    printf("- [DONE] Git configuration testing and validation\n");
77
+    printf("- [DONE] Actual account switching with git integration\n");
78
+    printf("- [DONE] Local vs global scope automatic detection\n");
79
+    printf("- [DONE] Git configuration validation and verification\n");
80
+    printf("\nPhase 4 Features [COMPLETE]:\n");
81
+    printf("- [DONE] Isolated SSH agents per account\n");
82
+    printf("- [DONE] SSH agent lifecycle management\n");
83
+    printf("- [DONE] SSH key loading and validation\n");
84
+    printf("- [DONE] SSH connection testing and isolation\n");
85
+    printf("- [DONE] Per-account SSH environment isolation\n");
86
+    printf("- [DONE] Secure SSH agent socket management\n");
87
+    printf("\nPhase 5 Features [COMPLETE]:\n");
88
+    printf("- [DONE] Isolated GPG environments with GNUPGHOME per account\n");
89
+    printf("- [DONE] Complete GPG signing and key management\n");
90
+    printf("- [DONE] GPG key import/export and validation\n");
91
+    printf("- [DONE] Git GPG signing configuration\n");
92
+    printf("- [DONE] GPG environment isolation and cleanup\n");
93
+    printf("- [DONE] Production-ready authentication isolation\n");
94
+    printf("\n[COMPLETE]: All phases implemented!\n");
95
+    printf("gitswitch-c provides complete SSH/GPG authentication isolation\n");
96
+}
97
+static int handle_add_command(gitswitch_ctx_t *ctx);
98
+static int handle_list_command(gitswitch_ctx_t *ctx);
99
+static int handle_remove_command(gitswitch_ctx_t *ctx, const char *identifier);
100
+static int handle_status_command(gitswitch_ctx_t *ctx);
101
+static int handle_switch_command(gitswitch_ctx_t *ctx, const char *identifier);
102
+static int handle_doctor_command(gitswitch_ctx_t *ctx);
103
+static int handle_config_command(gitswitch_ctx_t *ctx);
104
+
105
+int main(int argc, char *argv[]) {
106
+    gitswitch_ctx_t ctx;
107
+    int opt;
108
+    bool force_color = false;
109
+    bool no_color = false;
110
+    bool show_help = false;
111
+    bool show_version = false;
112
+    bool dry_run = false;
113
+    int exit_code = EXIT_SUCCESS;
114
+    
115
+    static struct option long_options[] = {
116
+        {"help", no_argument, 0, 'h'},
117
+        {"version", no_argument, 0, 'v'},
118
+        {"color", no_argument, 0, 'c'},
119
+        {"no-color", no_argument, 0, 'C'},
120
+        {"verbose", no_argument, 0, 'V'},
121
+        {"debug", no_argument, 0, 'd'},
122
+        {"dry-run", no_argument, 0, 'n'},
123
+        {"global", no_argument, 0, 'g'},
124
+        {"local", no_argument, 0, 'l'},
125
+        {0, 0, 0, 0}
126
+    };
127
+    
128
+    /* Initialize error handling */
129
+    if (error_init(LOG_LEVEL_INFO, NULL) != 0) {
130
+        fprintf(stderr, "Failed to initialize error handling\n");
131
+        return EXIT_FAILURE;
132
+    }
133
+    
134
+    /* Parse command line options */
135
+    while ((opt = getopt_long(argc, argv, "hvccVdngl", long_options, NULL)) != -1) {
136
+        switch (opt) {
137
+            case 'h':
138
+                show_help = true;
139
+                break;
140
+            case 'v':
141
+                show_version = true;
142
+                break;
143
+            case 'c':
144
+                force_color = true;
145
+                break;
146
+            case 'C':
147
+                no_color = true;
148
+                break;
149
+            case 'V':
150
+            case 'd':
151
+                set_log_level(LOG_LEVEL_DEBUG);
152
+                break;
153
+            case 'n':
154
+                dry_run = true;
155
+                break;
156
+            case 'g':
157
+                /* Global scope - will be handled by command handlers */
158
+                break;
159
+            case 'l':
160
+                /* Local scope - will be handled by command handlers */
161
+                break;
162
+            default:
163
+                print_usage(argv[0]);
164
+                error_cleanup();
165
+                return EXIT_FAILURE;
166
+        }
167
+    }
168
+    
169
+    /* Initialize display system */
170
+    if (display_init(force_color, no_color) != 0) {
171
+        log_error("Failed to initialize display system");
172
+        error_cleanup();
173
+        return EXIT_FAILURE;
174
+    }
175
+    
176
+    /* Handle special commands that don't need config */
177
+    if (show_version) {
178
+        print_version();
179
+        error_cleanup();
180
+        return EXIT_SUCCESS;
181
+    }
182
+    
183
+    if (show_help) {
184
+        print_usage(argv[0]);
185
+        error_cleanup();
186
+        return EXIT_SUCCESS;
187
+    }
188
+    
189
+    /* Initialize configuration system */
190
+    log_info("Initializing gitswitch-c configuration system");
191
+    if (config_init(&ctx) != 0) {
192
+        display_error("Configuration initialization failed", get_last_error()->message);
193
+        error_cleanup();
194
+        return EXIT_CONFIG_ERROR;
195
+    }
196
+    
197
+    /* Set dry run mode if requested */
198
+    ctx.config.dry_run = dry_run;
199
+    ctx.config.verbose = (get_last_error() != NULL && should_log(LOG_LEVEL_DEBUG));
200
+    
201
+    /* Parse command and arguments */
202
+    const char *command = NULL;
203
+    const char *arg1 = NULL;
204
+    
205
+    if (optind < argc) {
206
+        command = argv[optind];
207
+        if (optind + 1 < argc) {
208
+            arg1 = argv[optind + 1];
209
+        }
210
+    }
211
+    
212
+    /* Execute command */
213
+    if (command == NULL) {
214
+        /* No command specified - interactive mode or help */
215
+        if (ctx.account_count == 0) {
216
+            display_header("Welcome to gitswitch-c");
217
+            display_warning("No accounts configured yet");
218
+            printf("\nTo get started:\n");
219
+            printf("  1. Run 'gitswitch add' to create your first account\n");
220
+            printf("  2. Run 'gitswitch list' to see all accounts\n");
221
+            printf("  3. Run 'gitswitch <account>' to switch accounts\n");
222
+            printf("  4. Run 'gitswitch --help' for more options\n\n");
223
+        } else {
224
+            /* Show account list */
225
+            exit_code = handle_list_command(&ctx);
226
+        }
227
+    } else if (strcmp(command, "add") == 0) {
228
+        exit_code = handle_add_command(&ctx);
229
+    } else if (strcmp(command, "list") == 0 || strcmp(command, "ls") == 0) {
230
+        exit_code = handle_list_command(&ctx);
231
+    } else if (strcmp(command, "remove") == 0 || strcmp(command, "rm") == 0 || strcmp(command, "delete") == 0) {
232
+        if (!arg1) {
233
+            display_error("Missing account identifier", "Usage: gitswitch remove <account>");
234
+            exit_code = EXIT_FAILURE;
235
+        } else {
236
+            exit_code = handle_remove_command(&ctx, arg1);
237
+        }
238
+    } else if (strcmp(command, "status") == 0) {
239
+        exit_code = handle_status_command(&ctx);
240
+    } else if (strcmp(command, "doctor") == 0 || strcmp(command, "health") == 0) {
241
+        exit_code = handle_doctor_command(&ctx);
242
+    } else if (strcmp(command, "config") == 0) {
243
+        exit_code = handle_config_command(&ctx);
244
+    } else {
245
+        /* Assume it's an account identifier for switching */
246
+        exit_code = handle_switch_command(&ctx, command);
247
+    }
248
+    
249
+    /* Save configuration if it was modified and no errors occurred */
250
+    if (exit_code == EXIT_SUCCESS && !dry_run) {
251
+        if (config_save(&ctx, ctx.config.config_path) != 0) {
252
+            display_warning("Failed to save configuration changes");
253
+            /* Don't fail the command, just warn */
254
+        }
255
+    }
256
+    
257
+    /* Cleanup */
258
+    error_cleanup();
259
+    return exit_code == EXIT_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE;
260
+}
261
+
262
+/* Command handler implementations */
263
+
264
+static int handle_add_command(gitswitch_ctx_t *ctx) {
265
+    if (!ctx) return EXIT_FAILURE;
266
+    
267
+    if (accounts_add_interactive(ctx) != 0) {
268
+        display_error("Failed to add account", get_last_error()->message);
269
+        return EXIT_FAILURE;
270
+    }
271
+    
272
+    return EXIT_SUCCESS;
273
+}
274
+
275
+static int handle_list_command(gitswitch_ctx_t *ctx) {
276
+    if (!ctx) return EXIT_FAILURE;
277
+    
278
+    return accounts_list(ctx) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
279
+}
280
+
281
+static int handle_remove_command(gitswitch_ctx_t *ctx, const char *identifier) {
282
+    if (!ctx || !identifier) return EXIT_FAILURE;
283
+    
284
+    if (accounts_remove(ctx, identifier) != 0) {
285
+        display_error("Failed to remove account", get_last_error()->message);
286
+        return EXIT_FAILURE;
287
+    }
288
+    
289
+    return EXIT_SUCCESS;
290
+}
291
+
292
+static int handle_status_command(gitswitch_ctx_t *ctx) {
293
+    if (!ctx) return EXIT_FAILURE;
294
+    
295
+    return accounts_show_status(ctx) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
296
+}
297
+
298
+static int handle_switch_command(gitswitch_ctx_t *ctx, const char *identifier) {
299
+    if (!ctx || !identifier) return EXIT_FAILURE;
300
+    
301
+    if (ctx->config.dry_run) {
302
+        display_info("DRY RUN MODE - No actual changes will be made");
303
+    }
304
+    
305
+    if (accounts_switch(ctx, identifier) != 0) {
306
+        display_error("Failed to switch account", get_last_error()->message);
307
+        return EXIT_FAILURE;
308
+    }
309
+    
310
+    display_success("Successfully switched to account: %s", ctx->current_account->name);
311
+    
312
+    return EXIT_SUCCESS;
313
+}
314
+
315
+static int handle_doctor_command(gitswitch_ctx_t *ctx) {
316
+    if (!ctx) return EXIT_FAILURE;
317
+    
318
+    /* Check system requirements */
319
+    printf("[INFO]: Checking system requirements...\n");
320
+    
321
+    if (command_exists("git")) {
322
+        display_success("Git command found");
323
+    } else {
324
+        display_error("Git not found", "Please install git to use gitswitch");
325
+        return EXIT_FAILURE;
326
+    }
327
+    
328
+    if (command_exists("ssh-agent")) {
329
+        display_success("SSH agent found");
330
+    } else {
331
+        display_warning("SSH agent not found - SSH key management may not work");
332
+    }
333
+    
334
+    if (command_exists("gpg") || command_exists("gpg2")) {
335
+        display_success("GPG found");
336
+    } else {
337
+        display_warning("GPG not found - GPG signing will not work");
338
+    }
339
+    
340
+    /* Check configuration */
341
+    printf("\n[INFO]: Checking configuration...\n");
342
+    
343
+    if (config_validate(ctx) == 0) {
344
+        display_success("Configuration validation passed");
345
+    } else {
346
+        display_error("Configuration validation failed", get_last_error()->message);
347
+        return EXIT_FAILURE;
348
+    }
349
+    
350
+    /* Check all accounts */
351
+    return accounts_health_check(ctx) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
352
+}
353
+
354
+static int handle_config_command(gitswitch_ctx_t *ctx) {
355
+    if (!ctx) return EXIT_FAILURE;
356
+    
357
+    printf("📁 Configuration file: %s\n", ctx->config.config_path);
358
+    
359
+    if (!path_exists(ctx->config.config_path)) {
360
+        display_warning("Configuration file does not exist");
361
+        printf("Create default configuration? (y/N): ");
362
+        fflush(stdout);
363
+        
364
+        char input[64];
365
+        if (fgets(input, sizeof(input), stdin)) {
366
+            input[strcspn(input, "\n")] = '\0';
367
+            trim_whitespace(input);
368
+            
369
+            if (tolower(input[0]) == 'y') {
370
+                if (config_create_default(ctx->config.config_path) == 0) {
371
+                    display_success("Default configuration created");
372
+                    printf("Please edit the file to add your accounts.\n");
373
+                } else {
374
+                    display_error("Failed to create default configuration", get_last_error()->message);
375
+                    return EXIT_FAILURE;
376
+                }
377
+            }
378
+        }
379
+        return EXIT_SUCCESS;
380
+    }
381
+    
382
+    /* Show configuration info */
383
+    printf("Accounts: %zu configured\n", ctx->account_count);
384
+    printf("Default scope: %s\n", config_scope_to_string(ctx->config.default_scope));
385
+    
386
+    /* Check permissions */
387
+    mode_t file_mode;
388
+    if (get_file_permissions(ctx->config.config_path, &file_mode) == 0) {
389
+        if ((file_mode & 077) == 0) {
390
+            display_success("Configuration file permissions are secure (600)");
391
+        } else {
392
+            display_warning("Configuration file has unsafe permissions (%o)", file_mode & 0777);
393
+        }
394
+    }
395
+    
396
+    return EXIT_SUCCESS;
397
+}
398
+
src/main_simple.cadded
@@ -0,0 +1,132 @@
1
+/* gitswitch-c: Safe git identity switching with SSH/GPG isolation
2
+ * Simplified Phase 1 implementation for foundation testing
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <stdlib.h>
7
+#include <string.h>
8
+#include <getopt.h>
9
+#include <unistd.h>
10
+
11
+#include "gitswitch.h"
12
+#include "error.h"
13
+
14
+static void print_usage(const char *prog_name);
15
+static void print_version(void);
16
+
17
+int main(int argc, char *argv[]) {
18
+    int opt;
19
+    bool show_help = false;
20
+    bool show_version = false;
21
+    bool verbose = false;
22
+    
23
+    static struct option long_options[] = {
24
+        {"help", no_argument, 0, 'h'},
25
+        {"version", no_argument, 0, 'v'},
26
+        {"verbose", no_argument, 0, 'V'},
27
+        {0, 0, 0, 0}
28
+    };
29
+    
30
+    /* Initialize error handling */
31
+    if (error_init(LOG_LEVEL_INFO, NULL) != 0) {
32
+        fprintf(stderr, "Failed to initialize error handling\n");
33
+        return EXIT_FAILURE;
34
+    }
35
+    
36
+    /* Parse command line options */
37
+    while ((opt = getopt_long(argc, argv, "hvV", long_options, NULL)) != -1) {
38
+        switch (opt) {
39
+            case 'h':
40
+                show_help = true;
41
+                break;
42
+            case 'v':
43
+                show_version = true;
44
+                break;
45
+            case 'V':
46
+                verbose = true;
47
+                set_log_level(LOG_LEVEL_DEBUG);
48
+                break;
49
+            default:
50
+                print_usage(argv[0]);
51
+                error_cleanup();
52
+                return EXIT_FAILURE;
53
+        }
54
+    }
55
+    
56
+    /* Handle special commands */
57
+    if (show_version) {
58
+        print_version();
59
+        error_cleanup();
60
+        return EXIT_SUCCESS;
61
+    }
62
+    
63
+    if (show_help) {
64
+        print_usage(argv[0]);
65
+        error_cleanup();
66
+        return EXIT_SUCCESS;
67
+    }
68
+    
69
+    /* Phase 1 demonstration */
70
+    printf("┌──────────────────────────────────────┐\n");
71
+    printf("│   gitswitch-c Phase 1 Foundation    │\n");
72
+    printf("└──────────────────────────────────────┘\n\n");
73
+    
74
+    printf("✓ Error handling system: initialized\n");
75
+    printf("✓ Build system: working\n");
76
+    printf("✓ Command line parsing: functional\n");
77
+    
78
+    if (verbose) {
79
+        log_info("Verbose mode enabled");
80
+        log_debug("Debug logging active");
81
+    }
82
+    
83
+    /* Test error handling */
84
+    printf("\nTesting error handling:\n");
85
+    set_error(ERR_SUCCESS, "This is a test success message");
86
+    const error_context_t *err = get_last_error();
87
+    if (err->code == ERR_SUCCESS) {
88
+        printf("✓ Error context system working\n");
89
+    }
90
+    
91
+    printf("\nPhase 1 foundation is solid! ✨\n");
92
+    printf("Ready for Phase 2: Configuration management\n\n");
93
+    
94
+    /* Cleanup */
95
+    error_cleanup();
96
+    return EXIT_SUCCESS;
97
+}
98
+
99
+static void print_usage(const char *prog_name) {
100
+    printf("Usage: %s [OPTIONS] [COMMAND] [ARGS]\n", prog_name);
101
+    printf("\nThis is a Phase 1 development build of gitswitch-c\n");
102
+    printf("Safe git identity switching with SSH/GPG isolation\n\n");
103
+    printf("Options:\n");
104
+    printf("  --help, -h           Show this help message\n");
105
+    printf("  --version, -v        Show version information\n");
106
+    printf("  --verbose, -V        Enable verbose output\n\n");
107
+    printf("Phase 1 Status:\n");
108
+    printf("  ✓ Error handling system\n");
109
+    printf("  ✓ Build system\n");
110
+    printf("  ✓ Command line parsing\n");
111
+    printf("  • Configuration management (Phase 2)\n");
112
+    printf("  • Git operations (Phase 3)\n");
113
+    printf("  • SSH security framework (Phase 4)\n");
114
+    printf("  • GPG security framework (Phase 5)\n");
115
+    printf("  • Full CLI integration (Phase 6)\n");
116
+}
117
+
118
+static void print_version(void) {
119
+    printf("%s version %s (Phase 1 Development Build)\n", GITSWITCH_NAME, GITSWITCH_VERSION);
120
+    printf("Safe git identity switching with SSH/GPG isolation\n");
121
+    printf("Built with security and reliability in mind\n\n");
122
+    printf("Phase 1 Features:\n");
123
+    printf("• Comprehensive error handling and logging\n");
124
+    printf("• Security-focused utility functions\n");
125
+    printf("• Foundation for SSH/GPG isolation\n");
126
+    printf("• Robust build system with hardening\n\n");
127
+    printf("Upcoming Features:\n");
128
+    printf("• Isolated SSH agents per account\n");
129
+    printf("• Separate GPG environments\n");
130
+    printf("• Comprehensive validation\n");
131
+    printf("• Secure memory handling\n");
132
+}
src/ssh_manager.cadded
@@ -0,0 +1,829 @@
1
+/* SSH key and agent management with comprehensive isolation and security
2
+ * Implements per-account SSH agents to prevent key leakage between accounts
3
+ */
4
+
5
+#define _POSIX_C_SOURCE 200809L
6
+#define _DEFAULT_SOURCE
7
+#include <stdio.h>
8
+#include <stdlib.h>
9
+#include <string.h>
10
+#include <unistd.h>
11
+#include <sys/wait.h>
12
+#include <sys/stat.h>
13
+#include <signal.h>
14
+#include <errno.h>
15
+#include <fcntl.h>
16
+
17
+#include "ssh_manager.h"
18
+#include "error.h"
19
+#include "utils.h"
20
+#include "display.h"
21
+
22
+/* Internal helper functions */
23
+static int execute_ssh_command(const char *command, char *output, size_t output_size);
24
+static int setup_ssh_environment(ssh_config_t *ssh_config);
25
+static int create_isolated_agent_socket_dir(char *socket_dir, size_t socket_dir_size);
26
+static bool is_ssh_agent_running(pid_t pid);
27
+static int kill_ssh_agent_gracefully(pid_t pid);
28
+static int validate_ssh_agent_socket(const char *socket_path);
29
+static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config);
30
+
31
+/* Global SSH configuration for cleanup */
32
+static ssh_config_t *g_active_ssh_config = NULL;
33
+
34
+/* Initialize SSH manager */
35
+int ssh_manager_init(ssh_config_t *ssh_config, ssh_agent_mode_t mode) {
36
+    if (!ssh_config) {
37
+        set_error(ERR_INVALID_ARGS, "NULL ssh_config to ssh_manager_init");
38
+        return -1;
39
+    }
40
+    
41
+    log_debug("Initializing SSH manager with mode: %d", mode);
42
+    
43
+    /* Initialize structure */
44
+    memset(ssh_config, 0, sizeof(ssh_config_t));
45
+    ssh_config->mode = mode;
46
+    ssh_config->agent_pid = -1;
47
+    ssh_config->agent_owned = false;
48
+    
49
+    /* Validate SSH is available */
50
+    if (!command_exists("ssh")) {
51
+        set_error(ERR_SSH_NOT_FOUND, "SSH command not found in PATH");
52
+        return -1;
53
+    }
54
+    
55
+    if (!command_exists("ssh-agent")) {
56
+        set_error(ERR_SSH_AGENT_NOT_FOUND, "ssh-agent command not found in PATH");
57
+        return -1;
58
+    }
59
+    
60
+    if (!command_exists("ssh-add")) {
61
+        set_error(ERR_SSH_AGENT_NOT_FOUND, "ssh-add command not found in PATH");
62
+        return -1;
63
+    }
64
+    
65
+    /* Set up based on mode */
66
+    switch (mode) {
67
+        case SSH_AGENT_SYSTEM:
68
+            /* Use existing SSH_AUTH_SOCK */
69
+            if (getenv("SSH_AUTH_SOCK")) {
70
+                safe_strncpy(ssh_config->agent_socket_path, getenv("SSH_AUTH_SOCK"),
71
+                           sizeof(ssh_config->agent_socket_path));
72
+                log_info("Using system SSH agent at: %s", ssh_config->agent_socket_path);
73
+            } else {
74
+                log_warning("No system SSH agent found (SSH_AUTH_SOCK not set)");
75
+            }
76
+            break;
77
+            
78
+        case SSH_AGENT_ISOLATED:
79
+            /* Will create isolated agents on demand */
80
+            log_info("Initialized for isolated SSH agent mode");
81
+            break;
82
+            
83
+        case SSH_AGENT_NONE:
84
+            log_info("SSH agent management disabled");
85
+            break;
86
+            
87
+        default:
88
+            set_error(ERR_INVALID_ARGS, "Invalid SSH agent mode: %d", mode);
89
+            return -1;
90
+    }
91
+    
92
+    /* Register for cleanup */
93
+    g_active_ssh_config = ssh_config;
94
+    
95
+    log_info("SSH manager initialized successfully");
96
+    return 0;
97
+}
98
+
99
+/* Cleanup SSH manager */
100
+void ssh_manager_cleanup(ssh_config_t *ssh_config) {
101
+    if (!ssh_config) {
102
+        return;
103
+    }
104
+    
105
+    log_debug("Cleaning up SSH manager");
106
+    
107
+    /* Stop agent if we own it */
108
+    if (ssh_config->agent_owned && ssh_config->agent_pid > 0) {
109
+        log_info("Stopping owned SSH agent (PID: %d)", ssh_config->agent_pid);
110
+        ssh_stop_agent(ssh_config);
111
+    }
112
+    
113
+    /* Clear global reference */
114
+    if (g_active_ssh_config == ssh_config) {
115
+        g_active_ssh_config = NULL;
116
+    }
117
+    
118
+    /* Clear sensitive data */
119
+    secure_zero_memory(ssh_config, sizeof(ssh_config_t));
120
+    
121
+    log_debug("SSH manager cleanup complete");
122
+}
123
+
124
+/* Switch to account's SSH configuration */
125
+int ssh_switch_account(ssh_config_t *ssh_config, const account_t *account) {
126
+    char expanded_key_path[MAX_PATH_LEN];
127
+    
128
+    if (!ssh_config || !account) {
129
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_switch_account");
130
+        return -1;
131
+    }
132
+    
133
+    /* Skip if SSH not enabled for this account */
134
+    if (!account->ssh_enabled || strlen(account->ssh_key_path) == 0) {
135
+        log_debug("SSH not enabled for account: %s", account->name);
136
+        return 0;
137
+    }
138
+    
139
+    log_info("Switching SSH configuration for account: %s", account->name);
140
+    
141
+    /* Validate and expand key path */
142
+    if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
143
+        set_error(ERR_INVALID_PATH, "Failed to expand SSH key path: %s", account->ssh_key_path);
144
+        return -1;
145
+    }
146
+    
147
+    /* Validate key file */
148
+    if (ssh_validate_key_file(expanded_key_path) != 0) {
149
+        return -1; /* Error already set */
150
+    }
151
+    
152
+    /* Handle based on mode */
153
+    switch (ssh_config->mode) {
154
+        case SSH_AGENT_SYSTEM:
155
+            /* Clear existing keys and add new one to system agent */
156
+            if (strlen(ssh_config->agent_socket_path) > 0) {
157
+                log_debug("Clearing system SSH agent keys");
158
+                ssh_clear_agent_keys(ssh_config);
159
+                
160
+                log_debug("Adding key to system SSH agent: %s", expanded_key_path);
161
+                if (ssh_add_key(ssh_config, expanded_key_path) != 0) {
162
+                    set_error(ERR_SSH_KEY_LOAD_FAILED, "Failed to load key into system SSH agent");
163
+                    return -1;
164
+                }
165
+            } else {
166
+                log_warning("No system SSH agent available");
167
+            }
168
+            break;
169
+            
170
+        case SSH_AGENT_ISOLATED:
171
+            /* Start isolated agent for this account */
172
+            if (ssh_start_isolated_agent(ssh_config, account) != 0) {
173
+                return -1; /* Error already set */
174
+            }
175
+            
176
+            /* Add key to isolated agent */
177
+            if (ssh_add_key(ssh_config, expanded_key_path) != 0) {
178
+                set_error(ERR_SSH_KEY_LOAD_FAILED, "Failed to load key into isolated SSH agent");
179
+                return -1;
180
+            }
181
+            break;
182
+            
183
+        case SSH_AGENT_NONE:
184
+            /* No agent management - just validate key */
185
+            log_info("SSH agent management disabled - key validated but not loaded");
186
+            break;
187
+            
188
+        default:
189
+            set_error(ERR_INVALID_ARGS, "Invalid SSH agent mode");
190
+            return -1;
191
+    }
192
+    
193
+    /* Configure host alias if specified */
194
+    if (strlen(account->ssh_host_alias) > 0) {
195
+        if (ssh_configure_host_alias(account) != 0) {
196
+            log_warning("Failed to configure SSH host alias: %s", account->ssh_host_alias);
197
+            /* Don't fail completely for host alias issues */
198
+        }
199
+    }
200
+    
201
+    log_info("SSH configuration switched successfully for account: %s", account->name);
202
+    return 0;
203
+}
204
+
205
+/* Start isolated SSH agent */
206
+int ssh_start_isolated_agent(ssh_config_t *ssh_config, const account_t *account) {
207
+    char command[512];
208
+    char output[1024];
209
+    char socket_dir[MAX_PATH_LEN];
210
+    
211
+    if (!ssh_config || !account) {
212
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_start_isolated_agent");
213
+        return -1;
214
+    }
215
+    
216
+    log_info("Starting isolated SSH agent for account: %s", account->name);
217
+    
218
+    /* Stop any existing agent we own */
219
+    if (ssh_config->agent_owned && ssh_config->agent_pid > 0) {
220
+        log_debug("Stopping existing SSH agent");
221
+        ssh_stop_agent(ssh_config);
222
+    }
223
+    
224
+    /* Create secure socket directory */
225
+    if (create_isolated_agent_socket_dir(socket_dir, sizeof(socket_dir)) != 0) {
226
+        return -1;
227
+    }
228
+    
229
+    /* Build ssh-agent command with socket path */
230
+    if (snprintf(command, sizeof(command), 
231
+                 "ssh-agent -a '%s/ssh-agent.%s.sock'", 
232
+                 socket_dir, account->name) >= sizeof(command)) {
233
+        set_error(ERR_INVALID_ARGS, "SSH agent command too long");
234
+        return -1;
235
+    }
236
+    
237
+    log_debug("Starting SSH agent: %s", command);
238
+    
239
+    /* Execute ssh-agent */
240
+    if (execute_ssh_command(command, output, sizeof(output)) != 0) {
241
+        set_error(ERR_SSH_AGENT_START_FAILED, "Failed to start SSH agent");
242
+        return -1;
243
+    }
244
+    
245
+    /* Parse ssh-agent output to get socket and PID */
246
+    if (parse_ssh_agent_output(output, ssh_config) != 0) {
247
+        set_error(ERR_SSH_AGENT_START_FAILED, "Failed to parse ssh-agent output");
248
+        return -1;
249
+    }
250
+    
251
+    /* Validate the agent is working */
252
+    if (validate_ssh_agent_socket(ssh_config->agent_socket_path) != 0) {
253
+        set_error(ERR_SSH_AGENT_START_FAILED, "SSH agent socket validation failed");
254
+        return -1;
255
+    }
256
+    
257
+    /* Mark as owned */
258
+    ssh_config->agent_owned = true;
259
+    
260
+    /* Set up environment */
261
+    if (setup_ssh_environment(ssh_config) != 0) {
262
+        set_error(ERR_SSH_AGENT_START_FAILED, "Failed to set up SSH environment");
263
+        return -1;
264
+    }
265
+    
266
+    log_info("Isolated SSH agent started successfully (PID: %d, Socket: %s)", 
267
+             ssh_config->agent_pid, ssh_config->agent_socket_path);
268
+    return 0;
269
+}
270
+
271
+/* Stop SSH agent */
272
+int ssh_stop_agent(ssh_config_t *ssh_config) {
273
+    if (!ssh_config || ssh_config->agent_pid <= 0) {
274
+        return 0; /* Nothing to stop */
275
+    }
276
+    
277
+    if (!ssh_config->agent_owned) {
278
+        log_debug("Not stopping SSH agent - we don't own it");
279
+        return 0;
280
+    }
281
+    
282
+    log_info("Stopping SSH agent (PID: %d)", ssh_config->agent_pid);
283
+    
284
+    /* Try graceful shutdown first */
285
+    if (kill_ssh_agent_gracefully(ssh_config->agent_pid) == 0) {
286
+        log_debug("SSH agent stopped gracefully");
287
+    } else {
288
+        log_warning("Failed to stop SSH agent gracefully");
289
+    }
290
+    
291
+    /* Clean up socket file */
292
+    if (strlen(ssh_config->agent_socket_path) > 0) {
293
+        if (unlink(ssh_config->agent_socket_path) == 0) {
294
+            log_debug("Removed SSH agent socket: %s", ssh_config->agent_socket_path);
295
+        } else {
296
+            log_debug("Could not remove SSH agent socket (may already be gone)");
297
+        }
298
+    }
299
+    
300
+    /* Reset state */
301
+    ssh_config->agent_pid = -1;
302
+    ssh_config->agent_owned = false;
303
+    ssh_config->agent_socket_path[0] = '\0';
304
+    
305
+    /* Clear environment */
306
+    unsetenv("SSH_AUTH_SOCK");
307
+    unsetenv("SSH_AGENT_PID");
308
+    
309
+    return 0;
310
+}
311
+
312
+/* Clear all keys from SSH agent */
313
+int ssh_clear_agent_keys(ssh_config_t *ssh_config) {
314
+    char output[512];
315
+    
316
+    if (!ssh_config || strlen(ssh_config->agent_socket_path) == 0) {
317
+        log_debug("No SSH agent available to clear keys");
318
+        return 0;
319
+    }
320
+    
321
+    log_debug("Clearing all keys from SSH agent");
322
+    
323
+    /* Set up environment for ssh-add */
324
+    if (setup_ssh_environment(ssh_config) != 0) {
325
+        return -1;
326
+    }
327
+    
328
+    /* Execute ssh-add -D to delete all keys */
329
+    if (execute_ssh_command("ssh-add -D", output, sizeof(output)) != 0) {
330
+        log_warning("Failed to clear SSH agent keys (agent may be empty)");
331
+        /* This is not necessarily an error - agent might be empty */
332
+    } else {
333
+        log_debug("SSH agent keys cleared successfully");
334
+    }
335
+    
336
+    return 0;
337
+}
338
+
339
+/* Add key to SSH agent */
340
+int ssh_add_key(ssh_config_t *ssh_config, const char *key_path) {
341
+    char command[MAX_PATH_LEN + 32];
342
+    char output[512];
343
+    
344
+    if (!ssh_config || !key_path) {
345
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_add_key");
346
+        return -1;
347
+    }
348
+    
349
+    if (strlen(ssh_config->agent_socket_path) == 0) {
350
+        set_error(ERR_SSH_AGENT_NOT_FOUND, "No SSH agent available");
351
+        return -1;
352
+    }
353
+    
354
+    log_debug("Adding SSH key to agent: %s", key_path);
355
+    
356
+    /* Set up environment */
357
+    if (setup_ssh_environment(ssh_config) != 0) {
358
+        return -1;
359
+    }
360
+    
361
+    /* Build ssh-add command */
362
+    if (snprintf(command, sizeof(command), "ssh-add '%s'", key_path) >= sizeof(command)) {
363
+        set_error(ERR_INVALID_ARGS, "SSH add command too long");
364
+        return -1;
365
+    }
366
+    
367
+    /* Execute ssh-add */
368
+    if (execute_ssh_command(command, output, sizeof(output)) != 0) {
369
+        set_error(ERR_SSH_KEY_LOAD_FAILED, "Failed to add SSH key: %s", output);
370
+        return -1;
371
+    }
372
+    
373
+    log_info("SSH key added successfully: %s", key_path);
374
+    return 0;
375
+}
376
+
377
+/* List loaded SSH keys */
378
+int ssh_list_keys(ssh_config_t *ssh_config, char *output, size_t output_size) {
379
+    if (!ssh_config || !output || output_size == 0) {
380
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_list_keys");
381
+        return -1;
382
+    }
383
+    
384
+    if (strlen(ssh_config->agent_socket_path) == 0) {
385
+        safe_strncpy(output, "No SSH agent available", output_size);
386
+        return -1;
387
+    }
388
+    
389
+    /* Set up environment */
390
+    if (setup_ssh_environment(ssh_config) != 0) {
391
+        return -1;
392
+    }
393
+    
394
+    /* Execute ssh-add -l */
395
+    if (execute_ssh_command("ssh-add -l", output, output_size) != 0) {
396
+        safe_strncpy(output, "No keys loaded in SSH agent", output_size);
397
+        return -1;
398
+    }
399
+    
400
+    return 0;
401
+}
402
+
403
+/* Validate SSH key file */
404
+int ssh_validate_key_file(const char *key_path) {
405
+    struct stat key_stat;
406
+    mode_t key_mode;
407
+    
408
+    if (!key_path) {
409
+        set_error(ERR_INVALID_ARGS, "NULL key_path to ssh_validate_key_file");
410
+        return -1;
411
+    }
412
+    
413
+    /* Check if file exists */
414
+    if (stat(key_path, &key_stat) != 0) {
415
+        set_system_error(ERR_SSH_KEY_NOT_FOUND, "SSH key file not found: %s", key_path);
416
+        return -1;
417
+    }
418
+    
419
+    /* Check if it's a regular file */
420
+    if (!S_ISREG(key_stat.st_mode)) {
421
+        set_error(ERR_SSH_KEY_INVALID, "SSH key path is not a regular file: %s", key_path);
422
+        return -1;
423
+    }
424
+    
425
+    /* Check permissions - should be 600 (readable only by owner) */
426
+    key_mode = key_stat.st_mode & 0777;
427
+    if (key_mode != 0600) {
428
+        set_error(ERR_SSH_KEY_PERMISSIONS, 
429
+                  "SSH key file has unsafe permissions: %o (should be 600): %s",
430
+                  key_mode, key_path);
431
+        return -1;
432
+    }
433
+    
434
+    /* Check ownership - should be owned by current user */
435
+    if (key_stat.st_uid != getuid()) {
436
+        set_error(ERR_SSH_KEY_OWNERSHIP, "SSH key file not owned by current user: %s", key_path);
437
+        return -1;
438
+    }
439
+    
440
+    /* Basic content validation - check it looks like a private key */
441
+    FILE *key_file = fopen(key_path, "r");
442
+    if (!key_file) {
443
+        set_system_error(ERR_SSH_KEY_INVALID, "Cannot read SSH key file: %s", key_path);
444
+        return -1;
445
+    }
446
+    
447
+    char first_line[256];
448
+    bool valid_key = false;
449
+    
450
+    if (fgets(first_line, sizeof(first_line), key_file)) {
451
+        /* Check for common private key headers */
452
+        if (strstr(first_line, "-----BEGIN") && 
453
+            (strstr(first_line, "PRIVATE KEY") || 
454
+             strstr(first_line, "RSA PRIVATE KEY") ||
455
+             strstr(first_line, "OPENSSH PRIVATE KEY") ||
456
+             strstr(first_line, "EC PRIVATE KEY"))) {
457
+            valid_key = true;
458
+        }
459
+    }
460
+    
461
+    fclose(key_file);
462
+    
463
+    if (!valid_key) {
464
+        set_error(ERR_SSH_KEY_INVALID, "File does not appear to be a valid SSH private key: %s", key_path);
465
+        return -1;
466
+    }
467
+    
468
+    log_debug("SSH key validation passed: %s", key_path);
469
+    return 0;
470
+}
471
+
472
+/* Configure SSH host alias */
473
+int ssh_configure_host_alias(const account_t *account) {
474
+    char ssh_config_path[MAX_PATH_LEN];
475
+    char ssh_config_dir[MAX_PATH_LEN];
476
+    FILE *ssh_config_file;
477
+    char expanded_key_path[MAX_PATH_LEN];
478
+    
479
+    if (!account || strlen(account->ssh_host_alias) == 0) {
480
+        return 0; /* Nothing to configure */
481
+    }
482
+    
483
+    log_debug("Configuring SSH host alias: %s", account->ssh_host_alias);
484
+    
485
+    /* Get SSH config directory */
486
+    if (snprintf(ssh_config_dir, sizeof(ssh_config_dir), "%s/.ssh", getenv("HOME")) >= sizeof(ssh_config_dir)) {
487
+        set_error(ERR_INVALID_PATH, "SSH config directory path too long");
488
+        return -1;
489
+    }
490
+    
491
+    /* Create .ssh directory if it doesn't exist */
492
+    if (!path_exists(ssh_config_dir)) {
493
+        if (create_directory_recursive(ssh_config_dir, 0700) != 0) {
494
+            return -1;
495
+        }
496
+    }
497
+    
498
+    /* SSH config file path */
499
+    if (snprintf(ssh_config_path, sizeof(ssh_config_path), "%s/config", ssh_config_dir) >= sizeof(ssh_config_path)) {
500
+        set_error(ERR_INVALID_PATH, "SSH config file path too long");
501
+        return -1;
502
+    }
503
+    
504
+    /* Expand key path */
505
+    if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
506
+        return -1;
507
+    }
508
+    
509
+    /* Append to SSH config file */
510
+    ssh_config_file = fopen(ssh_config_path, "a");
511
+    if (!ssh_config_file) {
512
+        set_system_error(ERR_FILE_IO, "Failed to open SSH config file: %s", ssh_config_path);
513
+        return -1;
514
+    }
515
+    
516
+    /* Write host configuration */
517
+    fprintf(ssh_config_file, "\n# gitswitch-c configuration for account: %s\n", account->name);
518
+    fprintf(ssh_config_file, "Host %s\n", account->ssh_host_alias);
519
+    fprintf(ssh_config_file, "  IdentityFile %s\n", expanded_key_path);
520
+    fprintf(ssh_config_file, "  IdentitiesOnly yes\n");
521
+    fprintf(ssh_config_file, "  StrictHostKeyChecking no\n");
522
+    fprintf(ssh_config_file, "  UserKnownHostsFile /dev/null\n");
523
+    
524
+    fclose(ssh_config_file);
525
+    
526
+    /* Set proper permissions on SSH config file */
527
+    if (chmod(ssh_config_path, 0600) != 0) {
528
+        log_warning("Failed to set permissions on SSH config file");
529
+    }
530
+    
531
+    log_info("SSH host alias configured: %s -> %s", account->ssh_host_alias, expanded_key_path);
532
+    return 0;
533
+}
534
+
535
+/* Test SSH connection */
536
+int ssh_test_connection(const account_t *account, const char *host) {
537
+    char command[512];
538
+    char output[1024];
539
+    
540
+    if (!account || !host) {
541
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_test_connection");
542
+        return -1;
543
+    }
544
+    
545
+    log_debug("Testing SSH connection to: %s", host);
546
+    
547
+    /* Build SSH test command */
548
+    if (strlen(account->ssh_host_alias) > 0) {
549
+        /* Use host alias */
550
+        if (snprintf(command, sizeof(command), 
551
+                     "ssh -o ConnectTimeout=5 -o BatchMode=yes %s echo 'SSH connection test successful'",
552
+                     account->ssh_host_alias) >= sizeof(command)) {
553
+            set_error(ERR_INVALID_ARGS, "SSH test command too long");
554
+            return -1;
555
+        }
556
+    } else {
557
+        /* Use direct host with identity file */
558
+        char expanded_key_path[MAX_PATH_LEN];
559
+        if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
560
+            return -1;
561
+        }
562
+        
563
+        if (snprintf(command, sizeof(command),
564
+                     "ssh -o ConnectTimeout=5 -o BatchMode=yes -i '%s' %s echo 'SSH connection test successful'",
565
+                     expanded_key_path, host) >= sizeof(command)) {
566
+            set_error(ERR_INVALID_ARGS, "SSH test command too long");
567
+            return -1;
568
+        }
569
+    }
570
+    
571
+    /* Execute SSH test */
572
+    if (execute_ssh_command(command, output, sizeof(output)) != 0) {
573
+        set_error(ERR_SSH_CONNECTION_FAILED, "SSH connection test failed to %s: %s", host, output);
574
+        return -1;
575
+    }
576
+    
577
+    if (!strstr(output, "SSH connection test successful")) {
578
+        set_error(ERR_SSH_CONNECTION_FAILED, "SSH connection test did not return expected output");
579
+        return -1;
580
+    }
581
+    
582
+    log_info("SSH connection test successful to: %s", host);
583
+    return 0;
584
+}
585
+
586
+/* Internal helper functions */
587
+
588
+/* Execute SSH command */
589
+static int execute_ssh_command(const char *command, char *output, size_t output_size) {
590
+    FILE *pipe;
591
+    
592
+    if (!command) {
593
+        return -1;
594
+    }
595
+    
596
+    log_debug("Executing SSH command: %s", command);
597
+    
598
+    pipe = popen(command, "r");
599
+    if (!pipe) {
600
+        set_system_error(ERR_SYSTEM_COMMAND_FAILED, "Failed to execute SSH command");
601
+        return -1;
602
+    }
603
+    
604
+    /* Read output if buffer provided */
605
+    if (output && output_size > 0) {
606
+        size_t total_read = 0;
607
+        char *pos = output;
608
+        
609
+        while (total_read < output_size - 1 && 
610
+               fgets(pos, output_size - total_read, pipe)) {
611
+            size_t line_len = strlen(pos);
612
+            total_read += line_len;
613
+            pos += line_len;
614
+        }
615
+        output[total_read] = '\0';
616
+        
617
+        /* Remove trailing newline */
618
+        if (total_read > 0 && output[total_read - 1] == '\n') {
619
+            output[total_read - 1] = '\0';
620
+        }
621
+    }
622
+    
623
+    int exit_code = pclose(pipe);
624
+    if (exit_code != 0) {
625
+        log_debug("SSH command failed with exit code: %d", exit_code);
626
+        return -1;
627
+    }
628
+    
629
+    return 0;
630
+}
631
+
632
+/* Set up SSH environment variables */
633
+static int setup_ssh_environment(ssh_config_t *ssh_config) {
634
+    if (!ssh_config || strlen(ssh_config->agent_socket_path) == 0) {
635
+        return -1;
636
+    }
637
+    
638
+    /* Set SSH_AUTH_SOCK */
639
+    if (setenv("SSH_AUTH_SOCK", ssh_config->agent_socket_path, 1) != 0) {
640
+        set_system_error(ERR_SYSTEM_CALL, "Failed to set SSH_AUTH_SOCK");
641
+        return -1;
642
+    }
643
+    
644
+    /* Set SSH_AGENT_PID if we have it */
645
+    if (ssh_config->agent_pid > 0) {
646
+        char pid_str[32];
647
+        snprintf(pid_str, sizeof(pid_str), "%d", ssh_config->agent_pid);
648
+        if (setenv("SSH_AGENT_PID", pid_str, 1) != 0) {
649
+            set_system_error(ERR_SYSTEM_CALL, "Failed to set SSH_AGENT_PID");
650
+            return -1;
651
+        }
652
+    }
653
+    
654
+    log_debug("SSH environment configured: SSH_AUTH_SOCK=%s, SSH_AGENT_PID=%d",
655
+              ssh_config->agent_socket_path, ssh_config->agent_pid);
656
+    return 0;
657
+}
658
+
659
+/* Create isolated agent socket directory */
660
+static int create_isolated_agent_socket_dir(char *socket_dir, size_t socket_dir_size) {
661
+    const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
662
+    const char *tmp_dir = "/tmp";
663
+    
664
+    /* Prefer XDG_RUNTIME_DIR if available */
665
+    if (runtime_dir && path_exists(runtime_dir)) {
666
+        if (snprintf(socket_dir, socket_dir_size, "%s/gitswitch-ssh", runtime_dir) >= socket_dir_size) {
667
+            set_error(ERR_INVALID_PATH, "Socket directory path too long");
668
+            return -1;
669
+        }
670
+    } else {
671
+        if (snprintf(socket_dir, socket_dir_size, "%s/gitswitch-ssh-%d", tmp_dir, getuid()) >= socket_dir_size) {
672
+            set_error(ERR_INVALID_PATH, "Socket directory path too long");
673
+            return -1;
674
+        }
675
+    }
676
+    
677
+    /* Create directory with secure permissions */
678
+    if (!path_exists(socket_dir)) {
679
+        if (create_directory_recursive(socket_dir, 0700) != 0) {
680
+            return -1;
681
+        }
682
+        log_debug("Created SSH socket directory: %s", socket_dir);
683
+    }
684
+    
685
+    /* Verify permissions */
686
+    struct stat dir_stat;
687
+    if (stat(socket_dir, &dir_stat) != 0) {
688
+        set_system_error(ERR_FILE_IO, "Failed to stat socket directory");
689
+        return -1;
690
+    }
691
+    
692
+    if ((dir_stat.st_mode & 0777) != 0700) {
693
+        set_error(ERR_PERMISSION_DENIED, "Socket directory has insecure permissions");
694
+        return -1;
695
+    }
696
+    
697
+    return 0;
698
+}
699
+
700
+/* Check if SSH agent is running */
701
+static bool is_ssh_agent_running(pid_t pid) {
702
+    if (pid <= 0) {
703
+        return false;
704
+    }
705
+    
706
+    /* Use kill(pid, 0) to test if process exists */
707
+    return (kill(pid, 0) == 0);
708
+}
709
+
710
+/* Kill SSH agent gracefully */
711
+static int kill_ssh_agent_gracefully(pid_t pid) {
712
+    if (pid <= 0) {
713
+        return -1;
714
+    }
715
+    
716
+    if (!is_ssh_agent_running(pid)) {
717
+        log_debug("SSH agent (PID: %d) not running", pid);
718
+        return 0;
719
+    }
720
+    
721
+    /* Send SIGTERM first */
722
+    if (kill(pid, SIGTERM) != 0) {
723
+        set_system_error(ERR_SYSTEM_CALL, "Failed to send SIGTERM to SSH agent");
724
+        return -1;
725
+    }
726
+    
727
+    /* Wait a bit for graceful shutdown */
728
+    for (int i = 0; i < 10; i++) {
729
+        if (!is_ssh_agent_running(pid)) {
730
+            return 0;
731
+        }
732
+        usleep(100000); /* 100ms */
733
+    }
734
+    
735
+    /* Force kill if still running */
736
+    if (is_ssh_agent_running(pid)) {
737
+        log_warning("SSH agent did not respond to SIGTERM, sending SIGKILL");
738
+        if (kill(pid, SIGKILL) != 0) {
739
+            set_system_error(ERR_SYSTEM_CALL, "Failed to send SIGKILL to SSH agent");
740
+            return -1;
741
+        }
742
+    }
743
+    
744
+    return 0;
745
+}
746
+
747
+/* Validate SSH agent socket */
748
+static int validate_ssh_agent_socket(const char *socket_path) {
749
+    struct stat socket_stat;
750
+    
751
+    if (!socket_path) {
752
+        return -1;
753
+    }
754
+    
755
+    /* Check if socket exists */
756
+    if (stat(socket_path, &socket_stat) != 0) {
757
+        set_system_error(ERR_SSH_AGENT_SOCKET_INVALID, "SSH agent socket not found: %s", socket_path);
758
+        return -1;
759
+    }
760
+    
761
+    /* Check if it's a socket */
762
+    if (!S_ISSOCK(socket_stat.st_mode)) {
763
+        set_error(ERR_SSH_AGENT_SOCKET_INVALID, "Path is not a socket: %s", socket_path);
764
+        return -1;
765
+    }
766
+    
767
+    /* Check permissions */
768
+    if ((socket_stat.st_mode & 0777) != 0600) {
769
+        set_error(ERR_SSH_AGENT_SOCKET_INVALID, "SSH agent socket has wrong permissions: %s", socket_path);
770
+        return -1;
771
+    }
772
+    
773
+    return 0;
774
+}
775
+
776
+/* Parse ssh-agent output */
777
+static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config) {
778
+    char *line;
779
+    char *output_copy;
780
+    char *saveptr;
781
+    
782
+    if (!output || !ssh_config) {
783
+        return -1;
784
+    }
785
+    
786
+    /* Make a copy of output for parsing */
787
+    output_copy = strdup(output);
788
+    if (!output_copy) {
789
+        set_error(ERR_MEMORY_ALLOCATION, "Failed to allocate memory for parsing");
790
+        return -1;
791
+    }
792
+    
793
+    /* Parse line by line */
794
+    line = strtok_r(output_copy, "\n", &saveptr);
795
+    while (line) {
796
+        /* Look for SSH_AUTH_SOCK */
797
+        if (strstr(line, "SSH_AUTH_SOCK=")) {
798
+            char *socket_start = strchr(line, '=') + 1;
799
+            char *socket_end = strchr(socket_start, ';');
800
+            if (socket_end) {
801
+                *socket_end = '\0';
802
+            }
803
+            safe_strncpy(ssh_config->agent_socket_path, socket_start,
804
+                        sizeof(ssh_config->agent_socket_path));
805
+        }
806
+        
807
+        /* Look for SSH_AGENT_PID */
808
+        if (strstr(line, "SSH_AGENT_PID=")) {
809
+            char *pid_start = strchr(line, '=') + 1;
810
+            char *pid_end = strchr(pid_start, ';');
811
+            if (pid_end) {
812
+                *pid_end = '\0';
813
+            }
814
+            ssh_config->agent_pid = (pid_t)strtol(pid_start, NULL, 10);
815
+        }
816
+        
817
+        line = strtok_r(NULL, "\n", &saveptr);
818
+    }
819
+    
820
+    free(output_copy);
821
+    
822
+    /* Validate we got the required information */
823
+    if (strlen(ssh_config->agent_socket_path) == 0 || ssh_config->agent_pid <= 0) {
824
+        set_error(ERR_SSH_AGENT_START_FAILED, "Failed to parse ssh-agent output");
825
+        return -1;
826
+    }
827
+    
828
+    return 0;
829
+}
src/stubs.cadded
@@ -0,0 +1,29 @@
1
+/* Temporary stub implementations for Phase 1 compilation
2
+ * These will be replaced with actual implementations in later phases
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <string.h>
7
+#include "gitswitch.h"
8
+#include "display.h"
9
+#include "error.h"
10
+
11
+/* SSH manager stubs */
12
+int ssh_list_keys(ssh_config_t *ssh_config, char *output, size_t output_size) {
13
+    (void)ssh_config;
14
+    if (output && output_size > 0) {
15
+        strncpy(output, "SSH functionality not yet implemented", output_size - 1);
16
+        output[output_size - 1] = '\0';
17
+    }
18
+    return -1; /* Not implemented */
19
+}
20
+
21
+/* GPG manager stubs */
22
+int gpg_list_keys(gpg_config_t *gpg_config, char *output, size_t output_size) {
23
+    (void)gpg_config;
24
+    if (output && output_size > 0) {
25
+        strncpy(output, "GPG functionality not yet implemented", output_size - 1);
26
+        output[output_size - 1] = '\0';
27
+    }
28
+    return -1; /* Not implemented */
29
+}
src/toml_parser.cadded
1064 lines changed — click to load
@@ -0,0 +1,1064 @@
1
+/* Minimal, security-focused TOML parser implementation
2
+ * Built specifically for gitswitch-c with comprehensive input validation
3
+ */
4
+
5
+#include <stdio.h>
6
+#include <stdlib.h>
7
+#include <string.h>
8
+#include <ctype.h>
9
+#include <errno.h>
10
+#include <limits.h>
11
+#include <sys/stat.h>
12
+
13
+#include "toml_parser.h"
14
+#include "error.h"
15
+#include "utils.h"
16
+
17
+/* Internal parsing helper functions */
18
+static int parse_section_header(toml_parser_state_t *state, char *section_name);
19
+static int parse_key_value_pair(toml_parser_state_t *state, toml_keyvalue_t *kv);
20
+static int parse_string_value(toml_parser_state_t *state, char *value, size_t value_size);
21
+static int parse_integer_value(toml_parser_state_t *state, int *value);
22
+static int parse_boolean_value(toml_parser_state_t *state, bool *value);
23
+static void skip_whitespace(toml_parser_state_t *state);
24
+static void skip_comment(toml_parser_state_t *state);
25
+static bool is_at_end(const toml_parser_state_t *state);
26
+static char current_char(const toml_parser_state_t *state);
27
+static char advance_char(toml_parser_state_t *state);
28
+static bool match_char(toml_parser_state_t *state, char expected);
29
+static void set_parser_error(toml_parser_state_t *state, const char *message);
30
+static toml_section_t *find_section(toml_document_t *doc, const char *section_name);
31
+static toml_section_t *find_or_create_section(toml_document_t *doc, const char *section_name);
32
+static toml_keyvalue_t *find_key(toml_section_t *section, const char *key_name);
33
+
34
+/* Initialize TOML document structure */
35
+void toml_init_document(toml_document_t *doc) {
36
+    if (!doc) return;
37
+    
38
+    memset(doc, 0, sizeof(toml_document_t));
39
+    doc->is_valid = false;
40
+    doc->section_count = 0;
41
+}
42
+
43
+/* Parse TOML from file with comprehensive security validation */
44
+int toml_parse_file(const char *file_path, toml_document_t *doc) {
45
+    FILE *file;
46
+    struct stat file_stat;
47
+    char *buffer = NULL;
48
+    size_t file_size;
49
+    size_t bytes_read;
50
+    int result = -1;
51
+    
52
+    if (!file_path || !doc) {
53
+        set_error(ERR_INVALID_ARGS, "NULL arguments to toml_parse_file");
54
+        return -1;
55
+    }
56
+    
57
+    /* Security: Validate file path */
58
+    if (!toml_validate_file_path(file_path)) {
59
+        set_error(ERR_CONFIG_INVALID, "Invalid file path: %s", file_path);
60
+        return -1;
61
+    }
62
+    
63
+    /* Get file statistics for security checks */
64
+    if (stat(file_path, &file_stat) != 0) {
65
+        set_system_error(ERR_CONFIG_NOT_FOUND, "Cannot access config file: %s", file_path);
66
+        return -1;
67
+    }
68
+    
69
+    /* Security: Check file size limit */
70
+    if (file_stat.st_size > TOML_MAX_FILE_SIZE) {
71
+        set_error(ERR_CONFIG_INVALID, "Configuration file too large: %ld bytes (max: %d)", 
72
+                  file_stat.st_size, TOML_MAX_FILE_SIZE);
73
+        return -1;
74
+    }
75
+    
76
+    /* Security: Check file permissions (should not be world-readable) */
77
+    if (file_stat.st_mode & (S_IRGRP | S_IROTH)) {
78
+        set_error(ERR_PERMISSION_DENIED, "Configuration file has unsafe permissions: %o", 
79
+                  file_stat.st_mode & 0777);
80
+        return -1;
81
+    }
82
+    
83
+    file_size = (size_t)file_stat.st_size;
84
+    
85
+    /* Open file for reading */
86
+    file = fopen(file_path, "r");
87
+    if (!file) {
88
+        set_system_error(ERR_CONFIG_NOT_FOUND, "Failed to open config file: %s", file_path);
89
+        return -1;
90
+    }
91
+    
92
+    /* Allocate buffer for file content */
93
+    buffer = safe_malloc(file_size + 1);
94
+    if (!buffer) {
95
+        fclose(file);
96
+        return -1;
97
+    }
98
+    
99
+    /* Read file content */
100
+    bytes_read = fread(buffer, 1, file_size, file);
101
+    if (bytes_read != file_size) {
102
+        set_system_error(ERR_FILE_IO, "Failed to read complete config file: %s", file_path);
103
+        goto cleanup;
104
+    }
105
+    
106
+    buffer[file_size] = '\0';
107
+    fclose(file);
108
+    file = NULL;
109
+    
110
+    /* Security: Validate character set */
111
+    if (!toml_validate_safe_characters(buffer, file_size)) {
112
+        set_error(ERR_CONFIG_INVALID, "Configuration file contains unsafe characters");
113
+        goto cleanup;
114
+    }
115
+    
116
+    /* Security: Check for injection patterns */
117
+    if (!toml_check_injection_patterns(buffer, file_size)) {
118
+        set_error(ERR_CONFIG_INVALID, "Configuration file contains potentially malicious patterns");
119
+        goto cleanup;
120
+    }
121
+    
122
+    /* Store file path in document */
123
+    safe_strncpy(doc->file_path, file_path, sizeof(doc->file_path));
124
+    
125
+    /* Parse the TOML content */
126
+    result = toml_parse_string(buffer, file_size, doc);
127
+    
128
+cleanup:
129
+    if (file) fclose(file);
130
+    if (buffer) {
131
+        secure_zero_memory(buffer, file_size + 1);
132
+        free(buffer);
133
+    }
134
+    
135
+    return result;
136
+}
137
+
138
+/* Parse TOML from string buffer */
139
+int toml_parse_string(const char *toml_string, size_t length, toml_document_t *doc) {
140
+    toml_parser_state_t state;
141
+    char section_name[TOML_MAX_SECTION_LEN] = ""; /* Default to root section */
142
+    toml_section_t *current_section = NULL;
143
+    
144
+    if (!toml_string || !doc || length == 0) {
145
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_parse_string");
146
+        return -1;
147
+    }
148
+    
149
+    /* Initialize parser state */
150
+    memset(&state, 0, sizeof(state));
151
+    state.input = toml_string;
152
+    state.input_length = length;
153
+    state.position = 0;
154
+    state.line_number = 1;
155
+    state.column_number = 1;
156
+    state.has_error = false;
157
+    
158
+    /* Initialize document */
159
+    toml_init_document(doc);
160
+    
161
+    /* Parse line by line */
162
+    while (!is_at_end(&state) && !state.has_error) {
163
+        skip_whitespace(&state);
164
+        
165
+        if (is_at_end(&state)) {
166
+            break;
167
+        }
168
+        
169
+        char c = current_char(&state);
170
+        
171
+        /* Skip comments */
172
+        if (c == '#') {
173
+            skip_comment(&state);
174
+            continue;
175
+        }
176
+        
177
+        /* Parse section header */
178
+        if (c == '[') {
179
+            if (parse_section_header(&state, section_name) == 0) {
180
+                current_section = find_or_create_section(doc, section_name);
181
+                if (!current_section) {
182
+                    set_parser_error(&state, "Failed to create section");
183
+                    break;
184
+                }
185
+            }
186
+            continue;
187
+        }
188
+        
189
+        /* Parse key-value pair */
190
+        if (isalpha(c) || c == '_') {
191
+            if (!current_section) {
192
+                /* Create default section if none exists */
193
+                current_section = find_or_create_section(doc, "");
194
+                if (!current_section) {
195
+                    set_parser_error(&state, "Failed to create default section");
196
+                    break;
197
+                }
198
+            }
199
+            
200
+            if (current_section->key_count >= TOML_MAX_KEYS_PER_SECTION) {
201
+                set_parser_error(&state, "Too many keys in section");
202
+                break;
203
+            }
204
+            
205
+            toml_keyvalue_t *kv = &current_section->keys[current_section->key_count];
206
+            if (parse_key_value_pair(&state, kv) == 0) {
207
+                current_section->key_count++;
208
+            }
209
+            continue;
210
+        }
211
+        
212
+        /* Skip empty lines */
213
+        if (c == '\n' || c == '\r') {
214
+            advance_char(&state);
215
+            continue;
216
+        }
217
+        
218
+        /* Unknown character */
219
+        set_parser_error(&state, "Unexpected character");
220
+        break;
221
+    }
222
+    
223
+    if (state.has_error) {
224
+        set_error(ERR_CONFIG_INVALID, "TOML parsing failed at line %zu, column %zu: %s",
225
+                  state.line_number, state.column_number, state.error_message);
226
+        return -1;
227
+    }
228
+    
229
+    /* Validate the parsed document against our schema */
230
+    if (toml_validate_gitswitch_schema(doc) != 0) {
231
+        return -1;
232
+    }
233
+    
234
+    doc->is_valid = true;
235
+    log_debug("TOML document parsed successfully: %zu sections", doc->section_count);
236
+    
237
+    return 0;
238
+}
239
+
240
+/* Get string value from TOML document */
241
+int toml_get_string(const toml_document_t *doc, const char *section, 
242
+                    const char *key, char *value, size_t value_size) {
243
+    const toml_section_t *sec;
244
+    const toml_keyvalue_t *kv;
245
+    
246
+    if (!doc || !section || !key || !value || value_size == 0) {
247
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_get_string");
248
+        return -1;
249
+    }
250
+    
251
+    if (!doc->is_valid) {
252
+        set_error(ERR_CONFIG_INVALID, "TOML document is not valid");
253
+        return -1;
254
+    }
255
+    
256
+    sec = find_section((toml_document_t *)doc, section);
257
+    if (!sec) {
258
+        set_error(ERR_CONFIG_INVALID, "Section not found: %s", section);
259
+        return -1;
260
+    }
261
+    
262
+    kv = find_key((toml_section_t *)sec, key);
263
+    if (!kv || !kv->is_set) {
264
+        set_error(ERR_CONFIG_INVALID, "Key not found: %s.%s", section, key);
265
+        return -1;
266
+    }
267
+    
268
+    if (kv->type != TOML_TYPE_STRING) {
269
+        set_error(ERR_CONFIG_INVALID, "Key %s.%s is not a string", section, key);
270
+        return -1;
271
+    }
272
+    
273
+    /* Sanitize the value before returning */
274
+    return toml_sanitize_string(kv->value, value, value_size);
275
+}
276
+
277
+/* Get integer value from TOML document */
278
+int toml_get_integer(const toml_document_t *doc, const char *section, 
279
+                     const char *key, int *value) {
280
+    const toml_section_t *sec;
281
+    const toml_keyvalue_t *kv;
282
+    char *endptr;
283
+    long parsed_value;
284
+    
285
+    if (!doc || !section || !key || !value) {
286
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_get_integer");
287
+        return -1;
288
+    }
289
+    
290
+    if (!doc->is_valid) {
291
+        set_error(ERR_CONFIG_INVALID, "TOML document is not valid");
292
+        return -1;
293
+    }
294
+    
295
+    sec = find_section((toml_document_t *)doc, section);
296
+    if (!sec) {
297
+        set_error(ERR_CONFIG_INVALID, "Section not found: %s", section);
298
+        return -1;
299
+    }
300
+    
301
+    kv = find_key((toml_section_t *)sec, key);
302
+    if (!kv || !kv->is_set) {
303
+        set_error(ERR_CONFIG_INVALID, "Key not found: %s.%s", section, key);
304
+        return -1;
305
+    }
306
+    
307
+    if (kv->type != TOML_TYPE_INTEGER) {
308
+        set_error(ERR_CONFIG_INVALID, "Key %s.%s is not an integer", section, key);
309
+        return -1;
310
+    }
311
+    
312
+    errno = 0;
313
+    parsed_value = strtol(kv->value, &endptr, 10);
314
+    
315
+    if (errno != 0 || *endptr != '\0') {
316
+        set_error(ERR_CONFIG_INVALID, "Invalid integer value: %s", kv->value);
317
+        return -1;
318
+    }
319
+    
320
+    if (parsed_value < INT_MIN || parsed_value > INT_MAX) {
321
+        set_error(ERR_CONFIG_INVALID, "Integer value out of range: %ld", parsed_value);
322
+        return -1;
323
+    }
324
+    
325
+    *value = (int)parsed_value;
326
+    return 0;
327
+}
328
+
329
+/* Get boolean value from TOML document */
330
+int toml_get_boolean(const toml_document_t *doc, const char *section, 
331
+                     const char *key, bool *value) {
332
+    const toml_section_t *sec;
333
+    const toml_keyvalue_t *kv;
334
+    
335
+    if (!doc || !section || !key || !value) {
336
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_get_boolean");
337
+        return -1;
338
+    }
339
+    
340
+    if (!doc->is_valid) {
341
+        set_error(ERR_CONFIG_INVALID, "TOML document is not valid");
342
+        return -1;
343
+    }
344
+    
345
+    sec = find_section((toml_document_t *)doc, section);
346
+    if (!sec) {
347
+        set_error(ERR_CONFIG_INVALID, "Section not found: %s", section);
348
+        return -1;
349
+    }
350
+    
351
+    kv = find_key((toml_section_t *)sec, key);
352
+    if (!kv || !kv->is_set) {
353
+        set_error(ERR_CONFIG_INVALID, "Key not found: %s.%s", section, key);
354
+        return -1;
355
+    }
356
+    
357
+    if (kv->type != TOML_TYPE_BOOLEAN) {
358
+        set_error(ERR_CONFIG_INVALID, "Key %s.%s is not a boolean", section, key);
359
+        return -1;
360
+    }
361
+    
362
+    *value = (strcmp(kv->value, "true") == 0);
363
+    return 0;
364
+}
365
+
366
+/* Validate TOML document structure for gitswitch schema */
367
+int toml_validate_gitswitch_schema(const toml_document_t *doc) {
368
+    if (!doc) {
369
+        set_error(ERR_INVALID_ARGS, "NULL document to validate");
370
+        return -1;
371
+    }
372
+    
373
+    /* Check for required sections */
374
+    bool has_settings = false;
375
+    bool has_accounts = false;
376
+    
377
+    for (size_t i = 0; i < doc->section_count; i++) {
378
+        const toml_section_t *section = &doc->sections[i];
379
+        
380
+        if (strcmp(section->name, "settings") == 0) {
381
+            has_settings = true;
382
+            
383
+            /* Validate settings section */
384
+            bool has_default_scope = false;
385
+            for (size_t j = 0; j < section->key_count; j++) {
386
+                const toml_keyvalue_t *kv = &section->keys[j];
387
+                
388
+                if (strcmp(kv->key, "default_scope") == 0) {
389
+                    has_default_scope = true;
390
+                    if (kv->type != TOML_TYPE_STRING) {
391
+                        set_error(ERR_CONFIG_INVALID, "default_scope must be a string");
392
+                        return -1;
393
+                    }
394
+                    if (strcmp(kv->value, "local") != 0 && strcmp(kv->value, "global") != 0) {
395
+                        set_error(ERR_CONFIG_INVALID, "default_scope must be 'local' or 'global'");
396
+                        return -1;
397
+                    }
398
+                }
399
+            }
400
+            
401
+            if (!has_default_scope) {
402
+                set_error(ERR_CONFIG_INVALID, "settings section missing required default_scope");
403
+                return -1;
404
+            }
405
+        }
406
+        
407
+        if (string_starts_with(section->name, "accounts.")) {
408
+            has_accounts = true;
409
+            
410
+            /* Validate account section */
411
+            bool has_name = false, has_email = false;
412
+            
413
+            for (size_t j = 0; j < section->key_count; j++) {
414
+                const toml_keyvalue_t *kv = &section->keys[j];
415
+                
416
+                if (strcmp(kv->key, "name") == 0) {
417
+                    has_name = true;
418
+                    if (kv->type != TOML_TYPE_STRING || strlen(kv->value) == 0) {
419
+                        set_error(ERR_CONFIG_INVALID, "Account name must be a non-empty string");
420
+                        return -1;
421
+                    }
422
+                }
423
+                
424
+                if (strcmp(kv->key, "email") == 0) {
425
+                    has_email = true;
426
+                    if (kv->type != TOML_TYPE_STRING || !validate_email(kv->value)) {
427
+                        set_error(ERR_CONFIG_INVALID, "Account email must be a valid email address");
428
+                        return -1;
429
+                    }
430
+                }
431
+                
432
+                if (strcmp(kv->key, "ssh_key") == 0) {
433
+                    if (kv->type != TOML_TYPE_STRING) {
434
+                        set_error(ERR_CONFIG_INVALID, "ssh_key must be a string");
435
+                        return -1;
436
+                    }
437
+                    if (strlen(kv->value) > 0 && !toml_validate_file_path(kv->value)) {
438
+                        set_error(ERR_CONFIG_INVALID, "Invalid SSH key path: %s", kv->value);
439
+                        return -1;
440
+                    }
441
+                }
442
+                
443
+                if (strcmp(kv->key, "gpg_key") == 0) {
444
+                    if (kv->type != TOML_TYPE_STRING) {
445
+                        set_error(ERR_CONFIG_INVALID, "gpg_key must be a string");
446
+                        return -1;
447
+                    }
448
+                    if (strlen(kv->value) > 0 && !validate_key_id(kv->value)) {
449
+                        set_error(ERR_CONFIG_INVALID, "Invalid GPG key ID: %s", kv->value);
450
+                        return -1;
451
+                    }
452
+                }
453
+            }
454
+            
455
+            if (!has_name || !has_email) {
456
+                set_error(ERR_CONFIG_INVALID, "Account section %s missing required name or email", 
457
+                          section->name);
458
+                return -1;
459
+            }
460
+        }
461
+    }
462
+    
463
+    if (!has_settings) {
464
+        set_error(ERR_CONFIG_INVALID, "Configuration missing required [settings] section");
465
+        return -1;
466
+    }
467
+    
468
+    if (!has_accounts) {
469
+        log_info("Configuration has no account sections yet - this is normal for new installations");
470
+        /* This is not an error - allow empty configurations */
471
+    }
472
+    
473
+    log_debug("TOML document schema validation passed");
474
+    return 0;
475
+}
476
+
477
+/* Security validation: Check for safe characters only */
478
+bool toml_validate_safe_characters(const char *input, size_t length) {
479
+    if (!input) return false;
480
+    
481
+    for (size_t i = 0; i < length; i++) {
482
+        unsigned char c = (unsigned char)input[i];
483
+        
484
+        /* Allow printable ASCII, newlines, tabs, and carriage returns */
485
+        if (!(c >= 32 && c <= 126) && c != '\n' && c != '\r' && c != '\t') {
486
+            log_warning("Unsafe character found at position %zu: 0x%02x", i, c);
487
+            return false;
488
+        }
489
+    }
490
+    
491
+    return true;
492
+}
493
+
494
+/* Sanitize string value */
495
+int toml_sanitize_string(const char *input, char *output, size_t output_size) {
496
+    size_t input_len, output_pos = 0;
497
+    
498
+    if (!input || !output || output_size == 0) {
499
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_sanitize_string");
500
+        return -1;
501
+    }
502
+    
503
+    input_len = strlen(input);
504
+    
505
+    for (size_t i = 0; i < input_len && output_pos < output_size - 1; i++) {
506
+        char c = input[i];
507
+        
508
+        /* Remove or escape potentially dangerous characters */
509
+        if (c >= 32 && c <= 126 && c != '"' && c != '\\') {
510
+            output[output_pos++] = c;
511
+        }
512
+        /* Allow some whitespace */
513
+        else if (c == ' ' || c == '\t') {
514
+            output[output_pos++] = c;
515
+        }
516
+        /* Skip other characters */
517
+    }
518
+    
519
+    output[output_pos] = '\0';
520
+    return 0;
521
+}
522
+
523
+/* Validate file path for security */
524
+bool toml_validate_file_path(const char *path) {
525
+    if (!path || strlen(path) == 0) return true; /* Empty path is allowed */
526
+    
527
+    /* Check for directory traversal attempts */
528
+    if (strstr(path, "..") != NULL) {
529
+        log_warning("Directory traversal attempt in path: %s", path);
530
+        return false;
531
+    }
532
+    
533
+    /* Check for absolute paths outside user home */
534
+    if (path[0] == '/' && !string_starts_with(path, "/home/") && 
535
+        !string_starts_with(path, "/tmp/")) {
536
+        log_warning("Suspicious absolute path: %s", path);
537
+        return false;
538
+    }
539
+    
540
+    /* Check path length */
541
+    if (strlen(path) > 256) {
542
+        log_warning("Path too long: %zu characters", strlen(path));
543
+        return false;
544
+    }
545
+    
546
+    return true;
547
+}
548
+
549
+/* Check for TOML injection patterns */
550
+bool toml_check_injection_patterns(const char *input, size_t length) {
551
+    const char *dangerous_patterns[] = {
552
+        "$(", "`", "${", "\\x", "\\u", NULL
553
+    };
554
+    
555
+    if (!input) return false;
556
+    
557
+    for (int i = 0; dangerous_patterns[i] != NULL; i++) {
558
+        if (strstr(input, dangerous_patterns[i]) != NULL) {
559
+            log_warning("Potentially dangerous pattern found: %s", dangerous_patterns[i]);
560
+            return false;
561
+        }
562
+    }
563
+    
564
+    /* Check for excessive nesting or repetition */
565
+    size_t bracket_count = 0;
566
+    for (size_t i = 0; i < length; i++) {
567
+        if (input[i] == '[') {
568
+            bracket_count++;
569
+            if (bracket_count > 32) {
570
+                log_warning("Excessive bracket nesting detected");
571
+                return false;
572
+            }
573
+        }
574
+    }
575
+    
576
+    return true;
577
+}
578
+
579
+/* Internal helper functions implementation continues... */
580
+
581
+/* Find section in document */
582
+static toml_section_t *find_section(toml_document_t *doc, const char *section_name) {
583
+    if (!doc || !section_name) return NULL;
584
+    
585
+    for (size_t i = 0; i < doc->section_count; i++) {
586
+        if (strcmp(doc->sections[i].name, section_name) == 0) {
587
+            return &doc->sections[i];
588
+        }
589
+    }
590
+    
591
+    return NULL;
592
+}
593
+
594
+/* Find or create section */
595
+static toml_section_t *find_or_create_section(toml_document_t *doc, const char *section_name) {
596
+    toml_section_t *section;
597
+    
598
+    if (!doc || !section_name) return NULL;
599
+    
600
+    /* Try to find existing section */
601
+    section = find_section(doc, section_name);
602
+    if (section) return section;
603
+    
604
+    /* Create new section */
605
+    if (doc->section_count >= TOML_MAX_SECTIONS) {
606
+        log_error("Maximum number of sections exceeded: %d", TOML_MAX_SECTIONS);
607
+        return NULL;
608
+    }
609
+    
610
+    section = &doc->sections[doc->section_count];
611
+    memset(section, 0, sizeof(toml_section_t));
612
+    
613
+    safe_strncpy(section->name, section_name, sizeof(section->name));
614
+    section->is_set = true;
615
+    section->key_count = 0;
616
+    
617
+    doc->section_count++;
618
+    
619
+    return section;
620
+}
621
+
622
+/* Find key in section */
623
+static toml_keyvalue_t *find_key(toml_section_t *section, const char *key_name) {
624
+    if (!section || !key_name) return NULL;
625
+    
626
+    for (size_t i = 0; i < section->key_count; i++) {
627
+        if (strcmp(section->keys[i].key, key_name) == 0) {
628
+            return &section->keys[i];
629
+        }
630
+    }
631
+    
632
+    return NULL;
633
+}
634
+
635
+/* Parse section header [section.name] */
636
+static int parse_section_header(toml_parser_state_t *state, char *section_name) {
637
+    size_t name_pos = 0;
638
+    
639
+    if (!match_char(state, '[')) {
640
+        set_parser_error(state, "Expected '[' at start of section");
641
+        return -1;
642
+    }
643
+    
644
+    skip_whitespace(state);
645
+    
646
+    /* Parse section name */
647
+    while (!is_at_end(state) && current_char(state) != ']' && 
648
+           name_pos < TOML_MAX_SECTION_LEN - 1) {
649
+        char c = advance_char(state);
650
+        
651
+        if (isalnum(c) || c == '.' || c == '_' || c == '-') {
652
+            section_name[name_pos++] = c;
653
+        } else {
654
+            set_parser_error(state, "Invalid character in section name");
655
+            return -1;
656
+        }
657
+    }
658
+    
659
+    section_name[name_pos] = '\0';
660
+    
661
+    skip_whitespace(state);
662
+    
663
+    if (!match_char(state, ']')) {
664
+        set_parser_error(state, "Expected ']' at end of section");
665
+        return -1;
666
+    }
667
+    
668
+    return 0;
669
+}
670
+
671
+/* Parse key-value pair */
672
+static int parse_key_value_pair(toml_parser_state_t *state, toml_keyvalue_t *kv) {
673
+    size_t key_pos = 0;
674
+    
675
+    memset(kv, 0, sizeof(toml_keyvalue_t));
676
+    
677
+    /* Parse key name */
678
+    while (!is_at_end(state) && current_char(state) != '=' && 
679
+           key_pos < TOML_MAX_KEY_LEN - 1) {
680
+        char c = current_char(state);
681
+        
682
+        if (isalnum(c) || c == '_') {
683
+            kv->key[key_pos++] = advance_char(state);
684
+        } else if (isspace(c)) {
685
+            advance_char(state);
686
+            break;
687
+        } else {
688
+            set_parser_error(state, "Invalid character in key name");
689
+            return -1;
690
+        }
691
+    }
692
+    
693
+    kv->key[key_pos] = '\0';
694
+    
695
+    skip_whitespace(state);
696
+    
697
+    if (!match_char(state, '=')) {
698
+        set_parser_error(state, "Expected '=' after key name");
699
+        return -1;
700
+    }
701
+    
702
+    skip_whitespace(state);
703
+    
704
+    /* Determine value type and parse */
705
+    char c = current_char(state);
706
+    
707
+    if (c == '"') {
708
+        /* String value */
709
+        kv->type = TOML_TYPE_STRING;
710
+        return parse_string_value(state, kv->value, sizeof(kv->value));
711
+    } else if (c == 't' || c == 'f') {
712
+        /* Boolean value */
713
+        kv->type = TOML_TYPE_BOOLEAN;
714
+        bool bool_val;
715
+        if (parse_boolean_value(state, &bool_val) == 0) {
716
+            strcpy(kv->value, bool_val ? "true" : "false");
717
+            kv->is_set = true;
718
+            return 0;
719
+        }
720
+        return -1;
721
+    } else if (isdigit(c) || c == '-' || c == '+') {
722
+        /* Integer value */
723
+        kv->type = TOML_TYPE_INTEGER;
724
+        int int_val;
725
+        if (parse_integer_value(state, &int_val) == 0) {
726
+            snprintf(kv->value, sizeof(kv->value), "%d", int_val);
727
+            kv->is_set = true;
728
+            return 0;
729
+        }
730
+        return -1;
731
+    } else {
732
+        set_parser_error(state, "Invalid value type");
733
+        return -1;
734
+    }
735
+}
736
+
737
+/* Parse string value "..." */
738
+static int parse_string_value(toml_parser_state_t *state, char *value, size_t value_size) {
739
+    size_t value_pos = 0;
740
+    
741
+    if (!match_char(state, '"')) {
742
+        set_parser_error(state, "Expected '\"' at start of string");
743
+        return -1;
744
+    }
745
+    
746
+    while (!is_at_end(state) && current_char(state) != '"' && 
747
+           value_pos < value_size - 1) {
748
+        char c = advance_char(state);
749
+        
750
+        /* Handle escape sequences */
751
+        if (c == '\\' && !is_at_end(state)) {
752
+            char next = advance_char(state);
753
+            switch (next) {
754
+                case 'n': value[value_pos++] = '\n'; break;
755
+                case 't': value[value_pos++] = '\t'; break;
756
+                case 'r': value[value_pos++] = '\r'; break;
757
+                case '\\': value[value_pos++] = '\\'; break;
758
+                case '"': value[value_pos++] = '"'; break;
759
+                default:
760
+                    set_parser_error(state, "Invalid escape sequence");
761
+                    return -1;
762
+            }
763
+        } else {
764
+            value[value_pos++] = c;
765
+        }
766
+    }
767
+    
768
+    value[value_pos] = '\0';
769
+    
770
+    if (!match_char(state, '"')) {
771
+        set_parser_error(state, "Expected '\"' at end of string");
772
+        return -1;
773
+    }
774
+    
775
+    return 0;
776
+}
777
+
778
+/* Parse boolean value true/false */
779
+static int parse_boolean_value(toml_parser_state_t *state, bool *value) {
780
+    if (strncmp(&state->input[state->position], "true", 4) == 0) {
781
+        state->position += 4;
782
+        *value = true;
783
+        return 0;
784
+    } else if (strncmp(&state->input[state->position], "false", 5) == 0) {
785
+        state->position += 5;
786
+        *value = false;
787
+        return 0;
788
+    } else {
789
+        set_parser_error(state, "Invalid boolean value");
790
+        return -1;
791
+    }
792
+}
793
+
794
+/* Parse integer value */
795
+static int parse_integer_value(toml_parser_state_t *state, int *value) {
796
+    char num_str[32];
797
+    size_t num_pos = 0;
798
+    char *endptr;
799
+    long parsed_value;
800
+    
801
+    /* Handle optional sign */
802
+    char c = current_char(state);
803
+    if (c == '+' || c == '-') {
804
+        num_str[num_pos++] = advance_char(state);
805
+    }
806
+    
807
+    /* Parse digits */
808
+    while (!is_at_end(state) && isdigit(current_char(state)) && 
809
+           num_pos < sizeof(num_str) - 1) {
810
+        num_str[num_pos++] = advance_char(state);
811
+    }
812
+    
813
+    num_str[num_pos] = '\0';
814
+    
815
+    if (num_pos == 0 || (num_pos == 1 && (num_str[0] == '+' || num_str[0] == '-'))) {
816
+        set_parser_error(state, "Invalid integer format");
817
+        return -1;
818
+    }
819
+    
820
+    errno = 0;
821
+    parsed_value = strtol(num_str, &endptr, 10);
822
+    
823
+    if (errno != 0 || *endptr != '\0') {
824
+        set_parser_error(state, "Integer parsing error");
825
+        return -1;
826
+    }
827
+    
828
+    if (parsed_value < INT_MIN || parsed_value > INT_MAX) {
829
+        set_parser_error(state, "Integer out of range");
830
+        return -1;
831
+    }
832
+    
833
+    *value = (int)parsed_value;
834
+    return 0;
835
+}
836
+
837
+/* Parsing helper functions */
838
+
839
+static void skip_whitespace(toml_parser_state_t *state) {
840
+    while (!is_at_end(state)) {
841
+        char c = current_char(state);
842
+        if (c == ' ' || c == '\t') {
843
+            advance_char(state);
844
+        } else {
845
+            break;
846
+        }
847
+    }
848
+}
849
+
850
+static void skip_comment(toml_parser_state_t *state) {
851
+    while (!is_at_end(state) && current_char(state) != '\n') {
852
+        advance_char(state);
853
+    }
854
+    if (!is_at_end(state)) {
855
+        advance_char(state); /* Skip the newline */
856
+    }
857
+}
858
+
859
+static bool is_at_end(const toml_parser_state_t *state) {
860
+    return state->position >= state->input_length;
861
+}
862
+
863
+static char current_char(const toml_parser_state_t *state) {
864
+    if (is_at_end(state)) return '\0';
865
+    return state->input[state->position];
866
+}
867
+
868
+static char advance_char(toml_parser_state_t *state) {
869
+    if (is_at_end(state)) return '\0';
870
+    
871
+    char c = state->input[state->position++];
872
+    
873
+    if (c == '\n') {
874
+        state->line_number++;
875
+        state->column_number = 1;
876
+    } else {
877
+        state->column_number++;
878
+    }
879
+    
880
+    return c;
881
+}
882
+
883
+static bool match_char(toml_parser_state_t *state, char expected) {
884
+    if (is_at_end(state) || current_char(state) != expected) {
885
+        return false;
886
+    }
887
+    advance_char(state);
888
+    return true;
889
+}
890
+
891
+static void set_parser_error(toml_parser_state_t *state, const char *message) {
892
+    state->has_error = true;
893
+    safe_strncpy(state->error_message, message, sizeof(state->error_message));
894
+}
895
+
896
+/* Get all sections from document */
897
+int toml_get_sections(const toml_document_t *doc, char sections[][TOML_MAX_SECTION_LEN],
898
+                      size_t max_sections, size_t *section_count) {
899
+    if (!doc || !sections || !section_count) {
900
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_get_sections");
901
+        return -1;
902
+    }
903
+    
904
+    *section_count = 0;
905
+    
906
+    for (size_t i = 0; i < doc->section_count && *section_count < max_sections; i++) {
907
+        safe_strncpy(sections[*section_count], doc->sections[i].name, TOML_MAX_SECTION_LEN);
908
+        (*section_count)++;
909
+    }
910
+    
911
+    return 0;
912
+}
913
+
914
+/* Set string value in document */
915
+int toml_set_string(toml_document_t *doc, const char *section_name, 
916
+                    const char *key_name, const char *value) {
917
+    toml_section_t *section;
918
+    toml_keyvalue_t *kv;
919
+    
920
+    if (!doc || !section_name || !key_name || !value) {
921
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_set_string");
922
+        return -1;
923
+    }
924
+    
925
+    /* Find or create section */
926
+    section = find_or_create_section(doc, section_name);
927
+    if (!section) {
928
+        return -1;
929
+    }
930
+    
931
+    /* Find or create key */
932
+    kv = find_key(section, key_name);
933
+    if (!kv) {
934
+        /* Create new key-value pair */
935
+        if (section->key_count >= TOML_MAX_KEYS_PER_SECTION) {
936
+            set_error(ERR_CONFIG_INVALID, "Too many key-value pairs in section: %s", section_name);
937
+            return -1;
938
+        }
939
+        
940
+        kv = &section->keys[section->key_count];
941
+        safe_strncpy(kv->key, key_name, sizeof(kv->key));
942
+        section->key_count++;
943
+    }
944
+    
945
+    /* Set string value */
946
+    kv->type = TOML_TYPE_STRING;
947
+    safe_strncpy(kv->value, value, sizeof(kv->value));
948
+    kv->is_set = true;
949
+    
950
+    return 0;
951
+}
952
+
953
+/* Set boolean value in document */
954
+int toml_set_boolean(toml_document_t *doc, const char *section_name, 
955
+                     const char *key_name, bool value) {
956
+    toml_section_t *section;
957
+    toml_keyvalue_t *kv;
958
+    
959
+    if (!doc || !section_name || !key_name) {
960
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_set_boolean");
961
+        return -1;
962
+    }
963
+    
964
+    /* Find or create section */
965
+    section = find_or_create_section(doc, section_name);
966
+    if (!section) {
967
+        return -1;
968
+    }
969
+    
970
+    /* Find or create key */
971
+    kv = find_key(section, key_name);
972
+    if (!kv) {
973
+        /* Create new key-value pair */
974
+        if (section->key_count >= TOML_MAX_KEYS_PER_SECTION) {
975
+            set_error(ERR_CONFIG_INVALID, "Too many key-value pairs in section: %s", section_name);
976
+            return -1;
977
+        }
978
+        
979
+        kv = &section->keys[section->key_count];
980
+        safe_strncpy(kv->key, key_name, sizeof(kv->key));
981
+        section->key_count++;
982
+    }
983
+    
984
+    /* Set boolean value */
985
+    kv->type = TOML_TYPE_BOOLEAN;
986
+    safe_strncpy(kv->value, value ? "true" : "false", sizeof(kv->value));
987
+    kv->is_set = true;
988
+    
989
+    return 0;
990
+}
991
+
992
+/* Write document to file */
993
+int toml_write_file(const toml_document_t *doc, const char *file_path) {
994
+    FILE *file;
995
+    
996
+    if (!doc || !file_path) {
997
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to toml_write_file");
998
+        return -1;
999
+    }
1000
+    
1001
+    file = fopen(file_path, "w");
1002
+    if (!file) {
1003
+        set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to open file for writing: %s", file_path);
1004
+        return -1;
1005
+    }
1006
+    
1007
+    /* Write sections */
1008
+    for (size_t i = 0; i < doc->section_count; i++) {
1009
+        const toml_section_t *section = &doc->sections[i];
1010
+        
1011
+        /* Write section header */
1012
+        if (fprintf(file, "[%s]\n", section->name) < 0) {
1013
+            fclose(file);
1014
+            set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to write section header");
1015
+            return -1;
1016
+        }
1017
+        
1018
+        /* Write key-value pairs */
1019
+        for (size_t j = 0; j < section->key_count; j++) {
1020
+            const toml_keyvalue_t *kv = &section->keys[j];
1021
+            
1022
+            if (!kv->is_set) continue;
1023
+            
1024
+            switch (kv->type) {
1025
+                case TOML_TYPE_STRING:
1026
+                    if (fprintf(file, "%s = \"%s\"\n", kv->key, kv->value) < 0) {
1027
+                        fclose(file);
1028
+                        set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to write string value");
1029
+                        return -1;
1030
+                    }
1031
+                    break;
1032
+                    
1033
+                case TOML_TYPE_INTEGER:
1034
+                case TOML_TYPE_BOOLEAN:
1035
+                    if (fprintf(file, "%s = %s\n", kv->key, kv->value) < 0) {
1036
+                        fclose(file);
1037
+                        set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to write value");
1038
+                        return -1;
1039
+                    }
1040
+                    break;
1041
+                    
1042
+                case TOML_TYPE_INVALID:
1043
+                default:
1044
+                    break;
1045
+            }
1046
+        }
1047
+        
1048
+        /* Add blank line between sections */
1049
+        if (i < doc->section_count - 1) {
1050
+            fprintf(file, "\n");
1051
+        }
1052
+    }
1053
+    
1054
+    fclose(file);
1055
+    return 0;
1056
+}
1057
+
1058
+/* Cleanup TOML document */
1059
+void toml_cleanup_document(toml_document_t *doc) {
1060
+    if (!doc) return;
1061
+    
1062
+    /* Clear sensitive data */
1063
+    secure_zero_memory(doc, sizeof(toml_document_t));
1064
+}
src/utils.cadded
1126 lines changed — click to load
@@ -0,0 +1,1126 @@
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 (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 (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 (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
+        freopen("/dev/null", "r", stdin);
553
+        freopen("/dev/null", "w", stdout);
554
+        freopen("/dev/null", "w", stderr);
555
+        
556
+        /* Execute command */
557
+        execl("/bin/sh", "sh", "-c", command, (char *)NULL);
558
+        _exit(127); /* If exec fails */
559
+    }
560
+    
561
+    /* Parent process */
562
+    if (pidfile_path) {
563
+        pidfile = fopen(pidfile_path, "w");
564
+        if (pidfile) {
565
+            fprintf(pidfile, "%d\n", pid);
566
+            fclose(pidfile);
567
+        }
568
+    }
569
+    
570
+    return pid;
571
+}
572
+
573
+int kill_process_by_pidfile(const char *pidfile_path) {
574
+    FILE *pidfile;
575
+    pid_t pid;
576
+    
577
+    if (!pidfile_path) {
578
+        set_error(ERR_INVALID_ARGS, "NULL pidfile path");
579
+        return -1;
580
+    }
581
+    
582
+    pidfile = fopen(pidfile_path, "r");
583
+    if (!pidfile) {
584
+        set_system_error(ERR_FILE_IO, "Failed to open pidfile: %s", pidfile_path);
585
+        return -1;
586
+    }
587
+    
588
+    if (fscanf(pidfile, "%d", &pid) != 1) {
589
+        set_error(ERR_FILE_IO, "Failed to read PID from file: %s", pidfile_path);
590
+        fclose(pidfile);
591
+        return -1;
592
+    }
593
+    
594
+    fclose(pidfile);
595
+    
596
+    if (pid <= 0) {
597
+        set_error(ERR_INVALID_ARGS, "Invalid PID in file: %d", pid);
598
+        return -1;
599
+    }
600
+    
601
+    if (kill(pid, SIGTERM) != 0) {
602
+        if (errno == ESRCH) {
603
+            /* Process doesn't exist - clean up pidfile */
604
+            unlink(pidfile_path);
605
+            return 0;
606
+        }
607
+        set_system_error(ERR_SYSTEM_CALL, "Failed to kill process %d", pid);
608
+        return -1;
609
+    }
610
+    
611
+    /* Clean up pidfile */
612
+    unlink(pidfile_path);
613
+    
614
+    return 0;
615
+}
616
+
617
+bool process_is_running(pid_t pid) {
618
+    if (pid <= 0) return false;
619
+    return kill(pid, 0) == 0;
620
+}
621
+
622
+/* Environment utilities */
623
+
624
+int get_env_var(const char *name, char *buffer, size_t buffer_size) {
625
+    const char *value;
626
+    
627
+    if (!name || !buffer || buffer_size == 0) {
628
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to get_env_var");
629
+        return -1;
630
+    }
631
+    
632
+    value = getenv(name);
633
+    if (!value) {
634
+        buffer[0] = '\0';
635
+        return 1; /* Not an error, just not found */
636
+    }
637
+    
638
+    if (strlen(value) >= buffer_size) {
639
+        set_error(ERR_INVALID_ARGS, "Environment variable value too long");
640
+        return -1;
641
+    }
642
+    
643
+    strcpy(buffer, value);
644
+    return 0;
645
+}
646
+
647
+int set_env_var(const char *name, const char *value, bool overwrite) {
648
+    if (!name || !value) {
649
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to set_env_var");
650
+        return -1;
651
+    }
652
+    
653
+    if (setenv(name, value, overwrite ? 1 : 0) != 0) {
654
+        set_system_error(ERR_SYSTEM_CALL, "Failed to set environment variable: %s", name);
655
+        return -1;
656
+    }
657
+    
658
+    return 0;
659
+}
660
+
661
+int unset_env_var(const char *name) {
662
+    if (!name) {
663
+        set_error(ERR_INVALID_ARGS, "NULL name to unset_env_var");
664
+        return -1;
665
+    }
666
+    
667
+    if (unsetenv(name) != 0) {
668
+        set_system_error(ERR_SYSTEM_CALL, "Failed to unset environment variable: %s", name);
669
+        return -1;
670
+    }
671
+    
672
+    return 0;
673
+}
674
+
675
+/* Validation utilities */
676
+
677
+bool validate_email(const char *email) {
678
+    regex_t regex;
679
+    int result;
680
+    
681
+    if (!email || strlen(email) > MAX_EMAIL_LEN) {
682
+        return false;
683
+    }
684
+    
685
+    /* Basic email regex - not RFC compliant but good enough for git configs */
686
+    const char *pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
687
+    
688
+    result = regcomp(&regex, pattern, REG_EXTENDED);
689
+    if (result) return false;
690
+    
691
+    result = regexec(&regex, email, 0, NULL, 0);
692
+    regfree(&regex);
693
+    
694
+    return result == 0;
695
+}
696
+
697
+bool validate_name(const char *name) {
698
+    if (!name || strlen(name) == 0 || strlen(name) >= MAX_NAME_LEN) {
699
+        return false;
700
+    }
701
+    
702
+    /* Name should contain at least one non-whitespace character */
703
+    for (const char *p = name; *p; p++) {
704
+        if (!isspace((unsigned char)*p)) {
705
+            return true;
706
+        }
707
+    }
708
+    
709
+    return false;
710
+}
711
+
712
+bool validate_key_id(const char *key_id) {
713
+    if (!key_id || strlen(key_id) == 0 || strlen(key_id) >= MAX_KEY_ID_LEN) {
714
+        return false;
715
+    }
716
+    
717
+    /* Key ID should be hexadecimal */
718
+    for (const char *p = key_id; *p; p++) {
719
+        if (!isxdigit((unsigned char)*p)) {
720
+            return false;
721
+        }
722
+    }
723
+    
724
+    return true;
725
+}
726
+
727
+bool validate_file_path(const char *path) {
728
+    char expanded[MAX_PATH_LEN];
729
+    
730
+    if (!path || strlen(path) == 0 || strlen(path) >= MAX_PATH_LEN) {
731
+        return false;
732
+    }
733
+    
734
+    /* Expand path and check if it exists */
735
+    if (expand_path(path, expanded, sizeof(expanded)) != 0) {
736
+        return false;
737
+    }
738
+    
739
+    return path_exists(expanded);
740
+}
741
+
742
+/* Security utilities */
743
+
744
+void secure_zero_memory(void *ptr, size_t size) {
745
+    if (!ptr || size == 0) return;
746
+    
747
+    /* Use explicit_bzero if available, otherwise volatile memset */
748
+#ifdef __GLIBC__
749
+    explicit_bzero(ptr, size);
750
+#else
751
+    volatile unsigned char *p = ptr;
752
+    while (size--) {
753
+        *p++ = 0;
754
+    }
755
+#endif
756
+}
757
+
758
+int generate_random_string(char *buffer, size_t buffer_size, const char *charset) {
759
+    size_t charset_len;
760
+    size_t i;
761
+    FILE *urandom;
762
+    
763
+    if (!buffer || buffer_size == 0 || !charset) {
764
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to generate_random_string");
765
+        return -1;
766
+    }
767
+    
768
+    charset_len = strlen(charset);
769
+    if (charset_len == 0) {
770
+        set_error(ERR_INVALID_ARGS, "Empty charset");
771
+        return -1;
772
+    }
773
+    
774
+    urandom = fopen("/dev/urandom", "rb");
775
+    if (!urandom) {
776
+        set_system_error(ERR_FILE_IO, "Failed to open /dev/urandom");
777
+        return -1;
778
+    }
779
+    
780
+    for (i = 0; i < buffer_size - 1; i++) {
781
+        unsigned char rand_byte;
782
+        if (fread(&rand_byte, 1, 1, urandom) != 1) {
783
+            set_system_error(ERR_FILE_IO, "Failed to read random data");
784
+            fclose(urandom);
785
+            return -1;
786
+        }
787
+        buffer[i] = charset[rand_byte % charset_len];
788
+    }
789
+    
790
+    buffer[buffer_size - 1] = '\0';
791
+    fclose(urandom);
792
+    
793
+    return 0;
794
+}
795
+
796
+bool check_file_permissions_safe(const char *file_path, mode_t expected_mode) {
797
+    mode_t actual_mode;
798
+    
799
+    if (!file_path) return false;
800
+    
801
+    if (get_file_permissions(file_path, &actual_mode) != 0) {
802
+        return false;
803
+    }
804
+    
805
+    /* Check if permissions are as expected or more restrictive */
806
+    return (actual_mode & 07777) == expected_mode;
807
+}
808
+
809
+/* Configuration utilities */
810
+
811
+int get_config_directory(char *config_dir, size_t dir_size) {
812
+    char home[MAX_PATH_LEN];
813
+    
814
+    if (!config_dir || dir_size == 0) {
815
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to get_config_directory");
816
+        return -1;
817
+    }
818
+    
819
+    if (get_home_directory(home, sizeof(home)) != 0) {
820
+        return -1;
821
+    }
822
+    
823
+    if (snprintf(config_dir, dir_size, "%s/%s", home, DEFAULT_CONFIG_DIR) >= (int)dir_size) {
824
+        set_error(ERR_INVALID_ARGS, "Config directory path too long");
825
+        return -1;
826
+    }
827
+    
828
+    return 0;
829
+}
830
+
831
+int ensure_config_directory_exists(void) {
832
+    char config_dir[MAX_PATH_LEN];
833
+    
834
+    if (get_config_directory(config_dir, sizeof(config_dir)) != 0) {
835
+        return -1;
836
+    }
837
+    
838
+    if (!is_directory(config_dir)) {
839
+        if (create_directory_recursive(config_dir, PERM_USER_RWX) != 0) {
840
+            return -1;
841
+        }
842
+        log_info("Created config directory: %s", config_dir);
843
+    }
844
+    
845
+    return 0;
846
+}
847
+
848
+/* Terminal utilities */
849
+
850
+bool is_terminal(int fd) {
851
+    return isatty(fd) == 1;
852
+}
853
+
854
+int get_terminal_size(int *width, int *height) {
855
+    struct winsize ws;
856
+    
857
+    if (!width || !height) {
858
+        set_error(ERR_INVALID_ARGS, "NULL arguments to get_terminal_size");
859
+        return -1;
860
+    }
861
+    
862
+    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) {
863
+        set_system_error(ERR_SYSTEM_CALL, "Failed to get terminal size");
864
+        return -1;
865
+    }
866
+    
867
+    *width = ws.ws_col;
868
+    *height = ws.ws_row;
869
+    
870
+    return 0;
871
+}
872
+
873
+void disable_echo(void) {
874
+    struct termios new_termios;
875
+    
876
+    if (g_echo_disabled) return;
877
+    
878
+    if (tcgetattr(STDIN_FILENO, &g_original_termios) != 0) {
879
+        return; /* Can't save original, don't disable echo */
880
+    }
881
+    
882
+    new_termios = g_original_termios;
883
+    new_termios.c_lflag &= ~ECHO;
884
+    
885
+    if (tcsetattr(STDIN_FILENO, TCSANOW, &new_termios) == 0) {
886
+        g_echo_disabled = true;
887
+    }
888
+}
889
+
890
+void enable_echo(void) {
891
+    if (!g_echo_disabled) return;
892
+    
893
+    tcsetattr(STDIN_FILENO, TCSANOW, &g_original_termios);
894
+    g_echo_disabled = false;
895
+}
896
+
897
+/* Time utilities */
898
+
899
+void get_current_time_string(char *buffer, size_t buffer_size) {
900
+    time_t now;
901
+    struct tm *tm_info;
902
+    
903
+    if (!buffer || buffer_size == 0) return;
904
+    
905
+    time(&now);
906
+    tm_info = localtime(&now);
907
+    
908
+    if (tm_info) {
909
+        strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", tm_info);
910
+    } else {
911
+        strncpy(buffer, "UNKNOWN", buffer_size - 1);
912
+        buffer[buffer_size - 1] = '\0';
913
+    }
914
+}
915
+
916
+void get_timestamp_string(char *buffer, size_t buffer_size) {
917
+    time_t now;
918
+    
919
+    if (!buffer || buffer_size == 0) return;
920
+    
921
+    time(&now);
922
+    snprintf(buffer, buffer_size, "%ld", (long)now);
923
+}
924
+
925
+bool is_timestamp_expired(time_t timestamp, int max_age_seconds) {
926
+    time_t now;
927
+    time(&now);
928
+    return (now - timestamp) > max_age_seconds;
929
+}
930
+
931
+/* Comparison utilities */
932
+
933
+int compare_strings(const void *a, const void *b) {
934
+    return strcmp(*(const char **)a, *(const char **)b);
935
+}
936
+
937
+int compare_accounts_by_id(const void *a, const void *b) {
938
+    const account_t *acc_a = (const account_t *)a;
939
+    const account_t *acc_b = (const account_t *)b;
940
+    
941
+    if (acc_a->id < acc_b->id) return -1;
942
+    if (acc_a->id > acc_b->id) return 1;
943
+    return 0;
944
+}
945
+
946
+int compare_accounts_by_name(const void *a, const void *b) {
947
+    const account_t *acc_a = (const account_t *)a;
948
+    const account_t *acc_b = (const account_t *)b;
949
+    
950
+    return strcmp(acc_a->name, acc_b->name);
951
+}
952
+
953
+/* Array utilities */
954
+
955
+void sort_accounts(account_t *accounts, size_t count, 
956
+                   int (*compare)(const void *, const void *)) {
957
+    if (accounts && count > 1 && compare) {
958
+        qsort(accounts, count, sizeof(account_t), compare);
959
+    }
960
+}
961
+
962
+account_t *find_account_in_array(account_t *accounts, size_t count, 
963
+                                 const char *identifier) {
964
+    if (!accounts || !identifier || count == 0) {
965
+        return NULL;
966
+    }
967
+    
968
+    /* Try numeric ID first */
969
+    char *endptr;
970
+    unsigned long id = strtoul(identifier, &endptr, 10);
971
+    if (*endptr == '\0') {
972
+        /* It's a number - search by ID */
973
+        for (size_t i = 0; i < count; i++) {
974
+            if (accounts[i].id == (uint32_t)id) {
975
+                return &accounts[i];
976
+            }
977
+        }
978
+    }
979
+    
980
+    /* Search by name or description */
981
+    for (size_t i = 0; i < count; i++) {
982
+        if (strstr(accounts[i].name, identifier) ||
983
+            strstr(accounts[i].description, identifier) ||
984
+            strcmp(accounts[i].email, identifier) == 0) {
985
+            return &accounts[i];
986
+        }
987
+    }
988
+    
989
+    return NULL;
990
+}
991
+
992
+/* Memory utilities */
993
+
994
+void *safe_memset(void *ptr, int value, size_t size) {
995
+    if (!ptr || size == 0) {
996
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_memset");
997
+        return NULL;
998
+    }
999
+    
1000
+    return memset(ptr, value, size);
1001
+}
1002
+
1003
+void *safe_memcpy(void *dest, const void *src, size_t size) {
1004
+    if (!dest || !src || size == 0) {
1005
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_memcpy");
1006
+        return NULL;
1007
+    }
1008
+    
1009
+    return memcpy(dest, src, size);
1010
+}
1011
+
1012
+int safe_mlock(void *ptr, size_t size) {
1013
+#if defined(__linux__)
1014
+    if (!ptr || size == 0) {
1015
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_mlock");
1016
+        return -1;
1017
+    }
1018
+    
1019
+    if (mlock(ptr, size) != 0) {
1020
+        set_system_error(ERR_SYSTEM_CALL, "Failed to lock memory");
1021
+        return -1;
1022
+    }
1023
+    
1024
+    return 0;
1025
+#else
1026
+    /* Not supported on this platform */
1027
+    (void)ptr;
1028
+    (void)size;
1029
+    return 0;
1030
+#endif
1031
+}
1032
+
1033
+int safe_munlock(void *ptr, size_t size) {
1034
+#if defined(__linux__)
1035
+    if (!ptr || size == 0) {
1036
+        set_error(ERR_INVALID_ARGS, "Invalid arguments to safe_munlock");
1037
+        return -1;
1038
+    }
1039
+    
1040
+    if (munlock(ptr, size) != 0) {
1041
+        set_system_error(ERR_SYSTEM_CALL, "Failed to unlock memory");
1042
+        return -1;
1043
+    }
1044
+    
1045
+    return 0;
1046
+#else
1047
+    /* Not supported on this platform */
1048
+    (void)ptr;
1049
+    (void)size;
1050
+    return 0;
1051
+#endif
1052
+}
1053
+
1054
+/* Cleanup utilities */
1055
+
1056
+void cleanup_temporary_files(void) {
1057
+    /* Implementation would clean up any temporary files created */
1058
+    log_debug("Cleaning up temporary files");
1059
+}
1060
+
1061
+int register_cleanup_handler(void (*handler)(void)) {
1062
+    if (!handler) {
1063
+        set_error(ERR_INVALID_ARGS, "NULL handler to register_cleanup_handler");
1064
+        return -1;
1065
+    }
1066
+    
1067
+    if (g_cleanup_handler_count >= sizeof(g_cleanup_handlers) / sizeof(g_cleanup_handlers[0])) {
1068
+        set_error(ERR_INVALID_ARGS, "Too many cleanup handlers registered");
1069
+        return -1;
1070
+    }
1071
+    
1072
+    g_cleanup_handlers[g_cleanup_handler_count++] = handler;
1073
+    return 0;
1074
+}
1075
+
1076
+/* Debug utilities */
1077
+
1078
+void dump_account(const account_t *account) {
1079
+    if (!account) {
1080
+        log_debug("Account: NULL");
1081
+        return;
1082
+    }
1083
+    
1084
+    log_debug("Account dump:");
1085
+    log_debug("  ID: %u", account->id);
1086
+    log_debug("  Name: %s", account->name);
1087
+    log_debug("  Email: %s", account->email);
1088
+    log_debug("  Description: %s", account->description);
1089
+    log_debug("  SSH enabled: %s", account->ssh_enabled ? "yes" : "no");
1090
+    log_debug("  SSH key: %s", account->ssh_key_path);
1091
+    log_debug("  GPG enabled: %s", account->gpg_enabled ? "yes" : "no");
1092
+    log_debug("  GPG signing: %s", account->gpg_signing_enabled ? "yes" : "no");
1093
+    log_debug("  GPG key: %s", account->gpg_key_id);
1094
+}
1095
+
1096
+void dump_config(const config_t *config) {
1097
+    if (!config) {
1098
+        log_debug("Config: NULL");
1099
+        return;
1100
+    }
1101
+    
1102
+    log_debug("Config dump:");
1103
+    log_debug("  Default scope: %d", config->default_scope);
1104
+    log_debug("  Config path: %s", config->config_path);
1105
+    log_debug("  Verbose: %s", config->verbose ? "yes" : "no");
1106
+    log_debug("  Dry run: %s", config->dry_run ? "yes" : "no");
1107
+    log_debug("  Color output: %s", config->color_output ? "yes" : "no");
1108
+}
1109
+
1110
+void dump_context(const gitswitch_ctx_t *ctx) {
1111
+    if (!ctx) {
1112
+        log_debug("Context: NULL");
1113
+        return;
1114
+    }
1115
+    
1116
+    log_debug("Context dump:");
1117
+    log_debug("  Account count: %zu", ctx->account_count);
1118
+    log_debug("  Current account: %s", 
1119
+              ctx->current_account ? ctx->current_account->name : "none");
1120
+    
1121
+    dump_config(&ctx->config);
1122
+    
1123
+    for (size_t i = 0; i < ctx->account_count; i++) {
1124
+        dump_account(&ctx->accounts[i]);
1125
+    }
1126
+}