Issue #9309 - More comprehensive escaping of command line options

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2023-02-03 10:20:46 -06:00
parent 016de2faeb
commit 45fdebc0fb
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
2 changed files with 46 additions and 34 deletions

View File

@ -19,6 +19,17 @@ import java.util.List;
public class CommandLineBuilder public class CommandLineBuilder
{ {
// Matrix of 7-bit characters that needs escaping on command line.
private static final boolean[] NEEDS_ESCAPING = new boolean[127];
static
{
for (char c : " %$\\{}[]()\"|<>&;`!".toCharArray())
{
NEEDS_ESCAPING[c] = true;
}
}
public static File findExecutable(File root, String path) public static File findExecutable(File root, String path)
{ {
String npath = path.replace('/', File.separatorChar); String npath = path.replace('/', File.separatorChar);
@ -59,21 +70,28 @@ public class CommandLineBuilder
* *
* @param arg the argument to quote * @param arg the argument to quote
* @return the quoted and escaped argument * @return the quoted and escaped argument
* @deprecated use {@link #escape(String) instead}
*/ */
@Deprecated
public static String quote(String arg) public static String quote(String arg)
{ {
boolean needsQuoting = (arg.indexOf(' ') >= 0) || (arg.indexOf('"') >= 0); return escape(arg);
if (!needsQuoting)
{
return arg;
} }
/**
* Escape the raw string to make it suitable for use on a command line.
*
* @param arg the argument to escape
* @return the escaped argument
*/
public static String escape(String arg)
{
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
// buf.append('"');
boolean escaped = false; boolean escaped = false;
boolean quoted = false; boolean quoted = false;
for (char c : arg.toCharArray()) for (char c : arg.toCharArray())
{ {
if (!quoted && !escaped && ((c == '"') || (c == ' '))) if (!quoted && !escaped && NEEDS_ESCAPING[c])
{ {
buf.append("\\"); buf.append("\\");
} }
@ -85,7 +103,6 @@ public class CommandLineBuilder
escaped = (c == '\\'); escaped = (c == '\\');
buf.append(c); buf.append(c);
} }
// buf.append('"');
return buf.toString(); return buf.toString();
} }
@ -113,7 +130,7 @@ public class CommandLineBuilder
{ {
if (arg != null) if (arg != null)
{ {
args.add(quote(arg)); args.add(escape(arg));
} }
} }
@ -136,11 +153,11 @@ public class CommandLineBuilder
{ {
if ((value != null) && (value.length() > 0)) if ((value != null) && (value.length() > 0))
{ {
args.add(quote(name + "=" + value)); args.add(escape(name + "=" + value));
} }
else else
{ {
args.add(quote(name)); args.add(escape(name));
} }
} }
@ -180,7 +197,7 @@ public class CommandLineBuilder
{ {
buf.append(delim); buf.append(delim);
} }
buf.append(quote(arg)); buf.append(arg); // we assume escaping has occurred during addArg
} }
return buf.toString(); return buf.toString();

View File

@ -21,54 +21,49 @@ import static org.hamcrest.Matchers.is;
public class CommandLineBuilderTest public class CommandLineBuilderTest
{ {
private CommandLineBuilder cmd = new CommandLineBuilder("java");
@BeforeEach
public void setUp()
{
cmd.addEqualsArg("-Djava.io.tmpdir", "/home/java/temp dir/");
cmd.addArg("--version");
}
@Test @Test
public void testSimpleCommandline() public void testSimpleCommandline()
{ {
CommandLineBuilder cmd = new CommandLineBuilder("java");
cmd.addEqualsArg("-Djava.io.tmpdir", "/home/java/temp dir/");
cmd.addArg("--version");
assertThat(cmd.toString(), is("java -Djava.io.tmpdir=/home/java/temp\\ dir/ --version")); assertThat(cmd.toString(), is("java -Djava.io.tmpdir=/home/java/temp\\ dir/ --version"));
} }
@Test @Test
public void testQuotingSimple() public void testEscapedSimple()
{ {
assertQuoting("/opt/jetty", "/opt/jetty"); assertEscaping("/opt/jetty", "/opt/jetty");
} }
@Test @Test
public void testQuotingSpaceInPath() public void testEscapedSpaceInPath()
{ {
assertQuoting("/opt/jetty 7/home", "/opt/jetty\\ 7/home"); assertEscaping("/opt/jetty 7/home", "/opt/jetty\\ 7/home");
} }
@Test @Test
public void testQuotingSpaceAndQuotesInPath() public void testEscapedSpaceAndQuotesInPath()
{ {
assertQuoting("/opt/jetty 7 \"special\"/home", "/opt/jetty\\ 7\\ \\\"special\\\"/home"); assertEscaping("/opt/jetty 7 \"special\"/home", "/opt/jetty\\ 7\\ \\\"special\\\"/home");
} }
@Test @Test
public void testToStringIsQuotedEvenIfArgsAreNotQuotedForProcessBuilder() public void testEscapedFormattingString()
{ {
System.out.println(cmd.toString()); assertEscaping("%{client}a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t \"%r\" %s %O \"%{Referer}i\" \"%{User-Agent}i\"",
"\\%\\{client\\}a\\ -\\ \\%u\\ \\%\\{dd/MMM/yyyy:HH:mm:ss\\ ZZZ\\|GMT\\}t\\ \\\"\\%r\\\"\\ \\%s\\ \\%O\\ \\\"\\%\\{Referer\\}i\\\"\\ \\\"\\%\\{User-Agent\\}i\\\"");
} }
@Test @Test
public void testQuoteQuotationMarks() public void testEscapeQuotationMarks()
{ {
assertQuoting("-XX:OnOutOfMemoryError='kill -9 %p'", "-XX:OnOutOfMemoryError='kill -9 %p'"); assertEscaping("-XX:OnOutOfMemoryError='kill -9 %p'", "-XX:OnOutOfMemoryError='kill -9 %p'");
} }
private void assertQuoting(String raw, String expected) private void assertEscaping(String raw, String expected)
{ {
String actual = CommandLineBuilder.quote(raw); String actual = CommandLineBuilder.escape(raw);
assertThat("Quoted version of [" + raw + "]", actual, is(expected)); assertThat("Escaped version of [" + raw + "]", actual, is(expected));
} }
} }