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