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