/* * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /** * @test * @bug 8282008 * @requires (os.family == "windows") * @run main/othervm ArgCheck * @summary Check invocation of exe and non-exe programs using ProcessBuilder * and arguments with spaces, backslashes, and simple quoting. */ import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * Class to check invocation of java, .cmd, and vbs scripts with arguments and various quote cases. * Can be run standalone to compare results with other Java versions. */ public class ArgCheck { private static final Path SRC_DIR = Paths.get(System.getProperty("test.src", ".")); private static final Path WORK_DIR = Paths.get(System.getProperty("user.dir", ".")); private static final Path TEST_CLASSES = Paths.get(System.getProperty("test.classes", ".")); private static final String ECHO_CMD_PATH = WORK_DIR.resolve("EchoArguments.cmd").toString(); private static final String ECHO_VBS_PATH = WORK_DIR.resolve("EchoArguments.vbs").toString(); // Test argument containing both a space and a trailing backslash // Depending on the mode the final backslash may act as an escape that may turn an added quote to a literal quote private static final String SPACE_AND_BACKSLASH = "SPACE AND BACKSLASH\\"; private static final char DOUBLE_QUOTE = '"'; private static final char BACKSLASH = '\\'; private static final String AMBIGUOUS_PROP_NAME = "jdk.lang.Process.allowAmbiguousCommands"; private static final String AMBIGUOUS_PROP_VALUE = System.getProperty(AMBIGUOUS_PROP_NAME); private static final Boolean AMBIGUOUS_PROP_BOOLEAN = AMBIGUOUS_PROP_VALUE == null ? null : Boolean.valueOf(!"false".equals(AMBIGUOUS_PROP_VALUE)); private static final List ECHO_JAVA_ARGS = Arrays.asList("java", "-classpath", TEST_CLASSES.toString(), "ArgCheck"); private static final List ECHO_CMD_ARGS = Arrays.asList(ECHO_CMD_PATH); private static final List ECHO_VBS_ARGS = Arrays.asList("CScript", "/b", ECHO_VBS_PATH); /** * If zero arguments are supplied, run the test cases, by launching each as a child process. * If there are arguments, then this is a child Java process that prints each argument to stdout. * The test can be run manually with -Djdk.lang.Process.allowAmbiguousCommands={"true", "false", ""} * to run a matching subset of the tests. */ public static void main(String[] args) throws IOException { if (args.length > 0) { // Echo supplied arguments and exit for (String arg : args) System.out.println(arg); return; } System.out.println("Java Version: " + System.getProperty("java.version")); createFiles(); int errors = 0; int success = 0; int skipped = 0; for (CMD cmd : CASES) { // If System property jdk.lang.process.allowAmbiguousCommands matches the case, test it // If undefined, test them all if (AMBIGUOUS_PROP_BOOLEAN == null || AMBIGUOUS_PROP_BOOLEAN.booleanValue() == cmd.allowAmbiguous) { try { testCommand(cmd); success++; } catch (Exception ex) { ex.printStackTrace(); errors++; } } else { // skip unmatched cases skipped++; } } if (skipped > 0) { System.out.printf("%d cases skipped, they did not match the tests with jdk.lang.Process.allowAmbiguousCommands: %s%n", skipped, AMBIGUOUS_PROP_BOOLEAN); } System.out.printf("\nSuccess: %d, errors: %d%n", success, errors); if (errors > 0) { throw new RuntimeException("Errors: " + errors); } } /** * A CMD holds the parameters and the expected result of invoking a process with the parameters. */ static class CMD { /** * Construct a test case. * @param allowAmbiguous true/false to set property jdk.lang.Process.allowAmbiguousCommands * @param command list of command parameters to invoke the executable or script * @param arguments list of arguments (appended to the command) * @param expected expected lines of output from invoked command */ CMD(boolean allowAmbiguous, List command, List arguments, List expected) { this.allowAmbiguous = allowAmbiguous; this.command = command; this.arguments = arguments; this.expected = expected; } final boolean allowAmbiguous; final List command; final List arguments; final List expected; } /** * List of cases with the command, arguments, allowAmbiguous setting, and the expected results */ static final List CASES = Arrays.asList( // allowAmbiguousCommands = false, without application supplied double-quotes. // The space in the argument requires it to be quoted, the final backslash // must not be allowed to turn the quote that is added into a literal // instead of closing the quote. new CMD(false, ECHO_JAVA_ARGS, Arrays.asList(SPACE_AND_BACKSLASH, "ARG_1"), Arrays.asList(SPACE_AND_BACKSLASH, "ARG_1")), new CMD(false, ECHO_CMD_ARGS, Arrays.asList(SPACE_AND_BACKSLASH, "ARG_2"), Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_2")), new CMD(false, ECHO_VBS_ARGS, Arrays.asList(SPACE_AND_BACKSLASH, "ARG_3"), Arrays.asList(SPACE_AND_BACKSLASH + BACKSLASH, "ARG_3")), // allowAmbiguousCommands = false, WITH application supplied double-quotes around the argument // The argument has surrounding quotes so does not need further quoting. // However, for exe commands, the final backslash must not be allowed to turn the quote // into a literal instead of closing the quote. new CMD(false, ECHO_JAVA_ARGS, Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_11"), Arrays.asList(SPACE_AND_BACKSLASH, "ARG_11")), new CMD(false, ECHO_CMD_ARGS, Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_12"), Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_12")), new CMD(false, ECHO_VBS_ARGS, Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_13"), Arrays.asList(SPACE_AND_BACKSLASH + BACKSLASH, "ARG_13")), // Legacy mode tests; allowAmbiguousCommands = true; no application supplied quotes // The space in the argument requires it to be quoted, the final backslash // must not be allowed to turn the quote that is added into a literal // instead of closing the quote. new CMD(true, ECHO_JAVA_ARGS, Arrays.asList(SPACE_AND_BACKSLASH, "ARG_21"), Arrays.asList(SPACE_AND_BACKSLASH, "ARG_21")), new CMD(true, ECHO_CMD_ARGS, Arrays.asList(SPACE_AND_BACKSLASH, "ARG_22"), Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + BACKSLASH + DOUBLE_QUOTE, "ARG_22")), new CMD(true, ECHO_VBS_ARGS, Arrays.asList(SPACE_AND_BACKSLASH, "ARG_23"), Arrays.asList(SPACE_AND_BACKSLASH + BACKSLASH, "ARG_23")), // allowAmbiguousCommands = true, WITH application supplied double-quotes around the argument // The argument has surrounding quotes so does not need further quoting. // The backslash before the final quote is ignored and is interpreted differently for each command. new CMD(true, ECHO_JAVA_ARGS, Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_31"), Arrays.asList("SPACE AND BACKSLASH\" ARG_31")), new CMD(true, ECHO_CMD_ARGS, Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_32"), Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_32")), new CMD(true, ECHO_VBS_ARGS, Arrays.asList(DOUBLE_QUOTE + SPACE_AND_BACKSLASH + DOUBLE_QUOTE, "ARG_33"), Arrays.asList(SPACE_AND_BACKSLASH, "ARG_33")) ); /** * Common function to Invoke a process with the commands and check the result. * * @param cmd a CMD test case with arguments, allowAmbiguousCommands mode, and expected output */ private static void testCommand(CMD cmd) throws Exception { System.setProperty(AMBIGUOUS_PROP_NAME, Boolean.toString(cmd.allowAmbiguous)); List actual = null; List arguments = new ArrayList<>(cmd.command); arguments.addAll(cmd.arguments); try { // Launch the process and wait for termination ProcessBuilder pb = new ProcessBuilder(arguments); Process process = pb.start(); try (InputStream is = process.getInputStream()) { String str = readAllBytesAsString(is); str = str.replace("\r", ""); actual = Arrays.asList(str.split("\n")); } catch (IOException ioe) { throw new RuntimeException(ioe.getMessage(), ioe); } int exitCode = process.waitFor(); if (exitCode != 0) { actual = new ArrayList(actual); actual.add("Exit code: " + exitCode); } } catch (IOException ioe) { actual = Arrays.asList(ioe.getMessage().replace(arguments.get(0), "CMD")); } catch (Exception ex) { actual = Arrays.asList(ex.getMessage()); // Use exception message as output } if (!Objects.equals(actual, cmd.expected)) { System.out.println("Invoking(" + cmd.allowAmbiguous + "): " + arguments); if (actual.size() != cmd.expected.size()) { System.out.println("Args Length: actual: " + actual.size() + " expected: " + cmd.expected.size()); } System.out.println("Actual: " + actual); System.out.println("Expected: " + cmd.expected); System.out.println(); throw new RuntimeException("Unexpected output"); } } /** * Private method to readAllBytes as a String. * (InputStream.readAllBytes is not supported by the JDK until 9) * @param is an InputStream * @return a String with the contents * @throws IOException if an error occurs */ private static String readAllBytesAsString(InputStream is) throws IOException { final int BUF_SIZE = 8192; byte[] bytes = new byte[BUF_SIZE]; int off = 0; int len; while ((len = is.read(bytes, off, bytes.length - off)) > 0) { off += len; if (off >= bytes.length) { // no space in buffer, reallocate larger bytes = Arrays.copyOf(bytes, bytes.length + BUF_SIZE); } } return new String(bytes, 0, off, Charset.defaultCharset()); } /** * Initialize .cmd and .vbs scripts. * * @throws Error if an exception occurs */ private static void createFiles() throws IOException { Files.write(Paths.get(ECHO_CMD_PATH), EchoArgumentsCmd.getBytes(StandardCharsets.UTF_8)); Files.write(Paths.get(ECHO_VBS_PATH), EchoArgumentsVbs.getBytes(StandardCharsets.UTF_8)); } /** * Self contained .cmd to echo each argument on a separate line. */ static final String EchoArgumentsCmd = "@echo off\n" + "set p1=\n" + "set p2=\n" + "\n" + "if not [%1]==[] set p1=%1\n" + "if not [%2]==[] set p2=%2\n" + "if not [%3]==[] set p3=%3\n" + "if defined p1 echo %p1%\n" + "if defined p2 echo %p2%\n" + "if defined p3 echo %p3%\n" + "exit /b 0\n"; /** * Self contained .vbs to echo each argument on a separate line. */ static final String EchoArgumentsVbs = "Option Explicit\n" + "Dim arg\n" + "for each arg in WScript.Arguments\n" + " WScript.StdOut.WriteLine(arg)\n" + "Next\n"; }