diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 30c987f834..b6e59144fb 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -596,6 +596,13 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \ parts of the session continue running. Thus, we highly recommend that this variable isn't used unless necessary. Defaults to true. +`homectl`: + +* `$SYSTEMD_HOME_FIRSTBOOT_OVERRIDE` – if set to "1" will make `homectl + firstboot --prompt-new-user` interactively ask for user creation, even if + there already exists at least one regular user on the system. If set to "0" + will make the tool skip any such query. + `kernel-install`: * `$KERNEL_INSTALL_BYPASS` – If set to "1", execution of kernel-install is skipped diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 7f4be13b62..4748d5e2b5 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1470,3 +1470,19 @@ char* strrstr(const char *haystack, const char *needle) { } return NULL; } + +size_t str_common_prefix(const char *a, const char *b) { + assert(a); + assert(b); + + /* Returns the length of the common prefix of the two specified strings, or SIZE_MAX in case the + * strings are fully identical. */ + + for (size_t n = 0;; n++) { + char c = a[n]; + if (c != b[n]) + return n; + if (c == 0) + return SIZE_MAX; + } +} diff --git a/src/basic/string-util.h b/src/basic/string-util.h index 85f206c689..d94e52c09d 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -308,3 +308,5 @@ bool version_is_valid_versionspec(const char *s); ssize_t strlevenshtein(const char *x, const char *y); char* strrstr(const char *haystack, const char *needle); + +size_t str_common_prefix(const char *a, const char *b); diff --git a/src/basic/strv.c b/src/basic/strv.c index 1eea73fa7b..d81c9f127c 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -1238,3 +1238,24 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { *ret = TAKE_PTR(broken); return 0; } + +char** strv_filter_prefix(char *const*l, const char *prefix) { + _cleanup_strv_free_ char **f = NULL; + + /* Allocates a copy of 'l', but only copies over entries starting with 'prefix' */ + + if (isempty(prefix)) + return strv_copy(l); + + size_t sz = 0; + + STRV_FOREACH(i, l) { + if (!startswith(*i, prefix)) + continue; + + if (strv_extend_with_size(&f, &sz, *i) < 0) + return NULL; + } + + return TAKE_PTR(f); +} diff --git a/src/basic/strv.h b/src/basic/strv.h index 529afc386d..6478723715 100644 --- a/src/basic/strv.h +++ b/src/basic/strv.h @@ -268,3 +268,5 @@ int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const #define string_strv_ordered_hashmap_put(h, k, v) _string_strv_ordered_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS) int strv_rebreak_lines(char **l, size_t width, char ***ret); + +char** strv_filter_prefix(char *const*l, const char *prefix); diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 678cd4ed4d..f9e2a28024 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -26,6 +26,7 @@ #include "constants.h" #include "devnum-util.h" #include "env-util.h" +#include "errno-list.h" #include "fd-util.h" #include "fileio.h" #include "fs-util.h" @@ -103,22 +104,25 @@ int chvt(int vt) { return RET_NERRNO(ioctl(fd, VT_ACTIVATE, vt)); } -int read_one_char(FILE *f, char *ret, usec_t t, bool *need_nl) { +int read_one_char(FILE *f, char *ret, usec_t t, bool echo, bool *need_nl) { _cleanup_free_ char *line = NULL; struct termios old_termios; int r, fd; - assert(f); assert(ret); + if (!f) + f = stdin; + /* If this is a terminal, then switch canonical mode off, so that we can read a single * character. (Note that fmemopen() streams do not have an fd associated with them, let's handle that - * nicely.) */ + * nicely.) If 'echo' is false we'll also disable ECHO mode so that the pressed key is not made + * visible to the user. */ fd = fileno(f); if (fd >= 0 && tcgetattr(fd, &old_termios) >= 0) { struct termios new_termios = old_termios; - new_termios.c_lflag &= ~ICANON; + new_termios.c_lflag &= ~(ICANON|(echo ? 0 : ECHO)); new_termios.c_cc[VMIN] = 1; new_termios.c_cc[VTIME] = 0; @@ -201,7 +205,7 @@ int ask_char(char *ret, const char *replies, const char *fmt, ...) { fflush(stdout); - r = read_one_char(stdin, &c, DEFAULT_ASK_REFRESH_USEC, &need_nl); + r = read_one_char(stdin, &c, DEFAULT_ASK_REFRESH_USEC, /* echo= */ true, &need_nl); if (r < 0) { if (r == -ETIMEDOUT) @@ -228,94 +232,365 @@ int ask_char(char *ret, const char *replies, const char *fmt, ...) { } } -int ask_string(char **ret, const char *text, ...) { - _cleanup_free_ char *line = NULL; +typedef enum CompletionResult{ + COMPLETION_ALREADY, /* the input string is already complete */ + COMPLETION_FULL, /* completed the input string to be complete now */ + COMPLETION_PARTIAL, /* completed the input string so that is still incomplete */ + COMPLETION_NONE, /* found no matching completion */ + _COMPLETION_RESULT_MAX, + _COMPLETION_RESULT_INVALID = -EINVAL, + _COMPLETION_RESULT_ERRNO_MAX = -ERRNO_MAX, +} CompletionResult; + +static CompletionResult pick_completion(const char *string, char *const*completions, char **ret) { + _cleanup_free_ char *found = NULL; + bool partial = false; + + string = strempty(string); + + STRV_FOREACH(c, completions) { + + /* Ignore entries that are not actually completions */ + if (!startswith(*c, string)) + continue; + + /* Store first completion that matches */ + if (!found) { + found = strdup(*c); + if (!found) + return -ENOMEM; + + continue; + } + + /* If there's another completion that works truncate the one we already found by common + * prefix */ + size_t n = str_common_prefix(found, *c); + if (n == SIZE_MAX) + continue; + + found[n] = 0; + partial = true; + } + + *ret = TAKE_PTR(found); + + if (!*ret) + return COMPLETION_NONE; + if (partial) + return COMPLETION_PARTIAL; + + return streq(string, *ret) ? COMPLETION_ALREADY : COMPLETION_FULL; +} + +static void clear_by_backspace(size_t n) { + /* Erase the specified number of character cells backwards on the terminal */ + for (size_t i = 0; i < n; i++) + fputs("\b \b", stdout); +} + +int ask_string_full( + char **ret, + GetCompletionsCallback get_completions, + void *userdata, + const char *text, ...) { + va_list ap; int r; assert(ret); assert(text); + /* Output the prompt */ fputs(ansi_highlight(), stdout); - va_start(ap, text); vprintf(text, ap); va_end(ap); - fputs(ansi_normal(), stdout); - fflush(stdout); - r = read_line(stdin, LONG_LINE_MAX, &line); + _cleanup_free_ char *string = NULL; + size_t n = 0; + + /* Do interactive logic only if stdin + stdout are connected to the same place. And yes, we could use + * STDIN_FILENO and STDOUT_FILENO here, but let's be overly correct for once, after all libc allows + * swapping out stdin/stdout. */ + int fd_input = fileno(stdin); + int fd_output = fileno(stdout); + if (fd_input < 0 || fd_output < 0 || same_fd(fd_input, fd_output) <= 0) + goto fallback; + + /* Try to disable echo, which also tells us if this even is a terminal */ + struct termios old_termios; + if (tcgetattr(fd_input, &old_termios) < 0) + goto fallback; + + struct termios new_termios = old_termios; + termios_disable_echo(&new_termios); + if (tcsetattr(fd_input, TCSADRAIN, &new_termios) < 0) + return -errno; + + for (;;) { + int c = fgetc(stdin); + + /* On EOF or NUL, end the request, don't output anything anymore */ + if (IN_SET(c, EOF, 0)) + break; + + /* On Return also end the request, but make this visible */ + if (IN_SET(c, '\n', '\r')) { + fputc('\n', stdout); + break; + } + + if (c == '\t') { + /* Tab */ + + _cleanup_strv_free_ char **completions = NULL; + if (get_completions) { + r = get_completions(string, &completions, userdata); + if (r < 0) + goto fail; + } + + _cleanup_free_ char *new_string = NULL; + CompletionResult cr = pick_completion(string, completions, &new_string); + if (cr < 0) { + r = cr; + goto fail; + } + if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_FULL)) { + /* Output the new suffix we learned */ + fputs(ASSERT_PTR(startswith(new_string, strempty(string))), stdout); + + /* And update the whole string */ + free_and_replace(string, new_string); + n = strlen(string); + } + if (cr == COMPLETION_NONE) + fputc('\a', stdout); /* BEL */ + + if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_ALREADY)) { + /* If this worked only partially, or if the user hit TAB even though we were + * complete already, then show the remaining options (in the latter case just + * the one). */ + fputc('\n', stdout); + + _cleanup_strv_free_ char **filtered = strv_filter_prefix(completions, string); + if (!filtered) { + r = -ENOMEM; + goto fail; + } + + r = show_menu(filtered, + /* n_columns= */ SIZE_MAX, + /* column_width= */ SIZE_MAX, + /* ellipsize_percentage= */ 0, + /* grey_prefix=*/ string, + /* with_numbers= */ false); + if (r < 0) + goto fail; + + /* Show the prompt again */ + fputs(ansi_highlight(), stdout); + va_start(ap, text); + vprintf(text, ap); + va_end(ap); + fputs(ansi_normal(), stdout); + fputs(string, stdout); + } + + } else if (IN_SET(c, '\b', 127)) { + /* Backspace */ + + if (n == 0) + fputc('\a', stdout); /* BEL */ + else { + size_t m = utf8_last_length(string, n); + + char *e = string + n - m; + clear_by_backspace(utf8_console_width(e)); + + *e = 0; + n -= m; + } + + } else if (c == 21) { + /* Ctrl-u → erase all input */ + + clear_by_backspace(utf8_console_width(string)); + string[n = 0] = 0; + + } else if (c == 4) { + /* Ctrl-d → cancel this field input */ + + r = -ECANCELED; + goto fail; + + } else if (char_is_cc(c) || n >= LINE_MAX) + /* refuse control characters and too long strings */ + fputc('\a', stdout); /* BEL */ + else { + /* Regular char */ + + if (!GREEDY_REALLOC(string, n+2)) { + r = -ENOMEM; + goto fail; + } + + string[n++] = (char) c; + string[n] = 0; + + fputc(c, stdout); + } + + fflush(stdout); + } + + if (tcsetattr(fd_input, TCSADRAIN, &old_termios) < 0) + return -errno; + + if (!string) { + string = strdup(""); + if (!string) + return -ENOMEM; + } + + *ret = TAKE_PTR(string); + return 0; + +fail: + (void) tcsetattr(fd_input, TCSADRAIN, &old_termios); + return r; + +fallback: + /* A simple fallback without TTY magic */ + r = read_line(stdin, LONG_LINE_MAX, &string); if (r < 0) return r; if (r == 0) return -EIO; - *ret = TAKE_PTR(line); + *ret = TAKE_PTR(string); return 0; } bool any_key_to_proceed(void) { + + /* Insert a new line here as well as to when the user inputs, as this is also used during the boot up + * sequence when status messages may be interleaved with the current program output. This ensures + * that the status messages aren't appended on the same line as this message. */ + + fputc('\n', stdout); + fputs(ansi_highlight_magenta(), stdout); + fputs("-- Press any key to proceed --", stdout); + fputs(ansi_normal(), stdout); + fflush(stdout); + char key = 0; - bool need_nl = true; + (void) read_one_char(stdin, &key, USEC_INFINITY, /* echo= */ false, /* need_nl= */ NULL); - /* - * Insert a new line here as well as to when the user inputs, as this is also used during the - * boot up sequence when status messages may be interleaved with the current program output. - * This ensures that the status messages aren't appended on the same line as this message. - */ - puts("-- Press any key to proceed --"); - - (void) read_one_char(stdin, &key, USEC_INFINITY, &need_nl); - - if (need_nl) - putchar('\n'); + fputc('\n', stdout); + fputc('\n', stdout); + fflush(stdout); return key != 'q'; } -int show_menu(char **x, unsigned n_columns, unsigned width, unsigned percentage) { - unsigned break_lines, break_modulo; - size_t n, per_column, i, j; +static size_t widest_list_element(char *const*l) { + size_t w = 0; + + /* Returns the largest console width of all elements in 'l' */ + + STRV_FOREACH(i, l) + w = MAX(w, utf8_console_width(*i)); + + return w; +} + +int show_menu(char **x, + size_t n_columns, + size_t column_width, + unsigned ellipsize_percentage, + const char *grey_prefix, + bool with_numbers) { assert(n_columns > 0); - n = strv_length(x); - per_column = DIV_ROUND_UP(n, n_columns); + if (n_columns == SIZE_MAX) + n_columns = 3; - break_lines = lines(); + if (column_width == SIZE_MAX) { + size_t widest = widest_list_element(x); + + /* If not specified, derive column width from screen width */ + size_t column_max = (columns()-1) / n_columns; + + /* Subtract room for numbers */ + if (with_numbers && column_max > 6) + column_max -= 6; + + /* If columns would get too tight let's make this a linear list instead. */ + if (column_max < 10 && widest > 10) { + n_columns = 1; + column_max = columns()-1; + + if (with_numbers && column_max > 6) + column_max -= 6; + } + + column_width = CLAMP(widest+1, 10U, column_max); + } + + size_t n = strv_length(x); + size_t per_column = DIV_ROUND_UP(n, n_columns); + + size_t break_lines = lines(); if (break_lines > 2) break_lines--; /* The first page gets two extra lines, since we want to show * a title */ - break_modulo = break_lines; + size_t break_modulo = break_lines; if (break_modulo > 3) break_modulo -= 3; - for (i = 0; i < per_column; i++) { + for (size_t i = 0; i < per_column; i++) { - for (j = 0; j < n_columns; j++) { + for (size_t j = 0; j < n_columns; j++) { _cleanup_free_ char *e = NULL; if (j * per_column + i >= n) break; - e = ellipsize(x[j * per_column + i], width, percentage); + e = ellipsize(x[j * per_column + i], column_width, ellipsize_percentage); if (!e) - return log_oom(); + return -ENOMEM; - printf("%4zu) %-*s", j * per_column + i + 1, (int) width, e); + if (with_numbers) + printf("%s%4zu)%s ", + ansi_grey(), + j * per_column + i + 1, + ansi_normal()); + + if (grey_prefix && startswith(e, grey_prefix)) { + size_t k = MIN(strlen(grey_prefix), column_width); + printf("%s%.*s%s", + ansi_grey(), + (int) k, e, + ansi_normal()); + printf("%-*s", + (int) (column_width - k), e+k); + } else + printf("%-*s", (int) column_width, e); } putchar('\n'); /* on the first screen we reserve 2 extra lines for the title */ - if (i % break_lines == break_modulo) { + if (i % break_lines == break_modulo) if (!any_key_to_proceed()) return 0; - } } return 0; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index d6dd394bcf..698838e63f 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -80,11 +80,15 @@ int proc_cmdline_tty_size(const char *tty, unsigned *ret_rows, unsigned *ret_col int chvt(int vt); -int read_one_char(FILE *f, char *ret, usec_t timeout, bool *need_nl); +int read_one_char(FILE *f, char *ret, usec_t timeout, bool echo, bool *need_nl); int ask_char(char *ret, const char *replies, const char *text, ...) _printf_(3, 4); -int ask_string(char **ret, const char *text, ...) _printf_(2, 3); + +typedef int (*GetCompletionsCallback)(const char *key, char ***ret_list, void *userdata); +int ask_string_full(char **ret, GetCompletionsCallback cb, void *userdata, const char *text, ...) _printf_(4, 5); +#define ask_string(ret, text, ...) ask_string_full(ret, NULL, NULL, text, ##__VA_ARGS__) + bool any_key_to_proceed(void); -int show_menu(char **x, unsigned n_columns, unsigned width, unsigned percentage); +int show_menu(char **x, size_t n_columns, size_t column_width, unsigned ellipsize_percentage, const char *grey_prefix, bool with_numbers); int vt_disallocate(const char *name); diff --git a/src/basic/utf8.c b/src/basic/utf8.c index 2a9da59881..7b47df6f4f 100644 --- a/src/basic/utf8.c +++ b/src/basic/utf8.c @@ -609,3 +609,26 @@ size_t utf8_console_width(const char *str) { return n; } + +size_t utf8_last_length(const char *s, size_t n) { + int r; + + if (n == SIZE_MAX) + n = strlen(s); + + /* Determines length in bytes of last UTF-8 codepoint in string. If the string is empty, returns + * zero. Treats invalid UTF-8 codepoints as 1 sized ones. */ + + for (size_t last = 0;;) { + if (n == 0) + return last; + + r = utf8_encoded_valid_unichar(s, n); + if (r <= 0) + r = 1; /* treat invalid UTF-8 as byte-wide */ + + s += r; + n -= r; + last = r; + } +} diff --git a/src/basic/utf8.h b/src/basic/utf8.h index 221bc46a2d..f6158b2ef1 100644 --- a/src/basic/utf8.h +++ b/src/basic/utf8.h @@ -62,3 +62,5 @@ static inline char32_t utf16_surrogate_pair_to_unichar(char16_t lead, char16_t t size_t utf8_n_codepoints(const char *str); int utf8_char_console_width(const char *str); size_t utf8_console_width(const char *str); + +size_t utf8_last_length(const char *s, size_t n); diff --git a/src/cgtop/cgtop.c b/src/cgtop/cgtop.c index 08eae5988b..ce73599d90 100644 --- a/src/cgtop/cgtop.c +++ b/src/cgtop/cgtop.c @@ -954,7 +954,7 @@ static int loop(const char *root) { if (arg_batch) (void) usleep_safe(usec_add(usec_sub_unsigned(last_refresh, t), arg_delay)); else { - r = read_one_char(stdin, &key, usec_add(usec_sub_unsigned(last_refresh, t), arg_delay), NULL); + r = read_one_char(stdin, &key, usec_add(usec_sub_unsigned(last_refresh, t), arg_delay), /* echo= */ false, /* need_nl= */ NULL); if (r == -ETIMEDOUT) continue; if (r < 0) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index d00886fb80..99e75b3422 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -128,14 +128,45 @@ static void print_welcome(int rfd) { else printf("\nWelcome to your new installation of %s!\n", pn); - printf("\nPlease configure your system!\n\n"); + printf("\nPlease configure your system!\n"); any_key_to_proceed(); done = true; } -static int prompt_loop(int rfd, const char *text, char **l, unsigned percentage, bool (*is_valid)(int rfd, const char *name), char **ret) { +static int get_completions( + const char *key, + char ***ret_list, + void *userdata) { + + int r; + + if (!userdata) { + *ret_list = NULL; + return 0; + } + + _cleanup_strv_free_ char **copy = strv_copy(userdata); + if (!copy) + return -ENOMEM; + + r = strv_extend(©, "list"); + if (r < 0) + return r; + + *ret_list = TAKE_PTR(copy); + return 0; +} + +static int prompt_loop( + int rfd, + const char *text, + char **l, + unsigned ellipsize_percentage, + bool (*is_valid)(int rfd, const char *name), + char **ret) { + int r; assert(text); @@ -144,11 +175,14 @@ static int prompt_loop(int rfd, const char *text, char **l, unsigned percentage, for (;;) { _cleanup_free_ char *p = NULL; - unsigned u; - r = ask_string(&p, strv_isempty(l) ? "%s %s (empty to skip): " - : "%s %s (empty to skip, \"list\" to list options): ", - special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), text); + r = ask_string_full( + &p, + get_completions, + l, + strv_isempty(l) ? "%s %s (empty to skip): " + : "%s %s (empty to skip, \"list\" to list options): ", + special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), text); if (r < 0) return log_error_errno(r, "Failed to query user: %m"); @@ -159,14 +193,20 @@ static int prompt_loop(int rfd, const char *text, char **l, unsigned percentage, if (!strv_isempty(l)) { if (streq(p, "list")) { - r = show_menu(l, 3, 20, percentage); + r = show_menu(l, + /* n_columns= */ 3, + /* column_width= */ 20, + ellipsize_percentage, + /* grey_prefix= */ NULL, + /* with_numbers= */ true); if (r < 0) - return r; + return log_error_errno(r, "Failed to show menu: %m"); putchar('\n'); continue; } + unsigned u; r = safe_atou(p, &u); if (r >= 0) { if (u <= 0 || u > strv_length(l)) { diff --git a/src/home/homectl.c b/src/home/homectl.c index bfd4b3b574..7acaf59301 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2476,6 +2476,28 @@ static int acquire_group_list(char ***ret) { return !!*ret; } +static int group_completion_callback(const char *key, char ***ret_list, void *userdata) { + char ***available = userdata; + int r; + + if (!*available) { + r = acquire_group_list(available); + if (r < 0) + log_debug_errno(r, "Failed to enumerate available groups, ignoring: %m"); + } + + _cleanup_strv_free_ char **l = strv_copy(*available); + if (!l) + return -ENOMEM; + + r = strv_extend(&l, "list"); + if (r < 0) + return r; + + *ret_list = TAKE_PTR(l); + return 0; +} + static int create_interactively(void) { _cleanup_free_ char *username = NULL; int r; @@ -2485,7 +2507,12 @@ static int create_interactively(void) { return 0; } - any_key_to_proceed(); + printf("\nPlease create your user account!\n"); + + if (!any_key_to_proceed()) { + log_notice("Skipping."); + return 0; + } (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* switch_to_text= */ false); @@ -2522,12 +2549,21 @@ static int create_interactively(void) { return log_error_errno(r, "Failed to set userName field: %m"); _cleanup_strv_free_ char **available = NULL, **groups = NULL; - for (;;) { _cleanup_free_ char *s = NULL; - unsigned u; - r = ask_string(&s, + strv_sort_uniq(groups); + + if (!strv_isempty(groups)) { + _cleanup_free_ char *j = strv_join(groups, ", "); + if (!j) + return log_oom(); + + log_info("Currently selected groups: %s", j); + } + + r = ask_string_full(&s, + group_completion_callback, &available, "%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ", special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), username); if (r < 0) @@ -2547,15 +2583,21 @@ static int create_interactively(void) { continue; } - r = show_menu(available, /*n_columns=*/ 3, /*width=*/ 20, /*percentage=*/ 60); + r = show_menu(available, + /* n_columns= */ 3, + /* column_width= */ 20, + /* ellipsize_percentage= */ 60, + /* grey_prefix= */ NULL, + /* with_numbers= */ true); if (r < 0) - return r; + return log_error_errno(r, "Failed to show menu: %m"); putchar('\n'); continue; }; if (!strv_isempty(available)) { + unsigned u; r = safe_atou(s, &u); if (r >= 0) { if (u <= 0 || u > strv_length(available)) { @@ -2607,13 +2649,13 @@ static int create_interactively(void) { shell = mfree(shell); r = ask_string(&shell, - "%s Please enter the shell to use for user %s (empty to skip): ", + "%s Please enter the shell to use for user %s (empty for default): ", special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), username); if (r < 0) return log_error_errno(r, "Failed to query user for username: %m"); if (isempty(shell)) { - log_info("No data entered, skipping."); + log_info("No data entered, leaving at default."); break; } @@ -2664,12 +2706,20 @@ static int verb_firstboot(int argc, char *argv[], void *userdata) { if (r > 0) /* Already created users from credentials */ return 0; - r = has_regular_user(); - if (r < 0) - return r; - if (r > 0) { - log_info("Regular user already present in user database, skipping user creation."); + r = getenv_bool("SYSTEMD_HOME_FIRSTBOOT_OVERRIDE"); + if (r == 0) return 0; + if (r < 0) { + if (r != -ENXIO) + log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m"); + + r = has_regular_user(); + if (r < 0) + return r; + if (r > 0) { + log_info("Regular user already present in user database, skipping user creation."); + return 0; + } } return create_interactively(); diff --git a/src/journal/bsod.c b/src/journal/bsod.c index 2f06808cd4..68361b084a 100644 --- a/src/journal/bsod.c +++ b/src/journal/bsod.c @@ -228,9 +228,9 @@ static int display_emergency_message_fullscreen(const char *message) { goto cleanup; } - r = read_one_char(f, &read_character_buffer, USEC_INFINITY, NULL); + r = read_one_char(f, &read_character_buffer, USEC_INFINITY, /* echo= */ true, /* need_nl= */ NULL); if (r < 0 && r != -EINTR) - log_error_errno(r, "Failed to read character: %m"); + log_warning_errno(r, "Failed to read character, ignoring: %m"); r = 0; diff --git a/src/shared/ask-password-api.c b/src/shared/ask-password-api.c index 91d8945fd1..d1a4f24661 100644 --- a/src/shared/ask-password-api.c +++ b/src/shared/ask-password-api.c @@ -745,9 +745,8 @@ int ask_password_tty( if (ttyfd >= 0) (void) loop_write(ttyfd, NO_ECHO, SIZE_MAX); - } else if (p >= sizeof(passphrase)-1) { - - /* Reached the size limit */ + } else if (char_is_cc(c) || p >= sizeof(passphrase)-1) { + /* Don't accept control chars or overly long passphrases */ if (ttyfd >= 0) (void) loop_write(ttyfd, "\a", 1); diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index 1b447abda9..8e6862c41d 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -1408,4 +1408,23 @@ TEST(strrstr) { assert_se(!strrstr(p, "xx")); } +TEST(str_common_prefix) { + ASSERT_EQ(str_common_prefix("", ""), SIZE_MAX); + ASSERT_EQ(str_common_prefix("a", "a"), SIZE_MAX); + ASSERT_EQ(str_common_prefix("aa", "aa"), SIZE_MAX); + ASSERT_EQ(str_common_prefix("aa", "bb"), 0U); + ASSERT_EQ(str_common_prefix("bb", "aa"), 0U); + ASSERT_EQ(str_common_prefix("aa", "ab"), 1U); + ASSERT_EQ(str_common_prefix("ab", "aa"), 1U); + ASSERT_EQ(str_common_prefix("systemd-resolved", "systemd-networkd"), 8U); + ASSERT_EQ(str_common_prefix("systemd-", "systemd-networkd"), 8U); + ASSERT_EQ(str_common_prefix("systemd-networkd", "systemd-"), 8U); + ASSERT_EQ(str_common_prefix("syst", "systemd-networkd"), 4U); + ASSERT_EQ(str_common_prefix("systemd-networkd", "syst"), 4U); + ASSERT_EQ(str_common_prefix("s", "systemd-networkd"), 1U); + ASSERT_EQ(str_common_prefix("systemd-networkd", "s"), 1U); + ASSERT_EQ(str_common_prefix("", "systemd-networkd"), 0U); + ASSERT_EQ(str_common_prefix("systemd-networkd", ""), 0U); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/test/test-strv.c b/src/test/test-strv.c index b1d30d73a5..f0e9031b9a 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -1277,4 +1277,23 @@ TEST(strv_equal_ignore_order) { ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE("bar", "foo"), STRV_MAKE("bar", "foo", "bar", "foo", "foo"))); } +TEST(strv_filter_prefix) { + char **base = STRV_MAKE("foo", "bar", "baz", "foox", "zzz", "farb", "foerb"); + + _cleanup_strv_free_ char **x = ASSERT_PTR(strv_filter_prefix(base, "fo")); + ASSERT_TRUE(strv_equal(x, STRV_MAKE("foo", "foox", "foerb"))); + x = strv_free(x); + + x = ASSERT_PTR(strv_filter_prefix(base, "")); + ASSERT_TRUE(strv_equal(x, base)); + x = strv_free(x); + + x = ASSERT_PTR(strv_filter_prefix(base, "z")); + ASSERT_TRUE(strv_equal(x, STRV_MAKE("zzz"))); + x = strv_free(x); + + x = ASSERT_PTR(strv_filter_prefix(base, "zzz")); + ASSERT_TRUE(strv_equal(x, STRV_MAKE("zzz"))); +} + DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index ac7eacb01a..87304346e9 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -55,20 +55,20 @@ TEST(read_one_char) { assert_se(fputs("c\n", file) >= 0); rewind(file); - assert_se(read_one_char(file, &r, 1000000, &need_nl) >= 0); + assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) >= 0); assert_se(!need_nl); assert_se(r == 'c'); - assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0); + assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) < 0); rewind(file); assert_se(fputs("foobar\n", file) >= 0); rewind(file); - assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0); + assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) < 0); rewind(file); assert_se(fputs("\n", file) >= 0); rewind(file); - assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0); + assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) < 0); } TEST(getttyname_malloc) { diff --git a/src/test/test-utf8.c b/src/test/test-utf8.c index d60cf00bf3..18974e7664 100644 --- a/src/test/test-utf8.c +++ b/src/test/test-utf8.c @@ -227,6 +227,18 @@ TEST(utf8_to_utf16) { } } +TEST(utf8_last_length) { + ASSERT_EQ(utf8_last_length("", 0), 0U); + ASSERT_EQ(utf8_last_length("", SIZE_MAX), 0U); + ASSERT_EQ(utf8_last_length("a", 1), 1U); + ASSERT_EQ(utf8_last_length("a", SIZE_MAX), 1U); + ASSERT_EQ(utf8_last_length("ä", SIZE_MAX), strlen("ä")); + ASSERT_EQ(utf8_last_length("👊", SIZE_MAX), strlen("👊")); + ASSERT_EQ(utf8_last_length("koffa", SIZE_MAX), 1U); + ASSERT_EQ(utf8_last_length("koffä", SIZE_MAX), strlen("ä")); + ASSERT_EQ(utf8_last_length("koff👊", SIZE_MAX), strlen("👊")); +} + static int intro(void) { log_show_color(true); return EXIT_SUCCESS;