/* * Copyright (c) 2013, 2015, 2016, 2022, 2023 Jonas 'Sortie' Termansen. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * util.h * Shared Utility functions for Tix. */ #ifndef UTIL_H #define UTIL_H #define DEFAULT_GENERATION "3" extern char** environ; bool does_path_contain_dotdot(const char* path) { size_t index = 0; while ( path[index] ) { if ( path[index] == '.' && path[index+1] == '.' ) { index += 2; if ( !path[index] || path[index] == '/' ) return true; } while ( path[index] && path[index] != '/' ) index++; while ( path[index] == '/' ) index++; } return false; } bool parse_boolean(const char* str) { return strcmp(str, "true") == 0; } char* strdup_null(const char* src) { return src ? strdup(src) : NULL; } char* strdup_null_if_content(const char* src) { if ( src && !src[0] ) return NULL; return strdup_null(src); } const char* non_modify_basename(const char* path) { const char* last_slash = (char*) strrchr((char*) path, '/'); if ( !last_slash ) return path; return last_slash + 1; } typedef struct { char** strings; size_t length; size_t capacity; } string_array_t; string_array_t string_array_make(void) { string_array_t sa; sa.strings = NULL; sa.length = sa.capacity = 0; return sa; } void string_array_reset(string_array_t* sa) { for ( size_t i = 0; i < sa->length; i++ ) free(sa->strings[i]); free(sa->strings); *sa = string_array_make(); } bool string_array_append(string_array_t* sa, const char* str) { if ( sa->length == sa->capacity ) { size_t new_capacity = sa->capacity ? sa->capacity * 2 : 8; size_t new_size = sizeof(char*) * new_capacity; char** new_strings = (char**) realloc(sa->strings, new_size); if ( !new_strings ) return false; sa->strings = new_strings; sa->capacity = new_capacity; } char* copy = strdup_null(str); if ( str && !copy ) return false; sa->strings[sa->length++] = copy; return true; } bool string_array_append_token_string(string_array_t* sa, const char* str) { while ( *str ) { if ( isspace((unsigned char) *str) ) { str++; continue; } size_t input_length = 0; size_t output_length = 0; bool quoted = false; bool escaped = false; while ( str[input_length] && (escaped || quoted || !isspace((unsigned char) str[input_length])) ) { if ( !escaped && str[input_length] == '\\' ) escaped = true; else if ( !escaped && str[input_length] == '"' ) quoted = !quoted; else escaped = false, output_length++; input_length++; } if ( quoted || escaped ) return false; char* output = (char*) malloc(sizeof(char) * (output_length+1)); if ( !output ) return false; input_length = 0; output_length = 0; quoted = false; escaped = false; while ( str[input_length] && (escaped || quoted || !isspace((unsigned char) str[input_length])) ) { if ( !escaped && str[input_length] == '\\' ) escaped = true; else if ( !escaped && str[input_length] == '"' ) quoted = !quoted; else if ( escaped && str[input_length] == 'a' ) escaped = false, output[output_length++] = '\a'; else if ( escaped && str[input_length] == 'b' ) escaped = false, output[output_length++] = '\b'; else if ( escaped && str[input_length] == 'e' ) escaped = false, output[output_length++] = '\e'; else if ( escaped && str[input_length] == 'f' ) escaped = false, output[output_length++] = '\f'; else if ( escaped && str[input_length] == 'n' ) escaped = false, output[output_length++] = '\n'; else if ( escaped && str[input_length] == 'r' ) escaped = false, output[output_length++] = '\r'; else if ( escaped && str[input_length] == 't' ) escaped = false, output[output_length++] = '\t'; else if ( escaped && str[input_length] == 'v' ) escaped = false, output[output_length++] = '\v'; else escaped = false, output[output_length++] = str[input_length]; input_length++; } output[output_length] = '\0'; if ( !string_array_append(sa, output) ) { free(output); return false; } free(output); str += input_length; } return true; } bool is_token_string_special_character(char c) { return isspace((unsigned char) c) || c == '"' || c == '\\'; } char* token_string_of_string_array(const string_array_t* sa) { size_t result_length = 0; for ( size_t i = 0; i < sa->length; i++ ) { if ( i ) result_length++; for ( size_t n = 0; sa->strings[i][n]; n++ ) { if ( is_token_string_special_character(sa->strings[i][n]) ) result_length++; result_length++; } } char* result = (char*) malloc(sizeof(char) * (result_length + 1)); if ( !result ) return NULL; result_length = 0; for ( size_t i = 0; i < sa->length; i++ ) { if ( i ) result[result_length++] = ' '; for ( size_t n = 0; sa->strings[i][n]; n++ ) { if ( is_token_string_special_character(sa->strings[i][n]) ) result[result_length++] = '\\'; result[result_length++] = sa->strings[i][n]; } } result[result_length] = '\0'; return result; } void string_array_append_file(string_array_t* sa, FILE* fp) { char* entry = NULL; size_t entry_size = 0; ssize_t entry_length; while ( 0 < (entry_length = getline(&entry, &entry_size, fp)) ) { if ( entry[entry_length-1] == '\n' ) entry[--entry_length] = '\0'; string_array_append(sa, entry); } free(entry); assert(!ferror(fp)); } bool string_array_append_file_path(string_array_t* sa, const char* path) { FILE* fp = fopen(path, "r"); if ( !fp ) return false; string_array_append_file(sa, fp); fclose(fp); return true; } size_t string_array_find(string_array_t* sa, const char* str) { for ( size_t i = 0; i < sa->length; i++ ) if ( !strcmp(sa->strings[i], str) ) return i; return SIZE_MAX; } bool string_array_contains(string_array_t* sa, const char* str) { return string_array_find(sa, str) != SIZE_MAX; } size_t dictionary_lookup_index(string_array_t* sa, const char* key) { size_t keylen = strlen(key); for ( size_t i = 0; i < sa->length; i++ ) { const char* entry = sa->strings[i]; if ( strncmp(key, entry, keylen) != 0 ) continue; if ( entry[keylen] != '=' ) continue; return i; } return SIZE_MAX; } const char* dictionary_get_entry(string_array_t* sa, const char* key) { size_t index = dictionary_lookup_index(sa, key); if ( index == SIZE_MAX ) return NULL; return sa->strings[index]; } const char* dictionary_get_def(string_array_t* sa, const char* key, const char* def) { size_t keylen = strlen(key); const char* entry = dictionary_get_entry(sa, key); return entry ? entry + keylen + 1 : def; } const char* dictionary_get(string_array_t* sa, const char* key) { return dictionary_get_def(sa, key, NULL); } void dictionary_normalize_entry(char* entry) { bool key = true; size_t input_off, output_off; for ( input_off = output_off = 0; entry[input_off]; input_off++ ) { if ( key && isspace((unsigned char) entry[input_off]) ) continue; if ( key && (entry[input_off] == '=' || entry[input_off] == '#') ) key = false; entry[output_off++] = entry[input_off]; } entry[output_off] = '\0'; } bool is_identifier_char(char c) { return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '_'; } // 0 on success, -1 on error, -2 on syntax error. int variables_parse_fp(string_array_t* sa, const char* line, FILE* fp) { size_t i = 0; while ( isspace((unsigned char) line[i]) ) i++; if ( !line[i] || line[i] == '#' ) return 0; if ( line[i] == '=' ) return -2; size_t keylen = 0; while ( line[i + keylen] && line[i + keylen] != '=' && line[i + keylen] != '#' && !isspace((unsigned char) line[i + keylen]) ) { if ( fputc((unsigned char) line[i + keylen++], fp) == EOF ) return -1; } i += keylen; if ( line[i++] != '=' ) return -2; fputc('=', fp); bool escaped = false; bool singly_quote = false; bool doubly_quote = false; while ( true ) { unsigned char c = (unsigned char) line[i++]; if ( !c ) { i--; break; } else if ( !escaped && !singly_quote && c == '\\' ) escaped = true; else if ( !escaped && !doubly_quote && c == '\'' ) singly_quote = !singly_quote; else if ( !escaped && !singly_quote && c == '"' ) doubly_quote = !doubly_quote; else if ( !escaped && !singly_quote && c == '$' && (is_identifier_char(line[i]) || line[i] == '{') ) { size_t start = i; size_t keylen = 0; if ( line[start] == '{' ) { start++; while ( line[start + keylen] && line[start + keylen] != '}' ) keylen++; if ( !keylen ) return -2; if ( line[start + keylen] != '}' ) return -2; i = start + keylen + 1; } else { while ( line[start + keylen] && is_identifier_char(line[start + keylen]) ) keylen++; i = start + keylen; } const char* key = line + start; const char* value = NULL; for ( size_t n = 0; !value && n < sa->length; n++ ) { const char* entry = sa->strings[n]; if ( strncmp(key, entry, keylen) != 0 ) continue; if ( entry[keylen] != '=' ) continue; value = entry + keylen + 1; } if ( !value ) return -2; size_t length = strlen(value); if ( fwrite(value, 1, length, fp) != length ) return -1; } else { if ( !escaped && !singly_quote && !doubly_quote && isspace(c) ) { i--; break; } if ( fputc(c, fp) == EOF ) return -1; escaped = false; } } while ( isspace((unsigned char) line[i]) ) i++; if ( line[i] && line[i] != '#' ) return -2; return 1; } int variables_parse(string_array_t* sa, const char* line, char** out_ptr) { size_t out_size; FILE* fp = open_memstream(out_ptr, &out_size); if ( !fp ) return -1; int result = variables_parse_fp(sa, line, fp); if ( fclose(fp) == EOF ) result = -1; if ( result <= 0 ) free(*out_ptr); return result; } int variables_append_file(string_array_t* sa, FILE* fp) { char* line = NULL; size_t line_size = 0; ssize_t line_length; while ( 0 < (line_length = getline(&line, &line_size, fp)) ) { if ( line[line_length-1] == '\n' ) line[--line_length] = '\0'; char* entry; int result = variables_parse(sa, line, &entry); if ( result == 0 ) continue; if ( result < 0 ) { free(line); return result; } if ( !string_array_append(sa, entry) ) { free(entry); free(line); return -1; } free(entry); } free(line); if ( ferror(fp) ) return -1; return 0; } int variables_append_file_path(string_array_t* sa, const char* path) { FILE* fp = fopen(path, "r"); if ( !fp ) return -1; int result = variables_append_file(sa, fp); fclose(fp); return result; } __attribute__((format(printf, 1, 2))) char* print_string(const char* format, ...) { va_list ap; va_start(ap, format); char* ret; if ( vasprintf(&ret, format, ap) < 0 ) ret = NULL; va_end(ap); return ret; } char* read_single_line(FILE* fp) { char* ret = NULL; size_t ret_size = 0; ssize_t ret_len = getline(&ret, &ret_size, fp); if ( ret_len < 0 ) { free(ret); return NULL; } if ( ret[ret_len-1] == '\n' ) ret[--ret_len] = '\0'; return ret; } pid_t fork_or_death(void) { pid_t child_pid = fork(); if ( child_pid < 0 ) err(1, "fork"); return child_pid; } void waitpid_or_death_def(pid_t child_pid, bool die_on_error) { int status; waitpid(child_pid, &status, 0); if ( die_on_error ) { if ( WIFEXITED(status) && WEXITSTATUS(status) != 0 ) exit(WEXITSTATUS(status)); if ( WIFSIGNALED(status) ) errx(128 + WTERMSIG(status), "child with pid %ji was killed by " "signal %i (%s).", (intmax_t) child_pid, WTERMSIG(status), strsignal(WTERMSIG(status))); } } void waitpid_or_death(pid_t child_pid) { return waitpid_or_death_def(child_pid, true); } bool fork_and_wait_or_death_def(bool die_on_error) { pid_t child_pid = fork_or_death(); if ( !child_pid ) return true; waitpid_or_death_def(child_pid, die_on_error); return false; } bool fork_and_wait_or_death(void) { return fork_and_wait_or_death_def(true); } const char* getenv_def(const char* var, const char* def) { const char* ret = getenv(var); return ret ? ret : def; } int mkdir_p(const char* path, mode_t mode) { int saved_errno = errno; if ( !mkdir(path, mode) ) return 0; if ( errno == ENOENT ) { char* prev = strdup(path); if ( !prev ) return -1; int status = mkdir_p(dirname(prev), mode | 0500); free(prev); if ( status < 0 ) return -1; errno = saved_errno; if ( !mkdir(path, mode) ) return 0; } if ( errno == EEXIST ) return errno = saved_errno, 0; return -1; } static void compact_arguments(int* argc, char*** argv) { for ( int i = 0; i < *argc; i++ ) { while ( i < *argc && !(*argv)[i] ) { for ( int n = i; n < *argc; n++ ) (*argv)[n] = (*argv)[n+1]; (*argc)--; } } } char* GetBuildTriplet(void) { #if defined(__sortix__) && defined(__i386__) #if defined(__i686__) return "i686-sortix"; #elif defined(__i586__) return "i586-sortix"; #elif defined(__i486__) return "i486-sortix"; #else return "i386-sortix"; #endif #elif defined(__sortix__) && defined(__x86_64__) return strdup("x86_64-sortix"); #elif defined(__sortix__) #warning "Add your build triplet here" #endif FILE* fp = popen("cc -dumpmachine", "r"); if ( !fp ) return NULL; char* ret = read_single_line(fp); pclose(fp); return ret; } bool get_option_variable(const char* option, char** varptr, const char* arg, int argc, char** argv, int* ip, const char* argv0) { size_t option_len = strlen(option); if ( strncmp(option, arg, option_len) != 0 ) return false; if ( arg[option_len] == '=' ) { *varptr = strdup(arg + option_len + 1); return true; } if ( arg[option_len] != '\0' ) return false; if ( *ip + 1 == argc ) { fprintf(stderr, "%s: expected operand after `%s'\n", argv0, option); exit(1); } *varptr = strdup(argv[++*ip]), argv[*ip] = NULL; return true; } char* join_paths(const char* a, const char* b) { size_t a_len = strlen(a); bool has_slash = (a_len && a[a_len-1] == '/') || b[0] == '/'; return has_slash ? print_string("%s%s", a, b) : print_string("%s/%s", a, b); } bool IsFile(const char* path) { struct stat st; return stat(path, &st) == 0 && S_ISREG(st.st_mode); } bool IsDirectory(const char* path) { struct stat st; return stat(path, &st) == 0 && (S_ISDIR(st.st_mode) || (errno = ENOTDIR, false)); } size_t count_tar_components(const char* path) { if ( !*path ) return 0; size_t slashes = 1; for ( size_t i = 0; path[i]; i++ ) if ( path[i] == '/' ) slashes++; return slashes; } #define GET_OPTION_VARIABLE(str, varptr) \ get_option_variable(str, varptr, arg, argc, argv, &i, argv0) // TODO: This is a bit inefficient but doing it otherwise would involve looking // through the error stream for messages such as "file not found", which // can be hard to distinguish from the common "oh no, an error occured" // case in which we need to abort as well. bool TarContainsFile(const char* archive, const char* file) { int pipes[2]; if ( pipe(pipes) ) err(1, "pipe"); pid_t tar_pid = fork_or_death(); if ( !tar_pid ) { dup2(pipes[1], 1); close(pipes[1]); close(pipes[0]); const char* cmd_argv[] = { "tar", "--list", "--file", archive, NULL }; execvp(cmd_argv[0], (char* const*) cmd_argv); err(127, "%s", cmd_argv[0]); } close(pipes[1]); FILE* fp = fdopen(pipes[0], "r"); char* line = NULL; size_t line_size = 0; ssize_t line_len; bool ret = false; while ( 0 < (line_len = getline(&line, &line_size, fp)) ) { if ( line[line_len-1] == '\n' ) line[--line_len] = '\0'; if ( strcmp(line, file) == 0 ) { ret = true; #if !defined(__sortix__) kill(tar_pid, SIGPIPE); break; #endif } } free(line); if ( ferror(fp) ) err(1, "getline: tar"); fclose(fp); int tar_exit_status; waitpid(tar_pid, &tar_exit_status, 0); bool sigpiped = WIFSIGNALED(tar_exit_status) && WTERMSIG(tar_exit_status) == SIGPIPE; bool errored = !WIFEXITED(tar_exit_status) || WEXITSTATUS(tar_exit_status) != 0; if ( errored && !sigpiped ) { errx(1, "Unable to list contents of `%s'.", archive); exit(WEXITSTATUS(tar_exit_status)); } return ret; } void TarExtractFileToFD(const char* archive, const char* file, int fd) { pid_t tar_pid = fork_or_death(); if ( !tar_pid ) { if ( dup2(fd, 1) < 0 ) { warn("dup2"); _exit(127); } close(fd); const char* cmd_argv[] = { "tar", "--to-stdout", "--extract", "--file", archive, "--", file, NULL }; execvp(cmd_argv[0], (char* const*) cmd_argv); warn("%s", cmd_argv[0]); _exit(127); } int tar_exit_status; waitpid(tar_pid, &tar_exit_status, 0); if ( !WIFEXITED(tar_exit_status) || WEXITSTATUS(tar_exit_status) != 0 ) { errx(1, "Unable to extract `%s/%s'", archive, file); exit(WEXITSTATUS(tar_exit_status)); } } FILE* TarOpenFile(const char* archive, const char* file) { FILE* fp = tmpfile(); if ( !fp ) err(1, "tmpfile"); TarExtractFileToFD(archive, file, fileno(fp)); if ( fseeko(fp, 0, SEEK_SET) < 0 ) err(1, "fseeko(tmpfile(), 0, SEEK_SET)"); return fp; } void TarIndexToFD(const char* archive, int fd) { pid_t tar_pid = fork_or_death(); if ( !tar_pid ) { if ( dup2(fd, 1) < 0 ) { warn("dup2"); _exit(127); } close(fd); const char* cmd_argv[] = { "tar", "--list", "--file", archive, NULL }; execvp(cmd_argv[0], (char* const*) cmd_argv); warn("%s", cmd_argv[0]); _exit(127); } int tar_exit_status; waitpid(tar_pid, &tar_exit_status, 0); if ( !WIFEXITED(tar_exit_status) || WEXITSTATUS(tar_exit_status) != 0 ) { errx(1, "Unable to list contents of `%s'", archive); exit(WEXITSTATUS(tar_exit_status)); } } FILE* TarOpenIndex(const char* archive) { FILE* fp = tmpfile(); if ( !fp ) err(1, "tmpfile"); TarIndexToFD(archive, fileno(fp)); if ( fseeko(fp, 0, SEEK_SET) < 0 ) err(1, "fseeko(tmpfile(), 0, SEEK_SET)"); return fp; } const char* VerifyInfoVariable(string_array_t* info, const char* var, const char* path) { const char* ret = dictionary_get(info, var); if ( !ret ) errx(1, "error: `%s': no `%s' variable declared", path, var); return ret; } void VerifyTixInformation(string_array_t* tixinfo, const char* tix_path) { const char* tix_version = dictionary_get(tixinfo, "tix.version"); if ( !tix_version ) errx(1, "error: `%s': no `tix.version' variable declared", tix_path); if ( atoi(tix_version) != 1 ) errx(1, "error: `%s': tix version `%s' not supported", tix_path, tix_version); const char* tix_class = dictionary_get(tixinfo, "tix.class"); if ( !tix_class ) errx(1, "error: `%s': no `tix.class' variable declared", tix_path); if ( !strcmp(tix_class, "srctix") ) errx(1, "error: `%s': this object is a source tix and needs to be " "compiled into a binary tix prior to installation.", tix_path); if ( strcmp(tix_class, "tix") ) errx(1, "error: `%s': tix class `%s' is not `tix': this object is " "not suitable for installation.", tix_path, tix_class); if ( !(dictionary_get(tixinfo, "tix.platform")) ) errx(1, "error: `%s': no `tix.platform' variable declared", tix_path); if ( !(dictionary_get(tixinfo, "pkg.name")) ) errx(1, "error: `%s': no `pkg.name' variable declared", tix_path); } bool IsCollectionPrefixRatherThanCommand(const char* arg) { return strchr(arg, '/') || !strcmp(arg, ".") || !strcmp(arg, ".."); } void ParseOptionalCommandLineCollectionPrefix(char** collection, int* argcp, char*** argvp) { if ( 2 <= *argcp && IsCollectionPrefixRatherThanCommand((*argvp)[1]) ) { if ( !*collection ) { free(*collection); *collection = strdup((*argvp)[1]); } (*argvp)[1] = NULL; compact_arguments(argcp, argvp); } else if ( !*collection ) { *collection = strdup("/"); } } void VerifyCommandLineCollection(char** collection) { if ( !*collection ) errx(1, "error: you need to specify which tix collection to administer " "using --collection or giving the prefix as the first " "argument."); if ( !**collection ) { free(*collection); *collection = strdup("/"); } char* collection_rel = *collection; if ( !(*collection = realpath(collection_rel, NULL)) ) err(1, "realpath: %s", collection_rel); free(collection_rel); } void VerifyTixCollectionConfiguration(string_array_t* info, const char* path) { // TODO: After releasing Sortix 1.1, remove generation 2 compatibility. const char* tix_version = dictionary_get(info, "tix.version"); if ( tix_version ) { if ( !tix_version ) errx(1, "error: `%s': no `tix.version' variable declared", path); if ( atoi(tix_version) != 1 ) errx(1, "error: `%s': tix version `%s' not supported", path, tix_version); const char* tix_class = dictionary_get(info, "tix.class"); if ( !tix_class ) errx(1, "error: `%s': no `tix.class' variable declared", path); if ( strcmp(tix_class, "collection") != 0 ) errx(1, "error: `%s': error: unexpected tix class `%s'.", path, tix_class); if ( !(dictionary_get(info, "collection.prefix")) ) errx(1, "error: `%s': no `collection.prefix' variable declared", path); if ( !(dictionary_get(info, "collection.platform")) ) errx(1, "error: `%s': no `collection.platform' variable declared", path); return; } const char* version = dictionary_get(info, "TIX_COLLECTION_VERSION"); if ( !version ) errx(1, "%s: Mandatory TIX_COLLECTION_VERSION was not set", path); if ( atoi(version) != 3 ) errx(1, "%s: Unsupported: TIX_COLLECTION_VERSION: %s", path, version); if ( !(dictionary_get(info, "PREFIX")) ) errx(1, "%s: Mandatory PREFIX was not set", path); if ( !(dictionary_get(info, "PLATFORM")) ) errx(1, "%s: Mandatory PLATFORM was not set", path); } static pid_t original_pid; char* tmp_root = NULL; static void cleanup_tmp(void) { if ( original_pid != getpid() ) return; if ( !tmp_root ) return; pid_t pid = fork(); if ( pid < 0 ) { warn("fork"); return; } if ( pid == 0 ) { const char* cmd_argv[] = { "rm", "-rf", "--", (const char*) tmp_root, NULL, }; execvp(cmd_argv[0], (char* const*) cmd_argv); warn("%s", cmd_argv[0]); _exit(127); } int code; waitpid(pid, &code, 0); free(tmp_root); tmp_root = NULL; } void initialize_tmp(const char* tmp, const char* purpose) { if ( tmp_root ) errx(1, "error: initialize_tmp called twice"); if ( asprintf(&tmp_root, "%s/%s.XXXXXX", tmp, purpose) < 0 ) err(1, "error: asprintf"); if ( !mkdtemp(tmp_root) ) err(1, "mkdtemp: `%s'", tmp_root); original_pid = getpid(); if ( atexit(cleanup_tmp) != 0 ) { int errnum = errno; cleanup_tmp(); errno = errnum; err(1, "atexit"); } } mode_t get_umask_value(void) { mode_t result = umask(0); umask(result); return result; } int fchmod_plus_x(int fd) { struct stat st; if ( fstat(fd, &st) != 0 ) return -1; mode_t new_mode = st.st_mode | (0111 & ~get_umask_value()); if ( fchmod(fd, new_mode) != 0 ) return -1; return 0; } void fprint_shell_variable_assignment(FILE* fp, const char* variable, const char* value) { if ( value ) { fprintf(fp, "export %s='", variable); for ( size_t i = 0; value[i]; i++ ) if ( value[i] == '\'' ) fprintf(fp, "'\\''"); else fputc(value[i], fp); fprintf(fp, "'\n"); } else { fprintf(fp, "unset %s\n", variable); } } bool needs_single_quote(const char* string) { for ( size_t i = 0; string[i]; i++ ) { char c = string[i]; if ( !(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '/' || c == '_' || c == '.' || c == '+' || c == ':' || c == '%' || c == '$' || c == '{' || c == '}' || c == '-') ) return true; } return false; } void fwrite_variable(FILE* fp, const char* key, const char* value) { fprintf(fp, "%s=", key); if ( !needs_single_quote(value) ) fprintf(fp, "%s\n", value); else { fputc('\'', fp); for ( size_t i = 0; value[i]; i++ ) if ( value[i] == '\'' ) fprintf(fp, "'\\''"); else fputc(value[i], fp); fputs("'\n", fp); } } bool is_success_exit_status(int status) { return WIFEXITED(status) && WEXITSTATUS(status) == 0; } __attribute__((noreturn)) bool exit_like_exit_status(int status) { if ( WIFEXITED(status) ) exit(WEXITSTATUS(status)); if ( WIFSIGNALED(status) ) exit(128 + WTERMSIG(status)); exit(1); } enum recovery_state { RECOVERY_STATE_NONE, RECOVERY_STATE_PRINT_COMMAND, RECOVERY_STATE_RUN_SHELL, }; enum recovery_state recovery_configure_state_def(bool set, enum recovery_state to_what) { static enum recovery_state recovery_state = RECOVERY_STATE_NONE; if ( set ) recovery_state = to_what; return recovery_state; } enum recovery_state recovery_configure_state(bool set) { return recovery_configure_state_def(set, RECOVERY_STATE_NONE); } bool recovery_print_attempted_execution(void) { pid_t child_pid = fork(); if ( child_pid < 0 ) return false; if ( !child_pid ) { // Redirect stdout and stderr to /dev/null to prevent duplicate errors. // The recovery_execvp function below will automatically re-open the // terminal device when it needs to talk to the terminal. int dev_null = open("/dev/null", O_WRONLY); if ( 0 <= dev_null ) { dup2(dev_null, 1); dup2(dev_null, 2); close(dev_null); } recovery_configure_state_def(true, RECOVERY_STATE_PRINT_COMMAND); return true; } int status; waitpid(child_pid, &status, 0); return false; } bool recovery_run_shell(void) { pid_t child_pid = fork(); if ( child_pid < 0 ) return false; if ( !child_pid ) { recovery_configure_state_def(true, RECOVERY_STATE_RUN_SHELL); return true; } int status; waitpid(child_pid, &status, 0); return false; } int recovery_execvp(const char* path, char* const* argv) { if ( recovery_configure_state(false) == RECOVERY_STATE_NONE ) return execvp(path, argv); // Make sure that stdout and stderr go to an interactive terminal. if ( !isatty(1) ) { int dev_tty = open("/dev/tty", O_WRONLY); if ( 0 <= dev_tty ) { dup2(dev_tty, 1); dup2(dev_tty, 2); close(dev_tty); } } printf("Attempted command was: "); for ( int i = 0; argv[i]; i++ ) { if ( i ) putchar(' '); putchar('\''); for ( size_t n = 0; argv[i][n]; n++ ) if ( argv[i][n] == '\'' ) printf("'\\''"); else putchar(argv[i][n]); putchar('\''); } printf("\n"); fflush(stdout); if ( recovery_configure_state(false) == RECOVERY_STATE_PRINT_COMMAND ) _exit(0); const char* cmd_argv[] = { getenv_def("SHELL", "sh"), NULL }; execvp(cmd_argv[0], (char* const*) cmd_argv); err(127, "%s", cmd_argv[0]); } bool fork_and_wait_or_recovery(void) { int default_selection = 1; while ( true ) { pid_t child_pid = fork_or_death(); if ( !child_pid ) return true; int status; waitpid(child_pid, &status, 0); if ( is_success_exit_status(status) ) return false; if ( WIFEXITED(status) ) warnx("child with pid %ju exited with status %i.", (intmax_t) child_pid, WEXITSTATUS(status)); else if ( WIFSIGNALED(status) ) warnx("child with pid %ji was killed by signal %i (%s).", (intmax_t) child_pid, WTERMSIG(status), strsignal(WTERMSIG(status))); else warnx("child with pid %ji exited in an unusual manner (%i).", (intmax_t) child_pid, status); if ( recovery_print_attempted_execution() ) return true; if ( !isatty(0) ) exit_like_exit_status(status); FILE* output = fopen("/dev/tty", "we"); if ( !output ) exit_like_exit_status(status); retry_ask_recovery_method: fprintf(output, "\n"); fprintf(output, "1. Abort\n"); fprintf(output, "2. Try again\n"); fprintf(output, "3. Pretend command was successful\n"); fprintf(output, "4. Run $SHELL -i to investigate\n"); fprintf(output, "5. Dump environment\n"); fprintf(output, "\n"); fprintf(output, "Please choose one: [%i] ", default_selection); fflush(output); char* input = read_single_line(stdin); if ( !input ) { fprintf(output, "\n"); fclose(output); warn("stdin"); exit_like_exit_status(status); } int selection = default_selection; if ( input[0] ) { char* input_end; selection = (int) strtol(input, &input_end, 0); if ( *input_end ) { warnx("error: `%s' is not an allowed choice", input); goto retry_ask_recovery_method; } if ( 5 < selection ) { warnx("error: `%i' is not an allowed choice", selection); goto retry_ask_recovery_method; } } if ( selection == 1 ) exit_like_exit_status(status); if ( selection == 2 ) { fprintf(output, "\nTrying to execute command again.\n\n"); fclose(output); continue; } if ( selection == 3 ) { fprintf(output, "\nPretending the command executed successfully.\n\n"); fclose(output); return false; } if ( selection == 4 ) { fprintf(output, "\nDropping you to a recovery shell, type `exit' " "when you are done.\n\n"); if ( recovery_run_shell() ) return true; } if ( selection == 5 ) { for ( size_t i = 0; environ[i]; i++ ) fprintf(output, "%s\n", environ[i]); goto retry_ask_recovery_method; } default_selection = 2; goto retry_ask_recovery_method; } } #endif