diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48a66bd..b8056a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: run: pip install meson ninja - name: Install dependencies run: | - sudo apt-get install -y libudev-dev libxkbcommon-dev libdrm-dev libgbm-dev libegl1-mesa-dev libgles-dev libpango1.0-dev libsystemd-dev + sudo apt-get install -y check libudev-dev libxkbcommon-dev libdrm-dev libgbm-dev libegl1-mesa-dev libgles-dev libpango1.0-dev libsystemd-dev - name: Install libtsm run: | pip install cmake @@ -38,7 +38,7 @@ jobs: run: meson setup builddir/ - name: Create source distribution # no unit tests yet - run: meson dist -C builddir/ --no-tests + run: meson dist -C builddir/ - name: Create release note run: tools/extract_release_note.py NEWS ${{ github.workspace }}-release-note.txt - name: Upload Artifact @@ -55,4 +55,3 @@ jobs: with: files: builddir/meson-dist/* body_path: ${{ github.workspace }}-release-note.txt - diff --git a/docs/man/kmscon.1.xml.in b/docs/man/kmscon.1.xml.in index 594de2a..c6ddb0f 100644 --- a/docs/man/kmscon.1.xml.in +++ b/docs/man/kmscon.1.xml.in @@ -207,6 +207,12 @@ is parsed as regular option by kmscon. (default: /bin/login -p) + 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. + This example starts '/bin/bash -i' on each new terminal session: ./kmscon --login --debug --no-switchvt -- /bin/bash -i diff --git a/meson.build b/meson.build index 2c7f57f..3f4fce2 100644 --- a/meson.build +++ b/meson.build @@ -73,6 +73,7 @@ glesv2_deps = dependency('glesv2', disabler: true, required: require_glesv2) pango_deps = dependency('pangoft2', disabler: true, required: get_option('font_pango')) pixman_deps = dependency('pixman-1', disabler: true, required: get_option('renderer_pixman')) xsltproc = find_program('xsltproc', native: true, disabler: true, required: get_option('docs')) +check_deps = dependency('check', disabler: true, required: get_option('tests')) # # Handle feature options diff --git a/src/kmscon_conf.c b/src/kmscon_conf.c index dc328ef..2f3b151 100644 --- a/src/kmscon_conf.c +++ b/src/kmscon_conf.c @@ -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; diff --git a/src/shl_misc.h b/src/shl_misc.h index fc37d9d..b994bd3 100644 --- a/src/shl_misc.h +++ b/src/shl_misc.h @@ -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; diff --git a/tests/meson.build b/tests/meson.build index 2e759a1..4e05bb5 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -17,5 +17,12 @@ foreach name, deps : { exe = executable(test_name, f'@test_name@.c', dependencies: deps, ) - test(test_name, exe) endforeach + +test_shl = executable('test_shl', 'test_shl.c', + dependencies: [shl_deps, check_deps], +) +test('test_shl', test_shl, + protocol: 'tap', + env: {'CK_TAP_LOG_FILE_NAME': '-', 'CK_VERBOSITY': 'silent'}, +) diff --git a/tests/test_common.h b/tests/test_common.h new file mode 100644 index 0000000..34338fc --- /dev/null +++ b/tests/test_common.h @@ -0,0 +1,122 @@ +/* + * Kmscon - Test Helper + * + * Copyright (c) 2012-2013 David Herrmann + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * Test Helper + * This header includes all kinds of helpers for testing. It tries to include + * everything required and provides simple macros to avoid duplicating code in + * each test. We try to keep tests as small as possible and move everything that + * might be common here. + * + * We avoid sticking to our usual coding conventions (including headers in + * source files, etc. ..) and instead make this the most convenient we can. + */ + +#ifndef TEST_COMMON_H +#define TEST_COMMON_H + +#include +#include +#include +#include +#include +#include + +/* lower address-space is protected from user-allocation, so this is invalid */ +#define TEST_INVALID_PTR ((void*)0x10) + +#define UNUSED(x) (void)(x) + +#define TEST_DEFINE_CASE(_name) \ + static TCase *test_create_case_##_name(void) \ + { \ + TCase *tc; \ + \ + tc = tcase_create(#_name); \ + +#define TEST(_name) tcase_add_test(tc, _name); + +#define TEST_END_CASE \ + return tc; \ + } \ + +#define TEST_END NULL + +#define TEST_CASE(_name) test_create_case_##_name + +static inline Suite *test_create_suite(const char *name, ...) +{ + Suite *s; + va_list list; + TCase *(*fn)(void); + + s = suite_create(name); + + va_start(list, name); + while ((fn = va_arg(list, TCase *(*)(void)))) + suite_add_tcase(s, fn()); + va_end(list); + + return s; +} + +#define TEST_SUITE(_name, ...) test_create_suite((#_name), ##__VA_ARGS__) + +static inline int test_run_suite(Suite *s) +{ + int ret; + SRunner *sr; + + sr = srunner_create(s); + srunner_run_all(sr, CK_ENV); + ret = srunner_ntests_failed(sr); + srunner_free(sr); + + return ret; +} + +#define TEST_DEFINE(_suite) \ + int main(int argc, char **argv) \ + { \ + return test_run_suite(_suite); \ + } + +#ifndef ck_assert_mem_eq +#include +#define ck_assert_mem_eq(_x, _y, _len) \ + ck_assert(memcmp((_x), (_y), (_len)) == 0) +#define ck_assert_mem_ne(_x, _y, _len) \ + ck_assert(memcmp((_x), (_y), (_len)) != 0) +#define ck_assert_mem_lt(_x, _y, _len) \ + ck_assert(memcmp((_x), (_y), (_len)) < 0) +#define ck_assert_mem_le(_x, _y, _len) \ + ck_assert(memcmp((_x), (_y), (_len)) <= 0) +#define ck_assert_mem_gt(_x, _y, _len) \ + ck_assert(memcmp((_x), (_y), (_len)) > 0) +#define ck_assert_mem_ge(_x, _y, _len) \ + ck_assert(memcmp((_x), (_y), (_len)) >= 0) +#endif + +#endif /* TEST_COMMON_H */ diff --git a/tests/test_shl.c b/tests/test_shl.c new file mode 100644 index 0000000..b287e91 --- /dev/null +++ b/tests/test_shl.c @@ -0,0 +1,177 @@ +/* + * test_shl - Test shl library + * + * Copyright (c) 2022 Victor Westerhuis + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#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( + TEST_SUITE(shl, + TEST_CASE(misc), + TEST_END + ) +)