Skip to content

Commit

Permalink
Allow quoting to preserve spaces in the login option in config file
Browse files Browse the repository at this point in the history
There are three ways to quote supported, derived from shell quoting.

- Without surrounding quotes, backspace unconditionally escapes the
  following character. `\ ` is parsed as a single space which does not
  separate words.
- Within double quotes, backspace only escapes backspace and double
  quotes. `"\\"` is parsed as a single backslash, while `"\b"` is parsed
  as the two characters backslash and 'b'. All white space is preserved
  within quotes.
- Within single quotes, backspace does not work as an escape character.
  `'\"'` is parsed as the two characters backslash and dobule quote.
  All white space is preserved within quotes.

Fixes #57
  • Loading branch information
viccie30 committed Jul 25, 2022
1 parent 8ec10c3 commit 51e41dd
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 1 deletion.
6 changes: 6 additions & 0 deletions docs/man/kmscon.1.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@
is parsed as regular option by kmscon.
(default: /bin/login -p)</para>

<para>If this option is specified in the configuration file, the
argument is split into words using the rules of the POSIX shell.
It is possible to include whitespace in arguments by enclosing
the argument in double or single quotes or by prepending it with
a backslash.</para>

<para>This example starts '/bin/bash -i' on each new terminal session:
./kmscon --login --debug --no-switchvt -- /bin/bash -i</para>
</listitem>
Expand Down
2 changes: 1 addition & 1 deletion src/kmscon_conf.c
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ static int file_login(struct conf_option *opt, bool on, const char *arg)
return -EFAULT;
}

ret = shl_split_string(arg, &t, &size, ' ', false);
ret = shl_split_command_string(arg, &t, &size);
if (ret) {
log_error("cannot split 'login' config-option argument");
return ret;
Expand Down
213 changes: 213 additions & 0 deletions src/shl_misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,219 @@ static inline int shl_split_string(const char *arg, char ***out,
return 0;
}

/* This parses \arg and splits the string into a new allocated array. The array
* is stored in \out and is NULL terminated. \out_num is the number of entries
* in the array. You can set it to NULL to not retrieve this value. */
static inline int shl_split_command_string(const char *arg, char ***out,
unsigned int *out_num)
{
unsigned int i;
unsigned int num, len, size, pos;
char **list, *off;
enum { UNQUOTED, DOUBLE_QUOTED, SINGLE_QUOTED } quote_status;
bool in_word;

if (!arg || !out)
return -EINVAL;

num = 0;
size = 0;
len = 0;
quote_status = UNQUOTED;
for (i = 0; arg[i]; ++i) {
switch (arg[i]) {
case ' ':
case '\t':
switch (quote_status) {
case UNQUOTED:
if (len > 0) {
++num;
size += len + 1;
len = 0;
}
break;
case DOUBLE_QUOTED:
case SINGLE_QUOTED:
++len;
break;
}
break;
case '"':
switch (quote_status) {
case UNQUOTED:
quote_status = DOUBLE_QUOTED;
break;
case DOUBLE_QUOTED:
quote_status = UNQUOTED;
break;
case SINGLE_QUOTED:
++len;
break;
}
break;
case '\'':
switch (quote_status) {
case UNQUOTED:
quote_status = SINGLE_QUOTED;
break;
case DOUBLE_QUOTED:
++len;
break;
case SINGLE_QUOTED:
quote_status = UNQUOTED;
break;
}
break;
case '\\':
switch (quote_status) {
case UNQUOTED:
if (!arg[i + 1])
return -EINVAL;
++i;
++len;
break;
case DOUBLE_QUOTED:
if (arg[i + 1] == '"' || arg[i + 1] == '\\')
++i;
++len;
break;
case SINGLE_QUOTED:
++len;
break;
}
break;
default:
++len;
break;
}
}

if (quote_status != UNQUOTED)
return -EINVAL;

if (len > 0) {
++num;
size += len + 1;
}

list = malloc(sizeof(char*) * (num + 1) + size);
if (!list)
return -ENOMEM;

off = (void*)(((char*)list) + (sizeof(char*) * (num + 1)));
len = 0;
pos = 0;
in_word = false;
for (i = 0; arg[i]; ++i) {
switch (arg[i]) {
case ' ':
case '\t':
switch (quote_status) {
case UNQUOTED:
if (in_word) {
in_word = false;
*off = '\0';
++off;
}
break;
case DOUBLE_QUOTED:
case SINGLE_QUOTED:
*off = arg[i];
if (!in_word) {
in_word = true;
list[pos++] = off;
}
++off;
break;
}
break;
case '"':
switch (quote_status) {
case UNQUOTED:
quote_status = DOUBLE_QUOTED;
break;
case DOUBLE_QUOTED:
quote_status = UNQUOTED;
break;
case SINGLE_QUOTED:
*off = arg[i];
if (!in_word) {
in_word = true;
list[pos++] = off;
}
++off;
break;
}
break;
case '\'':
switch (quote_status) {
case UNQUOTED:
quote_status = SINGLE_QUOTED;
break;
case DOUBLE_QUOTED:
*off = arg[i];
if (!in_word) {
in_word = true;
list[pos++] = off;
}
++off;
break;
case SINGLE_QUOTED:
quote_status = UNQUOTED;
break;
}
break;
case '\\':
switch (quote_status) {
case UNQUOTED:
++i;
*off = arg[i];
if (!in_word) {
in_word = true;
list[pos++] = off;
}
++off;
break;
case DOUBLE_QUOTED:
if (arg[i + 1] == '"' || arg[i + 1] == '\\')
++i;
*off = arg[i];
if (!in_word) {
in_word = true;
list[pos++] = off;
}
++off;
break;
case SINGLE_QUOTED:
*off = arg[i];
if (!in_word) {
in_word = true;
list[pos++] = off;
}
++off;
break;
}
break;
default:
*off = arg[i];
if (!in_word) {
in_word = true;
list[pos++] = off;
}
++off;
break;
}
}
if (in_word)
*off = '\0';
list[pos] = NULL;

*out = list;
if (out_num)
*out_num = num;
return 0;
}

static inline int shl_dup_array_size(char ***out, char **argv, size_t len)
{
char **t, *off;
Expand Down
141 changes: 141 additions & 0 deletions tests/test_shl.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,149 @@
*/

#include "test_common.h"
#include "shl_misc.h"

#define check_assert_string_list_eq(X, Y) \
do { \
unsigned int i; \
const char **x, **y; \
\
x = (X); \
y = (Y); \
\
for (i = 0; x[i] && y[i]; ++i) \
ck_assert_str_eq(x[i], y[i]); \
ck_assert_ptr_eq(x[i], NULL); \
ck_assert_ptr_eq(y[i], NULL); \
} while (0)

START_TEST(test_split_command_string)
{
int ret;
unsigned int i, n, n_list, n_expected;
char **list;

const char *invalid_command_strings[] = {
"\"", "'", "\\",
"\"/bin/true", "'/bin/true", "/bin/true\\",
"ls -h \"*.c'",
};
n = sizeof(invalid_command_strings) / sizeof(invalid_command_strings[0]);

for (i = 0; i < n; ++i) {
list = TEST_INVALID_PTR;
n_list = -10;

ret = shl_split_command_string(invalid_command_strings[i],
&list, &n_list);
ck_assert_int_eq(ret, -EINVAL);
ck_assert_ptr_eq(list, TEST_INVALID_PTR);
ck_assert_uint_eq(n_list, (unsigned int)-10);
}

const char *expected_command_list[] = {
"'/bin/command with space",
"\t\\argument=\"quoted\"",
"plain\3argument",
" an\tother='ere",
"\"ends with \\",
"\\\"more\\bquotes\\",
NULL
};
n_expected = sizeof(expected_command_list) /
sizeof(expected_command_list[0]) - 1;
const char *valid_command_strings[] = {
"\\'/bin/command\\ with\\ space \\\t\\\\argument=\\\"quoted\\\" plain\3argument \\ an\\\tother=\\'ere \\\"ends\\ with\\ \\\\ \\\\\\\"more\\\\bquotes\\\\",
"\"'/bin/command with space\" \"\t\\argument=\\\"quoted\\\"\" \"plain\3argument\" \" an\tother='ere\" \"\\\"ends with \\\\\" \"\\\\\\\"more\\bquotes\\\\\"",
"\"'\"'/bin/command with space' '\t\\argument=\"quoted\"' 'plain\3argument' ' an\tother='\"'\"'ere' '\"ends with \\' '\\\"more\\bquotes\\'",
" \\'/bin/command\\ with\\ space\t\t\\\t\\\\argument=\\\"quoted\\\"\t plain\3argument \t\\ an\\\tother=\\'ere \\\"ends\\ with\\ \\\\ \t \\\\\\\"more\\\\\\bquotes\\\\ \t \t",
};
n = sizeof(valid_command_strings) / sizeof(valid_command_strings[0]);

for (i = 0; i < n; ++i) {
list = TEST_INVALID_PTR;
n_list = -10;

ret = shl_split_command_string(valid_command_strings[i], &list,
&n_list);
ck_assert_int_eq(ret, 0);
ck_assert_ptr_ne(list, TEST_INVALID_PTR);
check_assert_string_list_eq((const char**)list, expected_command_list);
ck_assert_uint_eq(n_list, n_expected);
}

const char *empty_command_strings[] = {
"",
" ",
"\t\t \t",
};
n = sizeof(empty_command_strings) / sizeof(empty_command_strings[0]);

for (i = 0; i < n; ++i) {
list = TEST_INVALID_PTR;
n_list = -10;

ret = shl_split_command_string(empty_command_strings[i], &list,
&n_list);
ck_assert_int_eq(ret, 0);
ck_assert_ptr_ne(list, TEST_INVALID_PTR);
ck_assert_ptr_eq(list[0], NULL);
ck_assert_uint_eq(n_list, 0);
}

{
list = TEST_INVALID_PTR;
n_list = -10;

ret = shl_split_command_string(valid_command_strings[0], &list,
&n_list);
ck_assert_int_eq(ret, 0);
ck_assert_ptr_ne(list, TEST_INVALID_PTR);
check_assert_string_list_eq((const char**)list, expected_command_list);
ck_assert_uint_eq(n_list, n_expected);
}

{
list = TEST_INVALID_PTR;

ret = shl_split_command_string(valid_command_strings[0], &list,
NULL);
ck_assert_int_eq(ret, 0);
ck_assert_ptr_ne(list, TEST_INVALID_PTR);
check_assert_string_list_eq((const char**)list, expected_command_list);
}

{
n_list = -10;

ret = shl_split_command_string(valid_command_strings[0], NULL,
&n_list);
ck_assert_int_eq(ret, -EINVAL);
ck_assert_uint_eq(n_list, (unsigned int)-10);
}

{
list = TEST_INVALID_PTR;
n_list = -10;

ret = shl_split_command_string(NULL, &list, &n_list);
ck_assert_int_eq(ret, -EINVAL);
ck_assert_ptr_eq(list, TEST_INVALID_PTR);
ck_assert_uint_eq(n_list, (unsigned int)-10);
}

{
n_list = -10;

ret = shl_split_command_string(NULL, NULL, &n_list);
ck_assert_int_eq(ret, -EINVAL);
ck_assert_uint_eq(n_list, (unsigned int)-10);
}
}
END_TEST

TEST_DEFINE_CASE(misc)
TEST(test_split_command_string)
TEST_END_CASE

TEST_DEFINE(
Expand Down

0 comments on commit 51e41dd

Please sign in to comment.