From 48e5684b3c37b337edd7004e68fc0690b58a84e6 Mon Sep 17 00:00:00 2001 From: Marcel Hollerbach Date: Wed, 26 Dec 2018 15:31:57 +0100 Subject: [PATCH] ecore: here comes a command line object the mixin for now can carry a command, which can be setted as an string. The string is then parsed again, this is done in order to make sure that everything that needs escaping really is escaped or parsed correctly. Differential Revision: https://phab.enlightenment.org/D7516 --- src/Makefile_Ecore.am | 12 +- src/lib/ecore/Ecore_Eo.h | 1 + src/lib/ecore/efl_core_command_line.c | 267 +++++++++++++++++++++++++ src/lib/ecore/efl_core_command_line.eo | 80 ++++++++ src/lib/ecore/meson.build | 4 +- src/tests/ecore/efl_app_suite.c | 1 + src/tests/ecore/efl_app_suite.h | 1 + src/tests/ecore/efl_app_test_cml.c | 85 ++++++++ src/tests/ecore/efl_app_test_cml.eo | 4 + src/tests/ecore/meson.build | 22 +- 10 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 src/lib/ecore/efl_core_command_line.c create mode 100644 src/lib/ecore/efl_core_command_line.eo create mode 100644 src/tests/ecore/efl_app_test_cml.c create mode 100644 src/tests/ecore/efl_app_test_cml.eo diff --git a/src/Makefile_Ecore.am b/src/Makefile_Ecore.am index 5f10ea7f2e..fd6074dec3 100644 --- a/src/Makefile_Ecore.am +++ b/src/Makefile_Ecore.am @@ -52,6 +52,10 @@ ecore_eolian_files_public = \ lib/ecore/efl_view_model.eo \ lib/ecore/efl_core_env.eo \ lib/ecore/efl_core_proc_env.eo \ + lib/ecore/efl_core_command_line.eo + +ecore_test_eolian_files = \ + tests/ecore/efl_app_test_cml.eo ecore_eolian_files = \ $(ecore_eolian_files_legacy) \ @@ -60,10 +64,14 @@ ecore_eolian_files = \ ecore_eolian_c = $(ecore_eolian_files:%.eo=%.eo.c) ecore_eolian_h = $(ecore_eolian_files:%.eo=%.eo.h) \ $(ecore_eolian_files_legacy:%.eo=%.eo.legacy.h) +ecore_test_c = $(ecore_test_eolian_files:%.eo=%.eo.c) +ecore_test_h = $(ecore_test_eolian_files:%.eo=%.eo.h) BUILT_SOURCES += \ $(ecore_eolian_c) \ - $(ecore_eolian_h) + $(ecore_eolian_h) \ + $(ecore_test_c) \ + $(ecore_test_h) ecoreeolianfilesdir = $(datadir)/eolian/include/ecore-@VMAJ@ ecoreeolianfiles_DATA = $(ecore_eolian_files_public) lib/ecore/efl_loop_timer.eo @@ -100,6 +108,7 @@ lib/ecore/ecore_job.c \ lib/ecore/ecore_main.c \ lib/ecore/ecore_event_message.c \ lib/ecore/ecore_event_message_handler.c \ +lib/ecore/efl_core_command_line.c \ lib/ecore/efl_core_env.c \ lib/ecore/efl_core_proc_env.c \ lib/ecore/efl_app.c \ @@ -339,6 +348,7 @@ tests/ecore/efl_app_test_loop.c \ tests/ecore/efl_app_test_loop_fd.c \ tests/ecore/efl_app_test_loop_timer.c \ tests/ecore/efl_app_test_promise.c \ +tests/ecore/efl_app_test_cml.c \ tests/ecore/efl_app_test_env.c \ tests/ecore/efl_app_suite.c \ tests/ecore/efl_app_suite.h diff --git a/src/lib/ecore/Ecore_Eo.h b/src/lib/ecore/Ecore_Eo.h index 348b0f5b6d..3615219c38 100644 --- a/src/lib/ecore/Ecore_Eo.h +++ b/src/lib/ecore/Ecore_Eo.h @@ -28,6 +28,7 @@ #include "efl_core_env.eo.h" #include "efl_core_proc_env.eo.h" +#include "efl_core_command_line.eo.h" #include "efl_loop_message.eo.h" #include "efl_loop_message_handler.eo.h" diff --git a/src/lib/ecore/efl_core_command_line.c b/src/lib/ecore/efl_core_command_line.c new file mode 100644 index 0000000000..74ae690c26 --- /dev/null +++ b/src/lib/ecore/efl_core_command_line.c @@ -0,0 +1,267 @@ +#ifdef HAVE_CONFIG_H +# include +#endif + +#define EFL_CORE_COMMAND_LINE_PROTECTED + +#include + +#define MY_CLASS EFL_CORE_COMMAND_LINE_MIXIN + +typedef struct { + Eina_Bool filled; + char *string_command; + Eina_Array *command; +} Efl_Core_Command_Line_Data; + +static Eina_Array * +_unescape(const char *s) +{ + Eina_Array *args; + const char *p; + char *tmp = NULL, *d = NULL; + if (!s) return NULL; + + Eina_Bool in_quote_dbl = EINA_FALSE; + Eina_Bool in_quote = EINA_FALSE; + + args = eina_array_new(16); + if (!args) return NULL; + for (p = s; *p; p++) + { + if (!tmp) tmp = d = strdup(p); + if (tmp) + { + if (in_quote_dbl) + { + switch (*p) + { + case '\"': + in_quote_dbl = EINA_FALSE; + *d = 0; + eina_array_push(args, eina_stringshare_add(tmp)); + free(tmp); + tmp = d = NULL; + break; + case '\\': + p++; + EINA_FALLTHROUGH + default: + *d = *p; + d++; + break; + } + } + else if (in_quote) + { + switch (*p) + { + case '\'': + in_quote = EINA_FALSE; + *d = 0; + eina_array_push(args, eina_stringshare_add(tmp)); + free(tmp); + tmp = d = NULL; + break; + case '\\': + p++; + EINA_FALLTHROUGH + default: + *d = *p; + d++; + break; + } + } + else + { + switch (*p) + { + case ' ': + case '\t': + case '\r': + case '\n': + *d = 0; + eina_array_push(args, eina_stringshare_add(tmp)); + free(tmp); + tmp = d = NULL; + break; + case '\"': + in_quote_dbl = EINA_TRUE; + break; + case '\'': + in_quote = EINA_TRUE; + break; + case '\\': + p++; + EINA_FALLTHROUGH + default: + *d = *p; + d++; + break; + } + } + } + } + if (tmp) + { + *d = 0; + eina_array_push(args, eina_stringshare_add(tmp)); + free(tmp); + } + return args; +} + +static char * +_escape(const char *s) +{ + Eina_Bool need_quote = EINA_FALSE; + const char *p; + char *s2 = malloc((strlen(s) * 2) + 1 + 2), *d; + + if (!s2) return NULL; + + for (p = s; *p; p++) + { + switch (*p) + { + case '\'': + case '\"': + case '$': + case '#': + case ';': + case '&': + case '`': + case '|': + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + case '>': + case '<': + case '\n': + case '\r': + case '\t': + case ' ': + need_quote = EINA_TRUE; + default: + break; + } + } + + d = s2; + if (need_quote) + { + *d = '\"'; + d++; + } + for (p = s; *p; p++, d++) + { + switch (*p) + { + case '\\': + case '\'': + case '\"': + *d = '\\'; + d++; + EINA_FALLTHROUGH + default: + *d = *p; + break; + } + } + if (need_quote) + { + *d = '\"'; + d++; + } + *d = 0; + return s2; +} + +EOLIAN static const char* +_efl_core_command_line_command_get(const Eo *obj EINA_UNUSED, Efl_Core_Command_Line_Data *pd) +{ + return eina_strdup(pd->string_command); +} + +EOLIAN static Eina_Accessor* +_efl_core_command_line_command_access(Eo *obj EINA_UNUSED, Efl_Core_Command_Line_Data *pd) +{ + return pd->command ? eina_array_accessor_new(pd->command) : NULL; +} + +static void +_remove_invalid_chars(char *command) +{ + for (unsigned int i = 0; i < strlen(command); ++i) + { + char c = command[i]; + if (c < 0x20 || c == 0x7f) + command[i] = '\x12'; + } +} + +EOLIAN static Eina_Bool +_efl_core_command_line_command_array_set(Eo *obj EINA_UNUSED, Efl_Core_Command_Line_Data *pd, Eina_Array *array) +{ + EINA_SAFETY_ON_TRUE_RETURN_VAL(pd->filled, EINA_FALSE); + Eina_Strbuf *command = eina_strbuf_new(); + unsigned int i = 0; + + pd->command = eina_array_new(eina_array_count(array)); + for (i = 0; i < (array ? eina_array_count(array) : 0); ++i) + { + char *content = eina_array_data_get(array, i); + char *param = calloc(1, strlen(content)); + + if (!param) + { + free(param); + while (eina_array_count(pd->command) > 0) + eina_stringshare_del(eina_array_pop(pd->command)); + eina_array_free(pd->command); + pd->command = NULL; + eina_array_free(array); + return EINA_FALSE; + } + + //build the command + if (i != 0) + eina_strbuf_append(command, " "); + eina_strbuf_append(command, _escape(content)); + //convert string to stringshare + strcpy(param, content); + _remove_invalid_chars(param); + eina_array_push(pd->command, eina_stringshare_add(param)); + free(param); + } + pd->string_command = eina_strbuf_release(command); + pd->filled = EINA_TRUE; + eina_array_free(array); + + return EINA_TRUE; +} + +EOLIAN static Eina_Bool +_efl_core_command_line_command_string_set(Eo *obj EINA_UNUSED, Efl_Core_Command_Line_Data *pd, const char *str) +{ + EINA_SAFETY_ON_TRUE_RETURN_VAL(pd->filled, EINA_FALSE); + + pd->string_command = eina_strdup(str); + _remove_invalid_chars(pd->string_command); + pd->command = _unescape(str); + if (!pd->command) + { + if (pd->string_command) + free(pd->string_command); + pd->string_command = NULL; + return EINA_FALSE; + } + pd->filled = EINA_TRUE; + + return EINA_TRUE; +} + +#include "efl_core_command_line.eo.c" diff --git a/src/lib/ecore/efl_core_command_line.eo b/src/lib/ecore/efl_core_command_line.eo new file mode 100644 index 0000000000..1cbb020856 --- /dev/null +++ b/src/lib/ecore/efl_core_command_line.eo @@ -0,0 +1,80 @@ +mixin Efl.Core.Command_Line { + [[A mixin that implements standard functions for command lines. + + This object parses the command line that gets passed, later the object can be accessed via accessor or the string directly. + ]] + methods { + @property command { + [[ A commandline that encodes arguments in a command string. + This command is unix shell-style, thus whitespace separates + arguments unless escaped. Also a semi-colon ';', ampersand + '&', pipe/bar '|', hash '#', bracket, square brace, brace + character ('(', ')', '[', ']', '{', '}'), exclamation + mark '!', backquote '`', greator or less than ('>' '<') + character unless escaped or in quotes would cause + args_count/value to not be generated properly, because + it would force complex shell interpretation which + will not be supported in evaluating the arg_count/value + information, but the final shell may interpret this if this + is executed via a command-line shell. To not be a complex + shell command, it should be simple with paths, options + and variable expansions, but nothing more complex involving + the above unescaped characters. + + "cat -option /path/file" + "cat 'quoted argument'" + "cat ~/path/escaped\ argument" + "/bin/cat escaped\ argument $VARIABLE" + etc. + + It should not try and use "complex shell features" if you + want the arg_count and arg_value set to be correct after + setting the command string. For example none of: + + "VAR=x /bin/command && /bin/othercommand >& /dev/null" + "VAR=x /bin/command `/bin/othercommand` | /bin/cmd2 && cmd3 &" + etc. + + If you set the command the arg_count/value property contents + can change and be completely re-evaluated by parsing the + command string into an argument array set along with + interpreting escapes back into individual argument strings. + ]] + get { + + } + values { + commandline : string; + } + } + command_access { + [[ Get the accessor which enables access to each argument that got passed to this object. ]] + return : accessor; + } + @property command_array { + [[ Use an array to fill this object + + Every element of a string is a argument. + ]] + set { + return : bool; [[On success $true, $false otherwise]] + } + values { + array : array @owned; [[An array where every array field is an argument]] + } + } + @property command_string { + [[ Use a string to fill this object + + The string will be split at every unescaped ' ', every resulting substring will be a new argument to the command line. + ]] + set { + return : bool; [[On success $true, $false otherwise]] + } + values { + str : string; [[A command in form of a string]] + } + + } + } +} diff --git a/src/lib/ecore/meson.build b/src/lib/ecore/meson.build index 98909cb618..375f745abd 100644 --- a/src/lib/ecore/meson.build +++ b/src/lib/ecore/meson.build @@ -76,7 +76,8 @@ pub_eo_files = [ 'efl_composite_model.eo', 'efl_view_model.eo', 'efl_core_env.eo', - 'efl_core_proc_env.eo' + 'efl_core_proc_env.eo', + 'efl_core_command_line.eo', ] foreach eo_file : pub_eo_files @@ -184,6 +185,7 @@ ecore_src = [ 'efl_appthread.c', 'efl_core_env.c', 'efl_core_proc_env.c', + 'efl_core_command_line.c', ] if sys_windows == true diff --git a/src/tests/ecore/efl_app_suite.c b/src/tests/ecore/efl_app_suite.c index cd26e2d95e..2cab632622 100644 --- a/src/tests/ecore/efl_app_suite.c +++ b/src/tests/ecore/efl_app_suite.c @@ -53,6 +53,7 @@ static const Efl_Test_Case etc[] = { { "Promise", efl_app_test_promise_3 }, { "Promise", efl_app_test_promise_safety }, { "Env", efl_test_efl_env }, + { "CML", efl_test_efl_cml }, { NULL, NULL } }; diff --git a/src/tests/ecore/efl_app_suite.h b/src/tests/ecore/efl_app_suite.h index 3a66dcdfcf..874d2bb503 100644 --- a/src/tests/ecore/efl_app_suite.h +++ b/src/tests/ecore/efl_app_suite.h @@ -12,5 +12,6 @@ void efl_app_test_promise_2(TCase *tc); void efl_app_test_promise_3(TCase *tc); void efl_app_test_promise_safety(TCase *tc); void efl_test_efl_env(TCase *tc); +void efl_test_efl_cml(TCase *tc); #endif /* _EFL_APP_SUITE_H */ diff --git a/src/tests/ecore/efl_app_test_cml.c b/src/tests/ecore/efl_app_test_cml.c new file mode 100644 index 0000000000..1b7cebf552 --- /dev/null +++ b/src/tests/ecore/efl_app_test_cml.c @@ -0,0 +1,85 @@ +#ifdef HAVE_CONFIG_H +# include +#endif + +#define EFL_CORE_COMMAND_LINE_PROTECTED + +#include +#include +#define EFL_NOLEGACY_API_SUPPORT +#include +#include "efl_app_suite.h" +#include "../efl_check.h" + +typedef struct { + +} Efl_App_Test_CML_Data; + +#include "efl_app_test_cml.eo.h" +#include "efl_app_test_cml.eo.c" + +static Eina_Array* +_construct_array(void) +{ + Eina_Array *array = eina_array_new(16); + + eina_array_push(array, "/bin/sh"); + eina_array_push(array, "-C"); + eina_array_push(array, "foo"); + eina_array_push(array, "--test"); + eina_array_push(array, "--option=done"); + eina_array_push(array, "--"); + eina_array_push(array, "asdf --test"); + return array; +} + +static const char* +_construct_string(void) +{ + return "/bin/sh -C foo --test --option=done -- \"asdf --test\""; +} + +EFL_START_TEST(efl_core_cml_string) +{ + Efl_App_Test_CML *cml = efl_add_ref(EFL_APP_TEST_CML_CLASS, NULL); + Eina_Array *content = _construct_array(); + Eina_Stringshare *str; + Eina_Bool b; + int i = 0; + + b = efl_core_command_line_command_string_set(cml, _construct_string()); + ck_assert_int_ne(b, 0); + + EINA_ACCESSOR_FOREACH(efl_core_command_line_command_access(cml), i, str) + { + ck_assert_str_eq(eina_array_data_get(content, i), str); + } + ck_assert_str_eq(efl_core_command_line_command_get(cml), _construct_string()); +} +EFL_END_TEST + +EFL_START_TEST(efl_core_cml_array) +{ + Efl_App_Test_CML *cml = efl_add_ref(EFL_APP_TEST_CML_CLASS, NULL); + Eina_Array *content1 = _construct_array(); + Eina_Array *content2 = _construct_array(); + Eina_Stringshare *str; + Eina_Bool b; + int i = 0; + + b = efl_core_command_line_command_array_set(cml, content1); + ck_assert_int_ne(b, 0); + + EINA_ACCESSOR_FOREACH(efl_core_command_line_command_access(cml), i, str) + { + ck_assert_str_eq(eina_array_data_get(content2, i), str); + } + ck_assert_str_eq(efl_core_command_line_command_get(cml), _construct_string()); +} +EFL_END_TEST + +void efl_test_efl_cml(TCase *tc) +{ + tcase_add_test(tc, efl_core_cml_string); + tcase_add_test(tc, efl_core_cml_array); +} diff --git a/src/tests/ecore/efl_app_test_cml.eo b/src/tests/ecore/efl_app_test_cml.eo new file mode 100644 index 0000000000..b0877e0cf7 --- /dev/null +++ b/src/tests/ecore/efl_app_test_cml.eo @@ -0,0 +1,4 @@ +class Efl.App.Test.CML extends Efl.Object implements Efl.Core.Command_Line +{ + +} diff --git a/src/tests/ecore/meson.build b/src/tests/ecore/meson.build index e3b4f6c851..c49d941355 100644 --- a/src/tests/ecore/meson.build +++ b/src/tests/ecore/meson.build @@ -76,14 +76,32 @@ efl_app_suite_src = [ 'efl_app_test_loop_fd.c', 'efl_app_test_loop_timer.c', 'efl_app_test_promise.c', - 'efl_app_test_env.c' + 'efl_app_test_env.c', + 'efl_app_test_cml.c', ] +priv_eo_files = [ + 'efl_app_test_cml.eo', +] + +priv_eo_file_target = [] +foreach eo_file : priv_eo_files + priv_eo_file_target += custom_target('eolian_gen_' + eo_file, + input : eo_file, + output : [eo_file + '.h'], + depfile : eo_file + '.d', + command : eolian_gen + [ '-I', meson.current_source_dir(), eolian_include_directories, + '-o', 'h:' + join_paths(meson.current_build_dir(), eo_file + '.h'), + '-o', 'c:' + join_paths(meson.current_build_dir(), eo_file + '.c'), + '-o', 'd:' + join_paths(meson.current_build_dir(), eo_file + '.d'), + '-gchd', '@INPUT@']) +endforeach + efl_app_suite_deps = [m] efl_app_suite_deps += ecore efl_app_suite = executable('efl_app_suite', - efl_app_suite_src, + efl_app_suite_src, priv_eo_file_target, dependencies: [efl_app_suite_deps, check], c_args : [ '-DTESTS_BUILD_DIR="'+meson.current_build_dir()+'"',