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