Browse files

ae.sys.cmd: Replace shell escaping code with the Phobos submission

  • Loading branch information...
1 parent e6d616e commit 39378eb929d46472073bf03007de2fae468706a3 @CyberShadow committed Apr 7, 2012
Showing with 336 additions and 37 deletions.
  1. +336 −37 sys/cmd.d
View
373 sys/cmd.d
@@ -27,41 +27,106 @@ string getTempFileName(string extension)
// ************************************************************************
-// Quote an argument in a manner conforming to the behavior of CommandLineToArgvW.
-// References:
-// * http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
-// * http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx
+// Will be made redundant by https://github.com/D-Programming-Language/phobos/pull/457
+
+/*
+ Command line arguments exist in three forms:
+ 1) string or char* array, as received by main.
+ Also used internally on POSIX systems.
+ 2) Command line string, as used in Windows'
+ CreateProcess and CommandLineToArgvW functions.
+ A specific quoting and escaping algorithm is used
+ to distinguish individual arguments.
+ 3) Shell command string, as written at a shell prompt
+ or passed to cmd /C - this one may contain shell
+ control characters, e.g. > or | for redirection /
+ piping - thus, yet another layer of escaping is
+ used to distinguish them from program arguments.
+
+ Except for escapeWindowsArgument, the intermediary
+ format (2) is hidden away from the user in this module.
+*/
+
+pure @safe nothrow
+private char[] charAllocator(size_t size) { return new char[size]; }
-string escapeWindowsArgument(string arg)
+/**
+ Quote an argument in a manner conforming to the behavior of
+ $(LINK2 http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx,
+ CommandLineToArgvW).
+*/
+
+pure nothrow
+string escapeWindowsArgument(in char[] arg)
+{
+ // Rationale for leaving this function as public:
+ // this algorithm of escaping paths is also used in other software,
+ // e.g. DMD's response files.
+
+ auto buf = escapeWindowsArgumentImpl!charAllocator(arg);
+ return assumeUnique(buf);
+}
+
+@safe nothrow
+private char[] escapeWindowsArgumentImpl(alias allocator)(in char[] arg)
+ if (is(typeof(allocator(size_t.init)[0] = char.init)))
{
- auto escapeIt = new bool[arg.length];
+ // References:
+ // * http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
+ // * http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx
+
+ // Calculate the total string size.
+
+ // Trailing backslashes must be escaped
bool escaping = true;
- foreach_reverse (i, c; arg)
+ // Result size = input size + 2 for surrounding quotes + 1 for the
+ // backslash for each escaped character.
+ size_t size = 1 + arg.length + 1;
+
+ foreach_reverse (c; arg)
{
if (c == '"')
- escapeIt[i] = escaping = true;
+ {
+ escaping = true;
+ size++;
+ }
else
if (c == '\\')
- escapeIt[i] = escaping;
+ {
+ if (escaping)
+ size++;
+ }
else
escaping = false;
}
- string s = `"`;
- foreach (i, c; arg)
+ // Construct result string.
+
+ auto buf = allocator(size);
+ size_t p = size;
+ buf[--p] = '"';
+ escaping = true;
+ foreach_reverse (c; arg)
{
- if (escapeIt[i])
- s ~= '\\';
- s ~= c;
+ if (c == '"')
+ escaping = true;
+ else
+ if (c != '\\')
+ escaping = false;
+
+ buf[--p] = c;
+ if (escaping)
+ buf[--p] = '\\';
}
- s ~= '"';
+ buf[--p] = '"';
+ assert(p == 0);
- return s;
+ return buf;
}
version(Windows) version(unittest)
{
- import win32.windows;
+ import core.sys.windows.windows;
import core.stdc.stddef;
extern (Windows) wchar_t** CommandLineToArgvW(wchar_t*, int*);
@@ -78,13 +143,12 @@ version(Windows) version(unittest)
`C:\Program Files\`,
];
- foreach (c1; `\" _*`)
- foreach (c2; `\" _*`)
- foreach (c3; `\" _*`)
- foreach (c4; `\" _*`)
- testStrings ~= [c1, c2, c3, c4].replace("*", "");
-
- import std.conv;
+ enum CHARS = `_x\" *&^`; // _ is placeholder for nothing
+ foreach (c1; CHARS)
+ foreach (c2; CHARS)
+ foreach (c3; CHARS)
+ foreach (c4; CHARS)
+ testStrings ~= [c1, c2, c3, c4].replace("_", "");
foreach (s; testStrings)
{
@@ -100,30 +164,265 @@ version(Windows) version(unittest)
}
}
-string escapeShellArgument(string arg)
+pure nothrow
+private string escapePosixArgument(in char[] arg)
{
+ auto buf = escapePosixArgumentImpl!charAllocator(arg);
+ return assumeUnique(buf);
+}
+
+pure @safe nothrow
+private char[] escapePosixArgumentImpl(alias allocator)(in char[] arg)
+ if (is(typeof(allocator(size_t.init)[0] = char.init)))
+{
+ // '\'' means: close quoted part of argument, append an escaped
+ // single quote, and reopen quotes
+
+ // Below code is equivalent to:
+ // return `'` ~ std.array.replace(arg, `'`, `'\''`) ~ `'`;
+
+ size_t size = 1 + arg.length + 1;
+ foreach (c; arg)
+ if (c == '\'')
+ size += 3;
+
+ auto buf = allocator(size);
+ size_t p = 0;
+ buf[p++] = '\'';
+ foreach (c; arg)
+ if (c == '\'')
+ buf[p..p+4] = `'\''`;
+ else
+ buf[p++] = c;
+ buf[p++] = '\'';
+ assert(p == size);
+
+ return buf;
+}
+
+@safe nothrow
+private auto escapeShellArgument(alias allocator)(in char[] arg)
+{
+ // The unittest for this function requires special
+ // preparation - see below.
+
version (Windows)
- {
- return escapeWindowsArgument(arg);
- }
+ return escapeWindowsArgumentImpl!allocator(arg);
else
+ return escapePosixArgumentImpl!allocator(arg);
+}
+
+pure nothrow
+private string escapeShellArguments(in char[][] args)
+{
+ char[] buf;
+
+ @safe nothrow
+ char[] allocator(size_t size)
{
- // '\'' means: close quoted part of argument, append an escaped
- // single quote, and reopen quotes
- return `'` ~ std.array.replace(arg, `'`, `'\''`) ~ `'`;
+ if (buf.length == 0)
+ return buf = new char[size];
+ else
+ {
+ auto p = buf.length;
+ buf.length = buf.length + 1 + size;
+ buf[p++] = ' ';
+ return buf[p..p+size];
+ }
}
+
+ foreach (arg; args)
+ escapeShellArgument!allocator(arg);
+ return assumeUnique(buf);
}
-string escapeShellCommand(string[] args)
+string escapeWindowsShellCommand(in char[] command)
+{
+ auto result = appender!string();
+ result.reserve(command.length);
+
+ foreach (c; command)
+ switch (c)
+ {
+ case '\0':
+ assert(0, "Cannot put NUL in command line");
+ case '\r':
+ case '\n':
+ assert(0, "CR/LF are not escapable");
+ case '\x01': .. case '\x09':
+ case '\x0B': .. case '\x0C':
+ case '\x0E': .. case '\x1F':
+ case '"':
+ case '^':
+ case '&':
+ case '<':
+ case '>':
+ case '|':
+ result.put('^');
+ goto default;
+ default:
+ result.put(c);
+ }
+ return result.data();
+}
+
+private string escapeShellCommandString(string command)
{
- import std.array, std.algorithm;
- string command = array(map!escapeShellArgument(args)).join(" ");
version (Windows)
+ return escapeWindowsShellCommand(command);
+ else
+ return command;
+}
+
+/**
+ Escape an argv-style argument array to be used with the
+ $(D system) or $(D shell) functions.
+
+ Example:
+---
+string url = "http://dlang.org/";
+system(escapeShellCommand("wget", url, "-O", "dlang-index.html"));
+---
+
+ Concatenate multiple $(D escapeShellCommand) and
+ $(D escapeShellFileName) results to use shell redirection or
+ piping operators.
+
+ Example:
+---
+system(
+ escapeShellCommand("curl", "http://dlang.org/download.html") ~
+ "|" ~
+ escapeShellCommand("grep", "-o", `http://\S*\.zip`) ~
+ ">" ~
+ escapeShellFileName("D download links.txt"));
+---
+*/
+
+string escapeShellCommand(in char[][] args...)
+{
+ return escapeShellCommandString(escapeShellArguments(args));
+}
+
+/**
+ Escape a filename to be used for shell redirection with
+ the $(D system) or $(D shell) functions.
+*/
+
+pure nothrow
+string escapeShellFileName(in char[] fn)
+{
+ // The unittest for this function requires special
+ // preparation - see below.
+
+ version (Windows)
+ return cast(string)('"' ~ fn ~ '"');
+ else
+ return escapePosixArgument(fn);
+}
+
+// Loop generating strings with random characters
+//version = unittest_burnin;
+
+version(unittest_burnin)
+unittest
+{
+ // There are no readily-available commands on all platforms suitable
+ // for properly testing command escaping. The behavior of CMD's "echo"
+ // built-in differs from the POSIX program, and Windows ports of POSIX
+ // environments (Cygwin, msys, gnuwin32) may interfere with their own
+ // "echo" ports.
+
+ // To run this unit test, create std_process_unittest_helper.d with the
+ // following content and compile it:
+ // import std.stdio, std.array; void main(string[] args) { write(args.join("\0")); }
+ // Then, test this module with:
+ // rdmd --main -unittest -version=unittest_burnin process.d
+
+ auto helper = rel2abs("std_process_unittest_helper");
+ assert(shell(helper ~ " hello").split("\0")[1..$] == ["hello"], "Helper malfunction");
+
+ void test(string[] s, string fn)
{
- // Follow CMD's rules for quote parsing (see "cmd /?").
- command = '"' ~ command ~ '"';
+ string e;
+ string[] g;
+
+ e = escapeShellCommand(helper ~ s);
+ {
+ scope(failure) writefln("shell() failed.\nExpected:\t%s\nEncoded:\t%s", s, [e]);
+ g = shell(e).split("\0")[1..$];
+ }
+ assert(s == g, format("shell() test failed.\nExpected:\t%s\nGot:\t\t%s\nEncoded:\t%s", s, g, [e]));
+
+ e = escapeShellCommand(helper ~ s) ~ ">" ~ escapeShellFileName(fn);
+ {
+ scope(failure) writefln("system() failed.\nExpected:\t%s\nFilename:\t%s\nEncoded:\t%s", s, [fn], [e]);
+ system(e);
+ g = readText(fn).split("\0")[1..$];
+ }
+ remove(fn);
+ assert(s == g, format("system() test failed.\nExpected:\t%s\nGot:\t\t%s\nEncoded:\t%s", s, g, [e]));
+ }
+
+ while (true)
+ {
+ string[] args;
+ foreach (n; 0..uniform(1, 4))
+ {
+ string arg;
+ foreach (l; 0..uniform(0, 10))
+ {
+ dchar c;
+ while (true)
+ {
+ version (Windows)
+ {
+ // As long as DMD's system() uses CreateProcessA,
+ // we can't reliably pass Unicode
+ c = uniform(0, 128);
+ }
+ else
+ c = uniform!ubyte();
+
+ if (c == 0)
+ continue; // argv-strings are zero-terminated
+ version (Windows)
+ if (c == '\r' || c == '\n')
+ continue; // newlines are unescapable on Windows
+ break;
+ }
+ arg ~= c;
+ }
+ args ~= arg;
+ }
+
+ // generate filename
+ string fn = "test_";
+ foreach (l; 0..uniform(1, 10))
+ {
+ dchar c;
+ while (true)
+ {
+ version (Windows)
+ c = uniform(0, 128); // as above
+ else
+ c = uniform!ubyte();
+
+ if (c == 0 || c == '/')
+ continue; // NUL and / are the only characters
+ // forbidden in POSIX filenames
+ version (Windows)
+ if (c < '\x20' || c == '<' || c == '>' || c == ':' ||
+ c == '"' || c == '\\' || c == '|' || c == '?' || c == '*')
+ continue; // http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx
+ break;
+ }
+
+ fn ~= c;
+ }
+
+ test(args, fn);
}
- return command;
}
// ************************************************************************

0 comments on commit 39378eb

Please sign in to comment.