From fb0bf9d1b9dbcac3d58bef9c86b6353eaf3d0c1e Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 21 Aug 2018 18:52:21 +0000 Subject: fixes escape for special cases (+ more test-cases): - `%` char to be escaped (quoted) in any case (regardless pairing flag), otherwise `%username%` will be interpolated as username. - escape of multiple backslashes before quote is different (as without following quote) in unpaired quote syntax (upaired flag set) --- tests/winPipe.test | 30 ++++++++++- win/tclWinPipe.c | 142 +++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 139 insertions(+), 33 deletions(-) diff --git a/tests/winPipe.test b/tests/winPipe.test index e817e85..c375d8f 100644 --- a/tests/winPipe.test +++ b/tests/winPipe.test @@ -332,7 +332,9 @@ proc _testExecArgs {single args} { set broken {} foreach args $args { if {$single & 1} { - set args [list $args] + # enclose single test-arg between 1st/3rd to be sure nothing is truncated + # (e. g. to cover unexpected trim by nts-zero case, and args don't recombined): + set args [list "1st" $args "3rd"] } foreach cmd $cmds { set e [list [file tail $path(echoArgs.tcl)] {*}$args] @@ -414,8 +416,15 @@ set injectList { "test\"whoami\\" "test\"\"whoami\\" "test\"\"\"whoami\\" "test\"\"\"\"whoami\\" + {test\\&\\test} {test"\\&\\test} + {"test\\&\\test} {"test"\\&\\"test"} + {test\\"&"\\test} {test"\\"&"\\test} + {"test\\"&"\\test} {"test"\\"&"\\"test"} + {test\"&whoami} {test"\"&whoami} {test""\"&whoami} {test"""\"&whoami} + {test\"\&whoami} {test"\"\&whoami} + {test""\"\&whoami} {test"""\"\&whoami} {test&whoami} {test|whoami} {"test&whoami} {"test|whoami} @@ -445,6 +454,25 @@ set injectList { {test^whoami} {test^^echo ^^^} {test"^whoami} {test"^^echo ^^^} {test"^echo ^^^"} {test""^echo" ^^^"} + + {test%USERDOMAIN%\%USERNAME%} + {test" %USERDOMAIN%\%USERNAME%} + {test%USERDOMAIN%\\%USERNAME%} + {test" %USERDOMAIN%\\%USERNAME%} + {test%USERDOMAIN%&%USERNAME%} + {test" %USERDOMAIN%&%USERNAME%} + {test%USERDOMAIN%\&\%USERNAME%} + {test" %USERDOMAIN%\&\%USERNAME%} + + {test%USERDOMAIN%\&\test} + {test" %USERDOMAIN%\&\test} + {test%USERDOMAIN%\\&\\test} + {test" %USERDOMAIN%\\&\\test} + + {test%USERDOMAIN%\&\"test} + {test" %USERDOMAIN%\&\"test} + {test%USERDOMAIN%\\&\\"test} + {test" %USERDOMAIN%\\&\\"test} } ### validate the pass-thru from BuildCommandLine() to the crt's parse_cmdline(). diff --git a/win/tclWinPipe.c b/win/tclWinPipe.c index a6a8f22..728e43a 100644 --- a/win/tclWinPipe.c +++ b/win/tclWinPipe.c @@ -1527,6 +1527,86 @@ ApplicationType( *---------------------------------------------------------------------- */ +static const char * +BuildCmdLineBypassBS( + const char *current, + const char **bspos +) { + /* mark first backslash possition */ + if (!*bspos) { + *bspos = current; + } + do { + current++; + } while (*current == '\\'); + return current; +} + +static void +QuoteCmdLineBackslash( + Tcl_DString *dsPtr, + const char *start, + const char *current, + const char *bspos +) { + if (!bspos) { + if (current > start) { /* part before current (special) */ + Tcl_DStringAppend(dsPtr, start, (int) (current - start)); + } + } else { + if (bspos > start) { /* part before first backslash */ + Tcl_DStringAppend(dsPtr, start, (int) (bspos - start)); + } + while (bspos++ < current) { /* each backslash twice */ + Tcl_DStringAppend(dsPtr, "\\\\", 2); + } + } +} + +static const char * +QuoteCmdLinePart( + Tcl_DString *dsPtr, + const char *start, + const char *special, + const char *specMetaChars, + const char **bspos +) { + if (!*bspos) { + /* rest before special (before quote) */ + QuoteCmdLineBackslash(dsPtr, start, special, NULL); + start = special; + } else { + /* rest before first backslash and backslashes into new quoted block */ + QuoteCmdLineBackslash(dsPtr, start, *bspos, NULL); + start = *bspos; + } + /* + * escape all special chars enclosed in quotes like `"..."`, note that here we + * don't must escape `\` (with `\`), because it's outside of the main quotes, + * so `\` remains `\`, but important - not at end of part, because results as + * before the quote, so `%\%\` should be escaped as `"%\%"\\`). + */ + Tcl_DStringAppend(dsPtr, "\"", 1); /* opening escape quote-char */ + do { + *bspos = NULL; + special++; + if (*special == '\\') { + /* bypass backslashes (and mark first backslash possition)*/ + special = BuildCmdLineBypassBS(special, bspos); + if (*special == '\0') break; + } + } while (*special && strchr(specMetaChars, *special)); + if (!*bspos) { + /* unescaped rest before quote */ + QuoteCmdLineBackslash(dsPtr, start, special, NULL); + } else { + /* unescaped rest before first backslash (rather belongs to the main block) */ + QuoteCmdLineBackslash(dsPtr, start, *bspos, NULL); + } + Tcl_DStringAppend(dsPtr, "\"", 1); /* closing escape quote-char */ + return special; +} + static void BuildCommandLine( const char *executable, /* Full path of executable (including @@ -1536,11 +1616,14 @@ BuildCommandLine( Tcl_DString *linePtr) /* Initialized Tcl_DString that receives the * command line (TCHAR). */ { - const char *arg, *start, *special; + const char *arg, *start, *special, *bspos; int quote = 0, i; Tcl_DString ds; - const static char *specMetaChars = "&|^<>!%()"; + /* characters to enclose in quotes if unpaired quote flag set */ + const static char *specMetaChars = "&|^<>!()%"; + /* characters to enclose in quotes in any case (regardless unpaired-flag) */ + const static char *specMetaChars2 = "%"; Tcl_DStringInit(&ds); @@ -1566,6 +1649,7 @@ BuildCommandLine( * 2 - previous arguments chain contains unpaired quote-char; */ quote &= ~1; /* reset escape flag */ + bspos = NULL; if (arg[0] == '\0') { quote = 1; } else { @@ -1585,26 +1669,22 @@ BuildCommandLine( /* nothing to escape */ Tcl_DStringAppend(&ds, arg, -1); } else { - /* start of argument (open quote-char) */ + /* start of argument (main opening quote-char) */ Tcl_DStringAppend(&ds, "\"", 1); start = arg; for (special = arg; *special != '\0'; ) { - /* `\\` or `\"` or `\` at end (so equal `\"` because quoted) */ - if (*special == '\\' && (special[1] == '\\' || special[1] == '"' || special[1] == '\0')) { - if (special > start) { - Tcl_DStringAppend(&ds, start, (int) (special - start)); - } - /* escape using backslash */ - Tcl_DStringAppend(&ds, "\\\\", 2); - start = ++special; - continue; + /* position of `\` is important before quote or at end (equal `\"` because quoted) */ + if (*special == '\\') { + /* bypass backslashes (and mark first backslash possition)*/ + special = BuildCmdLineBypassBS(special, &bspos); + if (*special == '\0') break; } /* ["] */ if (*special == '"') { quote ^= 2; /* invert unpaired flag - observe unpaired quotes */ - if (special > start) { - Tcl_DStringAppend(&ds, start, (int) (special - start)); - } + /* add part before (and escape backslashes before quote) */ + QuoteCmdLineBackslash(&ds, start, special, bspos); + bspos = NULL; /* escape using backslash */ Tcl_DStringAppend(&ds, "\\\"", 2); start = ++special; @@ -1612,27 +1692,25 @@ BuildCommandLine( } /* unpaired (escaped) quote causes special handling on meta-chars */ if ((quote & 2) && strchr(specMetaChars, *special)) { - if (special > start) { - Tcl_DStringAppend(&ds, start, (int) (special - start)); - } - /* unpaired - escape all special chars inside quotes like `"..."` */ - Tcl_DStringAppend(&ds, "\"", 1); - start = special; - do { - special++; - } while(*special && strchr(specMetaChars, *special)); - Tcl_DStringAppend(&ds, start, (int) (special - start)); - Tcl_DStringAppend(&ds, "\"", 1); - start = special; + special = QuoteCmdLinePart(&ds, start, special, specMetaChars, &bspos); + /* start to current or first backslash */ + start = !bspos ? special : bspos; continue; } + /* special case for % - should be enclosed always (paired also) */ + if (strchr(specMetaChars2, *special)) { + special = QuoteCmdLinePart(&ds, start, special, specMetaChars2, &bspos); + /* start to current or first backslash */ + start = !bspos ? special : bspos; + continue; + } + /* other not special (and not meta) character */ + bspos = NULL; /* reset last backslash possition (not interesting) */ special++; } - /* rest of argument (don't contain special chars) */ - if (special > start) { - Tcl_DStringAppend(&ds, start, (int) (special - start)); - } - /* end of argument (closed quote-char) */ + /* rest of argument (and escape backslashes before closing main quote) */ + QuoteCmdLineBackslash(&ds, start, special, bspos); + /* end of argument (main closing quote-char) */ Tcl_DStringAppend(&ds, "\"", 1); } } -- cgit v0.12