1 /*
   2  * Copyright (c) 2003, 2018, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.java.util.jar.pack;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.BufferedOutputStream;
  30 import java.io.File;
  31 import java.io.FileInputStream;
  32 import java.io.FileOutputStream;
  33 import java.io.IOException;
  34 import java.io.InputStream;
  35 import java.io.OutputStream;
  36 import java.io.PrintStream;
  37 import java.text.MessageFormat;
  38 import java.nio.file.Files;
  39 import java.nio.file.Path;
  40 import java.util.ArrayList;
  41 import java.util.Arrays;
  42 import java.util.HashMap;
  43 import java.util.Iterator;
  44 import java.util.List;
  45 import java.util.ListIterator;
  46 import java.util.Map;
  47 import java.util.Properties;
  48 import java.util.ResourceBundle;
  49 import java.util.SortedMap;
  50 import java.util.TreeMap;
  51 import java.util.jar.JarFile;
  52 import java.util.jar.JarOutputStream;
  53 import java.util.jar.Pack200;
  54 import java.util.zip.GZIPInputStream;
  55 import java.util.zip.GZIPOutputStream;
  56 
  57 /** Command line interface for Pack200.
  58  */
  59 
  60 @SuppressWarnings({"removal"})
  61 class Driver {
  62     private static final ResourceBundle RESOURCE =
  63         ResourceBundle.getBundle("com.sun.java.util.jar.pack.DriverResource");
  64     private static boolean suppressDeprecateMsg = false;
  65 
  66     public static void main(String[] ava) throws IOException {
  67         List<String> av = new ArrayList<>(Arrays.asList(ava));
  68 
  69         boolean doPack   = true;
  70         boolean doUnpack = false;
  71         boolean doRepack = false;
  72         boolean doZip = true;
  73         suppressDeprecateMsg = av.remove("-XDsuppress-tool-removal-message");
  74         String logFile = null;
  75         String verboseProp = Utils.DEBUG_VERBOSE;
  76 
  77         {
  78             // Non-standard, undocumented "--unpack" switch enables unpack mode.
  79             String arg0 = av.isEmpty() ? "" : av.get(0);
  80             switch (arg0) {
  81                 case "--pack":
  82                 av.remove(0);
  83                     break;
  84                 case "--unpack":
  85                 av.remove(0);
  86                 doPack = false;
  87                 doUnpack = true;
  88                     break;
  89             }
  90         }
  91 
  92         if (!suppressDeprecateMsg) {
  93             printDeprecateWarning(doPack, System.out);
  94         }
  95 
  96         // Collect engine properties here:
  97         Map<String,String> engProps = new HashMap<>();
  98         engProps.put(verboseProp, System.getProperty(verboseProp));
  99 
 100         String optionMap;
 101         String[] propTable;
 102         if (doPack) {
 103             optionMap = PACK200_OPTION_MAP;
 104             propTable = PACK200_PROPERTY_TO_OPTION;
 105         } else {
 106             optionMap = UNPACK200_OPTION_MAP;
 107             propTable = UNPACK200_PROPERTY_TO_OPTION;
 108         }
 109 
 110         // Collect argument properties here:
 111         Map<String,String> avProps = new HashMap<>();
 112         try {
 113             for (;;) {
 114                 String state = parseCommandOptions(av, optionMap, avProps);
 115                 // Translate command line options to Pack200 properties:
 116             eachOpt:
 117                 for (Iterator<String> opti = avProps.keySet().iterator();
 118                      opti.hasNext(); ) {
 119                     String opt = opti.next();
 120                     String prop = null;
 121                     for (int i = 0; i < propTable.length; i += 2) {
 122                         if (opt.equals(propTable[1+i])) {
 123                             prop = propTable[0+i];
 124                             break;
 125                         }
 126                     }
 127                     if (prop != null) {
 128                         String val = avProps.get(opt);
 129                         opti.remove();  // remove opt from avProps
 130                         if (!prop.endsWith(".")) {
 131                             // Normal string or boolean.
 132                             if (!(opt.equals("--verbose")
 133                                   || opt.endsWith("="))) {
 134                                 // Normal boolean; convert to T/F.
 135                                 boolean flag = (val != null);
 136                                 if (opt.startsWith("--no-"))
 137                                     flag = !flag;
 138                                 val = flag? "true": "false";
 139                             }
 140                             engProps.put(prop, val);
 141                         } else if (prop.contains(".attribute.")) {
 142                             for (String val1 : val.split("\0")) {
 143                                 String[] val2 = val1.split("=", 2);
 144                                 engProps.put(prop+val2[0], val2[1]);
 145                             }
 146                         } else {
 147                             // Collection property: pack.pass.file.cli.NNN
 148                             int idx = 1;
 149                             for (String val1 : val.split("\0")) {
 150                                 String prop1;
 151                                 do {
 152                                     prop1 = prop+"cli."+(idx++);
 153                                 } while (engProps.containsKey(prop1));
 154                                 engProps.put(prop1, val1);
 155                             }
 156                         }
 157                     }
 158                 }
 159 
 160                 // See if there is any other action to take.
 161                 if ("--config-file=".equals(state)) {
 162                     String propFile = av.remove(0);
 163                     Properties fileProps = new Properties();
 164                     try (InputStream propIn = new FileInputStream(propFile)) {
 165                         fileProps.load(propIn);
 166                     }
 167                     if (engProps.get(verboseProp) != null)
 168                         fileProps.list(System.out);
 169                     for (Map.Entry<Object,Object> me : fileProps.entrySet()) {
 170                         engProps.put((String) me.getKey(), (String) me.getValue());
 171                     }
 172                 } else if ("--version".equals(state)) {
 173                         System.out.println(MessageFormat.format(RESOURCE.getString(DriverResource.VERSION),
 174                                                                 Driver.class.getName(), "1.31, 07/05/05"));
 175                     return;
 176                 } else if ("--help".equals(state)) {
 177                     printUsage(doPack, true, System.out);
 178                     System.exit(0);
 179                     return;
 180                 } else {
 181                     break;
 182                 }
 183             }
 184         } catch (IllegalArgumentException ee) {
 185                 System.err.println(MessageFormat.format(RESOURCE.getString(DriverResource.BAD_ARGUMENT), ee));
 186             printUsage(doPack, false, System.err);
 187             System.exit(2);
 188             return;
 189         }
 190 
 191         // Deal with remaining non-engine properties:
 192         for (String opt : avProps.keySet()) {
 193             String val = avProps.get(opt);
 194             switch (opt) {
 195                 case "--repack":
 196                     doRepack = true;
 197                     break;
 198                 case "--no-gzip":
 199                     doZip = (val == null);
 200                     break;
 201                 case "--log-file=":
 202                     logFile = val;
 203                     break;
 204                 default:
 205                     throw new InternalError(MessageFormat.format(
 206                             RESOURCE.getString(DriverResource.BAD_OPTION),
 207                             opt, avProps.get(opt)));
 208             }
 209         }
 210 
 211         if (logFile != null && !logFile.equals("")) {
 212             if (logFile.equals("-")) {
 213                 System.setErr(System.out);
 214             } else {
 215                 OutputStream log = new FileOutputStream(logFile);
 216                 //log = new BufferedOutputStream(out);
 217                 System.setErr(new PrintStream(log));
 218             }
 219         }
 220 
 221         boolean verbose = (engProps.get(verboseProp) != null);
 222 
 223         String packfile = "";
 224         if (!av.isEmpty())
 225             packfile = av.remove(0);
 226 
 227         String jarfile = "";
 228         if (!av.isEmpty())
 229             jarfile = av.remove(0);
 230 
 231         String newfile = "";  // output JAR file if --repack
 232         String bakfile = "";  // temporary backup of input JAR
 233         String tmpfile = "";  // temporary file to be deleted
 234         if (doRepack) {
 235             // The first argument is the target JAR file.
 236             // (Note:  *.pac is nonstandard, but may be necessary
 237             // if a host OS truncates file extensions.)
 238             if (packfile.toLowerCase().endsWith(".pack") ||
 239                 packfile.toLowerCase().endsWith(".pac") ||
 240                 packfile.toLowerCase().endsWith(".gz")) {
 241                 System.err.println(MessageFormat.format(
 242                         RESOURCE.getString(DriverResource.BAD_REPACK_OUTPUT),
 243                         packfile));
 244                 printUsage(doPack, false, System.err);
 245                 System.exit(2);
 246             }
 247             newfile = packfile;
 248             // The optional second argument is the source JAR file.
 249             if (jarfile.equals("")) {
 250                 // If only one file is given, it is the only JAR.
 251                 // It serves as both input and output.
 252                 jarfile = newfile;
 253             }
 254             tmpfile = createTempFile(newfile, ".pack").getPath();
 255             packfile = tmpfile;
 256             doZip = false;  // no need to zip the temporary file
 257         }
 258 
 259         if (!av.isEmpty()
 260             // Accept jarfiles ending with .jar or .zip.
 261             // Accept jarfile of "-" (stdout), but only if unpacking.
 262             || !(jarfile.toLowerCase().endsWith(".jar")
 263                  || jarfile.toLowerCase().endsWith(".zip")
 264                  || (jarfile.equals("-") && !doPack))) {
 265             printUsage(doPack, false, System.err);
 266             System.exit(2);
 267             return;
 268         }
 269 
 270         if (doRepack)
 271             doPack = doUnpack = true;
 272         else if (doPack)
 273             doUnpack = false;
 274 
 275         Pack200.Packer jpack = Pack200.newPacker();
 276         Pack200.Unpacker junpack = Pack200.newUnpacker();
 277 
 278         jpack.properties().putAll(engProps);
 279         junpack.properties().putAll(engProps);
 280         if (doRepack && newfile.equals(jarfile)) {
 281             String zipc = getZipComment(jarfile);
 282             if (verbose && zipc.length() > 0)
 283                 System.out.println(MessageFormat.format(RESOURCE.getString(DriverResource.DETECTED_ZIP_COMMENT), zipc));
 284             if (zipc.indexOf(Utils.PACK_ZIP_ARCHIVE_MARKER_COMMENT) >= 0) {
 285                     System.out.println(MessageFormat.format(RESOURCE.getString(DriverResource.SKIP_FOR_REPACKED), jarfile));
 286                         doPack = false;
 287                         doUnpack = false;
 288                         doRepack = false;
 289             }
 290         }
 291 
 292         try {
 293 
 294             if (doPack) {
 295                 // Mode = Pack.
 296                 JarFile in = new JarFile(new File(jarfile));
 297                 OutputStream out;
 298                 // Packfile must be -, *.gz, *.pack, or *.pac.
 299                 if (packfile.equals("-")) {
 300                     out = System.out;
 301                     // Send warnings, etc., to stderr instead of stdout.
 302                     System.setOut(System.err);
 303                 } else if (doZip) {
 304                     if (!packfile.endsWith(".gz")) {
 305                     System.err.println(MessageFormat.format(RESOURCE.getString(DriverResource.WRITE_PACK_FILE), packfile));
 306                         printUsage(doPack, false, System.err);
 307                         System.exit(2);
 308                     }
 309                     out = new FileOutputStream(packfile);
 310                     out = new BufferedOutputStream(out);
 311                     out = new GZIPOutputStream(out);
 312                 } else {
 313                     if (!packfile.toLowerCase().endsWith(".pack") &&
 314                             !packfile.toLowerCase().endsWith(".pac")) {
 315                         System.err.println(MessageFormat.format(RESOURCE.getString(DriverResource.WRITE_PACKGZ_FILE),packfile));
 316                         printUsage(doPack, false, System.err);
 317                         System.exit(2);
 318                     }
 319                     out = new FileOutputStream(packfile);
 320                     out = new BufferedOutputStream(out);
 321                 }
 322                 jpack.pack(in, out);
 323                 //in.close();  // p200 closes in but not out
 324                 out.close();
 325             }
 326 
 327             if (doRepack && newfile.equals(jarfile)) {
 328                 // If the source and destination are the same,
 329                 // we will move the input JAR aside while regenerating it.
 330                 // This allows us to restore it if something goes wrong.
 331                 File bakf = createTempFile(jarfile, ".bak");
 332                 // On Windows target must be deleted see 4017593
 333                 bakf.delete();
 334                 boolean okBackup = new File(jarfile).renameTo(bakf);
 335                 if (!okBackup) {
 336                         throw new Error(MessageFormat.format(RESOURCE.getString(DriverResource.SKIP_FOR_MOVE_FAILED),bakfile));
 337                 } else {
 338                     // Open jarfile recovery bracket.
 339                     bakfile = bakf.getPath();
 340                 }
 341             }
 342 
 343             if (doUnpack) {
 344                 // Mode = Unpack.
 345                 InputStream in;
 346                 if (packfile.equals("-"))
 347                     in = System.in;
 348                 else
 349                     in = new FileInputStream(new File(packfile));
 350                 BufferedInputStream inBuf = new BufferedInputStream(in);
 351                 in = inBuf;
 352                 if (Utils.isGZIPMagic(Utils.readMagic(inBuf))) {
 353                     in = new GZIPInputStream(in);
 354                 }
 355                 String outfile = newfile.equals("")? jarfile: newfile;
 356                 OutputStream fileOut;
 357                 if (outfile.equals("-"))
 358                     fileOut = System.out;
 359                 else
 360                     fileOut = new FileOutputStream(outfile);
 361                 fileOut = new BufferedOutputStream(fileOut);
 362                 try (JarOutputStream out = new JarOutputStream(fileOut)) {
 363                     junpack.unpack(in, out);
 364                     // p200 closes in but not out
 365                 }
 366                 // At this point, we have a good jarfile (or newfile, if -r)
 367             }
 368 
 369             if (!bakfile.equals("")) {
 370                         // On success, abort jarfile recovery bracket.
 371                         new File(bakfile).delete();
 372                         bakfile = "";
 373             }
 374 
 375         } finally {
 376             // Close jarfile recovery bracket.
 377             if (!bakfile.equals("")) {
 378                 File jarFile = new File(jarfile);
 379                 jarFile.delete(); // Win32 requires this, see above
 380                 new File(bakfile).renameTo(jarFile);
 381             }
 382             // In all cases, delete temporary *.pack.
 383             if (!tmpfile.equals(""))
 384                 new File(tmpfile).delete();
 385         }
 386     }
 387 
 388     private static
 389     File createTempFile(String basefile, String suffix) throws IOException {
 390         File base = new File(basefile);
 391         String prefix = base.getName();
 392         if (prefix.length() < 3)  prefix += "tmp";
 393 
 394         File where = (base.getParentFile() == null && suffix.equals(".bak"))
 395                 ? new File(".").getAbsoluteFile()
 396                 : base.getParentFile();
 397 
 398         Path tmpfile = (where == null)
 399                 ? Files.createTempFile(prefix, suffix)
 400                 : Files.createTempFile(where.toPath(), prefix, suffix);
 401 
 402         return tmpfile.toFile();
 403     }
 404 
 405     private static
 406     void printDeprecateWarning(boolean doPack, PrintStream out) {
 407         String prog = doPack ? "pack200" : "unpack200";
 408         out.println(MessageFormat.format(RESOURCE.getString(DriverResource.DEPRECATED), prog));
 409     }
 410 
 411     private static
 412     void printUsage(boolean doPack, boolean full, PrintStream out) {
 413         String prog = doPack ? "pack200" : "unpack200";
 414         String[] packUsage = (String[])RESOURCE.getObject(DriverResource.PACK_HELP);
 415         String[] unpackUsage = (String[])RESOURCE.getObject(DriverResource.UNPACK_HELP);
 416         String[] usage = doPack? packUsage: unpackUsage;
 417         for (int i = 0; i < usage.length; i++) {
 418             out.println(usage[i]);
 419             if (!full) {
 420             out.println(MessageFormat.format(RESOURCE.getString(DriverResource.MORE_INFO), prog));
 421                 break;
 422             }
 423         }
 424         // Print a warning at the end
 425         // The full help page is long, the beginning warning could be out of sight
 426         if (full && !suppressDeprecateMsg) {
 427             printDeprecateWarning(doPack, out);
 428         }
 429     }
 430 
 431     private static
 432         String getZipComment(String jarfile) throws IOException {
 433         byte[] tail = new byte[1000];
 434         long filelen = new File(jarfile).length();
 435         if (filelen <= 0)  return "";
 436         long skiplen = Math.max(0, filelen - tail.length);
 437         try (InputStream in = new FileInputStream(new File(jarfile))) {
 438             in.skip(skiplen);
 439             in.read(tail);
 440             for (int i = tail.length-4; i >= 0; i--) {
 441                 if (tail[i+0] == 'P' && tail[i+1] == 'K' &&
 442                     tail[i+2] ==  5  && tail[i+3] ==  6) {
 443                     // Skip sig4, disks4, entries4, clen4, coff4, cmt2
 444                     i += 4+4+4+4+4+2;
 445                     if (i < tail.length)
 446                         return new String(tail, i, tail.length-i, "UTF8");
 447                     return "";
 448                 }
 449             }
 450             return "";
 451         }
 452     }
 453 
 454     private static final String PACK200_OPTION_MAP =
 455         (""
 456          +"--repack                 $ \n  -r +>- @--repack              $ \n"
 457          +"--no-gzip                $ \n  -g +>- @--no-gzip             $ \n"
 458          +"--strip-debug            $ \n  -G +>- @--strip-debug         $ \n"
 459          +"--no-keep-file-order     $ \n  -O +>- @--no-keep-file-order  $ \n"
 460          +"--segment-limit=      *> = \n  -S +>  @--segment-limit=      = \n"
 461          +"--effort=             *> = \n  -E +>  @--effort=             = \n"
 462          +"--deflate-hint=       *> = \n  -H +>  @--deflate-hint=       = \n"
 463          +"--modification-time=  *> = \n  -m +>  @--modification-time=  = \n"
 464          +"--pass-file=        *> &\0 \n  -P +>  @--pass-file=        &\0 \n"
 465          +"--unknown-attribute=  *> = \n  -U +>  @--unknown-attribute=  = \n"
 466          +"--class-attribute=  *> &\0 \n  -C +>  @--class-attribute=  &\0 \n"
 467          +"--field-attribute=  *> &\0 \n  -F +>  @--field-attribute=  &\0 \n"
 468          +"--method-attribute= *> &\0 \n  -M +>  @--method-attribute= &\0 \n"
 469          +"--code-attribute=   *> &\0 \n  -D +>  @--code-attribute=   &\0 \n"
 470          +"--config-file=      *>   . \n  -f +>  @--config-file=        . \n"
 471 
 472          // Negative options as required by CLIP:
 473          +"--no-strip-debug  !--strip-debug         \n"
 474          +"--gzip            !--no-gzip             \n"
 475          +"--keep-file-order !--no-keep-file-order  \n"
 476 
 477          // Non-Standard Options
 478          +"--verbose                $ \n  -v +>- @--verbose             $ \n"
 479          +"--quiet        !--verbose  \n  -q +>- !--verbose               \n"
 480          +"--log-file=           *> = \n  -l +>  @--log-file=           = \n"
 481          //+"--java-option=      *> = \n  -J +>  @--java-option=        = \n"
 482          +"--version                . \n  -V +>  @--version             . \n"
 483          +"--help               . \n  -? +> @--help . \n  -h +> @--help . \n"
 484 
 485          // Termination:
 486          +"--           . \n"  // end option sequence here
 487          +"-   +?    >- . \n"  // report error if -XXX present; else use stdout
 488          );
 489     // Note: Collection options use "\0" as a delimiter between arguments.
 490 
 491     // For Java version of unpacker (used for testing only):
 492     private static final String UNPACK200_OPTION_MAP =
 493         (""
 494          +"--deflate-hint=       *> = \n  -H +>  @--deflate-hint=       = \n"
 495          +"--verbose                $ \n  -v +>- @--verbose             $ \n"
 496          +"--quiet        !--verbose  \n  -q +>- !--verbose               \n"
 497          +"--remove-pack-file       $ \n  -r +>- @--remove-pack-file    $ \n"
 498          +"--log-file=           *> = \n  -l +>  @--log-file=           = \n"
 499          +"--config-file=        *> . \n  -f +>  @--config-file=        . \n"
 500 
 501          // Termination:
 502          +"--           . \n"  // end option sequence here
 503          +"-   +?    >- . \n"  // report error if -XXX present; else use stdin
 504          +"--version                . \n  -V +>  @--version             . \n"
 505          +"--help               . \n  -? +> @--help . \n  -h +> @--help . \n"
 506          );
 507 
 508     private static final String[] PACK200_PROPERTY_TO_OPTION = {
 509         Pack200.Packer.SEGMENT_LIMIT, "--segment-limit=",
 510         Pack200.Packer.KEEP_FILE_ORDER, "--no-keep-file-order",
 511         Pack200.Packer.EFFORT, "--effort=",
 512         Pack200.Packer.DEFLATE_HINT, "--deflate-hint=",
 513         Pack200.Packer.MODIFICATION_TIME, "--modification-time=",
 514         Pack200.Packer.PASS_FILE_PFX, "--pass-file=",
 515         Pack200.Packer.UNKNOWN_ATTRIBUTE, "--unknown-attribute=",
 516         Pack200.Packer.CLASS_ATTRIBUTE_PFX, "--class-attribute=",
 517         Pack200.Packer.FIELD_ATTRIBUTE_PFX, "--field-attribute=",
 518         Pack200.Packer.METHOD_ATTRIBUTE_PFX, "--method-attribute=",
 519         Pack200.Packer.CODE_ATTRIBUTE_PFX, "--code-attribute=",
 520         //Pack200.Packer.PROGRESS, "--progress=",
 521         Utils.DEBUG_VERBOSE, "--verbose",
 522         Utils.COM_PREFIX+"strip.debug", "--strip-debug",
 523     };
 524 
 525     private static final String[] UNPACK200_PROPERTY_TO_OPTION = {
 526         Pack200.Unpacker.DEFLATE_HINT, "--deflate-hint=",
 527         //Pack200.Unpacker.PROGRESS, "--progress=",
 528         Utils.DEBUG_VERBOSE, "--verbose",
 529         Utils.UNPACK_REMOVE_PACKFILE, "--remove-pack-file",
 530     };
 531 
 532     /*-*
 533      * Remove a set of command-line options from args,
 534      * storing them in the map in a canonicalized form.
 535      * <p>
 536      * The options string is a newline-separated series of
 537      * option processing specifiers.
 538      */
 539     private static
 540     String parseCommandOptions(List<String> args,
 541                                String options,
 542                                Map<String,String> properties) {
 543         //System.out.println(args+" // "+properties);
 544 
 545         String resultString = null;
 546 
 547         // Convert options string into optLines dictionary.
 548         TreeMap<String,String[]> optmap = new TreeMap<>();
 549     loadOptmap:
 550         for (String optline : options.split("\n")) {
 551             String[] words = optline.split("\\p{Space}+");
 552             if (words.length == 0)    continue loadOptmap;
 553             String opt = words[0];
 554             words[0] = "";  // initial word is not a spec
 555             if (opt.length() == 0 && words.length >= 1) {
 556                 opt = words[1];  // initial "word" is empty due to leading ' '
 557                 words[1] = "";
 558             }
 559             if (opt.length() == 0)    continue loadOptmap;
 560             String[] prevWords = optmap.put(opt, words);
 561             if (prevWords != null)
 562             throw new RuntimeException(MessageFormat.format(RESOURCE.getString(DriverResource.DUPLICATE_OPTION), optline.trim()));
 563         }
 564 
 565         // State machine for parsing a command line.
 566         ListIterator<String> argp = args.listIterator();
 567         ListIterator<String> pbp = new ArrayList<String>().listIterator();
 568     doArgs:
 569         for (;;) {
 570             // One trip through this loop per argument.
 571             // Multiple trips per option only if several options per argument.
 572             String arg;
 573             if (pbp.hasPrevious()) {
 574                 arg = pbp.previous();
 575                 pbp.remove();
 576             } else if (argp.hasNext()) {
 577                 arg = argp.next();
 578             } else {
 579                 // No more arguments at all.
 580                 break doArgs;
 581             }
 582         tryOpt:
 583             for (int optlen = arg.length(); ; optlen--) {
 584                 // One time through this loop for each matching arg prefix.
 585                 String opt;
 586                 // Match some prefix of the argument to a key in optmap.
 587             findOpt:
 588                 for (;;) {
 589                     opt = arg.substring(0, optlen);
 590                     if (optmap.containsKey(opt))  break findOpt;
 591                     if (optlen == 0)              break tryOpt;
 592                     // Decide on a smaller prefix to search for.
 593                     SortedMap<String,String[]> pfxmap = optmap.headMap(opt);
 594                     // pfxmap.lastKey is no shorter than any prefix in optmap.
 595                     int len = pfxmap.isEmpty() ? 0 : pfxmap.lastKey().length();
 596                     optlen = Math.min(len, optlen - 1);
 597                     opt = arg.substring(0, optlen);
 598                     // (Note:  We could cut opt down to its common prefix with
 599                     // pfxmap.lastKey, but that wouldn't save many cycles.)
 600                 }
 601                 opt = opt.intern();
 602                 assert(arg.startsWith(opt));
 603                 assert(opt.length() == optlen);
 604                 String val = arg.substring(optlen);  // arg == opt+val
 605 
 606                 // Execute the option processing specs for this opt.
 607                 // If no actions are taken, then look for a shorter prefix.
 608                 boolean didAction = false;
 609                 boolean isError = false;
 610 
 611                 int pbpMark = pbp.nextIndex();  // in case of backtracking
 612                 String[] specs = optmap.get(opt);
 613             eachSpec:
 614                 for (String spec : specs) {
 615                     if (spec.length() == 0)     continue eachSpec;
 616                     if (spec.startsWith("#"))   break eachSpec;
 617                     int sidx = 0;
 618                     char specop = spec.charAt(sidx++);
 619 
 620                     // Deal with '+'/'*' prefixes (spec conditions).
 621                     boolean ok;
 622                     switch (specop) {
 623                     case '+':
 624                         // + means we want an non-empty val suffix.
 625                         ok = (val.length() != 0);
 626                         specop = spec.charAt(sidx++);
 627                         break;
 628                     case '*':
 629                         // * means we accept empty or non-empty
 630                         ok = true;
 631                         specop = spec.charAt(sidx++);
 632                         break;
 633                     default:
 634                         // No condition prefix means we require an exact
 635                         // match, as indicated by an empty val suffix.
 636                         ok = (val.length() == 0);
 637                         break;
 638                     }
 639                     if (!ok)  continue eachSpec;
 640 
 641                     String specarg = spec.substring(sidx);
 642                     switch (specop) {
 643                     case '.':  // terminate the option sequence
 644                         resultString = (specarg.length() != 0)? specarg.intern(): opt;
 645                         break doArgs;
 646                     case '?':  // abort the option sequence
 647                         resultString = (specarg.length() != 0)? specarg.intern(): arg;
 648                         isError = true;
 649                         break eachSpec;
 650                     case '@':  // change the effective opt name
 651                         opt = specarg.intern();
 652                         break;
 653                     case '>':  // shift remaining arg val to next arg
 654                         pbp.add(specarg + val);  // push a new argument
 655                         val = "";
 656                         break;
 657                     case '!':  // negation option
 658                         String negopt = (specarg.length() != 0)? specarg.intern(): opt;
 659                         properties.remove(negopt);
 660                         properties.put(negopt, null);  // leave placeholder
 661                         didAction = true;
 662                         break;
 663                     case '$':  // normal "boolean" option
 664                         String boolval;
 665                         if (specarg.length() != 0) {
 666                             // If there is a given spec token, store it.
 667                             boolval = specarg;
 668                         } else {
 669                             String old = properties.get(opt);
 670                             if (old == null || old.length() == 0) {
 671                                 boolval = "1";
 672                             } else {
 673                                 // Increment any previous value as a numeral.
 674                                 boolval = ""+(1+Integer.parseInt(old));
 675                             }
 676                         }
 677                         properties.put(opt, boolval);
 678                         didAction = true;
 679                         break;
 680                     case '=':  // "string" option
 681                     case '&':  // "collection" option
 682                         // Read an option.
 683                         boolean append = (specop == '&');
 684                         String strval;
 685                         if (pbp.hasPrevious()) {
 686                             strval = pbp.previous();
 687                             pbp.remove();
 688                         } else if (argp.hasNext()) {
 689                             strval = argp.next();
 690                         } else {
 691                             resultString = arg + " ?";
 692                             isError = true;
 693                             break eachSpec;
 694                         }
 695                         if (append) {
 696                             String old = properties.get(opt);
 697                             if (old != null) {
 698                                 // Append new val to old with embedded delim.
 699                                 String delim = specarg;
 700                                 if (delim.length() == 0)  delim = " ";
 701                                 strval = old + specarg + strval;
 702                             }
 703                         }
 704                         properties.put(opt, strval);
 705                         didAction = true;
 706                         break;
 707                     default:
 708                         throw new RuntimeException(MessageFormat.format(RESOURCE.getString(DriverResource.BAD_SPEC),opt, spec));
 709                     }
 710                 }
 711 
 712                 // Done processing specs.
 713                 if (didAction && !isError) {
 714                     continue doArgs;
 715                 }
 716 
 717                 // The specs should have done something, but did not.
 718                 while (pbp.nextIndex() > pbpMark) {
 719                     // Remove anything pushed during these specs.
 720                     pbp.previous();
 721                     pbp.remove();
 722                 }
 723 
 724                 if (isError) {
 725                     throw new IllegalArgumentException(resultString);
 726                 }
 727 
 728                 if (optlen == 0) {
 729                     // We cannot try a shorter matching option.
 730                     break tryOpt;
 731                 }
 732             }
 733 
 734             // If we come here, there was no matching option.
 735             // So, push back the argument, and return to caller.
 736             pbp.add(arg);
 737             break doArgs;
 738         }
 739         // Report number of arguments consumed.
 740         args.subList(0, argp.nextIndex()).clear();
 741         // Report any unconsumed partial argument.
 742         while (pbp.hasPrevious()) {
 743             args.add(0, pbp.previous());
 744         }
 745         //System.out.println(args+" // "+properties+" -> "+resultString);
 746         return resultString;
 747     }
 748 }