diff --git a/cmdline/main.c b/cmdline/main.c index a9ab40e1..251fd1b9 100644 --- a/cmdline/main.c +++ b/cmdline/main.c @@ -45,46 +45,53 @@ static int needs_quote(const WCHAR* arg) static int argcat(WCHAR* cmd, int size, int pos, const WCHAR* arg) { + int has_quote; + /* space separator */ if (pos != 0) charcat(L' '); - if (!needs_quote(arg)) { + has_quote = needs_quote(arg); + + if (!has_quote) { while (*arg) charcat(*arg++); - return pos; - } + } else { + /* starting quote */ + charcat(L'"'); - charcat(L'"'); + while (*arg) { + int bl = 0; + while (*arg == L'\\') { + ++arg; + ++bl; + } - while (*arg) { - int bl = 0; - while (*arg == L'\\') { - ++arg; - ++bl; + if (*arg == 0) { + /* double backslashes before closing quote */ + bl = bl * 2; + while (bl--) + charcat(L'\\'); + } else if (*arg == '"') { + /* double backslashes + escape the quote */ + bl = bl * 2 + 1; + while (bl--) + charcat(L'\\'); + charcat(L'"'); + ++arg; + } else { + /* normal backslashes */ + while (bl--) + charcat(L'\\'); + charcat(*arg); + ++arg; + } } - if (*arg == 0) { - bl = bl * 2; - while (bl--) - charcat(L'\\'); - } else if (*arg == '"') { - bl = bl * 2 + 1; - while (bl--) - charcat(L'\\'); - charcat(L'"'); - ++arg; - } else { - while (bl--) - charcat(L'\\'); - charcat(*arg); - ++arg; - } + /* ending quote */ + charcat(L'"'); } - /* ending quote */ - charcat(L'"'); - return pos; } diff --git a/cmdline/snapraid.c b/cmdline/snapraid.c index 3747cdb0..087e2bbe 100644 --- a/cmdline/snapraid.c +++ b/cmdline/snapraid.c @@ -108,13 +108,355 @@ void test(int argc, char* argv[]) } #ifdef _WIN32 + /* basic cases - no special characters, no quotes needed */ + assert(strcmp(esc_shell("simple", buffer), "simple") == 0); + assert(strcmp(esc_shell("file.txt", buffer), "file.txt") == 0); + assert(strcmp(esc_shell("file123", buffer), "file123") == 0); + assert(strcmp(esc_shell("file_name-test.doc", buffer), "file_name-test.doc") == 0); + assert(strcmp(esc_shell(",._+:@/-", buffer), ",._+:@/-") == 0); + assert(strcmp(esc_shell("C:\\Users\\test", buffer), "C:\\Users\\test") == 0); + + /* space - requires quoting */ assert(strcmp(esc_shell(" ", buffer), "\" \"") == 0); - assert(strcmp(esc_shell(" \" ", buffer), "\" \"\\\"\" \"") == 0); - assert(strcmp(esc_shell("&|()<>^", buffer), "^&^|^(^)^<^>^^") == 0); - assert(strcmp(esc_shell("&|()<>^ ", buffer), "\"&|()<>^ \"") == 0); + assert(strcmp(esc_shell("file name.txt", buffer), "\"file name.txt\"") == 0); + assert(strcmp(esc_shell("my document.doc", buffer), "\"my document.doc\"") == 0); + assert(strcmp(esc_shell(" multiple spaces ", buffer), "\" multiple spaces \"") == 0); + + /* tab - requires quoting */ + assert(strcmp(esc_shell("\t", buffer), "\"\t\"") == 0); + assert(strcmp(esc_shell("file\tname", buffer), "\"file\tname\"") == 0); + + /* newline - requires quoting */ + assert(strcmp(esc_shell("\n", buffer), "\"\n\"") == 0); + assert(strcmp(esc_shell("line1\nline2", buffer), "\"line1\nline2\"") == 0); + + /* carriage return - requires quoting */ + assert(strcmp(esc_shell("\r", buffer), "\"\r\"") == 0); + assert(strcmp(esc_shell("text\r\n", buffer), "\"text\r\n\"") == 0); + + /* double quote - requires quoting and escaping with backslash */ + assert(strcmp(esc_shell("\"", buffer), "\"\\\"\"") == 0); + assert(strcmp(esc_shell("file\"name", buffer), "\"file\\\"name\"") == 0); + assert(strcmp(esc_shell("\"quoted\"", buffer), "\"\\\"quoted\\\"\"") == 0); + assert(strcmp(esc_shell("say \"hello\"", buffer), "\"say \\\"hello\\\"\"") == 0); + + /* ampersand - requires quoting */ + assert(strcmp(esc_shell("&", buffer), "\"&\"") == 0); + assert(strcmp(esc_shell("file&name", buffer), "\"file&name\"") == 0); + assert(strcmp(esc_shell("a&b&c", buffer), "\"a&b&c\"") == 0); + assert(strcmp(esc_shell("file & name", buffer), "\"file & name\"") == 0); + + /* pipe - requires quoting */ + assert(strcmp(esc_shell("|", buffer), "\"|\"") == 0); + assert(strcmp(esc_shell("file|name", buffer), "\"file|name\"") == 0); + assert(strcmp(esc_shell("a | b", buffer), "\"a | b\"") == 0); + + /* parentheses - requires quoting */ + assert(strcmp(esc_shell("(", buffer), "\"(\"") == 0); + assert(strcmp(esc_shell(")", buffer), "\")\"") == 0); + assert(strcmp(esc_shell("(test)", buffer), "\"(test)\"") == 0); + assert(strcmp(esc_shell("file (1)", buffer), "\"file (1)\"") == 0); + assert(strcmp(esc_shell("file(copy)", buffer), "\"file(copy)\"") == 0); + + /* angle brackets - requires quoting */ + assert(strcmp(esc_shell("<", buffer), "\"<\"") == 0); + assert(strcmp(esc_shell(">", buffer), "\">\"") == 0); + assert(strcmp(esc_shell("ac", buffer), "\"ac\"") == 0); + assert(strcmp(esc_shell("file > output", buffer), "\"file > output\"") == 0); + + /* caret - requires quoting */ + assert(strcmp(esc_shell("^", buffer), "\"^\"") == 0); + assert(strcmp(esc_shell("test^test", buffer), "\"test^test\"") == 0); + assert(strcmp(esc_shell("a ^ b", buffer), "\"a ^ b\"") == 0); + + /* multiple special chars - requires quoting */ + assert(strcmp(esc_shell("&|()<>^", buffer), "\"&|()<>^\"") == 0); + assert(strcmp(esc_shell("test&|test", buffer), "\"test&|test\"") == 0); + assert(strcmp(esc_shell("a & b | c", buffer), "\"a & b | c\"") == 0); + + /* percent sign - requires quoting */ + assert(strcmp(esc_shell("%", buffer), "\"%\"") == 0); + assert(strcmp(esc_shell("%%", buffer), "\"%%\"") == 0); + assert(strcmp(esc_shell("%PATH%", buffer), "\"%PATH%\"") == 0); + assert(strcmp(esc_shell("test%var%test", buffer), "\"test%var%test\"") == 0); + assert(strcmp(esc_shell("%PATH% file", buffer), "\"%PATH% file\"") == 0); + + /* exclamation mark - requires quoting */ + assert(strcmp(esc_shell("!", buffer), "\"!\"") == 0); + assert(strcmp(esc_shell("!VAR!", buffer), "\"!VAR!\"") == 0); + assert(strcmp(esc_shell("test!test", buffer), "\"test!test\"") == 0); + assert(strcmp(esc_shell("hello !world!", buffer), "\"hello !world!\"") == 0); + + /* equals sign - requires quoting */ + assert(strcmp(esc_shell("=", buffer), "\"=\"") == 0); + assert(strcmp(esc_shell("VAR=value", buffer), "\"VAR=value\"") == 0); + assert(strcmp(esc_shell("a=b", buffer), "\"a=b\"") == 0); + + /* semicolon - requires quoting */ + assert(strcmp(esc_shell(";", buffer), "\";\"") == 0); + assert(strcmp(esc_shell("cmd1;cmd2", buffer), "\"cmd1;cmd2\"") == 0); + + /* backslash - no quotes needed when alone or in path */ + assert(strcmp(esc_shell("\\", buffer), "\\") == 0); + assert(strcmp(esc_shell("C:\\", buffer), "C:\\") == 0); + assert(strcmp(esc_shell("C:\\Users", buffer), "C:\\Users") == 0); + assert(strcmp(esc_shell("path\\to\\file", buffer), "path\\to\\file") == 0); + assert(strcmp(esc_shell("C:\\folder\\", buffer), "C:\\folder\\") == 0); + + /* backslash with space - requires quoting, normal backslash inside */ + assert(strcmp(esc_shell("\\ ", buffer), "\"\\ \"") == 0); + assert(strcmp(esc_shell("C:\\ ", buffer), "\"C:\\ \"") == 0); + assert(strcmp(esc_shell("C:\\Program Files", buffer), "\"C:\\Program Files\"") == 0); + + /* trailing backslash with quotes - backslashes before closing quote must be doubled */ + assert(strcmp(esc_shell("C:\\folder\\ ", buffer), "\"C:\\folder\\ \"") == 0); + assert(strcmp(esc_shell("path\\ ", buffer), "\"path\\ \"") == 0); + assert(strcmp(esc_shell("C:\\My Documents\\", buffer), "\"C:\\My Documents\\\\\"") == 0); + + /* backslash before embedded quote - backslash before quote must be doubled */ + assert(strcmp(esc_shell("C:\\\"test\"", buffer), "\"C:\\\\\\\"test\\\"\"") == 0); + assert(strcmp(esc_shell("path\\\"file\"", buffer), "\"path\\\\\\\"file\\\"\"") == 0); + + /* multiple trailing backslashes before end with quotes */ + assert(strcmp(esc_shell("test\\\\ ", buffer), "\"test\\\\ \"") == 0); + assert(strcmp(esc_shell("path\\\\\\\\ ", buffer), "\"path\\\\\\\\ \"") == 0); + + /* backslash NOT before quote - normal backslash */ + assert(strcmp(esc_shell("test\\file ", buffer), "\"test\\file \"") == 0); + assert(strcmp(esc_shell("a\\b c", buffer), "\"a\\b c\"") == 0); + + /* control characters - require quoting */ + assert(strcmp(esc_shell("\x01", buffer), "\"\x01\"") == 0); + assert(strcmp(esc_shell("\x1F", buffer), "\"\x1F\"") == 0); + assert(strcmp(esc_shell("\x7F", buffer), "\"\x7F\"") == 0); /* DEL character */ + assert(strcmp(esc_shell("test\x01test", buffer), "\"test\x01test\"") == 0); + + /* complex real-world examples */ + assert(strcmp(esc_shell("C:\\Program Files\\App", buffer), "\"C:\\Program Files\\App\"") == 0); + assert(strcmp(esc_shell("C:\\Program Files (x86)\\", buffer), "\"C:\\Program Files (x86)\\\\\"") == 0); + assert(strcmp(esc_shell("file (copy).txt", buffer), "\"file (copy).txt\"") == 0); + assert(strcmp(esc_shell("setup-v1.0.exe", buffer), "setup-v1.0.exe") == 0); + assert(strcmp(esc_shell("setup v1.0.exe", buffer), "\"setup v1.0.exe\"") == 0); + + /* mixed quotes and special chars */ + assert(strcmp(esc_shell("say \"hi\" & exit", buffer), "\"say \\\"hi\\\" & exit\"") == 0); + assert(strcmp(esc_shell("test \"a|b\"", buffer), "\"test \\\"a|b\\\"\"") == 0); + + /* empty string */ + assert(strcmp(esc_shell("", buffer), "") == 0); + + /* all safe characters that don't need escaping */ + assert(strcmp(esc_shell("abcdefghijklmnopqrstuvwxyz", buffer), "abcdefghijklmnopqrstuvwxyz") == 0); + assert(strcmp(esc_shell("ABCDEFGHIJKLMNOPQRSTUVWXYZ", buffer), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0); + assert(strcmp(esc_shell("0123456789", buffer), "0123456789") == 0); + assert(strcmp(esc_shell("._-+,@:", buffer), "._-+,@:") == 0); #else - assert(strcmp(esc_shell(",._+:@%%/-", buffer), ",._+:@%%/-") == 0); + /* basic cases - no special characters */ + assert(strcmp(esc_shell("simple", buffer), "simple") == 0); + assert(strcmp(esc_shell("file.txt", buffer), "file.txt") == 0); + assert(strcmp(esc_shell("file123", buffer), "file123") == 0); + assert(strcmp(esc_shell("file_name-test.doc", buffer), "file_name-test.doc") == 0); + assert(strcmp(esc_shell(",._+:@/-", buffer), ",._+:@/-") == 0); + assert(strcmp(esc_shell("/usr/local/bin", buffer), "/usr/local/bin") == 0); + + /* empty string */ + assert(strcmp(esc_shell("", buffer), "") == 0); + + /* space - escape with backslash */ assert(strcmp(esc_shell(" ", buffer), "\\ ") == 0); + assert(strcmp(esc_shell("file name.txt", buffer), "file\\ name.txt") == 0); + assert(strcmp(esc_shell("my document.doc", buffer), "my\\ document.doc") == 0); + assert(strcmp(esc_shell(" spaces ", buffer), "\\ \\ spaces\\ \\ ") == 0); + assert(strcmp(esc_shell("a b c", buffer), "a\\ b\\ c") == 0); + + /* tab - escape with backslash */ + assert(strcmp(esc_shell("\t", buffer), "\\\t") == 0); + assert(strcmp(esc_shell("file\tname", buffer), "file\\\tname") == 0); + assert(strcmp(esc_shell("\t\t", buffer), "\\\t\\\t") == 0); + + /* newline - escape with backslash */ + assert(strcmp(esc_shell("\n", buffer), "\\\n") == 0); + assert(strcmp(esc_shell("line1\nline2", buffer), "line1\\\nline2") == 0); + assert(strcmp(esc_shell("\n\n", buffer), "\\\n\\\n") == 0); + + /* carriage return - escape with backslash */ + assert(strcmp(esc_shell("\r", buffer), "\\\r") == 0); + assert(strcmp(esc_shell("text\r\n", buffer), "text\\\r\\\n") == 0); + + /* tilde (home directory expansion) */ + assert(strcmp(esc_shell("~", buffer), "\\~") == 0); + assert(strcmp(esc_shell("~/file", buffer), "\\~/file") == 0); + assert(strcmp(esc_shell("file~name", buffer), "file\\~name") == 0); + assert(strcmp(esc_shell("~user", buffer), "\\~user") == 0); + + /* backtick (command substitution) */ + assert(strcmp(esc_shell("`", buffer), "\\`") == 0); + assert(strcmp(esc_shell("`command`", buffer), "\\`command\\`") == 0); + assert(strcmp(esc_shell("test`test", buffer), "test\\`test") == 0); + assert(strcmp(esc_shell("``", buffer), "\\`\\`") == 0); + + /* hash (comment) */ + assert(strcmp(esc_shell("#", buffer), "\\#") == 0); + assert(strcmp(esc_shell("#comment", buffer), "\\#comment") == 0); + assert(strcmp(esc_shell("file#name", buffer), "file\\#name") == 0); + assert(strcmp(esc_shell("test#123", buffer), "test\\#123") == 0); + + /* dollar sign (variable expansion) */ + assert(strcmp(esc_shell("$", buffer), "\\$") == 0); + assert(strcmp(esc_shell("$$", buffer), "\\$\\$") == 0); + assert(strcmp(esc_shell("$VAR", buffer), "\\$VAR") == 0); + assert(strcmp(esc_shell("${VAR}", buffer), "\\$\\{VAR\\}") == 0); + assert(strcmp(esc_shell("test$test", buffer), "test\\$test") == 0); + assert(strcmp(esc_shell("$1", buffer), "\\$1") == 0); + assert(strcmp(esc_shell("$PATH", buffer), "\\$PATH") == 0); + + /* ampersand (background job) */ + assert(strcmp(esc_shell("&", buffer), "\\&") == 0); + assert(strcmp(esc_shell("&&", buffer), "\\&\\&") == 0); + assert(strcmp(esc_shell("file&name", buffer), "file\\&name") == 0); + assert(strcmp(esc_shell("a&b&c", buffer), "a\\&b\\&c") == 0); + assert(strcmp(esc_shell("cmd1 & cmd2", buffer), "cmd1\\ \\&\\ cmd2") == 0); + + /* asterisk (wildcard) */ + assert(strcmp(esc_shell("*", buffer), "\\*") == 0); + assert(strcmp(esc_shell("**", buffer), "\\*\\*") == 0); + assert(strcmp(esc_shell("*.txt", buffer), "\\*.txt") == 0); + assert(strcmp(esc_shell("file*name", buffer), "file\\*name") == 0); + assert(strcmp(esc_shell("test*", buffer), "test\\*") == 0); + + /* parentheses (subshell) */ + assert(strcmp(esc_shell("(", buffer), "\\(") == 0); + assert(strcmp(esc_shell(")", buffer), "\\)") == 0); + assert(strcmp(esc_shell("()", buffer), "\\(\\)") == 0); + assert(strcmp(esc_shell("(test)", buffer), "\\(test\\)") == 0); + assert(strcmp(esc_shell("file(1)", buffer), "file\\(1\\)") == 0); + assert(strcmp(esc_shell("(a)(b)", buffer), "\\(a\\)\\(b\\)") == 0); + + /* backslash (escape character) */ + assert(strcmp(esc_shell("\\", buffer), "\\\\") == 0); + assert(strcmp(esc_shell("\\\\", buffer), "\\\\\\\\") == 0); + assert(strcmp(esc_shell("path\\to\\file", buffer), "path\\\\to\\\\file") == 0); + assert(strcmp(esc_shell("test\\test", buffer), "test\\\\test") == 0); + assert(strcmp(esc_shell("a\\b\\c", buffer), "a\\\\b\\\\c") == 0); + + /* pipe (pipeline) */ + assert(strcmp(esc_shell("|", buffer), "\\|") == 0); + assert(strcmp(esc_shell("||", buffer), "\\|\\|") == 0); + assert(strcmp(esc_shell("file|name", buffer), "file\\|name") == 0); + assert(strcmp(esc_shell("a|b|c", buffer), "a\\|b\\|c") == 0); + assert(strcmp(esc_shell("cmd1 | cmd2", buffer), "cmd1\\ \\|\\ cmd2") == 0); + + /* square brackets (wildcard) */ + assert(strcmp(esc_shell("[", buffer), "\\[") == 0); + assert(strcmp(esc_shell("]", buffer), "\\]") == 0); + assert(strcmp(esc_shell("[]", buffer), "\\[\\]") == 0); + assert(strcmp(esc_shell("[abc]", buffer), "\\[abc\\]") == 0); + assert(strcmp(esc_shell("file[1]", buffer), "file\\[1\\]") == 0); + assert(strcmp(esc_shell("[0-9]", buffer), "\\[0-9\\]") == 0); + + /* curly braces (brace expansion) */ + assert(strcmp(esc_shell("{", buffer), "\\{") == 0); + assert(strcmp(esc_shell("}", buffer), "\\}") == 0); + assert(strcmp(esc_shell("{}", buffer), "\\{\\}") == 0); + assert(strcmp(esc_shell("{a,b,c}", buffer), "\\{a,b,c\\}") == 0); + assert(strcmp(esc_shell("file{1,2}", buffer), "file\\{1,2\\}") == 0); + assert(strcmp(esc_shell("{1..10}", buffer), "\\{1..10\\}") == 0); + + /* semicolon (command separator) */ + assert(strcmp(esc_shell(";", buffer), "\\;") == 0); + assert(strcmp(esc_shell(";;", buffer), "\\;\\;") == 0); + assert(strcmp(esc_shell("cmd1;cmd2", buffer), "cmd1\\;cmd2") == 0); + assert(strcmp(esc_shell("test;test", buffer), "test\\;test") == 0); + assert(strcmp(esc_shell("a; b", buffer), "a\\;\\ b") == 0); + + /* single quote */ + assert(strcmp(esc_shell("'", buffer), "\\'") == 0); + assert(strcmp(esc_shell("''", buffer), "\\'\\'") == 0); + assert(strcmp(esc_shell("'test'", buffer), "\\'test\\'") == 0); + assert(strcmp(esc_shell("file'name", buffer), "file\\'name") == 0); + assert(strcmp(esc_shell("it's", buffer), "it\\'s") == 0); + + /* double quote */ + assert(strcmp(esc_shell("\"", buffer), "\\\"") == 0); + assert(strcmp(esc_shell("\"\"", buffer), "\\\"\\\"") == 0); + assert(strcmp(esc_shell("\"test\"", buffer), "\\\"test\\\"") == 0); + assert(strcmp(esc_shell("file\"name", buffer), "file\\\"name") == 0); + assert(strcmp(esc_shell("say \"hi\"", buffer), "say\\ \\\"hi\\\"") == 0); + + /* angle brackets (redirection) */ + assert(strcmp(esc_shell("<", buffer), "\\<") == 0); + assert(strcmp(esc_shell(">", buffer), "\\>") == 0); + assert(strcmp(esc_shell("<<", buffer), "\\<\\<") == 0); + assert(strcmp(esc_shell(">>", buffer), "\\>\\>") == 0); + assert(strcmp(esc_shell("ac", buffer), "a\\c") == 0); + assert(strcmp(esc_shell("file>output", buffer), "file\\>output") == 0); + assert(strcmp(esc_shell("cmd < in > out", buffer), "cmd\\ \\<\\ in\\ \\>\\ out") == 0); + + /* question mark (wildcard) */ + assert(strcmp(esc_shell("?", buffer), "\\?") == 0); + assert(strcmp(esc_shell("??", buffer), "\\?\\?") == 0); + assert(strcmp(esc_shell("file?.txt", buffer), "file\\?.txt") == 0); + assert(strcmp(esc_shell("test?test", buffer), "test\\?test") == 0); + assert(strcmp(esc_shell("file??", buffer), "file\\?\\?") == 0); + + /* equals sign (assignment in some contexts) */ + assert(strcmp(esc_shell("=", buffer), "\\=") == 0); + assert(strcmp(esc_shell("==", buffer), "\\=\\=") == 0); + assert(strcmp(esc_shell("VAR=value", buffer), "VAR\\=value") == 0); + assert(strcmp(esc_shell("a=b", buffer), "a\\=b") == 0); + assert(strcmp(esc_shell("PATH=/usr/bin", buffer), "PATH\\=/usr/bin") == 0); + + /* exclamation mark (history expansion) */ + assert(strcmp(esc_shell("!", buffer), "\\!") == 0); + assert(strcmp(esc_shell("!!", buffer), "\\!\\!") == 0); + assert(strcmp(esc_shell("test!test", buffer), "test\\!test") == 0); + assert(strcmp(esc_shell("!$", buffer), "\\!\\$") == 0); + assert(strcmp(esc_shell("!123", buffer), "\\!123") == 0); + + /* control characters (0x01-0x1F) - escape with backslash */ + assert(strcmp(esc_shell("\x01", buffer), "\\\x01") == 0); + assert(strcmp(esc_shell("\x02", buffer), "\\\x02") == 0); + assert(strcmp(esc_shell("\x1F", buffer), "\\\x1F") == 0); + assert(strcmp(esc_shell("test\x01test", buffer), "test\\\x01test") == 0); + + /* DEL character (0x7F) */ + assert(strcmp(esc_shell("\x7F", buffer), "\\\x7F") == 0); + assert(strcmp(esc_shell("test\x7Ftest", buffer), "test\\\x7Ftest") == 0); + + /* multiple special characters combined */ + assert(strcmp(esc_shell("$VAR & $OTHER", buffer), "\\$VAR\\ \\&\\ \\$OTHER") == 0); + assert(strcmp(esc_shell("*.txt | grep test", buffer), "\\*.txt\\ \\|\\ grep\\ test") == 0); + assert(strcmp(esc_shell("file (1) [copy].txt", buffer), "file\\ \\(1\\)\\ \\[copy\\].txt") == 0); + assert(strcmp(esc_shell("a & b | c", buffer), "a\\ \\&\\ b\\ \\|\\ c") == 0); + assert(strcmp(esc_shell("cmd1; cmd2 && cmd3", buffer), "cmd1\\;\\ cmd2\\ \\&\\&\\ cmd3") == 0); + + /* complex real-world examples */ + assert(strcmp(esc_shell("/home/user/My Documents", buffer), "/home/user/My\\ Documents") == 0); + assert(strcmp(esc_shell("/path/to/file (copy).txt", buffer), "/path/to/file\\ \\(copy\\).txt") == 0); + assert(strcmp(esc_shell("~/project/file-v1.0.tar.gz", buffer), "\\~/project/file-v1.0.tar.gz") == 0); + assert(strcmp(esc_shell("$(whoami)@$(hostname)", buffer), "\\$\\(whoami\\)@\\$\\(hostname\\)") == 0); + assert(strcmp(esc_shell("test && echo 'done'", buffer), "test\\ \\&\\&\\ echo\\ \\'done\\'") == 0); + assert(strcmp(esc_shell("file #1 [important].txt", buffer), "file\\ \\#1\\ \\[important\\].txt") == 0); + assert(strcmp(esc_shell("/tmp/test (1).txt", buffer), "/tmp/test\\ \\(1\\).txt") == 0); + assert(strcmp(esc_shell("var=$HOME/bin:$PATH", buffer), "var\\=\\$HOME/bin:\\$PATH") == 0); + + /* edge cases with multiple escapes */ + assert(strcmp(esc_shell("a\\ b", buffer), "a\\\\\\ b") == 0); + assert(strcmp(esc_shell("'\"test\"'", buffer), "\\'\\\"test\\\"\\'") == 0); + assert(strcmp(esc_shell("$(echo \"test\")", buffer), "\\$\\(echo\\ \\\"test\\\"\\)") == 0); + + /* all safe characters that don't need escaping */ + assert(strcmp(esc_shell("abcdefghijklmnopqrstuvwxyz", buffer), "abcdefghijklmnopqrstuvwxyz") == 0); + assert(strcmp(esc_shell("ABCDEFGHIJKLMNOPQRSTUVWXYZ", buffer), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0); + assert(strcmp(esc_shell("0123456789", buffer), "0123456789") == 0); + assert(strcmp(esc_shell("._-+,@:", buffer), "._-+,@:") == 0); + assert(strcmp(esc_shell("/path/to/file", buffer), "/path/to/file") == 0); + assert(strcmp(esc_shell("simple_file-name.txt", buffer), "simple_file-name.txt") == 0); + + /* file extensions and versions */ + assert(strcmp(esc_shell("file.tar.gz", buffer), "file.tar.gz") == 0); + assert(strcmp(esc_shell("app-v1.2.3.deb", buffer), "app-v1.2.3.deb") == 0); + assert(strcmp(esc_shell("test_2024-01-01.log", buffer), "test_2024-01-01.log") == 0); #endif printf("Everything OK\n"); diff --git a/cmdline/support.c b/cmdline/support.c index 650c2cd7..33db965c 100644 --- a/cmdline/support.c +++ b/cmdline/support.c @@ -449,15 +449,12 @@ void printp(double v, size_t pad) printl(buf, pad); } -#define ESCAPE(from,escape,to) \ - case from : \ +#define charcat(c) \ + do { \ if (p == end) \ goto bail; \ - *p++ = escape; \ - if (p == end) \ - goto bail; \ - *p++ = to; \ - break + *p++ = (c); \ + } while (0) const char* esc_tag(const char* str, char* buffer) { @@ -467,23 +464,29 @@ const char* esc_tag(const char* str, char* buffer) /* copy string with escaping */ while (*str) { - char c = *str; + char c = *str++; switch (c) { - - ESCAPE('\n', '\\', 'n'); - ESCAPE('\r', '\\', 'r'); - ESCAPE(':', '\\', 'd'); - ESCAPE('\\', '\\', '\\'); - + case '\n' : + charcat('\\'); + charcat('n'); + break; + case '\r' : + charcat('\\'); + charcat('r'); + break; + case ':' : + charcat('\\'); + charcat('d'); + break; + case '\\' : + charcat('\\'); + charcat('\\'); + break; default : - if (p == end) - goto bail; - *p++ = c; + charcat(c); break; } - - ++str; } /* put final 0 */ @@ -500,106 +503,144 @@ bail: /* LCOV_EXCL_STOP */ } +#ifdef _WIN32 +static int needs_quote(const char* arg) +{ + while (*arg) { + char c = *arg; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || + c == '&' || c == '|' || c == '(' || c == ')' || + c == '<' || c == '>' || c == '^' || c == '"' || + c == '%' || c == '!' || c == '=' || c == ';' || + (unsigned char)c < 32 || c == 127) + return 1; + ++arg; + } + + return 0; +} +#endif + +struct stream { + const char* str; + unsigned idx; + const char** map; + unsigned max; +}; + +static inline char ssget(struct stream* ss) +{ + /* get the next char */ + char c = *ss->str++; + + /* if one string is finished, go to the next */ + while (c == 0 && ss->idx + 1 < ss->max) { + ss->str = ss->map[++ss->idx]; + c = *ss->str++; + } + + return c; +} + +#ifdef _WIN32 const char* esc_shell_multi(const char** str_map, unsigned str_max, char* buffer) { char* begin = buffer; char* end = begin + ESC_MAX; char* p = begin; - unsigned str_mac; - const char* str; + struct stream ss; + char c; + unsigned i; + int has_quote; -#ifdef _WIN32 - int has_quote = 0; - - for (str_mac = 0; str_mac < str_max; ++str_mac) { - str = str_map[str_mac]; - if (strchr(str, ' ') != 0) + has_quote = 0; + for (i = 0; i < str_max; ++i) { + if (needs_quote(str_map[i])) has_quote = 1; } - if (has_quote) { - if (p == end) - goto bail; - *p++ = '"'; - } -#endif + ss.idx = 0; + ss.str = str_map[0]; + ss.map = str_map; + ss.max = str_max; - /* copy string with escaping */ - str_mac = 0; - str = str_map[str_mac]; - while (1) { - /* get the next char */ - char c = *str; + if (!has_quote) { + c = ssget(&ss); + while (c) { + charcat(c); + c = ssget(&ss); + } + } else { + /* starting quote */ + charcat('"'); - /* if one string is finished, go to the next */ - while (c == 0 && str_mac + 1 < str_max) { - ++str_mac; - str = str_map[str_mac]; - c = *str; + c = ssget(&ss); + while (c) { + int bl = 0; + while (c == '\\') { + ++bl; + c = ssget(&ss); + } + + if (c == 0) { + /* double backslashes before closing quote */ + bl = bl * 2; + while (bl--) + charcat('\\'); + break; + } else if (c == '"') { + /* double backslashes + escape the quote */ + bl = bl * 2 + 1; + while (bl--) + charcat('\\'); + charcat('"'); + } else { + /* normal backslashes */ + while (bl--) + charcat('\\'); + charcat(c); + } + + c = ssget(&ss); } - /* if we read all the strings, stop */ - if (!c) - break; + /* ending quote */ + charcat('"'); + } - switch (c) { -#ifdef _WIN32 - /* - * Windows shell escape - * - * The Windows NT Command Shell - * https://technet.microsoft.com/en-us/library/cc723564.aspx - */ - case '"' : - /* double quote, it needs to be quoted with \ */ - if (has_quote) { - /* " -> "\"" -> (close quote)(quoted with \ ")(reopen quote) */ - if (p == end) - goto bail; - *p++ = '"'; - if (p == end) - goto bail; - *p++ = '\\'; - if (p == end) - goto bail; - *p++ = '"'; - if (p == end) - goto bail; - *p++ = '"'; - } else { - /* " -> \" */ - if (p == end) - goto bail; - *p++ = '\\'; - if (p == end) - goto bail; - *p++ = '"'; - } - break; - case '&' : - case '|' : - case '(' : - case ')' : - case '<' : - case '>' : - case '^' : - /* reserved chars, they need to be quoted with ^ */ - if (has_quote) { - if (p == end) - goto bail; - *p++ = c; - } else { - if (p == end) - goto bail; - *p++ = '^'; - if (p == end) - goto bail; - *p++ = c; - } - break; + /* put final 0 */ + charcat(0); + + return begin; + +bail: + /* LCOV_EXCL_START */ + log_fatal("Escape for shell is too long\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ +} #else +const char* esc_shell_multi(const char** str_map, unsigned str_max, char* buffer) +{ + char* begin = buffer; + char* end = begin + ESC_MAX; + char* p = begin; + struct stream ss; + char c; + + ss.idx = 0; + ss.str = str_map[0]; + ss.map = str_map; + ss.max = str_max; + + c = ssget(&ss); + while (c) { + switch (c) { /* special chars that need to be quoted */ case ' ' : /* space */ + case '\t' : /* tab */ + case '\n' : /* newline */ + case '\r' : /* carriage return */ case '~' : /* home */ case '`' : /* command */ case '#' : /* comment */ @@ -620,37 +661,27 @@ const char* esc_shell_multi(const char** str_map, unsigned str_max, char* buffer case '<' : /* redirect */ case '>' : /* redirect */ case '?' : /* wildcard */ - if (p == end) - goto bail; - *p++ = '\\'; - if (p == end) - goto bail; - *p++ = c; + case '=' : /* assignment */ + case '!' : /* history expansion */ + charcat('\\'); + charcat(c); break; -#endif default : - /* unquoted */ - if (p == end) - goto bail; - *p++ = c; + /* check for control characters */ + if ((unsigned char)c < 32 || c == 127) { + charcat('\\'); + charcat(c); + } else { + charcat(c); + } break; } - ++str; + c = ssget(&ss); } -#ifdef _WIN32 - if (has_quote) { - if (p == end) - goto bail; - *p++ = '"'; - } -#endif - /* put final 0 */ - if (p == end) - goto bail; - *p = 0; + charcat(0); return begin; @@ -660,6 +691,7 @@ bail: exit(EXIT_FAILURE); /* LCOV_EXCL_STOP */ } +#endif char* strpolish(char* s) {