// // Getdown - application installer, patcher and launcher // Copyright (C) 2004-2018 Getdown authors // https://github.com/threerings/getdown/blob/master/LICENSE package com.threerings.getdown.data; import java.io.*; import java.net.URL; import java.security.MessageDigest; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.jar.JarEntry; import java.util.jar.JarFile; import com.threerings.getdown.util.FileUtil; import com.threerings.getdown.util.ProgressObserver; import com.threerings.getdown.util.StringUtil; import static com.threerings.getdown.Log.log; /** * Models a single file resource used by an {@link Application}. */ public class Resource implements Comparable { /** Defines special attributes for resources. */ public static enum Attr { /** Indicates that the resource should be unpacked. */ UNPACK, /** If present, when unpacking a resource, any directories created by the newly * unpacked resource will first be cleared of files before unpacking. */ CLEAN, /** Indicates that the resource should be marked executable. */ EXEC, /** Indicates that the resource should be downloaded before a UI is displayed. */ PRELOAD, /** Indicates that the resource is a jar containing native libs. */ NATIVE }; public static final EnumSet NORMAL = EnumSet.noneOf(Attr.class); public static final EnumSet UNPACK = EnumSet.of(Attr.UNPACK); public static final EnumSet EXEC = EnumSet.of(Attr.EXEC); public static final EnumSet PRELOAD = EnumSet.of(Attr.PRELOAD); public static final EnumSet NATIVE = EnumSet.of(Attr.NATIVE); /** * Computes the MD5 hash of the supplied file. * @param version the version of the digest protocol to use. */ public static String computeDigest (int version, File target, MessageDigest md, ProgressObserver obs) throws IOException { md.reset(); byte[] buffer = new byte[DIGEST_BUFFER_SIZE]; int read; boolean isJar = isJar(target.getPath()); boolean isPacked200Jar = isPacked200Jar(target.getPath()); // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic // manner to properly correlate jardiff patched jars with their unpatched originals if (isJar || isPacked200Jar){ File tmpJarFile = null; JarFile jar = null; try { // if this is a compressed jar file, uncompress it to compute the jar file digest if (isPacked200Jar){ tmpJarFile = new File(target.getPath() + ".tmp"); FileUtil.unpackPacked200Jar(target, tmpJarFile); jar = new JarFile(tmpJarFile); } else{ jar = new JarFile(target); } List entries = Collections.list(jar.entries()); Collections.sort(entries, ENTRY_COMP); int eidx = 0; for (JarEntry entry : entries) { // old versions of the digest code skipped metadata if (version < 2) { if (entry.getName().startsWith("META-INF")) { updateProgress(obs, eidx, entries.size()); continue; } } try (InputStream in = jar.getInputStream(entry)) { while ((read = in.read(buffer)) != -1) { md.update(buffer, 0, read); } } updateProgress(obs, eidx, entries.size()); } } finally { if (jar != null) { try { jar.close(); } catch (IOException ioe) { log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe); } } if (tmpJarFile != null) { FileUtil.deleteHarder(tmpJarFile); } } } else { long totalSize = target.length(), position = 0L; try (FileInputStream fin = new FileInputStream(target)) { while ((read = fin.read(buffer)) != -1) { md.update(buffer, 0, read); position += read; updateProgress(obs, position, totalSize); } } } return StringUtil.hexlate(md.digest()); } /** * Creates a resource with the supplied remote URL and local path. */ public Resource (String path, URL remote, File local, EnumSet attrs) { _path = path; _remote = remote; _local = local; _localNew = new File(local.toString() + "_new"); String lpath = _local.getPath(); _marker = new File(lpath + "v"); _attrs = attrs; _isJar = isJar(lpath); _isPacked200Jar = isPacked200Jar(lpath); boolean unpack = attrs.contains(Attr.UNPACK); if (unpack && _isJar) { _unpacked = _local.getParentFile(); } else if(unpack && _isPacked200Jar) { String dotJar = ".jar", lname = _local.getName(); String uname = lname.substring(0, lname.lastIndexOf(dotJar) + dotJar.length()); _unpacked = new File(_local.getParent(), uname); } } /** * Returns the path associated with this resource. */ public String getPath () { return _path; } /** * Returns the local location of this resource. */ public File getLocal () { return _local; } /** * Returns the location of the to-be-installed new version of this resource. */ public File getLocalNew () { return _localNew; } /** * Returns the location of the unpacked resource. */ public File getUnpacked () { return _unpacked; } /** * Returns the final target of this resource, whether it has been unpacked or not. */ public File getFinalTarget () { return shouldUnpack() ? getUnpacked() : getLocal(); } /** * Returns the remote location of this resource. */ public URL getRemote () { return _remote; } /** * Returns true if this resource should be unpacked as a part of the validation process. */ public boolean shouldUnpack () { return _attrs.contains(Attr.UNPACK) && !SysProps.noUnpack(); } /** * Returns true if this resource should be pre-downloaded. */ public boolean shouldPredownload () { return _attrs.contains(Attr.PRELOAD); } /** * Returns true if this resource is a native lib jar. */ public boolean isNative () { return _attrs.contains(Attr.NATIVE); } /** * Computes the MD5 hash of this resource's underlying file. * Note: This is both CPU and I/O intensive. * @param version the version of the digest protocol to use. */ public String computeDigest (int version, MessageDigest md, ProgressObserver obs) throws IOException { File file; if (_local.toString().toLowerCase(Locale.ROOT).endsWith(Application.CONFIG_FILE)) { file = _local; } else { file = _localNew.exists() ? _localNew : _local; } return computeDigest(version, file, md, obs); } /** * Returns true if this resource has an associated "validated" marker * file. */ public boolean isMarkedValid () { if (!_local.exists()) { clearMarker(); return false; } return _marker.exists(); } /** * Creates a "validated" marker file for this resource to indicate * that its MD5 hash has been computed and compared with the value in * the digest file. * * @throws IOException if we fail to create the marker file. */ public void markAsValid () throws IOException { _marker.createNewFile(); } /** * Removes any "validated" marker file associated with this resource. */ public void clearMarker () { if (_marker.exists() && !FileUtil.deleteHarder(_marker)) { log.warning("Failed to erase marker file '" + _marker + "'."); } } /** * Installs the {@code getLocalNew} version of this resource to {@code getLocal}. * @param validate whether or not to mark the resource as valid after installing. */ public void install (boolean validate) throws IOException { File source = getLocalNew(), dest = getLocal(); log.info("- " + source); if (!FileUtil.renameTo(source, dest)) { throw new IOException("Failed to rename " + source + " to " + dest); } applyAttrs(); if (validate) { markAsValid(); } } /** * Unpacks this resource file into the directory that contains it. */ public void unpack () throws IOException { // sanity check if (!_isJar && !_isPacked200Jar) { throw new IOException("Requested to unpack non-jar file '" + _local + "'."); } if (_isJar) { try (JarFile jar = new JarFile(_local)) { FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN)); } } else { FileUtil.unpackPacked200Jar(_local, _unpacked); } } /** * Applies this resources special attributes: unpacks this resource if needed, marks it as * executable if needed. */ public void applyAttrs () throws IOException { if (shouldUnpack()) { unpack(); } if (_attrs.contains(Attr.EXEC)) { FileUtil.makeExecutable(_local); } } /** * Wipes this resource file along with any "validated" marker file that may be associated with * it. */ public void erase () { clearMarker(); if (_local.exists() && !FileUtil.deleteHarder(_local)) { log.warning("Failed to erase resource '" + _local + "'."); } } @Override public int compareTo (Resource other) { return _path.compareTo(other._path); } @Override public boolean equals (Object other) { if (other instanceof Resource) { return _path.equals(((Resource)other)._path); } else { return false; } } @Override public int hashCode () { return _path.hashCode(); } @Override public String toString () { return _path; } /** Helper function to simplify the process of reporting progress. */ protected static void updateProgress (ProgressObserver obs, long pos, long total) { if (obs != null) { obs.progress((int)(100 * pos / total)); } } protected static boolean isJar (String path) { return path.endsWith(".jar") || path.endsWith(".jar_new"); } protected static boolean isPacked200Jar (String path) { return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") || path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new"); } protected String _path; protected URL _remote; protected File _local, _localNew, _marker, _unpacked; protected EnumSet _attrs; protected boolean _isJar, _isPacked200Jar; /** Used to sort the entries in a jar file. */ protected static final Comparator ENTRY_COMP = new Comparator() { @Override public int compare (JarEntry e1, JarEntry e2) { return e1.getName().compareTo(e2.getName()); } }; protected static final int DIGEST_BUFFER_SIZE = 5 * 1025; }