From 10ca24a2f13403d24482cefe634fd3cec419ca17 Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Sat, 8 Feb 2025 19:43:06 +0100 Subject: Split the code into more functions and add documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- sshexec.c | 488 +++++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 342 insertions(+), 146 deletions(-) diff --git a/sshexec.c b/sshexec.c index aca9ec9..4074795 100644 --- a/sshexec.c +++ b/sshexec.c @@ -9,13 +9,29 @@ #include +/** + * The name of the process + */ static const char *argv0 = "sshexec"; + +/** + * Whether the process is sshcd(1), + * otherwise it's sshexec(1) + */ static int is_sshcd = 0; +/** + * Print an error message and exit + * + * @param ... Message format string and arguments + */ #define exitf(...) (fprintf(stderr, __VA_ARGS__), exit(255)) +/** + * Print usage synopsis and exit + */ static void usage(void) { @@ -27,8 +43,19 @@ usage(void) } +/** + * File descriptor redirection + */ struct redirection { + /** + * First part of the redirection string, shall not be escaped + */ const char *asis; + + /** + * Second part of the redirection string, shall be escaped; + * or `NULL` if the entire string is contained in `.asis` + */ const char *escape; }; @@ -52,55 +79,143 @@ static const enum optclass { #undef X }; + +/** + * Command being constructor for ssh(1) + */ static char *command = NULL; + +/** + * The number of bytes allocated to `command` + */ static size_t command_size = 0; + +/** + * The number of bytes written to `command` + */ static size_t command_len = 0; -#define build_command(DATA, N)\ +/** + * The directory to use as the remote working directory + */ +static const char *dir = NULL; + +/** + * The ssh command to use + */ +static const char *ssh = NULL; + +/** + * Whether the command shall fail on failure to + * set remote working directory + */ +static int strict_cd; + +/** + * The string used to mark the next argument + * for unescaped inclusion in the command string + */ +static const char *asis = NULL; + +/** + * The number of times `asis` may be recognised + */ +static unsigned long long int nasis = ULLONG_MAX; + +/** + * File redirections to apply after directory switch + */ +static struct redirection *redirections = NULL; + +/** + * The number of elements in `redirections` + */ +static size_t nredirections = 0; + +/** + * The destination command line operand + */ +static char *destination; + + +/** + * Add text to `command` + * + * @param TEXT:const char * The text to add to `command` + * @param N:size_t The number of bytes to write + */ +#define build_command(TEXT, N)\ do {\ build_command_reserve(N);\ - memcpy(&command[command_len], (DATA), (N));\ + memcpy(&command[command_len], (TEXT), (N));\ command_len += (N);\ } while (0) +/** + * Add text to `command` + * + * @param TEXT:const char * The text to add to `command` + */ #define build_command_asis(S)\ build_command(S, strlen(S)) +/** + * NUL-byte terminate `command` + */ #define finalise_command()\ build_command("", 1) +/** + * Allocate space for `command` + * + * @param n The number of additional bytes (from what has currently + * been written, rather than currently allocated) `command` + * should have room for + */ static void build_command_reserve(size_t n) { - if (n > command_size - command_len) { - if (n < 512) - n = 512; - if (command_len + n > SIZE_MAX) - exitf("%s: could not allocate enough memory\n", argv0); - command_size = command_len + n; - command = realloc(command, command_size); - if (!command) - exitf("%s: could not allocate enough memory\n", argv0); - } + if (n <= command_size - command_len) + return; + + if (n < 512) + n = 512; + if (command_len + n > SIZE_MAX) + exitf("%s: could not allocate enough memory\n", argv0); + command_size = command_len + n; + command = realloc(command, command_size); + if (!command) + exitf("%s: could not allocate enough memory\n", argv0); } +/** + * Escape a string and add it to `command` + * + * @param arg The string to escape and add + */ static void build_command_escape(const char *arg) { size_t n = 0; + + /* Quote empty string */ if (!*arg) { build_command_asis("''"); return; } + + /* If the string only contains safe characters, add it would escaping */ while (isalnum(arg[n]) || arg[n] == '_' || arg[n] == '/') n += 1; if (!arg[n]) { build_command(arg, n); return; } + + /* Escape string, using quoted printf(1) statement */ build_command_asis("\"$(printf '"); goto start; while (*arg) { @@ -125,39 +240,124 @@ build_command_escape(const char *arg) } } build_command_asis("\\n')\""); + /* TODO sh(1) may remove all, not just the last LF */ } -int -main(int argc_unused, char *argv[]) +/** + * Construct `command` + * + * @param argv The command to execute remotely and its arguments (`NULL` terminated) + */ +static void +construct_command(char *argv[]) { - const char *dir = NULL; - const char *ssh = NULL; - const char *cd = NULL; - const char *asis = NULL; - const char *nasis_str = NULL; - struct redirection *redirections = NULL; - size_t nredirections = 0; - char *destination; - char *dest_dir_delim; - char **opts, *p; - size_t nopts; - enum optclass class; - const char *arg; - char opt; - const char **args; + int next_asis = 0; size_t i; - int strict_cd; - unsigned long long int nasis = ULLONG_MAX; - int next_asis; - (void) argc_unused; + /* Change directory */ + if (dir) { + build_command_asis("cd -- "); + build_command_escape(dir); + build_command_asis(strict_cd ? " && " : " ; "); + } - if (*argv) - argv0 = *argv++; + /* Execute command */ + if (is_sshcd) { + build_command_asis("exec \"$SHELL\" -l"); + goto command_constructed; + } + if (asis) + build_command_asis("( env --"); + else + build_command_asis("exec env --"); + for (; *argv; argv++) { + if (asis && nasis && !strcmp(*argv, asis)) { + nasis -= 1U; + next_asis = 1; + } else { + build_command_asis(" "); + if (next_asis) { + next_asis = 0; + build_command_asis(*argv); + } else { + build_command_escape(*argv); + } + } + } + if (asis) + build_command_asis(" )"); - p = strrchr(argv0, '/'); - is_sshcd = !strcmp(p ? &p[1] : argv0, "sshcd"); + /* Set redirections */ + for (i = 0; i < nredirections; i++) { + build_command_asis(" "); + build_command_asis(redirections[i].asis); + if (redirections[i].escape) { + build_command_asis(" "); + build_command_escape(redirections[i].escape); + } + } + + /* NUL-terminate `command` */ +command_constructed: + finalise_command(); +} + + +/** + * Replace "sshexec://" prefix in `destination` + * with "ssh://" and remove the directory + * component and, if it present, store the + * directory to `directory` + */ +static void +extract_directory_from_destination(void) +{ + char *dest_dir_delim; + if (!is_sshcd && !strncmp(destination, "sshexec://", sizeof("sshexec://") - 1U)) { + memmove(&destination[sizeof("ssh") - 1U], + &destination[sizeof("sshexec") - 1U], + strlen(&destination[sizeof("sshexec") - 1U]) + 1U); + goto using_ssh_prefix; + } else if (is_sshcd && !strncmp(destination, "sshcd://", sizeof("sshcd://") - 1U)) { + memmove(&destination[sizeof("ssh") - 1U], + &destination[sizeof("sshcd") - 1U], + strlen(&destination[sizeof("sshcd") - 1U]) + 1U); + goto using_ssh_prefix; + } else if (!strncmp(destination, "ssh://", sizeof("ssh://") - 1U)) { + using_ssh_prefix: + dest_dir_delim = strchr(&destination[sizeof("ssh://")], '/'); + } else { + dest_dir_delim = strchr(destination, ':'); + } + if (dest_dir_delim) { + if (dir) + exitf("%s: directory specified both in 'dir' option and in destination operand\n", argv0); + *dest_dir_delim++ = '\0'; + dir = dest_dir_delim; + } +} + + +/** + * Read and parse the sshexec options + * + * @param argv The arguments from the command line after + * the zeroth argument + * @return `argv` offset to skip pass any sshexec options + */ +static char ** +parse_sshexec_options(char *argv[]) +{ + const char *cd = NULL; + const char *nasis_str = NULL; + char *p; + + /* Check that we have any arguments to parse, and that we + * have the "{" mark the beginning of sshexec options. */ + if (!*argv || strcmp(*argv, "{")) + goto end_of_options; + argv++; #define STORE_OPT(VARP, OPT)\ if (!strncmp(*argv, OPT"=", sizeof(OPT))) {\ @@ -166,59 +366,66 @@ main(int argc_unused, char *argv[]) continue;\ } - if (*argv && !strcmp(*argv, "{")) { - argv++; - for (; *argv && strcmp(*argv, "}"); argv++) { - STORE_OPT(&ssh, "ssh") - STORE_OPT(&cd, "cd") + /* Get options */ + for (; *argv && strcmp(*argv, "}"); argv++) { + /* Options recognised in both sshexec(1) and sshcd(1) */ + STORE_OPT(&ssh, "ssh") + STORE_OPT(&cd, "cd") - if (is_sshcd) - usage(); - - STORE_OPT(&dir, "dir") - STORE_OPT(&asis, "asis") - STORE_OPT(&nasis_str, "nasis") + /* The rest of the options only recognised in sshexec(1) */ + if (is_sshcd) + usage(); - p = *argv; - while (isdigit(*p)) + /* Normal options */ + STORE_OPT(&dir, "dir") + STORE_OPT(&asis, "asis") + STORE_OPT(&nasis_str, "nasis") + + /* File descriptor redirections */ + p = *argv; + while (isdigit(*p)) + p++; + if (p[0] == '>') + p = &p[1 + (p[1] == '>' || p[1] == '|')]; + else if (p[0] == '<') + p = &p[1 + (p[1] == '>')]; + else + usage(); + if (p[p[0] == '&'] != '=') + usage(); + redirections = realloc(redirections, (nredirections + 1U) * sizeof(*redirections)); + if (!redirections) + exitf("%s: could not allocate enough memory\n", argv0); + if (*p == '&') { + p = &p[1]; + memmove(p, &p[1], strlen(&p[1]) + 1U); + if (isdigit(*p)) { p++; - if (p[0] == '>') - p = &p[1 + (p[1] == '>' || p[1] == '|')]; - else if (p[0] == '<') - p = &p[1 + (p[1] == '>')]; - else - usage(); - if (p[p[0] == '&'] != '=') - usage(); - redirections = realloc(redirections, (nredirections + 1U) * sizeof(*redirections)); - if (!redirections) - exitf("%s: could not allocate enough memory\n", argv0); - if (*p == '&') { - p = &p[1]; - memmove(p, &p[1], strlen(&p[1]) + 1U); - if (isdigit(*p)) { - p++; - while (isdigit(*p)) - p++; - } else if (*p == '-') { + while (isdigit(*p)) p++; - } - if (*p) - usage(); - redirections[nredirections].escape = NULL; - } else { - *p++ = '\0'; - redirections[nredirections].escape = p; + } else if (*p == '-') { + p++; } - redirections[nredirections++].asis = *argv; + if (*p) + usage(); + redirections[nredirections].escape = NULL; + } else { + *p++ = '\0'; + redirections[nredirections].escape = p; } - if (!*argv) - usage(); - argv++; + redirections[nredirections++].asis = *argv; } + /* Check that we have the "}" argument that marks the end of sshexec options */ + if (!*argv) + usage(); + argv++; + #undef STORE_OPT +end_of_options: + /* Parse options and set defaults */ + if (!ssh) ssh = "ssh"; @@ -241,8 +448,30 @@ main(int argc_unused, char *argv[]) usage(); } - opts = argv; - nopts = 0; + return argv; +} + + +/** + * Read and parse the ssh(1) options + * + * @param argv The return value of `parse_sshexec_options` + * @param nopts_out Output parameter for the number of option + * arguments, this includes all arguments the + * return value offsets `argv` except the "--" + * (if there is one), so this includes both + * the options themselves and their arguments + * @return `argv` offset to skip pass any ssh(1) options, + * and any "--" immediately after them + */ +static char ** +parse_ssh_options(char *argv[], size_t *nopts_out) +{ + enum optclass class; + const char *arg; + char opt; + size_t nopts = 0; + while (*argv) { if (!strcmp(*argv, "--")) { argv++; @@ -271,79 +500,47 @@ main(int argc_unused, char *argv[]) } } + *nopts_out = nopts; + return argv; +} + + +int +main(int argc_unused, char *argv[]) +{ + char **opts, *p; + size_t nopts; + const char **args; + size_t i; + + (void) argc_unused; + + /* Identify used command name */ + if (*argv) + argv0 = *argv++; + p = strrchr(argv0, '/'); + is_sshcd = !strcmp(p ? &p[1] : argv0, "sshcd"); + + /* Parse options */ + argv = parse_sshexec_options(argv); + argv = parse_ssh_options(opts = argv, &nopts); + + /* Check operand count and separate out `destination` from the command and arguments */ destination = *argv++; if (!destination || (is_sshcd ? !!*argv : !*argv)) usage(); + /* Validate `destination` */ if (!strcmp(destination, "-")) exitf("%s: the command argument must not be \"-\"\n", argv0); else if (strchr(destination, '=')) exitf("%s: the command argument must contain an \'=\'\n", argv0); - if (!is_sshcd && !strncmp(destination, "sshexec://", sizeof("sshexec://") - 1U)) { - memmove(&destination[sizeof("ssh") - 1U], - &destination[sizeof("sshexec") - 1U], - strlen(&destination[sizeof("sshexec") - 1U]) + 1U); - goto using_ssh_prefix; - } else if (is_sshcd && !strncmp(destination, "sshcd://", sizeof("sshcd://") - 1U)) { - memmove(&destination[sizeof("ssh") - 1U], - &destination[sizeof("sshcd") - 1U], - strlen(&destination[sizeof("sshcd") - 1U]) + 1U); - goto using_ssh_prefix; - } else if (!strncmp(destination, "ssh://", sizeof("ssh://") - 1U)) { - using_ssh_prefix: - dest_dir_delim = strchr(&destination[sizeof("ssh://")], '/'); - } else { - dest_dir_delim = strchr(destination, ':'); - } - if (dest_dir_delim) { - if (dir) - exitf("%s: directory specified both in 'dir' option and in destination operand\n", argv0); - *dest_dir_delim++ = '\0'; - dir = dest_dir_delim; - } - - if (dir) { - build_command_asis("cd -- "); - build_command_escape(dir); - build_command_asis(strict_cd ? " && " : " ; "); - } - if (is_sshcd) { - build_command_asis("exec \"$SHELL\" -l"); - goto command_constructed; - } - if (asis) - build_command_asis("( env --"); - else - build_command_asis("exec env --"); - next_asis = 0; - for (; *argv; argv++) { - if (asis && nasis && !strcmp(*argv, asis)) { - nasis -= 1U; - next_asis = 1; - } else { - build_command_asis(" "); - if (next_asis) { - next_asis = 0; - build_command_asis(*argv); - } else { - build_command_escape(*argv); - } - } - } - if (asis) - build_command_asis(" )"); - for (i = 0; i < nredirections; i++) { - build_command_asis(" "); - build_command_asis(redirections[i].asis); - if (redirections[i].escape) { - build_command_asis(" "); - build_command_escape(redirections[i].escape); - } - } -command_constructed: - finalise_command(); + /* Parse command line operands */ + extract_directory_from_destination(); + construct_command(argv); + /* Construct arguments for ssh(1) and execute */ i = 0; args = calloc(5U + (size_t)is_sshcd + nopts, sizeof(*args)); if (!args) @@ -362,7 +559,6 @@ command_constructed: args[i++] = destination; args[i++] = command; args[i++] = NULL; - #if defined(__GNUC__) # pragma GCC diagnostic ignored "-Wcast-qual" #endif -- cgit v1.2.3-70-g09d2