1
0
mirror of https://github.com/systemd/systemd.git synced 2025-03-28 02:50:16 +03:00

firstboot: add tab completion for interactive questions (#36271)

let's make it easier and quicker to go through firstboot questions by
providing TAB completion
This commit is contained in:
Lennart Poettering 2025-02-17 16:32:54 +01:00 committed by GitHub
commit 78367395a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 563 additions and 72 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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(&copy, "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)) {

View File

@ -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();

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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;