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