/* * confirm.c -- destructive-command confirmation gate. See confirm.h for * the full contract, return-value semantics, and design rationale * (xce-test-cli-ux spec, task 2.1; Requirements 9.1-9.4, 9.6-9.9). * * IMPLEMENTATION NOTES * * - confirm_destructive() checks confirm_is_skip_set() and * xcetest_is_interactive() BEFORE touching stdin or stderr at all -- * both are pure, side-effect-free reads, so ordering between them * doesn't matter, but checking them first is what makes the "proceed * without printing or reading anything" postcondition (9.4, 9.7) hold. * * - The prompt read is exactly one fgets() call on stdin, not a loop. * This is deliberate and is the crux of Requirement 9.9: glibc's * buffered stdio does NOT internally retry a read() that returns * -1/EINTR (it propagates the failure up as fgets() returning NULL * with errno left at EINTR) -- so as long as this function itself * never loops back to retry after seeing EINTR, a real Ctrl-C during * the prompt reliably falls straight through to the decline path. A * naive wrapper that did `while (fgets(...) == NULL) { if (errno == * EINTR) continue; ... }` would defeat this entirely; no such loop * exists here. (This assumes test.c's main() installs SIGINT via * xcetest_install_sigint() -- SA_RESTART cleared -- per task 1.3; see * confirm.h's precondition note.) * * - errno is reset to 0 immediately before the fgets() call. fgets() * does not promise to clear a stale errno on success, and NULL is the * only signal we get to even look at errno, so a stale nonzero errno * from some earlier, unrelated libc call could otherwise be * misread as this call's own EINTR. */ #include #include #include #include #include "confirm.h" static int g_confirm_skip = 0; /* 0 == confirmation required (default at * process start); mirrors * g_continue_on_error's exact style * (test.c). */ void confirm_set_skip(int skip) { g_confirm_skip = skip ? 1 : 0; } int confirm_is_skip_set(void) { return g_confirm_skip; } /* Accepted spellings: "y" or "yes", case-insensitive, tolerant of * incidental leading/trailing whitespace (including the trailing * newline fgets() leaves in place). Anything else -- "n", "no", empty, * garbage -- is NOT affirmative; confirm_destructive() maps every * non-affirmative case to the same "not run" outcome (see confirm.h's * two-valued return contract). */ static int is_affirmative(const char* raw) { char norm[8]; size_t i = 0, n = 0; while (raw[i] != '\0' && isspace((unsigned char)raw[i])) i++; while (raw[i] != '\0' && !isspace((unsigned char)raw[i]) && n < sizeof(norm) - 1) { norm[n++] = (char)tolower((unsigned char)raw[i]); i++; } norm[n] = '\0'; return (strcmp(norm, "y") == 0) || (strcmp(norm, "yes") == 0); } int confirm_destructive(const char* action_description) { char buf[64]; char* line; /* Requirement 9.6/9.7: the session-scoped toggle bypasses the prompt * entirely, regardless of TTY state. Requirement 9.4: a non-interactive * session bypasses it too. Neither path touches stdin/stderr. */ if (confirm_is_skip_set() || !xcetest_is_interactive()) return 1; /* Requirement 9.8: the prompt goes to stderr, never stdout -- a * destructive command's successful result data (if any) must stay * uncontaminated by this diagnostic text. Deliberately NOT routed * through libedit's readline() (see confirm.h). */ fprintf(stderr, "About to %s. Continue? [y/N] ", action_description); fflush(stderr); errno = 0; line = fgets(buf, sizeof(buf), stdin); if (line == NULL) { /* Requirement 9.9: a real Ctrl-C during the prompt's blocking * read surfaces as EINTR here (never retried -- see the file * header) and is treated exactly like a decline, not a distinct * "error" outcome. EOF (errno left at 0, or set to something * other than EINTR by the underlying read) is likewise mapped * to decline -- see confirm.h's return-contract note on why no * third outcome exists. */ return 0; } return is_affirmative(buf) ? 1 : 0; }