Fix quoting for printed file paths
Some checks are pending
Build / build-mac (push) Waiting to run
Build / build-linux (push) Waiting to run
Build / build-win (push) Waiting to run
CodeQL / Analyze (push) Waiting to run

This commit is contained in:
Andrea Mazzoleni 2025-10-27 00:50:21 +01:00
parent 21acc31bf1
commit 8ae0599c64
3 changed files with 538 additions and 157 deletions

View file

@ -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;
}

View file

@ -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("a<b>c", buffer), "\"a<b>c\"") == 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("a<b>c", buffer), "a\\<b\\>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");

View file

@ -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)
{