1/*
2 * Copyright (c) 2009, 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
26import java.io.*;
27import java.nio.charset.Charset;
28import java.util.*;
29import java.util.zip.*;
30import java.text.MessageFormat;
31
32/**
33 * A stripped-down version of Jar tool with a "-encoding" option to
34 * support non-UTF8 encoidng for entry name and comment.
35 */
36public class zip {
37    String program;
38    PrintStream out, err;
39    String fname;
40    String zname = "";
41    String[] files;
42    Charset cs = Charset.forName("UTF-8");
43
44    Map<String, File> entryMap = new HashMap<String, File>();
45    Set<File> entries = new LinkedHashSet<File>();
46    List<String> paths = new ArrayList<String>();
47
48    CRC32 crc32 = new CRC32();
49    /*
50     * cflag: create
51     * uflag: update
52     * xflag: xtract
53     * tflag: table
54     * vflag: verbose
55     * flag0: no zip compression (store only)
56     */
57    boolean cflag, uflag, xflag, tflag, vflag, flag0;
58
59    private static ResourceBundle rsrc;
60    static {
61        try {
62            // just use the jar message
63            rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar");
64        } catch (MissingResourceException e) {
65            throw new Error("Fatal: Resource for jar is missing");
66        }
67    }
68
69    public zip(PrintStream out, PrintStream err, String program) {
70        this.out = out;
71        this.err = err;
72        this.program = program;
73    }
74
75    private boolean ok;
76
77    public synchronized boolean run(String args[]) {
78        ok = true;
79        if (!parseArgs(args)) {
80            return false;
81        }
82        try {
83            if (cflag || uflag) {
84                if (fname != null) {
85                    zname = fname.replace(File.separatorChar, '/');
86                    if (zname.startsWith("./")) {
87                        zname = zname.substring(2);
88                    }
89                }
90            }
91            if (cflag) {
92                OutputStream out;
93                if (fname != null) {
94                    out = new FileOutputStream(fname);
95                } else {
96                    out = new FileOutputStream(FileDescriptor.out);
97                    if (vflag) {
98                         vflag = false;
99                    }
100                }
101                expand(null, files, false);
102                create(new BufferedOutputStream(out, 4096));
103                out.close();
104            } else if (uflag) {
105                File inputFile = null, tmpFile = null;
106                FileInputStream in;
107                FileOutputStream out;
108                if (fname != null) {
109                    inputFile = new File(fname);
110                    String path = inputFile.getParent();
111                    tmpFile = File.createTempFile("tmp", null,
112                              new File((path == null) ? "." : path));
113                    in = new FileInputStream(inputFile);
114                    out = new FileOutputStream(tmpFile);
115                } else {
116                    in = new FileInputStream(FileDescriptor.in);
117                    out = new FileOutputStream(FileDescriptor.out);
118                    vflag = false;
119                }
120                expand(null, files, true);
121                boolean updateOk = update(in, new BufferedOutputStream(out));
122                if (ok) {
123                    ok = updateOk;
124                }
125                in.close();
126                out.close();
127                if (fname != null) {
128                    inputFile.delete();
129                    if (!tmpFile.renameTo(inputFile)) {
130                        tmpFile.delete();
131                        throw new IOException(getMsg("error.write.file"));
132                    }
133                    tmpFile.delete();
134                }
135            } else if (tflag) {
136                replaceFSC(files);
137                if (fname != null) {
138                    list(fname, files);
139                } else {
140                    InputStream in = new FileInputStream(FileDescriptor.in);
141                    try{
142                        list(new BufferedInputStream(in), files);
143                    } finally {
144                        in.close();
145                    }
146                }
147            } else if (xflag) {
148                replaceFSC(files);
149                if (fname != null && files != null) {
150                    extract(fname, files);
151                } else {
152                    InputStream in = (fname == null)
153                        ? new FileInputStream(FileDescriptor.in)
154                        : new FileInputStream(fname);
155                    try {
156                        extract(new BufferedInputStream(in), files);
157                    } finally {
158                        in.close();
159                    }
160                }
161            }
162        } catch (IOException e) {
163            fatalError(e);
164            ok = false;
165        } catch (Error ee) {
166            ee.printStackTrace();
167            ok = false;
168        } catch (Throwable t) {
169            t.printStackTrace();
170            ok = false;
171        }
172        out.flush();
173        err.flush();
174        return ok;
175    }
176
177
178    boolean parseArgs(String args[]) {
179        try {
180            args = parse(args);
181        } catch (FileNotFoundException e) {
182            fatalError(formatMsg("error.cant.open", e.getMessage()));
183            return false;
184        } catch (IOException e) {
185            fatalError(e);
186            return false;
187        }
188        int count = 1;
189        try {
190            String flags = args[0];
191            if (flags.startsWith("-")) {
192                flags = flags.substring(1);
193            }
194            for (int i = 0; i < flags.length(); i++) {
195                switch (flags.charAt(i)) {
196                case 'c':
197                    if (xflag || tflag || uflag) {
198                        usageError();
199                        return false;
200                    }
201                    cflag = true;
202                    break;
203                case 'u':
204                    if (cflag || xflag || tflag) {
205                        usageError();
206                        return false;
207                    }
208                    uflag = true;
209                    break;
210                case 'x':
211                    if (cflag || uflag || tflag) {
212                        usageError();
213                        return false;
214                    }
215                    xflag = true;
216                    break;
217                case 't':
218                    if (cflag || uflag || xflag) {
219                        usageError();
220                        return false;
221                    }
222                    tflag = true;
223                    break;
224                case 'v':
225                    vflag = true;
226                    break;
227                case 'f':
228                    fname = args[count++];
229                    break;
230                case '0':
231                    flag0 = true;
232                    break;
233                default:
234                    error(formatMsg("error.illegal.option",
235                                String.valueOf(flags.charAt(i))));
236                    usageError();
237                    return false;
238                }
239            }
240        } catch (ArrayIndexOutOfBoundsException e) {
241            usageError();
242            return false;
243        }
244        if (!cflag && !tflag && !xflag && !uflag) {
245            error(getMsg("error.bad.option"));
246            usageError();
247            return false;
248        }
249        /* parse file arguments */
250        int n = args.length - count;
251        if (n > 0) {
252            int k = 0;
253            String[] nameBuf = new String[n];
254            try {
255                for (int i = count; i < args.length; i++) {
256                    if (args[i].equals("-encoding")) {
257                        cs = Charset.forName(args[++i]);
258                    } else if (args[i].equals("-C")) {
259                        /* change the directory */
260                        String dir = args[++i];
261                        dir = (dir.endsWith(File.separator) ?
262                               dir : (dir + File.separator));
263                        dir = dir.replace(File.separatorChar, '/');
264                        while (dir.indexOf("//") > -1) {
265                            dir = dir.replace("//", "/");
266                        }
267                        paths.add(dir.replace(File.separatorChar, '/'));
268                        nameBuf[k++] = dir + args[++i];
269                    } else {
270                        nameBuf[k++] = args[i];
271                    }
272                }
273            } catch (ArrayIndexOutOfBoundsException e) {
274                e.printStackTrace();
275                usageError();
276                return false;
277            }
278            if (k != 0) {
279                files = new String[k];
280                System.arraycopy(nameBuf, 0, files, 0, k);
281            }
282        } else if (cflag || uflag) {
283            error(getMsg("error.bad.uflag"));
284            usageError();
285            return false;
286        }
287        return true;
288    }
289
290    void expand(File dir, String[] files, boolean isUpdate) {
291        if (files == null) {
292            return;
293        }
294        for (int i = 0; i < files.length; i++) {
295            File f;
296            if (dir == null) {
297                f = new File(files[i]);
298            } else {
299                f = new File(dir, files[i]);
300            }
301            if (f.isFile()) {
302                if (entries.add(f)) {
303                    if (isUpdate)
304                        entryMap.put(entryName(f.getPath()), f);
305                }
306            } else if (f.isDirectory()) {
307                if (entries.add(f)) {
308                    if (isUpdate) {
309                        String dirPath = f.getPath();
310                        dirPath = (dirPath.endsWith(File.separator)) ? dirPath :
311                            (dirPath + File.separator);
312                        entryMap.put(entryName(dirPath), f);
313                    }
314                    expand(f, f.list(), isUpdate);
315                }
316            } else {
317                error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
318                ok = false;
319            }
320        }
321    }
322
323    void create(OutputStream out) throws IOException
324    {
325        try (ZipOutputStream zos = new ZipOutputStream(out, cs)) {
326            if (flag0) {
327                zos.setMethod(ZipOutputStream.STORED);
328            }
329            for (File file: entries) {
330                addFile(zos, file);
331            }
332        }
333    }
334
335    boolean update(InputStream in, OutputStream out) throws IOException
336    {
337        try (ZipInputStream zis = new ZipInputStream(in, cs);
338             ZipOutputStream zos = new ZipOutputStream(out, cs))
339        {
340            ZipEntry e = null;
341            byte[] buf = new byte[1024];
342            int n = 0;
343            boolean updateOk = true;
344
345            // put the old entries first, replace if necessary
346            while ((e = zis.getNextEntry()) != null) {
347                String name = e.getName();
348                if (!entryMap.containsKey(name)) { // copy the old stuff
349                    // do our own compression
350                    ZipEntry e2 = new ZipEntry(name);
351                    e2.setMethod(e.getMethod());
352                    e2.setTime(e.getTime());
353                    e2.setComment(e.getComment());
354                    e2.setExtra(e.getExtra());
355                    if (e.getMethod() == ZipEntry.STORED) {
356                        e2.setSize(e.getSize());
357                        e2.setCrc(e.getCrc());
358                    }
359                    zos.putNextEntry(e2);
360                    while ((n = zis.read(buf, 0, buf.length)) != -1) {
361                        zos.write(buf, 0, n);
362                    }
363                } else { // replace with the new files
364                    File f = entryMap.get(name);
365                    addFile(zos, f);
366                    entryMap.remove(name);
367                    entries.remove(f);
368                }
369            }
370
371            // add the remaining new files
372            for (File f: entries) {
373                addFile(zos, f);
374            }
375        }
376        return updateOk;
377    }
378
379    private String entryName(String name) {
380        name = name.replace(File.separatorChar, '/');
381        String matchPath = "";
382        for (String path : paths) {
383            if (name.startsWith(path) && (path.length() > matchPath.length())) {
384                matchPath = path;
385            }
386        }
387        name = name.substring(matchPath.length());
388
389        if (name.startsWith("/")) {
390            name = name.substring(1);
391        } else if (name.startsWith("./")) {
392            name = name.substring(2);
393        }
394        return name;
395    }
396
397    void addFile(ZipOutputStream zos, File file) throws IOException {
398        String name = file.getPath();
399        boolean isDir = file.isDirectory();
400        if (isDir) {
401            name = name.endsWith(File.separator) ? name :
402                (name + File.separator);
403        }
404        name = entryName(name);
405
406        if (name.equals("") || name.equals(".") || name.equals(zname)) {
407            return;
408        }
409
410        long size = isDir ? 0 : file.length();
411
412        if (vflag) {
413            out.print(formatMsg("out.adding", name));
414        }
415        ZipEntry e = new ZipEntry(name);
416        e.setTime(file.lastModified());
417        if (size == 0) {
418            e.setMethod(ZipEntry.STORED);
419            e.setSize(0);
420            e.setCrc(0);
421        } else if (flag0) {
422            e.setSize(size);
423            e.setMethod(ZipEntry.STORED);
424            crc32File(e, file);
425        }
426        zos.putNextEntry(e);
427        if (!isDir) {
428            byte[] buf = new byte[8192];
429            int len;
430            InputStream is = new BufferedInputStream(new FileInputStream(file));
431            while ((len = is.read(buf, 0, buf.length)) != -1) {
432                zos.write(buf, 0, len);
433            }
434            is.close();
435        }
436        zos.closeEntry();
437        /* report how much compression occurred. */
438        if (vflag) {
439            size = e.getSize();
440            long csize = e.getCompressedSize();
441            out.print(formatMsg2("out.size", String.valueOf(size),
442                        String.valueOf(csize)));
443            if (e.getMethod() == ZipEntry.DEFLATED) {
444                long ratio = 0;
445                if (size != 0) {
446                    ratio = ((size - csize) * 100) / size;
447                }
448                output(formatMsg("out.deflated", String.valueOf(ratio)));
449            } else {
450                output(getMsg("out.stored"));
451            }
452        }
453    }
454
455    private void crc32File(ZipEntry e, File f) throws IOException {
456        InputStream is = new BufferedInputStream(new FileInputStream(f));
457        byte[] buf = new byte[8192];
458        crc32.reset();
459        int r = 0;
460        int nread = 0;
461        long len = f.length();
462        while ((r = is.read(buf)) != -1) {
463            nread += r;
464            crc32.update(buf, 0, r);
465        }
466        is.close();
467        if (nread != (int) len) {
468            throw new ZipException(formatMsg(
469                        "error.incorrect.length", f.getPath()));
470        }
471        e.setCrc(crc32.getValue());
472    }
473
474    void replaceFSC(String files[]) {
475        if (files != null) {
476            for (String file : files) {
477                file = file.replace(File.separatorChar, '/');
478            }
479        }
480    }
481
482    Set<ZipEntry> newDirSet() {
483        return new HashSet<ZipEntry>() {
484            public boolean add(ZipEntry e) {
485                return (e == null || super.add(e));
486            }};
487    }
488
489    void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException {
490        for (ZipEntry ze : zes) {
491            long lastModified = ze.getTime();
492            if (lastModified != -1) {
493                File f = new File(ze.getName().replace('/', File.separatorChar));
494                f.setLastModified(lastModified);
495            }
496        }
497    }
498
499    void extract(InputStream in, String files[]) throws IOException {
500        ZipInputStream zis = new ZipInputStream(in, cs);
501        ZipEntry e;
502        Set<ZipEntry> dirs = newDirSet();
503        while ((e = zis.getNextEntry()) != null) {
504            if (files == null) {
505                dirs.add(extractFile(zis, e));
506            } else {
507                String name = e.getName();
508                for (String file : files) {
509                    if (name.startsWith(file)) {
510                        dirs.add(extractFile(zis, e));
511                        break;
512                    }
513                }
514            }
515        }
516        updateLastModifiedTime(dirs);
517    }
518
519    void extract(String fname, String files[]) throws IOException {
520        try (ZipFile zf = new ZipFile(fname, cs)) {
521            Set<ZipEntry> dirs = newDirSet();
522            Enumeration<? extends ZipEntry> zes = zf.entries();
523            while (zes.hasMoreElements()) {
524                ZipEntry e = zes.nextElement();
525                InputStream is;
526                if (files == null) {
527                    dirs.add(extractFile(zf.getInputStream(e), e));
528                } else {
529                    String name = e.getName();
530                    for (String file : files) {
531                        if (name.startsWith(file)) {
532                            dirs.add(extractFile(zf.getInputStream(e), e));
533                            break;
534                        }
535                    }
536                }
537            }
538        }
539        updateLastModifiedTime(dirs);
540    }
541
542    ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
543        ZipEntry rc = null;
544        String name = e.getName();
545        File f = new File(e.getName().replace('/', File.separatorChar));
546        if (e.isDirectory()) {
547            if (f.exists()) {
548                if (!f.isDirectory()) {
549                    throw new IOException(formatMsg("error.create.dir",
550                        f.getPath()));
551                }
552            } else {
553                if (!f.mkdirs()) {
554                    throw new IOException(formatMsg("error.create.dir",
555                        f.getPath()));
556                } else {
557                    rc = e;
558                }
559            }
560            if (vflag) {
561                output(formatMsg("out.create", name));
562            }
563        } else {
564            if (f.getParent() != null) {
565                File d = new File(f.getParent());
566                if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
567                    throw new IOException(formatMsg(
568                        "error.create.dir", d.getPath()));
569                }
570            }
571            OutputStream os = new FileOutputStream(f);
572            byte[] b = new byte[8192];
573            int len;
574            try {
575                while ((len = is.read(b, 0, b.length)) != -1) {
576                    os.write(b, 0, len);
577                }
578            } finally {
579                if (is instanceof ZipInputStream)
580                    ((ZipInputStream)is).closeEntry();
581                else
582                    is.close();
583                os.close();
584            }
585            if (vflag) {
586                if (e.getMethod() == ZipEntry.DEFLATED) {
587                    output(formatMsg("out.inflated", name));
588                } else {
589                    output(formatMsg("out.extracted", name));
590                }
591            }
592        }
593        long lastModified = e.getTime();
594        if (lastModified != -1) {
595            f.setLastModified(lastModified);
596        }
597        return rc;
598    }
599
600    void list(InputStream in, String files[]) throws IOException {
601        ZipInputStream zis = new ZipInputStream(in, cs);
602        ZipEntry e;
603        while ((e = zis.getNextEntry()) != null) {
604            zis.closeEntry();
605            printEntry(e, files);
606        }
607    }
608
609    void list(String fname, String files[]) throws IOException {
610        try (ZipFile zf = new ZipFile(fname, cs)) {
611            Enumeration<? extends ZipEntry> zes = zf.entries();
612            while (zes.hasMoreElements()) {
613                printEntry(zes.nextElement(), files);
614            }
615        }
616    }
617
618    void printEntry(ZipEntry e, String[] files) throws IOException {
619        if (files == null) {
620            printEntry(e);
621        } else {
622            String name = e.getName();
623            for (String file : files) {
624                if (name.startsWith(file)) {
625                    printEntry(e);
626                    return;
627                }
628            }
629        }
630    }
631
632    void printEntry(ZipEntry e) throws IOException {
633        if (vflag) {
634            StringBuilder sb = new StringBuilder();
635            String s = Long.toString(e.getSize());
636            for (int i = 6 - s.length(); i > 0; --i) {
637                sb.append(' ');
638            }
639            sb.append(s).append(' ').append(new Date(e.getTime()).toString());
640            sb.append(' ').append(e.getName());
641            output(sb.toString());
642        } else {
643            output(e.getName());
644        }
645    }
646
647    void usageError() {
648        error(
649        "Usage: zip {ctxu}[vf0] [zip-file] [-encoding encname][-C dir] files ...\n" +
650        "Options:\n" +
651        "   -c  create new archive\n" +
652        "   -t  list table of contents for archive\n" +
653        "   -x  extract named (or all) files from archive\n" +
654        "   -u  update existing archive\n" +
655        "   -v  generate verbose output on standard output\n" +
656        "   -f  specify archive file name\n" +
657        "   -0  store only; use no ZIP compression\n" +
658        "   -C  change to the specified directory and include the following file\n" +
659        "If any file is a directory then it is processed recursively.\n");
660    }
661
662    void fatalError(Exception e) {
663        e.printStackTrace();
664    }
665
666
667    void fatalError(String s) {
668        error(program + ": " + s);
669    }
670
671
672    protected void output(String s) {
673        out.println(s);
674    }
675
676    protected void error(String s) {
677        err.println(s);
678    }
679
680    private String getMsg(String key) {
681        try {
682            return (rsrc.getString(key));
683        } catch (MissingResourceException e) {
684            throw new Error("Error in message file");
685        }
686    }
687
688    private String formatMsg(String key, String arg) {
689        String msg = getMsg(key);
690        String[] args = new String[1];
691        args[0] = arg;
692        return MessageFormat.format(msg, (Object[]) args);
693    }
694
695    private String formatMsg2(String key, String arg, String arg1) {
696        String msg = getMsg(key);
697        String[] args = new String[2];
698        args[0] = arg;
699        args[1] = arg1;
700        return MessageFormat.format(msg, (Object[]) args);
701    }
702
703    public static String[] parse(String[] args) throws IOException
704    {
705        ArrayList<String> newArgs = new ArrayList<String>(args.length);
706        for (int i = 0; i < args.length; i++) {
707            String arg = args[i];
708            if (arg.length() > 1 && arg.charAt(0) == '@') {
709                arg = arg.substring(1);
710                if (arg.charAt(0) == '@') {
711                    newArgs.add(arg);
712                } else {
713                    loadCmdFile(arg, newArgs);
714                }
715            } else {
716                newArgs.add(arg);
717            }
718        }
719        return newArgs.toArray(new String[newArgs.size()]);
720    }
721
722    private static void loadCmdFile(String name, List<String> args) throws IOException
723    {
724        Reader r = new BufferedReader(new FileReader(name));
725        StreamTokenizer st = new StreamTokenizer(r);
726        st.resetSyntax();
727        st.wordChars(' ', 255);
728        st.whitespaceChars(0, ' ');
729        st.commentChar('#');
730        st.quoteChar('"');
731        st.quoteChar('\'');
732        while (st.nextToken() != st.TT_EOF) {
733            args.add(st.sval);
734        }
735        r.close();
736    }
737
738    public static void main(String args[]) {
739        zip z = new zip(System.out, System.err, "zip");
740        System.exit(z.run(args) ? 0 : 1);
741    }
742}
743
744