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
26package com.sun.java.util.jar.pack;
27
28import java.io.BufferedInputStream;
29import java.io.BufferedOutputStream;
30import java.io.File;
31import java.io.FileInputStream;
32import java.io.FileOutputStream;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.OutputStream;
36import java.io.PrintStream;
37import java.text.MessageFormat;
38import java.nio.file.Files;
39import java.nio.file.Path;
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.HashMap;
43import java.util.Iterator;
44import java.util.List;
45import java.util.ListIterator;
46import java.util.Map;
47import java.util.Properties;
48import java.util.ResourceBundle;
49import java.util.SortedMap;
50import java.util.TreeMap;
51import java.util.jar.JarFile;
52import java.util.jar.JarOutputStream;
53import java.util.jar.Pack200;
54import java.util.zip.GZIPInputStream;
55import java.util.zip.GZIPOutputStream;
56
57/** Command line interface for Pack200.
58 */
59class 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}
729