/* * $Id$ * * Copyright (c) 2003 by Rodney Kinney * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.tools; import static VASSAL.tools.IterableEnumeration.iterate; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Enumeration; import java.util.Properties; import java.util.jar.JarOutputStream; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import VASSAL.tools.io.IOUtils; /** * Automatically builds a .jar file that will update a Zip archive. * Usage: java VASSAL.tools.ZipUpdater <oldArchiveName> <newArchiveName> * will create a file named update<oldArchiveName>.jar Executing this jar (by double-clicking or * typing "java -jar update<oldArchiveName>.jar") will update the old archive so that its contents are identical to * the new archive. * User: rkinney * Date: Oct 23, 2003 */ public class ZipUpdater implements Runnable { private static final Logger logger = LoggerFactory.getLogger(ZipUpdater.class); public static final String CHECKSUM_RESOURCE = "checksums"; public static final String TARGET_ARCHIVE = "target"; public static final String UPDATED_ARCHIVE_NAME = "finalName"; public static final String ENTRIES_DIR = "entries/"; private File oldFile; private ZipFile oldZipFile; private Properties checkSums; public ZipUpdater(File input) throws IOException { this.oldFile = input; if (!oldFile.exists()) { throw new IOException("Could not find file " + input.getPath()); } } private long getCrc(ZipFile file, ZipEntry entry) throws IOException { long crc = -1; if (entry != null) { crc = entry.getCrc(); if (crc < 0) { CRC32 checksum = new CRC32(); final InputStream in = file.getInputStream(entry); try { final byte[] buffer = new byte[1024]; int count; while ((count = in.read(buffer)) >= 0) { checksum.update(buffer, 0, count); } in.close(); crc = checksum.getValue(); } finally { IOUtils.closeQuietly(in); } } } return crc; } private long copyEntry(ZipOutputStream output, ZipEntry newEntry) throws IOException { return writeEntry(oldZipFile.getInputStream(new ZipEntry(newEntry.getName())), output, newEntry); } private long replaceEntry(ZipOutputStream output, ZipEntry newEntry) throws IOException { final InputStream newContents = getClass().getResourceAsStream("/" + ENTRIES_DIR + newEntry.getName()); if (newContents == null) { throw new IOException("This updater was created with an original that differs from the file you're trying to update.\nLocal entry does not match original: "+newEntry.getName()); } BufferedInputStream in = null; try { in = new BufferedInputStream(newContents); long cs = writeEntry(newContents, output, newEntry); in.close(); return cs; } finally { IOUtils.closeQuietly(in); } } private long writeEntry(InputStream zis, ZipOutputStream output, ZipEntry newEntry) throws IOException { // FIXME: is there a better way to do this, so that the whole input // stream isn't in memory at once? final byte[] contents = IOUtils.toByteArray(zis); final CRC32 checksum = new CRC32(); checksum.update(contents); if (newEntry.getMethod() == ZipEntry.STORED) { newEntry.setSize(contents.length); newEntry.setCrc(checksum.getValue()); } output.putNextEntry(newEntry); output.write(contents, 0, contents.length); return checksum.getValue(); } public void write(File destination) throws IOException { checkSums = new Properties(); final InputStream rin = ZipUpdater.class.getResourceAsStream("/" + CHECKSUM_RESOURCE); if (rin == null) throw new IOException("Resource not found: " + CHECKSUM_RESOURCE); BufferedInputStream in = null; try { in = new BufferedInputStream(rin); checkSums.load(in); in.close(); } finally { IOUtils.closeQuietly(in); } final File tempFile = File.createTempFile("VSL", ".zip"); oldZipFile = new ZipFile(oldFile.getPath()); try { final ZipOutputStream output = new ZipOutputStream(new FileOutputStream(tempFile)); try { // FIXME: reinstate when we move to 1.6+. // for (String entryName : checkSums.stringPropertyNames()) { for (Enumeration<Object> e = checkSums.keys(); e.hasMoreElements();) { final String entryName = (String) e.nextElement(); long targetSum; try { targetSum = Long.parseLong(checkSums.getProperty(entryName, "<none>")); } // FIXME: review error message catch (NumberFormatException invalid) { throw new IOException("Invalid checksum " + checkSums.getProperty(entryName, "<none>") + " for entry " + entryName); } final ZipEntry entry = oldZipFile.getEntry(entryName); final ZipEntry newEntry = new ZipEntry(entryName); newEntry.setMethod(entry != null ? entry.getMethod() : ZipEntry.DEFLATED); if (targetSum == getCrc(oldZipFile, entry)) { if (targetSum != copyEntry(output, newEntry)) { throw new IOException("Checksum mismatch for entry " + entry.getName()); } } else { if (targetSum != replaceEntry(output, newEntry)) { throw new IOException("Checksum mismatch for entry " + entry.getName()); } } } } finally { try { output.close(); } // FIXME: review error message catch (IOException e) { logger.error("", e); } } } finally { oldZipFile.close(); } if (destination.getName().equals(oldFile.getName())) { String updatedName = destination.getName(); int index = updatedName.lastIndexOf('.'); String backup = index < 0 || index == updatedName.length() - 1 ? updatedName + "Backup" : updatedName.substring(0, index) + "Backup" + updatedName.substring(index); if (!oldFile.renameTo(new File(backup))) { throw new IOException("Unable to create backup file " + backup + ".\nUpdated file is in " + tempFile.getPath()); } } if (!tempFile.renameTo(destination)) { throw new IOException("Unable to write to file " + destination.getPath()+ ".\nUpdated file is in " + tempFile.getPath()); } } public void createUpdater(File newFile) throws IOException { String inputArchiveName = oldFile.getName(); int index = inputArchiveName.indexOf('.'); String jarName; if (index >= 0) { jarName = "update" + inputArchiveName.substring(0, index) + ".jar"; } else { jarName = "update" + inputArchiveName; } createUpdater(newFile, new File(jarName)); } public void createUpdater(File newFile, File updaterFile) throws IOException { if (!updaterFile.getName().endsWith(".jar")) { final String newName = updaterFile.getName().replace('.','_')+".jar"; updaterFile = new File(updaterFile.getParentFile(),newName); } checkSums = new Properties(); try { oldZipFile = new ZipFile(oldFile); final String inputArchiveName = oldFile.getName(); final ZipFile goal = new ZipFile(newFile); try { JarOutputStream out = null; try { out = new JarOutputStream( new BufferedOutputStream(new FileOutputStream(updaterFile))); for (ZipEntry entry : iterate(goal.entries())) { final long goalCrc = getCrc(goal, entry); final long inputCrc = getCrc(oldZipFile, oldZipFile.getEntry(entry.getName())); if (goalCrc != inputCrc) { final ZipEntry outputEntry = new ZipEntry(ENTRIES_DIR + entry.getName()); outputEntry.setMethod(entry.getMethod()); InputStream gis = null; try { gis = new BufferedInputStream(goal.getInputStream(entry)); writeEntry(gis, out, outputEntry); gis.close(); } finally { IOUtils.closeQuietly(gis); } } checkSums.put(entry.getName(), goalCrc + ""); } final ZipEntry manifestEntry = new ZipEntry("META-INF/MANIFEST.MF"); manifestEntry.setMethod(ZipEntry.DEFLATED); final StringBuilder buffer = new StringBuilder(); buffer.append("Manifest-Version: 1.0\n") .append("Main-Class: VASSAL.tools.ZipUpdater\n"); writeEntry( new ByteArrayInputStream(buffer.toString().getBytes("UTF-8")), out, manifestEntry); final ZipEntry nameEntry = new ZipEntry(TARGET_ARCHIVE); nameEntry.setMethod(ZipEntry.DEFLATED); writeEntry( new ByteArrayInputStream(inputArchiveName.getBytes("UTF-8")), out, nameEntry); final ZipEntry updatedEntry = new ZipEntry(UPDATED_ARCHIVE_NAME); updatedEntry.setMethod(ZipEntry.DEFLATED); writeEntry( new ByteArrayInputStream(newFile.getName().getBytes("UTF-8")), out, updatedEntry); final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); checkSums.store(byteOut, null); final ZipEntry sumEntry = new ZipEntry(CHECKSUM_RESOURCE); sumEntry.setMethod(ZipEntry.DEFLATED); writeEntry( new ByteArrayInputStream(byteOut.toByteArray()), out, sumEntry); final String className = getClass().getName().replace('.', '/') + ".class"; final ZipEntry classEntry = new ZipEntry(className); classEntry.setMethod(ZipEntry.DEFLATED); final InputStream is = getClass().getResourceAsStream("/" + className); if (is == null) throw new IOException("Resource not found: " + className); BufferedInputStream in = null; try { in = new BufferedInputStream(is); writeEntry(is, out, classEntry); in.close(); } finally { IOUtils.closeQuietly(in); } out.close(); } finally { IOUtils.closeQuietly(out); } goal.close(); } finally { IOUtils.closeQuietly(goal); } oldZipFile.close(); } finally { IOUtils.closeQuietly(oldZipFile); } } private String fileName; private Exception error; private ZipUpdater(String fileName, Exception error) { this.fileName = fileName; this.error = error; } public void run() { JOptionPane.showMessageDialog(null, "Unable to update " + fileName + ".\n" + error.getMessage(), "Update failed", JOptionPane.ERROR_MESSAGE); System.exit(0); } public static void main(String[] args) { String oldArchiveName = "<unknown>"; try { if (args.length > 1) { oldArchiveName = args[0]; String goal = args[1]; ZipUpdater updater = new ZipUpdater(new File(oldArchiveName)); updater.createUpdater(new File(goal)); } else { BufferedReader r = null; try { r = new BufferedReader(new InputStreamReader( ZipUpdater.class.getResourceAsStream("/" + TARGET_ARCHIVE))); oldArchiveName = r.readLine(); r.close(); } finally { if (r != null) { try { r.close(); } catch (IOException e) { e.printStackTrace(); } } } try { r = new BufferedReader(new InputStreamReader( ZipUpdater.class.getResourceAsStream("/" + UPDATED_ARCHIVE_NAME))); final String newArchiveName = r.readLine(); final ZipUpdater updater = new ZipUpdater(new File(oldArchiveName)); updater.write(new File(newArchiveName)); r.close(); } finally { if (r != null) { try { r.close(); } catch (IOException e) { e.printStackTrace(); } } } } } catch (final IOException e) { e.printStackTrace(); try { SwingUtilities.invokeAndWait(new ZipUpdater(oldArchiveName,e)); } catch (Exception e1) { e1.printStackTrace(); } } } }