1 /*
   2  * Copyright (c) 2015, 2017, 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 import java.io.*;
  25 import java.lang.reflect.Method;
  26 import java.nio.file.Files;
  27 import java.nio.file.Path;
  28 import java.nio.file.Paths;
  29 import java.util.ArrayList;
  30 import java.util.Arrays;
  31 import java.util.List;
  32 import java.util.function.Consumer;
  33 import java.util.jar.JarEntry;
  34 import java.util.jar.JarInputStream;
  35 import java.util.jar.JarOutputStream;
  36 import java.util.stream.Stream;
  37 
  38 import jdk.test.lib.util.FileUtils;
  39 import jdk.testlibrary.JDKToolFinder;
  40 import org.testng.annotations.BeforeTest;
  41 import org.testng.annotations.Test;
  42 
  43 import static java.lang.String.format;
  44 import static java.lang.System.out;
  45 import static java.nio.charset.StandardCharsets.UTF_8;
  46 import static org.testng.Assert.assertFalse;
  47 import static org.testng.Assert.assertTrue;
  48 
  49 /*
  50  * @test
  51  * @bug 8170952
  52  * @library /lib/testlibrary /test/lib
  53  * @build jdk.test.lib.util.FileUtils
  54  *        jdk.test.lib.Utils
  55  *        jdk.test.lib.Asserts
  56  *        jdk.test.lib.JDKToolFinder
  57  *        jdk.test.lib.JDKToolLauncher
  58  *        jdk.test.lib.Platform
  59  *        jdk.test.lib.process.*
  60  *        jdk.testlibrary.JDKToolFinder
  61  * @run testng CLICompatibility
  62  * @summary Basic test for compatibility of CLI options
  63  */
  64 
  65 public class CLICompatibility {
  66     static final Path TEST_CLASSES = Paths.get(System.getProperty("test.classes", "."));
  67     static final Path USER_DIR = Paths.get(System.getProperty("user.dir"));
  68 
  69     static final String TOOL_VM_OPTIONS = System.getProperty("test.tool.vm.opts", "");
  70 
  71     final boolean legacyOnly;  // for running on older JDK's ( test validation )
  72 
  73     // Resources we know to exist, that can be used for creating jar files.
  74     static final String RES1 = "CLICompatibility.class";
  75     static final String RES2 = "CLICompatibility$Result.class";
  76 
  77     @BeforeTest
  78     public void setupResourcesForJar() throws Exception {
  79         // Copy the files that we are going to use for creating/updating test
  80         // jar files, so that they can be referred to without '-C dir'
  81         Files.copy(TEST_CLASSES.resolve(RES1), USER_DIR.resolve(RES1));
  82         Files.copy(TEST_CLASSES.resolve(RES2), USER_DIR.resolve(RES2));
  83     }
  84 
  85     static final IOConsumer<InputStream> ASSERT_CONTAINS_RES1 = in -> {
  86         try (JarInputStream jin = new JarInputStream(in)) {
  87             assertTrue(jarContains(jin, RES1), "Failed to find " + RES1);
  88         }
  89     };
  90     static final IOConsumer<InputStream> ASSERT_CONTAINS_RES2 = in -> {
  91         try (JarInputStream jin = new JarInputStream(in)) {
  92             assertTrue(jarContains(jin, RES2), "Failed to find " + RES2);
  93         }
  94     };
  95     static final IOConsumer<InputStream> ASSERT_CONTAINS_MAINFEST = in -> {
  96         try (JarInputStream jin = new JarInputStream(in)) {
  97             assertTrue(jin.getManifest() != null, "No META-INF/MANIFEST.MF");
  98         }
  99     };
 100     static final IOConsumer<InputStream> ASSERT_DOES_NOT_CONTAIN_MAINFEST = in -> {
 101         try (JarInputStream jin = new JarInputStream(in)) {
 102             assertTrue(jin.getManifest() == null, "Found unexpected META-INF/MANIFEST.MF");
 103         }
 104     };
 105 
 106     static final FailCheckerWithMessage FAIL_TOO_MANY_MAIN_OPS =
 107         new FailCheckerWithMessage("You may not specify more than one '-cuxtid' options",
 108         /* legacy */ "{ctxui}[vfmn0Me] [jar-file] [manifest-file] [entry-point] [-C dir] files");
 109 
 110     // Create
 111 
 112     @Test
 113     public void createBadArgs() {
 114         final FailCheckerWithMessage FAIL_CREATE_NO_ARGS = new FailCheckerWithMessage(
 115                 "'c' flag requires manifest or input files to be specified!");
 116 
 117         jar("c")
 118             .assertFailure()
 119             .resultChecker(FAIL_CREATE_NO_ARGS);
 120 
 121         jar("-c")
 122             .assertFailure()
 123             .resultChecker(FAIL_CREATE_NO_ARGS);
 124 
 125         if (!legacyOnly)
 126             jar("--create")
 127                 .assertFailure()
 128                 .resultChecker(FAIL_CREATE_NO_ARGS);
 129 
 130         jar("ct")
 131             .assertFailure()
 132             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 133 
 134         jar("-ct")
 135             .assertFailure()
 136             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 137 
 138         if (!legacyOnly)
 139             jar("--create --list")
 140                 .assertFailure()
 141                 .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 142     }
 143 
 144     @Test
 145     public void createWriteToFile() throws IOException {
 146         Path path = Paths.get("createJarFile.jar");  // for creating
 147         String jn = path.toString();
 148         for (String opts : new String[]{"cf " + jn, "-cf " + jn, "--create --file=" + jn}) {
 149             if (legacyOnly && opts.startsWith("--"))
 150                 continue;
 151 
 152             jar(opts, RES1)
 153                 .assertSuccess()
 154                 .resultChecker(r -> {
 155                     ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path));
 156                     ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path));
 157                 });
 158         }
 159         FileUtils.deleteFileIfExistsWithRetry(path);
 160     }
 161 
 162     @Test
 163     public void createWriteToStdout() throws IOException {
 164         for (String opts : new String[]{"c", "-c", "--create"}) {
 165             if (legacyOnly && opts.startsWith("--"))
 166                 continue;
 167 
 168             jar(opts, RES1)
 169                 .assertSuccess()
 170                 .resultChecker(r -> {
 171                     ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
 172                     ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream());
 173                 });
 174         }
 175     }
 176 
 177     @Test
 178     public void createWriteToStdoutNoManifest() throws IOException {
 179         for (String opts : new String[]{"cM", "-cM", "--create --no-manifest"} ){
 180             if (legacyOnly && opts.startsWith("--"))
 181                 continue;
 182 
 183             jar(opts, RES1)
 184                 .assertSuccess()
 185                 .resultChecker(r -> {
 186                     ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
 187                     ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream());
 188                 });
 189         }
 190     }
 191 
 192     // Update
 193 
 194     @Test
 195     public void updateBadArgs() {
 196         final FailCheckerWithMessage FAIL_UPDATE_NO_ARGS = new FailCheckerWithMessage(
 197                 "'u' flag requires manifest, 'e' flag or input files to be specified!");
 198 
 199         jar("u")
 200             .assertFailure()
 201             .resultChecker(FAIL_UPDATE_NO_ARGS);
 202 
 203         jar("-u")
 204             .assertFailure()
 205             .resultChecker(FAIL_UPDATE_NO_ARGS);
 206 
 207         if (!legacyOnly)
 208             jar("--update")
 209                 .assertFailure()
 210                 .resultChecker(FAIL_UPDATE_NO_ARGS);
 211 
 212         jar("ut")
 213             .assertFailure()
 214             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 215 
 216         jar("-ut")
 217             .assertFailure()
 218             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 219 
 220         if (!legacyOnly)
 221             jar("--update --list")
 222                 .assertFailure()
 223                 .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 224     }
 225 
 226     @Test
 227     public void updateReadFileWriteFile() throws IOException {
 228         Path path = Paths.get("updateReadWriteStdout.jar");  // for updating
 229         String jn = path.toString();
 230 
 231         for (String opts : new String[]{"uf " + jn, "-uf " + jn, "--update --file=" + jn}) {
 232             if (legacyOnly && opts.startsWith("--"))
 233                 continue;
 234 
 235             createJar(path, RES1);
 236             jar(opts, RES2)
 237                 .assertSuccess()
 238                 .resultChecker(r -> {
 239                     ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path));
 240                     ASSERT_CONTAINS_RES2.accept(Files.newInputStream(path));
 241                     ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path));
 242                 });
 243         }
 244         FileUtils.deleteFileIfExistsWithRetry(path);
 245     }
 246 
 247     @Test
 248     public void updateReadStdinWriteStdout() throws IOException {
 249         Path path = Paths.get("updateReadStdinWriteStdout.jar");
 250 
 251         for (String opts : new String[]{"u", "-u", "--update"}) {
 252             if (legacyOnly && opts.startsWith("--"))
 253                 continue;
 254 
 255             createJar(path, RES1);
 256             jarWithStdin(path.toFile(), opts, RES2)
 257                 .assertSuccess()
 258                 .resultChecker(r -> {
 259                     ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
 260                     ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream());
 261                     ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream());
 262                 });
 263         }
 264         FileUtils.deleteFileIfExistsWithRetry(path);
 265     }
 266 
 267     @Test
 268     public void updateReadStdinWriteStdoutNoManifest() throws IOException {
 269         Path path = Paths.get("updateReadStdinWriteStdoutNoManifest.jar");
 270 
 271         for (String opts : new String[]{"uM", "-uM", "--update --no-manifest"} ){
 272             if (legacyOnly && opts.startsWith("--"))
 273                 continue;
 274 
 275             createJar(path, RES1);
 276             jarWithStdin(path.toFile(), opts, RES2)
 277                 .assertSuccess()
 278                 .resultChecker(r -> {
 279                     ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
 280                     ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream());
 281                     ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream());
 282                 });
 283         }
 284         FileUtils.deleteFileIfExistsWithRetry(path);
 285     }
 286 
 287     // List
 288 
 289     @Test
 290     public void listBadArgs() {
 291         jar("tx")
 292             .assertFailure()
 293             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 294 
 295         jar("-tx")
 296             .assertFailure()
 297             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 298 
 299         if (!legacyOnly)
 300             jar("--list --extract")
 301                 .assertFailure()
 302                 .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 303     }
 304 
 305     @Test
 306     public void listReadFromFileWriteToStdout() throws IOException {
 307         Path path = Paths.get("listReadFromFileWriteToStdout.jar");  // for listing
 308         createJar(path, RES1);
 309         String jn = path.toString();
 310 
 311         for (String opts : new String[]{"tf " + jn, "-tf " + jn, "--list --file " + jn}) {
 312             if (legacyOnly && opts.startsWith("--"))
 313                 continue;
 314 
 315             jar(opts)
 316                 .assertSuccess()
 317                 .resultChecker(r ->
 318                     assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1),
 319                                "Failed, got [" + r.output + "]")
 320                 );
 321         }
 322         FileUtils.deleteFileIfExistsWithRetry(path);
 323     }
 324 
 325     @Test
 326     public void listReadFromStdinWriteToStdout() throws IOException {
 327         Path path = Paths.get("listReadFromStdinWriteToStdout.jar");
 328         createJar(path, RES1);
 329 
 330         for (String opts : new String[]{"t", "-t", "--list"} ){
 331             if (legacyOnly && opts.startsWith("--"))
 332                 continue;
 333 
 334             jarWithStdin(path.toFile(), opts)
 335                 .assertSuccess()
 336                 .resultChecker(r ->
 337                     assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1),
 338                                "Failed, got [" + r.output + "]")
 339                 );
 340         }
 341         FileUtils.deleteFileIfExistsWithRetry(path);
 342     }
 343 
 344     // Extract
 345 
 346     @Test
 347     public void extractBadArgs() {
 348         jar("xi")
 349             .assertFailure()
 350             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 351 
 352         jar("-xi")
 353             .assertFailure()
 354             .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 355 
 356         if (!legacyOnly) {
 357             jar("--extract --generate-index")
 358                 .assertFailure()
 359                 .resultChecker(new FailCheckerWithMessage(
 360                                    "option --generate-index requires an argument"));
 361 
 362             jar("--extract --generate-index=foo")
 363                 .assertFailure()
 364                 .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
 365         }
 366     }
 367 
 368     @Test
 369     public void extractReadFromStdin() throws IOException {
 370         Path path = Paths.get("extract");
 371         Path jarPath = path.resolve("extractReadFromStdin.jar"); // for extracting
 372         createJar(jarPath, RES1);
 373 
 374         for (String opts : new String[]{"x" ,"-x", "--extract"}) {
 375             if (legacyOnly && opts.startsWith("--"))
 376                 continue;
 377 
 378             jarWithStdinAndWorkingDir(jarPath.toFile(), path.toFile(), opts)
 379                 .assertSuccess()
 380                 .resultChecker(r ->
 381                     assertTrue(Files.exists(path.resolve(RES1)),
 382                                "Expected to find:" + path.resolve(RES1))
 383                 );
 384             FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1));
 385         }
 386         FileUtils.deleteFileTreeWithRetry(path);
 387     }
 388 
 389     @Test
 390     public void extractReadFromFile() throws IOException {
 391         Path path = Paths.get("extract");
 392         String jn = "extractReadFromFile.jar";
 393         Path jarPath = path.resolve(jn);
 394         createJar(jarPath, RES1);
 395 
 396         for (String opts : new String[]{"xf "+jn ,"-xf "+jn, "--extract --file "+jn}) {
 397             if (legacyOnly && opts.startsWith("--"))
 398                 continue;
 399 
 400             jarWithStdinAndWorkingDir(null, path.toFile(), opts)
 401                 .assertSuccess()
 402                 .resultChecker(r ->
 403                     assertTrue(Files.exists(path.resolve(RES1)),
 404                                "Expected to find:" + path.resolve(RES1))
 405                 );
 406             FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1));
 407         }
 408         FileUtils.deleteFileTreeWithRetry(path);
 409     }
 410 
 411     // Basic help
 412 
 413     @Test
 414     public void helpBadOptionalArg() {
 415         if (legacyOnly)
 416             return;
 417 
 418         jar("--help:")
 419             .assertFailure();
 420 
 421         jar("--help:blah")
 422             .assertFailure();
 423     }
 424 
 425     @Test
 426     public void help() {
 427         if (legacyOnly)
 428             return;
 429 
 430         jar("-h")
 431             .assertSuccess()
 432             .resultChecker(r ->
 433                 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
 434                            "Failed, got [" + r.output + "]")
 435             );
 436 
 437         jar("--help")
 438             .assertSuccess()
 439             .resultChecker(r -> {
 440                 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
 441                            "Failed, got [" + r.output + "]");
 442                 assertFalse(r.output.contains("--do-not-resolve-by-default"));
 443                 assertFalse(r.output.contains("--warn-if-resolved"));
 444             });
 445 
 446         jar("--help:compat")
 447             .assertSuccess()
 448             .resultChecker(r ->
 449                 assertTrue(r.output.startsWith("Compatibility Interface:"),
 450                            "Failed, got [" + r.output + "]")
 451             );
 452 
 453         jar("--help-extra")
 454             .assertSuccess()
 455             .resultChecker(r -> {
 456                 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
 457                            "Failed, got [" + r.output + "]");
 458                 assertTrue(r.output.contains("--do-not-resolve-by-default"));
 459                 assertTrue(r.output.contains("--warn-if-resolved"));
 460             });
 461     }
 462 
 463     // -- Infrastructure
 464 
 465     static boolean jarContains(JarInputStream jis, String entryName)
 466         throws IOException
 467     {
 468         JarEntry e;
 469         boolean found = false;
 470         while((e = jis.getNextJarEntry()) != null) {
 471             if (e.getName().equals(entryName))
 472                 return true;
 473         }
 474         return false;
 475     }
 476 
 477     /* Creates a simple jar with entries of size 0, good enough for testing */
 478     static void createJar(Path path, String... entries) throws IOException {
 479         FileUtils.deleteFileIfExistsWithRetry(path);
 480         Path parent = path.getParent();
 481         if (parent != null)
 482             Files.createDirectories(parent);
 483         try (OutputStream out = Files.newOutputStream(path);
 484              JarOutputStream jos = new JarOutputStream(out)) {
 485             JarEntry je = new JarEntry("META-INF/MANIFEST.MF");
 486             jos.putNextEntry(je);
 487             jos.closeEntry();
 488 
 489             for (String entry : entries) {
 490                 je = new JarEntry(entry);
 491                 jos.putNextEntry(je);
 492                 jos.closeEntry();
 493             }
 494         }
 495     }
 496 
 497     static class FailCheckerWithMessage implements Consumer<Result> {
 498         final String[] messages;
 499         FailCheckerWithMessage(String... m) {
 500             messages = m;
 501         }
 502         @Override
 503         public void accept(Result r) {
 504             //out.printf("%s%n", r.output);
 505             boolean found = false;
 506             for (String m : messages) {
 507                 if (r.output.contains(m)) {
 508                     found = true;
 509                     break;
 510                 }
 511             }
 512             assertTrue(found,
 513                        "Excepted out to contain one of: " + Arrays.asList(messages)
 514                            + " but got: " + r.output);
 515         }
 516     }
 517 
 518     static Result jar(String... args) {
 519         return jarWithStdinAndWorkingDir(null, null, args);
 520     }
 521 
 522     static Result jarWithStdin(File stdinSource, String... args) {
 523         return jarWithStdinAndWorkingDir(stdinSource, null, args);
 524     }
 525 
 526     static Result jarWithStdinAndWorkingDir(File stdinFrom,
 527                                             File workingDir,
 528                                             String... args) {
 529         String jar = getJDKTool("jar");
 530         List<String> commands = new ArrayList<>();
 531         commands.add(jar);
 532         if (!TOOL_VM_OPTIONS.isEmpty()) {
 533             commands.addAll(Arrays.asList(TOOL_VM_OPTIONS.split("\\s+", -1)));
 534         }
 535         Stream.of(args).map(s -> s.split(" "))
 536                        .flatMap(Arrays::stream)
 537                        .forEach(x -> commands.add(x));
 538         ProcessBuilder p = new ProcessBuilder(commands);
 539         if (stdinFrom != null)
 540             p.redirectInput(stdinFrom);
 541         if (workingDir != null)
 542             p.directory(workingDir);
 543         return run(p);
 544     }
 545 
 546     static Result run(ProcessBuilder pb) {
 547         Process p;
 548         byte[] stdout, stderr;
 549         out.printf("Running: %s%n", pb.command());
 550         try {
 551             p = pb.start();
 552         } catch (IOException e) {
 553             throw new RuntimeException(
 554                     format("Couldn't start process '%s'", pb.command()), e);
 555         }
 556 
 557         String output;
 558         try {
 559             stdout = readAllBytes(p.getInputStream());
 560             stderr = readAllBytes(p.getErrorStream());
 561 
 562             output = toString(stdout, stderr);
 563         } catch (IOException e) {
 564             throw new RuntimeException(
 565                     format("Couldn't read process output '%s'", pb.command()), e);
 566         }
 567 
 568         try {
 569             p.waitFor();
 570         } catch (InterruptedException e) {
 571             throw new RuntimeException(
 572                     format("Process hasn't finished '%s'", pb.command()), e);
 573         }
 574         return new Result(p.exitValue(), stdout, stderr, output);
 575     }
 576 
 577     static final Path JAVA_HOME = Paths.get(System.getProperty("java.home"));
 578 
 579     static String getJDKTool(String name) {
 580         try {
 581             return JDKToolFinder.getJDKTool(name);
 582         } catch (Exception x) {
 583             Path j = JAVA_HOME.resolve("bin").resolve(name);
 584             if (Files.exists(j))
 585                 return j.toString();
 586             j = JAVA_HOME.resolve("..").resolve("bin").resolve(name);
 587             if (Files.exists(j))
 588                 return j.toString();
 589             throw new RuntimeException(x);
 590         }
 591     }
 592 
 593     static String toString(byte[] ba1, byte[] ba2) {
 594         return (new String(ba1, UTF_8)).concat(new String(ba2, UTF_8));
 595     }
 596 
 597     static class Result {
 598         final int exitValue;
 599         final byte[] stdout;
 600         final byte[] stderr;
 601         final String output;
 602 
 603         private Result(int exitValue, byte[] stdout, byte[] stderr, String output) {
 604             this.exitValue = exitValue;
 605             this.stdout = stdout;
 606             this.stderr = stderr;
 607             this.output = output;
 608         }
 609 
 610         InputStream stdoutAsStream() { return new ByteArrayInputStream(stdout); }
 611 
 612         Result assertSuccess() { assertTrue(exitValue == 0, output); return this; }
 613         Result assertFailure() { assertTrue(exitValue != 0, output); return this; }
 614 
 615         Result resultChecker(IOConsumer<Result> r) {
 616             try {  r.accept(this); return this; }
 617             catch (IOException x) { throw new UncheckedIOException(x); }
 618         }
 619 
 620         Result resultChecker(FailCheckerWithMessage c) { c.accept(this); return this; }
 621     }
 622 
 623     interface IOConsumer<T> { void accept(T t) throws IOException ;  }
 624 
 625     // readAllBytes implementation so the test can be run pre 1.9 ( legacyOnly )
 626     static byte[] readAllBytes(InputStream is) throws IOException {
 627         byte[] buf = new byte[8192];
 628         int capacity = buf.length;
 629         int nread = 0;
 630         int n;
 631         for (;;) {
 632             // read to EOF which may read more or less than initial buffer size
 633             while ((n = is.read(buf, nread, capacity - nread)) > 0)
 634                 nread += n;
 635 
 636             // if the last call to read returned -1, then we're done
 637             if (n < 0)
 638                 break;
 639 
 640             // need to allocate a larger buffer
 641             capacity = capacity << 1;
 642 
 643             buf = Arrays.copyOf(buf, capacity);
 644         }
 645         return (capacity == nread) ? buf : Arrays.copyOf(buf, nread);
 646     }
 647 
 648     // Standalone entry point for running with, possibly older, JDKs.
 649     public static void main(String[] args) throws Throwable {
 650         boolean legacyOnly = false;
 651         if (args.length != 0 && args[0].equals("legacyOnly"))
 652             legacyOnly = true;
 653 
 654         CLICompatibility test = new CLICompatibility(legacyOnly);
 655         for (Method m : CLICompatibility.class.getDeclaredMethods()) {
 656             if (m.getAnnotation(Test.class) != null) {
 657                 System.out.println("Invoking " + m.getName());
 658                 m.invoke(test);
 659             }
 660         }
 661     }
 662     CLICompatibility(boolean legacyOnly) { this.legacyOnly = legacyOnly; }
 663     CLICompatibility() { this.legacyOnly = false; }
 664 }