diff --git a/src/libgit2/transports/ssh_exec.c b/src/libgit2/transports/ssh_exec.c index 2bc98555539..f8dc56502e1 100644 --- a/src/libgit2/transports/ssh_exec.c +++ b/src/libgit2/transports/ssh_exec.c @@ -120,7 +120,7 @@ GIT_INLINE(int) ensure_transport_state( } static int get_ssh_cmdline( - git_str *out, + git_vector *args, ssh_exec_subtransport *transport, git_net_url *url, const char *command) @@ -128,7 +128,8 @@ static int get_ssh_cmdline( git_remote *remote = ((transport_smart *)transport->owner)->owner; git_repository *repo = remote->repo; git_config *cfg; - git_str ssh_cmd = GIT_STR_INIT; + git_str ssh_cmd = GIT_STR_INIT, ssh_path = GIT_STR_INIT, + url_and_host = GIT_STR_INIT, remote_cmd = GIT_STR_INIT; const char *default_ssh_cmd = "ssh"; int error; @@ -158,17 +159,45 @@ static int get_ssh_cmdline( else if ((error = git_config__get_string_buf(&ssh_cmd, cfg, "core.sshcommand")) < 0 && error != GIT_ENOTFOUND) goto done; - error = git_str_printf(out, "%s %s %s \"%s%s%s\" \"%s '%s'\"", - ssh_cmd.size > 0 ? ssh_cmd.ptr : default_ssh_cmd, - url->port_specified ? "-p" : "", - url->port_specified ? url->port : "", - url->username ? url->username : "", - url->username ? "@" : "", - url->host, - command, - url->path); + git_error_clear(); + + if ((error = git_fs_path_find_executable(&ssh_path, + ssh_cmd.size > 0 ? ssh_cmd.ptr : default_ssh_cmd)) < 0) + goto done; + + if ((error = git_vector_insert(args, git_str_detach(&ssh_path))) < 0) + goto done; + + if (url->port_specified) { + char *p = git__strdup("-p"); + char *port = git__strdup(url->port); + + if (!p || !port || + (error = git_vector_insert(args, p)) < 0 || + (error = git_vector_insert(args, port)) < 0) + goto done; + } + + if (url->username) { + if ((error = git_str_puts(&url_and_host, url->username)) < 0 || + (error = git_str_putc(&url_and_host, '@')) < 0) + goto done; + } + + if ((error = git_str_puts(&url_and_host, url->host)) < 0 || + (error = git_vector_insert(args, git_str_detach(&url_and_host))) < 0) + goto done; + + if ((error = git_str_puts(&remote_cmd, command)) < 0 || + (error = git_str_puts(&remote_cmd, " '")) < 0 || + (error = git_str_puts_escaped(&remote_cmd, url->path, "'!", "'\\", "'")) < 0 || + (error = git_str_puts(&remote_cmd, "'")) < 0 || + (error = git_vector_insert(args, git_str_detach(&remote_cmd))) < 0) + goto done; done: + git_str_dispose(&remote_cmd); + git_str_dispose(&url_and_host); git_str_dispose(&ssh_cmd); git_config_free(cfg); return error; @@ -183,7 +212,7 @@ static int start_ssh( git_process_options process_opts = GIT_PROCESS_OPTIONS_INIT; git_net_url url = GIT_NET_URL_INIT; - git_str ssh_cmdline = GIT_STR_INIT; + git_vector args = GIT_VECTOR_INIT; const char *command; int error; @@ -214,11 +243,12 @@ static int start_ssh( if (error < 0) goto done; - if ((error = get_ssh_cmdline(&ssh_cmdline, transport, &url, command)) < 0) + if ((error = get_ssh_cmdline(&args, transport, &url, command)) < 0) goto done; - if ((error = git_process_new_from_cmdline(&transport->process, - ssh_cmdline.ptr, env, ARRAY_SIZE(env), &process_opts)) < 0 || + if ((error = git_process_new(&transport->process, + (const char **)args.contents, args.length, + env, ARRAY_SIZE(env), &process_opts)) < 0 || (error = git_process_start(transport->process)) < 0) { git_process_free(transport->process); transport->process = NULL; @@ -226,7 +256,7 @@ static int start_ssh( } done: - git_str_dispose(&ssh_cmdline); + git_vector_dispose_deep(&args); git_net_url_dispose(&url); return error; } diff --git a/src/util/fs_path.c b/src/util/fs_path.c index e24836becaa..ff0836ff874 100644 --- a/src/util/fs_path.c +++ b/src/util/fs_path.c @@ -611,6 +611,37 @@ bool git_fs_path_isfile(const char *path) return S_ISREG(st.st_mode) != 0; } +#ifdef GIT_WIN32 + +bool git_fs_path_isexecutable(const char *path) +{ + struct stat st; + + GIT_ASSERT_ARG_WITH_RETVAL(path, false); + + if (git__suffixcmp_icase(path, ".exe") != 0 && + git__suffixcmp_icase(path, ".cmd") != 0) + return false; + + return (p_stat(path, &st) == 0); +} + +#else + +bool git_fs_path_isexecutable(const char *path) +{ + struct stat st; + + GIT_ASSERT_ARG_WITH_RETVAL(path, false); + if (p_stat(path, &st) < 0) + return false; + + return S_ISREG(st.st_mode) != 0 && + ((st.st_mode & S_IXUSR) != 0); +} + +#endif + bool git_fs_path_islink(const char *path) { struct stat st; @@ -2020,9 +2051,10 @@ int git_fs_path_owner_is_system(bool *out, const char *path) return git_fs_path_owner_is(out, path, GIT_FS_PATH_OWNER_ADMINISTRATOR); } -int git_fs_path_find_executable(git_str *fullpath, const char *executable) -{ #ifdef GIT_WIN32 + +static int find_executable(git_str *fullpath, const char *executable) +{ git_win32_path fullpath_w, executable_w; int error; @@ -2035,9 +2067,15 @@ int git_fs_path_find_executable(git_str *fullpath, const char *executable) error = git_str_put_w(fullpath, fullpath_w, wcslen(fullpath_w)); return error; +} + #else + +static int find_executable(git_str *fullpath, const char *executable) +{ git_str path = GIT_STR_INIT; const char *current_dir, *term; + size_t current_dirlen; bool found = false; if (git__getenv(&path, "PATH") < 0) @@ -2049,20 +2087,28 @@ int git_fs_path_find_executable(git_str *fullpath, const char *executable) if (! (term = strchr(current_dir, GIT_PATH_LIST_SEPARATOR))) term = strchr(current_dir, '\0'); + current_dirlen = term - current_dir; git_str_clear(fullpath); - if (git_str_put(fullpath, current_dir, (term - current_dir)) < 0 || - git_str_putc(fullpath, '/') < 0 || + + /* An empty path segment is treated as '.' */ + if (current_dirlen == 0 && git_str_putc(fullpath, '.')) + return -1; + else if (current_dirlen != 0 && + git_str_put(fullpath, current_dir, current_dirlen) < 0) + return -1; + + if (git_str_putc(fullpath, '/') < 0 || git_str_puts(fullpath, executable) < 0) return -1; - if (git_fs_path_isfile(fullpath->ptr)) { + if (git_fs_path_isexecutable(fullpath->ptr)) { found = true; break; } current_dir = term; - while (*current_dir == GIT_PATH_LIST_SEPARATOR) + if (*current_dir == GIT_PATH_LIST_SEPARATOR) current_dir++; } @@ -2073,5 +2119,19 @@ int git_fs_path_find_executable(git_str *fullpath, const char *executable) git_str_clear(fullpath); return GIT_ENOTFOUND; +} + #endif + +int git_fs_path_find_executable(git_str *fullpath, const char *executable) +{ + /* For qualified paths we do not look in PATH */ + if (strchr(executable, '/') != NULL) { + if (!git_fs_path_isexecutable(executable)) + return GIT_ENOTFOUND; + + return git_str_puts(fullpath, executable); + } + + return find_executable(fullpath, executable); } diff --git a/src/util/fs_path.h b/src/util/fs_path.h index 78b4c3dc0d9..d6cadc7dd23 100644 --- a/src/util/fs_path.h +++ b/src/util/fs_path.h @@ -204,6 +204,12 @@ extern bool git_fs_path_isdir(const char *path); */ extern bool git_fs_path_isfile(const char *path); +/** + * Check if the given path points to an executable. + * @return true or false + */ +extern bool git_fs_path_isexecutable(const char *path); + /** * Check if the given path points to a symbolic link. * @return true or false diff --git a/src/util/process.h b/src/util/process.h index 3ada6696d22..cf8d0de568a 100644 --- a/src/util/process.h +++ b/src/util/process.h @@ -84,6 +84,10 @@ extern int git_process_new( * are not thread safe, so you may not modify the environment during * this call. You can avoid this by setting `exclude_env` in the * options and providing the entire environment yourself. + * + * This function passes the `cmdline` to a shell and DOES NOT perform + * any safe escaping on the `cmdline` and SHOULD NOT be used on user + * input. Instead, use `git_process_new`. */ extern int git_process_new_from_cmdline( git_process **out, diff --git a/src/util/str.c b/src/util/str.c index 0b07c814702..7b8d99bc5f8 100644 --- a/src/util/str.c +++ b/src/util/str.c @@ -1065,10 +1065,13 @@ int git_str_puts_escaped( git_str *buf, const char *string, const char *esc_chars, - const char *esc_with) + const char *esc_prefix, + const char *esc_suffix) { const char *scan; - size_t total = 0, esc_len = strlen(esc_with), count, alloclen; + size_t total = 0, count, alloclen; + size_t esc_prefix_len = esc_prefix ? strlen(esc_prefix) : 0; + size_t esc_suffix_len = esc_suffix ? strlen(esc_suffix) : 0; if (!string) return 0; @@ -1080,7 +1083,7 @@ int git_str_puts_escaped( scan += count; /* count run of escaped characters */ count = strspn(scan, esc_chars); - total += count * (esc_len + 1); + total += count * (esc_prefix_len + esc_suffix_len + 1); scan += count; } @@ -1096,13 +1099,22 @@ int git_str_puts_escaped( buf->size += count; for (count = strspn(scan, esc_chars); count > 0; --count) { - /* copy escape sequence */ - memmove(buf->ptr + buf->size, esc_with, esc_len); - buf->size += esc_len; + /* copy escape prefix sequence */ + if (esc_prefix) { + memmove(buf->ptr + buf->size, esc_prefix, esc_prefix_len); + buf->size += esc_prefix_len; + } + /* copy character to be escaped */ buf->ptr[buf->size] = *scan; buf->size++; scan++; + + /* copy escape suffix sequence */ + if (esc_suffix) { + memmove(buf->ptr + buf->size, esc_suffix, esc_suffix_len); + buf->size += esc_suffix_len; + } } } diff --git a/src/util/str.h b/src/util/str.h index 588e6fc22bc..ad9b892cda3 100644 --- a/src/util/str.h +++ b/src/util/str.h @@ -268,21 +268,23 @@ int git_str_splice( * @param str String buffer to append data to * @param string String to escape and append * @param esc_chars Characters to be escaped - * @param esc_with String to insert in from of each found character + * @param esc_prefix String to insert as prefix of each found character + * @param esc_suffix String to insert as suffix of each found character * @return 0 on success, <0 on failure (probably allocation problem) */ extern int git_str_puts_escaped( git_str *str, const char *string, const char *esc_chars, - const char *esc_with); + const char *esc_prefix, + const char *esc_suffix); /** * Append string escaping characters that are regex special */ GIT_INLINE(int) git_str_puts_escape_regex(git_str *str, const char *string) { - return git_str_puts_escaped(str, string, "^.[]$()|*+?{}\\", "\\"); + return git_str_puts_escaped(str, string, "^.[]$()|*+?{}\\", "\\", NULL); } /** diff --git a/src/util/util.c b/src/util/util.c index e86bceeb5a8..3e4d93443ba 100644 --- a/src/util/util.c +++ b/src/util/util.c @@ -269,13 +269,26 @@ int git__prefixncmp_icase(const char *str, size_t str_n, const char *prefix) return prefixcmp(str, str_n, prefix, true); } -int git__suffixcmp(const char *str, const char *suffix) +static int suffixcmp(const char *str, const char *suffix, bool icase) { size_t a = strlen(str); size_t b = strlen(suffix); + if (a < b) return -1; - return strcmp(str + (a - b), suffix); + + return icase ? strcasecmp(str + (a - b), suffix) : + strcmp(str + (a - b), suffix); +} + +int git__suffixcmp(const char *str, const char *suffix) +{ + return suffixcmp(str, suffix, false); +} + +int git__suffixcmp_icase(const char *str, const char *suffix) +{ + return suffixcmp(str, suffix, true); } char *git__strtok(char **end, const char *sep) diff --git a/src/util/util.h b/src/util/util.h index 7053a9d494a..d3cbbf4dda4 100644 --- a/src/util/util.h +++ b/src/util/util.h @@ -57,6 +57,7 @@ extern int git__prefixcmp_icase(const char *str, const char *prefix); extern int git__prefixncmp(const char *str, size_t str_n, const char *prefix); extern int git__prefixncmp_icase(const char *str, size_t str_n, const char *prefix); extern int git__suffixcmp(const char *str, const char *suffix); +extern int git__suffixcmp_icase(const char *str, const char *suffix); GIT_INLINE(int) git__signum(int val) { diff --git a/src/util/win32/path_w32.c b/src/util/win32/path_w32.c index 7a559e45c58..2e90d2e8e2f 100644 --- a/src/util/win32/path_w32.c +++ b/src/util/win32/path_w32.c @@ -253,26 +253,67 @@ static void win32_path_iter_dispose(struct win32_path_iter *iter) iter->current_dir = NULL; } +struct executable_suffix { + const wchar_t *suffix; + size_t len; +}; + int git_win32_path_find_executable(git_win32_path fullpath, wchar_t *exe) { struct win32_path_iter path_iter; const wchar_t *dir; size_t dir_len, exe_len = wcslen(exe); bool found = false; + static struct executable_suffix suffixes[] = { { NULL, 0 }, { L".exe", 4 }, { L".cmd", 4 } }; + size_t skip_bare = 1, i; if (win32_path_iter_init(&path_iter) < 0) return -1; - while (win32_path_iter_next(&dir, &dir_len, &path_iter) != GIT_ITEROVER) { - if (git_win32_path_join(fullpath, dir, dir_len, exe, exe_len) < 0) + /* see if the given executable has an executable suffix; if so we will + * look for the explicit name directly, as well as with added suffixes. + */ + for (i = 0; i < ARRAY_SIZE(suffixes); i++) { + struct executable_suffix *suffix = &suffixes[i]; + + if (!suffix->len) continue; - if (_waccess(fullpath, 0) == 0) { - found = true; + if (exe_len < suffix->len) + continue; + + if (memcmp(&exe[exe_len - suffix->len], suffix->suffix, suffix->len) == 0) { + skip_bare = 0; break; } } + while (win32_path_iter_next(&dir, &dir_len, &path_iter) != GIT_ITEROVER && !found) { + /* + * if the given name has an executable suffix, then try looking for it + * directly. in all cases, append executable extensions + * (".exe", ".cmd"...) + */ + for (i = skip_bare; i < ARRAY_SIZE(suffixes); i++) { + struct executable_suffix *suffix = &suffixes[i]; + + if (git_win32_path_join(fullpath, dir, dir_len, exe, exe_len) < 0) + continue; + + if (suffix->len) { + if (dir_len + exe_len + 1 + suffix->len > MAX_PATH) + continue; + + wcscat(fullpath, suffix->suffix); + } + + if (_waccess(fullpath, 0) == 0) { + found = true; + break; + } + } + } + win32_path_iter_dispose(&path_iter); if (found) diff --git a/tests/util/gitstr.c b/tests/util/gitstr.c index aea35565bed..37936b5a231 100644 --- a/tests/util/gitstr.c +++ b/tests/util/gitstr.c @@ -691,17 +691,33 @@ void test_gitstr__puts_escaped(void) git_str a = GIT_STR_INIT; git_str_clear(&a); - cl_git_pass(git_str_puts_escaped(&a, "this is a test", "", "")); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "", "", "")); cl_assert_equal_s("this is a test", a.ptr); git_str_clear(&a); - cl_git_pass(git_str_puts_escaped(&a, "this is a test", "t", "\\")); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "", NULL, NULL)); + cl_assert_equal_s("this is a test", a.ptr); + + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "t", "\\", "")); + cl_assert_equal_s("\\this is a \\tes\\t", a.ptr); + + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "t", "\\", NULL)); cl_assert_equal_s("\\this is a \\tes\\t", a.ptr); git_str_clear(&a); - cl_git_pass(git_str_puts_escaped(&a, "this is a test", "i ", "__")); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "i ", "__", NULL)); cl_assert_equal_s("th__is__ __is__ a__ test", a.ptr); + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this is a test", "i ", "__", "!!")); + cl_assert_equal_s("th__i!!s__ !!__i!!s__ !!a__ !!test", a.ptr); + + git_str_clear(&a); + cl_git_pass(git_str_puts_escaped(&a, "this' is' an' escape! ", "'!", "'\\", "'")); + cl_assert_equal_s("this'\\'' is'\\'' an'\\'' escape'\\!' ", a.ptr); + git_str_clear(&a); cl_git_pass(git_str_puts_escape_regex(&a, "^match\\s*[A-Z]+.*")); cl_assert_equal_s("\\^match\\\\s\\*\\[A-Z\\]\\+\\.\\*", a.ptr); diff --git a/tests/util/path/core.c b/tests/util/path/core.c index d1935a816a9..2de709af21a 100644 --- a/tests/util/path/core.c +++ b/tests/util/path/core.c @@ -698,12 +698,20 @@ static void fix_path(git_str *s) #endif } +#ifdef GIT_WIN32 +# define PATH_DELIMITER "\\" +#else +# define PATH_DELIMITER "/" +#endif + void test_path_core__find_exe_in_path(void) { char *orig_path; git_str sandbox_path = GIT_STR_INIT; git_str new_path = GIT_STR_INIT, full_path = GIT_STR_INIT, - dummy_path = GIT_STR_INIT; + dummy_path_one = GIT_STR_INIT, + dummy_path_two = GIT_STR_INIT, + dummy_path_three = GIT_STR_INIT; #ifdef GIT_WIN32 static const char *bogus_path_1 = "c:\\does\\not\\exist\\"; @@ -716,28 +724,84 @@ void test_path_core__find_exe_in_path(void) orig_path = cl_getenv("PATH"); git_str_puts(&sandbox_path, clar_sandbox_path()); - git_str_joinpath(&dummy_path, sandbox_path.ptr, "dummmmmmmy_libgit2_file"); - cl_git_rewritefile(dummy_path.ptr, "this is a dummy file"); + + git_str_joinpath(&dummy_path_one, sandbox_path.ptr, "dummmmmmmy_libgit2_file_one"); + cl_git_rewritefile(dummy_path_one.ptr, "this is a dummy file"); + + git_str_joinpath(&dummy_path_two, sandbox_path.ptr, "dummmmmmmy_libgit2_file_two.exe"); + cl_git_rewritefile(dummy_path_two.ptr, "this is a dummy file"); + cl_must_pass(p_chmod("dummmmmmmy_libgit2_file_two.exe", 0777)); + + cl_must_pass(p_mkdir("sub", 0777)); + cl_must_pass(p_mkdir("sub/dir", 0777)); + git_str_joinpath(&dummy_path_three, sandbox_path.ptr, "sub/dir/dummmmmmmy_libgit2_file_three.exe"); + cl_git_rewritefile(dummy_path_three.ptr, "this is a dummy file"); + cl_must_pass(p_chmod("sub/dir/dummmmmmmy_libgit2_file_three.exe", 0777)); fix_path(&sandbox_path); - fix_path(&dummy_path); + fix_path(&dummy_path_one); + fix_path(&dummy_path_two); + fix_path(&dummy_path_three); cl_git_pass(git_str_printf(&new_path, "%s%c%s%c%s%c%s", bogus_path_1, GIT_PATH_LIST_SEPARATOR, orig_path, GIT_PATH_LIST_SEPARATOR, sandbox_path.ptr, GIT_PATH_LIST_SEPARATOR, bogus_path_2)); - check_setenv("PATH", new_path.ptr); + /* ensure that we cannot find a nonexistent file */ cl_git_fail_with(GIT_ENOTFOUND, git_fs_path_find_executable(&full_path, "this_file_does_not_exist")); - cl_git_pass(git_fs_path_find_executable(&full_path, "dummmmmmmy_libgit2_file")); - cl_assert_equal_s(full_path.ptr, dummy_path.ptr); + /* ensure that non-executable files are ignored */ + cl_git_fail_with(GIT_ENOTFOUND, git_fs_path_find_executable(&full_path, "dummmmmmmy_libgit2_file_one")); + + /* find an executable in the path */ + git_str_clear(&full_path); + cl_git_pass(git_fs_path_find_executable(&full_path, "dummmmmmmy_libgit2_file_two.exe")); + cl_assert_equal_s(full_path.ptr, dummy_path_two.ptr); + + /* find an executable in the current directory */ + git_str_clear(&new_path); + cl_git_pass(git_str_printf(&new_path, "%s%c%s%c%s%c%s", + bogus_path_1, GIT_PATH_LIST_SEPARATOR, + orig_path, GIT_PATH_LIST_SEPARATOR, + ".", GIT_PATH_LIST_SEPARATOR, + bogus_path_2)); + check_setenv("PATH", new_path.ptr); + + git_str_clear(&full_path); + cl_git_pass(git_fs_path_find_executable(&full_path, "dummmmmmmy_libgit2_file_two.exe")); + cl_assert_equal_s(full_path.ptr, "." PATH_DELIMITER "dummmmmmmy_libgit2_file_two.exe"); + + /* empty path is the same as current directory on POSIX */ +#ifndef GIT_WIN32 + git_str_clear(&new_path); + cl_git_pass(git_str_printf(&new_path, "%s%c%s%c%s%c%s", + bogus_path_1, GIT_PATH_LIST_SEPARATOR, + orig_path, GIT_PATH_LIST_SEPARATOR, + "", GIT_PATH_LIST_SEPARATOR, + bogus_path_2)); + check_setenv("PATH", new_path.ptr); + + git_str_clear(&full_path); + cl_git_pass(git_fs_path_find_executable(&full_path, "dummmmmmmy_libgit2_file_two.exe")); + cl_assert_equal_s(full_path.ptr, "." PATH_DELIMITER "dummmmmmmy_libgit2_file_two.exe"); +#endif + + /* don't look for qualified paths in PATH */ + cl_git_fail_with(GIT_ENOTFOUND, git_fs_path_find_executable(&full_path, "dir/dummmmmmmy_libgit2_file_three.exe")); + + /* but do allow users to use paths relative to current dir */ + git_str_clear(&full_path); + cl_git_pass(git_fs_path_find_executable(&full_path, "sub/dir/dummmmmmmy_libgit2_file_three.exe")); + cl_assert_equal_s(full_path.ptr, "sub/dir/dummmmmmmy_libgit2_file_three.exe"); git_str_dispose(&full_path); git_str_dispose(&new_path); - git_str_dispose(&dummy_path); + git_str_dispose(&dummy_path_one); + git_str_dispose(&dummy_path_two); + git_str_dispose(&dummy_path_three); git_str_dispose(&sandbox_path); git__free(orig_path); }