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