1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /**
  25  * @test
  26  * @bug 8027634 8210810
  27  * @summary Verify syntax of argument file
  28  * @build TestHelper
  29  * @run main ArgFileSyntax
  30  */
  31 import java.io.File;
  32 import java.io.IOException;
  33 import java.util.ArrayList;
  34 import java.util.Arrays;
  35 import java.util.Collections;
  36 import java.util.HashMap;
  37 import java.util.List;
  38 import java.util.Map;
  39 import java.util.regex.Matcher;
  40 import java.util.regex.Pattern;
  41 
  42 public class ArgFileSyntax extends TestHelper {
  43     // Buffer size in args.c readArgFile() method
  44     private static final int ARG_FILE_PARSER_BUF_SIZE = 4096;
  45 
  46     private File createArgFile(List<String> lines) throws IOException {
  47         File argFile = new File("argfile");
  48         argFile.delete();
  49         createAFile(argFile, lines);
  50         return argFile;
  51     }
  52 
  53     private void verifyOutput(List<String> args, TestResult tr) {
  54         if (args.isEmpty()) {
  55             return;
  56         }
  57 
  58         int i = 1;
  59         for (String x : args) {
  60             tr.matches(".*argv\\[" + i + "\\] = " + Pattern.quote(x) + ".*");
  61             i++;
  62         }
  63         if (! tr.testStatus) {
  64             System.out.println(tr);
  65             throw new RuntimeException("test fails");
  66         }
  67     }
  68 
  69     // arg file content,  expected options
  70     static String[] testCases[][] = {
  71         { // empty file
  72             {}, {}
  73         },
  74         { // comments and # inside quote
  75             { "# a couple of -X flags",
  76               "-Xmx32m",
  77               "-XshowSettings #inline comment",
  78               "-Dpound.in.quote=\"This property contains #.\"",
  79               "# add -version",
  80               "-version",
  81               "# trail comment"
  82             },
  83             { "-Xmx32m",
  84               "-XshowSettings",
  85               "-Dpound.in.quote=This property contains #.",
  86               "-version"
  87             }
  88         },
  89         { // open quote with continuation directive
  90           // multiple options in a line
  91             { "-cp \"c:\\\\java lib\\\\all;\\",
  92               "     c:\\\\lib\"",
  93               "-Xmx32m -XshowSettings",
  94               "-version"
  95             },
  96             { "-cp",
  97               "c:\\java lib\\all;c:\\lib",
  98               "-Xmx32m",
  99               "-XshowSettings",
 100               "-version"
 101             }
 102         },
 103         { // no continuation on open quote
 104           // multiple lines in a property
 105             { "-cp \"c:\\\\open quote\\\\all;",
 106               "     # c:\\\\lib\"",
 107               "-Dmultiple.lines=\"line 1\\nline 2\\n\\rline 3\"",
 108               "-Dopen.quote=\"Open quote to EOL",
 109               "-Dcontinue.with.leadingWS=\"Continue with\\",
 110               "  \\ leading WS.",
 111               "-Dcontinue.without.leadingWS=\"Continue without \\",
 112               "   leading WS.",
 113               "-Descape.seq=\"escaped chars: \\\"\\a\\b\\c\\f\\t\\v\\9\\6\\23\\82\\28\\377\\477\\278\\287\\n\"",
 114               "-version"
 115             },
 116             { "-cp",
 117               "c:\\open quote\\all;",
 118               "-Dmultiple.lines=line 1",
 119               // line 2 and line 3 shoule be in output, but not as arg[x]=
 120               "-Dopen.quote=Open quote to EOL",
 121               "-Dcontinue.with.leadingWS=Continue with leading WS.",
 122               "-Dcontinue.without.leadingWS=Continue without leading WS.",
 123               // cannot verify \n and \r as that break output lines
 124               "-Descape.seq=escaped chars: \"abc\f\tv96238228377477278287",
 125               "-version"
 126             }
 127         },
 128         { // No need to escape if not in quote
 129           // also quote part of a token
 130             { "-cp c:\\\"partial quote\"\\all",
 131               "-Xmx32m -XshowSettings",
 132               "-version"
 133             },
 134             { "-cp",
 135               "c:\\partial quote\\all",
 136               "-Xmx32m",
 137               "-XshowSettings",
 138               "-version"
 139             }
 140         },
 141         { // No recursive expansion
 142             { "-Xmx32m",
 143               "-cp",
 144               " # @cpfile should remains @cpfile",
 145               "@cpfile",
 146               "-version"
 147             },
 148             { "-Xmx32m",
 149               "-cp",
 150               "@cpfile",
 151               "-version"
 152             }
 153         },
 154         { // Mix quotation
 155             { "-Dsingle.in.double=\"Mix 'single' in double\"",
 156               "-Ddouble.in.single='Mix \"double\" in single'",
 157               "-Dsingle.in.single='Escape \\\'single\\\' in single'",
 158               "-Ddouble.in.double=\"Escape \\\"double\\\" in double\""
 159             },
 160             { "-Dsingle.in.double=Mix 'single' in double",
 161               "-Ddouble.in.single=Mix \"double\" in single",
 162               "-Dsingle.in.single=Escape 'single' in single",
 163               "-Ddouble.in.double=Escape \"double\" in double"
 164             },
 165         },
 166         { // \t\f as whitespace and in escape
 167             { "-Xmx32m\t-Xint\f-version",
 168               "-Dcontinue.with.leadingws=\"Line1\\",
 169               " \t\fcontinue with \\f<ff> and \\t<tab>"
 170             },
 171             { "-Xmx32m",
 172               "-Xint",
 173               "-version",
 174               "-Dcontinue.with.leadingws=Line1continue with \f<ff> and \t<tab>"
 175             }
 176         }
 177     };
 178 
 179     public List<List<List<String>>> loadCases() {
 180         List<List<List<String>>> rv = new ArrayList<>();
 181         for (String[][] testCaseArray: testCases) {
 182             List<List<String>> testCase = new ArrayList<>(2);
 183             testCase.add(Arrays.asList(testCaseArray[0]));
 184             testCase.add(Arrays.asList(testCaseArray[1]));
 185             rv.add(testCase);
 186         }
 187 
 188         // long lines
 189         String bag = "-Dgarbage=";
 190         String ver = "-version";
 191         // a token 8192 long
 192         char[] data = new char[2*ARG_FILE_PARSER_BUF_SIZE - bag.length()];
 193         Arrays.fill(data, 'O');
 194         List<String> scratch = new ArrayList<>();
 195         scratch.add("-Xmx32m");
 196         scratch.add(bag + String.valueOf(data));
 197         scratch.add(ver);
 198         rv.add(Collections.nCopies(2, scratch));
 199 
 200         data = new char[2*ARG_FILE_PARSER_BUF_SIZE + 1024];
 201         Arrays.fill(data, 'O');
 202         scratch = new ArrayList<>();
 203         scratch.add(bag + String.valueOf(data));
 204         scratch.add(ver);
 205         rv.add(Collections.nCopies(2, scratch));
 206 
 207         // 8210810: position escaping character at boundary
 208         // reserve space for quote and backslash
 209         data = new char[ARG_FILE_PARSER_BUF_SIZE - bag.length() - 2];
 210         Arrays.fill(data, 'O');
 211         scratch = new ArrayList<>();
 212         String filling = String.valueOf(data);
 213         scratch.add(bag + "'" + filling + "\\\\aaa\\\\'");
 214         scratch.add(ver);
 215         rv.add(List.of(scratch, List.of(bag + filling + "\\aaa\\", ver)));
 216 
 217         return rv;
 218     }
 219 
 220     // ensure the arguments in the file are read in correctly
 221     private void verifyParsing(List<String> lines, List<String> args) throws IOException {
 222         File argFile = createArgFile(lines);
 223         String fname = "@" + argFile.getName();
 224         Map<String, String> env = new HashMap<>();
 225         env.put(JLDEBUG_KEY, "true");
 226 
 227         TestResult tr;
 228         if (args.contains("-version")) {
 229             tr = doExec(env, javaCmd, fname);
 230         } else {
 231             tr = doExec(env, javaCmd, fname, "-version");
 232         }
 233         tr.checkPositive();
 234         verifyOutput(args, tr);
 235 
 236         String lastArg = args.contains("-version") ? "-Dlast.arg" : "-version";
 237         tr = doExec(env, javaCmd, "-Xint", fname, lastArg);
 238         List<String> scratch = new ArrayList<>();
 239         scratch.add("-Xint");
 240         scratch.addAll(args);
 241         scratch.add(lastArg);
 242         verifyOutput(scratch, tr);
 243 
 244         argFile.delete();
 245     }
 246 
 247     @Test
 248     public void testSyntax() throws IOException {
 249         List<List<List<String>>> allcases = loadCases();
 250         for (List<List<String>> test: allcases) {
 251             verifyParsing(test.get(0), test.get(1));
 252         }
 253     }
 254 
 255     @Test
 256     public void badCases() throws IOException {
 257         List<String> lines = Arrays.asList(
 258             "-Dno.escape=\"Forgot to escape backslash\\\" -version");
 259         File argFile = createArgFile(lines);
 260         String fname = "@" + argFile.getName();
 261         Map<String, String> env = new HashMap<>();
 262         env.put(JLDEBUG_KEY, "true");
 263 
 264         TestResult tr = doExec(env, javaCmd, fname);
 265         tr.contains("argv[1] = -Dno.escape=Forgot to escape backslash\" -version");
 266         tr.checkNegative();
 267         if (!tr.testStatus) {
 268             System.out.println(tr);
 269             throw new RuntimeException("test fails");
 270         }
 271         argFile.delete();
 272     }
 273 
 274     public static void main(String... args) throws Exception {
 275         ArgFileSyntax a = new ArgFileSyntax();
 276         a.run(args);
 277         if (testExitValue > 0) {
 278             System.out.println("Total of " + testExitValue + " failed");
 279             System.exit(1);
 280         } else {
 281             System.out.println("All tests pass");
 282         }
 283     }
 284 }