source
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
6fa45645fb89058eab83497c5f9ec9040c303373- Parents
-
56b2994 - Tree
3a0df48
6fa4564
6fa45645fb89058eab83497c5f9ec9040c30337356b2994
3a0df48| Status | File | + | - |
|---|---|---|---|
| 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, §ion_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(¤t_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(¤t_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@@ -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 = ¤t_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 = §ion->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 = §ion->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 §ion->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 = §ion->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 = §ion->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 = §ion->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@@ -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(®ex, pattern, REG_EXTENDED); | |
| 689 | + if (result) return false; | |
| 690 | + | |
| 691 | + result = regexec(®ex, email, 0, NULL, 0); | |
| 692 | + regfree(®ex); | |
| 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 | +} | |