/* * Copyright 1996-2007 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. */ package sun.tools.jar; import java.io.*; import java.util.*; import java.util.zip.*; import java.util.jar.*; import java.util.jar.Manifest; import java.text.MessageFormat; import sun.misc.JarIndex; /** * This class implements a simple utility for creating files in the JAR * (Java Archive) file format. The JAR format is based on the ZIP file * format, with optional meta-information stored in a MANIFEST entry. */ public class Main { String program; PrintStream out, err; String fname, mname, ename; String zname = ""; String[] files; String rootjar = null; Hashtable filesTable = new Hashtable(); Vector paths = new Vector(); Vector v; CRC32 crc32 = new CRC32(); /* * cflag: create * uflag: update * xflag: xtract * tflag: table * vflag: verbose * flag0: no zip compression (store only) * Mflag: DO NOT generate a manifest file (just ZIP) * iflag: generate jar index */ boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag; static final String MANIFEST = JarFile.MANIFEST_NAME; static final String MANIFEST_DIR = "META-INF/"; static final String VERSION = "1.0"; static final char SEPARATOR = File.separatorChar; static final String INDEX = JarIndex.INDEX_NAME; private static ResourceBundle rsrc; /** * If true, maintain compatibility with JDK releases prior to 6.0 by * timestamping extracted files with the time at which they are extracted. * Default is to use the time given in the archive. */ private static final boolean useExtractionTime = Boolean.getBoolean("sun.tools.jar.useExtractionTime"); /** * Initialize ResourceBundle */ static { try { rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar"); } catch (MissingResourceException e) { throw new Error("Fatal: Resource for jar is missing"); } } private String getMsg(String key) { try { return (rsrc.getString(key)); } catch (MissingResourceException e) { throw new Error("Error in message file"); } } private String formatMsg(String key, String arg) { String msg = getMsg(key); String[] args = new String[1]; args[0] = arg; return MessageFormat.format(msg, (Object[]) args); } private String formatMsg2(String key, String arg, String arg1) { String msg = getMsg(key); String[] args = new String[2]; args[0] = arg; args[1] = arg1; return MessageFormat.format(msg, (Object[]) args); } public Main(PrintStream out, PrintStream err, String program) { this.out = out; this.err = err; this.program = program; } private boolean ok; /* * Starts main program with the specified arguments. */ public synchronized boolean run(String args[]) { ok = true; if (!parseArgs(args)) { return false; } try { if (cflag || uflag) { if (fname != null) { // The name of the zip file as it would appear as its own // zip file entry. We use this to make sure that we don't // add the zip file to itself. zname = fname.replace(File.separatorChar, '/'); if (zname.startsWith("./")) { zname = zname.substring(2); } } } if (cflag) { Manifest manifest = null; InputStream in = null; if (!Mflag) { if (mname != null) { in = new FileInputStream(mname); manifest = new Manifest(new BufferedInputStream(in)); } else { manifest = new Manifest(); } addVersion(manifest); addCreatedBy(manifest); if (isAmbigousMainClass(manifest)) { if (in != null) { in.close(); } return false; } if (ename != null) { addMainClass(manifest, ename); } } OutputStream out; if (fname != null) { out = new FileOutputStream(fname); } else { out = new FileOutputStream(FileDescriptor.out); if (vflag) { // Disable verbose output so that it does not appear // on stdout along with file data // error("Warning: -v option ignored"); vflag = false; } } create(new BufferedOutputStream(out), expand(files), manifest); if (in != null) { in.close(); } out.close(); } else if (uflag) { File inputFile = null, tmpFile = null; FileInputStream in; FileOutputStream out; if (fname != null) { inputFile = new File(fname); String path = inputFile.getParent(); tmpFile = File.createTempFile("tmp", null, new File((path == null) ? "." : path)); in = new FileInputStream(inputFile); out = new FileOutputStream(tmpFile); } else { in = new FileInputStream(FileDescriptor.in); out = new FileOutputStream(FileDescriptor.out); vflag = false; } InputStream manifest = (!Mflag && (mname != null)) ? (new FileInputStream(mname)) : null; expand(files); boolean updateOk = update(in, new BufferedOutputStream(out), manifest); if (ok) { ok = updateOk; } in.close(); out.close(); if (manifest != null) { manifest.close(); } if (fname != null) { // on Win32, we need this delete inputFile.delete(); if (!tmpFile.renameTo(inputFile)) { tmpFile.delete(); throw new IOException(getMsg("error.write.file")); } tmpFile.delete(); } } else if (xflag || tflag) { InputStream in; if (fname != null) { in = new FileInputStream(fname); } else { in = new FileInputStream(FileDescriptor.in); } if (xflag) { extract(new BufferedInputStream(in), files); } else { list(new BufferedInputStream(in), files); } in.close(); } else if (iflag) { genIndex(rootjar, files); } } catch (IOException e) { fatalError(e); ok = false; } catch (Error ee) { ee.printStackTrace(); ok = false; } catch (Throwable t) { t.printStackTrace(); ok = false; } out.flush(); err.flush(); return ok; } /* * Parse command line arguments. */ boolean parseArgs(String args[]) { /* Preprocess and expand @file arguments */ try { args = CommandLine.parse(args); } catch (FileNotFoundException e) { fatalError(formatMsg("error.cant.open", e.getMessage())); return false; } catch (IOException e) { fatalError(e); return false; } /* parse flags */ int count = 1; try { String flags = args[0]; if (flags.startsWith("-")) { flags = flags.substring(1); } for (int i = 0; i < flags.length(); i++) { switch (flags.charAt(i)) { case 'c': if (xflag || tflag || uflag) { usageError(); return false; } cflag = true; break; case 'u': if (cflag || xflag || tflag) { usageError(); return false; } uflag = true; break; case 'x': if (cflag || uflag || tflag) { usageError(); return false; } xflag = true; break; case 't': if (cflag || uflag || xflag) { usageError(); return false; } tflag = true; break; case 'M': Mflag = true; break; case 'v': vflag = true; break; case 'f': fname = args[count++]; break; case 'm': mname = args[count++]; break; case '0': flag0 = true; break; case 'i': // do not increase the counter, files will contain rootjar rootjar = args[count++]; iflag = true; break; case 'e': ename = args[count++]; break; default: error(formatMsg("error.illegal.option", String.valueOf(flags.charAt(i)))); usageError(); return false; } } } catch (ArrayIndexOutOfBoundsException e) { usageError(); return false; } if (!cflag && !tflag && !xflag && !uflag && !iflag) { error(getMsg("error.bad.option")); usageError(); return false; } /* parse file arguments */ int n = args.length - count; if (n > 0) { int k = 0; String[] nameBuf = new String[n]; try { for (int i = count; i < args.length; i++) { if (args[i].equals("-C")) { /* change the directory */ String dir = args[++i]; dir = (dir.endsWith(File.separator) ? dir : (dir + File.separator)); dir = dir.replace(File.separatorChar, '/'); while (dir.indexOf("//") > -1) { dir = dir.replace("//", "/"); } paths.addElement(dir.replace(File.separatorChar, '/')); nameBuf[k++] = dir + args[++i]; } else { nameBuf[k++] = args[i]; } } } catch (ArrayIndexOutOfBoundsException e) { usageError(); return false; } files = new String[k]; System.arraycopy(nameBuf, 0, files, 0, k); } else if (cflag && (mname == null)) { error(getMsg("error.bad.cflag")); usageError(); return false; } else if (uflag) { if ((mname != null) || (ename != null)) { /* just want to update the manifest */ return true; } else { error(getMsg("error.bad.uflag")); usageError(); return false; } } return true; } /* * Expands list of files to process into full list of all files that * can be found by recursively descending directories. */ String[] expand(String[] files) { v = new Vector(); expand(null, files, v, filesTable); files = new String[v.size()]; for (int i = 0; i < files.length; i++) { files[i] = ((File)v.elementAt(i)).getPath(); } return files; } void expand(File dir, String[] files, Vector v, Hashtable t) { if (files == null) { return; } for (int i = 0; i < files.length; i++) { File f; if (dir == null) { f = new File(files[i]); } else { f = new File(dir, files[i]); } if (f.isFile()) { if (!t.contains(f)) { t.put(entryName(f.getPath()), f); v.addElement(f); } } else if (f.isDirectory()) { String dirPath = f.getPath(); dirPath = (dirPath.endsWith(File.separator)) ? dirPath : (dirPath + File.separator); t.put(entryName(dirPath), f); v.addElement(f); expand(f, f.list(), v, t); } else { error(formatMsg("error.nosuch.fileordir", String.valueOf(f))); ok = false; } } } /* * Creates a new JAR file. */ void create(OutputStream out, String[] files, Manifest manifest) throws IOException { ZipOutputStream zos = new JarOutputStream(out); if (flag0) { zos.setMethod(ZipOutputStream.STORED); } if (manifest != null) { if (vflag) { output(getMsg("out.added.manifest")); } ZipEntry e = new ZipEntry(MANIFEST_DIR); e.setTime(System.currentTimeMillis()); e.setSize(0); e.setCrc(0); zos.putNextEntry(e); e = new ZipEntry(MANIFEST); e.setTime(System.currentTimeMillis()); if (flag0) { crc32Manifest(e, manifest); } zos.putNextEntry(e); manifest.write(zos); zos.closeEntry(); } for (int i = 0; i < files.length; i++) { addFile(zos, new File(files[i])); } zos.close(); } /* * update an existing jar file. */ boolean update(InputStream in, OutputStream out, InputStream newManifest) throws IOException { Hashtable t = filesTable; Vector v = this.v; ZipInputStream zis = new ZipInputStream(in); ZipOutputStream zos = new JarOutputStream(out); ZipEntry e = null; boolean foundManifest = false; byte[] buf = new byte[1024]; int n = 0; boolean updateOk = true; if (t.containsKey(INDEX)) { addIndex((JarIndex)t.get(INDEX), zos); } // put the old entries first, replace if necessary while ((e = zis.getNextEntry()) != null) { String name = e.getName(); boolean isManifestEntry = name.toUpperCase( java.util.Locale.ENGLISH). equals(MANIFEST); if ((name.toUpperCase().equals(INDEX) && t.containsKey(INDEX)) || (Mflag && isManifestEntry)) { continue; } else if (isManifestEntry && ((newManifest != null) || (ename != null))) { foundManifest = true; if (newManifest != null) { // Don't read from the newManifest InputStream, as we // might need it below, and we can't re-read the same data // twice. FileInputStream fis = new FileInputStream(mname); boolean ambigous = isAmbigousMainClass(new Manifest(fis)); fis.close(); if (ambigous) { return false; } } // Update the manifest. Manifest old = new Manifest(zis); if (newManifest != null) { old.read(newManifest); } updateManifest(old, zos); } else { if (!t.containsKey(name)) { // copy the old stuff // do our own compression ZipEntry e2 = new ZipEntry(name); e2.setMethod(e.getMethod()); e2.setTime(e.getTime()); e2.setComment(e.getComment()); e2.setExtra(e.getExtra()); if (e.getMethod() == ZipEntry.STORED) { e2.setSize(e.getSize()); e2.setCrc(e.getCrc()); } zos.putNextEntry(e2); while ((n = zis.read(buf, 0, buf.length)) != -1) { zos.write(buf, 0, n); } } else { // replace with the new files addFile(zos, (File)(t.get(name))); t.remove(name); } } } t.remove(INDEX); // add the remaining new files if (!t.isEmpty()) { for (int i = 0; i < v.size(); i++) { File f = (File)v.elementAt(i); if (t.containsValue(f)) { addFile(zos, f); } } } if (!foundManifest) { if (newManifest != null) { Manifest m = new Manifest(newManifest); updateOk = !isAmbigousMainClass(m); if (updateOk) { updateManifest(m, zos); } } else if (ename != null) { updateManifest(new Manifest(), zos); } } zis.close(); zos.close(); return updateOk; } private void addIndex(JarIndex index, ZipOutputStream zos) throws IOException { ZipEntry e = new ZipEntry(INDEX); e.setTime(System.currentTimeMillis()); if (flag0) { e.setMethod(ZipEntry.STORED); File ifile = File.createTempFile("index", null, new File(".")); BufferedOutputStream bos = new BufferedOutputStream (new FileOutputStream(ifile)); index.write(bos); crc32File(e, ifile); bos.close(); ifile.delete(); } zos.putNextEntry(e); index.write(zos); if (vflag) { // output(getMsg("out.update.manifest")); } } private void updateManifest(Manifest m, ZipOutputStream zos) throws IOException { addVersion(m); addCreatedBy(m); if (ename != null) { addMainClass(m, ename); } ZipEntry e = new ZipEntry(MANIFEST); e.setTime(System.currentTimeMillis()); if (flag0) { e.setMethod(ZipEntry.STORED); crc32Manifest(e, m); } zos.putNextEntry(e); m.write(zos); if (vflag) { output(getMsg("out.update.manifest")); } } private String entryName(String name) { name = name.replace(File.separatorChar, '/'); String matchPath = ""; for (int i = 0; i < paths.size(); i++) { String path = (String)paths.elementAt(i); if (name.startsWith(path) && (path.length() > matchPath.length())) { matchPath = path; } } name = name.substring(matchPath.length()); if (name.startsWith("/")) { name = name.substring(1); } else if (name.startsWith("./")) { name = name.substring(2); } return name; } private void addVersion(Manifest m) { Attributes global = m.getMainAttributes(); if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) { global.put(Attributes.Name.MANIFEST_VERSION, VERSION); } } private void addCreatedBy(Manifest m) { Attributes global = m.getMainAttributes(); if (global.getValue(new Attributes.Name("Created-By")) == null) { String javaVendor = System.getProperty("java.vendor"); String jdkVersion = System.getProperty("java.version"); global.put(new Attributes.Name("Created-By"), jdkVersion + " (" + javaVendor + ")"); } } private void addMainClass(Manifest m, String mainApp) { Attributes global = m.getMainAttributes(); // overrides any existing Main-Class attribute global.put(Attributes.Name.MAIN_CLASS, mainApp); } private boolean isAmbigousMainClass(Manifest m) { if (ename != null) { Attributes global = m.getMainAttributes(); if ((global.get(Attributes.Name.MAIN_CLASS) != null)) { error(getMsg("error.bad.eflag")); usageError(); return true; } } return false; } /* * Adds a new file entry to the ZIP output stream. */ void addFile(ZipOutputStream zos, File file) throws IOException { String name = file.getPath(); boolean isDir = file.isDirectory(); if (isDir) { name = name.endsWith(File.separator) ? name : (name + File.separator); } name = entryName(name); if (name.equals("") || name.equals(".") || name.equals(zname)) { return; } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST)) && !Mflag) { if (vflag) { output(formatMsg("out.ignore.entry", name)); } return; } long size = isDir ? 0 : file.length(); if (vflag) { out.print(formatMsg("out.adding", name)); } ZipEntry e = new ZipEntry(name); e.setTime(file.lastModified()); if (size == 0) { e.setMethod(ZipEntry.STORED); e.setSize(0); e.setCrc(0); } else if (flag0) { e.setSize(size); e.setMethod(ZipEntry.STORED); crc32File(e, file); } zos.putNextEntry(e); if (!isDir) { byte[] buf = new byte[1024]; int len; InputStream is = new BufferedInputStream(new FileInputStream(file)); while ((len = is.read(buf, 0, buf.length)) != -1) { zos.write(buf, 0, len); } is.close(); } zos.closeEntry(); /* report how much compression occurred. */ if (vflag) { size = e.getSize(); long csize = e.getCompressedSize(); out.print(formatMsg2("out.size", String.valueOf(size), String.valueOf(csize))); if (e.getMethod() == ZipEntry.DEFLATED) { long ratio = 0; if (size != 0) { ratio = ((size - csize) * 100) / size; } output(formatMsg("out.deflated", String.valueOf(ratio))); } else { output(getMsg("out.stored")); } } } /* * compute the crc32 of a file. This is necessary when the ZipOutputStream * is in STORED mode. */ private void crc32Manifest(ZipEntry e, Manifest m) throws IOException { crc32.reset(); CRC32OutputStream os = new CRC32OutputStream(crc32); m.write(os); e.setSize((long) os.n); e.setCrc(crc32.getValue()); } /* * compute the crc32 of a file. This is necessary when the ZipOutputStream * is in STORED mode. */ private void crc32File(ZipEntry e, File f) throws IOException { InputStream is = new BufferedInputStream(new FileInputStream(f)); byte[] buf = new byte[1024]; crc32.reset(); int r = 0; int nread = 0; long len = f.length(); while ((r = is.read(buf)) != -1) { nread += r; crc32.update(buf, 0, r); } is.close(); if (nread != (int) len) { throw new JarException(formatMsg( "error.incorrect.length", f.getPath())); } e.setCrc(crc32.getValue()); } /* * Extracts specified entries from JAR file. */ void extract(InputStream in, String files[]) throws IOException { ZipInputStream zis = new ZipInputStream(in); ZipEntry e; // Set of all directory entries specified in archive. Dissallows // null entries. Disallows all entries if using pre-6.0 behavior. Set<ZipEntry> dirs = new HashSet<ZipEntry>() { public boolean add(ZipEntry e) { return ((e == null || useExtractionTime) ? false : super.add(e)); }}; while ((e = zis.getNextEntry()) != null) { if (files == null) { dirs.add(extractFile(zis, e)); } else { String name = e.getName(); for (int i = 0; i < files.length; i++) { String file = files[i].replace(File.separatorChar, '/'); if (name.startsWith(file)) { dirs.add(extractFile(zis, e)); break; } } } } // Update timestamps of directories specified in archive with their // timestamps as given in the archive. We do this after extraction, // instead of during, because creating a file in a directory changes // that directory's timestamp. for (ZipEntry dirEntry : dirs) { long lastModified = dirEntry.getTime(); if (lastModified != -1) { File dir = new File(dirEntry.getName().replace('/', File.separatorChar)); dir.setLastModified(lastModified); } } } /* * Extracts next entry from JAR file, creating directories as needed. If * the entry is for a directory which doesn't exist prior to this * invocation, returns that entry, otherwise returns null. */ ZipEntry extractFile(ZipInputStream zis, ZipEntry e) throws IOException { ZipEntry rc = null; String name = e.getName(); File f = new File(e.getName().replace('/', File.separatorChar)); if (e.isDirectory()) { if (f.exists()) { if (!f.isDirectory()) { throw new IOException(formatMsg("error.create.dir", f.getPath())); } } else { if (!f.mkdirs()) { throw new IOException(formatMsg("error.create.dir", f.getPath())); } else { rc = e; } } if (vflag) { output(formatMsg("out.create", name)); } } else { if (f.getParent() != null) { File d = new File(f.getParent()); if (!d.exists() && !d.mkdirs() || !d.isDirectory()) { throw new IOException(formatMsg( "error.create.dir", d.getPath())); } } OutputStream os = new FileOutputStream(f); byte[] b = new byte[512]; int len; while ((len = zis.read(b, 0, b.length)) != -1) { os.write(b, 0, len); } zis.closeEntry(); os.close(); if (vflag) { if (e.getMethod() == ZipEntry.DEFLATED) { output(formatMsg("out.inflated", name)); } else { output(formatMsg("out.extracted", name)); } } } if (!useExtractionTime) { long lastModified = e.getTime(); if (lastModified != -1) { f.setLastModified(lastModified); } } return rc; } /* * Lists contents of JAR file. */ void list(InputStream in, String files[]) throws IOException { ZipInputStream zis = new ZipInputStream(in); ZipEntry e; while ((e = zis.getNextEntry()) != null) { String name = e.getName(); /* * In the case of a compressed (deflated) entry, the entry size * is stored immediately following the entry data and cannot be * determined until the entry is fully read. Therefore, we close * the entry first before printing out its attributes. */ zis.closeEntry(); if (files == null) { printEntry(e); } else { for (int i = 0; i < files.length; i++) { String file = files[i].replace(File.separatorChar, '/'); if (name.startsWith(file)) { printEntry(e); break; } } } } } /** * Output the class index table to the INDEX.LIST file of the * root jar file. */ void dumpIndex(String rootjar, JarIndex index) throws IOException { filesTable.put(INDEX, index); File scratchFile = File.createTempFile("scratch", null, new File(".")); File jarFile = new File(rootjar); boolean updateOk = update(new FileInputStream(jarFile), new FileOutputStream(scratchFile), null); jarFile.delete(); if (!scratchFile.renameTo(jarFile)) { scratchFile.delete(); throw new IOException(getMsg("error.write.file")); } scratchFile.delete(); } private Hashtable jarTable = new Hashtable(); /* * Generate the transitive closure of the Class-Path attribute for * the specified jar file. */ Vector getJarPath(String jar) throws IOException { Vector files = new Vector(); files.add(jar); jarTable.put(jar, jar); // take out the current path String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1)); // class path attribute will give us jar file name with // '/' as separators, so we need to change them to the // appropriate one before we open the jar file. JarFile rf = new JarFile(jar.replace('/', File.separatorChar)); if (rf != null) { Manifest man = rf.getManifest(); if (man != null) { Attributes attr = man.getMainAttributes(); if (attr != null) { String value = attr.getValue(Attributes.Name.CLASS_PATH); if (value != null) { StringTokenizer st = new StringTokenizer(value); while (st.hasMoreTokens()) { String ajar = st.nextToken(); if (!ajar.endsWith("/")) { // it is a jar file ajar = path.concat(ajar); /* check on cyclic dependency */ if (jarTable.get(ajar) == null) { files.addAll(getJarPath(ajar)); } } } } } } } rf.close(); return files; } /** * Generate class index file for the specified root jar file. */ void genIndex(String rootjar, String[] files) throws IOException { Vector jars = getJarPath(rootjar); int njars = jars.size(); String[] jarfiles; if (njars == 1 && files != null) { // no class-path attribute defined in rootjar, will // use command line specified list of jars for (int i = 0; i < files.length; i++) { jars.addAll(getJarPath(files[i])); } njars = jars.size(); } jarfiles = (String[])jars.toArray(new String[njars]); JarIndex index = new JarIndex(jarfiles); dumpIndex(rootjar, index); } /* * Prints entry information. */ void printEntry(ZipEntry e) throws IOException { if (vflag) { StringBuffer sb = new StringBuffer(); String s = Long.toString(e.getSize()); for (int i = 6 - s.length(); i > 0; --i) { sb.append(' '); } sb.append(s).append(' ').append(new Date(e.getTime()).toString()); sb.append(' ').append(e.getName()); output(sb.toString()); } else { output(e.getName()); } } /* * Print usage message and die. */ void usageError() { error(getMsg("usage")); } /* * A fatal exception has been caught. No recovery possible */ void fatalError(Exception e) { e.printStackTrace(); } /* * A fatal condition has been detected; message is "s". * No recovery possible */ void fatalError(String s) { error(program + ": " + s); } /** * Print an output message; like verbose output and the like */ protected void output(String s) { out.println(s); } /** * Print an error mesage; like something is broken */ protected void error(String s) { err.println(s); } /* * Main routine to start program. */ public static void main(String args[]) { Main jartool = new Main(System.out, System.err, "jar"); System.exit(jartool.run(args) ? 0 : 1); } } /* * an OutputStream that doesn't send its output anywhere, (but could). * It's here to find the CRC32 of a manifest, necessary for STORED only * mode in ZIP. */ final class CRC32OutputStream extends java.io.OutputStream { CRC32 crc; int n = 0; CRC32OutputStream(CRC32 crc) { this.crc = crc; } public void write(int r) throws IOException { crc.update(r); n++; } public void write(byte[] b) throws IOException { crc.update(b, 0, b.length); n += b.length; } public void write(byte[] b, int off, int len) throws IOException { crc.update(b, off, len); n += len - off; } }