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