1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-04-23 06:32:10 +02:00

Bug 494246: prepare command line arguments properly for Windows GDB

Includes splitting out and expanding CommandLineArgsTest
from LaunchConfigurationAndRestartTest.

Change-Id: I19fa97a847d908c1c780ca767cf688f26a51d684
Signed-off-by: Jonah Graham <jonah@kichwacoders.com>
This commit is contained in:
Jonah Graham 2016-11-14 23:42:55 +00:00
parent 2ae122963c
commit 03b701c9a5
5 changed files with 421 additions and 152 deletions

View file

@ -24,14 +24,18 @@ import org.eclipse.osgi.service.environment.Constants;
*/
public class CommandLineUtil {
public static String[] argumentsToArray(String line) {
private static boolean isWindows() {
boolean osWin;
try {
osWin = Platform.getOS().equals(Constants.OS_WIN32);
} catch (Exception e) {
osWin = false;
}
if (osWin) {
return osWin;
}
public static String[] argumentsToArray(String line) {
if (isWindows()) {
return argumentsToArrayWindowsStyle(line);
} else {
return argumentsToArrayUnixStyle(line);
@ -260,4 +264,160 @@ public class CommandLineUtil {
}
return aList.toArray(new String[aList.size()]);
}
/**
* Converts argument array to a string suitable for passing to Bash like:
*
* This process reverses {@link #argumentsToArray(String)}, but does not
* restore the exact same results.
*
* @param args
* the arguments to convert and escape
* @param encodeNewline
* <code>true</code> if newline (<code>\r</code> or
* <code>\n</code>) should be encoded
*
* @return args suitable for passing to some process that decodes the string
* into an argument array
* @since 6.2
*/
public static String argumentsToString(String[] args, boolean encodeNewline) {
if (isWindows()) {
return argumentsToStringWindowsCreateProcess(args, encodeNewline);
} else {
// XXX: Bug 507568: We are currently using incorrect assumption that
// shell is always bash. AFAIK this is only problematic when
// encoding newlines
return argumentsToStringBash(args, encodeNewline);
}
}
/**
* Converts argument array to a string suitable for passing to Bash like:
*
* <pre>
* /bin/bash -c &lt;args&gt;
* </pre>
*
* In this case the arguments array passed to exec or equivalent will be:
*
* <pre>
* argv[0] = "/bin/bash"
* argv[1] = "-c"
* argv[2] = argumentsToStringBashStyle(argumentsAsArray)
* </pre>
*
* Replace and concatenate all occurrences of:
* <ul>
* <li><code>'</code> with <code>"'"</code>
* <p>
* (as <code>'</code> is used to surround everything else it has to be
* quoted or escaped)</li>
* <li>newline character, if encoded, with <code>$'\n'</code>
* <p>
* (<code>\n</code> is treated literally within quotes or as just 'n'
* otherwise, whilst supplying the newline character literally ends the
* command)</li>
* <li>Anything in between and around these occurrences is surrounded by
* single quotes.
* <p>
* (to prevent bash from carrying out substitutions or running arbitrary
* code with backticks or <code>$()</code>)</li>
* <ul>
*
* @param args
* the arguments to convert and escape
* @param encodeNewline
* <code>true</code> if newline (<code>\r</code> or
* <code>\n</code>) should be encoded
* @return args suitable for passing as single argument to bash
* @since 6.2
*/
public static String argumentsToStringBash(String[] args, boolean encodeNewline) {
StringBuilder builder = new StringBuilder();
for (String arg : args) {
if (builder.length() > 0) {
builder.append(' ');
}
builder.append('\'');
for (int j = 0; j < arg.length(); j++) {
char c = arg.charAt(j);
if (c == '\'') {
builder.append("'\"'\"'"); //$NON-NLS-1$
} else if (c == '\r' && encodeNewline) {
builder.append("'$'\\r''"); //$NON-NLS-1$
} else if (c == '\n' && encodeNewline) {
builder.append("'$'\\n''"); //$NON-NLS-1$
} else {
builder.append(c);
}
}
builder.append('\'');
}
return builder.toString();
}
/**
* Converts argument array to a string suitable for passing to Windows
* CreateProcess
*
* @param args
* the arguments to convert and escape
* @param encodeNewline
* <code>true</code> if newline (<code>\r</code> or
* <code>\n</code>) should be encoded
* @return args suitable for passing as single argument to CreateProcess on
* Windows
* @since 6.2
*/
public static String argumentsToStringWindowsCreateProcess(String[] args, boolean encodeNewline) {
StringBuilder builder = new StringBuilder();
for (String arg : args) {
if (builder.length() > 0) {
builder.append(' ');
}
builder.append('"');
for (int j = 0; j < arg.length(); j++) {
/*
* backslashes are special if and only if they are followed by a
* double-quote (") therefore doubling them depends on what is
* next
*/
int numBackslashes = 0;
for (; j < arg.length() && arg.charAt(j) == '\\'; j++) {
numBackslashes++;
}
if (j == arg.length()) {
appendNBackslashes(builder, numBackslashes * 2);
} else if (arg.charAt(j) == '"') {
appendNBackslashes(builder, numBackslashes * 2);
builder.append('"');
} else if ((arg.charAt(j) == '\n' || arg.charAt(j) == '\r') && encodeNewline) {
builder.append(' ');
} else {
/*
* this really is numBackslashes (no missing * 2), that is
* because next character is not a double-quote (")
*/
appendNBackslashes(builder, numBackslashes);
builder.append(arg.charAt(j));
}
}
builder.append('"');
}
return builder.toString();
}
private static void appendNBackslashes(StringBuilder builder, int numBackslashes) {
for (int i = 0; i < numBackslashes; i++) {
builder.append('\\');
}
}
}

View file

@ -15,6 +15,7 @@ package org.eclipse.cdt.dsf.mi.service.command.commands;
import java.util.ArrayList;
import org.eclipse.cdt.dsf.mi.service.IMIContainerDMContext;
import org.eclipse.cdt.utils.CommandLineUtil;
/**
* -gdb-set args ARGS
@ -34,45 +35,15 @@ public class MIGDBSetArgs extends MIGDBSet {
super(dmc, null);
fParameters = new ArrayList<Adjustable>();
fParameters.add(new MIStandardParameterAdjustable("args")); //$NON-NLS-1$
for (int i = 0; i < arguments.length; i++) {
fParameters.add(new MIArgumentAdjustable(arguments[i]));
}
}
private static class MIArgumentAdjustable extends MICommandAdjustable {
public MIArgumentAdjustable(String value) {
super(value);
}
@Override
public String getAdjustedValue() {
// Replace and concatenate all occurrences of:
// ' with "'"
// (as ' is used to surround everything else
// it has to be quoted or escaped)
// newline character with $'\n'
// (\n is treated literally within quotes or
// as just 'n' otherwise, whilst supplying
// the newline character literally ends the command)
// Anything in between and around these occurrences
// is surrounded by single quotes.
// (to prevent bash from carrying out substitutions
// or running arbitrary code with backticks or $())
StringBuilder builder = new StringBuilder();
builder.append('\'');
for (int j = 0; j < value.length(); j++) {
char c = value.charAt(j);
if (c == '\'') {
builder.append("'\"'\"'"); //$NON-NLS-1$
} else if (c == '\n') {
builder.append("'$'\\n''"); //$NON-NLS-1$
} else {
builder.append(c);
}
}
builder.append('\'');
return builder.toString();
}
/*
* GDB-MI terminates the -gdb-set on the newline, so we have to encode
* newlines or we get an MI error. Some platforms (e.g. Bash on
* non-windows) can encode newline into something that is received as a
* newline to the program, other platforms (windows) cannot encode the
* newline in anyway that is recived as a newline, so it is encoded as
* whitepsace.
*/
String args = CommandLineUtil.argumentsToString(arguments, true);
fParameters.add(new MINoChangeAdjustable(args));
}
}

View file

@ -0,0 +1,243 @@
/*******************************************************************************
* Copyright (c) 2011, 2016 Ericsson and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Ericsson - Initial Implementation
* Simon Marchi (Ericsson) - Remove a catch that just fails a test.
* Simon Marchi (Ericsson) - Disable tests for gdb < 7.2.
* Jonah Graham (Kichwa Coders) - Split arguments tests out of LaunchConfigurationAndRestartTest
*******************************************************************************/
package org.eclipse.cdt.tests.dsf.gdb.tests;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.TimeUnit;
import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.debug.service.IExpressions;
import org.eclipse.cdt.dsf.debug.service.IExpressions.IExpressionDMContext;
import org.eclipse.cdt.dsf.debug.service.IFormattedValues.FormattedValueDMData;
import org.eclipse.cdt.dsf.mi.service.MIExpressions;
import org.eclipse.cdt.dsf.mi.service.command.events.MIStoppedEvent;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.tests.dsf.gdb.framework.BaseParametrizedTestCase;
import org.eclipse.cdt.tests.dsf.gdb.framework.SyncUtil;
import org.eclipse.cdt.tests.dsf.gdb.launching.TestsPlugin;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class CommandLineArgsTest extends BaseParametrizedTestCase {
protected static final String EXEC_NAME = "LaunchConfigurationAndRestartTestApp.exe";
private DsfSession fSession;
private DsfServicesTracker fServicesTracker;
private IExpressions fExpService;
@Override
public void doBeforeTest() throws Exception {
removeTeminatedLaunchesBeforeTest();
setLaunchAttributes();
// Can't run the launch right away because each test needs to first set
// ICDTLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS
}
@Override
protected void setLaunchAttributes() {
super.setLaunchAttributes();
// Set the binary
setLaunchAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_NAME, EXEC_PATH + EXEC_NAME);
}
// This method cannot be tagged as @Before, because the launch is not
// running yet. We have to call this manually after all the proper
// parameters have been set for the launch
@Override
protected void doLaunch() throws Exception {
// perform the launch
super.doLaunch();
fSession = getGDBLaunch().getSession();
Runnable runnable = new Runnable() {
@Override
public void run() {
fServicesTracker = new DsfServicesTracker(TestsPlugin.getBundleContext(), fSession.getId());
fExpService = fServicesTracker.getService(IExpressions.class);
}
};
fSession.getExecutor().submit(runnable).get();
}
/**
* Convert a string of form 0x123456 "ab\"cd" to ab"cd
*/
protected String convertDetails(String details) {
// check parser assumptions on input format
assertThat(details, startsWith("0x"));
assertThat(details, containsString(" \""));
assertThat(details, endsWith("\""));
int firstSpace = details.indexOf(' ');
boolean lastWasEscape = false;
StringBuilder sb = new StringBuilder();
for (int i = firstSpace + 2; i < details.length() - 1; i++) {
char c = details.charAt(i);
if (lastWasEscape) {
switch (c) {
case 't':
sb.append('\t');
break;
case 'r':
sb.append('\r');
break;
case 'n':
sb.append('\n');
break;
default:
sb.append(c);
break;
}
lastWasEscape = false;
} else {
if (c == '\\') {
lastWasEscape = true;
} else {
sb.append(c);
}
}
}
assertFalse("unexpected trailing backslash (\\)", lastWasEscape);
return sb.toString();
}
/**
* Check that the target program received the arguments as expected
*
* @param expected
* arguments to check, e.g. check expected[0].equals(argv[1])
*/
protected void checkArguments(String... expected) throws Throwable {
MIStoppedEvent stoppedEvent = getInitialStoppedEvent();
// Check that argc is correct
final IExpressionDMContext argcDmc = SyncUtil.createExpression(stoppedEvent.getDMContext(), "argc");
Query<FormattedValueDMData> query = new Query<FormattedValueDMData>() {
@Override
protected void execute(DataRequestMonitor<FormattedValueDMData> rm) {
fExpService.getFormattedExpressionValue(
fExpService.getFormattedValueContext(argcDmc, MIExpressions.DETAILS_FORMAT), rm);
}
};
fExpService.getExecutor().execute(query);
FormattedValueDMData value = query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
assertTrue("Expected " + (1 + expected.length) + " but got " + value.getFormattedValue(),
value.getFormattedValue().trim().equals(Integer.toString(1 + expected.length)));
// check all argvs are correct
for (int i = 0; i < expected.length; i++) {
final IExpressionDMContext argvDmc = SyncUtil.createExpression(stoppedEvent.getDMContext(),
"argv[" + (i + 1) + "]");
Query<FormattedValueDMData> query2 = new Query<FormattedValueDMData>() {
@Override
protected void execute(DataRequestMonitor<FormattedValueDMData> rm) {
fExpService.getFormattedExpressionValue(
fExpService.getFormattedValueContext(argvDmc, MIExpressions.DETAILS_FORMAT), rm);
}
};
fExpService.getExecutor().execute(query2);
FormattedValueDMData value2 = query2.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
String details = value2.getFormattedValue();
String actual = convertDetails(details);
assertEquals(expected[i], actual);
}
}
/**
* Run the program, setting ATTR_PROGRAM_ARGUMENTS to the attrProgramArgs
* and ensuring debugged program receives args for argv (excluding argv[0]
* which isn't checked)
*/
protected void doTest(String attrProgramArgs, String... args) throws Throwable {
setLaunchAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, attrProgramArgs);
doLaunch();
checkArguments(args);
}
/**
* This test will tell the launch to set some arguments for the program. We
* will then check that the program has the same arguments.
*/
@Test
public void testSettingArguments() throws Throwable {
doTest("1 2 3\n4 5 6", "1", "2", "3", "4", "5", "6");
}
/**
* This test will tell the launch to set some arguments for the program. We
* will then check that the program has the same arguments. See bug 381804
*/
@Test
public void testSettingArgumentsWithSymbols() throws Throwable {
// Set a argument with double quotes and spaces, which should be
// considered a single argument
doTest("--c=\"c < s: 'a' t: 'b'>\"", "--c=c < s: 'a' t: 'b'>");
}
/**
* This test will tell the launch to set some more arguments for the
* program. We will then check that the program has the same arguments. See
* bug 474648
*/
@Test
public void testSettingArgumentsWithSpecialSymbols() throws Throwable {
// Test that arguments are parsed correctly:
// The string provided by the user is split into arguments on spaces
// except for those inside quotation marks, double or single.
// Any character within quotation marks or after the backslash character
// is treated literally, whilst these special characters have to be
// escaped explicitly to be recorded.
// All other characters including semicolons, backticks, pipes, dollars
// and newlines
// must be treated literally.
doTest("--abc=\"x;y;z\nsecondline: \"`date`$PS1\"`date | wc`\"",
"--abc=x;y;z\nsecondline: `date`$PS1`date | wc`");
}
/**
* Check combinations of quote characters
*/
@Test
public void testSettingArgumentsWithQuotes() throws Throwable {
doTest("\"'\" '\"'", "'", "\"");
}
/**
* Check tab characters
*/
@Test
public void testSettingArgumentsWithTabs() throws Throwable {
doTest("\"\t\"\t'\t'", "\t", "\t");
}
}

View file

@ -488,6 +488,10 @@ public class LaunchConfigurationAndRestartTest extends BaseParametrizedTestCase
/**
* This test will tell the launch to set some arguments for the program. We will
* then check that the program has the same arguments.
*
* NOTE: The main setting arguments tests are in {@link CommandLineArgsTest}, this
* test remains here to test interaction of command line arguments are restarting.
* See {@link #testSettingArgumentsRestart()}
*/
@Test
public void testSettingArguments() throws Throwable {
@ -552,116 +556,6 @@ public class LaunchConfigurationAndRestartTest extends BaseParametrizedTestCase
testSettingArguments();
}
/**
* This test will tell the launch to set some arguments for the program. We will
* then check that the program has the same arguments.
* See bug 381804
*/
@Test
public void testSettingArgumentsWithSymbols() throws Throwable {
// Set a argument with double quotes and spaces, which should be considered a single argument
String argumentToPreserveSpaces = "--c=\"c < s: 'a' t: 'b'>\"";
String argumentUsedByGDB = "\"--c=c < s: 'a' t: 'b'>\"";
setLaunchAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, argumentToPreserveSpaces);
doLaunch();
MIStoppedEvent stoppedEvent = getInitialStoppedEvent();
// Check that argc is correct
final IExpressionDMContext argcDmc = SyncUtil.createExpression(stoppedEvent.getDMContext(), "argc");
Query<FormattedValueDMData> query = new Query<FormattedValueDMData>() {
@Override
protected void execute(DataRequestMonitor<FormattedValueDMData> rm) {
fExpService.getFormattedExpressionValue(
fExpService.getFormattedValueContext(argcDmc, MIExpressions.DETAILS_FORMAT), rm);
}
};
fExpService.getExecutor().execute(query);
FormattedValueDMData value = query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
// Argc should be 2: the program name and the one arguments
assertTrue("Expected 2 but got " + value.getFormattedValue(),
value.getFormattedValue().trim().equals("2"));
// Check that argv is also correct.
final IExpressionDMContext argvDmc = SyncUtil.createExpression(stoppedEvent.getDMContext(), "argv[argc-1]");
Query<FormattedValueDMData> query2 = new Query<FormattedValueDMData>() {
@Override
protected void execute(DataRequestMonitor<FormattedValueDMData> rm) {
fExpService.getFormattedExpressionValue(
fExpService.getFormattedValueContext(argvDmc, MIExpressions.DETAILS_FORMAT), rm);
}
};
fExpService.getExecutor().execute(query2);
value = query2.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
assertTrue("Expected \"" + argumentUsedByGDB + "\" but got " + value.getFormattedValue(),
value.getFormattedValue().trim().endsWith(argumentUsedByGDB));
}
/**
* This test will tell the launch to set some more arguments for the program. We will
* then check that the program has the same arguments.
* See bug 474648
*/
@Test
public void testSettingArgumentsWithSpecialSymbols() throws Throwable {
// Test that arguments are parsed correctly:
// The string provided by the user is split into arguments on spaces
// except for those inside quotation marks, double or single.
// Any character within quotation marks or after the backslash character
// is treated literally, whilst these special characters have to be
// escaped explicitly to be recorded.
// All other characters including semicolons, backticks, pipes, dollars and newlines
// must be treated literally.
String argumentToPreserveSpaces = "--abc=\"x;y;z\nsecondline: \"`date`$PS1\"`date | wc`\"";
String argumentUsedByGDB = "\"--abc=x;y;z\\nsecondline: `date`$PS1`date | wc`\"";
setLaunchAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, argumentToPreserveSpaces);
doLaunch();
MIStoppedEvent stoppedEvent = getInitialStoppedEvent();
// Check that argc is correct
final IExpressionDMContext argcDmc = SyncUtil.createExpression(stoppedEvent.getDMContext(), "argc");
Query<FormattedValueDMData> query = new Query<FormattedValueDMData>() {
@Override
protected void execute(DataRequestMonitor<FormattedValueDMData> rm) {
fExpService.getFormattedExpressionValue(
fExpService.getFormattedValueContext(argcDmc, MIExpressions.DETAILS_FORMAT), rm);
}
};
fExpService.getExecutor().execute(query);
FormattedValueDMData value = query.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
// Argc should be 2: the program name and the four arguments.
assertTrue("Expected 2 but got " + value.getFormattedValue(),
value.getFormattedValue().trim().equals("2"));
// Check that argv is also correct.
final IExpressionDMContext argvDmc = SyncUtil.createExpression(stoppedEvent.getDMContext(), "argv[argc-1]");
Query<FormattedValueDMData> query2 = new Query<FormattedValueDMData>() {
@Override
protected void execute(DataRequestMonitor<FormattedValueDMData> rm) {
fExpService.getFormattedExpressionValue(
fExpService.getFormattedValueContext(argvDmc, MIExpressions.DETAILS_FORMAT), rm);
}
};
fExpService.getExecutor().execute(query2);
value = query2.get(TestsPlugin.massageTimeout(500), TimeUnit.MILLISECONDS);
assertTrue("Expected \"" + argumentUsedByGDB + "\" but got " + value.getFormattedValue(),
value.getFormattedValue().endsWith(argumentUsedByGDB));
}
/**
* Repeat the test testSettingArguments, but after a restart.
*/
@Test
public void testSettingArgumentsWithSymbolsRestart() throws Throwable {
fRestart = true;
testSettingArgumentsWithSymbols();
}
/**
* This test will tell the launch to "stop on main" at method main(), which we will verify.
*/

View file

@ -59,6 +59,7 @@ import org.junit.runners.Suite;
PostMortemCoreTest.class,
CommandTimeoutTest.class,
ThreadStackFrameSyncTest.class,
CommandLineArgsTest.class,
/* Add your test class here */
})
public class SuiteGdb {