1 // 2 // Getdown - application installer, patcher and launcher 3 // Copyright (C) 2004-2018 Getdown authors 4 // https://github.com/threerings/getdown/blob/master/LICENSE 5 6 package com.threerings.getdown.data; 7 8 import java.io.*; 9 import java.net.URL; 10 import java.nio.file.Files; 11 import java.nio.file.Paths; 12 import java.security.MessageDigest; 13 import java.util.Collections; 14 import java.util.Comparator; 15 import java.util.EnumSet; 16 import java.util.List; 17 import java.util.Locale; 18 import java.util.jar.JarEntry; 19 import java.util.jar.JarFile; 20 21 import com.threerings.getdown.util.FileUtil; 22 import com.threerings.getdown.util.ProgressObserver; 23 import com.threerings.getdown.util.StringUtil; 24 25 import static com.threerings.getdown.Log.log; 26 27 /** 28 * Models a single file resource used by an {@link Application}. 29 */ 30 public class Resource implements Comparable<Resource> 31 { 32 /** Defines special attributes for resources. */ 33 public static enum Attr { 34 /** Indicates that the resource should be unpacked. */ 35 UNPACK, 36 /** If present, when unpacking a resource, any directories created by the newly 37 * unpacked resource will first be cleared of files before unpacking. */ 38 CLEAN, 39 /** Indicates that the resource should be marked executable. */ 40 EXEC, 41 /** Indicates that the resource should be downloaded before a UI is displayed. */ 42 PRELOAD, 43 /** Indicates that the resource is a jar containing native libs. */ 44 NATIVE 45 }; 46 47 public static final EnumSet<Attr> NORMAL = EnumSet.noneOf(Attr.class); 48 public static final EnumSet<Attr> UNPACK = EnumSet.of(Attr.UNPACK); 49 public static final EnumSet<Attr> EXEC = EnumSet.of(Attr.EXEC); 50 public static final EnumSet<Attr> PRELOAD = EnumSet.of(Attr.PRELOAD); 51 public static final EnumSet<Attr> NATIVE = EnumSet.of(Attr.NATIVE); 52 53 /** 54 * Computes the MD5 hash of the supplied file. 55 * @param version the version of the digest protocol to use. 56 */ computeDigest(int version, File target, MessageDigest md, ProgressObserver obs)57 public static String computeDigest (int version, File target, MessageDigest md, 58 ProgressObserver obs) 59 throws IOException 60 { 61 md.reset(); 62 byte[] buffer = new byte[DIGEST_BUFFER_SIZE]; 63 int read; 64 65 boolean isJar = isJar(target.getPath()); 66 boolean isPacked200Jar = isPacked200Jar(target.getPath()); 67 68 // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic 69 // manner to properly correlate jardiff patched jars with their unpatched originals 70 if (isJar || isPacked200Jar){ 71 File tmpJarFile = null; 72 JarFile jar = null; 73 try { 74 // if this is a compressed jar file, uncompress it to compute the jar file digest 75 if (isPacked200Jar){ 76 tmpJarFile = new File(target.getPath() + ".tmp"); 77 FileUtil.unpackPacked200Jar(target, tmpJarFile); 78 jar = new JarFile(tmpJarFile); 79 } else{ 80 jar = new JarFile(target); 81 } 82 83 List<JarEntry> entries = Collections.list(jar.entries()); 84 Collections.sort(entries, ENTRY_COMP); 85 86 int eidx = 0; 87 for (JarEntry entry : entries) { 88 // old versions of the digest code skipped metadata 89 if (version < 2) { 90 if (entry.getName().startsWith("META-INF")) { 91 updateProgress(obs, eidx, entries.size()); 92 continue; 93 } 94 } 95 96 try (InputStream in = jar.getInputStream(entry)) { 97 while ((read = in.read(buffer)) != -1) { 98 md.update(buffer, 0, read); 99 } 100 } 101 102 updateProgress(obs, eidx, entries.size()); 103 } 104 105 } finally { 106 if (jar != null) { 107 try { 108 jar.close(); 109 } catch (IOException ioe) { 110 log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe); 111 } 112 } 113 if (tmpJarFile != null) { 114 FileUtil.deleteHarder(tmpJarFile); 115 } 116 } 117 118 } else { 119 long totalSize = target.length(), position = 0L; 120 try (FileInputStream fin = new FileInputStream(target)) { 121 while ((read = fin.read(buffer)) != -1) { 122 md.update(buffer, 0, read); 123 position += read; 124 updateProgress(obs, position, totalSize); 125 } 126 } 127 } 128 return StringUtil.hexlate(md.digest()); 129 } 130 131 /** 132 * Creates a resource with the supplied remote URL and local path. 133 */ Resource(String path, URL remote, File local, EnumSet<Attr> attrs)134 public Resource (String path, URL remote, File local, EnumSet<Attr> attrs) 135 { 136 _path = path; 137 _remote = remote; 138 _local = local; 139 _localNew = new File(local.toString() + "_new"); 140 String lpath = _local.getPath(); 141 _marker = new File(lpath + "v"); 142 143 _attrs = attrs; 144 _isJar = isJar(lpath); 145 _isPacked200Jar = isPacked200Jar(lpath); 146 boolean unpack = attrs.contains(Attr.UNPACK); 147 if (unpack && _isJar) { 148 _unpacked = _local.getParentFile(); 149 } else if(unpack && _isPacked200Jar) { 150 String dotJar = ".jar", lname = _local.getName(); 151 String uname = lname.substring(0, lname.lastIndexOf(dotJar) + dotJar.length()); 152 _unpacked = new File(_local.getParent(), uname); 153 } 154 } 155 156 /** 157 * Returns the path associated with this resource. 158 */ getPath()159 public String getPath () 160 { 161 return _path; 162 } 163 164 /** 165 * Returns the local location of this resource. 166 */ getLocal()167 public File getLocal () 168 { 169 return _local; 170 } 171 172 /** 173 * Returns the location of the to-be-installed new version of this resource. 174 */ getLocalNew()175 public File getLocalNew () 176 { 177 return _localNew; 178 } 179 180 /** 181 * Returns the location of the unpacked resource. 182 */ getUnpacked()183 public File getUnpacked () 184 { 185 return _unpacked; 186 } 187 188 /** 189 * Returns the final target of this resource, whether it has been unpacked or not. 190 */ getFinalTarget()191 public File getFinalTarget () 192 { 193 return shouldUnpack() ? getUnpacked() : getLocal(); 194 } 195 196 /** 197 * Returns the remote location of this resource. 198 */ getRemote()199 public URL getRemote () 200 { 201 return _remote; 202 } 203 204 /** 205 * Returns true if this resource should be unpacked as a part of the validation process. 206 */ shouldUnpack()207 public boolean shouldUnpack () 208 { 209 return _attrs.contains(Attr.UNPACK) && !SysProps.noUnpack(); 210 } 211 212 /** 213 * Returns true if this resource should be pre-downloaded. 214 */ shouldPredownload()215 public boolean shouldPredownload () 216 { 217 return _attrs.contains(Attr.PRELOAD); 218 } 219 220 /** 221 * Returns true if this resource is a native lib jar. 222 */ isNative()223 public boolean isNative () 224 { 225 return _attrs.contains(Attr.NATIVE); 226 } 227 228 /** 229 * Computes the MD5 hash of this resource's underlying file. 230 * <em>Note:</em> This is both CPU and I/O intensive. 231 * @param version the version of the digest protocol to use. 232 */ computeDigest(int version, MessageDigest md, ProgressObserver obs)233 public String computeDigest (int version, MessageDigest md, ProgressObserver obs) 234 throws IOException 235 { 236 File file; 237 if (_local.toString().toLowerCase(Locale.ROOT).endsWith(Application.CONFIG_FILE)) { 238 file = _local; 239 } else { 240 file = _localNew.exists() ? _localNew : _local; 241 } 242 return computeDigest(version, file, md, obs); 243 } 244 245 /** 246 * Returns true if this resource has an associated "validated" marker 247 * file. 248 */ isMarkedValid()249 public boolean isMarkedValid () 250 { 251 if (!_local.exists()) { 252 clearMarker(); 253 return false; 254 } 255 return _marker.exists(); 256 } 257 258 /** 259 * Creates a "validated" marker file for this resource to indicate 260 * that its MD5 hash has been computed and compared with the value in 261 * the digest file. 262 * 263 * @throws IOException if we fail to create the marker file. 264 */ markAsValid()265 public void markAsValid () 266 throws IOException 267 { 268 _marker.createNewFile(); 269 } 270 271 /** 272 * Removes any "validated" marker file associated with this resource. 273 */ clearMarker()274 public void clearMarker () 275 { 276 if (_marker.exists() && !FileUtil.deleteHarder(_marker)) { 277 log.warning("Failed to erase marker file '" + _marker + "'."); 278 } 279 } 280 281 /** 282 * Installs the {@code getLocalNew} version of this resource to {@code getLocal}. 283 * @param validate whether or not to mark the resource as valid after installing. 284 */ install(boolean validate)285 public void install (boolean validate) throws IOException { 286 File source = getLocalNew(), dest = getLocal(); 287 log.info("- " + source); 288 if (!FileUtil.renameTo(source, dest)) { 289 throw new IOException("Failed to rename " + source + " to " + dest); 290 } 291 applyAttrs(); 292 if (validate) { 293 markAsValid(); 294 } 295 } 296 297 /** 298 * Unpacks this resource file into the directory that contains it. 299 */ unpack()300 public void unpack () throws IOException 301 { 302 // sanity check 303 if (!_isJar && !_isPacked200Jar) { 304 throw new IOException("Requested to unpack non-jar file '" + _local + "'."); 305 } 306 if (_isJar) { 307 try (JarFile jar = new JarFile(_local)) { 308 FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN)); 309 } 310 } else { 311 FileUtil.unpackPacked200Jar(_local, _unpacked); 312 } 313 } 314 315 /** 316 * Applies this resources special attributes: unpacks this resource if needed, marks it as 317 * executable if needed. 318 */ applyAttrs()319 public void applyAttrs () throws IOException { 320 if (shouldUnpack()) { 321 unpack(); 322 } 323 if (_attrs.contains(Attr.EXEC)) { 324 FileUtil.makeExecutable(_local); 325 } 326 } 327 328 /** 329 * Wipes this resource file along with any "validated" marker file that may be associated with 330 * it. 331 */ erase()332 public void erase () 333 { 334 clearMarker(); 335 if (_local.exists() && !FileUtil.deleteHarder(_local)) { 336 log.warning("Failed to erase resource '" + _local + "'."); 337 } 338 } 339 compareTo(Resource other)340 @Override public int compareTo (Resource other) { 341 return _path.compareTo(other._path); 342 } 343 equals(Object other)344 @Override public boolean equals (Object other) 345 { 346 if (other instanceof Resource) { 347 return _path.equals(((Resource)other)._path); 348 } else { 349 return false; 350 } 351 } 352 hashCode()353 @Override public int hashCode () 354 { 355 return _path.hashCode(); 356 } 357 toString()358 @Override public String toString () 359 { 360 return _path; 361 } 362 363 /** Helper function to simplify the process of reporting progress. */ updateProgress(ProgressObserver obs, long pos, long total)364 protected static void updateProgress (ProgressObserver obs, long pos, long total) 365 { 366 if (obs != null) { 367 obs.progress((int)(100 * pos / total)); 368 } 369 } 370 isJar(String path)371 protected static boolean isJar (String path) 372 { 373 return path.endsWith(".jar") || path.endsWith(".jar_new"); 374 } 375 isPacked200Jar(String path)376 protected static boolean isPacked200Jar (String path) 377 { 378 return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") || 379 path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new"); 380 } 381 382 protected String _path; 383 protected URL _remote; 384 protected File _local, _localNew, _marker, _unpacked; 385 protected EnumSet<Attr> _attrs; 386 protected boolean _isJar, _isPacked200Jar; 387 388 /** Used to sort the entries in a jar file. */ 389 protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() { 390 @Override public int compare (JarEntry e1, JarEntry e2) { 391 return e1.getName().compareTo(e2.getName()); 392 } 393 }; 394 395 protected static final int DIGEST_BUFFER_SIZE = 5 * 1025; 396 } 397