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()+'"',