Split Getdown into multiple (Maven) modules.

- core: the main Getdown logic code
- tools: code to create digest.txt & patch files
- launcher: the standalone launcher/updater app
- ant: the Ant task for creating digest.txt files

This paves the way for a proper Jigsaw-ification of the Getdown code.

I may further factor code out of getdown-launcher and into getdown-core to
enable the use-case where an app embeds Getdown completely and does not use the
launcher app/UI. That will also make it easier to create a JavaFX UI and retire
the old Swing UI.

This also moves the obsolete applet code into a separate applet module, which
is merely a temporary holding area. It will be deleted in the next commit.
This commit is contained in:
Michael Bayne
2018-09-04 09:11:29 -07:00
parent 8ff9bf4d68
commit 4c383f99c7
63 changed files with 386 additions and 264 deletions
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.threerings</groupId>
<artifactId>getdown</artifactId>
<version>1.7.2-SNAPSHOT</version>
</parent>
<artifactId>getdown-core</artifactId>
<packaging>jar</packaging>
<name>Getdown Core</name>
<description>Core Getdown functionality</description>
<dependencies>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>samskivert</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource> <!-- include the LICENSE file in the jar -->
<directory>..</directory>
<includes><include>LICENSE</include></includes>
</resource>
</resources>
</build>
</project>
@@ -0,0 +1,17 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown;
import com.samskivert.util.Logger;
/**
* A placeholder class that contains a reference to the log object used by the Getdown code.
*/
public class Log
{
/** We dispatch our log messages through this logger. */
public static final Logger log = Logger.getLogger("com.threerings.getdown");
}
@@ -0,0 +1,71 @@
package com.threerings.getdown.classpath;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Represents the class path and it's elements of the application to be launched. The class path can
* either be represented as an {@link #asArgumentString() argument string} for the java command line
* or as an {@link #asUrls() array of URLs} to be used by a {@link URLClassLoader}.
*/
public class ClassPath
{
public ClassPath (LinkedHashSet<File> classPathEntries)
{
_classPathEntries = Collections.unmodifiableSet(classPathEntries);
}
/**
* Returns the class path as an java command line argument string, e.g.
*
* <pre>
* /path/to/a.jar:/path/to/b.jar
* </pre>
*/
public String asArgumentString ()
{
StringBuilder builder = new StringBuilder();
String delimiter = "";
for (File entry: _classPathEntries) {
builder.append(delimiter).append(entry.getAbsolutePath());
delimiter = File.pathSeparator;
}
return builder.toString();
}
/**
* Returns the class path entries as an array of URLs to be used for example by an
* {@link URLClassLoader}.
*/
public URL[] asUrls ()
{
URL[] urls = new URL[_classPathEntries.size()];
int i = 0;
for (File entry : _classPathEntries) {
urls[i++] = getURL(entry);
}
return urls;
}
public Set<File> getClassPathEntries ()
{
return _classPathEntries;
}
private static URL getURL (File file)
{
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new IllegalStateException("URL of file is illegal: " + file.getAbsolutePath(), e);
}
}
private final Set<File> _classPathEntries;
}
@@ -0,0 +1,76 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.classpath;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.concurrent.TimeUnit;
import com.threerings.getdown.classpath.cache.GarbageCollector;
import com.threerings.getdown.classpath.cache.ResourceCache;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Resource;
public class ClassPaths
{
static final String CACHE_DIR = ".cache";
/**
* Builds either a default or cached classpath based on {@code app}'s configuration.
*/
public static ClassPath buildClassPath (Application app) throws IOException
{
return app.useCodeCache() ? buildCachedClassPath(app) : buildDefaultClassPath(app);
}
/**
* Builds a {@link ClassPath} instance for {@code app} using the code resources in place in
* the app directory.
*/
public static ClassPath buildDefaultClassPath (Application app)
{
LinkedHashSet<File> classPathEntries = new LinkedHashSet<File>();
for (Resource resource: app.getActiveCodeResources()) {
classPathEntries.add(resource.getFinalTarget());
}
return new ClassPath(classPathEntries);
}
/**
* Builds a {@link ClassPath} instance for {@code app} by first copying the code resources into
* a cache directory and then referencing them from there. This avoids problems with
* overwriting in-use classpath elements when the application is later updated. This also
* "garbage collects" expired caches if necessary.
*/
public static ClassPath buildCachedClassPath (Application app) throws IOException
{
File cacheDir = new File(app.getAppdir(), CACHE_DIR);
// a negative value of code_cache_retention_days allows to clean up the cache forcefully
if (app.getCodeCacheRetentionDays() <= 0) {
runGarbageCollection(app, cacheDir);
}
ResourceCache cache = new ResourceCache(cacheDir);
LinkedHashSet<File> classPathEntries = new LinkedHashSet<File>();
for (Resource resource: app.getActiveCodeResources()) {
File entry = cache.cacheFile(resource.getFinalTarget(), app.getDigest(resource));
classPathEntries.add(entry);
}
if (app.getCodeCacheRetentionDays() > 0) {
runGarbageCollection(app, cacheDir);
}
return new ClassPath(classPathEntries);
}
private static void runGarbageCollection (Application app, File cacheDir)
{
long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
GarbageCollector.collect(cacheDir, retainMillis);
}
}
@@ -0,0 +1,64 @@
package com.threerings.getdown.classpath.cache;
import java.io.File;
import com.threerings.getdown.util.FileUtil;
/**
* Collects elements in the {@link ResourceCache cache} which became unused and deletes them
* afterwards.
*/
public class GarbageCollector
{
/**
* Collect and delete the garbage in the cache.
*/
public static void collect (File cacheDir, final long retentionPeriodMillis)
{
FileUtil.walkTree(cacheDir, new FileUtil.Visitor() {
@Override public void visit (File file) {
File cachedFile = getCachedFile(file);
File lastAccessedFile = getLastAccessedFile(file);
if (!cachedFile.exists() || !lastAccessedFile.exists()) {
if (cachedFile.exists()) {
FileUtil.deleteHarder(cachedFile);
} else {
FileUtil.deleteHarder(lastAccessedFile);
}
} else if (shouldDelete(lastAccessedFile, retentionPeriodMillis)) {
FileUtil.deleteHarder(lastAccessedFile);
FileUtil.deleteHarder(cachedFile);
}
File folder = file.getParentFile();
if (folder != null) {
String[] children = folder.list();
if (children != null && children.length == 0) {
FileUtil.deleteHarder(folder);
}
}
}
});
}
private static boolean shouldDelete (File lastAccessedFile, long retentionMillis)
{
return System.currentTimeMillis() - lastAccessedFile.lastModified() > retentionMillis;
}
private static File getLastAccessedFile (File file)
{
return isLastAccessedFile(file) ? file : new File(
file.getParentFile(), file.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
}
private static boolean isLastAccessedFile (File file)
{
return file.getName().endsWith(ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
}
private static File getCachedFile (File file)
{
return !isLastAccessedFile(file) ? file : new File(
file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf(".")));
}
}
@@ -0,0 +1,73 @@
package com.threerings.getdown.classpath.cache;
import java.io.File;
import java.io.IOException;
import com.threerings.getdown.util.FileUtil;
/**
* Maintains a cache of code resources. The cache allows multiple application instances of different
* versions to open at the same time.
*/
public class ResourceCache
{
public ResourceCache (File _cacheDir) throws IOException
{
this._cacheDir = _cacheDir;
createDirectoryIfNecessary(_cacheDir);
}
private void createDirectoryIfNecessary (File dir) throws IOException
{
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("unable to create directory: " + dir.getAbsolutePath());
}
}
/**
* Caches the given file under it's {@code digest}.
*
* @return the cached file
*/
public File cacheFile (File fileToCache, String digest) throws IOException
{
File cacheLocation = new File(_cacheDir, digest.substring(0, 2));
createDirectoryIfNecessary(cacheLocation);
File cachedFile = new File(cacheLocation, digest + getFileSuffix(fileToCache));
File lastAccessedFile = new File(
cacheLocation, cachedFile.getName() + LAST_ACCESSED_FILE_SUFFIX);
if (!cachedFile.exists()) {
createNewFile(cachedFile);
FileUtil.copy(fileToCache, cachedFile);
}
if (lastAccessedFile.exists()) {
lastAccessedFile.setLastModified(System.currentTimeMillis());
} else {
createNewFile(lastAccessedFile);
}
return cachedFile;
}
private void createNewFile (File fileToCreate) throws IOException
{
if (!fileToCreate.exists() && !fileToCreate.createNewFile()) {
throw new IOException("unable to create new file: " + fileToCreate.getAbsolutePath());
}
}
private String getFileSuffix (File fileToCache) {
String fileName = fileToCache.getName();
int index = fileName.lastIndexOf(".");
return index > -1 ? fileName.substring(index) : "";
}
private final File _cacheDir;
static final String LAST_ACCESSED_FILE_SUFFIX = ".lastAccessed";
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,230 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.*;
import com.samskivert.text.MessageUtil;
import com.samskivert.util.StringUtil;
import com.threerings.getdown.util.Config;
import com.threerings.getdown.util.ProgressObserver;
import static com.threerings.getdown.Log.log;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Manages the <code>digest.txt</code> file and the computing and processing of digests for an
* application.
*/
public class Digest
{
/** The current version of the digest protocol. */
public static final int VERSION = 2;
/**
* Returns the name of the digest file for the specified protocol version.
*/
public static String digestFile (int version) {
String infix = version > 1 ? String.valueOf(version) : "";
return FILE_NAME + infix + FILE_SUFFIX;
}
/**
* Returns the crypto algorithm used to sign digest files of the specified version.
*/
public static String sigAlgorithm (int version) {
switch (version) {
case 1: return "SHA1withRSA";
case 2: return "SHA256withRSA";
default: throw new IllegalArgumentException("Invalid digest version " + version);
}
}
/**
* Creates a digest file at the specified location using the supplied list of resources.
* @param version the version of the digest protocol to use.
*/
public static void createDigest (int version, List<Resource> resources, File output)
throws IOException
{
// first compute the digests for all the resources in parallel
ExecutorService exec = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
final Map<Resource, String> digests = new ConcurrentHashMap<>();
final BlockingQueue<Object> completed = new LinkedBlockingQueue<>();
final int fversion = version;
long start = System.currentTimeMillis();
Set<Resource> pending = new HashSet<>(resources);
for (final Resource rsrc : resources) {
exec.execute(new Runnable() {
public void run () {
try {
MessageDigest md = getMessageDigest(fversion);
digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
completed.add(rsrc);
} catch (Throwable t) {
completed.add(new IOException("Error computing digest for: " + rsrc).
initCause(t));
}
}
});
}
// queue a shutdown of the thread pool when the tasks are done
exec.shutdown();
try {
while (pending.size() > 0) {
Object done = completed.poll(600, TimeUnit.SECONDS);
if (done instanceof IOException) {
throw (IOException)done;
} else if (done instanceof Resource) {
pending.remove((Resource)done);
} else {
throw new AssertionError("What is this? " + done);
}
}
} catch (InterruptedException ie) {
throw new IOException("Timeout computing digests. Wow.");
}
StringBuilder data = new StringBuilder();
try (FileOutputStream fos = new FileOutputStream(output);
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
PrintWriter pout = new PrintWriter(osw)) {
// compute and append the digest of each resource in the list
for (Resource rsrc : resources) {
String path = rsrc.getPath();
String digest = digests.get(rsrc);
note(data, path, digest);
pout.println(path + " = " + digest);
}
// finally compute and append the digest for the file contents
MessageDigest md = getMessageDigest(version);
byte[] contents = data.toString().getBytes(UTF_8);
String filename = digestFile(version);
pout.println(filename + " = " + StringUtil.hexlate(md.digest(contents)));
}
long elapsed = System.currentTimeMillis() - start;
log.debug("Computed digests [rsrcs=" + resources.size() + ", time=" + elapsed + "ms]");
}
/**
* Obtains an appropriate message digest instance for use by the Getdown system.
*/
public static MessageDigest getMessageDigest (int version)
{
String algo = version > 1 ? "SHA-256" : "MD5";
try {
return MessageDigest.getInstance(algo);
} catch (NoSuchAlgorithmException nsae) {
throw new RuntimeException("JVM does not support " + algo + ". Gurp!");
}
}
/**
* Creates a digest instance which will parse and validate the digest in the supplied
* application directory, using the current digest version.
*/
public Digest (File appdir, boolean strictComments) throws IOException {
this(appdir, VERSION, strictComments);
}
/**
* Creates a digest instance which will parse and validate the digest in the supplied
* application directory.
* @param version the version of the digest protocol to use.
*/
public Digest (File appdir, int version, boolean strictComments) throws IOException
{
// parse and validate our digest file contents
String filename = digestFile(version);
StringBuilder data = new StringBuilder();
File dfile = new File(appdir, filename);
Config.ParseOpts opts = Config.createOpts(false);
opts.strictComments = strictComments;
// bias = toward key: the key is the filename and could conceivably contain = signs, value
// is the hex encoded hash which will not contain =
opts.biasToKey = true;
for (String[] pair : Config.parsePairs(dfile, opts)) {
if (pair[0].equals(filename)) {
_metaDigest = pair[1];
break;
}
_digests.put(pair[0], pair[1]);
note(data, pair[0], pair[1]);
}
// we've reached the end, validate our contents
MessageDigest md = getMessageDigest(version);
byte[] contents = data.toString().getBytes(UTF_8);
String hash = StringUtil.hexlate(md.digest(contents));
if (!hash.equals(_metaDigest)) {
String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, hash);
throw new IOException(err);
}
}
/**
* Returns the digest for the digest file.
*/
public String getMetaDigest ()
{
return _metaDigest;
}
/**
* Computes the hash of the specified resource and compares it with the value parsed from
* the digest file. Logs a message if the resource fails validation.
*
* @return true if the resource is valid, false if it failed the digest check or if an I/O
* error was encountered during the validation process.
*/
public boolean validateResource (Resource resource, ProgressObserver obs)
{
try {
String chash = resource.computeDigest(VERSION, getMessageDigest(VERSION), obs);
String ehash = _digests.get(resource.getPath());
if (chash.equals(ehash)) {
return true;
}
log.info("Resource failed digest check",
"rsrc", resource, "computed", chash, "expected", ehash);
} catch (Throwable t) {
log.info("Resource failed digest check", "rsrc", resource, "error", t);
}
return false;
}
/**
* Returns the digest of the given {@code resource}.
*/
public String getDigest (Resource resource)
{
return _digests.get(resource.getPath());
}
/** Used by {@link #createDigest} and {@link Digest}. */
protected static void note (StringBuilder data, String path, String digest)
{
data.append(path).append(" = ").append(digest).append("\n");
}
protected HashMap<String, String> _digests = new HashMap<>();
protected String _metaDigest = "";
protected static final String FILE_NAME = "digest";
protected static final String FILE_SUFFIX = ".txt";
}
@@ -0,0 +1,19 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
/**
* System property constants associated with Getdown.
*/
public class Properties
{
/** This property will be set to "true" on the application when it is being run by getdown. */
public static final String GETDOWN = "com.threerings.getdown";
/** If accepting connections from the launched application, this property
* will be set to the connection server port. */
public static final String CONNECT_PORT = "com.threerings.getdown.connectPort";
}
@@ -0,0 +1,367 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 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.samskivert.util.StringUtil;
import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.ProgressObserver;
import static com.threerings.getdown.Log.log;
/**
* Models a single file resource used by an {@link Application}.
*/
public class Resource implements Comparable<Resource>
{
/** Defines special attributes for resources. */
public static enum Attr {
/** Indicates that the resource should be unpacked. */
UNPACK,
/** Indicates that the resource should be marked executable. */
EXEC
};
public static final EnumSet<Attr> NORMAL = EnumSet.noneOf(Attr.class);
public static final EnumSet<Attr> UNPACK = EnumSet.of(Attr.UNPACK);
public static final EnumSet<Attr> EXEC = EnumSet.of(Attr.EXEC);
/**
* 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<JarEntry> 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<Attr> 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();
}
/**
* Computes the MD5 hash of this resource's underlying file.
* <em>Note:</em> 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}.
*/
public void install () 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();
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);
}
} 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<Attr> _attrs;
protected boolean _isJar, _isPacked200Jar;
/** Used to sort the entries in a jar file. */
protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() {
@Override public int compare (JarEntry e1, JarEntry e2) {
return e1.getName().compareTo(e2.getName());
}
};
protected static final int DIGEST_BUFFER_SIZE = 5 * 1025;
}
@@ -0,0 +1,166 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.threerings.getdown.util.VersionUtil;
/**
* This class encapsulates all system properties that are read and processed by Getdown. Don't
* stick a call to {@code System.getProperty} randomly into the code, put it in here and give it an
* accessor so that it's easy to see all of the secret system property arguments that Getdown makes
* use of.
*/
public class SysProps
{
/** Configures the appdir (in lieu of passing it in argv). Usage: {@code -Dappdir=foo}. */
public static String appDir () {
return System.getProperty("appdir");
}
/** Configures the appid (in lieu of passing it in argv). Usage: {@code -Dappid=foo}. */
public static String appId () {
return System.getProperty("appid");
}
/** Configures the appbase (in lieu of providing a skeleton getdown.txt, and as a last resort
* fallback). Usage: {@code -Dappbase=URL}. */
public static String appBase () {
return System.getProperty("appbase");
}
/** If true, disables redirection of logging into {@code launcher.log}.
* Usage: {@code -Dno_log_redir}. */
public static boolean noLogRedir () {
return System.getProperty("no_log_redir") != null;
}
/** Overrides the domain on {@code appbase}. Usage: {@code -Dappbase_domain=foo}. */
public static String appbaseDomain () {
return System.getProperty("appbase_domain");
}
/** Overrides enter {@code appbase}. Usage: {@code -Dappbase_override=URL}. */
public static String appbaseOverride () {
return System.getProperty("appbase_override");
}
/** If true, Getdown installs the app without ever bringing up a UI, except in the event of an
* error. NOTE: it does not launch the app. See {@link #launchInSilent}.
* Usage: {@code -Dsilent}. */
public static boolean silent () {
return System.getProperty("silent") != null;
}
/** If true, Getdown does not automatically install updates after downloading them. It waits
* for the application to call `Getdown.install`.
* Usage: {@code -Dno_install}. */
public static boolean noInstall () {
return System.getProperty("no_install") != null;
}
/** If true, Getdown installs the app without ever bringing up a UI and then launches it.
* Usage: {@code -Dsilent=launch}. */
public static boolean launchInSilent () {
return "launch".equals(System.getProperty("silent"));
}
/** Specifies the a delay (in minutes) to wait before starting the update and install process.
* Usage: {@code -Ddelay=N}. */
public static int startDelay () {
return Integer.getInteger("delay", 0);
}
/** If true, Getdown will not unpack {@code uresource} jars. Usage: {@code -Dno_unpack}. */
public static boolean noUnpack () {
return Boolean.getBoolean("no_unpack");
}
/** If true, Getdown will run the application in the same VM in which Getdown is running. If
* false (the default), Getdown will fork a new VM. Note that reusing the same VM prevents
* Getdown from configuring some launch-time-only VM parameters (like -mxN etc.).
* Usage: {@code -Ddirect}. */
public static boolean direct () {
return Boolean.getBoolean("direct");
}
/** Specifies the connection timeout (in seconds) to use when downloading control files from
* the server. This is chiefly useful when you are running in versionless mode and want Getdown
* to more quickly timeout its startup update check if the server with which it is
* communicating is not available. Usage: {@code -Dconnect_timeout=N}. */
public static int connectTimeout () {
return Integer.getInteger("connect_timeout", 0);
}
/** Specifies the read timeout (in seconds) to use when downloading all files from the server.
* The default is 30 seconds, meaning that if a download stalls for more than 30 seconds, the
* update process wil fail. Setting the timeout to zero (or a negative value) will disable it.
* Usage: {@code -Dread_timeout=N}. */
public static int readTimeout () {
return Integer.getInteger("read_timeout", 30);
}
/** Parses a Java version system property using the supplied regular expression. The numbers
* extracted from the regexp will be placed in each consecutive hundreds position in the
* returned value.
*
* <p>For example, {@code java.version} takes the form {@code 1.8.0_31}, and with the regexp
* {@code (\d+)\.(\d+)\.(\d+)(_\d+)?} we would parse {@code 1, 8, 0, 31} and combine them into
* the final value {@code 1080031}.
*
* <p>Note that non-numeric characters matched by the regular expression will simply be
* ignored, and optional groups which do not match are treated as zero in the final version
* calculation.
*
* <p>One can instead parse {@code java.runtime.version} which takes the form {@code
* 1.8.0_31-b13}. Using regexp {@code (\d+)\.(\d+)\.(\d+)_(\d+)-b(\d+)} we would parse
* {@code 1, 8, 0, 31, 13} and combine them into a final value {@code 108003113}.
*
* <p>Other (or future) JVMs may provide different version properties which can be parsed as
* desired using this general scheme as long as the numbers appear from left to right in order
* of significance.
*
* @throws IllegalArgumentException if no system named {@code propName} exists, or if
* {@code propRegex} does not match the returned version string.
*/
public static long parseJavaVersion (String propName, String propRegex) {
String verstr = System.getProperty(propName);
if (verstr == null) throw new IllegalArgumentException(
"No system property '" + propName + "'.");
long vers = VersionUtil.parseJavaVersion(propRegex, verstr);
if (vers == 0L) throw new IllegalArgumentException(
"Regexp '" + propRegex + "' does not match '" + verstr + "' (from " + propName + ")");
return vers;
}
/**
* Applies {@code appbase_override} or {@code appbase_domain} if they are set.
*/
public static String overrideAppbase (String appbase) {
String appbaseOverride = appbaseOverride();
if (appbaseOverride != null) {
return appbaseOverride;
} else {
return replaceDomain(appbase);
}
}
/**
* If appbase_domain property is set, replace the domain on the provided string.
*/
public static String replaceDomain (String appbase)
{
String appbaseDomain = appbaseDomain();
if (appbaseDomain != null) {
Matcher m = Pattern.compile("(https?://[^/]+)(.*)").matcher(appbase);
appbase = m.replaceAll(appbaseDomain + "$2");
}
return appbase;
}
}
@@ -0,0 +1,15 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.net;
import java.io.IOException;
/**
* Used to terminate the download process in its midst.
*/
public class DownloadAbortedException extends IOException
{
}
@@ -0,0 +1,263 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.net;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import com.threerings.getdown.data.Resource;
import static com.threerings.getdown.Log.log;
/**
* Handles the download of a collection of files, first issuing HTTP head requests to obtain size
* information and then downloading the files individually, reporting progress back via a callback
* interface.
*/
public abstract class Downloader extends Thread
{
/**
* An interface used to communicate status back to an external entity. <em>Note:</em> these
* methods are all called on the download thread, so implementors must take care to only
* execute thread-safe code or simply pass a message to the AWT thread, for example.
*/
public interface Observer
{
/**
* Called before the downloader begins the series of HTTP head requests to determine the
* size of the files it needs to download.
*/
public void resolvingDownloads ();
/**
* Called to inform the observer of ongoing progress toward completion of the overall
* downloading task. The caller is guaranteed to get at least one call reporting 100%
* completion.
*
* @param percent the percent completion, in terms of total file size, of the downloads.
* @param remaining the estimated download time remaining in seconds, or <code>-1</code> if
* the time can not yet be determined.
*
* @return true if the download should continue, false if it should be aborted.
*/
public boolean downloadProgress (int percent, long remaining);
/**
* Called if a failure occurs while checking for an update or downloading a file.
*
* @param rsrc the resource that was being downloaded when the error occurred, or
* <code>null</code> if the failure occurred while resolving downloads.
* @param e the exception detailing the failure.
*/
public void downloadFailed (Resource rsrc, Exception e);
}
/**
* Creates a downloader that will download the supplied list of resources and communicate with
* the specified observer. The {@link #download} method must be called on the downloader to
* initiate the download process.
*/
public Downloader (Collection<Resource> resources, Observer obs)
{
super("Downloader");
_resources = resources;
_obs = obs;
}
/**
* This method is invoked as the downloader thread and performs the actual downloading.
*/
@Override
public void run ()
{
download();
}
/**
* Start downloading the resources in this downloader.
*
* @return true if the download completed or failed for unexpected reasons (in which case the
* observer will have been notified), false if it was aborted by the observer.
*/
public boolean download ()
{
Resource current = null;
try {
// let the observer know that we're computing download size
if (_obs != null) {
_obs.resolvingDownloads();
}
// first compute the total size of our download
for (Resource resource : _resources) {
discoverSize(resource);
}
long totalSize = sum(_sizes.values());
log.info("Downloading " + totalSize + " bytes...");
// make a note of the time at which we started the download
_start = System.currentTimeMillis();
// now actually download the files
for (Resource resource : _resources) {
download(resource);
}
// finally report our download completion if we did not already do so when downloading
// our final resource
if (_obs != null && !_complete && !_obs.downloadProgress(100, 0)) {
return false;
}
} catch (DownloadAbortedException e) {
return false;
} catch (Exception e) {
if (_obs != null) {
_obs.downloadFailed(current, e);
} else {
log.warning("Observer failed.", e);
}
}
return true;
}
/**
* Notes the amount of data needed to download the given resource..
*/
protected void discoverSize (Resource rsrc)
throws IOException
{
_sizes.put(rsrc, Math.max(checkSize(rsrc), 0L));
}
/**
* Performs the protocol-specific portion of checking download size.
*/
protected abstract long checkSize (Resource rsrc) throws IOException;
/**
* Downloads the specified resource from its remote location to its local location.
*/
protected void download (Resource rsrc)
throws IOException
{
// make sure the resource's target directory exists
File parent = new File(rsrc.getLocal().getParent());
if (!parent.exists() && !parent.mkdirs()) {
log.warning("Failed to create target directory for resource '" + rsrc + "'. " +
"Download will certainly fail.");
}
doDownload(rsrc);
}
/**
* Periodically called by the protocol-specific downloaders to update their progress. This
* should be called at least once for each resource to be downloaded, with the total downloaded
* size for that resource. It can also be called periodically along the way for each resource
* to communicate incremental progress.
*
* @param rsrc the resource currently being downloaded.
* @param currentSize the number of bytes currently downloaded for said resource.
* @param actualSize the size reported for this resource now that we're actually downloading
* it. Some web servers lie about Content-length when doing a HEAD request, so by reporting
* updated sizes here we can recover from receiving bogus information in the earlier {@link
* #checkSize} phase.
*/
protected void updateObserver (Resource rsrc, long currentSize, long actualSize)
throws IOException
{
// update the actual size for this resource (but don't let it shrink)
_sizes.put(rsrc, actualSize = Math.max(actualSize, _sizes.get(rsrc)));
// update the current downloaded size for said resource; don't allow the downloaded bytes
// to exceed the original claimed size of the resource, otherwise our progress will get
// booched and we'll end up back on the Daily WTF: http://tinyurl.com/29wt4oq
_downloaded.put(rsrc, Math.min(actualSize, currentSize));
// notify the observer if it's been sufficiently long since our last notification
long now = System.currentTimeMillis();
if ((now - _lastUpdate) >= UPDATE_DELAY) {
_lastUpdate = now;
// total up our current and total bytes
long downloaded = sum(_downloaded.values());
long totalSize = sum(_sizes.values());
// compute our bytes per second
long secs = (now - _start) / 1000L;
long bps = (secs == 0) ? 0 : (downloaded / secs);
// compute our percentage completion
int pctdone = (totalSize == 0) ? 0 : (int)((downloaded * 100f) / totalSize);
// estimate our time remaining
long remaining = (bps <= 0 || totalSize == 0) ? -1 : (totalSize - downloaded) / bps;
// make sure we only report 100% exactly once
if (pctdone < 100 || !_complete) {
_complete = (pctdone == 100);
if (!_obs.downloadProgress(pctdone, remaining)) {
throw new DownloadAbortedException();
}
}
}
}
/**
* Sums the supplied values.
*/
protected static long sum (Iterable<Long> values)
{
long acc = 0L;
for (Long value : values) {
acc += value;
}
return acc;
}
/**
* Accomplishes the copying of the resource from remote location to local location using
* protocol-specific code
*/
protected abstract void doDownload (Resource rsrc) throws IOException;
/** The resources to be downloaded. */
protected Collection<Resource> _resources;
/** The reported sizes of our resources. */
protected Map<Resource, Long> _sizes = new HashMap<>();
/** The bytes downloaded for each resource. */
protected Map<Resource, Long> _downloaded = new HashMap<>();
/** The observer with whom we are communicating. */
protected Observer _obs;
/** Used while downloading. */
protected byte[] _buffer = new byte[4096];
/** The time at which the file transfer began. */
protected long _start;
/** The current transfer rate in bytes per second. */
protected long _bytesPerSecond;
/** The time at which the last progress update was posted to the progress observer. */
protected long _lastUpdate;
/** Whether the download has completed and the progress observer notified. */
protected boolean _complete;
/** The delay in milliseconds between notifying progress observers of file download
* progress. */
protected static final long UPDATE_DELAY = 500L;
}
@@ -0,0 +1,99 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.net;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.Collection;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.ConnectionUtil;
import static com.threerings.getdown.Log.log;
/**
* Implements downloading files over HTTP
*/
public class HTTPDownloader extends Downloader
{
public HTTPDownloader (Collection<Resource> resources, Observer obs)
{
super(resources, obs);
}
@Override
protected long checkSize (Resource rsrc)
throws IOException
{
URLConnection conn = ConnectionUtil.open(rsrc.getRemote(), 0, 0);
try {
// if we're accessing our data via HTTP, we only need a HEAD request
if (conn instanceof HttpURLConnection) {
HttpURLConnection hcon = (HttpURLConnection)conn;
hcon.setRequestMethod("HEAD");
hcon.connect();
// make sure we got a satisfactory response code
if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("Unable to check up-to-date for " +
rsrc.getRemote() + ": " + hcon.getResponseCode());
}
}
return conn.getContentLength();
} finally {
// let it be known that we're done with this connection
conn.getInputStream().close();
}
}
@Override
protected void doDownload (Resource rsrc)
throws IOException
{
// download the resource from the specified URL
URLConnection conn = ConnectionUtil.open(rsrc.getRemote(), 0, 0);
conn.connect();
// make sure we got a satisfactory response code
if (conn instanceof HttpURLConnection) {
HttpURLConnection hcon = (HttpURLConnection)conn;
if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("Unable to download resource " + rsrc.getRemote() + ": " +
hcon.getResponseCode());
}
}
long actualSize = conn.getContentLength();
log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
long currentSize = 0L;
try (InputStream in = conn.getInputStream();
FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
// TODO: look to see if we have a download info file
// containing info on potentially partially downloaded data;
// if so, use a "Range: bytes=HAVE-" header.
// read in the file data
int read;
while ((read = in.read(_buffer)) != -1) {
// write it out to our local copy
out.write(_buffer, 0, read);
// if we have no observer, then don't bother computing download statistics
if (_obs == null) {
continue;
}
// note that we've downloaded some data
currentSize += read;
updateObserver(rsrc, currentSize, actualSize);
}
}
}
}
@@ -0,0 +1,731 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.threerings.getdown.util;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* Utilities for encoding and decoding the Base64 representation of
* binary data. See RFCs <a
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
*/
public class Base64 {
/**
* Default values for encoder/decoder flags.
*/
public static final int DEFAULT = 0;
/**
* Encoder flag bit to omit the padding '=' characters at the end
* of the output (if any).
*/
public static final int NO_PADDING = 1;
/**
* Encoder flag bit to omit all line terminators (i.e., the output
* will be on one long line).
*/
public static final int NO_WRAP = 2;
/**
* Encoder flag bit to indicate lines should be terminated with a
* CRLF pair instead of just an LF. Has no effect if {@code
* NO_WRAP} is specified as well.
*/
public static final int CRLF = 4;
/**
* Encoder/decoder flag bit to indicate using the "URL and
* filename safe" variant of Base64 (see RFC 3548 section 4) where
* {@code -} and {@code _} are used in place of {@code +} and
* {@code /}.
*/
public static final int URL_SAFE = 8;
/**
* Flag to pass to {@link Base64OutputStream} to indicate that it
* should not close the output stream it is wrapping when it
* itself is closed.
*/
public static final int NO_CLOSE = 16;
// --------------------------------------------------------
// shared code
// --------------------------------------------------------
/* package */ static abstract class Coder {
public byte[] output;
public int op;
/**
* Encode/decode another block of input data. this.output is
* provided by the caller, and must be big enough to hold all
* the coded data. On exit, this.opwill be set to the length
* of the coded data.
*
* @param finish true if this is the final call to process for
* this object. Will finalize the coder state and
* include any final bytes in the output.
*
* @return true if the input so far is good; false if some
* error has been detected in the input stream..
*/
public abstract boolean process(byte[] input, int offset, int len, boolean finish);
/**
* @return the maximum number of bytes a call to process()
* could produce for the given number of input bytes. This may
* be an overestimate.
*/
public abstract int maxOutputSize(int len);
}
// --------------------------------------------------------
// decoding
// --------------------------------------------------------
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
* <p>The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param str the input String to decode, which is converted to
* bytes using ASCII
* @param flags controls certain features of the decoded output.
* Pass {@code DEFAULT} to decode standard Base64.
*
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
public static byte[] decode(String str, int flags) {
return decode(str.getBytes(US_ASCII), flags);
}
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
* <p>The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param input the input array to decode
* @param flags controls certain features of the decoded output.
* Pass {@code DEFAULT} to decode standard Base64.
*
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
public static byte[] decode(byte[] input, int flags) {
return decode(input, 0, input.length, flags);
}
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
* <p>The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param input the data to decode
* @param offset the position within the input array at which to start
* @param len the number of bytes of input to decode
* @param flags controls certain features of the decoded output.
* Pass {@code DEFAULT} to decode standard Base64.
*
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
public static byte[] decode(byte[] input, int offset, int len, int flags) {
// Allocate space for the most data the input could represent.
// (It could contain less if it contains whitespace, etc.)
Decoder decoder = new Decoder(flags, new byte[len*3/4]);
if (!decoder.process(input, offset, len, true)) {
throw new IllegalArgumentException("bad base-64");
}
// Maybe we got lucky and allocated exactly enough output space.
if (decoder.op == decoder.output.length) {
return decoder.output;
}
// Need to shorten the array, so allocate a new one of the
// right size and copy.
byte[] temp = new byte[decoder.op];
System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
return temp;
}
/* package */ static class Decoder extends Coder {
/**
* Lookup table for turning bytes into their position in the
* Base64 alphabet.
*/
private static final int DECODE[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
};
/**
* Decode lookup table for the "web safe" variant (RFC 3548
* sec. 4) where - and _ replace + and /.
*/
private static final int DECODE_WEBSAFE[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
};
/** Non-data values in the DECODE arrays. */
private static final int SKIP = -1;
private static final int EQUALS = -2;
/**
* States 0-3 are reading through the next input tuple.
* State 4 is having read one '=' and expecting exactly
* one more.
* State 5 is expecting no more data or padding characters
* in the input.
* State 6 is the error state; an error has been detected
* in the input and no future input can "fix" it.
*/
private int state; // state number (0 to 6)
private int value;
final private int[] alphabet;
public Decoder(int flags, byte[] output) {
this.output = output;
alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
state = 0;
value = 0;
}
/**
* @return an overestimate for the number of bytes {@code
* len} bytes could decode to.
*/
public int maxOutputSize(int len) {
return len * 3/4 + 10;
}
/**
* Decode another block of input data.
*
* @return true if the state machine is still healthy. false if
* bad base-64 data has been detected in the input stream.
*/
public boolean process(byte[] input, int offset, int len, boolean finish) {
if (this.state == 6) return false;
int p = offset;
len += offset;
// Using local variables makes the decoder about 12%
// faster than if we manipulate the member variables in
// the loop. (Even alphabet makes a measurable
// difference, which is somewhat surprising to me since
// the member variable is final.)
int state = this.state;
int value = this.value;
int op = 0;
final byte[] output = this.output;
final int[] alphabet = this.alphabet;
while (p < len) {
// Try the fast path: we're starting a new tuple and the
// next four bytes of the input stream are all data
// bytes. This corresponds to going through states
// 0-1-2-3-0. We expect to use this method for most of
// the data.
//
// If any of the next four bytes of input are non-data
// (whitespace, etc.), value will end up negative. (All
// the non-data values in decode are small negative
// numbers, so shifting any of them up and or'ing them
// together will result in a value with its top bit set.)
//
// You can remove this whole block and the output should
// be the same, just slower.
if (state == 0) {
while (p+4 <= len &&
(value = ((alphabet[input[p] & 0xff] << 18) |
(alphabet[input[p+1] & 0xff] << 12) |
(alphabet[input[p+2] & 0xff] << 6) |
(alphabet[input[p+3] & 0xff]))) >= 0) {
output[op+2] = (byte) value;
output[op+1] = (byte) (value >> 8);
output[op] = (byte) (value >> 16);
op += 3;
p += 4;
}
if (p >= len) break;
}
// The fast path isn't available -- either we've read a
// partial tuple, or the next four input bytes aren't all
// data, or whatever. Fall back to the slower state
// machine implementation.
int d = alphabet[input[p++] & 0xff];
switch (state) {
case 0:
if (d >= 0) {
value = d;
++state;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 1:
if (d >= 0) {
value = (value << 6) | d;
++state;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 2:
if (d >= 0) {
value = (value << 6) | d;
++state;
} else if (d == EQUALS) {
// Emit the last (partial) output tuple;
// expect exactly one more padding character.
output[op++] = (byte) (value >> 4);
state = 4;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 3:
if (d >= 0) {
// Emit the output triple and return to state 0.
value = (value << 6) | d;
output[op+2] = (byte) value;
output[op+1] = (byte) (value >> 8);
output[op] = (byte) (value >> 16);
op += 3;
state = 0;
} else if (d == EQUALS) {
// Emit the last (partial) output tuple;
// expect no further data or padding characters.
output[op+1] = (byte) (value >> 2);
output[op] = (byte) (value >> 10);
op += 2;
state = 5;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 4:
if (d == EQUALS) {
++state;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 5:
if (d != SKIP) {
this.state = 6;
return false;
}
break;
}
}
if (!finish) {
// We're out of input, but a future call could provide
// more.
this.state = state;
this.value = value;
this.op = op;
return true;
}
// Done reading input. Now figure out where we are left in
// the state machine and finish up.
switch (state) {
case 0:
// Output length is a multiple of three. Fine.
break;
case 1:
// Read one extra input byte, which isn't enough to
// make another output byte. Illegal.
this.state = 6;
return false;
case 2:
// Read two extra input bytes, enough to emit 1 more
// output byte. Fine.
output[op++] = (byte) (value >> 4);
break;
case 3:
// Read three extra input bytes, enough to emit 2 more
// output bytes. Fine.
output[op++] = (byte) (value >> 10);
output[op++] = (byte) (value >> 2);
break;
case 4:
// Read one padding '=' when we expected 2. Illegal.
this.state = 6;
return false;
case 5:
// Read all the padding '='s we expected and no more.
// Fine.
break;
}
this.state = state;
this.op = op;
return true;
}
}
// --------------------------------------------------------
// encoding
// --------------------------------------------------------
/**
* Base64-encode the given data and return a newly allocated
* String with the result.
*
* @param input the data to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static String encodeToString(byte[] input, int flags) {
return new String(encode(input, flags), US_ASCII);
}
/**
* Base64-encode the given data and return a newly allocated
* String with the result.
*
* @param input the data to encode
* @param offset the position within the input array at which to
* start
* @param len the number of bytes of input to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static String encodeToString(byte[] input, int offset, int len, int flags) {
return new String(encode(input, offset, len, flags), US_ASCII);
}
/**
* Base64-encode the given data and return a newly allocated
* byte[] with the result.
*
* @param input the data to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static byte[] encode(byte[] input, int flags) {
return encode(input, 0, input.length, flags);
}
/**
* Base64-encode the given data and return a newly allocated
* byte[] with the result.
*
* @param input the data to encode
* @param offset the position within the input array at which to
* start
* @param len the number of bytes of input to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static byte[] encode(byte[] input, int offset, int len, int flags) {
Encoder encoder = new Encoder(flags, null);
// Compute the exact length of the array we will produce.
int output_len = len / 3 * 4;
// Account for the tail of the data and the padding bytes, if any.
if (encoder.do_padding) {
if (len % 3 > 0) {
output_len += 4;
}
} else {
switch (len % 3) {
case 0: break;
case 1: output_len += 2; break;
case 2: output_len += 3; break;
}
}
// Account for the newlines, if any.
if (encoder.do_newline && len > 0) {
output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
(encoder.do_cr ? 2 : 1);
}
encoder.output = new byte[output_len];
encoder.process(input, offset, len, true);
assert encoder.op == output_len;
return encoder.output;
}
/* package */ static class Encoder extends Coder {
/**
* Emit a new line every this many output tuples. Corresponds to
* a 76-character line length (the maximum allowable according to
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
*/
public static final int LINE_GROUPS = 19;
/**
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private static final byte ENCODE[] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
};
/**
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private static final byte ENCODE_WEBSAFE[] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
};
final private byte[] tail;
/* package */ int tailLen;
private int count;
final public boolean do_padding;
final public boolean do_newline;
final public boolean do_cr;
final private byte[] alphabet;
public Encoder(int flags, byte[] output) {
this.output = output;
do_padding = (flags & NO_PADDING) == 0;
do_newline = (flags & NO_WRAP) == 0;
do_cr = (flags & CRLF) != 0;
alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
tail = new byte[2];
tailLen = 0;
count = do_newline ? LINE_GROUPS : -1;
}
/**
* @return an overestimate for the number of bytes {@code
* len} bytes could encode to.
*/
public int maxOutputSize(int len) {
return len * 8/5 + 10;
}
public boolean process(byte[] input, int offset, int len, boolean finish) {
// Using local variables makes the encoder about 9% faster.
final byte[] alphabet = this.alphabet;
final byte[] output = this.output;
int op = 0;
int count = this.count;
int p = offset;
len += offset;
int v = -1;
// First we need to concatenate the tail of the previous call
// with any input bytes available now and see if we can empty
// the tail.
switch (tailLen) {
case 0:
// There was no tail.
break;
case 1:
if (p+2 <= len) {
// A 1-byte tail with at least 2 bytes of
// input available now.
v = ((tail[0] & 0xff) << 16) |
((input[p++] & 0xff) << 8) |
(input[p++] & 0xff);
tailLen = 0;
};
break;
case 2:
if (p+1 <= len) {
// A 2-byte tail with at least 1 byte of input.
v = ((tail[0] & 0xff) << 16) |
((tail[1] & 0xff) << 8) |
(input[p++] & 0xff);
tailLen = 0;
}
break;
}
if (v != -1) {
output[op++] = alphabet[(v >> 18) & 0x3f];
output[op++] = alphabet[(v >> 12) & 0x3f];
output[op++] = alphabet[(v >> 6) & 0x3f];
output[op++] = alphabet[v & 0x3f];
if (--count == 0) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
count = LINE_GROUPS;
}
}
// At this point either there is no tail, or there are fewer
// than 3 bytes of input available.
// The main loop, turning 3 input bytes into 4 output bytes on
// each iteration.
while (p+3 <= len) {
v = ((input[p] & 0xff) << 16) |
((input[p+1] & 0xff) << 8) |
(input[p+2] & 0xff);
output[op] = alphabet[(v >> 18) & 0x3f];
output[op+1] = alphabet[(v >> 12) & 0x3f];
output[op+2] = alphabet[(v >> 6) & 0x3f];
output[op+3] = alphabet[v & 0x3f];
p += 3;
op += 4;
if (--count == 0) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
count = LINE_GROUPS;
}
}
if (finish) {
// Finish up the tail of the input. Note that we need to
// consume any bytes in tail before any bytes
// remaining in input; there should be at most two bytes
// total.
if (p-tailLen == len-1) {
int t = 0;
v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
tailLen -= t;
output[op++] = alphabet[(v >> 6) & 0x3f];
output[op++] = alphabet[v & 0x3f];
if (do_padding) {
output[op++] = '=';
output[op++] = '=';
}
if (do_newline) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
}
} else if (p-tailLen == len-2) {
int t = 0;
v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
(((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
tailLen -= t;
output[op++] = alphabet[(v >> 12) & 0x3f];
output[op++] = alphabet[(v >> 6) & 0x3f];
output[op++] = alphabet[v & 0x3f];
if (do_padding) {
output[op++] = '=';
}
if (do_newline) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
}
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
}
assert tailLen == 0;
assert p == len;
} else {
// Save the leftovers in tail to be consumed on the next
// call to encodeInternal.
if (p == len-1) {
tail[tailLen++] = input[p];
} else if (p == len-2) {
tail[tailLen++] = input[p];
tail[tailLen++] = input[p+1];
}
}
this.op = op;
this.count = count;
return true;
}
}
private Base64() { } // don't instantiate
}
@@ -0,0 +1,369 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.awt.Color;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import com.samskivert.util.ArrayUtil;
import com.samskivert.util.StringUtil;
import static com.threerings.getdown.Log.log;
/**
* Handles parsing and runtime access for Getdown's config files (mainly {@code getdown.txt}).
* These files contain zero or more mappings for a particular string key. Config values can be
* fetched as single strings, lists of strings, or parsed into primitives or compound data types
* like colors and rectangles.
*/
public class Config
{
/** Options that control the {@link #parsePairs} function. */
public static class ParseOpts {
// these should be tweaked as desired by the caller
public boolean biasToKey = false;
public boolean strictComments = false;
// these are filled in by parseConfig
public String osname = null;
public String osarch = null;
}
/**
* Creates a parse configuration, filling in the platform filters (or not) depending on the
* value of {@code checkPlatform}.
*/
public static ParseOpts createOpts (boolean checkPlatform) {
ParseOpts opts = new ParseOpts();
if (checkPlatform) {
opts.osname = StringUtil.deNull(System.getProperty("os.name")).toLowerCase(Locale.ROOT);
opts.osarch = StringUtil.deNull(System.getProperty("os.arch")).toLowerCase(Locale.ROOT);
}
return opts;
}
/**
* Parses a configuration file containing key/value pairs. The file must be in the UTF-8
* encoding.
*
* @param opts options that influence the parsing. See {@link #createOpts}.
*
* @return a list of <code>String[]</code> instances containing the key/value pairs in the
* order they were parsed from the file.
*/
public static List<String[]> parsePairs (File source, ParseOpts opts)
throws IOException
{
// annoyingly FileReader does not allow encoding to be specified (uses platform default)
try (FileInputStream fis = new FileInputStream(source);
InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
return parsePairs(input, opts);
}
}
/**
* See {@link #parsePairs(File,ParseOpts)}.
*/
public static List<String[]> parsePairs (Reader source, ParseOpts opts) throws IOException
{
List<String[]> pairs = new ArrayList<>();
for (String line : FileUtil.readLines(source)) {
// nix comments
int cidx = line.indexOf("#");
if (opts.strictComments ? cidx == 0 : cidx != -1) {
line = line.substring(0, cidx);
}
// trim whitespace and skip blank lines
line = line.trim();
if (StringUtil.isBlank(line)) {
continue;
}
// parse our key/value pair
String[] pair = new String[2];
// if we're biasing toward key, put all the extra = in the key rather than the value
int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("=");
if (eidx != -1) {
pair[0] = line.substring(0, eidx).trim();
pair[1] = line.substring(eidx+1).trim();
} else {
pair[0] = line;
pair[1] = "";
}
// if the pair has an os qualifier, we need to process it
if (pair[1].startsWith("[")) {
int qidx = pair[1].indexOf("]");
if (qidx == -1) {
log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]);
continue; // omit the pair entirely
}
// if we're checking qualifiers and the os doesn't match this qualifier, skip it
String quals = pair[1].substring(1, qidx);
if (opts.osname != null && !checkQualifiers(quals, opts.osname, opts.osarch)) {
log.debug("Skipping", "quals", quals,
"osname", opts.osname, "osarch", opts.osarch,
"key", pair[0], "value", pair[1]);
continue;
}
// otherwise filter out the qualifier text
pair[1] = pair[1].substring(qidx+1).trim();
}
pairs.add(pair);
}
return pairs;
}
/**
* Takes a comma-separated String of four integers and returns a rectangle using those ints as
* the its x, y, width, and height.
*/
public static Rectangle parseRect (String name, String value)
{
if (!StringUtil.isBlank(value)) {
int[] v = StringUtil.parseIntArray(value);
if (v != null && v.length == 4) {
return new Rectangle(v[0], v[1], v[2], v[3]);
}
log.warning("Ignoring invalid rect '" + name + "' config '" + value + "'.");
}
return null;
}
/**
* Parses the given hex color value (e.g. FFFFF) and returns a Color object with that value.
* If the given value is null of not a valid hexadecimal number, this will return null.
*/
public static Color parseColor (String hexValue)
{
if (!StringUtil.isBlank(hexValue)) {
try {
int rgba = Integer.parseInt(hexValue, 16);
boolean hasAlpha = hexValue.length() > 6;
return new Color(rgba, hasAlpha);
} catch (NumberFormatException e) {
log.warning("Ignoring invalid color", "hexValue", hexValue, "exception", e);
}
}
return null;
}
/**
* Parses a configuration file containing key/value pairs. The file must be in the UTF-8
* encoding.
*
* @return a map from keys to values, where a value will be an array of strings if more than
* one key/value pair in the config file was associated with the same key.
*/
public static Config parseConfig (File source, ParseOpts opts)
throws IOException
{
Map<String, Object> data = new HashMap<>();
// I thought that we could use HashMap<String, String[]> and put new String[] {pair[1]} for
// the null case, but it mysteriously dies on launch, so leaving it as HashMap<String,
// Object> for now
for (String[] pair : parsePairs(source, opts)) {
Object value = data.get(pair[0]);
if (value == null) {
data.put(pair[0], pair[1]);
} else if (value instanceof String) {
data.put(pair[0], new String[] { (String)value, pair[1] });
} else if (value instanceof String[]) {
String[] values = (String[])value;
String[] nvalues = new String[values.length+1];
System.arraycopy(values, 0, nvalues, 0, values.length);
nvalues[values.length] = pair[1];
data.put(pair[0], nvalues);
}
}
// special magic for the getdown.txt config: if the parsed data contains 'strict_comments =
// true' then we reparse the file with strict comments (i.e. # is only assumed to start a
// comment in column 0)
if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
opts.strictComments = true;
return parseConfig(source, opts);
}
return new Config(data);
}
public Config (Map<String, Object> data) {
_data = data;
}
/**
* Returns whether {@code name} has a value in this config.
*/
public boolean hasValue (String name) {
return _data.containsKey(name);
}
/**
* Returns the raw-value for {@code name}. This may be a {@code String}, {@code String[]}, or
* {@code null}.
*/
public Object getRaw (String name) {
return _data.get(name);
}
/**
* Returns the specified config value as a string, or {@code null}.
*/
public String getString (String name) {
return (String)_data.get(name);
}
/**
* Returns the specified config value as a string, or {@code def}.
*/
public String getString (String name, String def) {
String value = (String)_data.get(name);
return value == null ? def : value;
}
/**
* Returns the specified config value as a boolean.
*/
public boolean getBoolean (String name) {
return Boolean.parseBoolean(getString(name));
}
/**
* Massages a single string into an array and leaves existing array values as is. Simplifies
* access to parameters that are expected to be arrays.
*/
public String[] getMultiValue (String name)
{
Object value = _data.get(name);
if (value instanceof String) {
return new String[] { (String)value };
} else {
return (String[])value;
}
}
/** Used to parse rectangle specifications from the config file. */
public Rectangle getRect (String name, Rectangle def)
{
String value = getString(name);
Rectangle rect = parseRect(name, value);
return (rect == null) ? def : rect;
}
/**
* Parses and returns the config value for {@code name} as an int. If no value is provided,
* {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
* returned.
*/
public int getInt (String name, int def) {
String value = getString(name);
try {
return value == null ? def : Integer.parseInt(value);
} catch (Exception e) {
log.warning("Ignoring invalid int '" + name + "' config '" + value + "',");
return def;
}
}
/**
* Parses and returns the config value for {@code name} as a long. If no value is provided,
* {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
* returned.
*/
public long getLong (String name, long def) {
String value = getString(name);
try {
return value == null ? def : Long.parseLong(value);
} catch (Exception e) {
log.warning("Ignoring invalid long '" + name + "' config '" + value + "',");
return def;
}
}
/** Used to parse color specifications from the config file. */
public Color getColor (String name, Color def)
{
String value = getString(name);
Color color = parseColor(value);
return (color == null) ? def : color;
}
/** Parses a list of strings from the config file. */
public String[] getList (String name)
{
String value = getString(name);
return (value == null) ? ArrayUtil.EMPTY_STRING : StringUtil.parseStringArray(value);
}
/**
* Parses a URL from the config file, checking first for a localized version.
*/
public String getUrl (String name, String def)
{
String value = getString(name + "." + Locale.getDefault().getLanguage());
if (!StringUtil.isBlank(value)) {
return value;
}
value = getString(name);
return StringUtil.isBlank(value) ? def : value;
}
/**
* A helper function for {@link #parsePairs(Reader,ParseOpts)}. Qualifiers have the following
* form:
* <pre>
* id = os[-arch]
* ids = id | id,ids
* quals = !id | ids
* </pre>
* Examples: [linux-amd64,linux-x86_64], [windows], [mac os x], [!windows]. Negative qualifiers
* must appear alone, they cannot be used with other qualifiers (positive or negative).
*/
protected static boolean checkQualifiers (String quals, String osname, String osarch)
{
if (quals.startsWith("!")) {
if (quals.indexOf(",") != -1) { // sanity check
log.warning("Multiple qualifiers cannot be used when one of the qualifiers " +
"is negative", "quals", quals);
return false;
}
return !checkQualifier(quals.substring(1), osname, osarch);
}
for (String qual : quals.split(",")) {
if (checkQualifier(qual, osname, osarch)) {
return true; // if we have a positive match, we can immediately return true
}
}
return false; // we had no positive matches, so return false
}
/** A helper function for {@link #checkQualifiers}. */
protected static boolean checkQualifier (String qual, String osname, String osarch)
{
String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-");
String os = bits[0], arch = (bits.length > 1) ? bits[1] : "";
return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1);
}
private final Map<String, Object> _data;
}
@@ -0,0 +1,71 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import com.threerings.getdown.data.SysProps;
import com.threerings.getdown.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;
public class ConnectionUtil
{
/**
* Opens a connection to a URL, setting the authentication header if user info is present.
* @param url the URL to which to open a connection.
* @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the
* connection. If {@code 0} is supplied, the connection timeout specified via system properties
* will be used instead.
* @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from
* the connection. If {@code 0} is supplied, the read timeout specified via system properties
* will be used instead.
*/
public static URLConnection open (URL url, int connectTimeout, int readTimeout)
throws IOException
{
URLConnection conn = url.openConnection();
// configure a connect timeout, if requested
int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout();
if (ctimeout > 0) {
conn.setConnectTimeout(ctimeout * 1000);
}
// configure a read timeout, if requested
int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout();
if (rtimeout > 0) {
conn.setReadTimeout(rtimeout * 1000);
}
// If URL has a username:password@ before hostname, use HTTP basic auth
String userInfo = url.getUserInfo();
if (userInfo != null) {
// Remove any percent-encoding in the username/password
userInfo = URLDecoder.decode(userInfo, "UTF-8");
// Now base64 encode the auth info and make it a single line
String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT).
replaceAll("\\n","").replaceAll("\\r", "");
conn.setRequestProperty("Authorization", "Basic " + encoded);
}
return conn;
}
/**
* Opens a connection to a http or https URL, setting the authentication header if user info is
* present. Throws a class cast exception if the connection returned is not the right type. See
* {@link #open} for parameter documentation.
*/
public static HttpURLConnection openHttp (URL url, int connectTimeout, int readTimeout)
throws IOException
{
return (HttpURLConnection)open(url, connectTimeout, readTimeout);
}
}
@@ -0,0 +1,210 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.*;
import java.util.*;
import java.util.jar.*;
import java.util.zip.GZIPInputStream;
import com.samskivert.io.StreamUtil;
import com.samskivert.util.Logger;
import static com.threerings.getdown.Log.log;
/**
* File related utilities.
*/
public class FileUtil
{
/**
* Gets the specified source file to the specified destination file by hook or crook. Windows
* has all sorts of problems which we work around in this method.
*
* @return true if we managed to get the job done, false otherwise.
*/
public static boolean renameTo (File source, File dest)
{
// if we're on a civilized operating system we may be able to simple rename it
if (source.renameTo(dest)) {
return true;
}
// fall back to trying to rename the old file out of the way, rename the new file into
// place and then delete the old file
if (dest.exists()) {
File temp = new File(dest.getPath() + "_old");
if (temp.exists() && !deleteHarder(temp)) {
log.warning("Failed to delete old intermediate file " + temp + ".");
// the subsequent code will probably fail
}
if (dest.renameTo(temp) && source.renameTo(dest)) {
if (!deleteHarder(temp)) {
log.warning("Failed to delete intermediate file " + temp + ".");
}
return true;
}
}
// as a last resort, try copying the old data over the new
try (FileInputStream fin = new FileInputStream(source);
FileOutputStream fout = new FileOutputStream(dest)) {
StreamUtil.copy(fin, fout);
// close the input stream now so we can delete 'source'
fin.close();
if (!deleteHarder(source)) {
log.warning("Failed to delete " + source +
" after brute force copy to " + dest + ".");
}
return true;
} catch (IOException ioe) {
log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
return false;
}
}
/**
* "Tries harder" to delete {@code file} than just calling {@code delete} on it. Presently this
* just means "try a second time if the first time fails, and if that fails then try to delete
* when the virtual machine terminates." On Windows Vista, sometimes deletes fail but then
* succeed if you just try again. Given that delete failure is a rare occurrence, we can
* implement this hacky workaround without any negative consequences for normal behavior.
*/
public static boolean deleteHarder (File file) {
// if at first you don't succeed... try, try again
boolean deleted = (file.delete() || file.delete());
if (!deleted) {
file.deleteOnExit();
}
return deleted;
}
/**
* Reads the contents of the supplied input stream into a list of lines. Closes the reader on
* successful or failed completion.
*/
public static List<String> readLines (Reader in)
throws IOException
{
List<String> lines = new ArrayList<>();
try (BufferedReader bin = new BufferedReader(in)) {
for (String line = null; (line = bin.readLine()) != null; lines.add(line)) {}
}
return lines;
}
/**
* Unpacks the specified jar file into the specified target directory.
*/
public static void unpackJar (JarFile jar, File target) throws IOException
{
Enumeration<?> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = (JarEntry)entries.nextElement();
File efile = new File(target, entry.getName());
// if we're unpacking a normal jar file, it will have special path
// entries that allow us to create our directories first
if (entry.isDirectory()) {
if (!efile.exists() && !efile.mkdir()) {
log.warning("Failed to create jar entry path", "jar", jar, "entry", entry);
}
continue;
}
// but some do not, so we want to ensure that our directories exist
// prior to getting down and funky
File parent = new File(efile.getParent());
if (!parent.exists() && !parent.mkdirs()) {
log.warning("Failed to create jar entry parent", "jar", jar, "parent", parent);
continue;
}
try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
InputStream jin = jar.getInputStream(entry)) {
StreamUtil.copy(jin, fout);
} catch (Exception e) {
throw new IOException(
Logger.format("Failure unpacking", "jar", jar, "entry", efile), e);
}
}
}
/**
* Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
* packedJar} has a {@code .gz} extension, it will be gunzipped first.
*/
public static void unpackPacked200Jar (File packedJar, File target) throws IOException
{
try (InputStream packJarIn = new FileInputStream(packedJar);
JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
boolean gz = (packedJar.getName().endsWith(".gz") ||
packedJar.getName().endsWith(".gz_new"));
try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
Pack200.Unpacker unpacker = Pack200.newUnpacker();
unpacker.unpack(packJarIn2, jarOut);
}
}
}
/**
* Copies the given {@code source} file to the given {@code target}.
*/
public static void copy (File source, File target) throws IOException {
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(target)) {
StreamUtil.copy(in, out);
}
}
/**
* Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
*/
public static void makeExecutable (File file) {
try {
if (file.exists()) {
if (!file.setExecutable(true, false)) {
log.warning("Failed to mark as executable", "file", file);
}
}
} catch (Exception e) {
log.warning("Failed to mark as executable", "file", file, "error", e);
}
}
/**
* Used by {@link #walkTree}.
*/
public interface Visitor
{
void visit (File file);
}
/**
* Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
*/
public static void walkTree (File root, Visitor visitor)
{
File[] children = root.listFiles();
if (children == null) return;
Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
while (!stack.isEmpty()) {
File currentFile = stack.pop();
if (currentFile.exists()) {
visitor.visit(currentFile);
File[] currentChildren = currentFile.listFiles();
if (currentChildren != null) {
for (File file : currentChildren) {
stack.push(file);
}
}
}
}
}
}
@@ -0,0 +1,216 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Locale;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
import static com.threerings.getdown.Log.log;
/**
* Useful routines for launching Java applications from within other Java
* applications.
*/
public class LaunchUtil
{
/** The directory into which a local VM installation should be unpacked. */
public static final String LOCAL_JAVA_DIR = "java_vm";
/**
* Writes a <code>version.txt</code> file into the specified application directory and
* attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly
* specified version and relaunch the application.
*
* @param appdir the directory in which the application is installed.
* @param getdownJarName the name of the getdown jar file in the application directory. This is
* probably <code>getdown-pro.jar</code> or <code>getdown-retro-pro.jar</code> if you are using
* the results of the standard build.
* @param newVersion the new version to which Getdown will update when it is executed.
*
* @return true if the relaunch succeeded, false if we were unable to relaunch due to being on
* Windows 9x where we cannot launch subprocesses without waiting around for them to exit,
* reading their stdout and stderr all the while. If true is returned, the application may exit
* after making this call as it will be upgraded and restarted. If false is returned, the
* application should tell the user that they must restart the application manually.
*
* @exception IOException thrown if we were unable to create the <code>version.txt</code> file
* in the supplied application directory. If the version.txt file cannot be created, restarting
* Getdown will not cause the application to be upgraded, so the application will have to
* resort to telling the user that it is in a bad way.
*/
public static boolean updateVersionAndRelaunch (
File appdir, String getdownJarName, String newVersion)
throws IOException
{
// create the file that instructs Getdown to upgrade
File vfile = new File(appdir, "version.txt");
try (PrintStream ps = new PrintStream(new FileOutputStream(vfile))) {
ps.println(newVersion);
}
// make sure that we can find our getdown.jar file and can safely launch children
File pro = new File(appdir, getdownJarName);
if (mustMonitorChildren() || !pro.exists()) {
return false;
}
// do the deed
String[] args = new String[] {
getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath()
};
log.info("Running " + StringUtil.join(args, "\n "));
try {
Runtime.getRuntime().exec(args, null);
return true;
} catch (IOException ioe) {
log.warning("Failed to run getdown", ioe);
return false;
}
}
/**
* Reconstructs the path to the JVM used to launch this process.
*/
public static String getJVMPath (File appdir)
{
return getJVMPath(appdir, false);
}
/**
* Reconstructs the path to the JVM used to launch this process.
*
* @param windebug if true we will use java.exe instead of javaw.exe on Windows.
*/
public static String getJVMPath (File appdir, boolean windebug)
{
// first look in our application directory for an installed VM
String vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath(), windebug);
// then fall back to the VM in which we're already running
if (vmpath == null) {
vmpath = checkJVMPath(System.getProperty("java.home"), windebug);
}
// then throw up our hands and hope for the best
if (vmpath == null) {
log.warning("Unable to find java [appdir=" + appdir +
", java.home=" + System.getProperty("java.home") + "]!");
vmpath = "java";
}
// Oddly, the Mac OS X specific java flag -Xdock:name will only work if java is launched
// from /usr/bin/java, and not if launched by directly referring to <java.home>/bin/java,
// even though the former is a symlink to the latter! To work around this, see if the
// desired jvm is in fact pointed to by /usr/bin/java and, if so, use that instead.
if (RunAnywhere.isMacOS()) {
try {
File localVM = new File("/usr/bin/java").getCanonicalFile();
if (localVM.equals(new File(vmpath).getCanonicalFile())) {
vmpath = "/usr/bin/java";
}
} catch (IOException ioe) {
log.warning("Failed to check Mac OS canonical VM path.", ioe);
}
}
return vmpath;
}
/**
* Upgrades Getdown by moving an installation managed copy of the Getdown jar file over the
* non-managed copy (which would be used to run Getdown itself).
*
* <p> If the upgrade fails for a variety of reasons, warnings are logged but no other actions
* are taken. There's not much else one can do other than try again next time around.
*/
public static void upgradeGetdown (File oldgd, File curgd, File newgd)
{
// we assume getdown's jar file size changes with every upgrade, this is not guaranteed,
// but in reality it will, and it allows us to avoid pointlessly upgrading getdown every
// time the client is updated which is unnecessarily flirting with danger
if (!newgd.exists() || newgd.length() == curgd.length()) {
return;
}
log.info("Updating Getdown with " + newgd + "...");
// clear out any old getdown
if (oldgd.exists()) {
FileUtil.deleteHarder(oldgd);
}
// now try updating using renames
if (!curgd.exists() || curgd.renameTo(oldgd)) {
if (newgd.renameTo(curgd)) {
FileUtil.deleteHarder(oldgd); // yay!
try {
// copy the moved file back to getdown-dop-new.jar so that we don't end up
// downloading another copy next time
FileUtil.copy(curgd, newgd);
} catch (IOException e) {
log.warning("Error copying updated Getdown back: " + e);
}
return;
}
log.warning("Unable to renameTo(" + oldgd + ").");
// try to unfuck ourselves
if (!oldgd.renameTo(curgd)) {
log.warning("Oh God, why dost thee scorn me so.");
}
}
// that didn't work, let's try copying it
log.info("Attempting to upgrade by copying over " + curgd + "...");
try {
FileUtil.copy(newgd, curgd);
} catch (IOException ioe) {
log.warning("Mayday! Brute force copy method also failed.", ioe);
}
}
/**
* Returns true if, on this operating system, we have to stick around and read the stderr from
* our children processes to prevent them from filling their output buffers and hanging.
*/
public static boolean mustMonitorChildren ()
{
String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1);
}
/**
* Checks whether a Java Virtual Machine can be located in the supplied path.
*/
protected static String checkJVMPath (String vmhome, boolean windebug)
{
String vmbase = vmhome + File.separator + "bin" + File.separator;
String vmpath = vmbase + "java";
if (new File(vmpath).exists()) {
return vmpath;
}
if (!windebug) {
vmpath = vmbase + "javaw.exe";
if (new File(vmpath).exists()) {
return vmpath;
}
}
vmpath = vmbase + "java.exe";
if (new File(vmpath).exists()) {
return vmpath;
}
return null;
}
}
@@ -0,0 +1,50 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
/**
* Accumulates the progress from a number of (potentially parallel) elements into a single smoothly
* progressing progress.
*/
public class ProgressAggregator
{
public ProgressAggregator (ProgressObserver target, long[] sizes) {
_target = target;
_sizes = sizes;
_progress = new int[sizes.length];
}
public ProgressObserver startElement (final int index) {
return new ProgressObserver() {
public void progress (int percent) {
_progress[index] = percent;
updateAggProgress();
}
};
}
protected void updateAggProgress () {
long totalSize = 0L, currentSize = 0L;
synchronized (this) {
for (int ii = 0, ll = _sizes.length; ii < ll; ii++) {
long size = _sizes[ii];
totalSize += size;
currentSize += (int)((size * _progress[ii])/100.0);
}
}
_target.progress((int)(100.0*currentSize / totalSize));
}
protected static long sum (long[] sizes) {
long totalSize = 0L;
for (long size : sizes) totalSize += size;
return totalSize;
}
protected ProgressObserver _target;
protected long[] _sizes;
protected int[] _progress;
}
@@ -0,0 +1,18 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
/**
* Used to communicate progress.
*/
public interface ProgressObserver
{
/**
* Informs the observer that we have completed the specified
* percentage of the process.
*/
public void progress (int percent);
}
@@ -0,0 +1,116 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.samskivert.util.StringUtil;
import com.threerings.getdown.data.SysProps;
import static com.threerings.getdown.Log.log;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Version related utilities.
*/
public class VersionUtil
{
/**
* Reads a version number from a file.
*/
public static long readVersion (File vfile)
{
long fileVersion = -1;
try (BufferedReader bin =
new BufferedReader(new InputStreamReader(new FileInputStream(vfile), UTF_8))) {
String vstr = bin.readLine();
if (!StringUtil.isBlank(vstr)) {
fileVersion = Long.parseLong(vstr);
}
} catch (Exception e) {
log.info("Unable to read version file: " + e.getMessage());
}
return fileVersion;
}
/**
* Writes a version number to a file.
*/
public static void writeVersion (File vfile, long version) throws IOException
{
try (PrintStream out = new PrintStream(new FileOutputStream(vfile))) {
out.println(version);
} catch (Exception e) {
log.warning("Unable to write version file: " + e.getMessage());
}
}
/**
* Parses {@code versStr} using {@code versRegex} into a (long) integer version number.
* @see SysProps#parseJavaVersion
*/
public static long parseJavaVersion (String versRegex, String versStr)
{
Matcher m = Pattern.compile(versRegex).matcher(versStr);
if (!m.matches()) return 0L;
long vers = 0L;
for (int ii = 1; ii <= m.groupCount(); ii++) {
String valstr = m.group(ii);
int value = (valstr == null) ? 0 : parseInt(valstr);
vers *= 100;
vers += value;
}
return vers;
}
/**
* Reads and parses the version from the {@code release} file bundled with a JVM.
*/
public static long readReleaseVersion (File relfile, String versRegex)
{
try (BufferedReader in =
new BufferedReader(new InputStreamReader(new FileInputStream(relfile), UTF_8))) {
String line = null, relvers = null;
while ((line = in.readLine()) != null) {
if (line.startsWith("JAVA_VERSION=")) {
relvers = line.substring("JAVA_VERSION=".length()).replace('"', ' ').trim();
}
}
if (relvers == null) {
log.warning("No JAVA_VERSION line in 'release' file", "file", relfile);
return 0L;
}
return parseJavaVersion(versRegex, relvers);
} catch (Exception e) {
log.warning("Failed to read version from 'release' file", "file", relfile, e);
return 0L;
}
}
private static int parseInt (String str) {
int value = 0;
for (int ii = 0, ll = str.length(); ii < ll; ii++) {
char c = str.charAt(ii);
if (c >= '0' && c <= '9') {
value *= 10;
value += (c - '0');
}
}
return value;
}
}
@@ -0,0 +1,49 @@
package com.threerings.getdown.classpath;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedHashSet;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.assertEquals;
/**
* Tests for {@link ClassPath}.
*/
public class ClassPathTest
{
@Before public void createJarsAndSetupClassPath () throws IOException
{
_firstJar = _folder.newFile("a.jar");
_secondJar = _folder.newFile("b.jar");
LinkedHashSet<File> classPathEntries = new LinkedHashSet<File>();
classPathEntries.add(_firstJar);
classPathEntries.add(_secondJar);
_classPath = new ClassPath(classPathEntries);
}
@Test public void shouldCreateValidArgumentString ()
{
assertEquals(
_firstJar.getAbsolutePath() + File.pathSeparator + _secondJar.getAbsolutePath(),
_classPath.asArgumentString());
}
@Test public void shouldProvideJarUrls () throws MalformedURLException, URISyntaxException
{
URL[] actualUrls = _classPath.asUrls();
assertEquals(_firstJar, new File(actualUrls[0].toURI()));
assertEquals(_secondJar, new File(actualUrls[1].toURI()));
}
@Rule public TemporaryFolder _folder = new TemporaryFolder();
private File _firstJar, _secondJar;
private ClassPath _classPath;
}
@@ -0,0 +1,68 @@
package com.threerings.getdown.classpath;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Mockito.when;
import com.samskivert.util.FileUtil;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Resource;
@RunWith(MockitoJUnitRunner.class)
public class ClassPathsTest
{
@Before public void setupFilesAndResources () throws IOException
{
_firstJarFile = _appdir.newFile("a.jar");
_secondJarFile = _appdir.newFile("b.jar");
when(_firstJar.getFinalTarget()).thenReturn(_firstJarFile);
when(_secondJar.getFinalTarget()).thenReturn(_secondJarFile);
when(_application.getActiveCodeResources()).thenReturn(Arrays.asList(_firstJar, _secondJar));
when(_application.getAppdir()).thenReturn(_appdir.getRoot());
}
@Test public void shouldBuildDefaultClassPath () throws IOException
{
ClassPath classPath = ClassPaths.buildDefaultClassPath(_application);
String expectedClassPath = _firstJarFile.getAbsolutePath() + File.pathSeparator +
_secondJarFile.getAbsolutePath();
assertEquals(expectedClassPath, classPath.asArgumentString());
}
@Test public void shouldBuildCachedClassPath () throws IOException
{
when(_application.getDigest(_firstJar)).thenReturn("first");
when(_application.getDigest(_secondJar)).thenReturn("second");
when(_application.getCodeCacheRetentionDays()).thenReturn(1);
File firstCachedJarFile = FileUtil.newFile(
_appdir.getRoot(), ClassPaths.CACHE_DIR, "fi", "first.jar");
File secondCachedJarFile = FileUtil.newFile(
_appdir.getRoot(), ClassPaths.CACHE_DIR, "se", "second.jar");
String expectedClassPath = firstCachedJarFile.getAbsolutePath() + File.pathSeparator +
secondCachedJarFile.getAbsolutePath();
ClassPath classPath = ClassPaths.buildCachedClassPath(_application);
assertEquals(expectedClassPath, classPath.asArgumentString());
}
@Mock protected Application _application;
@Mock protected Resource _firstJar;
@Mock protected Resource _secondJar;
protected File _firstJarFile, _secondJarFile;
@Rule public TemporaryFolder _appdir = new TemporaryFolder();
}
@@ -0,0 +1,66 @@
package com.threerings.getdown.classpath.cache;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
/**
* Validates that cache garbage is collected and deleted correctly.
*/
public class GarbageCollectorTest
{
@Before public void setupFiles () throws IOException
{
_cachedFile = _folder.newFile("abc123.jar");
_lastAccessedFile = _folder.newFile("abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
}
@Test public void shouldDeleteCacheEntryIfRetentionPeriodIsReached ()
{
gcNow();
assertFalse(_cachedFile.exists());
assertFalse(_lastAccessedFile.exists());
}
@Test public void shouldDeleteCacheFolderIfFolderIsEmpty ()
{
gcNow();
assertFalse(_folder.getRoot().exists());
}
private void gcNow() {
GarbageCollector.collect(_folder.getRoot(), -1);
}
@Test public void shouldKeepFilesInCacheIfRententionPeriodIsNotReached ()
{
GarbageCollector.collect(_folder.getRoot(), TimeUnit.DAYS.toMillis(1));
assertTrue(_cachedFile.exists());
assertTrue(_lastAccessedFile.exists());
}
@Test public void shouldDeleteCachedFileIfLastAccessedFileIsMissing ()
{
assumeTrue(_lastAccessedFile.delete());
gcNow();
assertFalse(_cachedFile.exists());
}
@Test public void shouldDeleteLastAccessedFileIfCachedFileIsMissing ()
{
assumeTrue(_cachedFile.delete());
gcNow();
assertFalse(_lastAccessedFile.exists());
}
@Rule public TemporaryFolder _folder = new TemporaryFolder();
private File _cachedFile;
private File _lastAccessedFile;
}
@@ -0,0 +1,67 @@
package com.threerings.getdown.classpath.cache;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.*;
/**
* Asserts the correct functionality of the {@link ResourceCache}.
*/
public class ResourceCacheTest
{
@Before public void setupCache () throws IOException {
_fileToCache = _folder.newFile("filetocache.jar");
_cache = new ResourceCache(_folder.newFolder(".cache"));
}
@Test public void shouldCacheFile () throws IOException
{
assertEquals("abc123.jar", cacheFile().getName());
}
private File cacheFile() throws IOException
{
return _cache.cacheFile(_fileToCache, "abc123");
}
@Test public void shouldTrackFileUsage () throws IOException
{
String name = "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
File lastAccessedFile = new File(cacheFile().getParentFile(), name);
assertTrue(lastAccessedFile.exists());
}
@Test public void shouldNotCacheTheSameFile () throws Exception
{
File cachedFile = cacheFile();
cachedFile.setLastModified(YESTERDAY);
long expectedLastModified = cachedFile.lastModified();
// caching it another time
File sameCachedFile = cacheFile();
assertEquals(expectedLastModified, sameCachedFile.lastModified());
}
@Test public void shouldRememberWhenFileWasRequested () throws Exception
{
File cachedFile = cacheFile();
String name = cachedFile.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
File lastAccessedFile = new File(cachedFile.getParentFile(), name);
lastAccessedFile.setLastModified(YESTERDAY);
long lastAccessed = lastAccessedFile.lastModified();
// caching it another time
cacheFile();
assertTrue(lastAccessedFile.lastModified() > lastAccessed);
}
@Rule public TemporaryFolder _folder = new TemporaryFolder();
private File _fileToCache;
private ResourceCache _cache;
private static final long YESTERDAY = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
}
@@ -0,0 +1,42 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import org.junit.*;
import static org.junit.Assert.*;
public class SysPropsTest {
@After public void clearProps () {
System.clearProperty("appbase_domain");
System.clearProperty("appbase_override");
}
public static String[] APPBASES = {
"http://foobar.com/myapp",
"https://foobar.com/myapp",
"http://foobar.com:8080/myapp",
"https://foobar.com:8080/myapp"
};
@Test public void testAppbaseDomain () {
System.setProperty("appbase_domain", "https://barbaz.com");
for (String appbase : APPBASES) {
assertEquals("https://barbaz.com/myapp", SysProps.overrideAppbase(appbase));
}
System.setProperty("appbase_domain", "http://barbaz.com");
for (String appbase : APPBASES) {
assertEquals("http://barbaz.com/myapp", SysProps.overrideAppbase(appbase));
}
}
@Test public void testAppbaseOverride () {
System.setProperty("appbase_override", "https://barbaz.com/newapp");
for (String appbase : APPBASES) {
assertEquals("https://barbaz.com/newapp", SysProps.overrideAppbase(appbase));
}
}
}
@@ -0,0 +1,170 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import com.samskivert.util.RandomUtil;
import org.junit.*;
import static org.junit.Assert.*;
/**
* Tests {@link Config}.
*/
public class ConfigTest
{
public static class Pair {
public final String key;
public final String value;
public Pair (String key, String value) {
this.key = key;
this.value = value;
}
}
public static final Pair[] SIMPLE_PAIRS = {
new Pair("one", "two"),
new Pair("three", "four"),
new Pair("five", "six"),
new Pair("seven", "eight"),
new Pair("nine", "ten"),
};
@Test public void testSimplePairs () throws IOException
{
List<String[]> pairs = Config.parsePairs(
toReader(SIMPLE_PAIRS), Config.createOpts(true));
for (int ii = 0; ii < SIMPLE_PAIRS.length; ii++) {
assertEquals(SIMPLE_PAIRS[ii].key, pairs.get(ii)[0]);
assertEquals(SIMPLE_PAIRS[ii].value, pairs.get(ii)[1]);
}
}
@Test public void testQualifiedPairs () throws IOException
{
Pair linux = new Pair("one", "[linux] two");
Pair mac = new Pair("three", "[mac os x] four");
Pair linuxAndMac = new Pair("five", "[linux, mac os x] six");
Pair linux64 = new Pair("seven", "[linux-x86_64] eight");
Pair linux64s = new Pair("nine", "[linux-x86_64, linux-amd64] ten");
Pair mac64 = new Pair("eleven", "[mac os x-x86_64] twelve");
Pair win64 = new Pair("thirteen", "[windows-x86_64] fourteen");
Pair notWin = new Pair("fifteen", "[!windows] sixteen");
Pair[] pairs = { linux, mac, linuxAndMac, linux64, linux64s, mac64, win64, notWin };
Config.ParseOpts opts = Config.createOpts(false);
opts.osname = "linux";
opts.osarch = "i386";
List<String[]> parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(exists(parsed, linux.key));
assertTrue(!exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertTrue(!exists(parsed, linux64.key));
assertTrue(!exists(parsed, linux64s.key));
assertTrue(!exists(parsed, mac64.key));
assertTrue(!exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osarch = "x86_64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(exists(parsed, linux.key));
assertTrue(!exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertTrue(exists(parsed, linux64.key));
assertTrue(exists(parsed, linux64s.key));
assertTrue(!exists(parsed, mac64.key));
assertTrue(!exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osarch = "amd64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(exists(parsed, linux.key));
assertTrue(!exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertTrue(!exists(parsed, linux64.key));
assertTrue(exists(parsed, linux64s.key));
assertTrue(!exists(parsed, mac64.key));
assertTrue(!exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osname = "mac os x";
opts.osarch = "x86_64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(!exists(parsed, linux.key));
assertTrue(exists(parsed, mac.key));
assertTrue(exists(parsed, linuxAndMac.key));
assertTrue(!exists(parsed, linux64.key));
assertTrue(!exists(parsed, linux64s.key));
assertTrue(exists(parsed, mac64.key));
assertTrue(!exists(parsed, win64.key));
assertTrue(exists(parsed, notWin.key));
opts.osname = "windows";
opts.osarch = "i386";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(!exists(parsed, linux.key));
assertTrue(!exists(parsed, mac.key));
assertTrue(!exists(parsed, linuxAndMac.key));
assertTrue(!exists(parsed, linux64.key));
assertTrue(!exists(parsed, linux64s.key));
assertTrue(!exists(parsed, mac64.key));
assertTrue(!exists(parsed, win64.key));
assertTrue(!exists(parsed, notWin.key));
opts.osarch = "x86_64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(!exists(parsed, linux.key));
assertTrue(!exists(parsed, mac.key));
assertTrue(!exists(parsed, linuxAndMac.key));
assertTrue(!exists(parsed, linux64.key));
assertTrue(!exists(parsed, linux64s.key));
assertTrue(!exists(parsed, mac64.key));
assertTrue(exists(parsed, win64.key));
assertTrue(!exists(parsed, notWin.key));
opts.osarch = "amd64";
parsed = Config.parsePairs(toReader(pairs), opts);
assertTrue(!exists(parsed, linux.key));
assertTrue(!exists(parsed, mac.key));
assertTrue(!exists(parsed, linuxAndMac.key));
assertTrue(!exists(parsed, linux64.key));
assertTrue(!exists(parsed, linux64s.key));
assertTrue(!exists(parsed, mac64.key));
assertTrue(!exists(parsed, win64.key));
assertTrue(!exists(parsed, notWin.key));
}
protected static boolean exists (List<String[]> pairs, String key)
{
for (String[] pair : pairs) {
if (pair[0].equals(key)) {
return true;
}
}
return false;
}
protected static StringReader toReader (Pair[] pairs)
{
StringBuilder builder = new StringBuilder();
for (Pair pair : pairs) {
// throw some whitespace in to ensure it's trimmed
builder.append(whitespace()).append(pair.key).
append(whitespace()).append("=").
append(whitespace()).append(pair.value).
append(whitespace()).append("\n");
}
return new StringReader(builder.toString());
}
protected static String whitespace ()
{
return RandomUtil.getBoolean() ? " " : "";
}
}
@@ -0,0 +1,60 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.*;
/**
* Tests {@link FileUtil}.
*/
public class FileUtilTest
{
@Test public void testReadLines () throws IOException
{
String data = "This is a test\nof a file with\na few lines\n";
List<String> lines = FileUtil.readLines(new StringReader(data));
String[] linesBySplit = data.split("\n");
assertEquals(linesBySplit.length, lines.size());
for (int ii = 0; ii < lines.size(); ii++) {
assertEquals(linesBySplit[ii], lines.get(ii));
}
}
@Test public void shouldCopyFile () throws IOException
{
File source = _folder.newFile("source.txt");
File target = new File(_folder.getRoot(), "target.txt");
assertFalse(target.exists());
FileUtil.copy(source, target);
assertTrue(target.exists());
}
@Test public void shouldRecursivelyWalkOverFilesAndFolders () throws IOException
{
_folder.newFile("a.txt");
new File(_folder.newFolder("b"), "b.txt").createNewFile();
class CountingVisitor implements FileUtil.Visitor {
int fileCount = 0;
@Override public void visit(File file) {
fileCount++;
}
}
CountingVisitor visitor = new CountingVisitor();
FileUtil.walkTree(_folder.getRoot(), visitor);
assertEquals(3, visitor.fileCount);
}
@Rule public TemporaryFolder _folder = new TemporaryFolder();
}
@@ -0,0 +1,53 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class VersionUtilTest {
@Test
public void shouldParseJavaVersion ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "1.8.0_152");
assertEquals(1_080_152, version);
}
@Test
public void shouldParseJavaVersion8 ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "1.8");
assertEquals(1_080_000, version);
}
@Test
public void shouldParseJavaVersion9 ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "9");
assertEquals(9_000_000, version);
}
@Test
public void shouldParseJavaVersion10 ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "10");
assertEquals(10_000_000, version);
}
@Test
public void shouldParseJavaRuntimeVersion ()
{
long version = VersionUtil.parseJavaVersion(
"(\\d+)\\.(\\d+)\\.(\\d+)(_\\d+)?(-b\\d+)?", "1.8.0_131-b11");
assertEquals(108_013_111, version);
}
}