/* See LICENSE file for copyright and license details. */ #include "common.h" #ifndef TEST #define FLAG_MINUS 0x01 #define FLAG_PLUS 0x02 #define FLAG_HASH 0x04 #define FLAG_ZERO 0x08 #define FLAG_SPACE 0x10 #define TYPESIZE_CHAR -2 #define TYPESIZE_SHORT -1 #define TYPESIZE_INT 0 #define TYPESIZE_LONG +1 #define TYPESIZE_LONG_LONG +2 #define TYPESIZE_SIZE 3 #define TYPESIZE_PTRDIFF 4 #define TYPESIZE_INTPTR 5 #define TYPESIZE_INTMAX 6 union anyarray_const { const void *v; const signed char *hhi; const signed short int *hi; const signed int *i; const signed long int *li; const signed long long int *lli; const ssize_t *zi; const ptrdiff_t *ti; const intptr_t *pi; const intmax_t *ji; const unsigned char *hhu; const unsigned short int *hu; const unsigned int *u; const unsigned long int *lu; const unsigned long long int *llu; const size_t *zu; const ptrdiff_t *tu; const uintptr_t *pu; const uintmax_t *ju; const char *c; const char *const *s; }; union anyvalue { intmax_t i; uintmax_t u; char c[2]; const char *s; }; struct partial_string { char *s; size_t len; size_t size; int started; }; struct argument_format { size_t array_size; size_t width1; size_t width2; int array_input; int array_null_terminated; int typesize; int has_dot; char typeclass; unsigned flags; }; struct ignore_stack { char *stack; size_t top; size_t size; size_t active; }; static int resize_partial_string(struct partial_string *str, size_t req_size) { void *new = realloc(str->s, req_size); if (!new) return -1; str->s = new; str->size = req_size; return 0; } static int resize_partial_string_if_full(struct partial_string *str, size_t extra) { if (str->len == str->size) if (resize_partial_string(str, str->len + extra)) return -1; return 0; } static char * make_unsigned(uintmax_t value, char fmt, const char *plus, const char *prefix, size_t minwidth) { #if defined(__GNUC__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif const char *fmtstr; char *ret, *p; size_t len, zeroes, totlen; if (fmt == 'd' || fmt == 'i' || fmt == 'u') fmtstr = "%ju"; else if (fmt == 'o') fmtstr = "%jo"; else if (fmt == 'x') fmtstr = "%jx"; else if (fmt == 'X') fmtstr = "%jX"; else abort(); len = (size_t)snprintf(NULL, 0, fmtstr, value); zeroes = len < minwidth ? minwidth - len : 0; totlen = strlen(plus) + 1; if (strlen(prefix) > SIZE_MAX - totlen) goto enomem; totlen += strlen(prefix); if (zeroes + len > SIZE_MAX - totlen) goto enomem; totlen += zeroes + len; ret = malloc(totlen); if (!ret) goto enomem; p = stpcpy(stpcpy(ret, plus), prefix); while (zeroes--) *p++ = '0'; sprintf(p, fmtstr, value); return ret; enomem: errno = ENOMEM; return NULL; #if defined(__GNUC__) # pragma GCC diagnostic pop #endif } static char * make_signed(intmax_t value, char fmt, const char *plus, const char *prefix, size_t minwidth) { if (value < 0) return make_unsigned((uintmax_t)-(value + 1) + 1U, fmt, "-", prefix, minwidth); else return make_unsigned((uintmax_t)value, fmt, plus, prefix, minwidth); } static unsigned get_flag(char c) { return c == '-' ? FLAG_MINUS : c == '+' ? FLAG_PLUS : c == '#' ? FLAG_HASH : c == '0' ? FLAG_ZERO : c == ' ' ? FLAG_SPACE : 0U; } static unsigned remove_ignored_flags(unsigned flags, char fmt) { if (fmt == 's' || fmt == 'c') { flags &= FLAG_MINUS; } else if (fmt == 'i' || fmt == 'd') { if (flags & FLAG_PLUS) flags &= (unsigned)~FLAG_SPACE; } else { flags &= (unsigned)~(FLAG_PLUS | FLAG_SPACE); } return flags; } static const char * read_size(size_t *out, const char *fmt, va_list args, int optional) { size_t digit; if (*fmt == '*') { fmt++; *out = va_arg(args, size_t); } else if (!optional && !isdigit(*fmt)) { goto einval; } else { *out = 0; while (isdigit(*fmt)) { digit = (size_t)(*fmt++ - '0'); if (*out > (SIZE_MAX - digit) / 10U) goto einval; *out = *out * 10U + digit; } } return fmt; einval: errno = EINVAL; return NULL; } static const char * get_typesize(int *out, const char *fmt) { *out = 0; for (; *fmt == 'h'; fmt++) if (--*out < -2) goto einval; for (; *fmt == 'l'; fmt++) if (*out < 0 || ++*out > +2) goto einval; if (*fmt == 'z' || *fmt == 't' || *fmt == 'T' || *fmt == 'j') { if (*out) goto einval; *out = *fmt == 'z' ? TYPESIZE_SIZE : *fmt == 't' ? TYPESIZE_PTRDIFF : *fmt == 'T' ? TYPESIZE_INTPTR : /* non-standard */ TYPESIZE_INTMAX; fmt++; } /* * TODO add =N for intN_t/uintN_t (arbitrary N) * TODO add =+N for int_leastN_t/uint_leastN_t (arbitrary N) * TODO add =/N for int_fastN_t/uint_fastN_t (arbitrary N) * non-standard */ if (*fmt == 's' || *fmt == 'c') { if (*out) { /* TODO %ls should be `const wchar_t *` */ /* TODO %lc should be `wint_t` */ goto einval; } } return fmt; einval: errno = EINVAL; return NULL; } static intmax_t get_signed_va(va_list args, int typesize) { if (typesize == TYPESIZE_CHAR) return (intmax_t)(signed char)va_arg(args, signed int); else if (typesize == TYPESIZE_SHORT) return (intmax_t)(signed short int)va_arg(args, signed int); else if (typesize == TYPESIZE_INT) return (intmax_t)va_arg(args, signed int); else if (typesize == TYPESIZE_LONG) return (intmax_t)va_arg(args, signed long int); else if (typesize == TYPESIZE_LONG_LONG) return (intmax_t)va_arg(args, signed long long int); else if (typesize == TYPESIZE_SIZE) /* TODO since ssize_t may be bounded to -1, may it be bounded above INTMAX_MAX? */ return (intmax_t)va_arg(args, ssize_t); else if (typesize == TYPESIZE_PTRDIFF) return (intmax_t)va_arg(args, ptrdiff_t); else if (typesize == TYPESIZE_INTPTR) return (intmax_t)va_arg(args, intptr_t); else if (typesize == TYPESIZE_INTMAX) return (intmax_t)va_arg(args, intmax_t); else abort(); } static uintmax_t get_unsigned_va(va_list args, int typesize) { if (typesize == TYPESIZE_CHAR) return (uintmax_t)(unsigned char)va_arg(args, unsigned int); else if (typesize == TYPESIZE_SHORT) return (uintmax_t)(unsigned short int)va_arg(args, unsigned int); else if (typesize == TYPESIZE_INT) return (uintmax_t)va_arg(args, unsigned int); else if (typesize == TYPESIZE_LONG) return (uintmax_t)va_arg(args, unsigned long int); else if (typesize == TYPESIZE_LONG_LONG) return (uintmax_t)va_arg(args, unsigned long long int); else if (typesize == TYPESIZE_SIZE) return (uintmax_t)va_arg(args, size_t); else if (typesize == TYPESIZE_PTRDIFF) return (uintmax_t)va_arg(args, ptrdiff_t); /* TODO ptrdiff_t is signed */ else if (typesize == TYPESIZE_INTPTR) return (uintmax_t)va_arg(args, uintptr_t); else if (typesize == TYPESIZE_INTMAX) return (uintmax_t)va_arg(args, uintmax_t); else abort(); } static intmax_t get_signed_array(union anyarray_const *arrayp, int typesize) { if (typesize == TYPESIZE_CHAR) return (intmax_t)*arrayp->hhi++; else if (typesize == TYPESIZE_SHORT) return (intmax_t)*arrayp->hi++; else if (typesize == TYPESIZE_INT) return (intmax_t)*arrayp->i++; else if (typesize == TYPESIZE_LONG) return (intmax_t)*arrayp->li++; else if (typesize == TYPESIZE_LONG_LONG) return (intmax_t)*arrayp->lli++; else if (typesize == TYPESIZE_SIZE) return (intmax_t)*arrayp->zi++; /* TODO since ssize_t may be bounded to -1, may it be bounded above INTMAX_MAX? */ else if (typesize == TYPESIZE_PTRDIFF) return (intmax_t)*arrayp->ti++; else if (typesize == TYPESIZE_INTPTR) return (intmax_t)*arrayp->pi++; else if (typesize == TYPESIZE_INTMAX) return (intmax_t)*arrayp->ji++; else abort(); } static uintmax_t get_unsigned_array(union anyarray_const *arrayp, int typesize) { if (typesize == TYPESIZE_CHAR) return (uintmax_t)*arrayp->hhu++; else if (typesize == TYPESIZE_SHORT) return (uintmax_t)*arrayp->hu++; else if (typesize == TYPESIZE_INT) return (uintmax_t)*arrayp->u++; else if (typesize == TYPESIZE_LONG) return (uintmax_t)*arrayp->lu++; else if (typesize == TYPESIZE_LONG_LONG) return (uintmax_t)*arrayp->llu++; else if (typesize == TYPESIZE_SIZE) return (uintmax_t)*arrayp->zu++; else if (typesize == TYPESIZE_PTRDIFF) return (uintmax_t)*arrayp->tu++; /* TODO ptrdiff_t is signed */ else if (typesize == TYPESIZE_INTPTR) return (uintmax_t)*arrayp->pu++; else if (typesize == TYPESIZE_INTMAX) return (uintmax_t)*arrayp->ju++; else abort(); } static int get_value_va(union anyvalue *out, va_list args, char fmt, int typesize) { if (fmt == 'i' || fmt == 'd') { out->i = get_signed_va(args, typesize); } else if (fmt == 'u' || fmt == 'o' || fmt == 'x' || fmt == 'X') { out->u = get_unsigned_va(args, typesize); } else if (fmt == 's') { if (typesize) abort(); out->s = va_arg(args, const char *); } else if (fmt == 'c') { if (typesize) abort(); out->c[0] = (char)va_arg(args, int); out->c[1] = '\0'; } else { errno = EINVAL; return -1; } return 0; } static int get_value_array(union anyvalue *out, union anyarray_const *arrayp, char fmt, int typesize) { if (fmt == 'i' || fmt == 'd') { out->i = get_signed_array(arrayp, typesize); return !!out->i; } else if (fmt == 'u' || fmt == 'o' || fmt == 'x' || fmt == 'X') { out->u = get_unsigned_array(arrayp, typesize); return !!out->u; } else if (fmt == 's') { if (typesize) abort(); out->s = *arrayp->s++; return !!out->s; } else if (fmt == 'c') { if (typesize) abort(); out->c[0] = *arrayp->c++; out->c[1] = '\0'; return !!out->c[0]; } else { errno = EINVAL; return -1; } } static char * create_string(const union anyvalue *value, char fmt, const char *plus, const char *prefix, int has_dot, size_t width) { if (fmt == 'i' || fmt == 'd') { return make_signed(value->i, fmt, plus, value->i ? prefix : "", width); } else if (fmt == 'u' || fmt == 'o' || fmt == 'x' || fmt == 'X') { return make_unsigned(value->u, fmt, "", value->u ? prefix : "", width); } else if (fmt == 's') { return strndup(value->s, has_dot ? width : strlen(value->s)); } else if (fmt == 'c') { return strndup(value->c, has_dot ? width : 1); } else { errno = EINVAL; return NULL; } } static int pad_and_insert(struct partial_string *arg, const char *substring, size_t min_width, char pad_char, unsigned pad_before) { size_t pad_len, substring_len; substring_len = strlen(substring); pad_len = substring_len < min_width ? min_width - substring_len : 0; if (substring_len + pad_len > SIZE_MAX - arg->len) { errno = ENOMEM; return -1; } if (substring_len + pad_len + arg->len > arg->size) if (resize_partial_string(arg, substring_len + pad_len + arg->len)) return -1; if (pad_before) { memset(&arg->s[arg->len], pad_char, pad_len); arg->len += pad_len; } memcpy(&arg->s[arg->len], substring, substring_len); arg->len += substring_len; if (!pad_before) { memset(&arg->s[arg->len], pad_char, pad_len); arg->len += pad_len; } return 0; } static int insert_argument(struct partial_string *arg, const union anyvalue *value, char fmt, unsigned flags, size_t width1, int has_dot, size_t width2) { const char *plus, *prefix; char *substring; plus = (flags & FLAG_PLUS) ? "+" : (flags & FLAG_SPACE) ? " " : ""; prefix = !(flags & FLAG_HASH) ? "" : fmt == 'o' ? "0" : fmt == 'x' ? "0x" : fmt == 'X' ? "0X" : ""; if (fmt != 's' && fmt != 'c') { if (!has_dot) { width2 = width1; width1 = 0; } } substring = create_string(value, fmt, plus, prefix, has_dot, width2); if (!substring) return -1; if (pad_and_insert(arg, substring, width1, (flags & FLAG_ZERO) ? '0' : ' ', flags & FLAG_MINUS)) { free(substring); return -1; } free(substring); return 0; } static int get_and_insert_arguments(struct partial_string *arg, va_list args, const struct argument_format *arg_fmt, int ignoring) { union anyarray_const array; union anyvalue value; unsigned int flags; size_t n; int r; flags = remove_ignored_flags(arg_fmt->flags, arg_fmt->typeclass); if (arg_fmt->array_input) { array.v = va_arg(args, const void *); n = arg_fmt->array_size; while (arg_fmt->array_null_terminated || n--) { r = get_value_array(&value, &array, arg_fmt->typeclass, arg_fmt->typesize); if (r < 0) return -1; else if (!r && arg_fmt->array_null_terminated) break; if (!ignoring) if (insert_argument(arg, &value, arg_fmt->typeclass, flags, arg_fmt->width1, arg_fmt->has_dot, arg_fmt->width2)) return -1; } } else { if (get_value_va(&value, args, arg_fmt->typeclass, arg_fmt->typesize)) return -1; if (!ignoring) if (insert_argument(arg, &value, arg_fmt->typeclass, flags, arg_fmt->width1, arg_fmt->has_dot, arg_fmt->width2)) return -1; } return 0; } static const char * get_argument_format(struct argument_format *out, const char *fmt, va_list args) { unsigned flag; memset(out, 0, sizeof(*out)); if (*fmt == '[') { fmt++; out->array_input = 1; if (*fmt == ']') { out->array_null_terminated = 1; } else { fmt = read_size(&out->array_size, fmt, args, 1); if (!fmt) goto fail; } if (*fmt != ']') goto einval; fmt++; } while ((flag = get_flag(*fmt))) { out->flags |= flag; fmt++; } fmt = read_size(&out->width1, fmt, args, 0); if (!fmt) goto fail; if (*fmt == '.') { out->has_dot = 1; out->flags &= (unsigned)~FLAG_ZERO; fmt++; fmt = read_size(&out->width2, fmt, args, 0); if (!fmt) goto fail; } fmt = get_typesize(&out->typesize, fmt); if (!fmt) goto fail; out->typeclass = *fmt++; return fmt; einval: errno = EINVAL; fail: return NULL; } static void destroy_ignore_stack(struct ignore_stack *stack) { free(stack->stack); stack->stack = NULL; } static int push_ignore_stack(struct ignore_stack *stack, va_list args) { int show; void *new; size_t new_size; if (stack->top == stack->size) { new_size = stack->size + 8; new = realloc(stack->stack, new_size * sizeof(*stack->stack)); if (!new) return -1; stack->stack = new; stack->size = new_size; } show = va_arg(args, int); stack->stack[stack->top++] = (char)!show; stack->active += (size_t)!show; return 0; } static int pop_ignore_stack(struct ignore_stack *stack) { if (!stack->top) { errno = EINVAL; return -1; } if (stack->stack[--stack->top]) stack->active -= 1; return 0; } static int is_ignoring(struct ignore_stack *stack) { return stack->active > 0; } static const char * format(const char *fmt, struct partial_string *arg, struct ignore_stack *ignore_stack, va_list args) { struct argument_format arg_fmt; /* TODO $< should work as <(), * or number before < to select fd (default: STDOUT_FILENO) */ /* TODO $> should work as >(), * or number before > to select fd (default: STDIN_FILENO) */ /* TODO add support for embedding via %S and %C (do not set arg->started) */ /* TODO add support for floating point values */ if (*fmt == '$' || *fmt == '\'' || *fmt == '"' || *fmt == '_') { if (!is_ignoring(ignore_stack)) { if (arg->len == arg->size) if (resize_partial_string(arg, arg->size + 32)) goto fail; arg->s[arg->len++] = *fmt == '_' ? ' ' : *fmt; arg->started = 1; } fmt++; } else if (*fmt == '(') { fmt++; if (push_ignore_stack(ignore_stack, args)) goto fail; } else if (*fmt == ')') { fmt++; if (pop_ignore_stack(ignore_stack)) goto fail; } else { fmt = get_argument_format(&arg_fmt, fmt, args); if (!fmt) goto fail; if (get_and_insert_arguments(arg, args, &arg_fmt, is_ignoring(ignore_stack))) goto fail; arg->started = 1; } return fmt; fail: return NULL; } static char ** format_command(const char *fmt, va_list args, size_t *narguments_out) { size_t command_size = 0; char **command = NULL; struct ignore_stack ignore_stack; struct partial_string arg; void *new; *narguments_out = 0; memset(&ignore_stack, 0, sizeof(ignore_stack)); ignore_stack.stack = NULL; memset(&arg, 0, sizeof(arg)); arg.s = NULL; next_arg: arg.started = 0; arg.len = 0; while (*fmt) { if (*fmt == '$') { fmt++; fmt = format(fmt, &arg, &ignore_stack, args); if (!fmt) goto fail; } else if (isspace(*fmt)) { fmt++; if (!arg.started || is_ignoring(&ignore_stack)) continue; new = realloc(command, (command_size + 1) * sizeof(*command)); if (!new) goto fail; command = new; if (resize_partial_string_if_full(&arg, 32)) goto fail; arg.s[arg.len] = '\0'; command[command_size] = strdup(arg.s); if (!command[command_size]) goto fail; command_size++; goto next_arg; } else if (*fmt == '\'' || *fmt == '"') { arg.started = 1; /* TODO add support for quotes */ errno = EINVAL; goto fail; } else { if (!is_ignoring(&ignore_stack)) { if (resize_partial_string_if_full(&arg, 32)) goto fail; arg.s[arg.len++] = *fmt; arg.started = 1; } fmt++; } } new = realloc(command, (command_size + 2) * sizeof(*command)); if (!new) goto fail; command = new; if (arg.started) { if (resize_partial_string_if_full(&arg, 1)) goto fail; arg.s[arg.len] = '\0'; command[command_size++] = arg.s; } else { free(arg.s); } command[command_size] = NULL; destroy_ignore_stack(&ignore_stack); *narguments_out = command_size; return command; fail: free(arg.s); destroy_ignore_stack(&ignore_stack); while (command_size) free(command[--command_size]); free(command); return NULL; } static int append_arguments(struct libexec_command *cmd, va_list args) { size_t n = 0, i; va_list args2; void *new; va_copy(args2, args); while (va_arg(args2, const char *)) n += 1; va_end(args2); new = realloc(cmd->arguments, (cmd->narguments + n + 1) * sizeof(*cmd->arguments)); if (!new) return -1; cmd->arguments = new; for (i = 0; i < n; i++) { cmd->arguments[cmd->narguments + i] = strdup(va_arg(args, const char *)); if (!cmd->arguments[cmd->narguments + i]) { while (i) free(cmd->arguments[cmd->narguments + --i]); if (!cmd->narguments) { free(cmd->arguments); cmd->arguments = NULL; } return -1; } } cmd->arguments[cmd->narguments + n] = va_arg(args, char *); /* that's a NULL */ cmd->narguments += n; return 0; } int libexec_vconstruct_command(struct libexec_command *cmd, const char *fmt, va_list args) { size_t n; char **command = NULL; void *new; if (!cmd) { errno = EINVAL; return -1; } if (!fmt) { return append_arguments(cmd, args); } else { command = format_command(fmt, args, &n); if (!command) return -1; if (!cmd->narguments) { cmd->arguments = command; cmd->narguments = n; } else { new = realloc(cmd->arguments, (cmd->narguments + n + 1) * sizeof(*cmd->arguments)); if (!new) { while (n) free(command[--n]); free(command); return -1; } cmd->arguments = new; memcpy(&cmd->arguments[cmd->narguments], command, (n + 1) * sizeof(*command)); cmd->narguments += n; free(command); } return 0; } } #else TESTED_ELSEWHERE /* libexec_construct_command.c */ #endif