JAL-3130 adapted getdown src. attempt 2. first attempt failed due to cp'ed .git files
authorBen Soares <bsoares@dundee.ac.uk>
Mon, 15 Apr 2019 09:25:36 +0000 (10:25 +0100)
committerBen Soares <bsoares@dundee.ac.uk>
Mon, 15 Apr 2019 09:25:36 +0000 (10:25 +0100)
161 files changed:
getdown/src/getdown [deleted submodule]
getdown/src/getdown/.project [new file with mode: 0644]
getdown/src/getdown/.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
getdown/src/getdown/.settings/org.eclipse.m2e.core.prefs [new file with mode: 0644]
getdown/src/getdown/.travis.yml [new file with mode: 0644]
getdown/src/getdown/AUTHORS [new file with mode: 0644]
getdown/src/getdown/CHANGELOG.md [new file with mode: 0644]
getdown/src/getdown/LICENSE [new file with mode: 0644]
getdown/src/getdown/README.md [new file with mode: 0644]
getdown/src/getdown/ant/.project [new file with mode: 0644]
getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs [new file with mode: 0644]
getdown/src/getdown/ant/pom.xml [new file with mode: 0644]
getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java [new file with mode: 0644]
getdown/src/getdown/ant/target/getdown-ant-1.8.3-SNAPSHOT.jar [new file with mode: 0644]
getdown/src/getdown/ant/target/maven-archiver/pom.properties [new file with mode: 0644]
getdown/src/getdown/ant/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst [new file with mode: 0644]
getdown/src/getdown/ant/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst [new file with mode: 0644]
getdown/src/getdown/bin/differ [new file with mode: 0755]
getdown/src/getdown/bin/patcher [new file with mode: 0755]
getdown/src/getdown/core/.project [new file with mode: 0644]
getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs [new file with mode: 0644]
getdown/src/getdown/core/pom.xml [new file with mode: 0644]
getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java [new file with mode: 0644]
getdown/src/getdown/core/src/it/resources/testapp/background.png [new file with mode: 0644]
getdown/src/getdown/core/src/it/resources/testapp/crazyhashfile#txt [new file with mode: 0644]
getdown/src/getdown/core/src/it/resources/testapp/foo.jar [new file with mode: 0644]
getdown/src/getdown/core/src/it/resources/testapp/funny%test dir/some=file.txt [new file with mode: 0644]
getdown/src/getdown/core/src/it/resources/testapp/getdown.txt [new file with mode: 0644]
getdown/src/getdown/core/src/it/resources/testapp/script.sh [new file with mode: 0644]
getdown/src/getdown/core/src/it/resources/testapp/testapp.jar [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Build.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/ClassPath.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Properties.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffCodes.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressAggregator.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Rectangle.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java [new file with mode: 0644]
getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/ClassPathTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/PathBuilderTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/SysPropsTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/ColorTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/ConfigTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/FileUtilTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/StringUtilTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/VersionUtilTest.java [new file with mode: 0644]
getdown/src/getdown/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker [new file with mode: 0644]
getdown/src/getdown/core/target/antrun/build-main.xml [new file with mode: 0644]
getdown/src/getdown/core/target/classes/LICENSE [new file with mode: 0644]
getdown/src/getdown/core/target/getdown-core-1.8.3-SNAPSHOT.jar [new file with mode: 0644]
getdown/src/getdown/core/target/maven-archiver/pom.properties [new file with mode: 0644]
getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst [new file with mode: 0644]
getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst [new file with mode: 0644]
getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst [new file with mode: 0644]
getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.cache.GarbageCollectorTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.cache.ResourceCacheTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.ClassPathTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.EnvConfigTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.PathBuilderTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.SysPropsTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.ColorTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.ConfigTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.FileUtilTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.HostWhitelistTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.StringUtilTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.VersionUtilTest.xml [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.cache.GarbageCollectorTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.cache.ResourceCacheTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.ClassPathTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.EnvConfigTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.PathBuilderTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.SysPropsTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.ColorTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.ConfigTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.FileUtilTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.HostWhitelistTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.StringUtilTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.VersionUtilTest.txt [new file with mode: 0644]
getdown/src/getdown/core/target/test-classes/mockito-extensions/org.mockito.plugins.MockMaker [new file with mode: 0644]
getdown/src/getdown/launcher/.project [new file with mode: 0644]
getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs [new file with mode: 0644]
getdown/src/getdown/launcher/pom.xml [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/MultipleGetdownRunning.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties [new file with mode: 0644]
getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_de.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_es.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_fr.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_it.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_ja.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_ko.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_pt.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_zh.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT.jar [new file with mode: 0644]
getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT_proguard_base.jar [new file with mode: 0644]
getdown/src/getdown/launcher/target/maven-archiver/pom.properties [new file with mode: 0644]
getdown/src/getdown/launcher/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst [new file with mode: 0644]
getdown/src/getdown/launcher/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst [new file with mode: 0644]
getdown/src/getdown/launcher/target/proguard_map.txt [new file with mode: 0644]
getdown/src/getdown/launcher/target/proguard_seed.txt [new file with mode: 0644]
getdown/src/getdown/lib/SOURCE_HEADER [new file with mode: 0644]
getdown/src/getdown/lib/jRegistryKey.dll [new file with mode: 0644]
getdown/src/getdown/lib/jregistrykey/jregistrykey/1.0/jregistrykey-1.0.jar [new file with mode: 0644]
getdown/src/getdown/lib/jregistrykey/jregistrykey/1.0/jregistrykey-1.0.pom [new file with mode: 0644]
getdown/src/getdown/lib/jregistrykey/jregistrykey/maven-metadata-local.xml [new file with mode: 0644]
getdown/src/getdown/lib/manifest.mf [new file with mode: 0644]
getdown/src/getdown/pom.xml [new file with mode: 0644]

diff --git a/getdown/src/getdown b/getdown/src/getdown
deleted file mode 160000 (submodule)
index 0e2f116..0000000
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 0e2f11698da3c6b9ffd9ce7a1317ff42b96a1dbf
diff --git a/getdown/src/getdown/.project b/getdown/src/getdown/.project
new file mode 100644 (file)
index 0000000..ccd1d40
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>getdown</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.m2e.core.maven2Builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.m2e.core.maven2Nature</nature>
+       </natures>
+</projectDescription>
diff --git a/getdown/src/getdown/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..99f26c0
--- /dev/null
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/getdown/src/getdown/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/.settings/org.eclipse.m2e.core.prefs
new file mode 100644 (file)
index 0000000..f897a7f
--- /dev/null
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/getdown/src/getdown/.travis.yml b/getdown/src/getdown/.travis.yml
new file mode 100644 (file)
index 0000000..32f2196
--- /dev/null
@@ -0,0 +1,11 @@
+language: java
+sudo: false
+script: "mvn -B clean verify"
+
+cache:
+  directories:
+    - '$HOME/.m2/repository'
+
+jdk:
+  - openjdk7
+  - oraclejdk8
diff --git a/getdown/src/getdown/AUTHORS b/getdown/src/getdown/AUTHORS
new file mode 100644 (file)
index 0000000..5f389d0
--- /dev/null
@@ -0,0 +1,12 @@
+#
+# This is the official list of the AUTHORS of Getdown for copyright purposes.
+#
+# This is not the full list of contributors, see
+# https://github.com/threerings/getdown/graphs/contributors
+# for the full list of contributors.
+#
+# Contributors assign copyright of their work to the authors listed in the this file to keep life
+# simple.
+
+Michael Bayne <mdb@samskivert.com>
+Ray Greenwell <ray@threerings.net>
diff --git a/getdown/src/getdown/CHANGELOG.md b/getdown/src/getdown/CHANGELOG.md
new file mode 100644 (file)
index 0000000..098651e
--- /dev/null
@@ -0,0 +1,180 @@
+# Getdown Releases
+
+## 1.8.3 - Unreleased
+
+* Added support for `nresource` resources which must be jar files that contain native libraries.
+  Prior to launching the application, these resources will be unpacked and their contents added to
+  the `java.library.path` system property.
+
+* When the app is updated to require a new version of the JVM, that JVM will be downloaded and used
+  immediately during that app invocation (instead of one invocation later). Via PR#169.
+
+* When a custom JVM is installed, old JVM files will be deleted prior to unpacking the new JVM. Via
+  PR#170.
+
+* Number of concurrent downloads now defaults to num-cores minus one. Though downloads are I/O
+  bound rather than CPU bound, this still turns out to be a decent default.
+
+* Avoid checking for proxy config if `https.proxyHost` is set. This matches existing behavior when
+  `http.proxyHost` is set.
+
+* Added support for proxy authentication. A deployment must also use the
+  `com.threerings.getdown.spi.ProxyAuth` service provider interface to persist the proxy
+  credentials supplied by the user. Otherwise they will be requested every time Getdown runs, which
+  is not a viable user experience.
+
+## 1.8.2 - Nov 27, 2018
+
+* Fixed a data corruption bug introduced at last minute into 1.8.1 release. Oops.
+
+## 1.8.1 - Nov 26, 2018
+
+* If both an `appbase` and `appdir` are provided via some means (bootstrap properties file, system
+  property, etc.) and the app dir does not yet exist, Getdown will create it.
+
+* Added `max_concurrent_downloads` setting to `getdown.txt`. Controls what you would expect.
+  Defaults to two.
+
+* `bootstrap.properties` can now contain system properties which will be set prior to running
+  Getdown. They must be prefixed by `sys.`: for example `sys.silent = true` will set the `silent`
+  system property to `true`.
+
+* If Getdown is run in a headless JVM, it will avoid showing a UI but will attempt to install and
+  launch the application anyhow. Note that passing `-Dsilent` will override this behavior (because
+  in silent mode the default is only to install the app, not also launch it).
+
+* Fixed issue with `appid` not being properly used when specified via command line arg.
+
+* Fixed issue with running Getdown on single CPU systems (or virtual systems). It was attempting to
+  create a thread pool of size zero, which failed.
+
+* Fixed issue with backslashes (or other regular expression escape characters) in environment
+  variables being substituted into app arguments.
+
+## 1.8.0 - Oct 19, 2018
+
+* Added support for manually specifying the thread pool size via `-Dthread_pool_size`. Also reduced
+  the default thread pool size to `num_cpus-1` from `num_cpus`.
+
+* Added support for bundling a `bootstrap.properties` file with the Getdown jar file, which can
+  specify defaults for `appdir`, `appbase` and `appid`.
+
+* Added support for a host URL whitelist. Getdown can be custom built to refuse to operate with any
+  URL that does not match the built-time-specified whitelist. See `core/pom.xml` for details.
+
+* Removed the obsolete support for running Getdown in a signed applet. Applets are no longer
+  supported by any widely used browser.
+
+* Split the project into multiple Maven modules. See the notes on [migrating from 1.7 to 1.8] for
+  details.
+
+* A wide variety of small cleanups resulting from a security review generously performed by a
+  prospective user. This includes various uses of deterministic locales and encodings instead of
+  the platform default locale/encoding, in cases where platform/locale-specific behavior is not
+  desired or needed.
+
+* Made use of `appid` fall back to main app class if no `appid`-specific class is specified.
+
+* Added support for marking resources as executable (via `xresource`).
+
+* Fixed issue where entire tracking URL was being URL encoded.
+
+* Changed translations to avoid the use of the term 'game'. Use 'app' instead.
+
+## 1.7.1 - Jun 6, 2018
+
+* Made it possible to use `appbase_domain` with `https` URLs.
+
+* Fixed issue with undecorated splash window being unclosable if failures happen early in
+  initialization process. (#57)
+
+* Added support for transparent splash window. (#92)
+
+* Fixed problem with unpacked code resources (`ucode`) and `pack.gz` files. (#95)
+
+* Changed default Java version regex to support new Java 9+ version formats. (#93)
+
+* Ensure correct signature algorithm is used for each version of digest files. (#91)
+
+* Use more robust delete in all cases where Getdown needs to delete files. This should fix issues
+  with lingering files on Windows (where sometimes delete fails spuriously).
+
+## 1.7.0 - Dec 12, 2017
+
+* Fixed issue with `Digester` thread pool not being shutdown. (#89)
+
+* Fixed resource unpacking, which was broken by earlier change introducing resource installation
+  (downloading to `_new` files and then renaming into place). (#88)
+
+* The connect and read timeouts specified by system properties are now used for all the various
+  connections made by Getdown.
+
+* Proxy detection now uses a 5 second connect/read timeout, to avoid stalling for a long time in
+  certain problematic network conditions.
+
+* Getdown is now built against JDK 1.7 and requires JDK 1.7 (or newer) to run. Use the latest
+  Getdown 1.6.x release if you need to support Java 1.6.
+
+## 1.6.4 - Sep 17, 2017
+
+* `digest.txt` (and `digest2.txt`) computation now uses parallel jobs. Each resource to be verified
+  is a single job and the jobs are doled out to a thread pool with #CPUs threads. This allows large
+  builds to proceed faster as most dev machines have more than one core.
+
+* Resource verification is now performed in parallel (similar to the `digest.txt` computation, each
+  resource is a job farmed out to a thread pool). For large installations on multi-core machines,
+  this speeds up the verification phase of an installation or update.
+
+* Socket reads now have a 30 second default timeout. This can be changed by passing
+  `-Dread_timeout=N` (where N is seconds) to the JVM running Getdown.
+
+* Fixed issue with failing to install a downloaded and validated `_new` file.
+
+* Added support for "strict comments". In this mode, Getdown only treats `#` as starting a comment
+  if it appears in column zero. This allows `#` to occur on the right hand side of configuration
+  values (like in file names). To enable, put `strict_comments = true` in your `getdown.txt` file.
+
+## 1.6.3 - Apr 23, 2017
+
+* Fixed error parsing `cache_retention_days`. (#82)
+
+* Fixed error with new code cache. (9e23a426)
+
+## 1.6.2 - Feb 12, 2017
+
+* Fixed issue with installing local JVM, caused by new resource installation process. (#78)
+
+* Local JVM now uses absolute path to avoid issues with cwd.
+
+* Added `override_appbase` system property. This enables a Getdown app that normally talks to some
+  download server to be installed in such a way that it instead talks to some other download
+  server.
+
+## 1.6.1 - Feb 12, 2017
+
+* Fix issues with URL path encoding when downloading resources. (84af080b0)
+
+* Parsing `digest.txt` changed to allow `=` to appear in the filename. In `getdown.txt` we split on
+  the first `=` because `=` never appears in a key but may appear in a value. But in `digest.txt`
+  the format is `filename = hash` and `=` never appears in the hash but may appear in the filename,
+  so there we want to split on the _last_ `=` not the first.
+
+* Fixed bug with progress tracking and reporting. (256e0933)
+
+* Fix executable permissions on `jspawnhelper`. (#74)
+
+## 1.6 - Nov 5, 2016
+
+* This release and all those before it are considered ancient history. Check the commit history for
+  more details on what was in each of these releases.
+
+## 1.0 - Sep 21, 2010
+
+* The first Maven release of Getdown.
+
+## 0.1 - July 19, 2004
+
+* The first production use of Getdown (on https://www.puzzlepirates.com which is miraculously still
+  operational as of 2018 when this changelog was created).
+
+[migrating from 1.7 to 1.8]: https://github.com/threerings/getdown/wiki/Migrate17to18
diff --git a/getdown/src/getdown/LICENSE b/getdown/src/getdown/LICENSE
new file mode 100644 (file)
index 0000000..0d9b255
--- /dev/null
@@ -0,0 +1,24 @@
+Getdown - application installer, patcher and launcher
+
+Copyright (C) 2004-2016 Getdown authors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
+EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGE.
diff --git a/getdown/src/getdown/README.md b/getdown/src/getdown/README.md
new file mode 100644 (file)
index 0000000..7059c61
--- /dev/null
@@ -0,0 +1,111 @@
+## What is it?
+
+Getdown (yes, it's the funky stuff) is a system for deploying Java applications to end-user
+computers, as well as keeping those applications up to date.
+
+It was designed as a replacement for [Java Web Start](https://docs.oracle.com/javase/8/docs/technotes/guides/javaws/)
+due to limitations in Java Web Start's architecture which are outlined in the
+[rationale](https://github.com/threerings/getdown/wiki/Rationale) section.
+
+Note: Getdown was designed *in 2004* as an alternative to Java Web Start, because of design choices
+made by JWS that were problematic to the use cases its authors had. It is _not_ a drop-in
+replacement for JWS, aimed to help the developers left in the lurch by the deprecation of JWS in
+Java 9. It may still be a viable alternative for developers looking to replace JWS, but don't
+expect to find feature parity with JWS.
+
+## How do I use it?
+
+A tutorial and more detailed specification are available from the [Documentation] page. Questions
+can be posted to the [OOO Libs Google group].
+
+Note that because one can not rely on users having a JRE installed, you must create a custom
+installer for each platform that you plan to support (Windows, macOS, Linux) that installs a JRE,
+the Getdown launcher jar file, a stub configuration file that identifies the URL at which your real
+app manifest is hosted, and whatever the appropiate "desktop integration" is that provides an icon
+the user can click on. We have some details on the
+[installers](https://github.com/threerings/getdown/wiki/Installers) documentation page, though it
+is unfortunately not very detailed.
+
+## How does it work?
+
+The main design and operation of Getdown is detailed on the
+[design](https://github.com/threerings/getdown/wiki/Design) page. You can also browse the
+[javadoc documentation] and [source code] if you're interested in implementation details.
+
+## Where can I see it in action?
+
+Getdown was originally written by developers at [OOO] for the deployment of their Java-based
+massively multiplayer games. Try out any of the following games to see it in action:
+
+  * [Puzzle Pirates](https://www.puzzlepirates.com/) - OOO
+  * [Spiral Knights](https://www.spiralknights.com/) - OOO
+
+Getdown is implemented in Java, and is designed to deploy and update JVM-based applications. While
+it would be technically feasible to use Getdown to deploy non-JVM-based applications, it is not
+currently supported and it is unlikely that the overhead of bundling a JVM just to run Getdown
+would be worth it if the JVM were not also being used to run the target application.
+
+## Release notes
+
+See [CHANGELOG.md](CHANGELOG.md) for release notes.
+
+## Obtaining Getdown
+
+Getdown will likely need to be integrated into your build. We have separate instructions for
+[build integration]. You can also download the individual jar files from Maven Central if needed.
+Getdown is comprised of three Maven artifacts (jar files), though you probably only need the first
+one:
+
+  * [getdown-launcher](http://repo2.maven.org/maven2/com/threerings/getdown/getdown-launcher)
+    contains minified (via Proguard) code that you actually run to update and launch your app. It
+    also contains the tools needed to build a Getdown app distribution.
+
+  * [getdown-core](http://repo2.maven.org/maven2/com/threerings/getdown/getdown-core) contains the
+    core logic for downloading, verifying, patching and launching an app as well as the core logic
+    for creating an app distribution. It does not contain any user interface code. You would only
+    use this artifact if you were planning to integrate Getdown directly into your app.
+
+  * [getdown-ant](http://repo2.maven.org/maven2/com/threerings/getdown/getdown-ant) contains an Ant
+    task for building a Getdown app distribution. See the [build integration] instructions for
+    details.
+
+You can also:
+
+  * [Check out the code](https://github.com/threerings/getdown) and build it yourself.
+  * Browse the [source code] online.
+  * View the [javadoc documentation] online.
+
+## JVM Version Requirements
+
+  * Getdown version 1.8.x requires Java 7 VM or newer.
+  * Getdown version 1.7.x requires Java 7 VM or newer.
+  * Getdown version 1.6.x requires Java 6 VM or newer.
+  * Getdown version 1.5 and earlier requires Java 5 VM or newer.
+
+## Migrating from Getdown 1.7 to Getdown 1.8
+
+See [this document](https://github.com/threerings/getdown/wiki/Migrating-from-1.7-to-1.8) on the
+changes needed to migrate from Getdown 1.7 to 1.8.
+
+## Building
+
+Getdown is built with Maven in the standard ways. Invoke the following commands, for fun and
+profit:
+
+```
+% mvn compile  # builds the classes
+% mvn test     # builds and runs the unit tests
+% mvn package  # builds and creates jar file
+% mvn install  # builds, jars and installs in your local Maven repository
+```
+
+## Discussion
+
+Feel free to pop over to the [OOO Libs Google Group] to ask questions and get (and give) answers.
+
+[Documentation]: https://github.com/threerings/getdown/wiki
+[OOO Libs Google group]: http://groups.google.com/group/ooo-libs
+[source code]: https://github.com/threerings/getdown/tree/master/src/main/java/com/threerings/getdown/launcher
+[javadoc documentation]: https://threerings.github.com/getdown/apidocs/
+[OOO]: https://en.wikipedia.org/wiki/Three_Rings_Design
+[build integration]: https://github.com/threerings/getdown/wiki/Build-Integration
diff --git a/getdown/src/getdown/ant/.project b/getdown/src/getdown/ant/.project
new file mode 100644 (file)
index 0000000..097cb89
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>getdown-ant</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.m2e.core.maven2Builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.eclipse.m2e.core.maven2Nature</nature>
+       </natures>
+</projectDescription>
diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..e9441bb
--- /dev/null
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..54e5672
--- /dev/null
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.7
diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs
new file mode 100644 (file)
index 0000000..f897a7f
--- /dev/null
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/getdown/src/getdown/ant/pom.xml b/getdown/src/getdown/ant/pom.xml
new file mode 100644 (file)
index 0000000..f8231aa
--- /dev/null
@@ -0,0 +1,28 @@
+<?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.getdown</groupId>
+    <artifactId>getdown</artifactId>
+    <version>1.8.3-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>getdown-ant</artifactId>
+  <packaging>jar</packaging>
+  <name>Getdown Ant Task</name>
+  <description>An Ant task for building Getdown app distributions</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.threerings.getdown</groupId>
+      <artifactId>getdown-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.ant</groupId>
+      <artifactId>ant</artifactId>
+      <version>1.7.1</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
new file mode 100644 (file)
index 0000000..48cc8d4
--- /dev/null
@@ -0,0 +1,94 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tools;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.security.GeneralSecurityException;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Task;
+
+import com.threerings.getdown.data.Digest;
+
+/**
+ * An ant task used to create a <code>digest.txt</code> for a Getdown
+ * application deployment.
+ */
+public class DigesterTask extends Task
+{
+    /**
+     * Sets the application directory.
+     */
+    public void setAppdir (File appdir)
+    {
+        _appdir = appdir;
+    }
+
+    /**
+     * Sets the digest signing keystore.
+     */
+    public void setKeystore (File path)
+    {
+        _storepath = path;
+    }
+
+    /**
+     * Sets the keystore decryption key.
+     */
+    public void setStorepass (String password)
+    {
+        _storepass = password;
+    }
+
+    /**
+     * Sets the private key alias.
+     */
+    public void setAlias (String alias)
+    {
+        _storealias = alias;
+    }
+
+    /**
+     * Performs the actual work of the task.
+     */
+    @Override
+    public void execute () throws BuildException
+    {
+        // make sure appdir is set
+        if (_appdir == null) {
+            throw new BuildException("Must specify the path to the application directory " +
+                                     "via the 'appdir' attribute.");
+        }
+
+        // make sure _storepass and _keyalias are set, if _storepath is set
+        if (_storepath != null && (_storepass == null || _storealias == null)) {
+            throw new BuildException(
+                    "Must specify both a keystore password and a private key alias.");
+        }
+
+        try {
+            Digester.createDigests(_appdir, _storepath, _storepass, _storealias);
+        } catch (IOException ioe) {
+            throw new BuildException("Error creating digest: " + ioe.getMessage(), ioe);
+        } catch (GeneralSecurityException gse) {
+            throw new BuildException("Error creating signature: " + gse.getMessage(), gse);
+        }
+    }
+
+    /** The application directory in which we're creating a digest file. */
+    protected File _appdir;
+
+    /** The path to the keystore we'll use to sign the digest file, if any. */
+    protected File _storepath;
+
+    /** The decryption key for the keystore. */
+    protected String _storepass;
+
+    /** The private key alias. */
+    protected String _storealias;
+}
diff --git a/getdown/src/getdown/ant/target/getdown-ant-1.8.3-SNAPSHOT.jar b/getdown/src/getdown/ant/target/getdown-ant-1.8.3-SNAPSHOT.jar
new file mode 100644 (file)
index 0000000..ff501eb
Binary files /dev/null and b/getdown/src/getdown/ant/target/getdown-ant-1.8.3-SNAPSHOT.jar differ
diff --git a/getdown/src/getdown/ant/target/maven-archiver/pom.properties b/getdown/src/getdown/ant/target/maven-archiver/pom.properties
new file mode 100644 (file)
index 0000000..60817f2
--- /dev/null
@@ -0,0 +1,5 @@
+#Generated by Maven
+#Fri Apr 05 14:07:51 BST 2019
+version=1.8.3-SNAPSHOT
+groupId=com.threerings.getdown
+artifactId=getdown-ant
diff --git a/getdown/src/getdown/ant/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/getdown/src/getdown/ant/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644 (file)
index 0000000..2ad1bd7
--- /dev/null
@@ -0,0 +1 @@
+com/threerings/getdown/tools/DigesterTask.class
diff --git a/getdown/src/getdown/ant/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/getdown/src/getdown/ant/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644 (file)
index 0000000..f3127b7
--- /dev/null
@@ -0,0 +1 @@
+/Users/bsoares/git/getdown2/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
diff --git a/getdown/src/getdown/bin/differ b/getdown/src/getdown/bin/differ
new file mode 100755 (executable)
index 0000000..f48ed89
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# we'll magically try to match dist/getdown.jar or target/getdown-1.x-SNAPSHOT.jar
+java -classpath */getdown*.jar com.threerings.getdown.tools.Differ "$@"
diff --git a/getdown/src/getdown/bin/patcher b/getdown/src/getdown/bin/patcher
new file mode 100755 (executable)
index 0000000..e09f67d
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# we'll magically try to match dist/getdown.jar or target/getdown-1.x-SNAPSHOT.jar
+java -classpath */getdown*.jar com.threerings.getdown.tools.Patcher "$@"
diff --git a/getdown/src/getdown/core/.project b/getdown/src/getdown/core/.project
new file mode 100644 (file)
index 0000000..177252f
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>getdown-core</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.m2e.core.maven2Builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.eclipse.m2e.core.maven2Nature</nature>
+       </natures>
+</projectDescription>
diff --git a/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..0a9bbb8
--- /dev/null
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/it/java=UTF-8
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..54e5672
--- /dev/null
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.7
diff --git a/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs
new file mode 100644 (file)
index 0000000..f897a7f
--- /dev/null
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/getdown/src/getdown/core/pom.xml b/getdown/src/getdown/core/pom.xml
new file mode 100644 (file)
index 0000000..e564bf6
--- /dev/null
@@ -0,0 +1,132 @@
+<?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.getdown</groupId>
+    <artifactId>getdown</artifactId>
+    <version>1.8.3-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>getdown-core</artifactId>
+  <packaging>jar</packaging>
+  <name>Getdown Core</name>
+  <description>Core Getdown functionality</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>2.22.0</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <!-- By default, no host whitelist is added to the binary, so it can be used
+       to download and run applications from any server. To create a custom
+       Getdown build that can only talk to whitelisted servers, set this
+       property on the command line, e.g. -Dgetdown.host.whitelist=my.server.com
+       Wildcards can be used (*.mycompany.com) and multiple values can be
+       separated by commas (app1.foo.com,app2.bar.com,app3.baz.com). -->
+  <properties>
+    <getdown.host.whitelist />
+  </properties>
+
+  <build>
+    <resources>
+      <resource> <!-- include the LICENSE file in the jar -->
+        <directory>..</directory>
+        <includes><include>LICENSE</include></includes>
+      </resource>
+    </resources>
+
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <version>1.5</version>
+        <executions>
+          <execution>
+            <id>add-test-source</id>
+            <phase>process-resources</phase>
+            <goals>
+              <goal>add-test-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>src/it/java</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <version>1.8</version>
+        <executions>
+          <execution>
+            <id>gen-build</id>
+            <phase>generate-sources</phase>
+            <configuration>
+              <target>
+                <tstamp>
+                  <format property="getdown.build.time" pattern="yyyy-MM-dd HH:mm" />
+                </tstamp>
+                <copy file="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java.tmpl" tofile="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java" overwrite="true">
+                  <filterset>
+                    <filter token="build_time" value="${getdown.build.time}" />
+                    <filter token="build_version" value="${project.version}" />
+                    <filter token="host_whitelist" value="${getdown.host.whitelist}" />
+                  </filterset>
+                </copy>
+              </target>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-clean-plugin</artifactId>
+        <version>3.1.0</version>
+        <configuration>
+          <filesets>
+            <fileset>
+              <directory>${project.build.sourceDirectory}/</directory>
+              <includes>
+                <include>com/threerings/getdown/data/Build.java</include>
+              </includes>
+              <followSymlinks>false</followSymlinks>
+            </fileset>
+          </filesets>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <version>2.22.0</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <useFile>false</useFile>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java b/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
new file mode 100644 (file)
index 0000000..52b4b5e
--- /dev/null
@@ -0,0 +1,54 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tests;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.*;
+import static org.junit.Assert.*;
+
+import com.threerings.getdown.tools.Digester;
+
+public class DigesterIT {
+
+    @Test
+    public void testDigester () throws Exception {
+        Path appdir = Paths.get("src/it/resources/testapp");
+        Digester.createDigests(appdir.toFile(), null, null, null);
+
+        Path digest = appdir.resolve("digest.txt");
+        List<String> digestLines = Files.readAllLines(digest, StandardCharsets.UTF_8);
+        Files.delete(digest);
+
+        Path digest2 = appdir.resolve("digest2.txt");
+        List<String> digest2Lines = Files.readAllLines(digest2, StandardCharsets.UTF_8);
+        Files.delete(digest2);
+
+        assertEquals(Arrays.asList(
+            "getdown.txt = 779c74fb4b251e18faf6e240a0667964",
+            "testapp.jar = 404dafa55e78b25ec0e3a936357b1883",
+            "funny%test dir/some=file.txt = d8e8fca2dc0f896fd7cb4cb0031ba249",
+            "crazyhashfile#txt = f29d23fd5ab1781bd8d0760b3a516f16",
+            "foo.jar = 46ca4cc9079d9d019bb30cd21ebbc1ec",
+            "script.sh = f66e8ea25598e67e99c47d9b0b2a2cdf",
+            "digest.txt = f5561d85e4d80cc85883963897e58ff6"
+        ), digestLines);
+
+        assertEquals(Arrays.asList(
+            "getdown.txt = 4f0c657895c3c3a35fa55bf5951c64fa9b0694f8fc685af3f1d8635c639e066b",
+            "testapp.jar = c9cb1906afbf48f8654b416c3f831046bd3752a76137e5bf0a9af2f790bf48e0",
+            "funny%test dir/some=file.txt = f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
+            "crazyhashfile#txt = 6816889f922de38f145db215a28ad7c5e1badf7354b5cdab225a27486789fa3b",
+            "foo.jar = ea188b872e0496debcbe00aaadccccb12a8aa9b025bb62c130cd3d9b8540b062",
+            "script.sh = cca1c5c7628d9bf7533f655a9cfa6573d64afb8375f81960d1d832dc5135c988",
+            "digest2.txt = 70b442c9f56660561921da3368e1a206f05c379182fab3062750b7ddcf303407"
+        ), digest2Lines);
+    }
+}
diff --git a/getdown/src/getdown/core/src/it/resources/testapp/background.png b/getdown/src/getdown/core/src/it/resources/testapp/background.png
new file mode 100644 (file)
index 0000000..ff6a6ee
Binary files /dev/null and b/getdown/src/getdown/core/src/it/resources/testapp/background.png differ
diff --git a/getdown/src/getdown/core/src/it/resources/testapp/crazyhashfile#txt b/getdown/src/getdown/core/src/it/resources/testapp/crazyhashfile#txt
new file mode 100644 (file)
index 0000000..33bc373
--- /dev/null
@@ -0,0 +1 @@
+Hello crazy world.
diff --git a/getdown/src/getdown/core/src/it/resources/testapp/foo.jar b/getdown/src/getdown/core/src/it/resources/testapp/foo.jar
new file mode 100644 (file)
index 0000000..d040c01
Binary files /dev/null and b/getdown/src/getdown/core/src/it/resources/testapp/foo.jar differ
diff --git a/getdown/src/getdown/core/src/it/resources/testapp/funny%test dir/some=file.txt b/getdown/src/getdown/core/src/it/resources/testapp/funny%test dir/some=file.txt
new file mode 100644 (file)
index 0000000..9daeafb
--- /dev/null
@@ -0,0 +1 @@
+test
diff --git a/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt b/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt
new file mode 100644 (file)
index 0000000..3e0e538
--- /dev/null
@@ -0,0 +1,28 @@
+# where our app is hosted on the internets
+appbase = http://notused.com/testapp
+
+# the jar file that contains our code
+code = testapp.jar
+
+# the main entry point of our app
+class = com.threerings.testapp.TestApp
+
+# we pass the appdir to our app so that it can upgrade getdown
+apparg = %APPDIR%
+
+# test the %env% mechanism
+jvmarg = -Dusername=\%ENV.USER%
+
+strict_comments = true
+resource = funny%test dir/some=file.txt
+resource = crazyhashfile#txt
+uresource = foo.jar
+xresource = script.sh
+
+ui.name = Getdown Test App
+ui.background_image = background.png
+ui.progress = 17, 321, 458, 22
+ui.progress_bar = 336600
+ui.progress_text = FFFFFF
+ui.status = 57, 245, 373, 68
+ui.status_text = 000000
diff --git a/getdown/src/getdown/core/src/it/resources/testapp/script.sh b/getdown/src/getdown/core/src/it/resources/testapp/script.sh
new file mode 100644 (file)
index 0000000..e3a1aba
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Hello world!"
diff --git a/getdown/src/getdown/core/src/it/resources/testapp/testapp.jar b/getdown/src/getdown/core/src/it/resources/testapp/testapp.jar
new file mode 100644 (file)
index 0000000..fe9de02
Binary files /dev/null and b/getdown/src/getdown/core/src/it/resources/testapp/testapp.jar differ
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java
new file mode 100644 (file)
index 0000000..13b9956
--- /dev/null
@@ -0,0 +1,141 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.FieldPosition;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.logging.*;
+
+/**
+ * A placeholder class that contains a reference to the log object used by the Getdown code.
+ */
+public class Log
+{
+    public static class Shim {
+        /**
+         * Logs a debug message.
+         *
+         * @param message the message to be logged.
+         * @param args a list of key/value pairs and an optional final Throwable.
+         */
+        public void debug (Object message, Object... args) { doLog(0, message, args); }
+
+        /**
+         * Logs an info message.
+         *
+         * @param message the message to be logged.
+         * @param args a list of key/value pairs and an optional final Throwable.
+         */
+        public void info (Object message, Object... args) { doLog(1, message, args); }
+
+        /**
+         * Logs a warning message.
+         *
+         * @param message the message to be logged.
+         * @param args a list of key/value pairs and an optional final Throwable.
+         */
+        public void warning (Object message, Object... args) { doLog(2, message, args); }
+
+        /**
+         * Logs an error message.
+         *
+         * @param message the message to be logged.
+         * @param args a list of key/value pairs and an optional final Throwable.
+         */
+        public void error (Object message, Object... args) { doLog(3, message, args); }
+
+        protected void doLog (int levIdx, Object message, Object[] args) {
+            if (_impl.isLoggable(LEVELS[levIdx])) {
+                Throwable err = null;
+                int nn = args.length;
+                if (message instanceof Throwable) {
+                    err = (Throwable)message;
+                } else if (nn % 2 == 1 && (args[nn - 1] instanceof Throwable)) {
+                    err = (Throwable)args[--nn];
+                }
+                _impl.log(LEVELS[levIdx], format(message, args), err);
+            }
+        }
+
+        protected final Logger _impl = Logger.getLogger("com.threerings.getdown");
+    }
+
+    /** We dispatch our log messages through this logging shim. */
+    public static final Shim log = new Shim();
+
+    public static String format (Object message, Object... args) {
+        if (args.length < 2) return String.valueOf(message);
+        StringBuilder buf = new StringBuilder(String.valueOf(message));
+        if (buf.length() > 0) {
+            buf.append(' ');
+        }
+        buf.append('[');
+        for (int ii = 0; ii < args.length; ii += 2) {
+            if (ii > 0) {
+                buf.append(',').append(' ');
+            }
+            buf.append(args[ii]).append('=');
+            try {
+                buf.append(args[ii+1]);
+            } catch (Throwable t) {
+                buf.append("<toString() failure: ").append(t).append(">");
+            }
+        }
+        return buf.append(']').toString();
+    }
+
+    static {
+        Formatter formatter = new OneLineFormatter();
+        Logger logger = LogManager.getLogManager().getLogger("");
+        for (Handler handler : logger.getHandlers()) {
+            handler.setFormatter(formatter);
+        }
+    }
+
+    protected static class OneLineFormatter extends Formatter {
+        @Override public String format (LogRecord record) {
+            StringBuffer buf = new StringBuffer();
+
+            // append the timestamp
+            _date.setTime(record.getMillis());
+            _format.format(_date, buf, _fpos);
+
+            // append the log level
+            buf.append(" ");
+            buf.append(record.getLevel().getLocalizedName());
+            buf.append(" ");
+
+            // append the message itself
+            buf.append(formatMessage(record));
+            buf.append(System.lineSeparator());
+
+            // if an exception was also provided, append that
+            if (record.getThrown() != null) {
+                try {
+                    StringWriter sw = new StringWriter();
+                    PrintWriter pw = new PrintWriter(sw);
+                    record.getThrown().printStackTrace(pw);
+                    pw.close();
+                    buf.append(sw.toString());
+                } catch (Exception ex) {
+                    buf.append("Format failure:").append(ex);
+                }
+            }
+
+            return buf.toString();
+        }
+
+        protected Date _date = new Date();
+        protected SimpleDateFormat _format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss:SSS");
+        protected FieldPosition _fpos = new FieldPosition(SimpleDateFormat.DATE_FIELD);
+    }
+
+    protected static final String DATE_FORMAT = "{0,date} {0,time}";
+    protected static final Level[] LEVELS = {Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE};
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
new file mode 100644 (file)
index 0000000..67ea645
--- /dev/null
@@ -0,0 +1,99 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.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);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Collect and delete garbage in the native cache. It tries to find a jar file with a matching
+     * last modified file, and deletes the entire directory accordingly.
+     */
+    public static void collectNative (File cacheDir, final long retentionPeriodMillis)
+    {
+        File[] subdirs = cacheDir.listFiles();
+        if (subdirs != null) {
+            for (File dir : subdirs) {
+                if (dir.isDirectory()) {
+                    // Get all the native jars in the directory (there should only be one)
+                    for (File file : dir.listFiles()) {
+                        if (!file.getName().endsWith(".jar")) {
+                            continue;
+                        }
+                        File cachedFile = getCachedFile(file);
+                        File lastAccessedFile = getLastAccessedFile(file);
+                        if (!cachedFile.exists() || !lastAccessedFile.exists() ||
+                            shouldDelete(lastAccessedFile, retentionPeriodMillis)) {
+                            FileUtil.deleteDirHarder(dir);
+                        }
+                    }
+                } else {
+                    // @TODO There shouldn't be any loose files in native/ but if there are then
+                    // what? Delete them? file.delete();
+                }
+            }
+        }
+    }
+
+    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(".")));
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
new file mode 100644 (file)
index 0000000..0210e9a
--- /dev/null
@@ -0,0 +1,80 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.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 its {@code digest}.
+     * @param fileToCache file to cache.
+     * @param cacheSubdir the subdirectory of the cache directory in which to store the cached
+     * file. Usually either {@code digest} or a prefix of {@code digest}.
+     * @param digest a crypto digest of the cached files contents.
+     * @return the cached file.
+     */
+    public File cacheFile (File fileToCache, String cacheSubdir, String digest) throws IOException
+    {
+        File cacheLocation = new File(_cacheDir, cacheSubdir);
+        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";
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
new file mode 100644 (file)
index 0000000..501407c
--- /dev/null
@@ -0,0 +1,1826 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.*;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
+import com.sun.management.OperatingSystemMXBean;
+import java.lang.management.ManagementFactory;
+
+
+import com.threerings.getdown.util.*;
+// avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+
+import com.threerings.getdown.util.Base64;
+
+import static com.threerings.getdown.Log.log;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Parses and provide access to the information contained in the <code>getdown.txt</code>
+ * configuration file.
+ */
+public class Application
+{
+    /** The name of our configuration file. */
+    public static final String CONFIG_FILE = "getdown.txt";
+
+    /** The name of our target version file. */
+    public static final String VERSION_FILE = "version.txt";
+
+    /** System properties that are prefixed with this string will be passed through to our
+     * application (minus this prefix). */
+    public static final String PROP_PASSTHROUGH_PREFIX = "app.";
+
+    /** Suffix used for control file signatures. */
+    public static final String SIGNATURE_SUFFIX = ".sig";
+
+    /** A special classname that means 'use -jar code.jar' instead of a classname. */
+    public static final String MANIFEST_CLASS = "manifest";
+
+    /** Used to communicate information about the UI displayed when updating the application. */
+    public static final class UpdateInterface
+    {
+        /**
+         * The major steps involved in updating, along with some arbitrary percentages
+         * assigned to them, to mark global progress.
+         */
+        public enum Step
+        {
+            UPDATE_JAVA(10),
+            VERIFY_METADATA(15, 65, 95),
+            DOWNLOAD(40),
+            PATCH(60),
+            VERIFY_RESOURCES(70, 97),
+            REDOWNLOAD_RESOURCES(90),
+            UNPACK(98),
+            LAUNCH(99);
+
+            /** What is the final percent value for this step? */
+            public final List<Integer> defaultPercents;
+
+            /** Enum constructor. */
+            Step (int... percents)
+            {
+                this.defaultPercents = intsToList(percents);
+            }
+        }
+
+        /** The human readable name of this application. */
+        public final String name;
+
+        /** A background color, just in case. */
+        public final int background;
+
+        /** Background image specifiers for `RotatingBackgrounds`. */
+        public final List<String> rotatingBackgrounds;
+
+        /** The error background image for `RotatingBackgrounds`. */
+        public final String errorBackground;
+
+        /** The paths (relative to the appdir) of images for the window icon. */
+        public final List<String> iconImages;
+
+        /** The path (relative to the appdir) to a single background image. */
+        public final String backgroundImage;
+
+        /** The path (relative to the appdir) to the progress bar image. */
+        public final String progressImage;
+
+        /** The dimensions of the progress bar. */
+        public final Rectangle progress;
+
+        /** The color of the progress text. */
+        public final int progressText;
+
+        /** The color of the progress bar. */
+        public final int progressBar;
+
+        /** The dimensions of the status display. */
+        public final Rectangle status;
+
+        /** The color of the status text. */
+        public final int statusText;
+
+        /** The color of the text shadow. */
+        public final int textShadow;
+
+        /** Where to point the user for help with install errors. */
+        public final String installError;
+
+        /** The dimensions of the patch notes button. */
+        public final Rectangle patchNotes;
+
+        /** The patch notes URL. */
+        public final String patchNotesUrl;
+
+        /** Whether window decorations are hidden for the UI. */
+        public final boolean hideDecorations;
+
+        /** Whether progress text should be hidden or not. */
+        public final boolean hideProgressText;
+
+        /** The minimum number of seconds to display the GUI. This is to prevent the GUI from
+          * flashing up on the screen and immediately disappearing, which can be confusing to the
+          * user. */
+        public final int minShowSeconds;
+
+        /** The global percentages for each step. A step may have more than one, and
+         * the lowest reasonable one is used if a step is revisited. */
+        public final Map<Step, List<Integer>> stepPercentages;
+
+        /** Generates a string representation of this instance. */
+        @Override
+        public String toString ()
+        {
+            return "[name=" + name + ", bg=" + background + ", bg=" + backgroundImage +
+                ", pi=" + progressImage + ", prect=" + progress + ", pt=" + progressText +
+                ", pb=" + progressBar + ", srect=" + status + ", st=" + statusText +
+                ", shadow=" + textShadow + ", err=" + installError + ", nrect=" + patchNotes +
+                ", notes=" + patchNotesUrl + ", stepPercentages=" + stepPercentages +
+                ", hideProgressText" + hideProgressText + ", minShow=" + minShowSeconds + "]";
+        }
+
+        public UpdateInterface (Config config)
+        {
+            this.name = config.getString("ui.name");
+            this.progress = config.getRect("ui.progress", new Rectangle(5, 5, 300, 15));
+            this.progressText = config.getColor("ui.progress_text", Color.BLACK);
+            this.hideProgressText =  config.getBoolean("ui.hide_progress_text");
+            this.minShowSeconds = config.getInt("ui.min_show_seconds", 5);
+            this.progressBar = config.getColor("ui.progress_bar", 0x6699CC);
+            this.status = config.getRect("ui.status", new Rectangle(5, 25, 500, 100));
+            this.statusText = config.getColor("ui.status_text", Color.BLACK);
+            this.textShadow = config.getColor("ui.text_shadow", Color.CLEAR);
+            this.hideDecorations = config.getBoolean("ui.hide_decorations");
+            this.backgroundImage = config.getString("ui.background_image");
+            // default to black or white bg color, depending on the brightness of the progressText
+            int defaultBackground = (0.5f < Color.brightness(this.progressText)) ?
+                Color.BLACK : Color.WHITE;
+            this.background = config.getColor("ui.background", defaultBackground);
+            this.progressImage = config.getString("ui.progress_image");
+            this.rotatingBackgrounds = stringsToList(
+                config.getMultiValue("ui.rotating_background"));
+            this.iconImages = stringsToList(config.getMultiValue("ui.icon"));
+            this.errorBackground = config.getString("ui.error_background");
+
+            // On an installation error, where do we point the user.
+            String installError = config.getUrl("ui.install_error", null);
+            this.installError = (installError == null) ?
+                "m.default_install_error" : MessageUtil.taint(installError);
+
+            // the patch notes bits
+            this.patchNotes = config.getRect("ui.patch_notes", new Rectangle(5, 50, 112, 26));
+            this.patchNotesUrl = config.getUrl("ui.patch_notes_url", null);
+
+            // step progress percentage (defaults and then customized values)
+            EnumMap<Step, List<Integer>> stepPercentages = new EnumMap<>(Step.class);
+            for (Step step : Step.values()) {
+                stepPercentages.put(step, step.defaultPercents);
+            }
+            for (UpdateInterface.Step step : UpdateInterface.Step.values()) {
+                String spec = config.getString("ui.percents." + step.name());
+                if (spec != null) {
+                    try {
+                        stepPercentages.put(step, intsToList(StringUtil.parseIntArray(spec)));
+                    } catch (Exception e) {
+                        log.warning("Failed to parse percentages for " + step + ": " + spec);
+                    }
+                }
+            }
+            this.stepPercentages = Collections.unmodifiableMap(stepPercentages);
+        }
+    }
+
+    /**
+     * Used by {@link #verifyMetadata} to communicate status in circumstances where it needs to
+     * take network actions.
+     */
+    public static interface StatusDisplay
+    {
+        /** Requests that the specified status message be displayed. */
+        public void updateStatus (String message);
+    }
+
+    /**
+     * Contains metadata for an auxiliary resource group.
+     */
+    public static class AuxGroup {
+        public final String name;
+        public final List<Resource> codes;
+        public final List<Resource> rsrcs;
+
+        public AuxGroup (String name, List<Resource> codes, List<Resource> rsrcs) {
+            this.name = name;
+            this.codes = Collections.unmodifiableList(codes);
+            this.rsrcs = Collections.unmodifiableList(rsrcs);
+        }
+    }
+
+    /** The proxy that should be used to do HTTP downloads. This must be configured prior to using
+      * the application instance. Yes this is a public mutable field, no I'm not going to create a
+      * getter and setter just to pretend like that's not the case. */
+    public Proxy proxy = Proxy.NO_PROXY;
+
+    /**
+     * Creates an application instance which records the location of the <code>getdown.txt</code>
+     * configuration file from the supplied application directory.
+     *
+     */
+    public Application (EnvConfig envc) {
+        _envc = envc;
+        _config = getLocalPath(envc.appDir, CONFIG_FILE);
+    }
+
+    /**
+     * Returns the configured application directory.
+     */
+    public File getAppDir () {
+        return _envc.appDir;
+    }
+
+    /**
+     * Returns whether the application should cache code resources prior to launching the
+     * application.
+     */
+    public boolean useCodeCache ()
+    {
+        return _useCodeCache;
+    }
+
+    /**
+     * Returns the number of days a cached code resource is allowed to stay unused before it
+     * becomes eligible for deletion.
+     */
+    public int getCodeCacheRetentionDays ()
+    {
+        return _codeCacheRetentionDays;
+    }
+
+    /**
+     * Returns the configured maximum concurrent downloads. Used to cap simultaneous downloads of
+     * app files from its hosting server.
+     */
+    public int maxConcurrentDownloads () {
+        return _maxConcDownloads;
+    }
+
+    /**
+     * Returns a resource that refers to the application configuration file itself.
+     */
+    public Resource getConfigResource ()
+    {
+        try {
+            return createResource(CONFIG_FILE, Resource.NORMAL);
+        } catch (Exception e) {
+            throw new RuntimeException("Invalid appbase '" + _vappbase + "'.", e);
+        }
+    }
+
+    /**
+     * Returns a list of the code {@link Resource} objects used by this application.
+     */
+    public List<Resource> getCodeResources ()
+    {
+        return _codes;
+    }
+
+    /**
+     * Returns a list of the non-code {@link Resource} objects used by this application.
+     */
+    public List<Resource> getResources ()
+    {
+        return _resources;
+    }
+
+    /**
+     * Returns the digest of the given {@code resource}.
+     */
+    public String getDigest (Resource resource)
+    {
+        return _digest.getDigest(resource);
+    }
+
+    /**
+     * Returns a list of all the active {@link Resource} objects used by this application (code and
+     * non-code).
+     */
+    public List<Resource> getAllActiveResources ()
+    {
+        List<Resource> allResources = new ArrayList<>();
+        allResources.addAll(getActiveCodeResources());
+        allResources.addAll(getActiveResources());
+        return allResources;
+    }
+
+    /**
+     * Returns the auxiliary resource group with the specified name, or null.
+     */
+    public AuxGroup getAuxGroup (String name)
+    {
+        return _auxgroups.get(name);
+    }
+
+    /**
+     * Returns the set of all auxiliary resource groups defined by the application. An auxiliary
+     * resource group is a collection of resource files that are not downloaded unless a group
+     * token file is present in the application directory.
+     */
+    public Iterable<AuxGroup> getAuxGroups ()
+    {
+        return _auxgroups.values();
+    }
+
+    /**
+     * Returns true if the specified auxgroup has been "activated", false if not. Non-activated
+     * groups should be ignored, activated groups should be downloaded and patched along with the
+     * main resources.
+     */
+    public boolean isAuxGroupActive (String auxgroup)
+    {
+        Boolean active = _auxactive.get(auxgroup);
+        if (active == null) {
+            // TODO: compare the contents with the MD5 hash of the auxgroup name and the client's
+            // machine ident
+            active = getLocalPath(auxgroup + ".dat").exists();
+            _auxactive.put(auxgroup, active);
+        }
+        return active;
+    }
+
+    /**
+     * Returns all main code resources and all code resources from active auxiliary resource groups.
+     */
+    public List<Resource> getActiveCodeResources ()
+    {
+        ArrayList<Resource> codes = new ArrayList<>();
+        codes.addAll(getCodeResources());
+        for (AuxGroup aux : getAuxGroups()) {
+            if (isAuxGroupActive(aux.name)) {
+                codes.addAll(aux.codes);
+            }
+        }
+        return codes;
+    }
+
+    /**
+     * Returns all resources indicated to contain native library files (.dll, .so, etc.).
+     */
+    public List<Resource> getNativeResources ()
+    {
+        List<Resource> natives = new ArrayList<>();
+        for (Resource resource: _resources) {
+            if (resource.isNative()) {
+                natives.add(resource);
+            }
+        }
+        return natives;
+    }
+
+    /**
+     * Returns all non-code resources and all resources from active auxiliary resource groups.
+     */
+    public List<Resource> getActiveResources ()
+    {
+        ArrayList<Resource> rsrcs = new ArrayList<>();
+        rsrcs.addAll(getResources());
+        for (AuxGroup aux : getAuxGroups()) {
+            if (isAuxGroupActive(aux.name)) {
+                rsrcs.addAll(aux.rsrcs);
+            }
+        }
+        return rsrcs;
+    }
+
+    /**
+     * Returns a resource that can be used to download a patch file that will bring this
+     * application from its current version to the target version.
+     *
+     * @param auxgroup the auxiliary resource group for which a patch resource is desired or null
+     * for the main application patch resource.
+     */
+    public Resource getPatchResource (String auxgroup)
+    {
+        if (_targetVersion <= _version) {
+            log.warning("Requested patch resource for up-to-date or non-versioned application",
+                "cvers", _version, "tvers", _targetVersion);
+            return null;
+        }
+
+        String infix = (auxgroup == null) ? "" : ("-" + auxgroup);
+        String pfile = "patch" + infix + _version + ".dat";
+        try {
+            URL remote = new URL(createVAppBase(_targetVersion), encodePath(pfile));
+            return new Resource(pfile, remote, getLocalPath(pfile), Resource.NORMAL);
+        } catch (Exception e) {
+            log.warning("Failed to create patch resource path",
+                "pfile", pfile, "appbase", _appbase, "tvers", _targetVersion, "error", e);
+            return null;
+        }
+    }
+
+    /**
+     * Returns a resource for a zip file containing a Java VM that can be downloaded to use in
+     * place of the installed VM (in the case where the VM that launched Getdown does not meet the
+     * application's version requirements) or null if no VM is available for this platform.
+     */
+    public Resource getJavaVMResource ()
+    {
+        if (StringUtil.isBlank(_javaLocation)) {
+            return null;
+        }
+
+        String vmfile = LaunchUtil.LOCAL_JAVA_DIR + ".jar";
+               log.info("vmfile is '"+vmfile+"'");
+               System.out.println("vmfile is '"+vmfile+"'");
+        try {
+            URL remote = new URL(createVAppBase(_targetVersion), encodePath(_javaLocation));
+            log.info("Attempting to fetch jvm at "+remote.toString());
+            System.out.println("Attempting to fetch jvm at "+remote.toString());
+            return new Resource(vmfile, remote, getLocalPath(vmfile),
+                                EnumSet.of(Resource.Attr.UNPACK, Resource.Attr.CLEAN));
+        } catch (Exception e) {
+            log.warning("Failed to create VM resource", "vmfile", vmfile, "appbase", _appbase,
+                "tvers", _targetVersion, "javaloc", _javaLocation, "error", e);
+            System.out.println("Failed to create VM resource: vmfile="+vmfile+", appbase="+_appbase+
+                ", tvers="+_targetVersion+", javaloc="+_javaLocation+", error="+e);
+            return null;
+        }
+    }
+
+    /**
+     * Returns a resource that can be used to download an archive containing all files belonging to
+     * the application.
+     */
+    public Resource getFullResource ()
+    {
+        String file = "full";
+        try {
+            URL remote = new URL(createVAppBase(_targetVersion), encodePath(file));
+            return new Resource(file, remote, getLocalPath(file), Resource.NORMAL);
+        } catch (Exception e) {
+            log.warning("Failed to create full resource path",
+                "file", file, "appbase", _appbase, "tvers", _targetVersion, "error", e);
+            return null;
+        }
+    }
+
+    /**
+     * Returns the URL to use to report an initial download event. Returns null if no tracking
+     * start URL was configured for this application.
+     *
+     * @param event the event to be reported: start, jvm_start, jvm_complete, complete.
+     */
+    public URL getTrackingURL (String event)
+    {
+        try {
+            String suffix = _trackingURLSuffix == null ? "" : _trackingURLSuffix;
+            String ga = getGATrackingCode();
+            return _trackingURL == null ? null :
+                HostWhitelist.verify(new URL(_trackingURL + encodePath(event + suffix + ga)));
+        } catch (MalformedURLException mue) {
+            log.warning("Invalid tracking URL", "path", _trackingURL, "event", event, "error", mue);
+            return null;
+        }
+    }
+
+    /**
+     * Returns the URL to request to report that we have reached the specified percentage of our
+     * initial download. Returns null if no tracking request was configured for the specified
+     * percentage.
+     */
+    public URL getTrackingProgressURL (int percent)
+    {
+        if (_trackingPcts == null || !_trackingPcts.contains(percent)) {
+            return null;
+        }
+        return getTrackingURL("pct" + percent);
+    }
+
+    /**
+     * Returns the name of our tracking cookie or null if it was not set.
+     */
+    public String getTrackingCookieName ()
+    {
+        return _trackingCookieName;
+    }
+
+    /**
+     * Returns the name of our tracking cookie system property or null if it was not set.
+     */
+    public String getTrackingCookieProperty ()
+    {
+        return _trackingCookieProperty;
+    }
+
+    /**
+     * Instructs the application to parse its {@code getdown.txt} configuration and prepare itself
+     * for operation. The application base URL will be parsed first so that if there are errors
+     * discovered later, the caller can use the application base to download a new {@code
+     * getdown.txt} file and try again.
+     *
+     * @return a {@code Config} instance that contains information from the config file.
+     *
+     * @exception IOException thrown if there is an error reading the file or an error encountered
+     * during its parsing.
+     */
+    public Config init (boolean checkPlatform)
+        throws IOException
+    {
+        Config config = null;
+        File cfgfile = _config;
+        Config.ParseOpts opts = Config.createOpts(checkPlatform);
+        try {
+            // if we have a configuration file, read the data from it
+            if (cfgfile.exists()) {
+                config = Config.parseConfig(_config, opts);
+            }
+            // otherwise, try reading data from our backup config file; thanks to funny windows
+            // bullshit, we have to do this backup file fiddling in case we got screwed while
+            // updating getdown.txt during normal operation
+            else if ((cfgfile = getLocalPath(CONFIG_FILE + "_old")).exists()) {
+                config = Config.parseConfig(cfgfile, opts);
+            }
+            // otherwise, issue a warning that we found no getdown file
+            else {
+                log.info("Found no getdown.txt file", "appdir", getAppDir());
+            }
+        } catch (Exception e) {
+            log.warning("Failure reading config file", "file", config, e);
+        }
+
+        // if we failed to read our config file, check for an appbase specified via a system
+        // property; we can use that to bootstrap ourselves back into operation
+        if (config == null) {
+            String appbase = _envc.appBase;
+            log.info("Using 'appbase' from bootstrap config", "appbase", appbase);
+            Map<String, Object> cdata = new HashMap<>();
+            cdata.put("appbase", appbase);
+            config = new Config(cdata);
+        }
+
+        // first determine our application base, this way if anything goes wrong later in the
+        // process, our caller can use the appbase to download a new configuration file
+        _appbase = config.getString("appbase");
+        if (_appbase == null) {
+            throw new RuntimeException("m.missing_appbase");
+        }
+
+        // check if we're overriding the domain in the appbase
+        _appbase = SysProps.overrideAppbase(_appbase);
+
+        // make sure there's a trailing slash
+        if (!_appbase.endsWith("/")) {
+            _appbase = _appbase + "/";
+        }
+
+        // extract our version information
+        _version = config.getLong("version", -1L);
+
+        // if we are a versioned deployment, create a versioned appbase
+        try {
+            _vappbase = createVAppBase(_version);
+        } catch (MalformedURLException mue) {
+            String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
+            throw (IOException) new IOException(err).initCause(mue);
+        }
+
+        // check for a latest config URL
+        String latest = config.getString("latest");
+        if (latest != null) {
+            if (latest.startsWith(_appbase)) {
+                latest = _appbase + latest.substring(_appbase.length());
+            } else {
+                latest = SysProps.replaceDomain(latest);
+            }
+            try {
+                _latest = HostWhitelist.verify(new URL(latest));
+            } catch (MalformedURLException mue) {
+                log.warning("Invalid URL for latest attribute.", mue);
+            }
+        }
+
+        String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
+
+        // determine our application class name (use app-specific class _if_ one is provided)
+        _class = config.getString("class");
+        if (appPrefix.length() > 0) {
+            _class = config.getString(appPrefix + "class", _class);
+        }
+        if (_class == null) {
+            throw new IOException("m.missing_class");
+        }
+
+        // determine whether we want strict comments
+        _strictComments = config.getBoolean("strict_comments");
+
+        // check to see if we're using a custom java.version property and regex
+        _javaVersionProp = config.getString("java_version_prop", _javaVersionProp);
+        _javaVersionRegex = config.getString("java_version_regex", _javaVersionRegex);
+
+        // check to see if we require a particular JVM version and have a supplied JVM
+        _javaMinVersion = config.getLong("java_version", _javaMinVersion);
+        // we support java_min_version as an alias of java_version; it better expresses the check
+        // that's going on and better mirrors java_max_version
+        _javaMinVersion = config.getLong("java_min_version", _javaMinVersion);
+        // check to see if we require a particular max JVM version and have a supplied JVM
+        _javaMaxVersion = config.getLong("java_max_version", _javaMaxVersion);
+        // check to see if we require a particular JVM version and have a supplied JVM
+        _javaExactVersionRequired = config.getBoolean("java_exact_version_required");
+
+        // this is a little weird, but when we're run from the digester, we see a String[] which
+        // contains java locations for all platforms which we can't grok, but the digester doesn't
+        // need to know about that; when we're run in a real application there will be only one!
+        Object javaloc = config.getRaw("java_location");
+        if (javaloc instanceof String) {
+            _javaLocation = (String)javaloc;
+        }
+
+        // determine whether we have any tracking configuration
+        _trackingURL = config.getString("tracking_url");
+
+        // check for tracking progress percent configuration
+        String trackPcts = config.getString("tracking_percents");
+        if (!StringUtil.isBlank(trackPcts)) {
+            _trackingPcts = new HashSet<>();
+            for (int pct : StringUtil.parseIntArray(trackPcts)) {
+                _trackingPcts.add(pct);
+            }
+        } else if (!StringUtil.isBlank(_trackingURL)) {
+            _trackingPcts = new HashSet<>();
+            _trackingPcts.add(50);
+        }
+
+        // Check for tracking cookie configuration
+        _trackingCookieName = config.getString("tracking_cookie_name");
+        _trackingCookieProperty = config.getString("tracking_cookie_property");
+
+        // Some app may need an extra suffix added to the tracking URL
+        _trackingURLSuffix = config.getString("tracking_url_suffix");
+
+        // Some app may need to generate google analytics code
+        _trackingGAHash = config.getString("tracking_ga_hash");
+
+        // clear our arrays as we may be reinitializing
+        _codes.clear();
+        _resources.clear();
+        _auxgroups.clear();
+        _jvmargs.clear();
+        _appargs.clear();
+        _txtJvmArgs.clear();
+
+        // parse our code resources
+        if (config.getMultiValue("code") == null &&
+            config.getMultiValue("ucode") == null) {
+            throw new IOException("m.missing_code");
+        }
+        parseResources(config, "code", Resource.NORMAL, _codes);
+        parseResources(config, "ucode", Resource.UNPACK, _codes);
+
+        // parse our non-code resources
+        parseResources(config, "resource", Resource.NORMAL, _resources);
+        parseResources(config, "uresource", Resource.UNPACK, _resources);
+        parseResources(config, "xresource", Resource.EXEC, _resources);
+        parseResources(config, "presource", Resource.PRELOAD, _resources);
+        parseResources(config, "nresource", Resource.NATIVE, _resources);
+
+        // parse our auxiliary resource groups
+        for (String auxgroup : config.getList("auxgroups")) {
+            ArrayList<Resource> codes = new ArrayList<>();
+            parseResources(config, auxgroup + ".code", Resource.NORMAL, codes);
+            parseResources(config, auxgroup + ".ucode", Resource.UNPACK, codes);
+            ArrayList<Resource> rsrcs = new ArrayList<>();
+            parseResources(config, auxgroup + ".resource", Resource.NORMAL, rsrcs);
+            parseResources(config, auxgroup + ".xresource", Resource.EXEC, rsrcs);
+            parseResources(config, auxgroup + ".uresource", Resource.UNPACK, rsrcs);
+            parseResources(config, auxgroup + ".presource", Resource.PRELOAD, rsrcs);
+            parseResources(config, auxgroup + ".nresource", Resource.NATIVE, rsrcs);
+            _auxgroups.put(auxgroup, new AuxGroup(auxgroup, codes, rsrcs));
+        }
+
+        // transfer our JVM arguments (we include both "global" args and app_id-prefixed args)
+        String[] jvmargs = config.getMultiValue("jvmarg");
+        addAll(jvmargs, _jvmargs);
+        if (appPrefix.length() > 0) {
+            jvmargs = config.getMultiValue(appPrefix + "jvmarg");
+            addAll(jvmargs, _jvmargs);
+        }
+
+        // see if a percentage of physical memory option exists
+        int jvmmempc = config.getInt("jvmmempc", -1);
+        // app_id prefixed setting overrides
+        if (appPrefix.length() > 0) {
+            jvmmempc = config.getInt(appPrefix + "jvmmempc", jvmmempc);
+        }
+        if (0 <= jvmmempc && jvmmempc <= 100) {
+            final Object o = ManagementFactory.getOperatingSystemMXBean();
+
+            try {
+                if (o instanceof OperatingSystemMXBean) {
+                    final OperatingSystemMXBean osb = (OperatingSystemMXBean) o;
+                    long physicalMem = osb.getTotalPhysicalMemorySize();
+                    long requestedMem = physicalMem*jvmmempc/100;
+                    String[] maxMemHeapArg = new String[]{"-Xmx"+Long.toString(requestedMem)};
+                    // remove other max heap size arg
+                    ARG: for (int i = 0; i < _jvmargs.size(); i++) {
+                           if (_jvmargs.get(i) instanceof java.lang.String && _jvmargs.get(i).startsWith("-Xmx")) {
+                                _jvmargs.remove(i);
+                           }
+                    }
+                    addAll(maxMemHeapArg, _jvmargs);
+
+                }
+            }
+            catch (NoClassDefFoundError e) {
+                // com.sun.management.OperatingSystemMXBean doesn't exist in this JVM
+                System.out.println("No com.sun.management.OperatingSystemMXBean. Cannot use 'jvmmempc'.");
+            }
+        } else if (jvmmempc != -1) {
+          System.out.println("'jvmmempc' value must be in range 0 to 100 (read as '"+Integer.toString(jvmmempc)+"')");
+        }
+
+        // get the set of optimum JVM arguments
+        _optimumJvmArgs = config.getMultiValue("optimum_jvmarg");
+
+        // transfer our application arguments
+        String[] appargs = config.getMultiValue(appPrefix + "apparg");
+        addAll(appargs, _appargs);
+
+        // add the launch specific application arguments
+        _appargs.addAll(_envc.appArgs);
+
+        // look for custom arguments
+        fillAssignmentListFromPairs("extra.txt", _txtJvmArgs);
+
+        // determine whether we want to allow offline operation (defaults to false)
+        _allowOffline = config.getBoolean("allow_offline");
+
+        // look for a debug.txt file which causes us to run in java.exe on Windows so that we can
+        // obtain a thread dump of the running JVM
+        _windebug = getLocalPath("debug.txt").exists();
+
+        // whether to cache code resources and launch from cache
+        _useCodeCache = config.getBoolean("use_code_cache");
+        _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
+
+        // maximum simultaneous downloads
+        _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads",
+                                                      SysProps.threadPoolSize()));
+
+        // extract some info used to configure our child process on macOS
+        _dockName = config.getString("ui.name");
+        _dockIconPath = config.getString("ui.mac_dock_icon", "../desktop.icns");
+
+        return config;
+    }
+
+    /**
+     * Adds strings of the form pair0=pair1 to collector for each pair parsed out of pairLocation.
+     */
+    protected void fillAssignmentListFromPairs (String pairLocation, List<String> collector)
+    {
+        File pairFile = getLocalPath(pairLocation);
+        if (pairFile.exists()) {
+            try {
+                List<String[]> args = Config.parsePairs(pairFile, Config.createOpts(false));
+                for (String[] pair : args) {
+                    if (pair[1].length() == 0) {
+                        collector.add(pair[0]);
+                    } else {
+                        collector.add(pair[0] + "=" + pair[1]);
+                    }
+                }
+            } catch (Throwable t) {
+                log.warning("Failed to parse '" + pairFile + "': " + t);
+            }
+        }
+    }
+
+    /**
+     * Returns a URL from which the specified path can be fetched. Our application base URL is
+     * properly versioned and combined with the supplied path.
+     */
+    public URL getRemoteURL (String path)
+        throws MalformedURLException
+    {
+        return new URL(_vappbase, encodePath(path));
+    }
+
+    /**
+     * Returns the local path to the specified resource.
+     */
+    public File getLocalPath (String path)
+    {
+        return getLocalPath(getAppDir(), path);
+    }
+
+    /**
+     * Returns true if we either have no version requirement, are running in a JVM that meets our
+     * version requirements or have what appears to be a version of the JVM that meets our
+     * requirements.
+     */
+    public boolean haveValidJavaVersion ()
+    {
+        // if we're doing no version checking, then yay!
+        if (_javaMinVersion == 0 && _javaMaxVersion == 0) return true;
+
+        try {
+            // parse the version out of the java.version (or custom) system property
+            long version = SysProps.parseJavaVersion(_javaVersionProp, _javaVersionRegex);
+
+            log.info("Checking Java version", "current", version,
+                     "wantMin", _javaMinVersion, "wantMax", _javaMaxVersion);
+
+            // if we have an unpacked VM, check the 'release' file for its version
+            Resource vmjar = getJavaVMResource();
+            if (vmjar != null && vmjar.isMarkedValid()) {
+                File vmdir = new File(getAppDir(), LaunchUtil.LOCAL_JAVA_DIR);
+                File relfile = new File(vmdir, "release");
+                if (!relfile.exists()) {
+                    log.warning("Unpacked JVM missing 'release' file. Assuming valid version.");
+                    return true;
+                }
+
+                long vmvers = VersionUtil.readReleaseVersion(relfile, _javaVersionRegex);
+                if (vmvers == 0L) {
+                    log.warning("Unable to read version from 'release' file. Assuming valid.");
+                    return true;
+                }
+
+                version = vmvers;
+                log.info("Checking version of unpacked JVM [vers=" + version + "].");
+            }
+
+            if (_javaExactVersionRequired) {
+                if (version == _javaMinVersion) return true;
+                else {
+                    log.warning("An exact Java VM version is required.", "current", version,
+                                "required", _javaMinVersion);
+                    return false;
+                }
+            }
+
+            boolean minVersionOK = (_javaMinVersion == 0) || (version >= _javaMinVersion);
+            boolean maxVersionOK = (_javaMaxVersion == 0) || (version <= _javaMaxVersion);
+            return minVersionOK && maxVersionOK;
+
+        } catch (RuntimeException re) {
+            // if we can't parse the java version we're in weird land and should probably just try
+            // our luck with what we've got rather than try to download a new jvm
+            log.warning("Unable to parse VM version, hoping for the best",
+                        "error", re, "needed", _javaMinVersion);
+            return true;
+        }
+    }
+
+    /**
+     * Checks whether the app has a set of "optimum" JVM args that we wish to try first, detecting
+     * whether the launch is successful and, if necessary, trying again without the optimum
+     * arguments.
+     */
+    public boolean hasOptimumJvmArgs ()
+    {
+        return _optimumJvmArgs != null;
+    }
+
+    /**
+     * Returns true if the app should attempt to run even if we have no Internet connection.
+     */
+    public boolean allowOffline ()
+    {
+        return _allowOffline;
+    }
+
+    /**
+     * Attempts to redownload the <code>getdown.txt</code> file based on information parsed from a
+     * previous call to {@link #init}.
+     */
+    public void attemptRecovery (StatusDisplay status)
+        throws IOException
+    {
+        status.updateStatus("m.updating_metadata");
+        downloadConfigFile();
+    }
+
+    /**
+     * Downloads and replaces the <code>getdown.txt</code> and <code>digest.txt</code> files with
+     * those for the target version of our application.
+     */
+    public void updateMetadata ()
+        throws IOException
+    {
+        try {
+            // update our versioned application base with the target version
+            _vappbase = createVAppBase(_targetVersion);
+        } catch (MalformedURLException mue) {
+            String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
+            throw (IOException) new IOException(err).initCause(mue);
+        }
+
+        try {
+            // now re-download our control files; we download the digest first so that if it fails,
+            // our config file will still reference the old version and re-running the updater will
+            // start the whole process over again
+            downloadDigestFiles();
+            downloadConfigFile();
+
+        } catch (IOException ex) {
+            // if we are allowing offline execution, we want to allow the application to run in its
+            // current form rather than aborting the entire process; to do this, we delete the
+            // version.txt file and "trick" Getdown into thinking that it just needs to validate
+            // the application as is; next time the app runs when connected to the internet, it
+            // will have to rediscover that it needs updating and reattempt to update itself
+            if (_allowOffline) {
+                log.warning("Failed to update digest files.  Attempting offline operaton.", ex);
+                if (!FileUtil.deleteHarder(getLocalPath(VERSION_FILE))) {
+                    log.warning("Deleting version.txt failed.  This probably isn't going to work.");
+                }
+            } else {
+                throw ex;
+            }
+        }
+    }
+
+    /**
+     * Invokes the process associated with this application definition.
+     *
+     * @param optimum whether or not to include the set of optimum arguments (as opposed to falling
+     * back).
+     */
+    public Process createProcess (boolean optimum)
+        throws IOException
+    {
+        ArrayList<String> args = new ArrayList<>();
+
+        // reconstruct the path to the JVM
+        args.add(LaunchUtil.getJVMPath(getAppDir(), _windebug || optimum));
+
+        // check whether we're using -jar mode or -classpath mode
+        boolean dashJarMode = MANIFEST_CLASS.equals(_class);
+
+        // add the -classpath arguments if we're not in -jar mode
+        ClassPath classPath = PathBuilder.buildClassPath(this);
+        if (!dashJarMode) {
+            args.add("-classpath");
+            args.add(classPath.asArgumentString());
+        }
+
+        // we love our Mac users, so we do nice things to preserve our application identity
+        if (LaunchUtil.isMacOS()) {
+            args.add("-Xdock:icon=" + getLocalPath(_dockIconPath).getAbsolutePath());
+            args.add("-Xdock:name=" + _dockName);
+        }
+
+        // pass along our proxy settings
+        String proxyHost;
+        if ((proxyHost = System.getProperty("http.proxyHost")) != null) {
+            args.add("-Dhttp.proxyHost=" + proxyHost);
+            args.add("-Dhttp.proxyPort=" + System.getProperty("http.proxyPort"));
+            args.add("-Dhttps.proxyHost=" + proxyHost);
+            args.add("-Dhttps.proxyPort=" + System.getProperty("http.proxyPort"));
+        }
+
+        // add the marker indicating the app is running in getdown
+        args.add("-D" + Properties.GETDOWN + "=true");
+
+        // set the native library path if we have native resources
+        // @TODO optional getdown.txt parameter to set addCurrentLibraryPath to true or false?
+        ClassPath javaLibPath = PathBuilder.buildLibsPath(this, true);
+        if (javaLibPath != null) {
+            args.add("-Djava.library.path=" + javaLibPath.asArgumentString());
+        }
+
+        // pass along any pass-through arguments
+        for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
+            String key = (String)entry.getKey();
+            if (key.startsWith(PROP_PASSTHROUGH_PREFIX)) {
+                key = key.substring(PROP_PASSTHROUGH_PREFIX.length());
+                args.add("-D" + key + "=" + entry.getValue());
+            }
+        }
+
+        // add the JVM arguments
+        for (String string : _jvmargs) {
+            args.add(processArg(string));
+        }
+
+        // add the optimum arguments if requested and available
+        if (optimum && _optimumJvmArgs != null) {
+            for (String string : _optimumJvmArgs) {
+                args.add(processArg(string));
+            }
+        }
+
+        // add the arguments from extra.txt (after the optimum ones, in case they override them)
+        for (String string : _txtJvmArgs) {
+            args.add(processArg(string));
+        }
+
+        // if we're in -jar mode add those arguments, otherwise add the app class name
+        if (dashJarMode) {
+            args.add("-jar");
+            args.add(classPath.asArgumentString());
+        } else {
+            args.add(_class);
+        }
+
+        // finally add the application arguments
+        for (String string : _appargs) {
+            args.add(processArg(string));
+        }
+
+        String[] envp = createEnvironment();
+        String[] sargs = args.toArray(new String[args.size()]);
+        log.info("Running " + StringUtil.join(sargs, "\n  "));
+
+        return Runtime.getRuntime().exec(sargs, envp, getAppDir());
+    }
+
+    /**
+     * If the application provided environment variables, combine those with the current
+     * environment and return that in a style usable for {@link Runtime#exec(String, String[])}.
+     * If the application didn't provide any environment variables, null is returned to just use
+     * the existing environment.
+     */
+    protected String[] createEnvironment ()
+    {
+        List<String> envvar = new ArrayList<>();
+        fillAssignmentListFromPairs("env.txt", envvar);
+        if (envvar.isEmpty()) {
+            log.info("Didn't find any custom environment variables, not setting any.");
+            return null;
+        }
+
+        List<String> envAssignments = new ArrayList<>();
+        for (String assignment : envvar) {
+            envAssignments.add(processArg(assignment));
+        }
+        for (Map.Entry<String, String> environmentEntry : System.getenv().entrySet()) {
+            envAssignments.add(environmentEntry.getKey() + "=" + environmentEntry.getValue());
+        }
+        String[] envp = envAssignments.toArray(new String[envAssignments.size()]);
+        log.info("Environment " + StringUtil.join(envp, "\n "));
+        return envp;
+    }
+
+    /**
+     * Runs this application directly in the current VM.
+     */
+    public void invokeDirect () throws IOException
+    {
+        ClassPath classPath = PathBuilder.buildClassPath(this);
+        URL[] jarUrls = classPath.asUrls();
+
+        // create custom class loader
+        URLClassLoader loader = new URLClassLoader(jarUrls, ClassLoader.getSystemClassLoader()) {
+            @Override protected PermissionCollection getPermissions (CodeSource code) {
+                Permissions perms = new Permissions();
+                perms.add(new AllPermission());
+                return perms;
+            }
+        };
+        Thread.currentThread().setContextClassLoader(loader);
+
+        log.info("Configured URL class loader:");
+        for (URL url : jarUrls) log.info("  " + url);
+
+        // configure any system properties that we can
+        for (String jvmarg : _jvmargs) {
+            if (jvmarg.startsWith("-D")) {
+                jvmarg = processArg(jvmarg.substring(2));
+                int eqidx = jvmarg.indexOf("=");
+                if (eqidx == -1) {
+                    log.warning("Bogus system property: '" + jvmarg + "'?");
+                } else {
+                    System.setProperty(jvmarg.substring(0, eqidx), jvmarg.substring(eqidx+1));
+                }
+            }
+        }
+
+        // pass along any pass-through arguments
+        Map<String, String> passProps = new HashMap<>();
+        for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
+            String key = (String)entry.getKey();
+            if (key.startsWith(PROP_PASSTHROUGH_PREFIX)) {
+                key = key.substring(PROP_PASSTHROUGH_PREFIX.length());
+                passProps.put(key, (String)entry.getValue());
+            }
+        }
+        // we can't set these in the above loop lest we get a ConcurrentModificationException
+        for (Map.Entry<String, String> entry : passProps.entrySet()) {
+            System.setProperty(entry.getKey(), entry.getValue());
+        }
+
+        // prepare our app arguments
+        String[] args = new String[_appargs.size()];
+        for (int ii = 0; ii < args.length; ii++) args[ii] = processArg(_appargs.get(ii));
+
+        try {
+            log.info("Loading " + _class);
+            Class<?> appclass = loader.loadClass(_class);
+            Method main = appclass.getMethod("main", EMPTY_STRING_ARRAY.getClass());
+            log.info("Invoking main({" + StringUtil.join(args, ", ") + "})");
+            main.invoke(null, new Object[] { args });
+        } catch (Exception e) {
+            log.warning("Failure invoking app main", e);
+        }
+    }
+
+    /** Replaces the application directory and version in any argument. */
+    protected String processArg (String arg)
+    {
+        arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath());
+        arg = arg.replace("%VERSION%", String.valueOf(_version));
+
+        // if this argument contains %ENV.FOO% replace those with the associated values looked up
+        // from the environment
+        if (arg.contains(ENV_VAR_PREFIX)) {
+            StringBuffer sb = new StringBuffer();
+            Matcher matcher = ENV_VAR_PATTERN.matcher(arg);
+            while (matcher.find()) {
+                String varName = matcher.group(1), varValue = System.getenv(varName);
+                String repValue = varValue == null ? "MISSING:"+varName : varValue;
+                matcher.appendReplacement(sb, Matcher.quoteReplacement(repValue));
+            }
+            matcher.appendTail(sb);
+            arg = sb.toString();
+        }
+
+        return arg;
+    }
+
+    /**
+     * Loads the <code>digest.txt</code> file and verifies the contents of both that file and the
+     * <code>getdown.text</code> file. Then it loads the <code>version.txt</code> and decides
+     * whether or not the application needs to be updated or whether we can proceed to verification
+     * and execution.
+     *
+     * @return true if the application needs to be updated, false if it is up to date and can be
+     * verified and executed.
+     *
+     * @exception IOException thrown if we encounter an unrecoverable error while verifying the
+     * metadata.
+     */
+    public boolean verifyMetadata (StatusDisplay status)
+        throws IOException
+    {
+        log.info("Verifying application: " + _vappbase);
+        log.info("Version: " + _version);
+        log.info("Class: " + _class);
+
+        // this will read in the contents of the digest file and validate itself
+        try {
+            _digest = new Digest(getAppDir(), _strictComments);
+        } catch (IOException ioe) {
+            log.info("Failed to load digest: " + ioe.getMessage() + ". Attempting recovery...");
+        }
+
+        // if we have no version, then we are running in unversioned mode so we need to download
+        // our digest.txt file on every invocation
+        if (_version == -1) {
+            // make a note of the old meta-digest, if this changes we need to revalidate all of our
+            // resources as one or more of them have also changed
+            String olddig = (_digest == null) ? "" : _digest.getMetaDigest();
+            try {
+                status.updateStatus("m.checking");
+                downloadDigestFiles();
+                _digest = new Digest(getAppDir(), _strictComments);
+                if (!olddig.equals(_digest.getMetaDigest())) {
+                    log.info("Unversioned digest changed. Revalidating...");
+                    status.updateStatus("m.validating");
+                    clearValidationMarkers();
+                }
+            } catch (IOException ioe) {
+                log.warning("Failed to refresh non-versioned digest: " +
+                            ioe.getMessage() + ". Proceeding...");
+            }
+        }
+
+        // regardless of whether we're versioned, if we failed to read the digest from disk, try to
+        // redownload the digest file and give it another good college try; this time we allow
+        // exceptions to propagate up to the caller as there is nothing else we can do
+        if (_digest == null) {
+            status.updateStatus("m.updating_metadata");
+            downloadDigestFiles();
+            _digest = new Digest(getAppDir(), _strictComments);
+        }
+
+        // now verify the contents of our main config file
+        Resource crsrc = getConfigResource();
+        if (!_digest.validateResource(crsrc, null)) {
+            status.updateStatus("m.updating_metadata");
+            // attempt to redownload both of our metadata files; again we pass errors up to our
+            // caller because there's nothing we can do to automatically recover
+            downloadConfigFile();
+            downloadDigestFiles();
+            _digest = new Digest(getAppDir(), _strictComments);
+            // revalidate everything if we end up downloading new metadata
+            clearValidationMarkers();
+            // if the new copy validates, reinitialize ourselves; otherwise report baffling hoseage
+            if (_digest.validateResource(crsrc, null)) {
+                init(true);
+            } else {
+                log.warning(CONFIG_FILE + " failed to validate even after redownloading. " +
+                            "Blindly forging onward.");
+            }
+        }
+
+        // start by assuming we are happy with our version
+        _targetVersion = _version;
+
+        // if we are a versioned application, read in the contents of the version.txt file
+        // and/or check the latest config URL for a newer version
+        if (_version != -1) {
+            File vfile = getLocalPath(VERSION_FILE);
+            long fileVersion = VersionUtil.readVersion(vfile);
+            if (fileVersion != -1) {
+                _targetVersion = fileVersion;
+            }
+
+            if (_latest != null) {
+                try (InputStream in = ConnectionUtil.open(proxy, _latest, 0, 0).getInputStream();
+                     InputStreamReader reader = new InputStreamReader(in, UTF_8);
+                     BufferedReader bin = new BufferedReader(reader)) {
+                    for (String[] pair : Config.parsePairs(bin, Config.createOpts(false))) {
+                        if (pair[0].equals("version")) {
+                            _targetVersion = Math.max(Long.parseLong(pair[1]), _targetVersion);
+                            if (fileVersion != -1 && _targetVersion > fileVersion) {
+                                // replace the file with the newest version
+                                try (FileOutputStream fos = new FileOutputStream(vfile);
+                                     PrintStream out = new PrintStream(fos)) {
+                                    out.println(_targetVersion);
+                                }
+                            }
+                            break;
+                        }
+                    }
+                } catch (Exception e) {
+                    log.warning("Unable to retrieve version from latest config file.", e);
+                }
+            }
+        }
+
+        // finally let the caller know if we need an update
+        return _version != _targetVersion;
+    }
+
+    /**
+     * Verifies the code and media resources associated with this application. A list of resources
+     * that do not exist or fail the verification process will be returned. If all resources are
+     * ready to go, null will be returned and the application is considered ready to run.
+     *
+     * @param obs a progress observer that will be notified of verification progress. NOTE: this
+     * observer may be called from arbitrary threads, so if you update a UI based on calls to it,
+     * you have to take care to get back to your UI thread.
+     * @param alreadyValid if non-null a 1 element array that will have the number of "already
+     * validated" resources filled in.
+     * @param unpacked a set to populate with unpacked resources.
+     * @param toInstall a list into which to add resources that need to be installed.
+     * @param toDownload a list into which to add resources that need to be downloaded.
+     */
+    public void verifyResources (
+        ProgressObserver obs, int[] alreadyValid, Set<Resource> unpacked,
+        Set<Resource> toInstall, Set<Resource> toDownload)
+        throws InterruptedException
+    {
+        // resources are verified on background threads supplied by the thread pool, and progress
+        // is reported by posting runnable actions to the actions queue which is processed by the
+        // main (UI) thread
+        ExecutorService exec = Executors.newFixedThreadPool(SysProps.threadPoolSize());
+        final BlockingQueue<Runnable> actions = new LinkedBlockingQueue<Runnable>();
+        final int[] completed = new int[1];
+
+        long start = System.currentTimeMillis();
+
+        // obtain the sizes of the resources to validate
+        List<Resource> rsrcs = getAllActiveResources();
+        long[] sizes = new long[rsrcs.size()];
+        long totalSize = 0;
+        for (int ii = 0; ii < sizes.length; ii++) {
+            totalSize += sizes[ii] = rsrcs.get(ii).getLocal().length();
+        }
+        final ProgressObserver fobs = obs;
+        // as long as we forward aggregated progress updates to the UI thread, having multiple
+        // threads update a progress aggregator is "mostly" thread-safe
+        final ProgressAggregator pagg = new ProgressAggregator(new ProgressObserver() {
+            public void progress (final int percent) {
+                actions.add(new Runnable() {
+                    public void run () {
+                        fobs.progress(percent);
+                    }
+                });
+            }
+        }, sizes);
+
+        final int[] fAlreadyValid = alreadyValid;
+        final Set<Resource> toInstallAsync = new ConcurrentSkipListSet<>(toInstall);
+        final Set<Resource> toDownloadAsync = new ConcurrentSkipListSet<>();
+        final Set<Resource> unpackedAsync = new ConcurrentSkipListSet<>();
+
+        for (int ii = 0; ii < sizes.length; ii++) {
+            final Resource rsrc = rsrcs.get(ii);
+            final int index = ii;
+            exec.execute(new Runnable() {
+                public void run () {
+                    verifyResource(rsrc, pagg.startElement(index), fAlreadyValid,
+                                   unpackedAsync, toInstallAsync, toDownloadAsync);
+                    actions.add(new Runnable() {
+                        public void run () {
+                            completed[0] += 1;
+                        }
+                    });
+                }
+            });
+        }
+
+        while (completed[0] < rsrcs.size()) {
+            // we should be getting progress completion updates WAY more often than one every
+            // minute, so if things freeze up for 60 seconds, abandon ship
+            Runnable action = actions.poll(60, TimeUnit.SECONDS);
+            action.run();
+        }
+
+        exec.shutdown();
+
+        toInstall.addAll(toInstallAsync);
+        toDownload.addAll(toDownloadAsync);
+        unpacked.addAll(unpackedAsync);
+
+        long complete = System.currentTimeMillis();
+        log.info("Verified resources", "count", rsrcs.size(), "size", (totalSize/1024) + "k",
+                 "duration", (complete-start) + "ms");
+    }
+
+    private void verifyResource (Resource rsrc, ProgressObserver obs, int[] alreadyValid,
+                                 Set<Resource> unpacked,
+                                 Set<Resource> toInstall, Set<Resource> toDownload) {
+        if (rsrc.isMarkedValid()) {
+            if (alreadyValid != null) {
+                alreadyValid[0]++;
+            }
+            obs.progress(100);
+            return;
+        }
+
+        try {
+            if (_digest.validateResource(rsrc, obs)) {
+                // if the resource has a _new file, add it to to-install list
+                if (rsrc.getLocalNew().exists()) {
+                    toInstall.add(rsrc);
+                    return;
+                }
+                rsrc.applyAttrs();
+                unpacked.add(rsrc);
+                rsrc.markAsValid();
+                return;
+            }
+
+        } catch (Exception e) {
+            log.info("Failure verifying resource. Requesting redownload...",
+                     "rsrc", rsrc, "error", e);
+
+        } finally {
+            obs.progress(100);
+        }
+        toDownload.add(rsrc);
+    }
+
+    /**
+     * Unpacks the resources that require it (we know that they're valid).
+     *
+     * @param unpacked a set of resources to skip because they're already unpacked.
+     */
+    public void unpackResources (ProgressObserver obs, Set<Resource> unpacked)
+        throws InterruptedException
+    {
+        List<Resource> rsrcs = getActiveResources();
+
+        // remove resources that we don't want to unpack
+        for (Iterator<Resource> it = rsrcs.iterator(); it.hasNext(); ) {
+            Resource rsrc = it.next();
+            if (!rsrc.shouldUnpack() || unpacked.contains(rsrc)) {
+                it.remove();
+            }
+        }
+
+        // obtain the sizes of the resources to unpack
+        long[] sizes = new long[rsrcs.size()];
+        for (int ii = 0; ii < sizes.length; ii++) {
+            sizes[ii] = rsrcs.get(ii).getLocal().length();
+        }
+
+        ProgressAggregator pagg = new ProgressAggregator(obs, sizes);
+        for (int ii = 0; ii < sizes.length; ii++) {
+            Resource rsrc = rsrcs.get(ii);
+            ProgressObserver pobs = pagg.startElement(ii);
+            try {
+                rsrc.unpack();
+            } catch (IOException ioe) {
+                log.warning("Failure unpacking resource", "rsrc", rsrc, ioe);
+            }
+            pobs.progress(100);
+        }
+    }
+
+    /**
+     * Clears all validation marker files.
+     */
+    public void clearValidationMarkers ()
+    {
+        clearValidationMarkers(getAllActiveResources().iterator());
+    }
+
+    /**
+     * Returns the version number for the application.  Should only be called after successful
+     * return of verifyMetadata.
+     */
+    public long getVersion ()
+    {
+        return _version;
+    }
+
+    /**
+     * Creates a versioned application base URL for the specified version.
+     */
+    protected URL createVAppBase (long version)
+        throws MalformedURLException
+    {
+        String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version);
+        return HostWhitelist.verify(new URL(url));
+    }
+
+    /**
+     * Clears all validation marker files for the resources in the supplied iterator.
+     */
+    protected void clearValidationMarkers (Iterator<Resource> iter)
+    {
+        while (iter.hasNext()) {
+            iter.next().clearMarker();
+        }
+    }
+
+    /**
+     * Downloads a new copy of CONFIG_FILE.
+     */
+    protected void downloadConfigFile ()
+        throws IOException
+    {
+        downloadControlFile(CONFIG_FILE, 0);
+    }
+
+    /**
+     * @return true if gettingdown.lock was unlocked, already locked by this application or if
+     * we're not locking at all.
+     */
+    public synchronized boolean lockForUpdates ()
+    {
+        if (_lock != null && _lock.isValid()) {
+            return true;
+        }
+        try {
+            _lockChannel = new RandomAccessFile(getLocalPath("gettingdown.lock"), "rw").getChannel();
+        } catch (FileNotFoundException e) {
+            log.warning("Unable to create lock file", "message", e.getMessage(), e);
+            return false;
+        }
+        try {
+            _lock = _lockChannel.tryLock();
+        } catch (IOException e) {
+            log.warning("Unable to create lock", "message", e.getMessage(), e);
+            return false;
+        } catch (OverlappingFileLockException e) {
+            log.warning("The lock is held elsewhere in this JVM", e);
+            return false;
+        }
+        log.info("Able to lock for updates: " + (_lock != null));
+        return _lock != null;
+    }
+
+    /**
+     * Release gettingdown.lock
+     */
+    public synchronized void releaseLock ()
+    {
+        if (_lock != null) {
+            log.info("Releasing lock");
+            try {
+                _lock.release();
+            } catch (IOException e) {
+                log.warning("Unable to release lock", "message", e.getMessage(), e);
+            }
+            try {
+                _lockChannel.close();
+            } catch (IOException e) {
+                log.warning("Unable to close lock channel", "message", e.getMessage(), e);
+            }
+            _lockChannel = null;
+            _lock = null;
+        }
+    }
+
+    /**
+     * Downloads the digest files and validates their signature.
+     * @throws IOException
+     */
+    protected void downloadDigestFiles ()
+        throws IOException
+    {
+        for (int version = 1; version <= Digest.VERSION; version++) {
+            downloadControlFile(Digest.digestFile(version), version);
+        }
+    }
+
+    /**
+     * Downloads a new copy of the specified control file, optionally validating its signature.
+     * If the download is successful, moves it over the old file on the filesystem.
+     *
+     * <p> TODO: Switch to PKCS #7 or CMS.
+     *
+     * @param sigVersion if {@code 0} no validation will be performed, if {@code > 0} then this
+     * should indicate the version of the digest file being validated which indicates which
+     * algorithm to use to verify the signature. See {@link Digest#VERSION}.
+     */
+    protected void downloadControlFile (String path, int sigVersion)
+        throws IOException
+    {
+        File target = downloadFile(path);
+
+        if (sigVersion > 0) {
+            if (_envc.certs.isEmpty()) {
+                log.info("No signing certs, not verifying digest.txt", "path", path);
+
+            } else {
+                File signatureFile = downloadFile(path + SIGNATURE_SUFFIX);
+                byte[] signature = null;
+                try (FileInputStream signatureStream = new FileInputStream(signatureFile)) {
+                    signature = StreamUtil.toByteArray(signatureStream);
+                } finally {
+                    FileUtil.deleteHarder(signatureFile); // delete the file regardless
+                }
+
+                byte[] buffer = new byte[8192];
+                int length, validated = 0;
+                for (Certificate cert : _envc.certs) {
+                    try (FileInputStream dataInput = new FileInputStream(target)) {
+                        Signature sig = Signature.getInstance(Digest.sigAlgorithm(sigVersion));
+                        sig.initVerify(cert);
+                        while ((length = dataInput.read(buffer)) != -1) {
+                            sig.update(buffer, 0, length);
+                        }
+
+                        if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
+                            log.info("Signature does not match", "cert", cert.getPublicKey());
+                            continue;
+                        } else {
+                            log.info("Signature matches", "cert", cert.getPublicKey());
+                            validated++;
+                        }
+
+                    } catch (IOException ioe) {
+                        log.warning("Failure validating signature of " + target + ": " + ioe);
+
+                    } catch (GeneralSecurityException gse) {
+                        // no problem!
+
+                    }
+                }
+
+                // if we couldn't find a key that validates our digest, we are the hosed!
+                if (validated == 0) {
+                    // delete the temporary digest file as we know it is invalid
+                    FileUtil.deleteHarder(target);
+                    throw new IOException("m.corrupt_digest_signature_error");
+                }
+            }
+        }
+
+        // now move the temporary file over the original
+        File original = getLocalPath(path);
+        if (!FileUtil.renameTo(target, original)) {
+            throw new IOException("Failed to rename(" + target + ", " + original + ")");
+        }
+    }
+
+    /**
+     * Download a path to a temporary file, returning a {@link File} instance with the path
+     * contents.
+     */
+    protected File downloadFile (String path)
+        throws IOException
+    {
+        File target = getLocalPath(path + "_new");
+
+        URL targetURL = null;
+        try {
+            targetURL = getRemoteURL(path);
+        } catch (Exception e) {
+            log.warning("Requested to download invalid control file",
+                "appbase", _vappbase, "path", path, "error", e);
+            throw (IOException) new IOException("Invalid path '" + path + "'.").initCause(e);
+        }
+
+        log.info("Attempting to refetch '" + path + "' from '" + targetURL + "'.");
+
+        // stream the URL into our temporary file
+        URLConnection uconn = ConnectionUtil.open(proxy, targetURL, 0, 0);
+        // we have to tell Java not to use caches here, otherwise it will cache any request for
+        // same URL for the lifetime of this JVM (based on the URL string, not the URL object);
+        // if the getdown.txt file, for example, changes in the meanwhile, we would never hear
+        // about it; turning off caches is not a performance concern, because when Getdown asks
+        // to download a file, it expects it to come over the wire, not from a cache
+        uconn.setUseCaches(false);
+        uconn.setRequestProperty("Accept-Encoding", "gzip");
+        try (InputStream fin = uconn.getInputStream()) {
+            String encoding = uconn.getContentEncoding();
+            boolean gzip = "gzip".equalsIgnoreCase(encoding);
+            try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) {
+                try (FileOutputStream fout = new FileOutputStream(target)) {
+                    StreamUtil.copy(fin2, fout);
+                }
+            }
+        }
+
+        return target;
+    }
+
+    /** Helper function for creating {@link Resource} instances. */
+    protected Resource createResource (String path, EnumSet<Resource.Attr> attrs)
+        throws MalformedURLException
+    {
+        return new Resource(path, getRemoteURL(path), getLocalPath(path), attrs);
+    }
+
+    /** Helper function to add all values in {@code values} (if non-null) to {@code target}. */
+    protected static void addAll (String[] values, List<String> target) {
+        if (values != null) {
+            for (String value : values) {
+                target.add(value);
+            }
+        }
+    }
+
+    /**
+     * Make an immutable List from the specified int array.
+     */
+    public static List<Integer> intsToList (int[] values)
+    {
+        List<Integer> list = new ArrayList<>(values.length);
+        for (int val : values) {
+            list.add(val);
+        }
+        return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Make an immutable List from the specified String array.
+     */
+    public static List<String> stringsToList (String[] values)
+    {
+        return values == null ? null : Collections.unmodifiableList(Arrays.asList(values));
+    }
+
+    /** Used to parse resources with the specified name. */
+    protected void parseResources (Config config, String name, EnumSet<Resource.Attr> attrs,
+                                   List<Resource> list)
+    {
+        String[] rsrcs = config.getMultiValue(name);
+        if (rsrcs == null) {
+            return;
+        }
+        for (String rsrc : rsrcs) {
+            try {
+                list.add(createResource(rsrc, attrs));
+            } catch (Exception e) {
+                log.warning("Invalid resource '" + rsrc + "'. " + e);
+            }
+        }
+    }
+
+    /** Possibly generates and returns a google analytics tracking cookie. */
+    protected String getGATrackingCode ()
+    {
+        if (_trackingGAHash == null) {
+            return "";
+        }
+        long time = System.currentTimeMillis() / 1000;
+        if (_trackingStart == 0) {
+            _trackingStart = time;
+        }
+        if (_trackingId == 0) {
+            int low = 100000000, high = 1000000000;
+            _trackingId = low + _rando.nextInt(high-low);
+        }
+        StringBuilder cookie = new StringBuilder("&utmcc=__utma%3D").append(_trackingGAHash);
+        cookie.append(".").append(_trackingId);
+        cookie.append(".").append(_trackingStart).append(".").append(_trackingStart);
+        cookie.append(".").append(time).append(".1%3B%2B");
+        cookie.append("__utmz%3D").append(_trackingGAHash).append(".");
+        cookie.append(_trackingStart).append(".1.1.");
+        cookie.append("utmcsr%3D(direct)%7Cutmccn%3D(direct)%7Cutmcmd%3D(none)%3B");
+        int low = 1000000000, high = 2000000000;
+        cookie.append("&utmn=").append(_rando.nextInt(high-low));
+        return cookie.toString();
+    }
+
+    /**
+     * Encodes a path for use in a URL.
+     */
+    protected static String encodePath (String path)
+    {
+        try {
+            // we want to keep slashes because we're encoding an entire path; also we need to turn
+            // + into %20 because web servers don't like + in paths or file names, blah
+            return URLEncoder.encode(path, "UTF-8").replace("%2F", "/").replace("+", "%20");
+        } catch (UnsupportedEncodingException ue) {
+            log.warning("Failed to URL encode " + path + ": " + ue);
+            return path;
+        }
+    }
+
+    protected File getLocalPath (File appdir, String path)
+    {
+        return new File(appdir, path);
+    }
+
+    protected final EnvConfig _envc;
+    protected File _config;
+    protected Digest _digest;
+
+    protected long _version = -1;
+    protected long _targetVersion = -1;
+    protected String _appbase;
+    protected URL _vappbase;
+    protected URL _latest;
+    protected String _class;
+    protected String _dockName;
+    protected String _dockIconPath;
+    protected boolean _strictComments;
+    protected boolean _windebug;
+    protected boolean _allowOffline;
+    protected int _maxConcDownloads;
+
+    protected String _trackingURL;
+    protected Set<Integer> _trackingPcts;
+    protected String _trackingCookieName;
+    protected String _trackingCookieProperty;
+    protected String _trackingURLSuffix;
+    protected String _trackingGAHash;
+    protected long _trackingStart;
+    protected int _trackingId;
+
+    protected String _javaVersionProp = "java.version";
+    protected String _javaVersionRegex = "(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?";
+    protected long _javaMinVersion, _javaMaxVersion;
+    protected boolean _javaExactVersionRequired;
+    protected String _javaLocation;
+
+    protected List<Resource> _codes = new ArrayList<>();
+    protected List<Resource> _resources = new ArrayList<>();
+
+    protected boolean _useCodeCache;
+    protected int _codeCacheRetentionDays;
+
+    protected Map<String,AuxGroup> _auxgroups = new HashMap<>();
+    protected Map<String,Boolean> _auxactive = new HashMap<>();
+
+    protected List<String> _jvmargs = new ArrayList<>();
+    protected List<String> _appargs = new ArrayList<>();
+
+    protected String[] _optimumJvmArgs;
+
+    protected List<String> _txtJvmArgs = new ArrayList<>();
+
+    /** If a warning has been issued about not being able to set modtimes. */
+    protected boolean _warnedAboutSetLastModified;
+
+    /** Locks gettingdown.lock in the app dir. Held the entire time updating is going on.*/
+    protected FileLock _lock;
+
+    /** Channel to the file underlying _lock.  Kept around solely so the lock doesn't close. */
+    protected FileChannel _lockChannel;
+
+    protected Random _rando = new Random();
+
+    protected static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    protected static final String ENV_VAR_PREFIX = "%ENV.";
+    protected static final Pattern ENV_VAR_PATTERN = Pattern.compile("%ENV\\.(.*?)%");
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Build.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Build.java
new file mode 100644 (file)
index 0000000..19a9fc5
--- /dev/null
@@ -0,0 +1,43 @@
+//
+// 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.Arrays;
+import java.util.List;
+
+import com.threerings.getdown.util.StringUtil;
+
+/**
+ * Contains static data provided during the build process.
+ */
+public class Build {
+
+    /** The date and time at which the code was built: in {@code yyyy-MM-dd HH:mm} format. */
+    public static String time () {
+        return "2019-04-05 14:07";
+    }
+
+    /** The Maven version of the Getdown project. */
+    public static String version () {
+        return "1.8.3-SNAPSHOT";
+    }
+
+    /**
+     * <p>The hosts which Getdown is allowed to communicate with. An empty list indicates that
+     * no whitelist is configured and there are no limitations. By default, no host whitelist
+     * is added to the binary, so it can be used to download and run applications from any
+     * server.
+     *
+     * <p>To create a custom Getdown build that can only talk to whitelisted servers, set
+     * the {@code getdown.host.whitelist} property on the command line while building the JAR
+     * (e.g. {@code mvn package -Dgetdown.host.whitelist=my.server.com}). Wildcards can be used
+     * (e.g. {@code *.mycompany.com}) and multiple values can be separated by commas
+     * (e.g. {@code app1.foo.com,app2.bar.com,app3.baz.com}).
+     */
+    public static List<String> hostWhitelist () {
+        return Arrays.asList(StringUtil.parseStringArray(""));
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl
new file mode 100644 (file)
index 0000000..60a8ff3
--- /dev/null
@@ -0,0 +1,43 @@
+//
+// 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.Arrays;
+import java.util.List;
+
+import com.threerings.getdown.util.StringUtil;
+
+/**
+ * Contains static data provided during the build process.
+ */
+public class Build {
+
+    /** The date and time at which the code was built: in {@code yyyy-MM-dd HH:mm} format. */
+    public static String time () {
+        return "@build_time@";
+    }
+
+    /** The Maven version of the Getdown project. */
+    public static String version () {
+        return "@build_version@";
+    }
+
+    /**
+     * <p>The hosts which Getdown is allowed to communicate with. An empty list indicates that
+     * no whitelist is configured and there are no limitations. By default, no host whitelist
+     * is added to the binary, so it can be used to download and run applications from any
+     * server.
+     *
+     * <p>To create a custom Getdown build that can only talk to whitelisted servers, set
+     * the {@code getdown.host.whitelist} property on the command line while building the JAR
+     * (e.g. {@code mvn package -Dgetdown.host.whitelist=my.server.com}). Wildcards can be used
+     * (e.g. {@code *.mycompany.com}) and multiple values can be separated by commas
+     * (e.g. {@code app1.foo.com,app2.bar.com,app3.baz.com}).
+     */
+    public static List<String> hostWhitelist () {
+        return Arrays.asList(StringUtil.parseStringArray("@host_whitelist@"));
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/ClassPath.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/ClassPath.java
new file mode 100644 (file)
index 0000000..9c2fce3
--- /dev/null
@@ -0,0 +1,76 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.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;
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
new file mode 100644 (file)
index 0000000..bc8d140
--- /dev/null
@@ -0,0 +1,228 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.concurrent.*;
+
+import com.threerings.getdown.util.Config;
+import com.threerings.getdown.util.MessageUtil;
+import com.threerings.getdown.util.ProgressObserver;
+import com.threerings.getdown.util.StringUtil;
+
+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(SysProps.threadPoolSize());
+        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";
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
new file mode 100644 (file)
index 0000000..6de4e2e
--- /dev/null
@@ -0,0 +1,229 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.*;
+
+import com.threerings.getdown.util.StringUtil;
+
+/** Configuration that comes from our "environment" (command line args, sys props, etc.). */
+public final class EnvConfig {
+
+    /** Used to report problems or feedback by {@link #create}. */
+    public static final class Note {
+        public static enum Level { INFO, WARN, ERROR };
+        public static Note info (String msg) { return new Note(Level.INFO, msg); }
+        public static Note warn (String msg) { return new Note(Level.WARN, msg); }
+        public static Note error (String msg) { return new Note(Level.ERROR, msg); }
+        public final Level level;
+        public final String message;
+        public Note (Level level, String message) {
+            this.level = level;
+            this.message = message;
+        }
+    }
+
+    /**
+     * Creates an environment config, obtaining information (in order) from the following sources:
+     *
+     * <ul>
+     * <li> A {@code bootstrap.properties} file bundled with the jar. </li>
+     * <li> System properties supplied to the JVM. </li>
+     * <li> The supplied command line arguments ({@code argv}). </li>
+     * </ul>
+     *
+     * If a later source supplies a configuration already provided by a prior source, a warning
+     * message will be logged to indicate the conflict, and the prior source will be used.
+     *
+     * @param notes a list into which notes are added, to be logged after the logging system has
+     * been initialized (which cannot happen until the appdir is known). If any {@code ERROR} notes
+     * are included, the app should terminate after reporting them.
+     * @return an env config instance, or {@code null} if no appdir could be located via any
+     * configuration source.
+     */
+    public static EnvConfig create (String[] argv, List<Note> notes) {
+        String appDir = null, appDirProv = null;
+        String appId = null, appIdProv = null;
+        String appBase = null, appBaseProv = null;
+
+        // start with bootstrap.properties config, if avaialble
+        try {
+            ResourceBundle bundle = ResourceBundle.getBundle("bootstrap");
+            if (bundle.containsKey("appdir")) {
+                appDir = bundle.getString("appdir");
+                appDir = appDir.replace(USER_HOME_KEY, System.getProperty("user.home"));
+                appDirProv = "bootstrap.properties";
+            }
+            if (bundle.containsKey("appid")) {
+                appId = bundle.getString("appid");
+                appIdProv = "bootstrap.properties";
+            }
+            if (bundle.containsKey("appbase")) {
+                appBase = bundle.getString("appbase");
+                appBaseProv = "bootstrap.properties";
+            }
+            // if any system properties are specified (keys prefixed with sys.), set those up
+            for (String key : bundle.keySet()) {
+                if (key.startsWith("sys.")) {
+                    String skey = key.substring(4);
+                    String svalue = bundle.getString(key);
+                    notes.add(Note.info("Setting system property from bundle: " +
+                                        skey + "='" + svalue + "'"));
+                    System.setProperty(skey, svalue);
+                }
+            }
+
+        } catch (MissingResourceException e) {
+            // bootstrap.properties is optional; no need for a warning
+        }
+
+        // next seek config from system properties
+        String spropsAppDir = SysProps.appDir();
+        if (!StringUtil.isBlank(spropsAppDir)) {
+            if (appDir == null) {
+                appDir = spropsAppDir;
+                appDirProv = "system property";
+            } else {
+                notes.add(Note.warn("Ignoring 'appdir' system property, have appdir via '" +
+                                    appDirProv + "'"));
+            }
+        }
+        String spropsAppId = SysProps.appId();
+        if (!StringUtil.isBlank(spropsAppId)) {
+            if (appId == null) {
+                appId = spropsAppId;
+                appIdProv = "system property";
+            } else {
+                notes.add(Note.warn("Ignoring 'appid' system property, have appid via '" +
+                                    appIdProv + "'"));
+            }
+        }
+        String spropsAppBase = SysProps.appBase();
+        if (!StringUtil.isBlank(spropsAppBase)) {
+            if (appBase == null) {
+                appBase = spropsAppBase;
+                appBaseProv = "system property";
+            } else {
+                notes.add(Note.warn("Ignoring 'appbase' system property, have appbase via '" +
+                                    appBaseProv + "'"));
+            }
+        }
+
+        // finally obtain config from command line arguments
+        String argvAppDir = argv.length > 0 ? argv[0] : null;
+        if (!StringUtil.isBlank(argvAppDir)) {
+            if (appDir == null) {
+                appDir = argvAppDir;
+                appDirProv = "command line";
+            } else {
+                notes.add(Note.warn("Ignoring 'appdir' command line arg, have appdir via '" +
+                                    appDirProv + "'"));
+            }
+        }
+        String argvAppId = argv.length > 1 ? argv[1] : null;
+        if (!StringUtil.isBlank(argvAppId)) {
+            if (appId == null) {
+                appId = argvAppId;
+                appIdProv = "command line";
+            } else {
+                notes.add(Note.warn("Ignoring 'appid' command line arg, have appid via '" +
+                                    appIdProv + "'"));
+            }
+        }
+
+        // ensure that we were able to fine an app dir
+        if (appDir == null) {
+            return null; // caller will report problem to user
+        }
+
+        notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
+        if (appId != null) notes.add(Note.info("Using appid from " + appIdProv + ": " + appId));
+        if (appBase != null) notes.add(
+            Note.info("Using appbase from " + appBaseProv + ": " + appBase));
+
+        // ensure that the appdir refers to a directory that exists
+        File appDirFile = new File(appDir);
+        if (!appDirFile.exists()) {
+            // if we have a bootstrap URL then we auto-create the app dir; this enables an
+            // installer to simply place a getdown.jar file somewhere and create an OS shortcut
+            // that runs getdown with an appdir and appbase specified, and have getdown create the
+            // appdir and download the app into it
+            if (!StringUtil.isBlank(appBase)) {
+                if (appDirFile.mkdirs()) {
+                    notes.add(Note.info("Auto-created app directory '" + appDir + "'"));
+                } else {
+                    notes.add(Note.warn("Unable to auto-create app dir: '" + appDir + "'"));
+                }
+            } else {
+                notes.add(Note.error("Invalid appdir '" + appDir + "': directory does not exist"));
+                return null;
+            }
+        } else if (!appDirFile.isDirectory()) {
+            notes.add(Note.error("Invalid appdir '" + appDir + "': refers to non-directory"));
+            return null;
+        }
+
+        // pass along anything after the first two args as extra app args
+        List<String> appArgs = argv.length > 2 ?
+            Arrays.asList(argv).subList(2, argv.length) :
+            Collections.<String>emptyList();
+
+        // load X.509 certificate if it exists
+        File crtFile = new File(appDirFile, Digest.digestFile(Digest.VERSION) + ".crt");
+        List<Certificate> certs = new ArrayList<>();
+        if (crtFile.exists()) {
+            try (FileInputStream fis = new FileInputStream(crtFile)) {
+                X509Certificate certificate = (X509Certificate)
+                    CertificateFactory.getInstance("X.509").generateCertificate(fis);
+                certs.add(certificate);
+            } catch (Exception e) {
+                notes.add(Note.error("Certificate error: " + e.getMessage()));
+            }
+        }
+
+        return new EnvConfig(appDirFile, appId, appBase, certs, appArgs);
+    }
+
+    /** The directory in which the application and metadata is stored. */
+    public final File appDir;
+
+    /** Either {@code null} or an identifier for a secondary application that should be
+      * launched. That app will use {@code appid.class} and {@code appid.apparg} to configure
+      * itself but all other parameters will be the same as the primary app. */
+    public final String appId;
+
+    /** Either {@code null} or fallback {@code appbase} to use if one cannot be read from a
+      * {@code getdown.txt} file during startup. */
+    public final String appBase;
+
+    /** Zero or more signing certificates used to verify the digest file. */
+    public final List<Certificate> certs;
+
+    /** Additional arguments to pass on to launched application. These will be added after the
+      * args in the getdown.txt file. */
+    public final List<String> appArgs;
+
+    public EnvConfig (File appDir) {
+        this(appDir, null, null, Collections.<Certificate>emptyList(),
+             Collections.<String>emptyList());
+    }
+
+    private EnvConfig (File appDir, String appId, String appBase, List<Certificate> certs,
+                       List<String> appArgs) {
+        this.appDir = appDir;
+        this.appId = appId;
+        this.appBase = appBase;
+        this.certs = certs;
+        this.appArgs = appArgs;
+    }
+
+    private static final String USER_HOME_KEY = "${user.home}";
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
new file mode 100644 (file)
index 0000000..b0a1dc9
--- /dev/null
@@ -0,0 +1,135 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.JarFile;
+
+import com.threerings.getdown.cache.GarbageCollector;
+import com.threerings.getdown.cache.ResourceCache;
+import com.threerings.getdown.util.FileUtil;
+import static com.threerings.getdown.Log.log;
+
+public class PathBuilder
+{
+    /** Name of directory to store cached code files in. */
+    public static final String CODE_CACHE_DIR = ".cache";
+
+    /** Name of directory to store cached native resources in. */
+    public static final String NATIVE_CACHE_DIR = ".ncache";
+
+    /**
+     * 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 codeCacheDir = new File(app.getAppDir(), CODE_CACHE_DIR);
+
+        // a negative value of code_cache_retention_days allows to clean up the cache forcefully
+        long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
+        if (retainMillis != 0L) {
+            GarbageCollector.collect(codeCacheDir, retainMillis);
+        }
+
+        ResourceCache cache = new ResourceCache(codeCacheDir);
+        LinkedHashSet<File> classPathEntries = new LinkedHashSet<>();
+        for (Resource resource: app.getActiveCodeResources()) {
+            String digest = app.getDigest(resource);
+            File entry = cache.cacheFile(resource.getFinalTarget(), digest.substring(0, 2), digest);
+            classPathEntries.add(entry);
+        }
+
+        return new ClassPath(classPathEntries);
+    }
+
+    /**
+     * Builds a {@link ClassPath} instance by first caching all native jars (indicated by
+     * nresource=[native jar]), unpacking them, and referencing the locations of each of the
+     * unpacked files. Also performs garbage collection similar to {@link #buildCachedClassPath}
+     *
+     * @param app                   used to determine native jars and related information.
+     * @param addCurrentLibraryPath if true, it adds the locations referenced by
+     *                              {@code System.getProperty("java.library.path")} as well.
+     * @return a classpath instance if at least one native resource was found and unpacked,
+     *         {@code null} if no native resources were used by the application.
+     */
+    public static ClassPath buildLibsPath (Application app,
+                                           boolean addCurrentLibraryPath) throws IOException {
+        List<Resource> resources = app.getNativeResources();
+        if (resources.isEmpty()) {
+            return null;
+        }
+
+        LinkedHashSet<File> nativedirs = new LinkedHashSet<>();
+        File nativeCacheDir = new File(app.getAppDir(), NATIVE_CACHE_DIR);
+        ResourceCache cache = new ResourceCache(nativeCacheDir);
+
+        // negative value forces total garbage collection, 0 avoids garbage collection at all
+        long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
+        if (retainMillis != 0L) {
+            GarbageCollector.collectNative(nativeCacheDir, retainMillis);
+        }
+
+        for (Resource resource : resources) {
+            // Use untruncated cache subdirectory names to avoid overwriting issues when unpacking,
+            // in the off chance that two native jars share a directory AND contain files with the
+            // same names
+            String digest = app.getDigest(resource);
+            File cachedFile = cache.cacheFile(resource.getFinalTarget(), digest, digest);
+            File cachedParent = cachedFile.getParentFile();
+            File unpackedIndicator = new File(cachedParent, cachedFile.getName() + ".unpacked");
+
+            if (!unpackedIndicator.exists()) {
+                try {
+                    FileUtil.unpackJar(new JarFile(cachedFile), cachedParent, false);
+                    unpackedIndicator.createNewFile();
+                } catch (IOException ioe) {
+                    log.warning("Failed to unpack native jar",
+                                "file", cachedFile.getAbsolutePath(), ioe);
+                    // Keep going and unpack the other jars...
+                }
+            }
+
+            nativedirs.add(cachedFile.getParentFile());
+        }
+
+        if (addCurrentLibraryPath) {
+            for (String path : System.getProperty("java.library.path").split(File.pathSeparator)) {
+                nativedirs.add(new File(path));
+            }
+        }
+
+        return new ClassPath(nativedirs);
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Properties.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Properties.java
new file mode 100644 (file)
index 0000000..e70bd4b
--- /dev/null
@@ -0,0 +1,19 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+/**
+ * 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";
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
new file mode 100644 (file)
index 0000000..3e2f446
--- /dev/null
@@ -0,0 +1,394 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.*;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import com.threerings.getdown.util.FileUtil;
+import com.threerings.getdown.util.ProgressObserver;
+import com.threerings.getdown.util.StringUtil;
+
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Models a single file resource used by an {@link Application}.
+ */
+public class Resource implements Comparable<Resource>
+{
+    /** Defines special attributes for resources. */
+    public static enum Attr {
+        /** Indicates that the resource should be unpacked. */
+        UNPACK,
+        /** If present, when unpacking a resource, any directories created by the newly
+          * unpacked resource will first be cleared of files before unpacking. */
+        CLEAN,
+        /** Indicates that the resource should be marked executable. */
+        EXEC,
+        /** Indicates that the resource should be downloaded before a UI is displayed. */
+        PRELOAD,
+        /** Indicates that the resource is a jar containing native libs. */
+        NATIVE
+    };
+
+    public static final EnumSet<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);
+    public static final EnumSet<Attr> PRELOAD = EnumSet.of(Attr.PRELOAD);
+    public static final EnumSet<Attr> NATIVE  = EnumSet.of(Attr.NATIVE);
+
+    /**
+     * Computes the MD5 hash of the supplied file.
+     * @param version the version of the digest protocol to use.
+     */
+    public static String computeDigest (int version, File target, MessageDigest md,
+                                        ProgressObserver obs)
+        throws IOException
+    {
+        md.reset();
+        byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
+        int read;
+
+        boolean isJar = isJar(target.getPath());
+        boolean isPacked200Jar = isPacked200Jar(target.getPath());
+
+        // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
+        // manner to properly correlate jardiff patched jars with their unpatched originals
+        if (isJar || isPacked200Jar){
+            File tmpJarFile = null;
+            JarFile jar = null;
+            try {
+                // if this is a compressed jar file, uncompress it to compute the jar file digest
+                if (isPacked200Jar){
+                    tmpJarFile = new File(target.getPath() + ".tmp");
+                    FileUtil.unpackPacked200Jar(target, tmpJarFile);
+                    jar = new JarFile(tmpJarFile);
+                } else{
+                    jar = new JarFile(target);
+                }
+
+                List<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();
+    }
+
+    /**
+     * Returns true if this resource should be pre-downloaded.
+     */
+    public boolean shouldPredownload ()
+    {
+        return _attrs.contains(Attr.PRELOAD);
+    }
+
+    /**
+     * Returns true if this resource is a native lib jar.
+     */
+    public boolean isNative ()
+    {
+        return _attrs.contains(Attr.NATIVE);
+    }
+
+    /**
+     * Computes the MD5 hash of this resource's underlying file.
+     * <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}.
+     * @param validate whether or not to mark the resource as valid after installing.
+     */
+    public void install (boolean validate) throws IOException {
+        File source = getLocalNew(), dest = getLocal();
+        log.info("- " + source);
+        if (!FileUtil.renameTo(source, dest)) {
+            throw new IOException("Failed to rename " + source + " to " + dest);
+        }
+        applyAttrs();
+        if (validate) {
+            markAsValid();
+        }
+    }
+
+    /**
+     * Unpacks this resource file into the directory that contains it.
+     */
+    public void unpack () throws IOException
+    {
+        // sanity check
+        if (!_isJar && !_isPacked200Jar) {
+            throw new IOException("Requested to unpack non-jar file '" + _local + "'.");
+        }
+        if (_isJar) {
+            try (JarFile jar = new JarFile(_local)) {
+                FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN));
+            }
+        } else {
+            FileUtil.unpackPacked200Jar(_local, _unpacked);
+        }
+    }
+
+    /**
+     * Applies this resources special attributes: unpacks this resource if needed, marks it as
+     * executable if needed.
+     */
+    public void applyAttrs () throws IOException {
+        if (shouldUnpack()) {
+            unpack();
+        }
+        if (_attrs.contains(Attr.EXEC)) {
+            FileUtil.makeExecutable(_local);
+        }
+    }
+
+    /**
+     * Wipes this resource file along with any "validated" marker file that may be associated with
+     * it.
+     */
+    public void erase ()
+    {
+        clearMarker();
+        if (_local.exists() && !FileUtil.deleteHarder(_local)) {
+            log.warning("Failed to erase resource '" + _local + "'.");
+        }
+    }
+
+    @Override public int compareTo (Resource other) {
+        return _path.compareTo(other._path);
+    }
+
+    @Override public boolean equals (Object other)
+    {
+        if (other instanceof Resource) {
+            return _path.equals(((Resource)other)._path);
+        } else {
+            return false;
+        }
+    }
+
+    @Override public int hashCode ()
+    {
+        return _path.hashCode();
+    }
+
+    @Override public String toString ()
+    {
+        return _path;
+    }
+
+    /** Helper function to simplify the process of reporting progress. */
+    protected static void updateProgress (ProgressObserver obs, long pos, long total)
+    {
+        if (obs != null) {
+            obs.progress((int)(100 * pos / total));
+        }
+    }
+
+    protected static boolean isJar (String path)
+    {
+        return path.endsWith(".jar") || path.endsWith(".jar_new");
+    }
+
+    protected static boolean isPacked200Jar (String path)
+    {
+        return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
+            path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new");
+    }
+
+    protected String _path;
+    protected URL _remote;
+    protected File _local, _localNew, _marker, _unpacked;
+    protected EnumSet<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;
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
new file mode 100644 (file)
index 0000000..0d96ecb
--- /dev/null
@@ -0,0 +1,185 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.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 bootstrap appbase (used 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;
+    }
+
+    /** Instructs Getdown to install/update the app without ever bringing up a UI (except in the
+      * event of an error), and then launch it.
+      * Usage: {@code -Dsilent=launch}. */
+    public static boolean launchInSilent () {
+        return "launch".equals(System.getProperty("silent"));
+    }
+
+    /**
+     * Instructs Getdown to launch the app without updating it, or ever bringing up a UI (except
+     * in the event of an error).
+     * Usage: {@code -Dsilent=noupdate}.
+     */
+    public static boolean noUpdate() {
+        return "noupdate".equals(System.getProperty("silent"));
+    }
+
+    /** 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;
+    }
+
+    /** Specifies the delay (in minutes) to wait before starting the update and install process.
+      * Minimum delay is 0 minutes, or no delay (negative values are rounded up to 0 minutes).
+      * Maximum delay is 1 day, or 1440 minutes (larger values are rounded down to 1 day).
+      * Usage: {@code -Ddelay=N}. */
+    public static int startDelay () {
+        return Math.min(Math.max(Integer.getInteger("delay", 0), 0), 60 * 24);
+    }
+
+    /** 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);
+    }
+
+    /** Returns the number of threads used to perform digesting and verifying operations in
+      * parallel. Usage: {@code -Dthread_pool_size=N} */
+    public static int threadPoolSize () {
+        int defaultSize = Math.max(Runtime.getRuntime().availableProcessors()-1, 1);
+        return Integer.getInteger("thread_pool_size", defaultSize);
+    }
+
+    /** 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;
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
new file mode 100644 (file)
index 0000000..6033e2f
--- /dev/null
@@ -0,0 +1,229 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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 java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+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 protected
+ * callback methods. <em>Note:</em> these methods are all called arbitrary download threads, so
+ * implementors must take care to only execute thread-safe code or simply pass a message to the AWT
+ * thread, for example.
+ */
+public abstract class Downloader
+{
+    /**
+     * Start the downloading process.
+     * @param resources the resources to download.
+     * @param maxConcurrent the maximum number of concurrent downloads allowed.
+     * @return true if the download completed, false if it was aborted (via {@link #abort}).
+     */
+    public boolean download (Collection<Resource> resources, int maxConcurrent)
+    {
+        // first compute the total size of our download
+        resolvingDownloads();
+        for (Resource rsrc : resources) {
+            try {
+                _sizes.put(rsrc, Math.max(checkSize(rsrc), 0L));
+            } catch (IOException ioe) {
+                downloadFailed(rsrc, ioe);
+            }
+        }
+
+        long totalSize = sum(_sizes.values());
+        log.info("Downloading " + resources.size() + " resources",
+                 "totalBytes", totalSize, "maxConcurrent", maxConcurrent);
+
+        // make a note of the time at which we started the download
+        _start = System.currentTimeMillis();
+
+        // start the downloads
+        ExecutorService exec = Executors.newFixedThreadPool(maxConcurrent);
+        for (final Resource rsrc : resources) {
+            // 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 + "'.");
+            }
+
+            exec.execute(new Runnable() {
+                @Override public void run () {
+                    try {
+                        if (_state != State.ABORTED) {
+                            download(rsrc);
+                        }
+                    } catch (IOException ioe) {
+                        _state = State.FAILED;
+                        downloadFailed(rsrc, ioe);
+                    }
+                }
+            });
+        }
+        exec.shutdown();
+
+        // wait for the downloads to complete
+        try {
+            exec.awaitTermination(10, TimeUnit.DAYS);
+
+            // report download completion if we did not already do so via our final resource
+            if (_state == State.DOWNLOADING) {
+                downloadProgress(100, 0);
+            }
+
+        } catch (InterruptedException ie) {
+            exec.shutdownNow();
+            downloadFailed(null, ie);
+        }
+
+        return _state != State.ABORTED;
+    }
+
+    /**
+     * Aborts the in-progress download.
+     */
+    public void abort () {
+        _state = State.ABORTED;
+    }
+
+    /**
+     * Called before the downloader begins the series of HTTP head requests to determine the
+     * size of the files it needs to download.
+     */
+    protected void resolvingDownloads () {}
+
+    /**
+     * Reports ongoing progress toward completion of the overall downloading task. One call is
+     * guaranteed to be made reporting 100% completion if the download is not aborted and no
+     * resources fail.
+     *
+     * @param percent the percent completion of the complete download process (based on total bytes
+     * downloaded versus total byte size of all resources).
+     * @param remaining the estimated download time remaining in seconds, or {@code -1} if the time
+     * can not yet be determined.
+     */
+    protected void downloadProgress (int percent, long remaining) {}
+
+    /**
+     * Called if a failure occurs while downloading a resource. No progress will be reported after
+     * a download fails, but additional download failures may be reported.
+     *
+     * @param rsrc the resource that failed to download, or null if the download failed due to
+     * thread interruption.
+     * @param cause the exception detailing the failure.
+     */
+    protected void downloadFailed (Resource rsrc, Exception cause) {}
+
+    /**
+     * Performs the protocol-specific portion of checking download size.
+     */
+    protected abstract long checkSize (Resource rsrc) throws IOException;
+
+    /**
+     * 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 synchronized void reportProgress (Resource rsrc, long currentSize, long actualSize)
+    {
+        // 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;
+
+            // if we're complete or failed, when we don't want to report again
+            if (_state == State.DOWNLOADING) {
+                if (pctdone == 100) _state = State.COMPLETE;
+                downloadProgress(pctdone, remaining);
+            }
+        }
+    }
+
+    /**
+     * Sums the supplied values.
+     */
+    protected static long sum (Iterable<Long> values)
+    {
+        long acc = 0L;
+        for (Long value : values) {
+            acc += value;
+        }
+        return acc;
+    }
+
+    protected enum State { DOWNLOADING, COMPLETE, FAILED, ABORTED }
+
+    /**
+     * Accomplishes the copying of the resource from remote location to local location using
+     * protocol-specific code. This method should periodically check whether {@code _state} is set
+     * to aborted and abort any in-progress download if so.
+     */
+    protected abstract void download (Resource rsrc) throws IOException;
+
+    /** 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 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;
+
+    /** A wee state machine to ensure we call our callbacks sanely. */
+    protected volatile State _state = State.DOWNLOADING;
+
+    /** The delay in milliseconds between notifying progress observers of file download
+      * progress. */
+    protected static final long UPDATE_DELAY = 500L;
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
new file mode 100644 (file)
index 0000000..a7a3287
--- /dev/null
@@ -0,0 +1,115 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.net;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+
+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 (Proxy proxy)
+    {
+        _proxy = proxy;
+    }
+
+    @Override protected long checkSize (Resource rsrc) throws IOException
+    {
+        URLConnection conn = ConnectionUtil.open(_proxy, 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 download (Resource rsrc) throws IOException
+    {
+        // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a
+        // system property
+        if (true) {
+            // download the resource from the specified URL
+            URLConnection conn = ConnectionUtil.open(_proxy, 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;
+            byte[] buffer = new byte[4*4096];
+            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) {
+                    // abort the download if the downloader is aborted
+                    if (_state == State.ABORTED) {
+                        break;
+                    }
+                    // write it out to our local copy
+                    out.write(buffer, 0, read);
+                    // note that we've downloaded some data
+                    currentSize += read;
+                    reportProgress(rsrc, currentSize, actualSize);
+                }
+            }
+
+        } else {
+            log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown");
+            File localNew = rsrc.getLocalNew();
+            try (ReadableByteChannel rbc = Channels.newChannel(rsrc.getRemote().openStream());
+                 FileOutputStream fos = new FileOutputStream(localNew)) {
+                // TODO: more work is needed here, transferFrom can fail to transfer the entire
+                // file, in which case it's not clear what we're supposed to do.. call it again?
+                // will it repeatedly fail?
+                fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+                reportProgress(rsrc, localNew.length(), localNew.length());
+            }
+        }
+    }
+
+    protected final Proxy _proxy;
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
new file mode 100644 (file)
index 0000000..22446ec
--- /dev/null
@@ -0,0 +1,32 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.spi;
+
+/**
+ * A service provider interface that handles the storage of proxy credentials.
+ */
+public interface ProxyAuth
+{
+    /** Credentials for a proxy server. */
+    public static class Credentials {
+        public final String username;
+        public final String password;
+        public Credentials (String username, String password) {
+            this.username = username;
+            this.password = password;
+        }
+    }
+
+    /**
+     * Loads the credentials for the app installed in {@code appDir}.
+     */
+    public Credentials loadCredentials (String appDir);
+
+    /**
+     * Encrypts and saves the credentials for the app installed in {@code appDir}.
+     */
+    public void saveCredentials (String appDir, String username, String password);
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
new file mode 100644 (file)
index 0000000..c2e740b
--- /dev/null
@@ -0,0 +1,232 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tools;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+import java.security.MessageDigest;
+
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Digest;
+import com.threerings.getdown.data.EnvConfig;
+import com.threerings.getdown.data.Resource;
+import com.threerings.getdown.util.FileUtil;
+import com.threerings.getdown.util.StreamUtil;
+
+/**
+ * Generates patch files between two particular revisions of an
+ * application. The differences between all the files in the two
+ * revisions are bundled into a single patch file which is placed into the
+ * target version directory.
+ */
+public class Differ
+{
+    /**
+     * Creates a single patch file that contains the differences between
+     * the two specified application directories. The patch file will be
+     * created in the <code>nvdir</code> directory with name
+     * <code>patchV.dat</code> where V is the old application version.
+     */
+    public void createDiff (File nvdir, File ovdir, boolean verbose)
+        throws IOException
+    {
+        // sanity check
+        String nvers = nvdir.getName();
+        String overs = ovdir.getName();
+        try {
+            if (Long.parseLong(nvers) <= Long.parseLong(overs)) {
+                String err = "New version (" + nvers + ") must be greater " +
+                    "than old version (" + overs + ").";
+                throw new IOException(err);
+            }
+        } catch (NumberFormatException nfe) {
+            throw new IOException("Non-numeric versions? [nvers=" + nvers +
+                                  ", overs=" + overs + "].");
+        }
+
+        Application oapp = new Application(new EnvConfig(ovdir));
+        oapp.init(false);
+        ArrayList<Resource> orsrcs = new ArrayList<>();
+        orsrcs.addAll(oapp.getCodeResources());
+        orsrcs.addAll(oapp.getResources());
+
+        Application napp = new Application(new EnvConfig(nvdir));
+        napp.init(false);
+        ArrayList<Resource> nrsrcs = new ArrayList<>();
+        nrsrcs.addAll(napp.getCodeResources());
+        nrsrcs.addAll(napp.getResources());
+
+        // first create a patch for the main application
+        File patch = new File(nvdir, "patch" + overs + ".dat");
+        createPatch(patch, orsrcs, nrsrcs, verbose);
+
+        // next create patches for any auxiliary resource groups
+        for (Application.AuxGroup ag : napp.getAuxGroups()) {
+            orsrcs = new ArrayList<>();
+            Application.AuxGroup oag = oapp.getAuxGroup(ag.name);
+            if (oag != null) {
+                orsrcs.addAll(oag.codes);
+                orsrcs.addAll(oag.rsrcs);
+            }
+            nrsrcs = new ArrayList<>();
+            nrsrcs.addAll(ag.codes);
+            nrsrcs.addAll(ag.rsrcs);
+            patch = new File(nvdir, "patch-" + ag.name + overs + ".dat");
+            createPatch(patch, orsrcs, nrsrcs, verbose);
+        }
+    }
+
+    protected void createPatch (File patch, ArrayList<Resource> orsrcs,
+                                ArrayList<Resource> nrsrcs, boolean verbose)
+        throws IOException
+    {
+        int version = Digest.VERSION;
+        MessageDigest md = Digest.getMessageDigest(version);
+        try (FileOutputStream fos = new FileOutputStream(patch);
+             BufferedOutputStream buffered = new BufferedOutputStream(fos);
+             JarOutputStream jout = new JarOutputStream(buffered)) {
+
+            // for each file in the new application, it either already exists
+            // in the old application, or it is new
+            for (Resource rsrc : nrsrcs) {
+                int oidx = orsrcs.indexOf(rsrc);
+                Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx);
+                if (orsrc != null) {
+                    // first see if they are the same
+                    String odig = orsrc.computeDigest(version, md, null);
+                    String ndig = rsrc.computeDigest(version, md, null);
+                    if (odig.equals(ndig)) {
+                        if (verbose) {
+                            System.out.println("Unchanged: " + rsrc.getPath());
+                        }
+                        // by leaving it out, it will be left as is during the
+                        // patching process
+                        continue;
+                    }
+
+                    // otherwise potentially create a jar diff
+                    if (rsrc.getPath().endsWith(".jar")) {
+                        if (verbose) {
+                            System.out.println("JarDiff: " + rsrc.getPath());
+                        }
+                        // here's a juicy one: JarDiff blindly pulls ZipEntry
+                        // objects out of one jar file and stuffs them into
+                        // another without clearing out things like the
+                        // compressed size, so if, for whatever reason (like
+                        // different JRE versions or phase of the moon) the
+                        // compressed size in the old jar file is different
+                        // than the compressed size generated when creating the
+                        // jardiff jar file, ZipOutputStream will choke and
+                        // we'll be hosed; so we recreate the jar files in
+                        // their entirety before running jardiff on 'em
+                        File otemp = rebuildJar(orsrc.getLocal());
+                        File temp = rebuildJar(rsrc.getLocal());
+                        jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.PATCH));
+                        jarDiff(otemp, temp, jout);
+                        FileUtil.deleteHarder(otemp);
+                        FileUtil.deleteHarder(temp);
+                        continue;
+                    }
+                }
+
+                if (verbose) {
+                    System.out.println("Addition: " + rsrc.getPath());
+                }
+                jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.CREATE));
+                pipe(rsrc.getLocal(), jout);
+            }
+
+            // now any file remaining in orsrcs needs to be removed
+            for (Resource rsrc : orsrcs) {
+                // add an entry with the resource name and the deletion suffix
+                if (verbose) {
+                    System.out.println("Removal: " + rsrc.getPath());
+                }
+                jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.DELETE));
+            }
+
+            System.out.println("Created patch file: " + patch);
+
+        } catch (IOException ioe) {
+            FileUtil.deleteHarder(patch);
+            throw ioe;
+        }
+    }
+
+    protected File rebuildJar (File target)
+        throws IOException
+    {
+        File temp = File.createTempFile("differ", "jar");
+        try (JarFile jar = new JarFile(target);
+             FileOutputStream tempFos = new FileOutputStream(temp);
+             BufferedOutputStream tempBos = new BufferedOutputStream(tempFos);
+             JarOutputStream jout = new JarOutputStream(tempBos)) {
+            byte[] buffer = new byte[4096];
+            for (Enumeration< JarEntry > iter = jar.entries(); iter.hasMoreElements();) {
+                JarEntry entry = iter.nextElement();
+                entry.setCompressedSize(-1);
+                jout.putNextEntry(entry);
+                try (InputStream in = jar.getInputStream(entry)) {
+                    int size = in.read(buffer);
+                    while (size != -1) {
+                        jout.write(buffer, 0, size);
+                        size = in.read(buffer);
+                    }
+                }
+            }
+        }
+        return temp;
+    }
+
+    protected void jarDiff (File ofile, File nfile, JarOutputStream jout)
+        throws IOException
+    {
+        JarDiff.createPatch(ofile.getPath(), nfile.getPath(), jout, false);
+    }
+
+    public static void main (String[] args)
+    {
+        if (args.length < 2) {
+            System.err.println(
+                "Usage: Differ [-verbose] new_vers_dir old_vers_dir");
+            System.exit(255);
+        }
+        Differ differ = new Differ();
+        boolean verbose = false;
+        int aidx = 0;
+        if (args[0].equals("-verbose")) {
+            verbose = true;
+            aidx++;
+        }
+        try {
+            differ.createDiff(new File(args[aidx++]),
+                              new File(args[aidx++]), verbose);
+        } catch (IOException ioe) {
+            System.err.println("Error: " + ioe.getMessage());
+            System.exit(255);
+        }
+    }
+
+    protected static void pipe (File file, JarOutputStream jout)
+        throws IOException
+    {
+        try (FileInputStream fin = new FileInputStream(file)) {
+            StreamUtil.copy(fin, jout);
+        }
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
new file mode 100644 (file)
index 0000000..b04a653
--- /dev/null
@@ -0,0 +1,129 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tools;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.Signature;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Digest;
+import com.threerings.getdown.data.EnvConfig;
+import com.threerings.getdown.data.Resource;
+import com.threerings.getdown.util.Base64;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Handles the generation of the digest.txt file.
+ */
+public class Digester
+{
+    /**
+     * A command line entry point for the digester.
+     */
+    public static void main (String[] args)
+        throws IOException, GeneralSecurityException
+    {
+        switch (args.length) {
+        case 1:
+            createDigests(new File(args[0]), null, null, null);
+            break;
+        case 4:
+            createDigests(new File(args[0]), new File(args[1]), args[2], args[3]);
+            break;
+        default:
+            System.err.println("Usage: Digester app_dir [keystore_path password alias]");
+            System.exit(255);
+        }
+    }
+
+    /**
+     * Creates digest file(s) and optionally signs them if {@code keystore} is not null.
+     */
+    public static void createDigests (File appdir, File keystore, String password, String alias)
+        throws IOException, GeneralSecurityException
+    {
+        for (int version = 1; version <= Digest.VERSION; version++) {
+            createDigest(version, appdir);
+            if (keystore != null) {
+                signDigest(version, appdir, keystore, password, alias);
+            }
+        }
+    }
+
+    /**
+     * Creates a digest file in the specified application directory.
+     */
+    public static void createDigest (int version, File appdir)
+        throws IOException
+    {
+        File target = new File(appdir, Digest.digestFile(version));
+        System.out.println("Generating digest file '" + target + "'...");
+
+        // create our application and instruct it to parse its business
+        Application app = new Application(new EnvConfig(appdir));
+        app.init(false);
+
+        List<Resource> rsrcs = new ArrayList<>();
+        rsrcs.add(app.getConfigResource());
+        rsrcs.addAll(app.getCodeResources());
+        rsrcs.addAll(app.getResources());
+        for (Application.AuxGroup ag : app.getAuxGroups()) {
+            rsrcs.addAll(ag.codes);
+            rsrcs.addAll(ag.rsrcs);
+        }
+
+        // now generate the digest file
+        Digest.createDigest(version, rsrcs, target);
+    }
+
+    /**
+     * Creates a digest file in the specified application directory.
+     */
+    public static void signDigest (int version, File appdir,
+                                   File storePath, String storePass, String storeAlias)
+        throws IOException, GeneralSecurityException
+    {
+        String filename = Digest.digestFile(version);
+        File inputFile = new File(appdir, filename);
+        File signatureFile = new File(appdir, filename + Application.SIGNATURE_SUFFIX);
+
+        try (FileInputStream storeInput = new FileInputStream(storePath);
+             FileInputStream dataInput = new FileInputStream(inputFile);
+             FileOutputStream signatureOutput = new FileOutputStream(signatureFile)) {
+
+            // initialize the keystore
+            KeyStore store = KeyStore.getInstance("JKS");
+            store.load(storeInput, storePass.toCharArray());
+            PrivateKey key = (PrivateKey)store.getKey(storeAlias, storePass.toCharArray());
+
+            // sign the digest file
+            String algo = Digest.sigAlgorithm(version);
+            Signature sig = Signature.getInstance(algo);
+            byte[] buffer = new byte[8192];
+            int length;
+
+            sig.initSign(key);
+            while ((length = dataInput.read(buffer)) != -1) {
+                sig.update(buffer, 0, length);
+            }
+
+            // Write out the signature
+            String signed = Base64.encodeToString(sig.sign(), Base64.DEFAULT);
+            signatureOutput.write(signed.getBytes(UTF_8));
+        }
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
new file mode 100644 (file)
index 0000000..1cea0ea
--- /dev/null
@@ -0,0 +1,449 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+/*
+ * @(#)JarDiff.java 1.7 05/11/17
+ *
+ * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * -Redistribution of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *
+ * -Redistribution in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ *
+ * Neither the name of Sun Microsystems, Inc. or the names of contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
+ * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
+ * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN")
+ * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
+ * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
+ * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
+ * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
+ * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
+ * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
+ * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ *
+ * You acknowledge that this software is not designed, licensed or intended
+ * for use in the design, construction, operation or maintenance of any
+ * nuclear facility.
+ */
+
+package com.threerings.getdown.tools;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * JarDiff is able to create a jar file containing the delta between two jar files (old and new).
+ * The delta jar file can then be applied to the old jar file to reconstruct the new jar file.
+ *
+ * <p> Refer to the JNLP spec for details on how this is done.
+ *
+ * @version 1.13, 06/26/03
+ */
+public class JarDiff implements JarDiffCodes
+{
+    private static final int DEFAULT_READ_SIZE = 2048;
+    private static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
+    private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
+
+    // The JARDiff.java is the stand-alone jardiff.jar tool. Thus, we do not depend on Globals.java
+    // and other stuff here. Instead, we use an explicit _debug flag.
+    private static boolean _debug;
+
+    /**
+     * Creates a patch from the two passed in files, writing the result to <code>os</code>.
+     */
+    public static void createPatch (String oldPath, String newPath,
+                                    OutputStream os, boolean minimal) throws IOException
+    {
+        try (JarFile2 oldJar = new JarFile2(oldPath);
+             JarFile2 newJar = new JarFile2(newPath)) {
+
+            HashMap<String,String> moved = new HashMap<>();
+            HashSet<String> implicit = new HashSet<>();
+            HashSet<String> moveSrc = new HashSet<>();
+            HashSet<String> newEntries = new HashSet<>();
+
+            // FIRST PASS
+            // Go through the entries in new jar and
+            // determine which files are candidates for implicit moves
+            // ( files that has the same filename and same content in old.jar
+            // and new.jar )
+            // and for files that cannot be implicitly moved, we will either
+            // find out whether it is moved or new (modified)
+            for (JarEntry newEntry : newJar) {
+                String newname = newEntry.getName();
+
+                // Return best match of contents, will return a name match if possible
+                String oldname = oldJar.getBestMatch(newJar, newEntry);
+                if (oldname == null) {
+                    // New or modified entry
+                    if (_debug) {
+                        System.out.println("NEW: "+ newname);
+                    }
+                    newEntries.add(newname);
+                } else {
+                    // Content already exist - need to do a move
+
+                    // Should do implicit move? Yes, if names are the same, and
+                    // no move command already exist from oldJar
+                    if (oldname.equals(newname) && !moveSrc.contains(oldname)) {
+                        if (_debug) {
+                            System.out.println(newname + " added to implicit set!");
+                        }
+                        implicit.add(newname);
+                    } else {
+                        // The 1.0.1/1.0 JarDiffPatcher cannot handle
+                        // multiple MOVE command with same src.
+                        // The work around here is if we are going to generate
+                        // a MOVE command with duplicate src, we will
+                        // instead add the target as a new file.  This way
+                        // the jardiff can be applied by 1.0.1/1.0
+                        // JarDiffPatcher also.
+                        if (!minimal && (implicit.contains(oldname) ||
+                                         moveSrc.contains(oldname) )) {
+
+                            // generate non-minimal jardiff
+                            // for backward compatibility
+
+                            if (_debug) {
+
+                                System.out.println("NEW: "+ newname);
+                            }
+                            newEntries.add(newname);
+                        } else {
+                            // Use newname as key, since they are unique
+                            if (_debug) {
+                                System.err.println("moved.put " + newname + " " + oldname);
+                            }
+                            moved.put(newname, oldname);
+                            moveSrc.add(oldname);
+                        }
+                        // Check if this disables an implicit 'move <oldname> <oldname>'
+                        if (implicit.contains(oldname) && minimal) {
+
+                            if (_debug) {
+                                System.err.println("implicit.remove " + oldname);
+
+                                System.err.println("moved.put " + oldname + " " + oldname);
+
+                            }
+                            implicit.remove(oldname);
+                            moved.put(oldname, oldname);
+                            moveSrc.add(oldname);
+                        }
+                    }
+                }
+            }
+
+            // SECOND PASS: <deleted files> = <oldjarnames> - <implicitmoves> -
+            // <source of move commands> - <new or modified entries>
+            ArrayList<String> deleted = new ArrayList<>();
+            for (JarEntry oldEntry : oldJar) {
+                String oldName = oldEntry.getName();
+                if (!implicit.contains(oldName) && !moveSrc.contains(oldName)
+                    && !newEntries.contains(oldName)) {
+                    if (_debug) {
+                        System.err.println("deleted.add " + oldName);
+                    }
+                    deleted.add(oldName);
+                }
+            }
+
+            //DEBUG
+            if (_debug) {
+                //DEBUG:  print out moved map
+                System.out.println("MOVED MAP!!!");
+                for (Map.Entry<String,String> entry : moved.entrySet()) {
+                    System.out.println(entry);
+                }
+
+                //DEBUG:  print out IMOVE map
+                System.out.println("IMOVE MAP!!!");
+                for (String newName : implicit) {
+                    System.out.println("key is " + newName);
+                }
+            }
+
+            JarOutputStream jos = new JarOutputStream(os);
+
+            // Write out all the MOVEs and REMOVEs
+            createIndex(jos, deleted, moved);
+
+            // Put in New and Modified entries
+            for (String newName : newEntries) {
+                if (_debug) {
+                    System.out.println("New File: " + newName);
+                }
+                writeEntry(jos, newJar.getEntryByName(newName), newJar);
+            }
+
+            jos.finish();
+//            jos.close();
+        }
+    }
+
+    /**
+     * Writes the index file out to <code>jos</code>.
+     * <code>oldEntries</code> gives the names of the files that were removed,
+     * <code>movedMap</code> maps from the new name to the old name.
+     */
+    private static void createIndex (JarOutputStream jos, List<String> oldEntries,
+                                     Map<String,String> movedMap)
+        throws IOException
+    {
+        StringWriter writer = new StringWriter();
+        writer.write(VERSION_HEADER);
+        writer.write("\r\n");
+
+        // Write out entries that have been removed
+        for (String name : oldEntries) {
+            writer.write(REMOVE_COMMAND);
+            writer.write(" ");
+            writeEscapedString(writer, name);
+            writer.write("\r\n");
+        }
+
+        // And those that have moved
+        for (String newName : movedMap.keySet()) {
+            String oldName = movedMap.get(newName);
+            writer.write(MOVE_COMMAND);
+            writer.write(" ");
+            writeEscapedString(writer, oldName);
+            writer.write(" ");
+            writeEscapedString(writer, newName);
+            writer.write("\r\n");
+        }
+
+        jos.putNextEntry(new JarEntry(INDEX_NAME));
+        byte[] bytes = writer.toString().getBytes(UTF_8);
+        jos.write(bytes, 0, bytes.length);
+    }
+
+    protected static Writer writeEscapedString (Writer writer, String string)
+        throws IOException
+    {
+        int index = 0;
+        int last = 0;
+        char[] chars = null;
+
+        while ((index = string.indexOf(' ', index)) != -1) {
+            if (last != index) {
+                if (chars == null) {
+                    chars = string.toCharArray();
+                }
+                writer.write(chars, last, index - last);
+            }
+            last = index;
+            index++;
+            writer.write('\\');
+        }
+        if (last != 0 && chars != null) {
+            writer.write(chars, last, chars.length - last);
+        }
+        else {
+            // no spaces
+            writer.write(string);
+        }
+
+        return writer;
+    }
+
+    private static void writeEntry (JarOutputStream jos, JarEntry entry, JarFile2 file)
+        throws IOException
+    {
+        try (InputStream data = file.getJarFile().getInputStream(entry)) {
+            jos.putNextEntry(entry);
+            int size = data.read(newBytes);
+            while (size != -1) {
+                jos.write(newBytes, 0, size);
+                size = data.read(newBytes);
+            }
+        }
+    }
+
+    /**
+     * JarFile2 wraps a JarFile providing some convenience methods.
+     */
+    private static class JarFile2 implements Iterable<JarEntry>, Closeable
+    {
+        private JarFile _jar;
+        private List<JarEntry> _entries;
+        private HashMap<String,JarEntry> _nameToEntryMap;
+        private HashMap<Long,LinkedList<JarEntry>> _crcToEntryMap;
+
+        public JarFile2 (String path) throws IOException {
+            _jar = new JarFile(new File(path));
+            index();
+        }
+
+        public JarFile getJarFile () {
+            return _jar;
+        }
+
+        // from interface Iterable<JarEntry>
+        @Override
+        public Iterator<JarEntry> iterator () {
+            return _entries.iterator();
+        }
+
+        public JarEntry getEntryByName (String name) {
+            return _nameToEntryMap.get(name);
+        }
+
+        /**
+         * Returns true if the two InputStreams differ.
+         */
+        private static boolean differs (InputStream oldIS, InputStream newIS) throws IOException {
+            int newSize = 0;
+            int oldSize;
+            int total = 0;
+            boolean retVal = false;
+
+            while (newSize != -1) {
+                newSize = newIS.read(newBytes);
+                oldSize = oldIS.read(oldBytes);
+
+                if (newSize != oldSize) {
+                    if (_debug) {
+                        System.out.println("\tread sizes differ: " + newSize +
+                                           " " + oldSize + " total " + total);
+                    }
+                    retVal = true;
+                    break;
+                }
+                if (newSize > 0) {
+                    while (--newSize >= 0) {
+                        total++;
+                        if (newBytes[newSize] != oldBytes[newSize]) {
+                            if (_debug) {
+                                System.out.println("\tbytes differ at " +
+                                                   total);
+                            }
+                            retVal = true;
+                            break;
+                        }
+                        if ( retVal ) {
+                            //Jump out
+                            break;
+                        }
+                        newSize = 0;
+                    }
+                }
+            }
+
+            return retVal;
+        }
+
+        public String getBestMatch (JarFile2 file, JarEntry entry) throws IOException {
+            // check for same name and same content, return name if found
+            if (contains(file, entry)) {
+                return (entry.getName());
+            }
+
+            // return name of same content file or null
+            return (hasSameContent(file,entry));
+        }
+
+        public boolean contains (JarFile2 f, JarEntry e) throws IOException {
+
+            JarEntry thisEntry = getEntryByName(e.getName());
+
+            // Look up name in 'this' Jar2File - if not exist return false
+            if (thisEntry == null)
+                return false;
+
+            // Check CRC - if no match - return false
+            if (thisEntry.getCrc() != e.getCrc())
+                return false;
+
+            // Check contents - if no match - return false
+            try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
+                 InputStream newIS = f.getJarFile().getInputStream(e)) {
+                return !differs(oldIS, newIS);
+            }
+        }
+
+        public String hasSameContent (JarFile2 file, JarEntry entry) throws IOException {
+            String thisName = null;
+            Long crcL = Long.valueOf(entry.getCrc());
+            // check if this jar contains files with the passed in entry's crc
+            if (_crcToEntryMap.containsKey(crcL)) {
+                // get the Linked List with files with the crc
+                LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
+                // go through the list and check for content match
+                ListIterator<JarEntry> li = ll.listIterator(0);
+                while (li.hasNext()) {
+                    JarEntry thisEntry = li.next();
+                    // check for content match
+                    try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
+                         InputStream newIS = file.getJarFile().getInputStream(entry)) {
+                        if (!differs(oldIS, newIS)) {
+                            thisName = thisEntry.getName();
+                            return thisName;
+                        }
+                    }
+                }
+            }
+            return thisName;
+        }
+
+        private void index () throws IOException {
+            Enumeration<JarEntry> entries = _jar.entries();
+
+            _nameToEntryMap = new HashMap<>();
+            _crcToEntryMap = new HashMap<>();
+            _entries = new ArrayList<>();
+            if (_debug) {
+                System.out.println("indexing: " + _jar.getName());
+            }
+            if (entries != null) {
+                while (entries.hasMoreElements()) {
+                    JarEntry entry = entries.nextElement();
+                    long crc = entry.getCrc();
+                    Long crcL = Long.valueOf(crc);
+                    if (_debug) {
+                        System.out.println("\t" + entry.getName() + " CRC " + crc);
+                    }
+
+                    _nameToEntryMap.put(entry.getName(), entry);
+                    _entries.add(entry);
+
+                    // generate the CRC to entries map
+                    if (_crcToEntryMap.containsKey(crcL)) {
+                        // key exist, add the entry to the correcponding linked list
+                        LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
+                        ll.add(entry);
+                        _crcToEntryMap.put(crcL, ll);
+
+                    } else {
+                        // create a new entry in the hashmap for the new key
+                        LinkedList<JarEntry> ll = new LinkedList<JarEntry>();
+                        ll.add(entry);
+                        _crcToEntryMap.put(crcL, ll);
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            _jar.close();
+        }
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffCodes.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffCodes.java
new file mode 100644 (file)
index 0000000..3b5db80
--- /dev/null
@@ -0,0 +1,24 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tools;
+
+/**
+ * Constants shared by {@link JarDiff} and {@link JarDiffPatcher}.
+ */
+public interface JarDiffCodes
+{
+    /** The name of the jardiff control file. */
+    String INDEX_NAME = "META-INF/INDEX.JD";
+
+    /** The version header used in the control file. */
+    String VERSION_HEADER = "version 1.0";
+
+    /** A jardiff command to remove an entry. */
+    String REMOVE_COMMAND = "remove";
+
+    /** A jardiff command to move an entry. */
+    String MOVE_COMMAND = "move";
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
new file mode 100644 (file)
index 0000000..b5a0a17
--- /dev/null
@@ -0,0 +1,292 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tools;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+
+import com.threerings.getdown.util.ProgressObserver;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Applies a jardiff patch to a jar file.
+ */
+public class JarDiffPatcher implements JarDiffCodes
+{
+    /**
+     * Patches the specified jar file using the supplied patch file and writing
+     * the new jar file to the supplied target.
+     *
+     * @param jarPath the path to the original jar file.
+     * @param diffPath the path to the jardiff patch file.
+     * @param target the output stream to which we will write the patched jar.
+     * @param observer an optional observer to be notified of patching progress.
+     *
+     * @throws IOException if any problem occurs during patching.
+     */
+    public void patchJar (String jarPath, String diffPath, File target, ProgressObserver observer)
+        throws IOException
+    {
+        File oldFile = new File(jarPath), diffFile = new File(diffPath);
+
+        try (JarFile oldJar = new JarFile(oldFile);
+             JarFile jarDiff = new JarFile(diffFile);
+             JarOutputStream jos = new JarOutputStream(new FileOutputStream(target))) {
+
+            Set<String> ignoreSet = new HashSet<>();
+            Map<String, String> renameMap = new HashMap<>();
+            determineNameMapping(jarDiff, ignoreSet, renameMap);
+
+            // get all keys in renameMap
+            String[] keys = renameMap.keySet().toArray(new String[renameMap.size()]);
+
+            // Files to implicit move
+            Set<String> oldjarNames  = new HashSet<>();
+            Enumeration<JarEntry> oldEntries = oldJar.entries();
+            if (oldEntries != null) {
+                while  (oldEntries.hasMoreElements()) {
+                    oldjarNames.add(oldEntries.nextElement().getName());
+                }
+            }
+
+            // size depends on the three parameters below, which is basically the
+            // counter for each loop that do the actual writes to the output file
+            // since oldjarNames.size() changes in the first two loop below, we
+            // need to adjust the size accordingly also when oldjarNames.size()
+            // changes
+            double size = oldjarNames.size() + keys.length + jarDiff.size();
+            double currentEntry = 0;
+
+            // Handle all remove commands
+            oldjarNames.removeAll(ignoreSet);
+            size -= ignoreSet.size();
+
+            // Add content from JARDiff
+            Enumeration<JarEntry> entries = jarDiff.entries();
+            if (entries != null) {
+                while (entries.hasMoreElements()) {
+                    JarEntry entry = entries.nextElement();
+                    if (!INDEX_NAME.equals(entry.getName())) {
+                        updateObserver(observer, currentEntry, size);
+                        currentEntry++;
+                        writeEntry(jos, entry, jarDiff);
+
+                        // Remove entry from oldjarNames since no implicit move is
+                        // needed
+                        boolean wasInOld = oldjarNames.remove(entry.getName());
+
+                        // Update progress counters. If it was in old, we do not
+                        // need an implicit move, so adjust total size.
+                        if (wasInOld) {
+                            size--;
+                        }
+
+                    } else {
+                        // no write is done, decrement size
+                        size--;
+                    }
+                }
+            }
+
+            // go through the renameMap and apply move for each entry
+            for (String newName : keys) {
+                // Apply move <oldName> <newName> command
+                String oldName = renameMap.get(newName);
+
+                // Get source JarEntry
+                JarEntry oldEntry = oldJar.getJarEntry(oldName);
+                if (oldEntry == null) {
+                    String moveCmd = MOVE_COMMAND + oldName + " " + newName;
+                    throw new IOException("error.badmove: " + moveCmd);
+                }
+
+                // Create dest JarEntry
+                JarEntry newEntry = new JarEntry(newName);
+                newEntry.setTime(oldEntry.getTime());
+                newEntry.setSize(oldEntry.getSize());
+                newEntry.setCompressedSize(oldEntry.getCompressedSize());
+                newEntry.setCrc(oldEntry.getCrc());
+                newEntry.setMethod(oldEntry.getMethod());
+                newEntry.setExtra(oldEntry.getExtra());
+                newEntry.setComment(oldEntry.getComment());
+
+                updateObserver(observer, currentEntry, size);
+                currentEntry++;
+
+                try (InputStream data = oldJar.getInputStream(oldEntry)) {
+                    writeEntry(jos, newEntry, data);
+                }
+
+                // Remove entry from oldjarNames since no implicit move is needed
+                boolean wasInOld = oldjarNames.remove(oldName);
+
+                // Update progress counters. If it was in old, we do not need an
+                // implicit move, so adjust total size.
+                if (wasInOld) {
+                    size--;
+                }
+            }
+
+            // implicit move
+            Iterator<String> iEntries = oldjarNames.iterator();
+            if (iEntries != null) {
+                while (iEntries.hasNext()) {
+                    String name = iEntries.next();
+                    JarEntry entry = oldJar.getJarEntry(name);
+                    if (entry == null) {
+                        // names originally retrieved from the JAR, so this should never happen
+                        throw new AssertionError("JAR entry not found: " + name);
+                    }
+                    updateObserver(observer, currentEntry, size);
+                    currentEntry++;
+                    writeEntry(jos, entry, oldJar);
+                }
+            }
+            updateObserver(observer, currentEntry, size);
+        }
+    }
+
+    protected void updateObserver (ProgressObserver observer, double currentSize, double size)
+    {
+        if (observer != null) {
+            observer.progress((int)(100*currentSize/size));
+        }
+    }
+
+    protected void determineNameMapping (
+        JarFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
+        throws IOException
+    {
+        InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME));
+        if (is == null) {
+            throw new IOException("error.noindex");
+        }
+
+        LineNumberReader indexReader =
+            new LineNumberReader(new InputStreamReader(is, UTF_8));
+        String line = indexReader.readLine();
+        if (line == null || !line.equals(VERSION_HEADER)) {
+            throw new IOException("jardiff.error.badheader: " + line);
+        }
+
+        while ((line = indexReader.readLine()) != null) {
+            if (line.startsWith(REMOVE_COMMAND)) {
+                List<String> sub = getSubpaths(
+                    line.substring(REMOVE_COMMAND.length()));
+
+                if (sub.size() != 1) {
+                    throw new IOException("error.badremove: " + line);
+                }
+                ignoreSet.add(sub.get(0));
+
+            } else if (line.startsWith(MOVE_COMMAND)) {
+                List<String> sub = getSubpaths(
+                    line.substring(MOVE_COMMAND.length()));
+                if (sub.size() != 2) {
+                    throw new IOException("error.badmove: " + line);
+                }
+
+                // target of move should be the key
+                if (renameMap.put(sub.get(1), sub.get(0)) != null) {
+                    // invalid move - should not move to same target twice
+                    throw new IOException("error.badmove: " + line);
+                }
+
+            } else if (line.length() > 0) {
+                throw new IOException("error.badcommand: " + line);
+            }
+        }
+    }
+
+    protected List<String> getSubpaths (String path)
+    {
+        int index = 0;
+        int length = path.length();
+        ArrayList<String> sub = new ArrayList<>();
+
+        while (index < length) {
+            while (index < length && Character.isWhitespace
+                   (path.charAt(index))) {
+                index++;
+            }
+            if (index < length) {
+                int start = index;
+                int last = start;
+                String subString = null;
+
+                while (index < length) {
+                    char aChar = path.charAt(index);
+                    if (aChar == '\\' && (index + 1) < length &&
+                        path.charAt(index + 1) == ' ') {
+
+                        if (subString == null) {
+                            subString = path.substring(last, index);
+                        } else {
+                            subString += path.substring(last, index);
+                        }
+                        last = ++index;
+                    } else if (Character.isWhitespace(aChar)) {
+                        break;
+                    }
+                    index++;
+                }
+                if (last != index) {
+                    if (subString == null) {
+                        subString = path.substring(last, index);
+                    } else {
+                        subString += path.substring(last, index);
+                    }
+                }
+                sub.add(subString);
+            }
+        }
+        return sub;
+    }
+
+    protected void writeEntry (JarOutputStream jos, JarEntry entry, JarFile file)
+        throws IOException
+    {
+        try (InputStream data = file.getInputStream(entry)) {
+            writeEntry(jos, entry, data);
+        }
+    }
+
+    protected void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data)
+        throws IOException
+    {
+        jos.putNextEntry(new JarEntry(entry.getName()));
+
+        // Read the entry
+        int size = data.read(newBytes);
+        while (size != -1) {
+            jos.write(newBytes, 0, size);
+            size = data.read(newBytes);
+        }
+    }
+
+    protected static final int DEFAULT_READ_SIZE = 2048;
+
+    protected static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
+    protected static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
new file mode 100644 (file)
index 0000000..4ead59b
--- /dev/null
@@ -0,0 +1,205 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tools;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+import com.threerings.getdown.util.FileUtil;
+import com.threerings.getdown.util.ProgressObserver;
+import com.threerings.getdown.util.StreamUtil;
+
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Applies a unified patch file to an application directory, providing
+ * percentage completion feedback along the way. <em>Note:</em> the
+ * patcher is not thread safe. Create a separate patcher instance for each
+ * patching action that is desired.
+ */
+public class Patcher
+{
+    /** A suffix appended to file names to indicate that a file should be newly created. */
+    public static final String CREATE = ".create";
+
+    /** A suffix appended to file names to indicate that a file should be patched. */
+    public static final String PATCH = ".patch";
+
+    /** A suffix appended to file names to indicate that a file should be deleted. */
+    public static final String DELETE = ".delete";
+
+    /**
+     * Applies the specified patch file to the application living in the
+     * specified application directory. The supplied observer, if
+     * non-null, will be notified of progress along the way.
+     *
+     * <p><em>Note:</em> this method runs on the calling thread, thus the
+     * caller may want to make use of a separate thread in conjunction
+     * with the patcher so that the user interface is not blocked for the
+     * duration of the patch.
+     */
+    public void patch (File appdir, File patch, ProgressObserver obs)
+        throws IOException
+    {
+        // save this information for later
+        _obs = obs;
+        _plength = patch.length();
+
+        try (JarFile file = new JarFile(patch)) {
+            Enumeration<JarEntry> entries = file.entries(); // old skool!
+            while (entries.hasMoreElements()) {
+                JarEntry entry = entries.nextElement();
+                String path = entry.getName();
+                long elength = entry.getCompressedSize();
+
+                // depending on the suffix, we do The Right Thing (tm)
+                if (path.endsWith(CREATE)) {
+                    path = strip(path, CREATE);
+                    log.info("Creating " + path + "...");
+                    createFile(file, entry, new File(appdir, path));
+
+                } else if (path.endsWith(PATCH)) {
+                    path = strip(path, PATCH);
+                    log.info("Patching " + path + "...");
+                    patchFile(file, entry, appdir, path);
+
+                } else if (path.endsWith(DELETE)) {
+                    path = strip(path, DELETE);
+                    log.info("Removing " + path + "...");
+                    File target = new File(appdir, path);
+                    if (!FileUtil.deleteHarder(target)) {
+                        log.warning("Failure deleting '" + target + "'.");
+                    }
+
+                } else {
+                    log.warning("Skipping bogus patch file entry: " + path);
+                }
+
+                // note that we've completed this entry
+                _complete += elength;
+            }
+        }
+    }
+
+    protected String strip (String path, String suffix)
+    {
+        return path.substring(0, path.length() - suffix.length());
+    }
+
+    protected void createFile (JarFile file, ZipEntry entry, File target)
+    {
+        // create our copy buffer if necessary
+        if (_buffer == null) {
+            _buffer = new byte[COPY_BUFFER_SIZE];
+        }
+
+        // make sure the file's parent directory exists
+        File pdir = target.getParentFile();
+        if (!pdir.exists() && !pdir.mkdirs()) {
+            log.warning("Failed to create parent for '" + target + "'.");
+        }
+
+        try (InputStream in = file.getInputStream(entry);
+             FileOutputStream fout = new FileOutputStream(target)) {
+
+            int total = 0, read;
+            while ((read = in.read(_buffer)) != -1) {
+                total += read;
+                fout.write(_buffer, 0, read);
+                updateProgress(total);
+            }
+
+        } catch (IOException ioe) {
+            log.warning("Error creating '" + target + "': " + ioe);
+        }
+    }
+
+    protected void patchFile (JarFile file, ZipEntry entry,
+                              File appdir, String path)
+    {
+        File target = new File(appdir, path);
+        File patch = new File(appdir, entry.getName());
+        File otarget = new File(appdir, path + ".old");
+        JarDiffPatcher patcher = null;
+
+        // make sure no stale old target is lying around to mess us up
+        FileUtil.deleteHarder(otarget);
+
+        // pipe the contents of the patch into a file
+        try (InputStream in = file.getInputStream(entry);
+             FileOutputStream fout = new FileOutputStream(patch)) {
+
+            StreamUtil.copy(in, fout);
+            StreamUtil.close(fout);
+
+            // move the current version of the jar to .old
+            if (!FileUtil.renameTo(target, otarget)) {
+                log.warning("Failed to .oldify '" + target + "'.");
+                return;
+            }
+
+            // we'll need this to pass progress along to our observer
+            final long elength = entry.getCompressedSize();
+            ProgressObserver obs = new ProgressObserver() {
+                public void progress (int percent) {
+                    updateProgress((int)(percent * elength / 100));
+                }
+            };
+
+            // now apply the patch to create the new target file
+            patcher = new JarDiffPatcher();
+            patcher.patchJar(otarget.getPath(), patch.getPath(), target, obs);
+
+        } catch (IOException ioe) {
+            if (patcher == null) {
+                log.warning("Failed to write patch file '" + patch + "': " + ioe);
+            } else {
+                log.warning("Error patching '" + target + "': " + ioe);
+            }
+
+        } finally {
+            // clean up our temporary files
+            FileUtil.deleteHarder(patch);
+            FileUtil.deleteHarder(otarget);
+        }
+    }
+
+    protected void updateProgress (int progress)
+    {
+        if (_obs != null) {
+            _obs.progress((int)(100 * (_complete + progress) / _plength));
+        }
+    }
+
+    public static void main (String[] args)
+    {
+        if (args.length != 2) {
+            System.err.println("Usage: Patcher appdir patch_file");
+            System.exit(-1);
+        }
+
+        Patcher patcher = new Patcher();
+        try {
+            patcher.patch(new File(args[0]), new File(args[1]), null);
+        } catch (IOException ioe) {
+            System.err.println("Error: " + ioe.getMessage());
+            System.exit(-1);
+        }
+    }
+
+    protected ProgressObserver _obs;
+    protected long _complete, _plength;
+    protected byte[] _buffer;
+
+    protected static final int COPY_BUFFER_SIZE = 4096;
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
new file mode 100644 (file)
index 0000000..2a5db79
--- /dev/null
@@ -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 {@code 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
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
new file mode 100644 (file)
index 0000000..047cead
--- /dev/null
@@ -0,0 +1,27 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+/**
+ * Utilities for handling ARGB colors.
+ */
+public class Color
+{
+    public final static int CLEAR = 0x00000000;
+    public final static int WHITE = 0xFFFFFFFF;
+    public final static int BLACK = 0xFF000000;
+
+    public static float brightness (int argb) {
+        // TODO: we're ignoring alpha here...
+        int red = (argb >> 16) & 0xFF;
+        int green = (argb >> 8) & 0xFF;
+        int blue = (argb >> 0) & 0xFF;
+        int max = Math.max(Math.max(red, green), blue);
+        return ((float) max) / 255.0f;
+    }
+
+    private Color () {}
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
new file mode 100644 (file)
index 0000000..4fc5e16
--- /dev/null
@@ -0,0 +1,378 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.net.URL;
+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 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
+{
+    /** Empty configuration. */
+    public static final Config EMPTY = new Config(new HashMap<String, Object>());
+
+    /** 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. FFCC99) and returns an {@code Integer} with that
+     * value. If the given value is null or not a valid hexadecimal number, this will return null.
+     */
+    public static Integer parseColor (String hexValue)
+    {
+        if (!StringUtil.isBlank(hexValue)) {
+            try {
+                // if no alpha channel is specified, use 255 (full alpha)
+                int alpha = hexValue.length() > 6 ? 0 : 0xFF000000;
+                return Integer.parseInt(hexValue, 16) | alpha;
+            } 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 int getColor (String name, int def)
+    {
+        String value = getString(name);
+        Integer 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) ? new String[0] : 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)) {
+            value = getString(name);
+        }
+        if (StringUtil.isBlank(value)) {
+            value = def;
+        }
+        if (!StringUtil.isBlank(value)) {
+            try {
+                HostWhitelist.verify(new URL(value));
+            } catch (MalformedURLException e) {
+                log.warning("Invalid URL.", "url", value, e);
+                value = null;
+            }
+        }
+        return 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;
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java
new file mode 100644 (file)
index 0000000..21b0569
--- /dev/null
@@ -0,0 +1,73 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLDecoder;
+
+import com.threerings.getdown.data.SysProps;
+
+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 proxy the proxy via which to perform HTTP connections.
+     * @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 (Proxy proxy, URL url, int connectTimeout, int readTimeout)
+        throws IOException
+    {
+        URLConnection conn = url.openConnection(proxy);
+
+        // 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 (
+        Proxy proxy, URL url, int connectTimeout, int readTimeout) throws IOException
+    {
+        return (HttpURLConnection)open(proxy, url, connectTimeout, readTimeout);
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
new file mode 100644 (file)
index 0000000..ec9d887
--- /dev/null
@@ -0,0 +1,239 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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.threerings.getdown.Log;
+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 {
+            copy(source, dest);
+        } catch (IOException ioe) {
+            log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
+            return false;
+        }
+
+        if (!deleteHarder(source)) {
+            log.warning("Failed to delete " + source + " after brute force copy to " + dest + ".");
+        }
+        return true;
+    }
+
+    /**
+     * "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;
+    }
+
+    /**
+     * Force deletes {@code file} and all of its children recursively using {@link #deleteHarder}.
+     * Note that some children may still be deleted even if {@code false} is returned. Also, since
+     * {@link #deleteHarder} is used, the {@code file} could be deleted once the jvm exits even if
+     * {@code false} is returned.
+     *
+     * @param file file to delete.
+     * @return true iff {@code file} was successfully deleted.
+     */
+    public static boolean deleteDirHarder (File file) {
+        if (file.isDirectory()) {
+            for (File child : file.listFiles()) {
+                deleteDirHarder(child);
+            }
+        }
+        return deleteHarder(file);
+    }
+
+    /**
+     * 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.
+     * @param cleanExistingDirs if true, all files in all directories contained in {@code jar} will
+     * be deleted prior to unpacking the jar.
+     */
+    public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
+        throws IOException
+    {
+        if (cleanExistingDirs) {
+            Enumeration<?> entries = jar.entries();
+            while (entries.hasMoreElements()) {
+                JarEntry entry = (JarEntry)entries.nextElement();
+                if (entry.isDirectory()) {
+                    File efile = new File(target, entry.getName());
+                    if (efile.exists()) {
+                        for (File f : efile.listFiles()) {
+                            if (!f.isDirectory())
+                            f.delete();
+                        }
+                    }
+                }
+            }
+        }
+
+        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(
+                    Log.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);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
new file mode 100644 (file)
index 0000000..c05992a
--- /dev/null
@@ -0,0 +1,54 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+import com.threerings.getdown.data.Build;
+
+/**
+ * Optional support for compiling a URL host whitelist into the Getdown JAR.
+ * Useful if you're on the paranoid end of the security spectrum.
+ *
+ * @see Build#hostWhitelist()
+ */
+public final class HostWhitelist
+{
+    /**
+     * Verifies that the specified URL should be accessible, per the built-in host whitelist.
+     * See {@link Build#hostWhitelist()} and {@link #verify(List,URL)}.
+     */
+    public static URL verify (URL url) throws MalformedURLException
+    {
+        return verify(Build.hostWhitelist(), url);
+    }
+
+    /**
+     * Verifies that the specified URL should be accessible, per the supplied host whitelist.
+     * If the URL should not be accessible, this method throws a {@link MalformedURLException}.
+     * If the URL should be accessible, this method simply returns the {@link URL} passed in.
+     */
+    public static URL verify (List<String> hosts, URL url) throws MalformedURLException
+    {
+        if (url == null || hosts.isEmpty()) {
+            // either there is no URL to validate or no whitelist was configured
+            return url;
+        }
+
+        String urlHost = url.getHost();
+        for (String host : hosts) {
+            String regex = host.replace(".", "\\.").replace("*", ".*");
+            if (urlHost.matches(regex)) {
+                return url;
+            }
+        }
+
+        throw new MalformedURLException(
+            "The host for the specified URL (" + url + ") is not in the host whitelist: " + hosts);
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
new file mode 100644 (file)
index 0000000..829e38f
--- /dev/null
@@ -0,0 +1,251 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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 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);
+        if (vmpath == null && isMacOS()) {
+                       vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR + "/Contents/Home").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 (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);
+    }
+
+    /**
+     * Returns true if we're running in a JVM that identifies its operating system as Windows.
+     */
+    public static final boolean isWindows () { return _isWindows; }
+
+    /**
+     * Returns true if we're running in a JVM that identifies its operating system as MacOS.
+     */
+    public static final boolean isMacOS () { return _isMacOS; }
+
+    /**
+     * Returns true if we're running in a JVM that identifies its operating system as Linux.
+     */
+    public static final boolean isLinux () { return _isLinux; }
+
+    /**
+     * 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;
+    }
+
+    /** Flag indicating that we're on Windows; initialized when this class is first loaded. */
+    protected static boolean _isWindows;
+    /** Flag indicating that we're on MacOS; initialized when this class is first loaded. */
+    protected static boolean _isMacOS;
+    /** Flag indicating that we're on Linux; initialized when this class is first loaded. */
+    protected static boolean _isLinux;
+
+    static {
+        try {
+            String osname = System.getProperty("os.name");
+            osname = (osname == null) ? "" : osname;
+            _isWindows = (osname.indexOf("Windows") != -1);
+            _isMacOS = (osname.indexOf("Mac OS") != -1 ||
+                        osname.indexOf("MacOS") != -1);
+            _isLinux = (osname.indexOf("Linux") != -1);
+        } catch (Exception e) {
+            // can't grab system properties; we'll just pretend we're not on any of these OSes
+        }
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
new file mode 100644 (file)
index 0000000..28dbdcf
--- /dev/null
@@ -0,0 +1,144 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+public class MessageUtil {
+
+    /**
+     * Returns whether or not the provided string is tainted. See {@link #taint}. Null strings
+     * are considered untainted.
+     */
+    public static boolean isTainted (String text)
+    {
+        return text != null && text.startsWith(TAINT_CHAR);
+    }
+
+    /**
+     * Call this to "taint" any string that has been entered by an entity outside the application
+     * so that the translation code knows not to attempt to translate this string when doing
+     * recursive translations.
+     */
+    public static String taint (Object text)
+    {
+        return TAINT_CHAR + text;
+    }
+
+    /**
+     * Removes the tainting character added to a string by {@link #taint}. If the provided string
+     * is not tainted, this silently returns the originally provided string.
+     */
+    public static String untaint (String text)
+    {
+        return isTainted(text) ? text.substring(TAINT_CHAR.length()) : text;
+    }
+
+    /**
+     * Composes a message key with an array of arguments. The message can subsequently be
+     * decomposed and translated without prior knowledge of how many arguments were provided.
+     */
+    public static String compose (String key, Object... args)
+    {
+        StringBuilder buf = new StringBuilder();
+        buf.append(key);
+        buf.append('|');
+        for (int i = 0; i < args.length; i++) {
+            if (i > 0) {
+                buf.append('|');
+            }
+            // escape the string while adding to the buffer
+            String arg = (args[i] == null) ? "" : String.valueOf(args[i]);
+            int alength = arg.length();
+            for (int p = 0; p < alength; p++) {
+                char ch = arg.charAt(p);
+                if (ch == '|') {
+                    buf.append("\\!");
+                } else if (ch == '\\') {
+                    buf.append("\\\\");
+                } else {
+                    buf.append(ch);
+                }
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Compose a message with String args. This is just a convenience so callers do not have to
+     * cast their String[] to an Object[].
+     */
+    public static String compose (String key, String... args)
+    {
+        return compose(key, (Object[]) args);
+    }
+
+    /**
+     * A convenience method for calling {@link #compose(String,Object[])} with an array of
+     * arguments that will be automatically tainted (see {@link #taint}).
+     */
+    public static String tcompose (String key, Object... args)
+    {
+        int acount = args.length;
+        String[] targs = new String[acount];
+        for (int ii = 0; ii < acount; ii++) {
+            targs[ii] = taint(args[ii]);
+        }
+        return compose(key, (Object[]) targs);
+    }
+
+    /**
+     * A convenience method for calling {@link #compose(String,String[])} with an array of argument
+     * that will be automatically tainted.
+     */
+    public static String tcompose (String key, String... args)
+    {
+        for (int ii = 0, nn = args.length; ii < nn; ii++) {
+            args[ii] = taint(args[ii]);
+        }
+        return compose(key, args);
+    }
+
+    /**
+     * Used to escape single quotes so that they are not interpreted by <code>MessageFormat</code>.
+     * As we assume all single quotes are to be escaped, we cannot use the characters
+     * <code>{</code> and <code>}</code> in our translation strings, but this is a small price to
+     * pay to have to differentiate between messages that will and won't eventually be parsed by a
+     * <code>MessageFormat</code> instance.
+     */
+    public static String escape (String message)
+    {
+        return message.replace("'", "''");
+    }
+
+    /**
+     * Unescapes characters that are escaped in a call to compose.
+     */
+    public static String unescape (String value)
+    {
+        int bsidx = value.indexOf('\\');
+        if (bsidx == -1) {
+            return value;
+        }
+
+        StringBuilder buf = new StringBuilder();
+        int vlength = value.length();
+        for (int ii = 0; ii < vlength; ii++) {
+            char ch = value.charAt(ii);
+            if (ch != '\\' || ii == vlength-1) {
+                buf.append(ch);
+            } else {
+                // look at the next character
+                ch = value.charAt(++ii);
+                buf.append((ch == '!') ? '|' : ch);
+            }
+        }
+
+        return buf.toString();
+    }
+
+    /** Text prefixed by this character will be considered tainted when doing recursive
+     * translations and won't be translated. */
+    protected static final String TAINT_CHAR = "~";
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressAggregator.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressAggregator.java
new file mode 100644 (file)
index 0000000..d74b011
--- /dev/null
@@ -0,0 +1,50 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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;
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
new file mode 100644 (file)
index 0000000..ad4c560
--- /dev/null
@@ -0,0 +1,18 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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);
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Rectangle.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Rectangle.java
new file mode 100644 (file)
index 0000000..3671d7d
--- /dev/null
@@ -0,0 +1,40 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+/**
+ * An immutable rectangle.
+ */
+public class Rectangle
+{
+    public final int x;
+    public final int y;
+    public final int width;
+    public final int height;
+
+    public Rectangle (int x, int y, int width, int height)
+    {
+        this.x = x;
+        this.y = y;
+        this.width = width;
+        this.height = height;
+    }
+
+    public Rectangle union (Rectangle other) {
+        int x1 = Math.min(x, other.x);
+        int x2 = Math.max(x + width, other.x + other.width);
+        int y1 = Math.min(y, other.y);
+        int y2 = Math.max(y + height, other.y + other.height);
+        return new Rectangle(x1, y1, x2 - x1, y2 - y1);
+    }
+
+    /** {@inheritDoc} */
+    public String toString ()
+    {
+        return getClass().getName() + "[x=" + x + ", y=" + y +
+            ", width=" + width + ", height=" + height + "]";
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
new file mode 100644 (file)
index 0000000..373cfff
--- /dev/null
@@ -0,0 +1,96 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+
+import static com.threerings.getdown.Log.log;
+
+public class StreamUtil {
+    /**
+     * Convenient close for a stream. Use in a finally clause and love life.
+     */
+    public static void close (InputStream in)
+    {
+        if (in != null) {
+            try {
+                in.close();
+            } catch (IOException ioe) {
+                log.warning("Error closing input stream", "stream", in, "cause", ioe);
+            }
+        }
+    }
+
+    /**
+     * Convenient close for a stream. Use in a finally clause and love life.
+     */
+    public static void close (OutputStream out)
+    {
+        if (out != null) {
+            try {
+                out.close();
+            } catch (IOException ioe) {
+                log.warning("Error closing output stream", "stream", out, "cause", ioe);
+            }
+        }
+    }
+
+    /**
+     * Convenient close for a Reader. Use in a finally clause and love life.
+     */
+    public static void close (Reader in)
+    {
+        if (in != null) {
+            try {
+                in.close();
+            } catch (IOException ioe) {
+                log.warning("Error closing reader", "reader", in, "cause", ioe);
+            }
+        }
+    }
+
+    /**
+     * Convenient close for a Writer. Use in a finally clause and love life.
+     */
+    public static void close (Writer out)
+    {
+        if (out != null) {
+            try {
+                out.close();
+            } catch (IOException ioe) {
+                log.warning("Error closing writer", "writer", out, "cause", ioe);
+            }
+        }
+    }
+
+    /**
+     * Copies the contents of the supplied input stream to the supplied output stream.
+     */
+    public static <T extends OutputStream> T copy (InputStream in, T out)
+        throws IOException
+    {
+        byte[] buffer = new byte[4096];
+        for (int read = 0; (read = in.read(buffer)) > 0; ) {
+            out.write(buffer, 0, read);
+        }
+        return out;
+    }
+
+    /**
+     * Reads the contents of the supplied stream into a byte array.
+     */
+    public static byte[] toByteArray (InputStream stream)
+        throws IOException
+    {
+        return copy(stream, new ByteArrayOutputStream()).toByteArray();
+    }
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
new file mode 100644 (file)
index 0000000..03d3c9c
--- /dev/null
@@ -0,0 +1,206 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import java.util.StringTokenizer;
+
+public class StringUtil {
+
+    /**
+     * @return true if the specified string could be a valid URL (contains no illegal characters)
+     */
+    public static boolean couldBeValidUrl (String url)
+    {
+        return url.matches("[A-Za-z0-9\\-\\._~:/\\?#\\[\\]@!$&'\\(\\)\\*\\+,;=%]+");
+    }
+
+    /**
+     * @return true if the string is null or consists only of whitespace, false otherwise.
+     */
+    public static boolean isBlank (String value)
+    {
+        for (int ii = 0, ll = (value == null) ? 0 : value.length(); ii < ll; ii++) {
+            if (!Character.isWhitespace(value.charAt(ii))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Parses an array of integers from it's string representation. The array should be represented
+     * as a bare list of numbers separated by commas, for example:
+     *
+     * <pre>25, 17, 21, 99</pre>
+     *
+     * Any inability to parse the int array will result in the function returning null.
+     */
+    public static int[] parseIntArray (String source)
+    {
+        StringTokenizer tok = new StringTokenizer(source, ",");
+        int[] vals = new int[tok.countTokens()];
+        for (int i = 0; tok.hasMoreTokens(); i++) {
+            try {
+                // trim the whitespace from the token
+                vals[i] = Integer.parseInt(tok.nextToken().trim());
+            } catch (NumberFormatException nfe) {
+                return null;
+            }
+        }
+        return vals;
+    }
+
+    /**
+     * Parses an array of strings from a single string. The array should be represented as a bare
+     * list of strings separated by commas, for example:
+     *
+     * <pre>mary, had, a, little, lamb, and, an, escaped, comma,,</pre>
+     *
+     * If a comma is desired in one of the strings, it should be escaped by putting two commas in a
+     * row. Any inability to parse the string array will result in the function returning null.
+     */
+    public static String[] parseStringArray (String source)
+    {
+        return parseStringArray(source, false);
+    }
+
+    /**
+     * Like {@link #parseStringArray(String)} but can be instructed to invoke {@link String#intern}
+     * on the strings being parsed into the array.
+     */
+    public static String[] parseStringArray (String source, boolean intern)
+    {
+        int tcount = 0, tpos = -1, tstart = 0;
+
+        // empty strings result in zero length arrays
+        if (source.length() == 0) {
+            return new String[0];
+        }
+
+        // sort out escaped commas
+        source = source.replace(",,", "%COMMA%");
+
+        // count up the number of tokens
+        while ((tpos = source.indexOf(",", tpos+1)) != -1) {
+            tcount++;
+        }
+
+        String[] tokens = new String[tcount+1];
+        tpos = -1; tcount = 0;
+
+        // do the split
+        while ((tpos = source.indexOf(",", tpos+1)) != -1) {
+            tokens[tcount] = source.substring(tstart, tpos);
+            tokens[tcount] = tokens[tcount].trim().replace("%COMMA%", ",");
+            if (intern) {
+                tokens[tcount] = tokens[tcount].intern();
+            }
+            tstart = tpos+1;
+            tcount++;
+        }
+
+        // grab the last token
+        tokens[tcount] = source.substring(tstart);
+        tokens[tcount] = tokens[tcount].trim().replace("%COMMA%", ",");
+
+        return tokens;
+    }
+
+    /**
+     * @return the supplied string if it is non-null, "" if it is null.
+     */
+    public static String deNull (String value)
+    {
+        return (value == null) ? "" : value;
+    }
+
+    /**
+     * Generates a string from the supplied bytes that is the HEX encoded representation of those
+     * bytes.  Returns the empty string for a <code>null</code> or empty byte array.
+     *
+     * @param bytes the bytes for which we want a string representation.
+     * @param count the number of bytes to stop at (which will be coerced into being {@code <=} the
+     * length of the array).
+     */
+    public static String hexlate (byte[] bytes, int count)
+    {
+        if (bytes == null) {
+            return "";
+        }
+
+        count = Math.min(count, bytes.length);
+        char[] chars = new char[count*2];
+
+        for (int i = 0; i < count; i++) {
+            int val = bytes[i];
+            if (val < 0) {
+                val += 256;
+            }
+            chars[2*i] = XLATE.charAt(val/16);
+            chars[2*i+1] = XLATE.charAt(val%16);
+        }
+
+        return new String(chars);
+    }
+
+    /**
+     * Generates a string from the supplied bytes that is the HEX encoded representation of those
+     * bytes.
+     */
+    public static String hexlate (byte[] bytes)
+    {
+        return (bytes == null) ? "" : hexlate(bytes, bytes.length);
+    }
+
+    /**
+     * Joins an array of strings (or objects which will be converted to strings) into a single
+     * string separated by commas.
+     */
+    public static String join (Object[] values)
+    {
+        return join(values, false);
+    }
+
+    /**
+     * Joins an array of strings into a single string, separated by commas, and optionally escaping
+     * commas that occur in the individual string values such that a subsequent call to {@link
+     * #parseStringArray} would recreate the string array properly. Any elements in the values
+     * array that are null will be treated as an empty string.
+     */
+    public static String join (Object[] values, boolean escape)
+    {
+        return join(values, ", ", escape);
+    }
+
+    /**
+     * Joins the supplied array of strings into a single string separated by the supplied
+     * separator.
+     */
+    public static String join (Object[] values, String separator)
+    {
+        return join(values, separator, false);
+    }
+
+    /**
+     * Helper function for the various <code>join</code> methods.
+     */
+    protected static String join (Object[] values, String separator, boolean escape)
+    {
+        StringBuilder buf = new StringBuilder();
+        int vlength = values.length;
+        for (int i = 0; i < vlength; i++) {
+            if (i > 0) {
+                buf.append(separator);
+            }
+            String value = (values[i] == null) ? "" : values[i].toString();
+            buf.append((escape) ? value.replace(",", ",,") : value);
+        }
+        return buf.toString();
+    }
+
+    /** Used by {@link #hexlate} and {@link #unhexlate}. */
+    protected static final String XLATE = "0123456789abcdef";
+}
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
new file mode 100644 (file)
index 0000000..49e4e6e
--- /dev/null
@@ -0,0 +1,114 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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.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;
+    }
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
new file mode 100644 (file)
index 0000000..d5a3937
--- /dev/null
@@ -0,0 +1,71 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.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;
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
new file mode 100644 (file)
index 0000000..860c72a
--- /dev/null
@@ -0,0 +1,72 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.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", "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);
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/ClassPathTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/ClassPathTest.java
new file mode 100644 (file)
index 0000000..5344f3b
--- /dev/null
@@ -0,0 +1,54 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.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;
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
new file mode 100644 (file)
index 0000000..6178651
--- /dev/null
@@ -0,0 +1,142 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.io.File;
+
+import org.junit.*;
+import static org.junit.Assert.*;
+
+public class EnvConfigTest {
+
+    static String CWD = System.getProperty("user.dir");
+    static String TESTID = "testid";
+    static String TESTBASE = "https://test.com/test";
+
+    private void debugNotes(List<EnvConfig.Note> notes) {
+        for (EnvConfig.Note note : notes) {
+            System.out.println(note.message);
+        }
+    }
+
+    private void checkNoNotes (List<EnvConfig.Note> notes) {
+        StringBuilder msg = new StringBuilder();
+        for (EnvConfig.Note note : notes) {
+            if (note.level != EnvConfig.Note.Level.INFO) {
+                msg.append("\n").append(note.message);
+            }
+        }
+        if (msg.length() > 0) {
+            fail("Unexpected notes:" + msg.toString());
+        }
+    }
+    private void checkDir (EnvConfig cfg) {
+        assertTrue(cfg != null);
+        assertEquals(new File(CWD), cfg.appDir);
+    }
+    private void checkNoAppId (EnvConfig cfg) {
+        assertNull(cfg.appId);
+    }
+    private void checkAppId (EnvConfig cfg, String appId) {
+        assertEquals(appId, cfg.appId);
+    }
+    private void checkAppBase (EnvConfig cfg, String appBase) {
+        assertEquals(appBase, cfg.appBase);
+    }
+    private void checkNoAppBase (EnvConfig cfg) {
+        assertNull(cfg.appBase);
+    }
+    private void checkNoAppArgs (EnvConfig cfg) {
+        assertTrue(cfg.appArgs.isEmpty());
+    }
+    private void checkAppArgs (EnvConfig cfg, String... args) {
+        assertEquals(Arrays.asList(args), cfg.appArgs);
+    }
+
+    @Test public void testArgvDir () {
+        List<EnvConfig.Note> notes = new ArrayList<>();
+        String[] args = { CWD };
+        EnvConfig cfg = EnvConfig.create(args, notes);
+        // debugNotes(notes);
+        checkNoNotes(notes);
+        checkDir(cfg);
+        checkNoAppId(cfg);
+        checkNoAppBase(cfg);
+        checkNoAppArgs(cfg);
+    }
+
+    @Test public void testArgvDirId () {
+        List<EnvConfig.Note> notes = new ArrayList<>();
+        String[] args = { CWD, TESTID };
+        EnvConfig cfg = EnvConfig.create(args, notes);
+        // debugNotes(notes);
+        checkNoNotes(notes);
+        checkDir(cfg);
+        checkAppId(cfg, TESTID);
+        checkNoAppBase(cfg);
+        checkNoAppArgs(cfg);
+    }
+
+    @Test public void testArgvDirArgs () {
+        List<EnvConfig.Note> notes = new ArrayList<>();
+        String[] args = { CWD, "", "one", "two" };
+        EnvConfig cfg = EnvConfig.create(args, notes);
+        // debugNotes(notes);
+        checkNoNotes(notes);
+        checkDir(cfg);
+        checkNoAppId(cfg);
+        checkNoAppBase(cfg);
+        checkAppArgs(cfg, "one", "two");
+    }
+
+    @Test public void testArgvDirIdArgs () {
+        List<EnvConfig.Note> notes = new ArrayList<>();
+        String[] args = { CWD, TESTID, "one", "two" };
+        EnvConfig cfg = EnvConfig.create(args, notes);
+        // debugNotes(notes);
+        checkNoNotes(notes);
+        checkDir(cfg);
+        checkAppId(cfg, TESTID);
+        checkNoAppBase(cfg);
+        checkAppArgs(cfg, "one", "two");
+    }
+
+    private EnvConfig sysPropsConfig (List<EnvConfig.Note> notes, String... keyVals) {
+        for (int ii = 0; ii < keyVals.length; ii += 2) {
+            System.setProperty(keyVals[ii], keyVals[ii+1]);
+        }
+        EnvConfig cfg = EnvConfig.create(new String[0], notes);
+        for (int ii = 0; ii < keyVals.length; ii += 2) {
+            System.clearProperty(keyVals[ii]);
+        }
+        return cfg;
+    }
+
+    @Test public void testSysPropsDir () {
+        List<EnvConfig.Note> notes = new ArrayList<>();
+        EnvConfig cfg = sysPropsConfig(notes, "appdir", CWD);
+        // debugNotes(notes);
+        checkNoNotes(notes);
+        checkDir(cfg);
+        checkNoAppId(cfg);
+        checkNoAppBase(cfg);
+        checkNoAppArgs(cfg);
+    }
+
+    @Test public void testSysPropsDirIdBase () {
+        List<EnvConfig.Note> notes = new ArrayList<>();
+        EnvConfig cfg = sysPropsConfig(notes, "appdir", CWD, "appid", TESTID, "appbase", TESTBASE);
+        // debugNotes(notes);
+        checkNoNotes(notes);
+        checkDir(cfg);
+        checkAppId(cfg, TESTID);
+        checkAppBase(cfg, TESTBASE);
+        checkNoAppArgs(cfg);
+    }
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/PathBuilderTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/PathBuilderTest.java
new file mode 100644 (file)
index 0000000..7f35094
--- /dev/null
@@ -0,0 +1,70 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+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;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PathBuilderTest
+{
+    @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 = PathBuilder.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);
+
+        Path firstCachedJarFile = _appdir.getRoot().toPath().
+            resolve(PathBuilder.CODE_CACHE_DIR).resolve("fi").resolve("first.jar");
+
+        Path secondCachedJarFile = _appdir.getRoot().toPath().
+            resolve(PathBuilder.CODE_CACHE_DIR).resolve("se").resolve("second.jar");
+
+        String expectedClassPath = firstCachedJarFile.toAbsolutePath() + File.pathSeparator +
+            secondCachedJarFile.toAbsolutePath();
+
+        ClassPath classPath = PathBuilder.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();
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/SysPropsTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/SysPropsTest.java
new file mode 100644 (file)
index 0000000..042a13f
--- /dev/null
@@ -0,0 +1,63 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import org.junit.*;
+import static org.junit.Assert.*;
+
+public class SysPropsTest {
+
+    @After public void clearProps () {
+        System.clearProperty("delay");
+        System.clearProperty("appbase_domain");
+        System.clearProperty("appbase_override");
+    }
+
+    private static final String[] APPBASES = {
+        "http://foobar.com/myapp",
+        "https://foobar.com/myapp",
+        "http://foobar.com:8080/myapp",
+        "https://foobar.com:8080/myapp"
+    };
+
+    @Test public void testStartDelay () {
+
+        assertEquals(0, SysProps.startDelay());
+
+        System.setProperty("delay", "x");
+        assertEquals(0, SysProps.startDelay());
+
+        System.setProperty("delay", "-7");
+        assertEquals(0, SysProps.startDelay());
+
+        System.setProperty("delay", "7");
+        assertEquals(7, SysProps.startDelay());
+
+        System.setProperty("delay", "1440");
+        assertEquals(1440, SysProps.startDelay());
+
+        System.setProperty("delay", "1441");
+        assertEquals(1440, SysProps.startDelay());
+    }
+
+    @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));
+        }
+    }
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/ColorTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/ColorTest.java
new file mode 100644 (file)
index 0000000..7aa48ee
--- /dev/null
@@ -0,0 +1,23 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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;
+
+/**
+ * Tests {@link Color}.
+ */
+public class ColorTest
+{
+    @Test
+    public void testBrightness() {
+        assertEquals(0, Color.brightness(0xFF000000), 0.0000001);
+        assertEquals(1, Color.brightness(0xFFFFFFFF), 0.0000001);
+        assertEquals(0.0117647, Color.brightness(0xFF010203), 0.0000001);
+        assertEquals(1, Color.brightness(0xFF00FFC8), 0.0000001);
+    }
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/ConfigTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/ConfigTest.java
new file mode 100644 (file)
index 0000000..cdc5a91
--- /dev/null
@@ -0,0 +1,171 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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 java.util.Random;
+
+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 _rando.nextBoolean() ? " " : "";
+    }
+
+    protected static Random _rando = new Random();
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/FileUtilTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/FileUtilTest.java
new file mode 100644 (file)
index 0000000..cfd53a2
--- /dev/null
@@ -0,0 +1,60 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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();
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java
new file mode 100644 (file)
index 0000000..703afef
--- /dev/null
@@ -0,0 +1,159 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+
+/**
+ * Tests {@link HostWhitelist}.
+ */
+public class HostWhitelistTest
+{
+    @Test
+    public void testVerify () throws MalformedURLException
+    {
+        checkCanVerify("foo.com", "http://foo.com", true);
+        checkCanVerify("foo.com", "http://foo.com/", true);
+        checkCanVerify("foo.com", "http://foo.com/x/y/z", true);
+        checkCanVerify("foo.com", "http://www.foo.com", false);
+        checkCanVerify("foo.com", "http://www.foo.com/", false);
+        checkCanVerify("foo.com", "http://www.foo.com/x/y/z", false);
+        checkCanVerify("foo.com", "http://a.b.foo.com", false);
+        checkCanVerify("foo.com", "http://a.b.foo.com/", false);
+        checkCanVerify("foo.com", "http://a.b.foo.com/x/y/z", false);
+        checkCanVerify("foo.com", "http://oo.com", false);
+        checkCanVerify("foo.com", "http://f.oo.com", false);
+        checkCanVerify("foo.com", "http://a.f.oo.com", false);
+
+        checkCanVerify("*.foo.com", "http://foo.com", false);
+        checkCanVerify("*.foo.com", "http://foo.com/", false);
+        checkCanVerify("*.foo.com", "http://foo.com/x/y/z", false);
+        checkCanVerify("*.foo.com", "http://www.foo.com", true);
+        checkCanVerify("*.foo.com", "http://www.foo.com/", true);
+        checkCanVerify("*.foo.com", "http://www.foo.com/x/y/z", true);
+        checkCanVerify("*.foo.com", "http://a.b.foo.com", true);
+        checkCanVerify("*.foo.com", "http://a.b.foo.com/", true);
+        checkCanVerify("*.foo.com", "http://a.b.foo.com/x/y/z", true);
+        checkCanVerify("*.foo.com", "http://oo.com", false);
+        checkCanVerify("*.foo.com", "http://f.oo.com", false);
+        checkCanVerify("*.foo.com", "http://a.f.oo.com", false);
+
+        checkCanVerify("*.com", "http://foo.com", true);
+        checkCanVerify("*.com", "http://foo.com/", true);
+        checkCanVerify("*.com", "http://foo.com/x/y/z", true);
+        checkCanVerify("*.com", "http://www.foo.com", true);
+        checkCanVerify("*.com", "http://www.foo.com/", true);
+        checkCanVerify("*.com", "http://www.foo.com/x/y/z", true);
+        checkCanVerify("*.com", "http://a.b.foo.com", true);
+        checkCanVerify("*.com", "http://a.b.foo.com/", true);
+        checkCanVerify("*.com", "http://a.b.foo.com/x/y/z", true);
+        checkCanVerify("*.com", "http://oo.com", true);
+        checkCanVerify("*.com", "http://f.oo.com", true);
+        checkCanVerify("*.com", "http://a.f.oo.com", true);
+
+        checkCanVerify("*.net", "http://foo.com", false);
+        checkCanVerify("*.net", "http://foo.com/", false);
+        checkCanVerify("*.net", "http://foo.com/x/y/z", false);
+        checkCanVerify("*.net", "http://www.foo.com", false);
+        checkCanVerify("*.net", "http://www.foo.com/", false);
+        checkCanVerify("*.net", "http://www.foo.com/x/y/z", false);
+        checkCanVerify("*.net", "http://a.b.foo.com", false);
+        checkCanVerify("*.net", "http://a.b.foo.com/", false);
+        checkCanVerify("*.net", "http://a.b.foo.com/x/y/z", false);
+        checkCanVerify("*.net", "http://oo.com", false);
+        checkCanVerify("*.net", "http://f.oo.com", false);
+        checkCanVerify("*.net", "http://a.f.oo.com", false);
+
+        checkCanVerify("www.*.com", "http://foo.com", false);
+        checkCanVerify("www.*.com", "http://foo.com/", false);
+        checkCanVerify("www.*.com", "http://foo.com/x/y/z", false);
+        checkCanVerify("www.*.com", "http://www.foo.com", true);
+        checkCanVerify("www.*.com", "http://www.foo.com/", true);
+        checkCanVerify("www.*.com", "http://www.foo.com/x/y/z", true);
+        checkCanVerify("www.*.com", "http://a.b.foo.com", false);
+        checkCanVerify("www.*.com", "http://a.b.foo.com/", false);
+        checkCanVerify("www.*.com", "http://a.b.foo.com/x/y/z", false);
+        checkCanVerify("www.*.com", "http://oo.com", false);
+        checkCanVerify("www.*.com", "http://f.oo.com", false);
+        checkCanVerify("www.*.com", "http://a.f.oo.com", false);
+        checkCanVerify("www.*.com", "http://www.a.f.oo.com", true);
+
+        checkCanVerify("foo.*", "http://foo.com", true);
+        checkCanVerify("foo.*", "http://foo.com/", true);
+        checkCanVerify("foo.*", "http://foo.com/x/y/z", true);
+        checkCanVerify("foo.*", "http://www.foo.com", false);
+        checkCanVerify("foo.*", "http://www.foo.com/", false);
+        checkCanVerify("foo.*", "http://www.foo.com/x/y/z", false);
+        checkCanVerify("foo.*", "http://a.b.foo.com", false);
+        checkCanVerify("foo.*", "http://a.b.foo.com/", false);
+        checkCanVerify("foo.*", "http://a.b.foo.com/x/y/z", false);
+        checkCanVerify("foo.*", "http://oo.com", false);
+        checkCanVerify("foo.*", "http://f.oo.com", false);
+        checkCanVerify("foo.*", "http://a.f.oo.com", false);
+
+        checkCanVerify("*.foo.*", "http://foo.com", false);
+        checkCanVerify("*.foo.*", "http://foo.com/", false);
+        checkCanVerify("*.foo.*", "http://foo.com/x/y/z", false);
+        checkCanVerify("*.foo.*", "http://www.foo.com", true);
+        checkCanVerify("*.foo.*", "http://www.foo.com/", true);
+        checkCanVerify("*.foo.*", "http://www.foo.com/x/y/z", true);
+        checkCanVerify("*.foo.*", "http://a.b.foo.com", true);
+        checkCanVerify("*.foo.*", "http://a.b.foo.com/", true);
+        checkCanVerify("*.foo.*", "http://a.b.foo.com/x/y/z", true);
+        checkCanVerify("*.foo.*", "http://oo.com", false);
+        checkCanVerify("*.foo.*", "http://f.oo.com", false);
+        checkCanVerify("*.foo.*", "http://a.f.oo.com", false);
+
+        checkCanVerify("127.0.0.1", "http://127.0.0.1", true);
+        checkCanVerify("127.0.0.1", "http://127.0.0.1/", true);
+        checkCanVerify("127.0.0.1", "http://127.0.0.1/x/y/z", true);
+        checkCanVerify("*.0.0.1", "http://127.0.0.1/abc", true);
+        checkCanVerify("127.*.0.1", "http://127.0.0.1/abc", true);
+        checkCanVerify("127.0.*.1", "http://127.0.0.1/abc", true);
+        checkCanVerify("127.0.0.*", "http://127.0.0.1/abc", true);
+        checkCanVerify("127.*.1", "http://127.0.0.1/abc", true);
+        checkCanVerify("*.0.1", "http://127.0.0.1/abc", true);
+        checkCanVerify("127.0.*", "http://127.0.0.1/abc", true);
+        checkCanVerify("*", "http://127.0.0.1/abc", true);
+        checkCanVerify("127.0.0.2", "http://127.0.0.1", false);
+        checkCanVerify("127.0.2.1", "http://127.0.0.1", false);
+        checkCanVerify("127.2.0.1", "http://127.0.0.1", false);
+        checkCanVerify("222.0.0.1", "http://127.0.0.1", false);
+
+        checkCanVerify("", "http://foo.com", true);
+        checkCanVerify("", "http://aaa.bbb.net/xyz", true);
+        checkCanVerify("", "https://127.0.0.1/abc", true);
+
+        checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://aaa.bbb.com/m", true);
+        checkCanVerify("aaa.bbb.com, xxx.yyy.com,*.jjj.net", "http://xxx.yyy.com/n", true);
+        checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://www.jjj.net/o", true);
+    }
+
+    private static void checkCanVerify (String whitelist, String url, boolean expectedToPass)
+        throws MalformedURLException
+    {
+        List<String> w = Arrays.asList(StringUtil.parseStringArray(whitelist));
+        URL u = new URL(url);
+        boolean passed;
+
+        try {
+            HostWhitelist.verify(w, u);
+            passed = true;
+        } catch (MalformedURLException e) {
+            passed = false;
+        }
+
+        assertEquals("with whitelist '" + whitelist + "' and URL '" + url + "'",
+            expectedToPass, passed);
+    }
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/StringUtilTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/StringUtilTest.java
new file mode 100644 (file)
index 0000000..f70bab9
--- /dev/null
@@ -0,0 +1,28 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import org.junit.Test;
+
+import static com.threerings.getdown.util.StringUtil.couldBeValidUrl;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests {@link StringUtil}.
+ */
+public class StringUtilTest
+{
+    @Test public void testCouldBeValidUrl ()
+    {
+        assertTrue(couldBeValidUrl("http://www.foo.com/"));
+        assertTrue(couldBeValidUrl("http://www.foo.com/A-B-C/1_2_3/~bar/q.jsp?x=u+i&y=2;3;4#baz%20baz"));
+        assertTrue(couldBeValidUrl("https://user:secret@www.foo.com/"));
+
+        assertFalse(couldBeValidUrl("http://www.foo.com & echo hello"));
+        assertFalse(couldBeValidUrl("http://www.foo.com\""));
+    }
+}
diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/VersionUtilTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/util/VersionUtilTest.java
new file mode 100644 (file)
index 0000000..165fbe3
--- /dev/null
@@ -0,0 +1,53 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 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);
+    }
+}
diff --git a/getdown/src/getdown/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/getdown/src/getdown/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644 (file)
index 0000000..1f0955d
--- /dev/null
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/getdown/src/getdown/core/target/antrun/build-main.xml b/getdown/src/getdown/core/target/antrun/build-main.xml
new file mode 100644 (file)
index 0000000..6fe3710
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<project name="maven-antrun-" default="main"  >
+<target name="main">
+  <tstamp>
+    <format property="getdown.build.time" pattern="yyyy-MM-dd HH:mm"/>
+  </tstamp>
+  <copy file="/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl" tofile="/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/Build.java" overwrite="true">
+    <filterset>
+      <filter value="${getdown.build.time}" token="build_time"/>
+      <filter value="1.8.3-SNAPSHOT" token="build_version"/>
+      <filter value="" token="host_whitelist"/>
+    </filterset>
+  </copy>
+</target>
+</project>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/classes/LICENSE b/getdown/src/getdown/core/target/classes/LICENSE
new file mode 100644 (file)
index 0000000..0d9b255
--- /dev/null
@@ -0,0 +1,24 @@
+Getdown - application installer, patcher and launcher
+
+Copyright (C) 2004-2016 Getdown authors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
+EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGE.
diff --git a/getdown/src/getdown/core/target/getdown-core-1.8.3-SNAPSHOT.jar b/getdown/src/getdown/core/target/getdown-core-1.8.3-SNAPSHOT.jar
new file mode 100644 (file)
index 0000000..545f20b
Binary files /dev/null and b/getdown/src/getdown/core/target/getdown-core-1.8.3-SNAPSHOT.jar differ
diff --git a/getdown/src/getdown/core/target/maven-archiver/pom.properties b/getdown/src/getdown/core/target/maven-archiver/pom.properties
new file mode 100644 (file)
index 0000000..6990c1c
--- /dev/null
@@ -0,0 +1,5 @@
+#Generated by Maven
+#Fri Apr 05 14:07:47 BST 2019
+version=1.8.3-SNAPSHOT
+groupId=com.threerings.getdown
+artifactId=getdown-core
diff --git a/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644 (file)
index 0000000..4110671
--- /dev/null
@@ -0,0 +1,63 @@
+com/threerings/getdown/util/StringUtil.class
+com/threerings/getdown/util/LaunchUtil.class
+com/threerings/getdown/tools/JarDiff.class
+com/threerings/getdown/data/Application$2$1.class
+com/threerings/getdown/tools/Patcher$1.class
+com/threerings/getdown/data/Application$StatusDisplay.class
+com/threerings/getdown/util/Color.class
+com/threerings/getdown/Log$OneLineFormatter.class
+com/threerings/getdown/tools/Patcher.class
+com/threerings/getdown/spi/ProxyAuth.class
+com/threerings/getdown/data/EnvConfig.class
+com/threerings/getdown/tools/Differ.class
+com/threerings/getdown/net/Downloader$State.class
+com/threerings/getdown/data/Application$3.class
+com/threerings/getdown/util/StreamUtil.class
+com/threerings/getdown/util/Config$ParseOpts.class
+com/threerings/getdown/net/Downloader$1.class
+com/threerings/getdown/tools/JarDiffCodes.class
+com/threerings/getdown/data/EnvConfig$Note$Level.class
+com/threerings/getdown/util/HostWhitelist.class
+com/threerings/getdown/util/Base64$Coder.class
+com/threerings/getdown/Log.class
+com/threerings/getdown/data/Digest$1.class
+com/threerings/getdown/data/Application.class
+com/threerings/getdown/data/Properties.class
+com/threerings/getdown/data/Application$AuxGroup.class
+com/threerings/getdown/util/VersionUtil.class
+com/threerings/getdown/util/Base64$Decoder.class
+com/threerings/getdown/data/Resource$Attr.class
+com/threerings/getdown/data/Resource$1.class
+com/threerings/getdown/data/Build.class
+com/threerings/getdown/util/Rectangle.class
+com/threerings/getdown/util/ConnectionUtil.class
+com/threerings/getdown/cache/GarbageCollector.class
+com/threerings/getdown/util/Base64$Encoder.class
+com/threerings/getdown/tools/JarDiff$JarFile2.class
+com/threerings/getdown/util/Base64.class
+com/threerings/getdown/data/ClassPath.class
+com/threerings/getdown/util/FileUtil$Visitor.class
+com/threerings/getdown/data/Application$2.class
+com/threerings/getdown/Log$Shim.class
+com/threerings/getdown/data/Resource.class
+com/threerings/getdown/data/SysProps.class
+com/threerings/getdown/data/Digest.class
+com/threerings/getdown/tools/JarDiffPatcher.class
+com/threerings/getdown/cache/GarbageCollector$1.class
+com/threerings/getdown/data/Application$3$1.class
+com/threerings/getdown/util/ProgressAggregator$1.class
+com/threerings/getdown/data/Application$UpdateInterface$Step.class
+com/threerings/getdown/data/PathBuilder.class
+com/threerings/getdown/util/MessageUtil.class
+com/threerings/getdown/util/Config.class
+com/threerings/getdown/spi/ProxyAuth$Credentials.class
+com/threerings/getdown/net/Downloader.class
+com/threerings/getdown/data/EnvConfig$Note.class
+com/threerings/getdown/util/ProgressObserver.class
+com/threerings/getdown/tools/Digester.class
+com/threerings/getdown/cache/ResourceCache.class
+com/threerings/getdown/util/FileUtil.class
+com/threerings/getdown/net/HTTPDownloader.class
+com/threerings/getdown/data/Application$UpdateInterface.class
+com/threerings/getdown/util/ProgressAggregator.class
+com/threerings/getdown/data/Application$1.class
diff --git a/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644 (file)
index 0000000..b8d8e16
--- /dev/null
@@ -0,0 +1,35 @@
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/Properties.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/ProgressAggregator.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/ClassPath.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffCodes.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/Log.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/Rectangle.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/Build.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
+/Users/bsoares/git/getdown2/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
diff --git a/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
new file mode 100644 (file)
index 0000000..6e36ab2
--- /dev/null
@@ -0,0 +1,15 @@
+com/threerings/getdown/util/FileUtilTest.class
+com/threerings/getdown/util/ColorTest.class
+com/threerings/getdown/util/HostWhitelistTest.class
+com/threerings/getdown/data/ClassPathTest.class
+com/threerings/getdown/data/EnvConfigTest.class
+com/threerings/getdown/util/ConfigTest.class
+com/threerings/getdown/data/PathBuilderTest.class
+com/threerings/getdown/util/ConfigTest$Pair.class
+com/threerings/getdown/util/StringUtilTest.class
+com/threerings/getdown/cache/ResourceCacheTest.class
+com/threerings/getdown/cache/GarbageCollectorTest.class
+com/threerings/getdown/util/VersionUtilTest.class
+com/threerings/getdown/tests/DigesterIT.class
+com/threerings/getdown/util/FileUtilTest$1CountingVisitor.class
+com/threerings/getdown/data/SysPropsTest.class
diff --git a/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/getdown/src/getdown/core/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
new file mode 100644 (file)
index 0000000..c98d02a
--- /dev/null
@@ -0,0 +1,13 @@
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/data/SysPropsTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/util/ColorTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/util/StringUtilTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/data/PathBuilderTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/util/VersionUtilTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/util/ConfigTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/util/FileUtilTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/data/ClassPathTest.java
+/Users/bsoares/git/getdown2/getdown/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.cache.GarbageCollectorTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.cache.GarbageCollectorTest.xml
new file mode 100644 (file)
index 0000000..b1f9264
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="5" failures="0" name="com.threerings.getdown.cache.GarbageCollectorTest" time="0.023" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.cache.GarbageCollectorTest" name="shouldDeleteCachedFileIfLastAccessedFileIsMissing" time="0.019"/>
+  <testcase classname="com.threerings.getdown.cache.GarbageCollectorTest" name="shouldDeleteCacheFolderIfFolderIsEmpty" time="0"/>
+  <testcase classname="com.threerings.getdown.cache.GarbageCollectorTest" name="shouldDeleteLastAccessedFileIfCachedFileIsMissing" time="0.001"/>
+  <testcase classname="com.threerings.getdown.cache.GarbageCollectorTest" name="shouldDeleteCacheEntryIfRetentionPeriodIsReached" time="0.001"/>
+  <testcase classname="com.threerings.getdown.cache.GarbageCollectorTest" name="shouldKeepFilesInCacheIfRententionPeriodIsNotReached" time="0.002"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.cache.ResourceCacheTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.cache.ResourceCacheTest.xml
new file mode 100644 (file)
index 0000000..67105c2
--- /dev/null
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="4" failures="0" name="com.threerings.getdown.cache.ResourceCacheTest" time="0.002" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.cache.ResourceCacheTest" name="shouldCacheFile" time="0"/>
+  <testcase classname="com.threerings.getdown.cache.ResourceCacheTest" name="shouldTrackFileUsage" time="0"/>
+  <testcase classname="com.threerings.getdown.cache.ResourceCacheTest" name="shouldNotCacheTheSameFile" time="0"/>
+  <testcase classname="com.threerings.getdown.cache.ResourceCacheTest" name="shouldRememberWhenFileWasRequested" time="0.002"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.ClassPathTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.ClassPathTest.xml
new file mode 100644 (file)
index 0000000..fc47a93
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="2" failures="0" name="com.threerings.getdown.data.ClassPathTest" time="0.003" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.data.ClassPathTest" name="shouldProvideJarUrls" time="0.002"/>
+  <testcase classname="com.threerings.getdown.data.ClassPathTest" name="shouldCreateValidArgumentString" time="0.001"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.EnvConfigTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.EnvConfigTest.xml
new file mode 100644 (file)
index 0000000..c643f44
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="6" failures="0" name="com.threerings.getdown.data.EnvConfigTest" time="0.006" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.data.EnvConfigTest" name="testSysPropsDir" time="0.004"/>
+  <testcase classname="com.threerings.getdown.data.EnvConfigTest" name="testArgvDirId" time="0"/>
+  <testcase classname="com.threerings.getdown.data.EnvConfigTest" name="testArgvDir" time="0"/>
+  <testcase classname="com.threerings.getdown.data.EnvConfigTest" name="testSysPropsDirIdBase" time="0.001"/>
+  <testcase classname="com.threerings.getdown.data.EnvConfigTest" name="testArgvDirIdArgs" time="0.001"/>
+  <testcase classname="com.threerings.getdown.data.EnvConfigTest" name="testArgvDirArgs" time="0"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.PathBuilderTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.PathBuilderTest.xml
new file mode 100644 (file)
index 0000000..1bf9d19
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="2" failures="0" name="com.threerings.getdown.data.PathBuilderTest" time="0.051" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.data.PathBuilderTest" name="shouldBuildCachedClassPath" time="0.048"/>
+  <testcase classname="com.threerings.getdown.data.PathBuilderTest" name="shouldBuildDefaultClassPath" time="0.003"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.SysPropsTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.data.SysPropsTest.xml
new file mode 100644 (file)
index 0000000..cd06cc3
--- /dev/null
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="3" failures="0" name="com.threerings.getdown.data.SysPropsTest" time="0.002" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.data.SysPropsTest" name="testAppbaseOverride" time="0.001"/>
+  <testcase classname="com.threerings.getdown.data.SysPropsTest" name="testAppbaseDomain" time="0.001"/>
+  <testcase classname="com.threerings.getdown.data.SysPropsTest" name="testStartDelay" time="0"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.ColorTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.ColorTest.xml
new file mode 100644 (file)
index 0000000..1fa7137
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="1" failures="0" name="com.threerings.getdown.util.ColorTest" time="0" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.util.ColorTest" name="testBrightness" time="0"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.ConfigTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.ConfigTest.xml
new file mode 100644 (file)
index 0000000..6f7d3e9
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="2" failures="0" name="com.threerings.getdown.util.ConfigTest" time="0.017" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.util.ConfigTest" name="testSimplePairs" time="0.001"/>
+  <testcase classname="com.threerings.getdown.util.ConfigTest" name="testQualifiedPairs" time="0.016"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.FileUtilTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.FileUtilTest.xml
new file mode 100644 (file)
index 0000000..f9cc27e
--- /dev/null
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="3" failures="0" name="com.threerings.getdown.util.FileUtilTest" time="0.004" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.util.FileUtilTest" name="shouldCopyFile" time="0.001"/>
+  <testcase classname="com.threerings.getdown.util.FileUtilTest" name="testReadLines" time="0.001"/>
+  <testcase classname="com.threerings.getdown.util.FileUtilTest" name="shouldRecursivelyWalkOverFilesAndFolders" time="0.002"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.HostWhitelistTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.HostWhitelistTest.xml
new file mode 100644 (file)
index 0000000..5836b6b
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="1" failures="0" name="com.threerings.getdown.util.HostWhitelistTest" time="0.009" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.util.HostWhitelistTest" name="testVerify" time="0.009"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.StringUtilTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.StringUtilTest.xml
new file mode 100644 (file)
index 0000000..e7e4341
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="1" failures="0" name="com.threerings.getdown.util.StringUtilTest" time="0" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.util.StringUtilTest" name="testCouldBeValidUrl" time="0"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.VersionUtilTest.xml b/getdown/src/getdown/core/target/surefire-reports/TEST-com.threerings.getdown.util.VersionUtilTest.xml
new file mode 100644 (file)
index 0000000..f17e12d
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<testsuite tests="5" failures="0" name="com.threerings.getdown.util.VersionUtilTest" time="0.001" errors="0" skipped="0">
+  <properties>
+    <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+    <property name="sun.boot.library.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib"/>
+    <property name="java.vm.version" value="25.202-b08"/>
+    <property name="gopherProxySet" value="false"/>
+    <property name="java.vm.vendor" value="Oracle Corporation"/>
+    <property name="maven.multiModuleProjectDirectory" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.vendor.url" value="https://adoptopenjdk.net/"/>
+    <property name="path.separator" value=":"/>
+    <property name="guice.disable.misplaced.annotation.check" value="true"/>
+    <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+    <property name="file.encoding.pkg" value="sun.io"/>
+    <property name="user.country" value="GB"/>
+    <property name="sun.java.launcher" value="SUN_STANDARD"/>
+    <property name="sun.os.patch.level" value="unknown"/>
+    <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+    <property name="user.dir" value="/Users/bsoares/git/getdown2/getdown"/>
+    <property name="java.runtime.version" value="1.8.0_202-b08"/>
+    <property name="java.awt.graphicsenv" value="sun.awt.CGraphicsEnvironment"/>
+    <property name="java.endorsed.dirs" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/endorsed"/>
+    <property name="os.arch" value="x86_64"/>
+    <property name="java.io.tmpdir" value="/var/folders/l1/hnbhx1t55lx82wctsg09z0jwc62nf3/T/"/>
+    <property name="line.separator" value="
+"/>
+    <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+    <property name="os.name" value="Mac OS X"/>
+    <property name="classworlds.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/bin/m2.conf"/>
+    <property name="sun.jnu.encoding" value="UTF-8"/>
+    <property name="java.library.path" value="/Users/bsoares/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
+    <property name="maven.conf" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/conf"/>
+    <property name="java.specification.name" value="Java Platform API Specification"/>
+    <property name="java.class.version" value="52.0"/>
+    <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+    <property name="os.version" value="10.13.6"/>
+    <property name="library.jansi.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/lib/jansi-native"/>
+    <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="user.home" value="/Users/bsoares"/>
+    <property name="user.timezone" value="Europe/London"/>
+    <property name="java.awt.printerjob" value="sun.lwawt.macosx.CPrinterJob"/>
+    <property name="java.specification.version" value="1.8"/>
+    <property name="file.encoding" value="UTF-8"/>
+    <property name="user.name" value="bsoares"/>
+    <property name="java.class.path" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2/boot/plexus-classworlds-2.5.2.jar"/>
+    <property name="java.vm.specification.version" value="1.8"/>
+    <property name="sun.arch.data.model" value="64"/>
+    <property name="java.home" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre"/>
+    <property name="sun.java.command" value="org.codehaus.plexus.classworlds.launcher.Launcher package"/>
+    <property name="java.specification.vendor" value="Oracle Corporation"/>
+    <property name="user.language" value="en"/>
+    <property name="awt.toolkit" value="sun.lwawt.macosx.LWCToolkit"/>
+    <property name="java.vm.info" value="mixed mode"/>
+    <property name="java.version" value="1.8.0_202"/>
+    <property name="java.ext.dirs" value="/Users/bsoares/Library/Java/Extensions:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java"/>
+    <property name="sun.boot.class.path" value="/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/resources.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/rt.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/sunrsasign.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jsse.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jce.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/charsets.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/lib/jfr.jar:/Users/bsoares/buildtools/jvm/OpenJDK8/OpenJDK8U-jdk_x64_mac_hotspot_8u202b08/jdk8u202-b08/Contents/Home/jre/classes"/>
+    <property name="java.vendor" value="AdoptOpenJdk"/>
+    <property name="maven.home" value="/Users/bsoares/.mvnvm/apache-maven-3.5.2"/>
+    <property name="file.separator" value="/"/>
+    <property name="java.vendor.url.bug" value="https://github.com/AdoptOpenJDK/openjdk-build/issues"/>
+    <property name="sun.cpu.endian" value="little"/>
+    <property name="sun.io.unicode.encoding" value="UnicodeBig"/>
+    <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
+    <property name="sun.cpu.isalist" value=""/>
+  </properties>
+  <testcase classname="com.threerings.getdown.util.VersionUtilTest" name="shouldParseJavaRuntimeVersion" time="0.001"/>
+  <testcase classname="com.threerings.getdown.util.VersionUtilTest" name="shouldParseJavaVersion10" time="0"/>
+  <testcase classname="com.threerings.getdown.util.VersionUtilTest" name="shouldParseJavaVersion8" time="0"/>
+  <testcase classname="com.threerings.getdown.util.VersionUtilTest" name="shouldParseJavaVersion9" time="0"/>
+  <testcase classname="com.threerings.getdown.util.VersionUtilTest" name="shouldParseJavaVersion" time="0"/>
+</testsuite>
\ No newline at end of file
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.cache.GarbageCollectorTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.cache.GarbageCollectorTest.txt
new file mode 100644 (file)
index 0000000..92b4d7d
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.cache.GarbageCollectorTest
+-------------------------------------------------------------------------------
+Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.074 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.cache.ResourceCacheTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.cache.ResourceCacheTest.txt
new file mode 100644 (file)
index 0000000..754966c
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.cache.ResourceCacheTest
+-------------------------------------------------------------------------------
+Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.003 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.ClassPathTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.ClassPathTest.txt
new file mode 100644 (file)
index 0000000..c7133d5
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.data.ClassPathTest
+-------------------------------------------------------------------------------
+Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.003 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.EnvConfigTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.EnvConfigTest.txt
new file mode 100644 (file)
index 0000000..33de8bc
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.data.EnvConfigTest
+-------------------------------------------------------------------------------
+Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.007 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.PathBuilderTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.PathBuilderTest.txt
new file mode 100644 (file)
index 0000000..f7d5f9b
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.data.PathBuilderTest
+-------------------------------------------------------------------------------
+Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.804 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.SysPropsTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.data.SysPropsTest.txt
new file mode 100644 (file)
index 0000000..fc7d058
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.data.SysPropsTest
+-------------------------------------------------------------------------------
+Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.003 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.ColorTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.ColorTest.txt
new file mode 100644 (file)
index 0000000..cc44a40
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.util.ColorTest
+-------------------------------------------------------------------------------
+Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.ConfigTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.ConfigTest.txt
new file mode 100644 (file)
index 0000000..b8da385
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.util.ConfigTest
+-------------------------------------------------------------------------------
+Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.FileUtilTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.FileUtilTest.txt
new file mode 100644 (file)
index 0000000..bca7cfe
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.util.FileUtilTest
+-------------------------------------------------------------------------------
+Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.HostWhitelistTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.HostWhitelistTest.txt
new file mode 100644 (file)
index 0000000..bd0d97d
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.util.HostWhitelistTest
+-------------------------------------------------------------------------------
+Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.01 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.StringUtilTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.StringUtilTest.txt
new file mode 100644 (file)
index 0000000..14f28f8
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.util.StringUtilTest
+-------------------------------------------------------------------------------
+Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec
diff --git a/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.VersionUtilTest.txt b/getdown/src/getdown/core/target/surefire-reports/com.threerings.getdown.util.VersionUtilTest.txt
new file mode 100644 (file)
index 0000000..32439d1
--- /dev/null
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.threerings.getdown.util.VersionUtilTest
+-------------------------------------------------------------------------------
+Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec
diff --git a/getdown/src/getdown/core/target/test-classes/mockito-extensions/org.mockito.plugins.MockMaker b/getdown/src/getdown/core/target/test-classes/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644 (file)
index 0000000..1f0955d
--- /dev/null
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/getdown/src/getdown/launcher/.project b/getdown/src/getdown/launcher/.project
new file mode 100644 (file)
index 0000000..d77a6e8
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>getdown-launcher</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.m2e.core.maven2Builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.eclipse.m2e.core.maven2Nature</nature>
+       </natures>
+</projectDescription>
diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..abdea9a
--- /dev/null
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..54e5672
--- /dev/null
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.7
diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs
new file mode 100644 (file)
index 0000000..f897a7f
--- /dev/null
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/getdown/src/getdown/launcher/pom.xml b/getdown/src/getdown/launcher/pom.xml
new file mode 100644 (file)
index 0000000..cf94571
--- /dev/null
@@ -0,0 +1,176 @@
+<?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.getdown</groupId>
+    <artifactId>getdown</artifactId>
+    <version>1.8.3-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>getdown-launcher</artifactId>
+  <packaging>jar</packaging>
+  <name>Getdown Launcher</name>
+  <description>The Getdown app updater/launcher</description>
+
+  <repositories>
+    <repository>
+      <id>lib-repo</id>
+      <url>file://${basedir}/../lib</url>
+    </repository>
+  </repositories>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.threerings.getdown</groupId>
+      <artifactId>getdown-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.samskivert</groupId>
+      <artifactId>samskivert</artifactId>
+      <version>1.2</version>
+    </dependency>
+    <dependency>
+      <groupId>jregistrykey</groupId>
+      <artifactId>jregistrykey</artifactId>
+      <version>1.0</version>
+      <optional>true</optional>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>com.github.wvengen</groupId>
+        <artifactId>proguard-maven-plugin</artifactId>
+        <version>2.0.14</version>
+        <executions>
+         <execution>
+           <phase>package</phase>
+            <goals><goal>proguard</goal></goals>
+         </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>net.sf.proguard</groupId>
+            <artifactId>proguard-base</artifactId>
+            <version>6.0.3</version>
+            <scope>runtime</scope>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <proguardVersion>6.0.3</proguardVersion>
+          <outputDirectory>${project.build.directory}</outputDirectory>
+          <outjar>${project.build.finalName}.jar</outjar>
+          <injar>${project.build.finalName}.jar</injar>
+          <assembly>
+            <inclusions>
+              <inclusion>
+                <groupId>com.threerings.getdown</groupId>
+                <artifactId>getdown-core</artifactId>
+              </inclusion>
+              <inclusion>
+                <groupId>com.samskivert</groupId>
+                <artifactId>samskivert</artifactId>
+                <filter>
+                  !**/*.java,
+                  !**/swing/RuntimeAdjust*,
+                  !**/swing/util/ButtonUtil*,
+                  !**/util/CalendarUtil*,
+                  !**/util/Calendars*,
+                  !**/util/Log4JLogger*,
+                  !**/util/PrefsConfig*,
+                  !**/util/SignalUtil*,
+                  com/samskivert/Log.class,
+                  **/samskivert/io/**,
+                  **/samskivert/swing/**,
+                  **/samskivert/text/**,
+                  **/samskivert/util/**
+                </filter>
+              </inclusion>
+              <inclusion>
+                <groupId>jregistrykey</groupId>
+                <artifactId>jregistrykey</artifactId>
+              </inclusion>
+            </inclusions>
+          </assembly>
+          <obfuscate>true</obfuscate>
+          <options>
+            <option>-keep public class com.threerings.getdown.** { *; }</option>
+            <option>-keep public class ca.beq.util.win32.registry.** { *; }</option>
+            <option>-keepattributes Exceptions, InnerClasses, Signature</option>
+          </options>
+          <libs>
+            <lib>${rt.jar.path}</lib>
+          </libs>
+          <addMavenDescriptor>false</addMavenDescriptor>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>3.1.0</version>
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>com.threerings.getdown.launcher.GetdownApp</mainClass>
+            </manifest>
+            <manifestEntries>
+              <Permissions>all-permissions</Permissions>
+              <Application-Name>Getdown</Application-Name>
+              <Codebase>*</Codebase>
+              <Application-Library-Allowable-Codebase>*</Application-Library-Allowable-Codebase>
+              <Caller-Allowable-Codebase>*</Caller-Allowable-Codebase>
+              <Trusted-Library>true</Trusted-Library>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <!-- finagling to find rt.jar -->
+    <profile>
+      <id>non-mac-jre</id>
+      <activation>
+        <file><exists>${java.home}/../lib/rt.jar</exists></file>
+      </activation>
+      <properties>
+        <rt.jar.path>${java.home}/../lib/rt.jar</rt.jar.path>
+      </properties>
+    </profile>
+    <profile>
+      <id>non-mac-jdk</id>
+      <activation>
+        <file><exists>${java.home}/lib/rt.jar</exists></file>
+      </activation>
+      <properties>
+        <rt.jar.path>${java.home}/lib/rt.jar</rt.jar.path>
+      </properties>
+    </profile>
+    <profile>
+      <id>java-9-jdk</id>
+      <activation>
+        <file><exists>${java.home}/jmods/java.base.jmod</exists></file>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>com.github.wvengen</groupId>
+            <artifactId>proguard-maven-plugin</artifactId>
+            <configuration>
+              <libs>
+                <lib>${java.home}/jmods/java.base.jmod</lib>
+                <lib>${java.home}/jmods/java.desktop.jmod</lib>
+                <lib>${java.home}/jmods/java.logging.jmod</lib>
+                <lib>${java.home}/jmods/jdk.jsobject.jmod</lib>
+              </libs>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
new file mode 100644 (file)
index 0000000..dc1e54e
--- /dev/null
@@ -0,0 +1,100 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import com.samskivert.swing.GroupLayout;
+import com.samskivert.swing.Spacer;
+import com.samskivert.swing.VGroupLayout;
+
+import com.threerings.getdown.util.MessageUtil;
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Displays a confirmation that the user wants to abort installation.
+ */
+public final class AbortPanel extends JFrame
+    implements ActionListener
+{
+    public AbortPanel (Getdown getdown, ResourceBundle msgs)
+    {
+        _getdown = getdown;
+        _msgs = msgs;
+
+        setLayout(new VGroupLayout());
+        setResizable(false);
+        setTitle(get("m.abort_title"));
+
+        JLabel message = new JLabel(get("m.abort_confirm"));
+        message.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        add(message);
+        add(new Spacer(5, 5));
+
+        JPanel row = GroupLayout.makeButtonBox(GroupLayout.CENTER);
+        JButton button;
+        row.add(button = new JButton(get("m.abort_ok")));
+        button.setActionCommand("ok");
+        button.addActionListener(this);
+        row.add(button = new JButton(get("m.abort_cancel")));
+        button.setActionCommand("cancel");
+        button.addActionListener(this);
+        getRootPane().setDefaultButton(button);
+        add(row);
+    }
+
+    // documentation inherited
+    @Override
+    public Dimension getPreferredSize ()
+    {
+        // this is annoyingly hardcoded, but we can't just force the width
+        // or the JLabel will claim a bogus height thinking it can lay its
+        // text out all on one line which will booch the whole UI's
+        // preferred size
+        return new Dimension(300, 200);
+    }
+
+    // documentation inherited from interface
+    public void actionPerformed (ActionEvent e)
+    {
+        String cmd = e.getActionCommand();
+        if (cmd.equals("ok")) {
+            System.exit(0);
+        } else {
+            setVisible(false);
+        }
+    }
+
+    /** Used to look up localized messages. */
+    protected String get (String key)
+    {
+        // if this string is tainted, we don't translate it, instead we
+        // simply remove the taint character and return it to the caller
+        if (MessageUtil.isTainted(key)) {
+            return MessageUtil.untaint(key);
+        }
+        try {
+            return _msgs.getString(key);
+        } catch (MissingResourceException mre) {
+            log.warning("Missing translation message '" + key + "'.");
+            return key;
+        }
+    }
+
+    protected Getdown _getdown;
+    protected ResourceBundle _msgs;
+}
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
new file mode 100644 (file)
index 0000000..99def4f
--- /dev/null
@@ -0,0 +1,1071 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.GraphicsEnvironment;
+import java.awt.Image;
+import java.awt.event.ActionEvent;
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.*;
+
+import javax.imageio.ImageIO;
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLayeredPane;
+
+import com.samskivert.swing.util.SwingUtil;
+import com.threerings.getdown.data.*;
+import com.threerings.getdown.data.Application.UpdateInterface.Step;
+import com.threerings.getdown.net.Downloader;
+import com.threerings.getdown.net.HTTPDownloader;
+import com.threerings.getdown.tools.Patcher;
+import com.threerings.getdown.util.*;
+
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Manages the main control for the Getdown application updater and deployment system.
+ */
+public abstract class Getdown extends Thread
+    implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
+{
+    public Getdown (EnvConfig envc)
+    {
+        super("Getdown");
+        try {
+            // If the silent property exists, install without bringing up any gui. If it equals
+            // launch, start the application after installing. Otherwise, just install and exit.
+            _silent = SysProps.silent();
+            if (_silent) {
+                _launchInSilent = SysProps.launchInSilent();
+                _noUpdate = SysProps.noUpdate();
+            }
+            // If we're running in a headless environment and have not otherwise customized
+            // silence, operate without a UI and do launch the app.
+            if (!_silent && GraphicsEnvironment.isHeadless()) {
+                log.info("Running in headless JVM, will attempt to operate without UI.");
+                _silent = true;
+                _launchInSilent = true;
+            }
+            _delay = SysProps.startDelay();
+        } catch (SecurityException se) {
+            // don't freak out, just assume non-silent and no delay; we're probably already
+            // recovering from a security failure
+        }
+        try {
+            _msgs = ResourceBundle.getBundle("com.threerings.getdown.messages");
+        } catch (Exception e) {
+            // welcome to hell, where java can't cope with a classpath that contains jars that live
+            // in a directory that contains a !, at least the same bug happens on all platforms
+            String dir = envc.appDir.toString();
+            if (dir.equals(".")) {
+                dir = System.getProperty("user.dir");
+            }
+            String errmsg = "The directory in which this application is installed:\n" + dir +
+                "\nis invalid (" + e.getMessage() + "). If the full path to the app directory " +
+                "contains the '!' character, this will trigger this error.";
+            fail(errmsg);
+        }
+        _app = new Application(envc);
+        _startup = System.currentTimeMillis();
+    }
+
+    /**
+     * Returns true if there are pending new resources, waiting to be installed.
+     */
+    public boolean isUpdateAvailable ()
+    {
+        return _readyToInstall && !_toInstallResources.isEmpty();
+    }
+
+    /**
+     * Installs the currently pending new resources.
+     */
+    public void install () throws IOException
+    {
+        if (SysProps.noInstall()) {
+            log.info("Skipping install due to 'no_install' sysprop.");
+        } else if (_readyToInstall) {
+            log.info("Installing " + _toInstallResources.size() + " downloaded resources:");
+            for (Resource resource : _toInstallResources) {
+                resource.install(true);
+            }
+            _toInstallResources.clear();
+            _readyToInstall = false;
+            log.info("Install completed.");
+        } else {
+            log.info("Nothing to install.");
+        }
+    }
+
+    @Override
+    public void run ()
+    {
+        // if we have no messages, just bail because we're hosed; the error message will be
+        // displayed to the user already
+        if (_msgs == null) {
+            return;
+        }
+
+        log.info("Getdown starting", "version", Build.version(), "built", Build.time());
+
+        // determine whether or not we can write to our install directory
+        File instdir = _app.getLocalPath("");
+        if (!instdir.canWrite()) {
+            String path = instdir.getPath();
+            if (path.equals(".")) {
+                path = System.getProperty("user.dir");
+            }
+            fail(MessageUtil.tcompose("m.readonly_error", path));
+            return;
+        }
+
+        try {
+            _dead = false;
+            // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
+            // run the app anyway because we're prepared to cope with not being able to update
+            if (detectProxy() || _app.allowOffline()) {
+                getdown();
+            } else if (_silent) {
+                log.warning("Need a proxy, but we don't want to bother anyone.  Exiting.");
+            } else {
+                // create a panel they can use to configure the proxy settings
+                _container = createContainer();
+                // allow them to close the window to abort the proxy configuration
+                _dead = true;
+                configureContainer();
+                ProxyPanel panel = new ProxyPanel(this, _msgs);
+                // set up any existing configured proxy
+                String[] hostPort = ProxyUtil.loadProxy(_app);
+                panel.setProxy(hostPort[0], hostPort[1]);
+                _container.add(panel, BorderLayout.CENTER);
+                showContainer();
+            }
+
+        } catch (Exception e) {
+            log.warning("run() failed.", e);
+            String msg = e.getMessage();
+            if (msg == null) {
+                msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
+            } else if (!msg.startsWith("m.")) {
+                // try to do something sensible based on the type of error
+                if (e instanceof FileNotFoundException) {
+                    msg = MessageUtil.compose(
+                        "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
+                } else {
+                    msg = MessageUtil.compose(
+                        "m.init_error", MessageUtil.taint(msg), _ifc.installError);
+                }
+            }
+            fail(msg);
+        }
+    }
+
+    /**
+     * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
+     */
+    public void configProxy (String host, String port, String username, String password)
+    {
+        log.info("User configured proxy", "host", host, "port", port);
+
+        if (!StringUtil.isBlank(host)) {
+            ProxyUtil.configProxy(_app, host, port, username, password);
+        }
+
+        // clear out our UI
+        disposeContainer();
+        _container = null;
+
+        // fire up a new thread
+        new Thread(this).start();
+    }
+
+    protected boolean detectProxy () {
+        if (ProxyUtil.autoDetectProxy(_app)) {
+            return true;
+        }
+
+        // otherwise see if we actually need a proxy; first we have to initialize our application
+        // to get some sort of interface configuration and the appbase URL
+        log.info("Checking whether we need to use a proxy...");
+        try {
+            readConfig(true);
+        } catch (IOException ioe) {
+            // no worries
+        }
+        updateStatus("m.detecting_proxy");
+        if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) {
+            return false;
+        }
+
+        // we got through, so we appear not to require a proxy; make a blank proxy config so that
+        // we don't go through this whole detection process again next time
+        log.info("No proxy appears to be needed.");
+        ProxyUtil.saveProxy(_app, null, null);
+        return true;
+    }
+
+    protected void readConfig (boolean preloads) throws IOException {
+        Config config = _app.init(true);
+        if (preloads) doPredownloads(_app.getResources());
+        _ifc = new Application.UpdateInterface(config);
+    }
+
+    /**
+     * Downloads and installs (without verifying) any resources that are marked with a
+     * {@code PRELOAD} attribute.
+     * @param resources the full set of resources from the application (the predownloads will be
+     * extracted from it).
+     */
+    protected void doPredownloads (Collection<Resource> resources) {
+        List<Resource> predownloads = new ArrayList<>();
+        for (Resource rsrc : resources) {
+            if (rsrc.shouldPredownload() && !rsrc.getLocal().exists()) {
+                predownloads.add(rsrc);
+            }
+        }
+
+        try {
+            download(predownloads);
+            for (Resource rsrc : predownloads) {
+                rsrc.install(false); // install but don't validate yet
+            }
+        } catch (IOException ioe) {
+            log.warning("Failed to predownload resources. Continuing...", ioe);
+        }
+    }
+
+    /**
+     * Does the actual application validation, update and launching business.
+     */
+    protected void getdown ()
+    {
+        try {
+            // first parses our application deployment file
+            try {
+                readConfig(true);
+            } catch (IOException ioe) {
+                log.warning("Failed to initialize: " + ioe);
+                _app.attemptRecovery(this);
+                // and re-initalize
+                readConfig(true);
+                // and force our UI to be recreated with the updated info
+                createInterfaceAsync(true);
+            }
+            if (!_noUpdate && !_app.lockForUpdates()) {
+                throw new MultipleGetdownRunning();
+            }
+
+            // Update the config modtime so a sleeping getdown will notice the change.
+            File config = _app.getLocalPath(Application.CONFIG_FILE);
+            if (!config.setLastModified(System.currentTimeMillis())) {
+                log.warning("Unable to set modtime on config file, will be unable to check for " +
+                            "another instance of getdown running while this one waits.");
+            }
+            if (_delay > 0) {
+                // don't hold the lock while waiting, let another getdown proceed if it starts.
+                _app.releaseLock();
+                // Store the config modtime before waiting the delay amount of time
+                long lastConfigModtime = config.lastModified();
+                log.info("Waiting " + _delay + " minutes before beginning actual work.");
+                Thread.sleep(_delay * 60 * 1000);
+                if (lastConfigModtime < config.lastModified()) {
+                    log.warning("getdown.txt was modified while getdown was waiting.");
+                    throw new MultipleGetdownRunning();
+                }
+            }
+
+            // if no_update was specified, directly start the app without updating
+            if (_noUpdate) {
+                log.info("Launching without update!");
+                launch();
+                return;
+            }
+
+            // we create this tracking counter here so that we properly note the first time through
+            // the update process whether we previously had validated resources (which means this
+            // is not a first time install); we may, in the course of updating, wipe out our
+            // validation markers and revalidate which would make us think we were doing a fresh
+            // install if we didn't specifically remember that we had validated resources the first
+            // time through
+            int[] alreadyValid = new int[1];
+
+            // we'll keep track of all the resources we unpack
+            Set<Resource> unpacked = new HashSet<>();
+
+            _toInstallResources = new HashSet<>();
+            _readyToInstall = false;
+
+            // setStep(Step.START);
+            for (int ii = 0; ii < MAX_LOOPS; ii++) {
+                // make sure we have the desired version and that the metadata files are valid...
+                setStep(Step.VERIFY_METADATA);
+                setStatusAsync("m.validating", -1, -1L, false);
+                if (_app.verifyMetadata(this)) {
+                    log.info("Application requires update.");
+                    update();
+                    // loop back again and reverify the metadata
+                    continue;
+                }
+
+                // now verify (and download) our resources...
+                setStep(Step.VERIFY_RESOURCES);
+                setStatusAsync("m.validating", -1, -1L, false);
+                Set<Resource> toDownload = new HashSet<>();
+                _app.verifyResources(_progobs, alreadyValid, unpacked,
+                                     _toInstallResources, toDownload);
+
+                if (toDownload.size() > 0) {
+                    // we have resources to download, also note them as to-be-installed
+                    for (Resource r : toDownload) {
+                        if (!_toInstallResources.contains(r)) {
+                            _toInstallResources.add(r);
+                        }
+                    }
+
+                    try {
+                        // if any of our resources have already been marked valid this is not a
+                        // first time install and we don't want to enable tracking
+                        _enableTracking = (alreadyValid[0] == 0);
+                        reportTrackingEvent("app_start", -1);
+
+                        // redownload any that are corrupt or invalid...
+                        log.info(toDownload.size() + " of " + _app.getAllActiveResources().size() +
+                                 " rsrcs require update (" + alreadyValid[0] + " assumed valid).");
+                        setStep(Step.REDOWNLOAD_RESOURCES);
+                        download(toDownload);
+
+                        reportTrackingEvent("app_complete", -1);
+
+                    } finally {
+                        _enableTracking = false;
+                    }
+
+                    // now we'll loop back and try it all again
+                    continue;
+                }
+
+                // if we aren't running in a JVM that meets our version requirements, either
+                // complain or attempt to download and install the appropriate version
+                if (!_app.haveValidJavaVersion()) {
+                    // download and install the necessary version of java, then loop back again and
+                    // reverify everything; if we can't download java; we'll throw an exception
+                    log.info("Attempting to update Java VM...");
+                    setStep(Step.UPDATE_JAVA);
+                    _enableTracking = true; // always track JVM downloads
+                    try {
+                        updateJava();
+                    } finally {
+                        _enableTracking = false;
+                    }
+                    continue;
+                }
+
+                // if we were downloaded in full from another service (say, Steam), we may
+                // not have unpacked all of our resources yet
+                if (Boolean.getBoolean("check_unpacked")) {
+                    File ufile = _app.getLocalPath("unpacked.dat");
+                    long version = -1;
+                    long aversion = _app.getVersion();
+                    if (!ufile.exists()) {
+                        ufile.createNewFile();
+                    } else {
+                        version = VersionUtil.readVersion(ufile);
+                    }
+
+                    if (version < aversion) {
+                        log.info("Performing unpack", "version", version, "aversion", aversion);
+                        setStep(Step.UNPACK);
+                        updateStatus("m.validating");
+                        _app.unpackResources(_progobs, unpacked);
+                        try {
+                            VersionUtil.writeVersion(ufile, aversion);
+                        } catch (IOException ioe) {
+                            log.warning("Failed to update unpacked version", ioe);
+                        }
+                    }
+                }
+
+                // assuming we're not doing anything funny, install the update
+                _readyToInstall = true;
+                install();
+
+                // Only launch if we aren't in silent mode. Some mystery program starting out
+                // of the blue would be disconcerting.
+                if (!_silent || _launchInSilent) {
+                    // And another final check for the lock. It'll already be held unless
+                    // we're in silent mode.
+                    _app.lockForUpdates();
+                    launch();
+                }
+                return;
+            }
+
+            log.warning("Pants! We couldn't get the job done.");
+            throw new IOException("m.unable_to_repair");
+
+        } catch (Exception e) {
+            log.warning("getdown() failed.", e);
+            String msg = e.getMessage();
+            if (msg == null) {
+                msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
+            } else if (!msg.startsWith("m.")) {
+                // try to do something sensible based on the type of error
+                if (e instanceof FileNotFoundException) {
+                    msg = MessageUtil.compose(
+                        "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
+                } else {
+                    msg = MessageUtil.compose(
+                        "m.init_error", MessageUtil.taint(msg), _ifc.installError);
+                }
+            }
+            // Since we're dead, clear off the 'time remaining' label along with displaying the
+            // error message
+            fail(msg);
+            _app.releaseLock();
+        }
+    }
+
+    // documentation inherited from interface
+    @Override
+    public void updateStatus (String message)
+    {
+        setStatusAsync(message, -1, -1L, true);
+    }
+
+    /**
+     * Load the image at the path. Before trying the exact path/file specified we will look to see
+     * if we can find a localized version by sticking a {@code _<language>} in front of the "." in
+     * the filename.
+     */
+    @Override
+    public BufferedImage loadImage (String path)
+    {
+        if (StringUtil.isBlank(path)) {
+            return null;
+        }
+
+        File imgpath = null;
+        try {
+            // First try for a localized image.
+            String localeStr = Locale.getDefault().getLanguage();
+            imgpath = _app.getLocalPath(path.replace(".", "_" + localeStr + "."));
+            return ImageIO.read(imgpath);
+        } catch (IOException ioe) {
+            // No biggie, we'll try the generic one.
+        }
+
+        // If that didn't work, try a generic one.
+        try {
+            imgpath = _app.getLocalPath(path);
+            return ImageIO.read(imgpath);
+        } catch (IOException ioe2) {
+            log.warning("Failed to load image", "path", imgpath, "error", ioe2);
+            return null;
+        }
+    }
+
+    /**
+     * Downloads and installs an Java VM bundled with the application. This is called if we are not
+     * running with the necessary Java version.
+     */
+    protected void updateJava ()
+        throws IOException
+    {
+        Resource vmjar = _app.getJavaVMResource();
+        if (vmjar == null) {
+            throw new IOException("m.java_download_failed");
+        }
+
+        reportTrackingEvent("jvm_start", -1);
+
+        updateStatus("m.downloading_java");
+        List<Resource> list = new ArrayList<>();
+        list.add(vmjar);
+        download(list);
+
+        reportTrackingEvent("jvm_unpack", -1);
+
+        updateStatus("m.unpacking_java");
+        vmjar.install(true);
+
+        // these only run on non-Windows platforms, so we use Unix file separators
+        String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
+        FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java"));
+        FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper"));
+        FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper"));
+
+        // lastly regenerate the .jsa dump file that helps Java to start up faster
+        String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
+        try {
+            log.info("Regenerating classes.jsa for " + vmpath + "...");
+            Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
+        } catch (Exception e) {
+            log.warning("Failed to regenerate .jsa dump file", "error", e);
+        }
+
+        reportTrackingEvent("jvm_complete", -1);
+    }
+
+    /**
+     * Called if the application is determined to be of an old version.
+     */
+    protected void update ()
+        throws IOException
+    {
+        // first clear all validation markers
+        _app.clearValidationMarkers();
+
+        // attempt to download the patch files
+        Resource patch = _app.getPatchResource(null);
+        if (patch != null) {
+            List<Resource> list = new ArrayList<>();
+            list.add(patch);
+
+            // add the auxiliary group patch files for activated groups
+            for (Application.AuxGroup aux : _app.getAuxGroups()) {
+                if (_app.isAuxGroupActive(aux.name)) {
+                    patch = _app.getPatchResource(aux.name);
+                    if (patch != null) {
+                        list.add(patch);
+                    }
+                }
+            }
+
+            // show the patch notes button, if applicable
+            if (!StringUtil.isBlank(_ifc.patchNotesUrl)) {
+                createInterfaceAsync(false);
+                EventQueue.invokeLater(new Runnable() {
+                    public void run () {
+                        _patchNotes.setVisible(true);
+                    }
+                });
+            }
+
+            // download the patch files...
+            setStep(Step.DOWNLOAD);
+            download(list);
+
+            // and apply them...
+            setStep(Step.PATCH);
+            updateStatus("m.patching");
+
+            long[] sizes = new long[list.size()];
+            Arrays.fill(sizes, 1L);
+            ProgressAggregator pragg = new ProgressAggregator(_progobs, sizes);
+            int ii = 0; for (Resource prsrc : list) {
+                ProgressObserver pobs = pragg.startElement(ii++);
+                try {
+                    // install the patch file (renaming them from _new)
+                    prsrc.install(false);
+                    // now apply the patch
+                    Patcher patcher = new Patcher();
+                    patcher.patch(prsrc.getLocal().getParentFile(), prsrc.getLocal(), pobs);
+                } catch (Exception e) {
+                    log.warning("Failed to apply patch", "prsrc", prsrc, e);
+                }
+
+                // clean up the patch file
+                if (!FileUtil.deleteHarder(prsrc.getLocal())) {
+                    log.warning("Failed to delete '" + prsrc + "'.");
+                }
+            }
+        }
+
+        // if the patch resource is null, that means something was booched in the application, so
+        // we skip the patching process but update the metadata which will result in a "brute
+        // force" upgrade
+
+        // finally update our metadata files...
+        _app.updateMetadata();
+        // ...and reinitialize the application
+        readConfig(false);
+    }
+
+    /**
+     * Called if the application is determined to require resource downloads.
+     */
+    protected void download (Collection<Resource> resources)
+        throws IOException
+    {
+        // create our user interface
+        createInterfaceAsync(false);
+
+        Downloader dl = new HTTPDownloader(_app.proxy) {
+            @Override protected void resolvingDownloads () {
+                updateStatus("m.resolving");
+            }
+
+            @Override protected void downloadProgress (int percent, long remaining) {
+                // check for another getdown running at 0 and every 10% after that
+                if (_lastCheck == -1 || percent >= _lastCheck + 10) {
+                    if (_delay > 0) {
+                        // stop the presses if something else is holding the lock
+                        boolean locked = _app.lockForUpdates();
+                        _app.releaseLock();
+                        if (locked) abort();
+                    }
+                    _lastCheck = percent;
+                }
+                setStatusAsync("m.downloading", stepToGlobalPercent(percent), remaining, true);
+                if (percent > 0) {
+                    reportTrackingEvent("progress", percent);
+                }
+            }
+
+            @Override protected void downloadFailed (Resource rsrc, Exception e) {
+                updateStatus(MessageUtil.tcompose("m.failure", e.getMessage()));
+                log.warning("Download failed", "rsrc", rsrc, e);
+            }
+
+            /** The last percentage at which we checked for another getdown running, or -1 for not
+             * having checked at all. */
+            protected int _lastCheck = -1;
+        };
+        if (!dl.download(resources, _app.maxConcurrentDownloads())) {
+            // if we aborted due to detecting another getdown running, we want to report here
+            throw new MultipleGetdownRunning();
+        }
+    }
+
+    /**
+     * Called to launch the application if everything is determined to be ready to go.
+     */
+    protected void launch ()
+    {
+        setStep(Step.LAUNCH);
+        setStatusAsync("m.launching", stepToGlobalPercent(100), -1L, false);
+
+        try {
+            if (invokeDirect()) {
+                // we want to close the Getdown window, as the app is launching
+                disposeContainer();
+                _app.releaseLock();
+                _app.invokeDirect();
+
+            } else {
+                Process proc;
+                if (_app.hasOptimumJvmArgs()) {
+                    // if we have "optimum" arguments, we want to try launching with them first
+                    proc = _app.createProcess(true);
+
+                    long fallback = System.currentTimeMillis() + FALLBACK_CHECK_TIME;
+                    boolean error = false;
+                    while (fallback > System.currentTimeMillis()) {
+                        try {
+                            error = proc.exitValue() != 0;
+                            break;
+                        } catch (IllegalThreadStateException e) {
+                            Thread.yield();
+                        }
+                    }
+
+                    if (error) {
+                        log.info("Failed to launch with optimum arguments; falling back.");
+                        proc = _app.createProcess(false);
+                    }
+                } else {
+                    proc = _app.createProcess(false);
+                }
+
+                // close standard in to avoid choking standard out of the launched process
+                proc.getInputStream().close();
+                // close standard out, since we're not going to write to anything to it anyway
+                proc.getOutputStream().close();
+
+                // on Windows 98 and ME we need to stick around and read the output of stderr lest
+                // the process fill its output buffer and choke, yay!
+                final InputStream stderr = proc.getErrorStream();
+                if (LaunchUtil.mustMonitorChildren()) {
+                    // close our window if it's around
+                    disposeContainer();
+                    _container = null;
+                    copyStream(stderr, System.err);
+                    log.info("Process exited: " + proc.waitFor());
+
+                } else {
+                    // spawn a daemon thread that will catch the early bits of stderr in case the
+                    // launch fails
+                    Thread t = new Thread() {
+                        @Override public void run () {
+                            copyStream(stderr, System.err);
+                        }
+                    };
+                    t.setDaemon(true);
+                    t.start();
+                }
+            }
+
+            // if we have a UI open and we haven't been around for at least 5 seconds (the default
+            // for min_show_seconds), don't stick a fork in ourselves straight away but give our
+            // lovely user a chance to see what we're doing
+            long uptime = System.currentTimeMillis() - _startup;
+            long minshow = _ifc.minShowSeconds * 1000L;
+            if (_container != null && uptime < minshow) {
+                try {
+                    Thread.sleep(minshow - uptime);
+                } catch (Exception e) {
+                }
+            }
+
+            // pump the percent up to 100%
+            setStatusAsync(null, 100, -1L, false);
+            exit(0);
+
+        } catch (Exception e) {
+            log.warning("launch() failed.", e);
+        }
+    }
+
+    /**
+     * Creates our user interface, which we avoid doing unless we actually have to update
+     * something. NOTE: this happens on the next UI tick, not immediately.
+     *
+     * @param reinit - if the interface should be reinitialized if it already exists.
+     */
+    protected void createInterfaceAsync (final boolean reinit)
+    {
+        if (_silent || (_container != null && !reinit)) {
+            return;
+        }
+
+        EventQueue.invokeLater(new Runnable() {
+            public void run () {
+                if (_container == null || reinit) {
+                    if (_container == null) {
+                        _container = createContainer();
+                    } else {
+                        _container.removeAll();
+                    }
+                    configureContainer();
+                    _layers = new JLayeredPane();
+                    _container.add(_layers, BorderLayout.CENTER);
+                    _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) {
+                        @Override public void actionPerformed (ActionEvent event) {
+                            showDocument(_ifc.patchNotesUrl);
+                        }
+                    });
+                    _patchNotes.setFont(StatusPanel.FONT);
+                    _layers.add(_patchNotes);
+                    _status = new StatusPanel(_msgs);
+                    _layers.add(_status);
+                    initInterface();
+                }
+                showContainer();
+            }
+        });
+    }
+
+    /**
+     * Initializes the interface with the current UpdateInterface and backgrounds.
+     */
+    protected void initInterface ()
+    {
+        RotatingBackgrounds newBackgrounds = getBackground();
+        if (_background == null || newBackgrounds.getNumImages() > 0) {
+            // Leave the old _background in place if there is an old one to leave in place
+            // and the new getdown.txt didn't yield any images.
+            _background = newBackgrounds;
+        }
+        _status.init(_ifc, _background, getProgressImage());
+        Dimension size = _status.getPreferredSize();
+        _status.setSize(size);
+        _layers.setPreferredSize(size);
+
+        _patchNotes.setBounds(_ifc.patchNotes.x, _ifc.patchNotes.y,
+                              _ifc.patchNotes.width, _ifc.patchNotes.height);
+        _patchNotes.setVisible(false);
+
+        // we were displaying progress while the UI wasn't up. Now that it is, whatever progress
+        // is left is scaled into a 0-100 DISPLAYED progress.
+        _uiDisplayPercent = _lastGlobalPercent;
+        _stepMinPercent = _lastGlobalPercent = 0;
+    }
+
+    protected RotatingBackgrounds getBackground ()
+    {
+        if (_ifc.rotatingBackgrounds != null) {
+            if (_ifc.backgroundImage != null) {
+                log.warning("ui.background_image and ui.rotating_background were both specified. " +
+                            "The rotating images are being used.");
+            }
+            return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground,
+                Getdown.this);
+        } else if (_ifc.backgroundImage != null) {
+            return new RotatingBackgrounds(loadImage(_ifc.backgroundImage));
+        } else {
+            return new RotatingBackgrounds();
+        }
+    }
+
+    protected Image getProgressImage ()
+    {
+        return loadImage(_ifc.progressImage);
+    }
+
+    protected void handleWindowClose ()
+    {
+        if (_dead) {
+            exit(0);
+        } else {
+            if (_abort == null) {
+                _abort = new AbortPanel(Getdown.this, _msgs);
+            }
+            _abort.pack();
+            SwingUtil.centerWindow(_abort);
+            _abort.setVisible(true);
+            _abort.setState(JFrame.NORMAL);
+            _abort.requestFocus();
+        }
+    }
+
+    /**
+     * Update the status to indicate getdown has failed for the reason in <code>message</code>.
+     */
+    protected void fail (String message)
+    {
+        _dead = true;
+        setStatusAsync(message, stepToGlobalPercent(0), -1L, true);
+    }
+
+    /**
+     * Set the current step, which will be used to globalize per-step percentages.
+     */
+    protected void setStep (Step step)
+    {
+        int finalPercent = -1;
+        for (Integer perc : _ifc.stepPercentages.get(step)) {
+            if (perc > _stepMaxPercent) {
+                finalPercent = perc;
+                break;
+            }
+        }
+        if (finalPercent == -1) {
+            // we've gone backwards and this step will be ignored
+            return;
+        }
+
+        _stepMaxPercent = finalPercent;
+        _stepMinPercent = _lastGlobalPercent;
+    }
+
+    /**
+     * Convert a step percentage to the global percentage.
+     */
+    protected int stepToGlobalPercent (int percent)
+    {
+        int adjustedMaxPercent =
+            ((_stepMaxPercent - _uiDisplayPercent) * 100) / (100 - _uiDisplayPercent);
+        _lastGlobalPercent = Math.max(_lastGlobalPercent,
+            _stepMinPercent + (percent * (adjustedMaxPercent - _stepMinPercent)) / 100);
+        return _lastGlobalPercent;
+    }
+
+    /**
+     * Updates the status. NOTE: this happens on the next UI tick, not immediately.
+     */
+    protected void setStatusAsync (final String message, final int percent, final long remaining,
+                                   boolean createUI)
+    {
+        if (_status == null && createUI) {
+            createInterfaceAsync(false);
+        }
+
+        EventQueue.invokeLater(new Runnable() {
+            public void run () {
+                if (_status == null) {
+                    if (message != null) {
+                        log.info("Dropping status '" + message + "'.");
+                    }
+                    return;
+                }
+                if (message != null) {
+                    _status.setStatus(message, _dead);
+                }
+                if (_dead) {
+                    _status.setProgress(0, -1L);
+                } else if (percent >= 0) {
+                    _status.setProgress(percent, remaining);
+                }
+            }
+        });
+    }
+
+    protected void reportTrackingEvent (String event, int progress)
+    {
+        if (!_enableTracking) {
+            return;
+
+        } else if (progress > 0) {
+            // we need to make sure we do the right thing if we skip over progress levels
+            do {
+                URL url = _app.getTrackingProgressURL(++_reportedProgress);
+                if (url != null) {
+                    new ProgressReporter(url).start();
+                }
+            } while (_reportedProgress <= progress);
+
+        } else {
+            URL url = _app.getTrackingURL(event);
+            if (url != null) {
+                new ProgressReporter(url).start();
+            }
+        }
+    }
+
+    /**
+     * Creates the container in which our user interface will be displayed.
+     */
+    protected abstract Container createContainer ();
+
+    /**
+     * Configures the interface container based on the latest UI config.
+     */
+    protected abstract void configureContainer ();
+
+    /**
+     * Shows the container in which our user interface will be displayed.
+     */
+    protected abstract void showContainer ();
+
+    /**
+     * Disposes the container in which we have our user interface.
+     */
+    protected abstract void disposeContainer ();
+
+    /**
+     * If this method returns true we will run the application in the same JVM, otherwise we will
+     * fork off a new JVM. Some options are not supported if we do not fork off a new JVM.
+     */
+    protected boolean invokeDirect ()
+    {
+        return SysProps.direct();
+    }
+
+    /**
+     * Requests to show the document at the specified URL in a new window.
+     */
+    protected abstract void showDocument (String url);
+
+    /**
+     * Requests that Getdown exit.
+     */
+    protected abstract void exit (int exitCode);
+
+    /**
+     * Copies the supplied stream from the specified input to the specified output. Used to copy
+     * our child processes stderr and stdout to our own stderr and stdout.
+     */
+    protected static void copyStream (InputStream in, PrintStream out)
+    {
+        try {
+            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                out.print(line);
+                out.flush();
+            }
+        } catch (IOException ioe) {
+            log.warning("Failure copying", "in", in, "out", out, "error", ioe);
+        }
+    }
+
+    /** Used to fetch a progress report URL. */
+    protected class ProgressReporter extends Thread
+    {
+        public ProgressReporter (URL url) {
+            setDaemon(true);
+            _url = url;
+        }
+
+        @Override
+        public void run () {
+            try {
+                HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
+
+                // if we have a tracking cookie configured, configure the request with it
+                if (_app.getTrackingCookieName() != null &&
+                    _app.getTrackingCookieProperty() != null) {
+                    String val = System.getProperty(_app.getTrackingCookieProperty());
+                    if (val != null) {
+                        ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
+                    }
+                }
+
+                // now request our tracking URL and ensure that we get a non-error response
+                ucon.connect();
+                try {
+                    if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                        log.warning("Failed to report tracking event",
+                            "url", _url, "rcode", ucon.getResponseCode());
+                    }
+                } finally {
+                    ucon.disconnect();
+                }
+
+            } catch (IOException ioe) {
+                log.warning("Failed to report tracking event", "url", _url, "error", ioe);
+            }
+        }
+
+        protected URL _url;
+    }
+
+    /** Used to pass progress on to our user interface. */
+    protected ProgressObserver _progobs = new ProgressObserver() {
+        public void progress (int percent) {
+            setStatusAsync(null, stepToGlobalPercent(percent), -1L, false);
+        }
+    };
+
+    protected Application _app;
+    protected Application.UpdateInterface _ifc = new Application.UpdateInterface(Config.EMPTY);
+
+    protected ResourceBundle _msgs;
+    protected Container _container;
+    protected JLayeredPane _layers;
+    protected StatusPanel _status;
+    protected JButton _patchNotes;
+    protected AbortPanel _abort;
+    protected RotatingBackgrounds _background;
+
+    protected boolean _dead;
+    protected boolean _silent;
+    protected boolean _launchInSilent;
+    protected boolean _noUpdate;
+    protected long _startup;
+
+    protected Set<Resource> _toInstallResources;
+    protected boolean _readyToInstall;
+
+    protected boolean _enableTracking = true;
+    protected int _reportedProgress = 0;
+
+    /** Number of minutes to wait after startup before beginning any real heavy lifting. */
+    protected int _delay;
+
+    protected int _stepMaxPercent;
+    protected int _stepMinPercent;
+    protected int _lastGlobalPercent;
+    protected int _uiDisplayPercent;
+
+    protected static final int MAX_LOOPS = 5;
+    protected static final long FALLBACK_CHECK_TIME = 1000L;
+}
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
new file mode 100644 (file)
index 0000000..fde79f3
--- /dev/null
@@ -0,0 +1,253 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.EventQueue;
+import java.awt.Image;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.KeyStroke;
+import javax.swing.WindowConstants;
+
+import com.samskivert.swing.util.SwingUtil;
+import com.threerings.getdown.data.EnvConfig;
+import com.threerings.getdown.data.SysProps;
+import com.threerings.getdown.util.LaunchUtil;
+import com.threerings.getdown.util.StringUtil;
+import static com.threerings.getdown.Log.log;
+
+/**
+ * The main application entry point for Getdown.
+ */
+public class GetdownApp
+{
+    /**
+     * The main entry point of the Getdown launcher application.
+     */
+    public static void main (String[] argv) {
+        try {
+            start(argv);
+        } catch (Exception e) {
+            log.warning("main() failed.", e);
+        }
+    }
+
+    /**
+     * Runs Getdown as an application, using the arguments supplie as {@code argv}.
+     * @return the {@code Getdown} instance that is running. {@link Getdown#start} will have been
+     * called on it.
+     * @throws Exception if anything goes wrong starting Getdown.
+     */
+    public static Getdown start (String[] argv) throws Exception {
+        List<EnvConfig.Note> notes = new ArrayList<>();
+        EnvConfig envc = EnvConfig.create(argv, notes);
+        if (envc == null) {
+            if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message);
+            else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]");
+            System.exit(-1);
+        }
+
+        // pipe our output into a file in the application directory
+        if (!SysProps.noLogRedir()) {
+            File logFile = new File(envc.appDir, "launcher.log");
+            try {
+                PrintStream logOut = new PrintStream(
+                    new BufferedOutputStream(new FileOutputStream(logFile)), true);
+                System.setOut(logOut);
+                System.setErr(logOut);
+            } catch (IOException ioe) {
+                log.warning("Unable to redirect output to '" + logFile + "': " + ioe);
+            }
+        }
+
+        // report any notes from reading our env config, and abort if necessary
+        boolean abort = false;
+        for (EnvConfig.Note note : notes) {
+            switch (note.level) {
+            case INFO: log.info(note.message); break;
+            case WARN: log.warning(note.message); break;
+            case ERROR: log.error(note.message); abort = true; break;
+            }
+        }
+        if (abort) System.exit(-1);
+
+        // record a few things for posterity
+        log.info("------------------ VM Info ------------------");
+        log.info("-- OS Name: " + System.getProperty("os.name"));
+        log.info("-- OS Arch: " + System.getProperty("os.arch"));
+        log.info("-- OS Vers: " + System.getProperty("os.version"));
+        log.info("-- Java Vers: " + System.getProperty("java.version"));
+        log.info("-- Java Home: " + System.getProperty("java.home"));
+        log.info("-- User Name: " + System.getProperty("user.name"));
+        log.info("-- User Home: " + System.getProperty("user.home"));
+        log.info("-- Cur dir: " + System.getProperty("user.dir"));
+        log.info("---------------------------------------------");
+
+        Getdown app = new Getdown(envc) {
+            @Override
+            protected Container createContainer () {
+                // create our user interface, and display it
+                if (_frame == null) {
+                    _frame = new JFrame("");
+                    _frame.addWindowListener(new WindowAdapter() {
+                        @Override
+                        public void windowClosing (WindowEvent evt) {
+                            handleWindowClose();
+                        }
+                    });
+                    // handle close on ESC
+                    String cancelId = "Cancel"; // $NON-NLS-1$
+                    _frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
+                        KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelId);
+                    _frame.getRootPane().getActionMap().put(cancelId, new AbstractAction() {
+                        public void actionPerformed (ActionEvent e) {
+                            handleWindowClose();
+                        }
+                    });
+                    // this cannot be called in configureContainer as it is only allowed before the
+                    // frame has been displayed for the first time
+                    _frame.setUndecorated(_ifc.hideDecorations);
+                    _frame.setResizable(false);
+                } else {
+                    _frame.getContentPane().removeAll();
+                }
+                _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+                return _frame.getContentPane();
+            }
+
+            @Override
+            protected void configureContainer () {
+                if (_frame == null) return;
+
+                _frame.setTitle(_ifc.name);
+
+                try {
+                    _frame.setBackground(new Color(_ifc.background, true));
+                } catch (Exception e) {
+                    log.warning("Failed to set background", "bg", _ifc.background, e);
+                }
+
+                if (_ifc.iconImages != null) {
+                    ArrayList<Image> icons = new ArrayList<>();
+                    for (String path : _ifc.iconImages) {
+                        Image img = loadImage(path);
+                        if (img == null) {
+                            log.warning("Error loading icon image", "path", path);
+                        } else {
+                            icons.add(img);
+                        }
+                    }
+                    if (icons.isEmpty()) {
+                        log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
+                    } else {
+                        _frame.setIconImages(icons);
+                    }
+                }
+            }
+
+            @Override
+            protected void showContainer () {
+                if (_frame != null) {
+                    _frame.pack();
+                    SwingUtil.centerWindow(_frame);
+                    _frame.setVisible(true);
+                }
+            }
+
+            @Override
+            protected void disposeContainer () {
+                if (_frame != null) {
+                    _frame.dispose();
+                    _frame = null;
+                }
+            }
+
+            @Override
+            protected void showDocument (String url) {
+                if (!StringUtil.couldBeValidUrl(url)) {
+                    // command injection would be possible if we allowed e.g. spaces and double quotes
+                    log.warning("Invalid document URL.", "url", url);
+                    return;
+                }
+                String[] cmdarray;
+                if (LaunchUtil.isWindows()) {
+                    String osName = System.getProperty("os.name", "");
+                    if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) {
+                        cmdarray = new String[] {
+                            "command.com", "/c", "start", "\"" + url + "\"" };
+                    } else {
+                        cmdarray = new String[] {
+                            "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" };
+                    }
+                } else if (LaunchUtil.isMacOS()) {
+                    cmdarray = new String[] { "open", url };
+                } else { // Linux, Solaris, etc.
+                    cmdarray = new String[] { "firefox", url };
+                }
+                try {
+                    Runtime.getRuntime().exec(cmdarray);
+                } catch (Exception e) {
+                    log.warning("Failed to open browser.", "cmdarray", cmdarray, e);
+                }
+            }
+
+            @Override
+            protected void exit (int exitCode) {
+                // if we're running the app in the same JVM, don't call System.exit, but do
+                // make double sure that the download window is closed.
+                if (invokeDirect()) {
+                    disposeContainer();
+                } else {
+                    System.exit(exitCode);
+                }
+            }
+
+            @Override
+            protected void fail (String message) {
+                super.fail(message);
+                // super.fail causes the UI to be created (if needed) on the next UI tick, so we
+                // want to wait until that happens before we attempt to redecorate the window
+                EventQueue.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        // if the frame was set to be undecorated, make window decoration available
+                        // to allow the user to close the window
+                        if (_frame != null && _frame.isUndecorated()) {
+                            _frame.dispose();
+                            Color bg = _frame.getBackground();
+                            if (bg != null && bg.getAlpha() < 255) {
+                                // decorated windows do not allow alpha backgrounds
+                                _frame.setBackground(
+                                    new Color(bg.getRed(), bg.getGreen(), bg.getBlue()));
+                            }
+                            _frame.setUndecorated(false);
+                            showContainer();
+                        }
+                    }
+                });
+            }
+
+            protected JFrame _frame;
+        };
+        app.start();
+        return app;
+    }
+}
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/MultipleGetdownRunning.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/MultipleGetdownRunning.java
new file mode 100644 (file)
index 0000000..5ac7449
--- /dev/null
@@ -0,0 +1,20 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.io.IOException;
+
+/**
+ * Thrown when it's detected that multiple instances of the same getdown installer are running.
+ */
+public class MultipleGetdownRunning extends IOException
+{
+    public MultipleGetdownRunning ()
+    {
+        super("m.another_getdown_running");
+    }
+
+}
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
new file mode 100644 (file)
index 0000000..2178273
--- /dev/null
@@ -0,0 +1,195 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JTextField;
+
+import com.samskivert.swing.GroupLayout;
+import com.samskivert.swing.Spacer;
+import com.samskivert.swing.VGroupLayout;
+import com.threerings.getdown.util.MessageUtil;
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Displays an interface with which the user can configure their proxy
+ * settings.
+ */
+public final class ProxyPanel extends JPanel implements ActionListener
+{
+    public ProxyPanel (Getdown getdown, ResourceBundle msgs)
+    {
+        _getdown = getdown;
+        _msgs = msgs;
+
+        setLayout(new VGroupLayout());
+        setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+        add(new SaneLabelField(get("m.configure_proxy")));
+        add(new Spacer(5, 5));
+
+        JPanel row = new JPanel(new GridLayout());
+        row.add(new SaneLabelField(get("m.proxy_host")), BorderLayout.WEST);
+        row.add(_host = new SaneTextField());
+        add(row);
+
+        row = new JPanel(new GridLayout());
+        row.add(new SaneLabelField(get("m.proxy_port")), BorderLayout.WEST);
+        row.add(_port = new SaneTextField());
+        add(row);
+
+        add(new Spacer(5, 5));
+
+        row = new JPanel(new GridLayout());
+        row.add(new SaneLabelField(get("m.proxy_auth_required")), BorderLayout.WEST);
+        _useAuth = new JCheckBox();
+        row.add(_useAuth);
+        add(row);
+
+        row = new JPanel(new GridLayout());
+        row.add(new SaneLabelField(get("m.proxy_username")), BorderLayout.WEST);
+        _username = new SaneTextField();
+        _username.setEnabled(false);
+        row.add(_username);
+        add(row);
+
+        row = new JPanel(new GridLayout());
+        row.add(new SaneLabelField(get("m.proxy_password")), BorderLayout.WEST);
+        _password = new SanePasswordField();
+        _password.setEnabled(false);
+        row.add(_password);
+        add(row);
+
+        _useAuth.addItemListener(new ItemListener() {
+            @Override public void itemStateChanged (ItemEvent event) {
+                boolean selected = (event.getStateChange() == ItemEvent.SELECTED);
+                _username.setEnabled(selected);
+                _password.setEnabled(selected);
+            }
+        });
+
+        add(new Spacer(5, 5));
+
+        row = GroupLayout.makeButtonBox(GroupLayout.CENTER);
+        JButton button;
+        row.add(button = new JButton(get("m.proxy_ok")));
+        button.setActionCommand("ok");
+        button.addActionListener(this);
+        row.add(button = new JButton(get("m.proxy_cancel")));
+        button.setActionCommand("cancel");
+        button.addActionListener(this);
+        add(row);
+    }
+
+    public void setProxy (String host, String port) {
+        if (host != null) {
+            _host.setText(host);
+        }
+        if (port != null) {
+            _port.setText(port);
+        }
+    }
+
+    // documentation inherited
+    @Override
+    public void addNotify ()
+    {
+        super.addNotify();
+        _host.requestFocusInWindow();
+    }
+
+    // documentation inherited
+    @Override
+    public Dimension getPreferredSize ()
+    {
+        // this is annoyingly hardcoded, but we can't just force the width
+        // or the JLabel will claim a bogus height thinking it can lay its
+        // text out all on one line which will booch the whole UI's
+        // preferred size
+        return new Dimension(500, 320);
+    }
+
+    // documentation inherited from interface
+    @Override
+    public void actionPerformed (ActionEvent e)
+    {
+        String cmd = e.getActionCommand();
+        if (cmd.equals("ok")) {
+            String user = null, pass = null;
+            if (_useAuth.isSelected()) {
+                user = _username.getText();
+                // we have to keep the proxy password around for every HTTP request, so having it
+                // in a char[] that gets zeroed out after use is not viable for this use case
+                pass = new String(_password.getPassword());
+            }
+            _getdown.configProxy(_host.getText(), _port.getText(), user, pass);
+        } else {
+            // they canceled, we're outta here
+            System.exit(0);
+        }
+    }
+
+    /** Used to look up localized messages. */
+    protected String get (String key)
+    {
+        // if this string is tainted, we don't translate it, instead we
+        // simply remove the taint character and return it to the caller
+        if (MessageUtil.isTainted(key)) {
+            return MessageUtil.untaint(key);
+        }
+        try {
+            return _msgs.getString(key);
+        } catch (MissingResourceException mre) {
+            log.warning("Missing translation message '" + key + "'.");
+            return key;
+        }
+    }
+
+    protected static class SaneLabelField extends JLabel {
+        public SaneLabelField(String message) { super(message); }
+        @Override public Dimension getPreferredSize () {
+            return clampWidth(super.getPreferredSize(), 200);
+        }
+    }
+    protected static class SaneTextField extends JTextField {
+        @Override public Dimension getPreferredSize () {
+            return clampWidth(super.getPreferredSize(), 150);
+        }
+    }
+    protected static class SanePasswordField extends JPasswordField {
+        @Override public Dimension getPreferredSize () {
+            return clampWidth(super.getPreferredSize(), 150);
+        }
+    }
+
+    protected static Dimension clampWidth (Dimension dim, int minWidth) {
+        dim.width = Math.max(dim.width, minWidth);
+        return dim;
+    }
+
+    protected Getdown _getdown;
+    protected ResourceBundle _msgs;
+
+    protected JTextField _host;
+    protected JTextField _port;
+    protected JCheckBox _useAuth;
+    protected JTextField _username;
+    protected JPasswordField _password;
+}
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
new file mode 100644 (file)
index 0000000..a36b5fa
--- /dev/null
@@ -0,0 +1,210 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.Authenticator;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+import ca.beq.util.win32.registry.RegistryKey;
+import ca.beq.util.win32.registry.RegistryValue;
+import ca.beq.util.win32.registry.RootKey;
+
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.spi.ProxyAuth;
+import com.threerings.getdown.util.Config;
+import com.threerings.getdown.util.ConnectionUtil;
+import com.threerings.getdown.util.LaunchUtil;
+import com.threerings.getdown.util.StringUtil;
+
+import static com.threerings.getdown.Log.log;
+
+public class ProxyUtil {
+
+    public static boolean autoDetectProxy (Application app)
+    {
+        String host = null, port = null;
+
+        // check for a proxy configured via system properties
+        if (System.getProperty("https.proxyHost") != null) {
+            host = System.getProperty("https.proxyHost");
+            port = System.getProperty("https.proxyPort");
+        }
+        if (StringUtil.isBlank(host) && System.getProperty("http.proxyHost") != null) {
+            host = System.getProperty("http.proxyHost");
+            port = System.getProperty("http.proxyPort");
+        }
+
+        // check the Windows registry
+        if (StringUtil.isBlank(host) && LaunchUtil.isWindows()) {
+            try {
+                String rhost = null, rport = null;
+                boolean enabled = false;
+                RegistryKey.initialize();
+                RegistryKey r = new RegistryKey(RootKey.HKEY_CURRENT_USER, PROXY_REGISTRY);
+                for (Iterator<?> iter = r.values(); iter.hasNext(); ) {
+                    RegistryValue value = (RegistryValue)iter.next();
+                    if (value.getName().equals("ProxyEnable")) {
+                        enabled = value.getStringValue().equals("1");
+                    }
+                    if (value.getName().equals("ProxyServer")) {
+                        String strval = value.getStringValue();
+                        int cidx = strval.indexOf(":");
+                        if (cidx != -1) {
+                            rport = strval.substring(cidx+1);
+                            strval = strval.substring(0, cidx);
+                        }
+                        rhost = strval;
+                    }
+                }
+                if (enabled) {
+                    host = rhost;
+                    port = rport;
+                } else {
+                    log.info("Detected no proxy settings in the registry.");
+                }
+            } catch (Throwable t) {
+                log.info("Failed to find proxy settings in Windows registry", "error", t);
+            }
+        }
+
+        // look for a proxy.txt file
+        if (StringUtil.isBlank(host)) {
+            String[] hostPort = loadProxy(app);
+            host = hostPort[0];
+            port = hostPort[1];
+        }
+
+        if (StringUtil.isBlank(host)) {
+            return false;
+        }
+
+        // yay, we found a proxy configuration, configure it in the app
+        initProxy(app, host, port, null, null);
+        return true;
+    }
+
+    public static boolean canLoadWithoutProxy (URL rurl)
+    {
+        log.info("Testing whether proxy is needed, via: " + rurl);
+        try {
+            // try to make a HEAD request for this URL (use short connect and read timeouts)
+            URLConnection conn = ConnectionUtil.open(Proxy.NO_PROXY, rurl, 5, 5);
+            if (conn instanceof HttpURLConnection) {
+                HttpURLConnection hcon = (HttpURLConnection)conn;
+                try {
+                    hcon.setRequestMethod("HEAD");
+                    hcon.connect();
+                    // make sure we got a satisfactory response code
+                    int rcode = hcon.getResponseCode();
+                    if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
+                        rcode == HttpURLConnection.HTTP_FORBIDDEN) {
+                        log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
+                    } else {
+                        return true;
+                    }
+                } finally {
+                    hcon.disconnect();
+                }
+            } else {
+                // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
+                return true;
+            }
+        } catch (IOException ioe) {
+            log.info("Failed to HEAD " + rurl + ": " + ioe);
+            log.info("We probably need a proxy, but auto-detection failed.");
+        }
+        return false;
+    }
+
+    public static void configProxy (Application app, String host, String port,
+                                    String username, String password) {
+        // save our proxy host and port in a local file
+        saveProxy(app, host, port);
+
+        // save our credentials via the SPI
+        if (!StringUtil.isBlank(username) && !StringUtil.isBlank(password)) {
+            ServiceLoader<ProxyAuth> loader = ServiceLoader.load(ProxyAuth.class);
+            Iterator<ProxyAuth> iterator = loader.iterator();
+            String appDir = app.getAppDir().getAbsolutePath();
+            while (iterator.hasNext()) {
+                iterator.next().saveCredentials(appDir, username, password);
+            }
+        }
+
+        // also configure them in the app
+        initProxy(app, host, port, username, password);
+    }
+
+    public static String[] loadProxy (Application app) {
+        File pfile = app.getLocalPath("proxy.txt");
+        if (pfile.exists()) {
+            try {
+                Config pconf = Config.parseConfig(pfile, Config.createOpts(false));
+                return new String[] { pconf.getString("host"), pconf.getString("port") };
+            } catch (IOException ioe) {
+                log.warning("Failed to read '" + pfile + "': " + ioe);
+            }
+        }
+        return new String[] { null, null};
+    }
+
+    public static void saveProxy (Application app, String host, String port) {
+        File pfile = app.getLocalPath("proxy.txt");
+        try (PrintStream pout = new PrintStream(new FileOutputStream(pfile))) {
+            if (!StringUtil.isBlank(host)) {
+                pout.println("host = " + host);
+            }
+            if (!StringUtil.isBlank(port)) {
+                pout.println("port = " + port);
+            }
+        } catch (IOException ioe) {
+            log.warning("Error creating proxy file '" + pfile + "': " + ioe);
+        }
+    }
+
+    public static void initProxy (Application app, String host, String port,
+                                  String username, String password)
+    {
+        // check whether we have saved proxy credentials
+        String appDir = app.getAppDir().getAbsolutePath();
+        ServiceLoader<ProxyAuth> loader = ServiceLoader.load(ProxyAuth.class);
+        Iterator<ProxyAuth> iter = loader.iterator();
+        ProxyAuth.Credentials creds = iter.hasNext() ? iter.next().loadCredentials(appDir) : null;
+        if (creds != null) {
+            username = creds.username;
+            password = creds.password;
+        }
+        boolean haveCreds = !StringUtil.isBlank(username) && !StringUtil.isBlank(password);
+
+        int pport = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port);
+        log.info("Using proxy", "host", host, "port", pport, "haveCreds", haveCreds);
+        app.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pport));
+
+        if (haveCreds) {
+            final String fuser = username;
+            final char[] fpass = password.toCharArray();
+            Authenticator.setDefault(new Authenticator() {
+                @Override protected PasswordAuthentication getPasswordAuthentication () {
+                    return new PasswordAuthentication(fuser, fpass);
+                }
+            });
+        }
+    }
+
+    protected static final String PROXY_REGISTRY =
+        "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
+}
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
new file mode 100644 (file)
index 0000000..d3aa2bd
--- /dev/null
@@ -0,0 +1,132 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.awt.Image;
+import java.util.List;
+
+import static com.threerings.getdown.Log.log;
+
+public final class RotatingBackgrounds
+{
+    public interface ImageLoader {
+        /** Loads and returns the image with the supplied path. */
+        public Image loadImage (String path);
+    }
+
+    /**
+     * Creates a placeholder if there are no images. Just returns null from getImage every time.
+     */
+    public RotatingBackgrounds ()
+    {
+        makeEmpty();
+    }
+
+    /** Creates a single image background. */
+    public RotatingBackgrounds (Image background)
+    {
+        percentages = new int[] { 0 };
+        minDisplayTime = new int[] { 0 };
+        images = new Image[] { background };
+        errorImage = images[0];
+    }
+
+    /**
+     * Create a sequence of images to be rotated through from <code>backgrounds</code>.
+     *
+     * Each String in backgrounds should be the path to the image, a semicolon, and the minimum
+     * amount of time to display the image in seconds. Each image will be active for an equal
+     * percentage of the download process, unless one hasn't been active for its minimum display
+     * time when the next should be shown. In that case, it's left up until its been there for its
+     * minimum display time and then the next one gets to come up.
+     */
+    public RotatingBackgrounds (List<String> backgrounds, String errorBackground, ImageLoader loader)
+    {
+        percentages = new int[backgrounds.size()];
+        minDisplayTime = new int[backgrounds.size()];
+        images = new Image[backgrounds.size()];
+        for (int ii = 0; ii < backgrounds.size(); ii++) {
+            String background = backgrounds.get(ii);
+            String[] pieces = background.split(";");
+            if (pieces.length != 2) {
+                log.warning("Unable to parse background image '" + background + "'");
+                makeEmpty();
+                return;
+            }
+            images[ii] = loader.loadImage(pieces[0]);
+            try {
+                minDisplayTime[ii] = Integer.parseInt(pieces[1]);
+            } catch (NumberFormatException e) {
+                log.warning("Unable to parse background image display time '" + background + "'");
+                makeEmpty();
+                return;
+            }
+            percentages[ii] = (int)((ii/(float)backgrounds.size()) * 100);
+        }
+        if (errorBackground == null) {
+            errorImage = images[0];
+        } else {
+            errorImage = loader.loadImage(errorBackground);
+        }
+    }
+
+    /**
+     * @return the image to display at the given progress or null if there aren't any.
+     */
+    public Image getImage (int progress)
+    {
+        if (images.length == 0) {
+            return null;
+        }
+        long now = System.currentTimeMillis();
+        if (current != images.length - 1
+            && (current == -1 || (progress >= percentages[current + 1] &&
+                    (now - currentDisplayStart) / 1000 > minDisplayTime[current]))) {
+            current++;
+            currentDisplayStart = now;
+        }
+        return images[current];
+    }
+
+    /**
+     * Returns the image to display if an error has caused getdown to fail.
+     */
+    public Image getErrorImage ()
+    {
+        return errorImage;
+    }
+
+    /**
+     * @return the number of images in this RotatingBackgrounds
+     */
+    public int getNumImages() {
+        return images.length;
+    }
+
+    protected void makeEmpty ()
+    {
+        percentages = new int[] {};
+        minDisplayTime = new int[] {};
+        images = new Image[] {};
+    }
+
+    /** Time at which the currently displayed image was first displayed in millis. */
+    protected long currentDisplayStart;
+
+    /** The index of the currently displayed image or -1 if we haven't displayed any. */
+    protected int current = -1;
+
+    protected Image[] images;
+
+    /** The image to display if getdown has failed due to an error. */
+    protected Image errorImage;
+
+    /** Percentage at which each image should be displayed. */
+    protected int[] percentages;
+
+    /** Time to show each image in seconds. */
+    protected int[] minDisplayTime;
+}
diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
new file mode 100644 (file)
index 0000000..99f44ca
--- /dev/null
@@ -0,0 +1,396 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.ImageObserver;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import javax.swing.JComponent;
+import javax.swing.Timer;
+
+import com.samskivert.swing.Label;
+import com.samskivert.swing.LabelStyleConstants;
+import com.samskivert.swing.util.SwingUtil;
+import com.samskivert.util.Throttle;
+
+import com.threerings.getdown.data.Application.UpdateInterface;
+import com.threerings.getdown.util.MessageUtil;
+import com.threerings.getdown.util.Rectangle;
+import com.threerings.getdown.util.StringUtil;
+
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Displays download and patching status.
+ */
+public final class StatusPanel extends JComponent
+    implements ImageObserver
+{
+    public StatusPanel (ResourceBundle msgs)
+    {
+        _msgs = msgs;
+
+        // Add a bit of "throbbing" to the display by updating the number of dots displayed after
+        // our status. This lets users know things are still working.
+        _timer = new Timer(1000,
+            new ActionListener() {
+                public void actionPerformed (ActionEvent event) {
+                    if (_status != null && !_displayError) {
+                        _statusDots = (_statusDots % 3) + 1; // 1, 2, 3, 1, 2, 3, etc.
+                        updateStatusLabel();
+                    }
+                }
+            });
+    }
+
+    public void init (UpdateInterface ifc, RotatingBackgrounds bg, Image barimg)
+    {
+        _ifc = ifc;
+        _bg = bg;
+        Image img = _bg.getImage(_progress);
+        int width = img == null ? -1 : img.getWidth(this);
+        int height = img == null ? -1 : img.getHeight(this);
+        if (width == -1 || height == -1) {
+            Rectangle bounds = ifc.progress.union(ifc.status);
+            // assume the x inset defines the frame padding; add it on the left, right, and bottom
+            _psize = new Dimension(bounds.x + bounds.width + bounds.x,
+                                   bounds.y + bounds.height + bounds.x);
+        } else {
+            _psize = new Dimension(width, height);
+        }
+        _barimg = barimg;
+        invalidate();
+    }
+
+    @Override
+    public boolean imageUpdate (Image img, int infoflags, int x, int y, int width, int height)
+    {
+        boolean updated = false;
+        if ((infoflags & WIDTH) != 0) {
+            _psize.width = width;
+            updated = true;
+        }
+        if ((infoflags & HEIGHT) != 0) {
+            _psize.height = height;
+            updated = true;
+        }
+        if (updated) {
+            invalidate();
+            setSize(_psize);
+            getParent().setSize(_psize);
+        }
+        return (infoflags & ALLBITS) == 0;
+    }
+
+    /**
+     * Adjusts the progress display to the specified percentage.
+     */
+    public void setProgress (int percent, long remaining)
+    {
+        boolean needsRepaint = false;
+
+        // maybe update the progress label
+        if (_progress != percent) {
+            _progress = percent;
+            if (!_ifc.hideProgressText) {
+                String msg = MessageFormat.format(get("m.complete"), percent);
+                _newplab = createLabel(msg, new Color(_ifc.progressText, true));
+            }
+            needsRepaint = true;
+        }
+
+        // maybe update the remaining label
+        if (remaining > 1) {
+            // skip this estimate if it's been less than a second since our last one came in
+            if (!_rthrottle.throttleOp()) {
+                _remain[_ridx++%_remain.length] = remaining;
+            }
+
+            // smooth the remaining time by taking the trailing average of the last four values
+            remaining = 0;
+            int values = Math.min(_ridx, _remain.length);
+            for (int ii = 0; ii < values; ii++) {
+                remaining += _remain[ii];
+            }
+            remaining /= values;
+
+            if (!_ifc.hideProgressText) {
+                // now compute our display value
+                int minutes = (int)(remaining / 60), seconds = (int)(remaining % 60);
+                String remstr = minutes + ":" + ((seconds < 10) ? "0" : "") + seconds;
+                String msg = MessageFormat.format(get("m.remain"), remstr);
+                _newrlab = createLabel(msg, new Color(_ifc.statusText, true));
+            }
+            needsRepaint = true;
+
+        } else if (_rlabel != null || _newrlab != null) {
+            _rthrottle = new Throttle(1, 1000);
+            _ridx = 0;
+            _newrlab = _rlabel = null;
+            needsRepaint = true;
+        }
+
+        if (needsRepaint) {
+            repaint();
+        }
+    }
+
+    /**
+     * Displays the specified status string.
+     */
+    public void setStatus (String status, boolean displayError)
+    {
+        _status = xlate(status);
+        _displayError = displayError;
+        updateStatusLabel();
+    }
+
+    /**
+     * Stop the throbbing.
+     */
+    public void stopThrob ()
+    {
+        _timer.stop();
+        _statusDots = 3;
+        updateStatusLabel();
+    }
+
+    @Override
+    public void addNotify ()
+    {
+        super.addNotify();
+        _timer.start();
+    }
+
+    @Override
+    public void removeNotify ()
+    {
+        _timer.stop();
+        super.removeNotify();
+    }
+
+    // documentation inherited
+    @Override
+    public void paintComponent (Graphics g)
+    {
+        super.paintComponent(g);
+        Graphics2D gfx = (Graphics2D)g;
+
+        // attempt to draw a background image...
+        Image img;
+        if (_displayError) {
+            img = _bg.getErrorImage();
+        } else {
+            img = _bg.getImage(_progress);
+        }
+        if (img != null) {
+            gfx.drawImage(img, 0, 0, this);
+        }
+
+        Object oalias = SwingUtil.activateAntiAliasing(gfx);
+
+        // if we have new labels; lay them out
+        if (_newlab != null) {
+            _newlab.layout(gfx);
+            _label = _newlab;
+            _newlab = null;
+        }
+        if (_newplab != null) {
+            _newplab.layout(gfx);
+            _plabel = _newplab;
+            _newplab = null;
+        }
+        if (_newrlab != null) {
+            _newrlab.layout(gfx);
+            _rlabel = _newrlab;
+            _newrlab = null;
+        }
+
+        if (_barimg != null) {
+            gfx.setClip(_ifc.progress.x, _ifc.progress.y,
+                        _progress * _ifc.progress.width / 100,
+                        _ifc.progress.height);
+            gfx.drawImage(_barimg, _ifc.progress.x, _ifc.progress.y, null);
+            gfx.setClip(null);
+        } else {
+            gfx.setColor(new Color(_ifc.progressBar, true));
+            gfx.fillRect(_ifc.progress.x, _ifc.progress.y,
+                         _progress * _ifc.progress.width / 100,
+                         _ifc.progress.height);
+        }
+
+        if (_plabel != null) {
+            int xmarg = (_ifc.progress.width - _plabel.getSize().width)/2;
+            int ymarg = (_ifc.progress.height - _plabel.getSize().height)/2;
+            _plabel.render(gfx, _ifc.progress.x + xmarg, _ifc.progress.y + ymarg);
+        }
+
+        if (_label != null) {
+            _label.render(gfx, _ifc.status.x, getStatusY(_label));
+        }
+
+        if (_rlabel != null) {
+            // put the remaining label at the end of the status area. This could be dangerous
+            // but I think the only time we would display it is with small statuses.
+            int x = _ifc.status.x + _ifc.status.width - _rlabel.getSize().width;
+            _rlabel.render(gfx, x, getStatusY(_rlabel));
+        }
+
+        SwingUtil.restoreAntiAliasing(gfx, oalias);
+    }
+
+    // documentation inherited
+    @Override
+    public Dimension getPreferredSize ()
+    {
+        return _psize;
+    }
+
+    /**
+     * Update the status label.
+     */
+    protected void updateStatusLabel ()
+    {
+        String status = _status;
+        if (!_displayError) {
+            for (int ii = 0; ii < _statusDots; ii++) {
+                status += " .";
+            }
+        }
+        _newlab = createLabel(status, new Color(_ifc.statusText, true));
+        // set the width of the label to the width specified
+        int width = _ifc.status.width;
+        if (width == 0) {
+            // unless we had trouble reading that width, in which case use the entire window
+            width = getWidth();
+        }
+        // but the window itself might not be initialized and have a width of 0
+        if (width > 0) {
+            _newlab.setTargetWidth(width);
+        }
+        repaint();
+    }
+
+    /**
+     * Get the y coordinate of a label in the status area.
+     */
+    protected int getStatusY (Label label)
+    {
+        // if the status region is higher than the progress region, we
+        // want to align the label with the bottom of its region
+        // rather than the top
+        if (_ifc.status.y > _ifc.progress.y) {
+            return _ifc.status.y;
+        }
+        return _ifc.status.y + (_ifc.status.height - label.getSize().height);
+    }
+
+    /**
+     * Create a label, taking care of adding the shadow if needed.
+     */
+    protected Label createLabel (String text, Color color)
+    {
+        Label label = new Label(text, color, FONT);
+        if (_ifc.textShadow != 0) {
+            label.setAlternateColor(new Color(_ifc.textShadow, true));
+            label.setStyle(LabelStyleConstants.SHADOW);
+        }
+        return label;
+    }
+
+    /** Used by {@link #setStatus}. */
+    protected String xlate (String compoundKey)
+    {
+        // to be more efficient about creating unnecessary objects, we
+        // do some checking before splitting
+        int tidx = compoundKey.indexOf('|');
+        if (tidx == -1) {
+            return get(compoundKey);
+
+        } else {
+            String key = compoundKey.substring(0, tidx);
+            String argstr = compoundKey.substring(tidx+1);
+            String[] args = argstr.split("\\|");
+            // unescape and translate the arguments
+            for (int i = 0; i < args.length; i++) {
+                // if the argument is tainted, do no further translation
+                // (it might contain |s or other fun stuff)
+                if (MessageUtil.isTainted(args[i])) {
+                    args[i] = MessageUtil.unescape(MessageUtil.untaint(args[i]));
+                } else {
+                    args[i] = xlate(MessageUtil.unescape(args[i]));
+                }
+            }
+            return get(key, args);
+        }
+    }
+
+    /** Used by {@link #setStatus}. */
+    protected String get (String key, String[] args)
+    {
+        String msg = get(key);
+        if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
+        return key + String.valueOf(Arrays.asList(args));
+    }
+
+    /** Used by {@link #setStatus}, and {@link #setProgress}. */
+    protected String get (String key)
+    {
+        // if we have no _msgs that means we're probably recovering from a
+        // failure to load the translation messages in the first place, so
+        // just give them their key back because it's probably an english
+        // string; whee!
+        if (_msgs == null) {
+            return key;
+        }
+
+        // if this string is tainted, we don't translate it, instead we
+        // simply remove the taint character and return it to the caller
+        if (MessageUtil.isTainted(key)) {
+            return MessageUtil.untaint(key);
+        }
+        try {
+            return _msgs.getString(key);
+        } catch (MissingResourceException mre) {
+            log.warning("Missing translation message '" + key + "'.");
+            return key;
+        }
+    }
+
+    protected Image _barimg;
+    protected RotatingBackgrounds _bg;
+    protected Dimension _psize;
+
+    protected ResourceBundle _msgs;
+
+    protected int _progress = -1;
+    protected String _status;
+    protected int _statusDots = 1;
+    protected boolean _displayError;
+    protected Label _label, _newlab;
+    protected Label _plabel, _newplab;
+    protected Label _rlabel, _newrlab;
+
+    protected UpdateInterface _ifc;
+    protected Timer _timer;
+
+    protected long[] _remain = new long[4];
+    protected int _ridx;
+    protected Throttle _rthrottle = new Throttle(1, 1000L);
+
+    protected static final Font FONT = new Font("SansSerif", Font.BOLD, 12);
+}
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties
new file mode 100644 (file)
index 0000000..19b2999
--- /dev/null
@@ -0,0 +1,110 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Abort installation?
+m.abort_confirm = <html>Are you sure you want to stop installation? \
+  You can resume at a later time by running the application again.</html>
+m.abort_ok = Quit
+m.abort_cancel = Continue installation
+
+m.detecting_proxy = Trying to auto-detect proxy settings
+
+m.configure_proxy = <html>We were unable to connect to the application server to download data. \
+  <p> Please make sure that no virus scanner or firewall is blocking network communicaton with \
+  the server. \
+  <p> Your computer may access the Internet through a proxy and we were unable to automatically \
+  detect your proxy settings. If you know your proxy settings, you can enter them below.</html>
+
+m.proxy_extra = <html>If you are sure that you don't use a proxy then \
+  perhaps there is a temporary Internet outage that is preventing us from \
+  communicating with the servers. In this case, you can cancel and try \
+  installing again later.</html>
+
+m.proxy_host = Proxy IP
+m.proxy_port = Proxy port
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = OK
+m.proxy_cancel = Cancel
+
+m.downloading_java = Downloading Java Virtual Machine
+m.unpacking_java = Unpacking Java Virtual Machine
+
+m.resolving = Resolving downloads
+m.downloading = Downloading data
+m.failure = Download failed: {0}
+
+m.checking = Checking for update
+m.validating = Validating
+m.patching = Patching
+m.launching = Launching
+
+m.patch_notes = Patch Notes
+m.play_again = Play Again
+
+m.complete = {0}% complete
+m.remain = {0} remaining
+
+m.updating_metadata = Downloading control files
+
+m.init_failed = Our configuration file is missing or corrupt. Attempting \
+  to download a new copy...
+
+m.java_download_failed = We were unable to automatically download the \
+  necessary version of Java for your computer.\n\n\
+  Please go to www.java.com and download the latest version of \
+  Java, then try running the application again.
+
+m.java_unpack_failed = We were unable to unpack an updated version of \
+  Java. Please make sure you have at least 100 MB of free space on your \
+  harddrive and try running the application again.\n\n\
+  If that does not solve the problem, go to www.java.com and download and \
+  install the latest version of Java and try again.
+
+m.unable_to_repair = We were unable to download the necessary files after \
+  five attempts. You can try running the application again, but if it \
+  fails you may need to uninstall and reinstall.
+
+m.unknown_error = The application has failed to launch due to some strange \
+  error from which we could not recover. Please visit\n{0} for information on \
+  how to recover.
+m.init_error = The application has failed to launch due to the following \
+  error:\n{0}\n\nPlease visit\n{1} for \
+  information on how to handle such problems.
+
+m.readonly_error = The directory in which this application is installed: \
+  \n{0}\nis read-only. Please install the application into a directory where \
+  you have write access.
+
+m.missing_resource = The application has failed to launch due to a missing \
+  resource:\n{0}\n\nPlease visit\n{1} for information on how to handle such \
+  problems.
+
+m.insufficient_permissions_error = You did not accept this application's \
+ digital signature. If you want to run the application, you will need to accept \
+ its digital signature.\n\nTo do so, you will need to quit your web browser, \
+ restart it, and return to this web page to relaunch the application. When the \
+ security dialog is shown, click the button to accept the digital signature \
+ and grant this application the privileges it needs to run.
+
+m.corrupt_digest_signature_error = We couldn't verify the application's digital \
+ signature.\nPlease check that you are launching the application from\nthe \
+ correct website.
+
+m.default_install_error = the support section of the website
+
+m.another_getdown_running = Multiple instances of this application's \
+ installer are running. This one will stop and let another complete.
+
+m.applet_stopped = Getdown's applet was told to stop working.
+
+# application/digest errors
+m.missing_appbase = The configuration file is missing the 'appbase'.
+m.invalid_version = The configuration file specifies an invalid version.
+m.invalid_appbase = The configuration file specifies an invalid 'appbase'.
+m.missing_class = The configuration file is missing the application class.
+m.missing_code = The configuration file specifies no code resources.
+m.invalid_digest_file = The digest file is invalid.
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties
new file mode 100644 (file)
index 0000000..8e36835
--- /dev/null
@@ -0,0 +1,116 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Installation abbrechen?
+m.abort_confirm = <html>Bist du sicher, dass du die Installation abbrechen \
+m\u00f6chtest? \
+  Du kannst sp\u00e4ter fortfahren, indem du die Anwendung erneut \
+ausf\u00fchrst.</html>
+m.abort_ok = Beenden
+m.abort_cancel = Installation fortsetzen
+
+m.detecting_proxy = Versuche Proxy-Einstellungen automatisch zu ermitteln
+
+m.configure_proxy = <html>Es konnte keine Verbindung zum Applikations-Server aufgebaut werden. \
+  <p>Bitte kontrollieren Sie die Proxyeinstellungen und stellen Sie sicher, dass keine lokal oder \
+  im Netzwerk betriebene Sicherheitsanwendung (Virenscanner, Firewall, etc.) die Kommunikation \
+  mit dem Server blockiert.<br> \
+  Wenn kein Proxy verwendet werden soll, l\u00f6schen Sie bitte alle Eintr\u00e4ge in den unten \
+  stehenden Feldern und klicken sie auf OK.</html>
+
+m.proxy_extra = <html>Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \
+  an Ihren Administrator.</html>
+
+m.proxy_host = Proxy-Adresse
+m.proxy_port = Proxy-Port
+m.proxy_username = Benutzername
+m.proxy_password = Passwort
+m.proxy_auth_required = Authentisierung erforderlich
+m.proxy_ok = OK
+m.proxy_cancel = Abbrechen
+
+m.downloading_java = Lade Java Virtual Machine herunter
+m.unpacking_java = Entpacke Java Virtual Machine
+
+m.resolving = Bereite Download vor
+m.downloading = Lade Daten herunter
+m.failure = Download fehlgeschlagen: {0}
+
+m.checking = Suche nach Updates
+m.validating = Validiere Download
+m.patching = Patche
+m.launching = Starte
+
+m.patch_notes = Patchnotes
+
+m.complete = {0}% abgeschlossen
+m.remain = {0} \u00fcbrig
+
+m.updating_metadata = Lade Steuerungsdateien herunter
+
+m.init_failed = Unsere Konfigurationsdatei fehlt oder ist besch\u00e4digt. \
+Versuche, eine neue Kopie herunterzuladen...
+
+m.java_download_failed = Wir konnten die notwendige Javaversion f\u00fcr deinen \
+Computer nicht automatisch herunterladen. \n\n \
+Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \
+Anwendung erneut starten.
+
+m.java_unpack_failed = Wir konnten die aktualisierte Javaversion nicht \
+entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \
+Festplatte frei sind und versuche dann die Anwendung erneut zu \
+starten.\n\n\ \
+Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \
+Javaversion herunterladen und installieren und dann erneut versuchen.
+
+m.unable_to_repair = Wir konnten die notwendigen Dateien nach 5 Versuchen \
+nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \
+aber wenn dies erneut fehlschl\u00e4gt, musst du die Anwendung deinstallieren \
+und erneut installieren.
+
+m.unknown_error = Die Anwendung konnte wegen eines unbekannten Fehlers \
+nicht gestartet werden. Bitte auf \n{0} weiterlesen.
+
+m.init_error = Die Anwendung konnte wegen folgendem Fehler nicht gestartet \
+werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \
+solchen Problemen vorzugehen ist.
+
+m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist: \
+ \n{0}\nist nicht schreibberechtigt. Bitte in ein Verzeichnis mit \
+Schreibzugriff installieren.
+
+m.missing_resource = Die Anwendung konnte nicht gestartet werden, da die \
+folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \
+weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist.
+
+m.insufficient_permissions_error = Du hast die digitale Signatur dieser \
+Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \
+musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \
+deinen Browser beenden, neu starten und erneut die Anwendung von dieser \
+Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \
+digitale Signatur akzeptieren, um der Anwendung die n\u00f6tigen Rechte zu \
+geben, die sie braucht, um zu laufen.
+
+m.corrupt_digest_signature_error = Wir konnten die digitale Signatur \
+dieser Anwendung nicht \u00fcberpr\u00fcfen.\nBitte \u00fcberpr\u00fcfe, ob du die Anwendung \
+von der richtigen Webseite aus startest.
+
+m.default_install_error = der Support-Webseite
+
+m.another_getdown_running = Diese Installationsanwendung l\u00e4uft in mehreren \
+Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \
+Vorgang erledigen lassen.
+
+m.applet_stopped = Die Anwendung wurde beendet.
+
+
+# application/digest errors
+m.missing_appbase = In der Konfigurationsdatei fehlt die 'appbase'.
+m.invalid_version = In der Konfigurationsdatei steht die falsche Version.
+m.invalid_appbase = In der Konfigurationsdatei steht die falsche 'appbase'.
+m.missing_class = In der Konfigurationsdatei fehlt die Anwendungsklasse.
+m.missing_code = Die Konfigurationsdatei enth\u00e4lt keine Codequellen.
+m.invalid_digest_file = Die Hashwertedatei ist ung\u00fcltig.
+
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties
new file mode 100644 (file)
index 0000000..609b025
--- /dev/null
@@ -0,0 +1,115 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = \u00bfCancelar la instalaci\u00f3n?
+m.abort_confirm = <html>\u00bfEst\u00e1s seguro de querer cancelar la instalaci\u00f3n? \
+  Puedes continuarla despu\u00e9s si corres de nuevo la aplicaci\u00f3n.</html>
+m.abort_ok = Cancelar
+m.abort_cancel = Continuar la instalaci\u00f3n
+
+m.detecting_proxy = Detectando autom\u00e1ticamente la configuraci\u00f3n proxy
+
+m.configure_proxy = <html>No ha sido posible conectar con nuestros servidores para \
+  descargar los datos del juego. \
+  <ul><li> Si el cortafuegos de Windows o Norton Internet Security tiene instrucciones \
+  de bloquear <code>javaw.exe</code> no podemos descargar el juego. Necesitar\u00e1s \
+  permitir que <code>javaw.exe</code> tenga acceso al Internet. Puedes intentar \
+  correr el juego de nuevo, pero es posible que debas dar permisos a javaw.exe en la \
+  configuraci\u00f3n de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ).</ul> \
+  <p> Es posible que tu computadora tenga acceso al Internet por medio de un proxy por lo que \
+  no ha sido posible detectar autom\u00e1ticamente tu configuraci\u00f3n. Si conoces tu \
+  configuraci\u00f3n proxy, puedes anotarla abajo.</html>
+
+m.proxy_extra = <html>Si est\u00e1s seguro de que no tienes un proxy entonces \
+  tal vez exista un falla temporal en el Internet que est\u00e1 evitando que podamos \
+  comunicarnos con los servidores. En este caso, puedes cancelar e intentar \
+  instalarla de nuevo m\u00e1s tarde.</html>
+
+m.proxy_host = IP proxy
+m.proxy_port = Puerto proxy
+m.proxy_username = Nombre de usuario
+m.proxy_password = Contrase\u00f1a
+m.proxy_auth_required = Autenticacion requerida
+m.proxy_ok = OK
+m.proxy_cancel = Cancelar
+
+m.downloading_java = Descargando Java Virtual Machine
+m.unpacking_java = Desempacando Java Virtual Machine
+
+m.resolving = Resolviendo descarga
+m.downloading = Descargando datos
+m.failure = Descarga fallida: {0}
+
+m.checking = Buscando actualizaciones
+m.validating = Validando
+m.patching = Parchando
+m.launching = Lanzando
+
+m.patch_notes = Notas del parche
+
+m.complete = {0}% completado
+m.remain = {0} restante
+
+m.updating_metadata = Descargando los archivos de control
+
+m.init_failed = Un archivo de configuraci\u00f3n est\u00e1 faltante o est\u00e1 corrupto. Intentando \
+  descargar una nueva copia...
+
+m.java_download_failed = No ha sido posible descargar autom\u00e1ticamente la \
+  versi\u00f3n de Java necesaria para tu computadora.\n\n\
+  Por favor ve a www.java.com y descarga la \u00faltima versi\u00f3n de \
+  Java, despu\u00e9s intenta correr de nuevo la aplicaci\u00f3n.
+
+m.java_unpack_failed = No ha sido posible desempacar una versi\u00f3n actualizada de \
+  Java. Por favor aseg\u00farate de tener al menos 100 MB de espacio libre en tu \
+  disco duro e intenta correr de nuevo la aplicaci\u00f3n.\n\n\
+  Si eso no soluciona el problema, ve a www.java.com y descarga e \
+  instala la \u00faltima versi\u00f3n de Java e intenta de nuevo.
+
+m.unable_to_repair = No ha sido posible descargar los archivos necesarios despu\u00e9s de \
+  cinco intentos. Puedes intentar correr de nuevo la aplicaci\u00f3n, pero si falla \
+  de nuevo podr\u00edas necesitar desinstalar y reinstalar.
+
+m.unknown_error = La aplicaci\u00f3n no ha podido iniciar debido a un extra\u00f1o \
+  error del que no se pudo recobrar. Por favor visita\n{0} para ver informaci\u00f3n acerca \
+  de como recuperarla.
+m.init_error = La aplicaci\u00f3n no ha podido iniciar debido al siguiente \
+  error:\n{0}\n\nPor favor visita\n{1} para \
+  ver informaci\u00f3n acerca de como manejar ese tipo de problemas.
+
+m.readonly_error = El directorio en el que esta aplicaci\u00f3n est\u00e1 instalada: \
+  \n{0}\nes solo lectura. Por favor instala la aplicaci\u00f3n en un directorio en el cual \
+  tengas acceso de escritura.
+
+m.missing_resource = La aplicaci\u00f3n no ha podido iniciar debido a un recurso \
+  faltante:\n{0}\n\nPor favor visita\n{1} para informaci\u00f3n acerca de como solucionar \
+  estos problemas.
+
+m.insufficient_permissions_error = No aceptaste la firma digital de \
+ esta aplicaci\u00f3n. Si quieres correr la aplicaci\u00f3n, necesitas aceptar \
+ su firma digital.\n\nPara hacerlo, necesitas cerrar tu navegador, \
+ reiniciarlo, y regresar a esta p\u00e1gina web para reiniciar la aplicaci\u00f3n. Cuando se muestre \
+ el di\u00e1logo de seguridad, haz clic en el bot\u00f3n para aceptar la firmar digital \
+ y otorgar a esta aplicaci\u00f3n los privilegios que necesita para correr.
+
+m.corrupt_digest_signature_error = No pudimos verificar la firma digital \
+ de la aplicaci\u00f3n.\nPor favor revisa que est\u00e9s lanzando la aplicaci\u00f3n desde\nel \
+ sitio web correcto.
+
+m.default_install_error = la secci\u00f3n de asistencia de este sitio web
+
+m.another_getdown_running = Est\u00e1n corriendo m\u00faltiples instancias de \
+ este instalador.  Este se detendr\u00e1 para permitir que otra contin\u00fae.
+
+m.applet_stopped = Se le dijo al applet de Getdown que dejara de trabajar.
+
+# application/digest errors
+m.missing_appbase = Al archivo de configuraci\u00f3n le falta el 'appbase'.
+m.invalid_version = El archivo de configuraci\u00f3n especifica una versi\u00f3n no v\u00e1lida.
+m.invalid_appbase = El archivo de configuraci\u00f3n especifica un 'appbase' no v\u00e1lido.
+m.missing_class = Al archivo de configuraci\u00f3n le falta la clase de aplicaci\u00f3n.
+m.missing_code = El archivo de configuraci\u00f3n especifica que no hay recursos de c\u00f3digo.
+m.invalid_digest_file = El archivo digest no es v\u00e1lido.
+
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties
new file mode 100644 (file)
index 0000000..3666204
--- /dev/null
@@ -0,0 +1,111 @@
+#
+# $Id: messages.properties 485 2012-03-08 22:05:30Z ray.j.greenwell $
+#
+# Getdown translation messages
+
+m.abort_title = Annuler l'installation?
+m.abort_confirm =<html>\u00cates-vous s\u00fbr de vouloir annuler l'installation? \
+   Vous pourrez reprendre l'installation en ex\u00e9cutant l'application de nouveau.</html>
+m.abort_ok = Quitter
+m.abort_cancel = Continuer l'installation
+
+m.detecting_proxy = D\u00e9tection automatique des r\u00e9glages proxy
+
+m.configure_proxy =<html>Connexion au serveur impossible. \
+   <ul><li>  Veuillez v\u00e9rifier que <code>javaw.exe</code> n'est bloqu\u00e9 \
+   par aucun pare-feu ou antivirus. \
+   Vous pouvez vous rendre sur la configuration du pare-feu windows via \
+   (D\u00e9marrer ->  Panneau de Configuration ->  Pare-feu Windows ).</ul> \
+   <p>  Il est \u00e9galement possible que vous soyez derri\u00e8re un proxy que l'application \
+   est incapable de d\u00e9tecter automatiquement. \
+   Si tel est le cas, veuillez saisir les r\u00e9glages proxy ci-dessous.</html>
+
+m.proxy_extra =<html>Si vous \u00eates certain de ne pas utiliser de proxy, il est \
+   possible qu'une interruption temporaire de la connexion internet emp\u00fbche la \
+   communication avec les serveurs. Dans ce cas, vous pouvez relancer \
+   l'installation ult\u00e9rieurement.</html>
+
+m.proxy_host = Proxy IP
+m.proxy_port = Proxy port
+m.proxy_username = Nom d'utilisateur
+m.proxy_password = Mot de passe
+m.proxy_auth_required = Identification requise
+m.proxy_ok = OK
+m.proxy_cancel = Annuler
+
+m.downloading_java = T\u00e9l\u00e9chargement en cours de la Machine Virtuelle Java
+m.unpacking_java = D\u00e9compression en cours de la Machine Virtuelle Java
+
+m.resolving = R\u00e9solution des t\u00e9l\u00e9chargements en cours
+m.downloading = T\u00e9l\u00e9chargement des donn\u00e9es en cours
+m.failure = \u00c9chec du t\u00e9l\u00e9chargement: {0}
+
+m.checking = V\u00e9rification de la mise-\u00e0-jour en cours
+m.validating = Validation en cours
+m.patching = Modification en cours
+m.launching = Lancement en cours
+
+m.patch_notes = Notes de mise-\u00e0-jour
+
+m.complete = Complet \u00e0 {0}%
+m.remain = {0} restant
+
+m.updating_metadata = T\u00e9l\u00e9chargement des fichiers de contr\u00f4les en cours
+
+m.init_failed = Notre fichier de configuration est perdu ou corrompu. T\u00e9l\u00e9chargement \
+   d'une nouvelle copie en cours ...
+
+m.java_download_failed = Impossible de t\u00e9l\u00e9charger automatiquement la \
+   version de Java n\u00e9cessaire.\n\n\
+   Veuillez vous rendre sur www.java.com et t\u00e9l\u00e9charger et installer la version \
+   la plus r\u00e9cente de Java, avant d'ex\u00e9cuter l'application \u00e0 nouveau.
+
+m.java_unpack_failed = Impossible de d\u00e9compresser la version de \
+   Java n\u00e9cessaire. Veuillez v\u00e9rifier que vous avez au moins 100 MB d'espace libre \
+   sur votre disque dur puis tenter d'ex\u00e9cuter l'application \u00e0 nouveau.\n\n\
+   Si le probl\u00e8me persiste, rendez vous www.java.com et t\u00e9l\u00e9chargez et \
+   installez la version plus r\u00e9cente de Java puis essayez de nouveau.
+
+m.unable_to_repair = Impossible de t\u00e9l\u00e9charger les fichiers n\u00e9cessaires apr\u00e8s \
+   cinq tentatives. Vous pouvez tenter d'ex\u00e9cuter l'application \u00e0 nouveau, mais il est \
+   possible qu'une d\u00e9sinstallation / r\u00e9installation soit n\u00e9cessaire.
+
+m.unknown_error = Une erreur inconnue a fait \u00e9chouer le lancement de l'application. \
+   Veuillez visiter\n{0} pour plus d'informations.
+m.init_error = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause de l'erreur \
+   suivante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
+
+m.readonly_error = Le r\u00e9pertoire d'installation de cette application: \
+   \n{0}\nest en lecture seule. Veuillez installer l'application dans un r\u00e9pertoire avec \
+   un acc\u00e8s en \u00e9criture.
+
+m.missing_resource = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause d'une \
+   ressource manquante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
+
+m.insufficient_permissions_error = Vous n'avez pas accepter la signature \
+  num\u00e9rique de cette application. Si vous souhaitez ex\u00e9cuter cette application, vous \
+  devez accepter sa signature num\u00e9rique.\n\nAfin de le faire, vous devez quitter votre \
+  navigateur, le red\u00e9marrer, retourner \u00e0 cette page puis relancer l'application. \
+  Une fois la bo\u00eete de dialogue de s\u00e9curit\u00e9 affich\u00e9e, cliquez sur le bouton \
+  pour accepter la signature num\u00e9rique et accorder les permissions n\u00e9cessaires au bon \
+  fonctionnement de l'application.
+
+m.corrupt_digest_signature_error = Nous ne pouvons pas v\u00e9rifier la signature num\u00e9rique \
+  de l'application.\nVeuillez v\u00e9rifier que vous lancez l'application \ndepuis \
+  la bonne adresse internet.
+
+m.default_install_error = la section de support du site
+
+m.another_getdown_running = Plusieurs instances d'installation de cette \
+  application sont d\u00e9j\u00e0 en cours d'ex\u00e9cution.  Cette instance va s'arr\u00eater \
+  afin de permettre aux autres d'aboutir.
+
+m.applet_stopped = L'appelet Getdown a \u00e9t\u00e9 stopp\u00e9e.
+
+# application/digest errors
+m.missing_appbase = Le fichier de configuration ne contient pas 'appbase'.
+m.invalid_version = Le fichier de configuration sp\u00e9cifie une version invalide.
+m.invalid_appbase = Le fichier de configuration sp\u00e9cifie un 'appbase' invalide.
+m.missing_class = Le fichier de configuration ne contient pas la classe de l'application.
+m.missing_code = Le fichier de configuration ne sp\u00e9cifie aucune ressource de code.
+m.invalid_digest_file = Le fichier digest est invalide.
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties
new file mode 100644 (file)
index 0000000..33b3260
--- /dev/null
@@ -0,0 +1,114 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Annullare l'installazione?
+m.abort_confirm = <html>Sei sicuro di voler annullare l'installazione? \
+  Potrai riprenderla in seguito, riavviando nuovamente l'applicazione.</html>
+m.abort_ok = Chiudi
+m.abort_cancel = Continua l'installazione
+
+m.detecting_proxy = Provo a recuperare le configurazioni del proxy
+
+m.configure_proxy = <html>Impossibile collegarsi al server per \
+  recuperare i dati. \
+  <ul><li> Se il Firewall di Windows o Norton Internet Security bloccano \
+  <code>javaw.exe</code> non si possono scaricare i dati. Devi \
+  permettere a <code>javaw.exe</code> di accedere a internet. Puoi provare \
+  di nuovo, ma dovresti abilitare javaw.exe nella tua configurazione \
+  del firewall ( Start -> Pannello di Controllo -> Windows Firewall ).</ul> \
+  <p> Il tuo computer potrebbe accedere a internet attraverso un proxy e \
+  questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \
+  tue impostazioni del proxy, puoi inserirle di seguito.</html>
+
+m.proxy_extra = <html>Se sei sicuro di non usare proxy  \
+  potrebbe essere un problema di internet o di collegamento con il server. \
+  In questo caso puoi annullare e ripetere l'installazione più tardi.</html>
+
+m.proxy_host = IP Proxy
+m.proxy_port = Porta Proxy
+m.proxy_username = Nome utente
+m.proxy_password = Parola d'ordine
+m.proxy_auth_required = Autenticazione richiesta
+m.proxy_ok = OK
+m.proxy_cancel = Annulla
+
+m.downloading_java = Scaricando la Java Virtual Machine
+m.unpacking_java = Scompattando la Java Virtual Machine
+
+m.resolving = Recuperando i file da scaricare
+m.downloading = Download dei dati
+m.failure = Download fallito: {0}
+
+m.checking = Sto controllando gli aggiornamenti
+m.validating = Validazione
+m.patching = Applico le patch
+m.launching = Avvio
+
+m.patch_notes = Note delle Patch
+m.play_again = Avvia Nuovamente
+
+m.complete = {0}% completato
+m.remain = {0} rimasto
+
+m.updating_metadata = Scarico i file di controllo
+
+m.init_failed = La configurazione Ã¨ corrotta o mancante. Provo a \
+  scaricarne una nuova copia...
+
+m.java_download_failed = Impossibile scaricare la versione corretta \
+  di Java per il tuo computer.\n\n\
+  Visita www.java.com e scarica l'ultima versione di \
+  Java, poi lancia di nuovo l'applicazione.
+
+m.java_unpack_failed = Impossibile scompattare l'aggiornamento di \
+  Java. Verifica di avere almeno 100 MB di spazio libero nel tuo \
+  hard disk e prova a rilanciare l'applicazione.\n\n\
+  Se l'errore persiste, vistia www.java.com, scarica e \
+  installa l'ultima versione di Java e riprova.
+
+m.unable_to_repair = Impossibile scaricare i file necessari dopo 5 \
+  tentativi. Puoi provare a rilanciare l'applicazione, ma se fallisce \
+  di nuovo potresti dover reinstallarla.
+
+m.unknown_error = L'applicazione non Ã¨ stata avviata a causa di uno strano \
+  errore che non conosco. Visita\n{0} per avere informazioni \
+  in merito.
+m.init_error = L'applicazione non Ã¨ stata avviata a causa del seguente \
+  errore:\n{0}\n\nVistita\n{1} per avere \
+  informazioni su come risolvere il problema.
+
+m.readonly_error = La directory dove l'applicazione Ã¨ installata: \
+  \n{0}\nè in sola lettura. Installa l'applicazione dove hai i diritti \
+  di scrittura.
+
+m.missing_resource = L'applicazione non Ã¨ stata avviata a causa di mancanza \
+  di risorse:\n{0}\n\nVisita\n{1} per avere informazioni su come risolvere \
+  questi problemi.
+
+m.insufficient_permissions_error = Non hai accettato la \
+ firma digitale. Se vuoi eseguire l'applicazione devi accettare la \
+ firma digitale.\n\nPer farlo, riavvia il tuo browser \
+ e ritorna in questa pagina per rilanciare l'applicazione. Quando l'avviso \
+ di sicurezza viene mostrato, clicca per accettare la firma digitale \
+ ed eseguire l'applicazione con i privilegi necessari.
+
+m.corrupt_digest_signature_error = Impossibile verificare la firma digitale dell'applicazione \
+ .\nControlla di aver lanciato l'applicazione dal\n\
+ sito web corretto.
+
+m.default_install_error = la sezione di supporto del sito
+
+m.another_getdown_running = E' già in esecuzione un'istanza del programma. \
+ Questa verrà chiusa.
+
+m.applet_stopped = L'applet di Getdown Ã¨ stata interrotta.
+
+# application/digest errors
+m.missing_appbase = Il tag "appbase" Ã¨ mancante.
+m.invalid_version = Il file di configurazione non contiene una versione valida (tag "version").
+m.invalid_appbase = Il tag "appbase" non Ã¨ valido.
+m.missing_class = Il file di configurazione non contiene la classe da eseguire (tag "class").
+m.missing_code = Il file di configurazione non contiene alcuna risorsa (tag "code").
+m.invalid_digest_file = Il file di digest non Ã¨ valido.
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties
new file mode 100644 (file)
index 0000000..c344c16
--- /dev/null
@@ -0,0 +1,107 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f 
+m.abort_confirm = <html>\u672c\u5f53\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f  \
+  \u5f8c\u3067\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u305f\u969b\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u518d\u958b\u3067\u304d\u307e\u3059\u3002</html> 
+m.abort_ok = \u4e2d\u6b62 
+m.abort_cancel = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7d9a\u884c 
+
+m.detecting_proxy = \u81ea\u52d5\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u5b9f\u884c\u4e2d
+
+m.configure_proxy = <html>\u30b5\u30fc\u30d0\u306b\u63a5\u7d9a\u3067\u304d\u306a\u3044\u305f\u3081\u3001\u30b2\u30fc\u30e0\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b \
+  \u5931\u6557\u3057\u307e\u3057\u305f\u3002  \
+  <ul><li>\u30a6\u30a3\u30f3\u30c9\u30a6\u30ba\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u307e\u305f\u306f\u30ce\u30fc\u30c8\u30f3\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u304c \
+  <code>javaw.exe</code>\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b\u3088\u3046\u8a2d\u5b9a\u3057\u3066\u3042\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30e0\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3002  \u8a2d\u5b9a\u3092 \
+  <code>javaw.exe</code>\u7d4c\u7531\u3067\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u3088\u3046\u306b\u5909\u66f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002  \u30b2\u30fc\u30e0\u3092\u518d\u8d77\u52d5 \
+  \u3057\u305f\u5f8c\u3001\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u306e\u8a2d\u5b9a\u304b\u3089javaw.exe \u3092\u524a\u9664 \
+  \u3057\u3066\u304f\u3060\u3055\u3044\uff08\u30b9\u30bf\u30fc\u30c8\u2192\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u2192\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\uff09\u3002</ul> \
+  <p>\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u81ea\u52d5\u691c\u51fa\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306f \
+  \u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u3078\u30a2\u30af\u30bb\u30b9\u3057\u3066\u3044\u307e\u3059\u3002  \u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u8a73\u7d30\u304c \
+  \u308f\u304b\u3063\u3066\u3044\u308b\u5834\u5408\u306f\u3001\u4e0b\u306b\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html> 
+
+m.proxy_extra = <html>\u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u4e00\u6642\u7684\u306a \
+  \u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306e\u4e0d\u5177\u5408\u306b\u3088\u308a\u3001\u30b5\u30fc\u30d0\u3068\u4ea4\u4fe1\u3067\u304d\u306a\u3044\u72b6\u614b\u306b\u3042\u308b \
+  \u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002  \u305d\u306e\u5834\u5408\u306f\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u30ad\u30e3\u30f3\u30bb\u30eb\u3057\u3066\u3001 \
+  \u5f8c\u307b\u3069\u6539\u3081\u3066\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html> 
+
+m.proxy_host = \u30d7\u30ed\u30ad\u30b7IP 
+m.proxy_port = \u30d7\u30ed\u30ad\u30b7\u30dd\u30fc\u30c8
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = OK  
+m.proxy_cancel = \u30ad\u30e3\u30f3\u30bb\u30eb 
+
+m.downloading_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+m.unpacking_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u89e3\u51cd\u4e2d
+
+m.resolving = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306e\u8a2d\u5b9a\u4e2d
+m.downloading = \u30c7\u30fc\u30bf\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+m.failure = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5931\u6557\uff1a  {0} 
+
+m.checking = \u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u306e\u78ba\u8a8d\u4e2d
+m.validating = \u8a8d\u8a3c\u4e2d
+m.patching = \u4fee\u6b63\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u5b9f\u884c\u4e2d
+m.launching = \u5b9f\u884c\u4e2d
+
+m.complete = {0}\uff05\u5b8c\u4e86 
+m.remain = \u3000\u6b8b\u308a{0} 
+
+m.updating_metadata = \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+
+m.init_failed = \u74b0\u5883\u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u307e\u305f\u306f\u58ca\u308c\u3066\u3044\u307e\u3059\u3002  \u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
+  \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d\u2026 
+
+m.java_download_failed = \u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u3001Java\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u6700\u65b0 \
+  \u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u81ea\u52d5\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\n \
+  www.java.com \u304b\u3089\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u624b\u52d5\u3067\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u3001 \
+  \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.java_unpack_failed = Java\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u89e3\u51cd \
+  \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u30cf\u30fc\u30c9\u30c9\u30e9\u30a4\u30d6\u306e\u30e1\u30e2\u30ea\u304c100MB\u4ee5\u4e0a\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304b\u3089 \
+  \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n \
+  \u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001www.java.com \u304b\u3089Java\u306e\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
+  \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u304b\u3089\u3001\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002 
+
+m.unable_to_repair = 5\u56de\u8a66\u884c\u3057\u307e\u3057\u305f\u304c\u3001\u5fc5\u8981\u306a\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9 \
+  \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u5f8c\u307b\u3069\u6539\u3081\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \
+  \u518d\u5ea6\u5931\u6557\u3057\u305f\u5834\u5408\u306f\u3001\u30a2\u30f3\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u5f8c\u306b\u518d\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.unknown_error = \u539f\u56e0\u4e0d\u660e\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c \
+  \u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u89e3\u6c7a\u65b9\u6cd5\u3092\n{0}\u3067 \
+  \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+m.init_error = \u6b21\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
+  \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067 \
+  \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.readonly_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u305f\u30d5\u30a9\u30eb\u30c0\u306f  \
+  \n{0}\n\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002  \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u66f8\u304d\u8fbc\u307f\u304c\u3067\u304d\u308b\u30d5\u30a9\u30eb\u30c0\u306b \
+  \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.missing_resource = \u30ea\u30bd\u30fc\u30b9\u4e0d\u660e\u306e\u305f\u3081\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
+  \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067\u78ba\u8a8d \
+  \u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.insufficient_permissions_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u62d2\u5426 \
+ \u3055\u308c\u307e\u3057\u305f\u3002  \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3059\u308b\u5834\u5408\u306f\u3001\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u306e\u627f\u8a8d\u304c \
+ \u5fc5\u8981\u3067\u3059\u3002\n\n\u627f\u8a8d\u306b\u306f\u3001\u30d6\u30e9\u30a6\u30b6\u3092\u9589\u3058\u3066\u304b\u3089\u518d\u5ea6\u958b\u304d\u3001 \
+ \u672c\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3092\u518d\u8868\u793a\u3057\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044  \u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u306e \
+ \u8b66\u544a\u304c\u8868\u793a\u3055\u308c\u305f\u6642\u306f\u3001\u5b9f\u884c\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u3092\u627f\u8a8d\u3057\u3001 \
+ \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.corrupt_digest_signature_error = \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u8a8d\u8a3c \
+ \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\u6307\u5b9a\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u304b\u3089\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u304b\n \
+ \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.default_install_error = \u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3067\u306e\u30b5\u30dd\u30fc\u30c8\u8868\u793a 
+
+# application/digest errors
+m.missing_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306eappbase\u304c\u4e0d\u660e\u3067\u3059\u3002 
+m.invalid_version = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306f\u7121\u52b9\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 
+m.invalid_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u306aappbase\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 
+m.missing_class = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30af\u30e9\u30b9\u304c\u4e0d\u660e\u3067\u3059\u3002 
+m.missing_code = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u3067\u30b3\u30fc\u30c9\u30ea\u30bd\u30fc\u30b9\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 
+m.invalid_digest_file = \u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u3067\u3059\u3002 
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties
new file mode 100644 (file)
index 0000000..3f8a47f
--- /dev/null
@@ -0,0 +1,102 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?
+m.abort_confirm = <html>\uC815\uB9D0\uB85C \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \
+  \uB098\uC911\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uC5EC \uC124\uCE58\uB97C \uC7AC\uAC1C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624.</html>
+m.abort_ok = \uC911\uC9C0
+m.abort_cancel = \uACC4\uC18D\uD558\uC5EC \uC124\uCE58
+
+m.detecting_proxy = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
+
+m.configure_proxy = <html>\uAC8C\uC784 \uB370\uC774\uD130\uB97C \uBC1B\uAE30 \uC704\uD55C \uC11C\uBC84 \uC811\uC18D\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.\
+  <ul><li>\uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD \uB610\uB294 \uB178\uD134 \uC778\uD130\uB137 \uC2DC\uD050\uB9AC\uD2F0\uAC00 <code>javaw.exe</code>\uC774 \uC124\uC815\uC5D0\uC11C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC744 \uACBD\uC6B0, \
+  \uAC8C\uC784 \uB370\uC774\uD130\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
+  <code>javaw.exe</code>\uAC00 \uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD560 \uC218 \uC788\uB3C4\uB85D \uC124\uC815\uC744 \uBCC0\uACBD\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+  \uAC8C\uC784\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD55C \uD6C4, \uBC29\uD654\uBCBD \uC124\uC815\uC5D0\uC11C javaw.exe\uB97C \uC0AD\uC81C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+  ( \uC2DC\uC791 -> \uC81C\uC5B4\uD310 -> \uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD )</ul> \
+  <p> \uCEF4\uD4E8\uD130\uAC00 \uD504\uB85D\uC2DC \uC11C\uBC84\uB97C \uD1B5\uD574 \uC778\uD130\uB137\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB2E4\uBA74, \uD504\uB85D\uC2DC \uC124\uC815\uC758 \uC790\uB3D9 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC73C\uBBC0\uB85C, \
+  \uC0AC\uC6A9\uD558\uB294 \uD504\uB85D\uC2DC \uC124\uC815\uC744 \uC54C\uACE0 \uC788\uC744 \uACBD\uC6B0 \uC544\uB798\uC5D0 \uC785\uB825\uD558\uC5EC \uC8FC\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.</html>
+
+m.proxy_extra = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
+
+m.proxy_host = \uD504\uB85D\uC2DC IP
+m.proxy_port = \uD504\uB85D\uC2DC \uD3EC\uD2B8
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = OK
+m.proxy_cancel = \uCDE8\uC18C
+
+m.downloading_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uB2E4\uC6B4\uB85C\uB4DC \uC911
+m.unpacking_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uC555\uCD95\uC744 \uD574\uC81C\uD558\uB294 \uC911
+
+m.resolving = \uB2E4\uC6B4\uB85C\uB4DC \uBD84\uC11D \uC911
+m.downloading = \uB370\uC774\uD130 \uB2E4\uC6B4\uB85C\uB4DC \uC911
+m.failure = \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: {0}
+
+m.checking = \uC5C5\uB370\uC774\uD2B8 \uCCB4\uD06C
+m.validating = \uC720\uD6A8\uC131 \uAC80\uC0AC \uC911
+m.patching = \uD328\uCE58 \uC911
+m.launching = \uC2E4\uD589 \uC911
+
+m.patch_notes = \uD328\uCE58 \uB178\uD2B8
+m.play_again = \uB2E4\uC2DC \uC2E4\uD589
+
+m.complete = {0}% \uC644\uB8CC
+m.remain = {0} \uB0A8\uC74C
+
+m.updating_metadata = \uCEE8\uD2B8\uB864 \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911
+
+m.init_failed = \uC124\uC815 \uD30C\uC77C\uC774 \uB204\uB77D\uB418\uC5C8\uAC70\uB098 \uBCC0\uD615\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
+  \uC0C8\uB85C\uC6B4 \uBCF5\uC0AC\uBCF8\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911\uC785\uB2C8\uB2E4...
+
+m.java_download_failed = \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uD544\uC694\uD55C \uC0C8\uB85C\uC6B4 \uBC84\uC804\uC758 \uC790\uBC14\uB97C \uC790\uB3D9\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\n\
+  \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
+  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
+
+m.java_unpack_failed = \uC5C5\uB370\uC774\uD2B8\uB41C \uBC84\uC804\uC758 \uC790\uBC14\uC758 \uC555\uCD95\uC744 \uD480 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
+  \uD558\uB4DC\uB4DC\uB77C\uC774\uBE0C\uC5D0 \uCD5C\uC18C\uD55C 100MB\uC758 \uC6A9\uB7C9\uC744 \uD655\uBCF4\uD55C \uC774\uD6C4, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.\n\n\
+  \uB9CC\uC57D \uBB38\uC81C\uAC00 \uD574\uACB0\uB418\uC9C0 \uC54A\uB294\uB2E4\uBA74, \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
+  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
+
+m.unable_to_repair = \uB2E4\uC12F\uBC88\uC758 \uC2DC\uB3C4\uC5D0\uB3C4 \uD544\uC694\uD55C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \
+  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2DC\uC791\uD574\uBCF4\uC2DC\uACE0, \uADF8\uB798\uB3C4 \uB2E4\uC6B4\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD55C\uB2E4\uBA74, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC81C\uAC70\uD55C \uD6C4, \uB2E4\uC2DC \uC2E4\uD589\uD574\uBCF4\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.unknown_error = \uBCF5\uAD6C\uB420 \uC218 \uC5C6\uB294 \uC624\uB958\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
+  \n{0}\uC5D0 \uB300\uD55C \uBCF5\uAD6C \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.init_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC544\uB798\uC640 \uAC19\uC740 \uC5D0\uB7EC\uB85C \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC5D0\uB7EC:\
+  \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.readonly_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC124\uCE58\uB41C \uB514\uB809\uD1A0\uB9AC: \
+  \n{0}\n\uAC00 \uC77D\uAE30 \uC804\uC6A9\uC785\uB2C8\uB2E4. \uC77D\uAE30 \uAD8C\uD55C\uC774 \uC2B9\uC778\uB41C \uB809\uD1A0\uB9AC\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC124\uCE58\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.missing_resource = \uB9AC\uC18C\uC2A4\uC758 \uC190\uC2E4\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. : \
+  \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.insufficient_permissions_error = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \
+ \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uAE30 \uC704\uD574\uC11C \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+ \n\n\uADF8\uB9AC\uACE0 \uB098\uC11C \uC6F9 \uBE0C\uB77C\uC6B0\uC800\uB97C \uB2EB\uACE0 \uB2E4\uC2DC \uC2DC\uC791\uD558\uC5EC \uC6F9\uD398\uC774\uC9C0\uB85C \uB3CC\uC544\uC640 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC7AC\uC2DC\uC791\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. \
+ \uBCF4\uC548\uC5D0 \uB300\uD55C \uB300\uD654\uC0C1\uC790\uAC00 \uBCF4\uC774\uBA74, \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC5D0 \uB300\uD55C \uD655\uC778 \uBC84\uD2BC\uC744 \uD074\uB9AD\uD558\uACE0, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uAE30 \uC704\uD55C \
+ \uAD8C\uD55C\uC744 \uBD80\uC5EC\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.corrupt_digest_signature_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n \
+ \uC62C\uBC14\uB978 \uC6F9\uC0AC\uC774\uD2B8\uC5D0\uC11C \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uACE0 \uC788\uB294 \uC9C0 \uD655\uC778\uBC14\uB78D\uB2C8\uB2E4.
+
+m.default_install_error = \uC6F9\uC0AC\uC774\uD2B8\uC758 \uC9C0\uC6D0 \uBA54\uB274(support section)\uB97C \uD655\uC778\uD558\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.another_getdown_running = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uC778\uC2A4\uD1A8\uB7EC\uC758 \uB2E4\uC911 \uC778\uC2A4\uD134\uC2A4\uAC00 \uC2E4\uD589\uC911\uC785\uB2C8\uB2E4. \
+ \uD558\uB098\uAC00 \uC644\uB8CC\uB420 \uB54C\uAE4C\uC9C0 \uC911\uB2E8\uB429\uB2C8\uB2E4.
+
+m.applet_stopped = Getdown \uC560\uD50C\uB9BF \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
+
+# application/digest errors
+m.missing_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 'appbase' \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
+m.invalid_version = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C \uBC84\uC804\uC774 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
+m.invalid_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C 'appbase'\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
+m.missing_class = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uD074\uB798\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
+m.missing_code = \uC124\uC815 \uD30C\uC77C\uC5D0 \uB9AC\uC18C\uC2A4\uC5D0 \uB300\uD55C \uCF54\uB4DC\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
+m.invalid_digest_file = \uB2E4\uC774\uC81C\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC798\uBABB\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties
new file mode 100644 (file)
index 0000000..47db91c
--- /dev/null
@@ -0,0 +1,118 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Cancelar a instala\u00E7\u00E3o?
+m.abort_confirm = <html>Tem certeza que deseja cancelar a instala\u00E7\u00E3o? \
+  Voc\u00EA pode continuar a instala\u00E7\u00E3o mais tarde, \
+  basta executar a aplica\u00E7\u00E3o novamente.</html>
+m.abort_ok = Sair
+m.abort_cancel = Continuar a instala\u00E7\u00E3o
+
+m.detecting_proxy = Tentando detectar automaticamente as configura\u00E7\u00F5es de proxy
+
+m.configure_proxy = <html>N\u00E3o foi poss\u00EDvel conectar aos nossos servidores para \
+  fazer o download dos dados. \
+  <ul><li> Se o Firewall do Windows ou o Norton Internet Security est\u00E1 configurado \
+  para bloquear o programa <code>javaw.exe</code> n\u00E3o ser\u00E1 poss\u00EDvel realizar \
+  o download. Voc\u00EA ter\u00E1 que permitir que o programa <code>javaw.exe</code> acesse \
+  a internet. Voc\u00EA pode tentar executar o programa novamente, mas voc\u00EA precisa \
+  remover o programa javaw.exe das configura\u00E7\u00F5es do firewall (Iniciar -> Painel \
+  de controle -> Firewall do Windows).</ul> \
+  <p> Seu computador pode estar acessando a internet atrav\u00E9s de um proxy e n\u00E3o foi \
+  capaz de detectar automaticamente as configura\u00E7\u00F5es de proxy. \
+  Voc\u00EA pode informar esses dados abaixo.</html>
+
+m.proxy_extra = <html>Se voc\u00EA tem certeza que n\u00E3o usa um proxy, ent\u00E3o pode ser \
+  que exista um problema tempor\u00E1rio que est\u00E1 impedindo a comunica\u00E7\u00E3o \
+  com os nossos servidores. Neste caso voc\u00EA pode cancelar e tentar instalar novamente \
+  mais tarde.</html>
+
+m.proxy_host = IP do Proxy
+m.proxy_port = Porta do Proxy
+m.proxy_username = Nome de usu\u00e1rio
+m.proxy_password = Senha
+m.proxy_auth_required = Autentifica\u00e7\u00e3o requerida
+m.proxy_ok = OK
+m.proxy_cancel = Cancelar
+
+m.downloading_java = Fazendo o download da m\u00E1quina virtual Java
+m.unpacking_java = Descompactando a m\u00E1quina virtual Java
+
+m.resolving = Resolvendo downloads
+m.downloading = Transferindo dados
+m.failure = Download falhou: {0}
+
+m.checking = Verificando atualiza\u00E7\u00F5es
+m.validating = Validando
+m.patching = Atualizando
+m.launching = Executando
+
+m.patch_notes = Corrigir notas
+m.play_again = Jogar de novo
+
+m.complete = {0}% completo
+m.remain = {0} Permanecer
+
+m.updating_metadata = Transferindo arquivos de controle
+
+m.init_failed = Nosso arquivo de configura\u00E7\u00E3o est\u00E1 ausente ou corrompido. Tente \
+  baixar uma nova c\u00F3pia...
+
+m.java_download_failed = N\u00E3o conseguimos baixar automaticamente a\
+  vers\u00E3o necess\u00E1ria do Java para o seu computador.\n\n\
+  Por favor, acesse www.java.com, baixe e instale a \u00FAltima vers\u00E3o do \
+  Java, em seguida, tente executar o aplicativo novamente.
+
+m.java_unpack_failed = N\u00E3o conseguimos descompactar uma vers\u00E3o atualizada do \
+  Java. Por favor, certifique-se de ter pelo menos 100 MB de espa\u00E7o livre em seu \
+  disco r\u00EDgido e tente executar o aplicativo novamente. \n\n\
+  Se isso n\u00E3o resolver o problema, acesse www.java.com,baixe e \
+  instale a \u00FAltima vers\u00E3o do Java e tente novamente.
+
+m.unable_to_repair = N\u00E3o conseguimos baixar os arquivos necess\u00E1rios depois de \
+  cinco tentativas. Voc\u00EA pode tentar executar o aplicativo novamente, mas se ele \
+  falhar pode ser necess\u00E1rio desinstalar e reinstalar.
+
+m.unknown_error = A aplica\u00E7\u00E3o falhou ao iniciar devido a algum erro estranho \
+  do qual n\u00E3o conseguimos recuperar. Por favor, visite \n{0} para obter \
+  informa\u00E7\u00F5es sobre como recuperar.
+m.init_error = A aplica\u00E7\u00E3o falhou ao iniciar devido ao seguinte \
+  erro:\n{0}\n\nPor favor visite \n{1} para \
+  informa\u00E7\u00F5es sobre como lidar com esse problema.
+
+m.readonly_error =O diret\u00F3rio no qual este aplicativo est\u00E1 instalado: \
+  \n{0}\n \u00E9 somente leitura. Por favor, instale o aplicativo em um diret\u00F3rio onde \
+  voc\u00EA tem acesso de grava\u00E7\u00E3o.
+
+m.missing_resource = A aplica\u00E7\u00E3o falhou ao iniciar devido a uma falta \
+  de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informa\u00E7\u00F5es sobre \
+  como lidar com tal problema.
+
+m.insufficient_permissions_error = Voc\u00EA n\u00E3o aceitou a assinatura digital \
+  do aplicativo. Se voc\u00EA quiser executar o aplicativo, voc\u00EA ter\u00E1 que aceitar \
+  a assinatura digital. \n\nPara fazer isso, voc\u00EA ter\u00E1 que sair do seu navegador, \
+  reinici\u00E1-lo, e retornar a esta p\u00E1gina web para executar a aplica\u00E7\u00E3o. \
+  Quando o di\u00E1logo de seguran\u00E7a aparecer, clique no bot\u00E3o para aceitar a \
+  assinatura digital e conceder a este aplicativo os privil\u00E9gios necess\u00E1rios \
+  para executar.
+
+m.corrupt_digest_signature_error = N\u00E3o conseguimos verificar a assinatura digital \
+  do aplicativo.\nPor favor, verifique se voc\u00EA est\u00E1 utilizando o aplicativo \nde um \
+  site correto.
+
+m.default_install_error = a se\u00E7\u00E3o de suporte do site
+
+m.another_getdown_running = V\u00E1rias inst\u00E2ncias desta aplica\u00E7\u00E3o \
+  est\u00E3o em execu\u00E7\u00E3o. Esta ir\u00E1 parar e deixar outra completar suas atividades.
+
+m.applet_stopped = Foi solicitado ao miniaplicativo GetDow que parasse de trabalhar.
+
+# application/digest errors
+m.missing_appbase = O arquivo de configura\u00E7\u00E3o n\u00E3o possui o 'AppBase'.
+m.invalid_version = O arquivo de configura\u00E7\u00E3o especifica uma vers\u00E3o inv\u00E1lida.
+m.invalid_appbase = O arquivo de configura\u00E7\u00E3o especifica um 'AppBase' inv\u00E1lido.
+m.missing_class = O arquivo de configura\u00E7\u00E3o n\u00E3o possui a classe de aplicativo.
+m.missing_code = O arquivo de configura\u00E7\u00E3o n\u00E3o especifica um recurso de c\u00F3digo.
+m.invalid_digest_file = O arquivo digest \u00E9 inv\u00E1lido.
diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties
new file mode 100644 (file)
index 0000000..2c27543
--- /dev/null
@@ -0,0 +1,61 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.detecting_proxy = \u641c\u5bfb\u4ee3\u7406\u670d\u52a1\u5668
+
+m.configure_proxy = <html>\u6211\u4eec\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u4e0b\u8f7d\u6e38\u620f\u6570\u636e\u3002\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e \
+  \u60a8\u7684\u8ba1\u7b97\u673a\u662f\u901a\u8fc7\u4ee3\u7406\u670d\u52a1\u5668\u8fde\u63a5\u4e92\u8054\u7f51\u7684\uff0c\u5e76\u4e14\u6211\u4eec\u65e0\u6cd5\u81ea\u52a8\u83b7\u5f97\u4ee3\u7406\u670d\u52a1\u5668\u7684 \
+  \u8bbe\u7f6e\u3002\u5982\u679c\u60a8\u77e5\u9053\u60a8\u4ee3\u7406\u670d\u52a1\u5668\u7684\u8bbe\u7f6e\uff0c\u60a8\u53ef\u4ee5\u5728\u4e0b\u9762\u8f93\u5165\u3002</html>
+
+m.proxy_extra = <html>\u5982\u679c\u60a8\u786e\u5b9a\u60a8\u6ca1\u6709\u4f7f\u7528\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e\u6682\u65f6\u65e0\u6cd5 \
+  \u8fde\u63a5\u5230\u4e92\u8054\u7f51\uff0c\u5bfc\u81f4\u65e0\u6cd5\u548c\u670d\u52a1\u5668\u901a\u8baf\u3002\u8fd9\u79cd\u60c5\u51b5\uff0c\u60a8\u53ef\u4ee5\u53d6\u6d88\uff0c\u7a0d\u5019\u518d\u91cd\u65b0\u5b89\u88c5\u3002<br><br> \
+  \u5982\u679c\u60a8\u65e0\u6cd5\u786e\u5b9a\u60a8\u662f\u5426\u4f7f\u7528\u4e86\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8bf7\u8bbf\u95ee\u6211\u4eec\u7f51\u7ad9\u4e2d\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c \
+  \u4e86\u89e3\u5982\u4f55\u68c0\u6d4b\u60a8\u7684\u4ee3\u7406\u670d\u52a1\u5668\u8bbe\u7f6e\u3002</html>
+
+m.proxy_host = \u4ee3\u7406\u670d\u52a1\u5668\u7684IP\u5730\u5740
+m.proxy_port = \u4ee3\u7406\u670d\u52a1\u5668\u7684\u7aef\u53e3\u53f7
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = \u786e\u5b9a
+m.proxy_cancel = \u53d6\u6d88
+
+m.resolving = \u5206\u6790\u9700\u4e0b\u8f7d\u5185\u5bb9
+m.downloading = \u4e0b\u8f7d\u6570\u636e
+m.failure = \u4e0b\u8f7d\u5931\u8d25: {0}
+
+m.checking = \u68c0\u67e5\u66f4\u65b0\u5185\u5bb9
+m.validating = \u786e\u8ba4
+m.patching = \u5347\u7ea7
+m.launching = \u542f\u52a8
+
+m.complete = {0}% \u5b8c\u6210
+m.remain = {0} \u5269\u4f59\u65f6\u95f4
+
+m.updating_metadata = \u4e0b\u8f7d\u63a7\u5236\u6587\u4ef6
+
+m.init_failed = \u65e0\u6cd5\u627e\u5230\u914d\u7f6e\u6587\u4ef6\u6216\u5df2\u635f\u574f\u3002\u5c1d\u8bd5\u91cd\u65b0\u4e0b\u8f7d...
+
+m.unable_to_repair = \u7ecf\u8fc75\u6b21\u5c1d\u8bd5\uff0c\u4f9d\u7136\u65e0\u6cd5\u4e0b\u8f7d\u6240\u9700\u7684\u6587\u4ef6\u3002\
+\u60a8\u53ef\u4ee5\u91cd\u65b0\u8fd0\u884c\u7a0b\u5e8f\uff0c\u4f46\u662f\u5982\u679c\u4f9d\u7136\u5931\u8d25\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u53cd\u5b89\u88c5\u5e76\u91cd\u65b0\u5b89\u88c5\u3002
+
+
+m.unknown_error = \u7531\u4e8e\u4e00\u4e9b\u65e0\u6cd5\u56de\u590d\u7684\u4e25\u91cd\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\u3002\
+\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u89e3\u51b3\u95ee\u9898\u3002
+
+m.init_error = \u7531\u4e8e\u4e0b\u5217\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
+\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u9519\u8bef\u3002
+
+
+m.missing_resource = \u7531\u4e8e\u65e0\u6cd5\u627e\u5230\u4e0b\u5217\u8d44\u6e90\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
+\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u95ee\u9898\u3002
+
+# application/digest errors
+m.missing_appbase = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230 'appbase'\u3002
+m.invalid_version = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684\u7248\u672c\u3002
+m.invalid_appbase = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684 'appbase'\u3002
+m.missing_class = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u7a0b\u5e8f\u6587\u4ef6\u3002
+m.missing_code = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u6307\u5b9a\u7684\u8d44\u6e90\u3002
+m.invalid_digest_file = \u65e0\u6548\u7684\u914d\u7f6e\u6587\u4ef6\u3002
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages.properties
new file mode 100644 (file)
index 0000000..19b2999
--- /dev/null
@@ -0,0 +1,110 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Abort installation?
+m.abort_confirm = <html>Are you sure you want to stop installation? \
+  You can resume at a later time by running the application again.</html>
+m.abort_ok = Quit
+m.abort_cancel = Continue installation
+
+m.detecting_proxy = Trying to auto-detect proxy settings
+
+m.configure_proxy = <html>We were unable to connect to the application server to download data. \
+  <p> Please make sure that no virus scanner or firewall is blocking network communicaton with \
+  the server. \
+  <p> Your computer may access the Internet through a proxy and we were unable to automatically \
+  detect your proxy settings. If you know your proxy settings, you can enter them below.</html>
+
+m.proxy_extra = <html>If you are sure that you don't use a proxy then \
+  perhaps there is a temporary Internet outage that is preventing us from \
+  communicating with the servers. In this case, you can cancel and try \
+  installing again later.</html>
+
+m.proxy_host = Proxy IP
+m.proxy_port = Proxy port
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = OK
+m.proxy_cancel = Cancel
+
+m.downloading_java = Downloading Java Virtual Machine
+m.unpacking_java = Unpacking Java Virtual Machine
+
+m.resolving = Resolving downloads
+m.downloading = Downloading data
+m.failure = Download failed: {0}
+
+m.checking = Checking for update
+m.validating = Validating
+m.patching = Patching
+m.launching = Launching
+
+m.patch_notes = Patch Notes
+m.play_again = Play Again
+
+m.complete = {0}% complete
+m.remain = {0} remaining
+
+m.updating_metadata = Downloading control files
+
+m.init_failed = Our configuration file is missing or corrupt. Attempting \
+  to download a new copy...
+
+m.java_download_failed = We were unable to automatically download the \
+  necessary version of Java for your computer.\n\n\
+  Please go to www.java.com and download the latest version of \
+  Java, then try running the application again.
+
+m.java_unpack_failed = We were unable to unpack an updated version of \
+  Java. Please make sure you have at least 100 MB of free space on your \
+  harddrive and try running the application again.\n\n\
+  If that does not solve the problem, go to www.java.com and download and \
+  install the latest version of Java and try again.
+
+m.unable_to_repair = We were unable to download the necessary files after \
+  five attempts. You can try running the application again, but if it \
+  fails you may need to uninstall and reinstall.
+
+m.unknown_error = The application has failed to launch due to some strange \
+  error from which we could not recover. Please visit\n{0} for information on \
+  how to recover.
+m.init_error = The application has failed to launch due to the following \
+  error:\n{0}\n\nPlease visit\n{1} for \
+  information on how to handle such problems.
+
+m.readonly_error = The directory in which this application is installed: \
+  \n{0}\nis read-only. Please install the application into a directory where \
+  you have write access.
+
+m.missing_resource = The application has failed to launch due to a missing \
+  resource:\n{0}\n\nPlease visit\n{1} for information on how to handle such \
+  problems.
+
+m.insufficient_permissions_error = You did not accept this application's \
+ digital signature. If you want to run the application, you will need to accept \
+ its digital signature.\n\nTo do so, you will need to quit your web browser, \
+ restart it, and return to this web page to relaunch the application. When the \
+ security dialog is shown, click the button to accept the digital signature \
+ and grant this application the privileges it needs to run.
+
+m.corrupt_digest_signature_error = We couldn't verify the application's digital \
+ signature.\nPlease check that you are launching the application from\nthe \
+ correct website.
+
+m.default_install_error = the support section of the website
+
+m.another_getdown_running = Multiple instances of this application's \
+ installer are running. This one will stop and let another complete.
+
+m.applet_stopped = Getdown's applet was told to stop working.
+
+# application/digest errors
+m.missing_appbase = The configuration file is missing the 'appbase'.
+m.invalid_version = The configuration file specifies an invalid version.
+m.invalid_appbase = The configuration file specifies an invalid 'appbase'.
+m.missing_class = The configuration file is missing the application class.
+m.missing_code = The configuration file specifies no code resources.
+m.invalid_digest_file = The digest file is invalid.
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_de.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_de.properties
new file mode 100644 (file)
index 0000000..8e36835
--- /dev/null
@@ -0,0 +1,116 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Installation abbrechen?
+m.abort_confirm = <html>Bist du sicher, dass du die Installation abbrechen \
+m\u00f6chtest? \
+  Du kannst sp\u00e4ter fortfahren, indem du die Anwendung erneut \
+ausf\u00fchrst.</html>
+m.abort_ok = Beenden
+m.abort_cancel = Installation fortsetzen
+
+m.detecting_proxy = Versuche Proxy-Einstellungen automatisch zu ermitteln
+
+m.configure_proxy = <html>Es konnte keine Verbindung zum Applikations-Server aufgebaut werden. \
+  <p>Bitte kontrollieren Sie die Proxyeinstellungen und stellen Sie sicher, dass keine lokal oder \
+  im Netzwerk betriebene Sicherheitsanwendung (Virenscanner, Firewall, etc.) die Kommunikation \
+  mit dem Server blockiert.<br> \
+  Wenn kein Proxy verwendet werden soll, l\u00f6schen Sie bitte alle Eintr\u00e4ge in den unten \
+  stehenden Feldern und klicken sie auf OK.</html>
+
+m.proxy_extra = <html>Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \
+  an Ihren Administrator.</html>
+
+m.proxy_host = Proxy-Adresse
+m.proxy_port = Proxy-Port
+m.proxy_username = Benutzername
+m.proxy_password = Passwort
+m.proxy_auth_required = Authentisierung erforderlich
+m.proxy_ok = OK
+m.proxy_cancel = Abbrechen
+
+m.downloading_java = Lade Java Virtual Machine herunter
+m.unpacking_java = Entpacke Java Virtual Machine
+
+m.resolving = Bereite Download vor
+m.downloading = Lade Daten herunter
+m.failure = Download fehlgeschlagen: {0}
+
+m.checking = Suche nach Updates
+m.validating = Validiere Download
+m.patching = Patche
+m.launching = Starte
+
+m.patch_notes = Patchnotes
+
+m.complete = {0}% abgeschlossen
+m.remain = {0} \u00fcbrig
+
+m.updating_metadata = Lade Steuerungsdateien herunter
+
+m.init_failed = Unsere Konfigurationsdatei fehlt oder ist besch\u00e4digt. \
+Versuche, eine neue Kopie herunterzuladen...
+
+m.java_download_failed = Wir konnten die notwendige Javaversion f\u00fcr deinen \
+Computer nicht automatisch herunterladen. \n\n \
+Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \
+Anwendung erneut starten.
+
+m.java_unpack_failed = Wir konnten die aktualisierte Javaversion nicht \
+entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \
+Festplatte frei sind und versuche dann die Anwendung erneut zu \
+starten.\n\n\ \
+Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \
+Javaversion herunterladen und installieren und dann erneut versuchen.
+
+m.unable_to_repair = Wir konnten die notwendigen Dateien nach 5 Versuchen \
+nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \
+aber wenn dies erneut fehlschl\u00e4gt, musst du die Anwendung deinstallieren \
+und erneut installieren.
+
+m.unknown_error = Die Anwendung konnte wegen eines unbekannten Fehlers \
+nicht gestartet werden. Bitte auf \n{0} weiterlesen.
+
+m.init_error = Die Anwendung konnte wegen folgendem Fehler nicht gestartet \
+werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \
+solchen Problemen vorzugehen ist.
+
+m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist: \
+ \n{0}\nist nicht schreibberechtigt. Bitte in ein Verzeichnis mit \
+Schreibzugriff installieren.
+
+m.missing_resource = Die Anwendung konnte nicht gestartet werden, da die \
+folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \
+weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist.
+
+m.insufficient_permissions_error = Du hast die digitale Signatur dieser \
+Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \
+musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \
+deinen Browser beenden, neu starten und erneut die Anwendung von dieser \
+Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \
+digitale Signatur akzeptieren, um der Anwendung die n\u00f6tigen Rechte zu \
+geben, die sie braucht, um zu laufen.
+
+m.corrupt_digest_signature_error = Wir konnten die digitale Signatur \
+dieser Anwendung nicht \u00fcberpr\u00fcfen.\nBitte \u00fcberpr\u00fcfe, ob du die Anwendung \
+von der richtigen Webseite aus startest.
+
+m.default_install_error = der Support-Webseite
+
+m.another_getdown_running = Diese Installationsanwendung l\u00e4uft in mehreren \
+Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \
+Vorgang erledigen lassen.
+
+m.applet_stopped = Die Anwendung wurde beendet.
+
+
+# application/digest errors
+m.missing_appbase = In der Konfigurationsdatei fehlt die 'appbase'.
+m.invalid_version = In der Konfigurationsdatei steht die falsche Version.
+m.invalid_appbase = In der Konfigurationsdatei steht die falsche 'appbase'.
+m.missing_class = In der Konfigurationsdatei fehlt die Anwendungsklasse.
+m.missing_code = Die Konfigurationsdatei enth\u00e4lt keine Codequellen.
+m.invalid_digest_file = Die Hashwertedatei ist ung\u00fcltig.
+
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_es.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_es.properties
new file mode 100644 (file)
index 0000000..609b025
--- /dev/null
@@ -0,0 +1,115 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = \u00bfCancelar la instalaci\u00f3n?
+m.abort_confirm = <html>\u00bfEst\u00e1s seguro de querer cancelar la instalaci\u00f3n? \
+  Puedes continuarla despu\u00e9s si corres de nuevo la aplicaci\u00f3n.</html>
+m.abort_ok = Cancelar
+m.abort_cancel = Continuar la instalaci\u00f3n
+
+m.detecting_proxy = Detectando autom\u00e1ticamente la configuraci\u00f3n proxy
+
+m.configure_proxy = <html>No ha sido posible conectar con nuestros servidores para \
+  descargar los datos del juego. \
+  <ul><li> Si el cortafuegos de Windows o Norton Internet Security tiene instrucciones \
+  de bloquear <code>javaw.exe</code> no podemos descargar el juego. Necesitar\u00e1s \
+  permitir que <code>javaw.exe</code> tenga acceso al Internet. Puedes intentar \
+  correr el juego de nuevo, pero es posible que debas dar permisos a javaw.exe en la \
+  configuraci\u00f3n de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ).</ul> \
+  <p> Es posible que tu computadora tenga acceso al Internet por medio de un proxy por lo que \
+  no ha sido posible detectar autom\u00e1ticamente tu configuraci\u00f3n. Si conoces tu \
+  configuraci\u00f3n proxy, puedes anotarla abajo.</html>
+
+m.proxy_extra = <html>Si est\u00e1s seguro de que no tienes un proxy entonces \
+  tal vez exista un falla temporal en el Internet que est\u00e1 evitando que podamos \
+  comunicarnos con los servidores. En este caso, puedes cancelar e intentar \
+  instalarla de nuevo m\u00e1s tarde.</html>
+
+m.proxy_host = IP proxy
+m.proxy_port = Puerto proxy
+m.proxy_username = Nombre de usuario
+m.proxy_password = Contrase\u00f1a
+m.proxy_auth_required = Autenticacion requerida
+m.proxy_ok = OK
+m.proxy_cancel = Cancelar
+
+m.downloading_java = Descargando Java Virtual Machine
+m.unpacking_java = Desempacando Java Virtual Machine
+
+m.resolving = Resolviendo descarga
+m.downloading = Descargando datos
+m.failure = Descarga fallida: {0}
+
+m.checking = Buscando actualizaciones
+m.validating = Validando
+m.patching = Parchando
+m.launching = Lanzando
+
+m.patch_notes = Notas del parche
+
+m.complete = {0}% completado
+m.remain = {0} restante
+
+m.updating_metadata = Descargando los archivos de control
+
+m.init_failed = Un archivo de configuraci\u00f3n est\u00e1 faltante o est\u00e1 corrupto. Intentando \
+  descargar una nueva copia...
+
+m.java_download_failed = No ha sido posible descargar autom\u00e1ticamente la \
+  versi\u00f3n de Java necesaria para tu computadora.\n\n\
+  Por favor ve a www.java.com y descarga la \u00faltima versi\u00f3n de \
+  Java, despu\u00e9s intenta correr de nuevo la aplicaci\u00f3n.
+
+m.java_unpack_failed = No ha sido posible desempacar una versi\u00f3n actualizada de \
+  Java. Por favor aseg\u00farate de tener al menos 100 MB de espacio libre en tu \
+  disco duro e intenta correr de nuevo la aplicaci\u00f3n.\n\n\
+  Si eso no soluciona el problema, ve a www.java.com y descarga e \
+  instala la \u00faltima versi\u00f3n de Java e intenta de nuevo.
+
+m.unable_to_repair = No ha sido posible descargar los archivos necesarios despu\u00e9s de \
+  cinco intentos. Puedes intentar correr de nuevo la aplicaci\u00f3n, pero si falla \
+  de nuevo podr\u00edas necesitar desinstalar y reinstalar.
+
+m.unknown_error = La aplicaci\u00f3n no ha podido iniciar debido a un extra\u00f1o \
+  error del que no se pudo recobrar. Por favor visita\n{0} para ver informaci\u00f3n acerca \
+  de como recuperarla.
+m.init_error = La aplicaci\u00f3n no ha podido iniciar debido al siguiente \
+  error:\n{0}\n\nPor favor visita\n{1} para \
+  ver informaci\u00f3n acerca de como manejar ese tipo de problemas.
+
+m.readonly_error = El directorio en el que esta aplicaci\u00f3n est\u00e1 instalada: \
+  \n{0}\nes solo lectura. Por favor instala la aplicaci\u00f3n en un directorio en el cual \
+  tengas acceso de escritura.
+
+m.missing_resource = La aplicaci\u00f3n no ha podido iniciar debido a un recurso \
+  faltante:\n{0}\n\nPor favor visita\n{1} para informaci\u00f3n acerca de como solucionar \
+  estos problemas.
+
+m.insufficient_permissions_error = No aceptaste la firma digital de \
+ esta aplicaci\u00f3n. Si quieres correr la aplicaci\u00f3n, necesitas aceptar \
+ su firma digital.\n\nPara hacerlo, necesitas cerrar tu navegador, \
+ reiniciarlo, y regresar a esta p\u00e1gina web para reiniciar la aplicaci\u00f3n. Cuando se muestre \
+ el di\u00e1logo de seguridad, haz clic en el bot\u00f3n para aceptar la firmar digital \
+ y otorgar a esta aplicaci\u00f3n los privilegios que necesita para correr.
+
+m.corrupt_digest_signature_error = No pudimos verificar la firma digital \
+ de la aplicaci\u00f3n.\nPor favor revisa que est\u00e9s lanzando la aplicaci\u00f3n desde\nel \
+ sitio web correcto.
+
+m.default_install_error = la secci\u00f3n de asistencia de este sitio web
+
+m.another_getdown_running = Est\u00e1n corriendo m\u00faltiples instancias de \
+ este instalador.  Este se detendr\u00e1 para permitir que otra contin\u00fae.
+
+m.applet_stopped = Se le dijo al applet de Getdown que dejara de trabajar.
+
+# application/digest errors
+m.missing_appbase = Al archivo de configuraci\u00f3n le falta el 'appbase'.
+m.invalid_version = El archivo de configuraci\u00f3n especifica una versi\u00f3n no v\u00e1lida.
+m.invalid_appbase = El archivo de configuraci\u00f3n especifica un 'appbase' no v\u00e1lido.
+m.missing_class = Al archivo de configuraci\u00f3n le falta la clase de aplicaci\u00f3n.
+m.missing_code = El archivo de configuraci\u00f3n especifica que no hay recursos de c\u00f3digo.
+m.invalid_digest_file = El archivo digest no es v\u00e1lido.
+
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_fr.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_fr.properties
new file mode 100644 (file)
index 0000000..3666204
--- /dev/null
@@ -0,0 +1,111 @@
+#
+# $Id: messages.properties 485 2012-03-08 22:05:30Z ray.j.greenwell $
+#
+# Getdown translation messages
+
+m.abort_title = Annuler l'installation?
+m.abort_confirm =<html>\u00cates-vous s\u00fbr de vouloir annuler l'installation? \
+   Vous pourrez reprendre l'installation en ex\u00e9cutant l'application de nouveau.</html>
+m.abort_ok = Quitter
+m.abort_cancel = Continuer l'installation
+
+m.detecting_proxy = D\u00e9tection automatique des r\u00e9glages proxy
+
+m.configure_proxy =<html>Connexion au serveur impossible. \
+   <ul><li>  Veuillez v\u00e9rifier que <code>javaw.exe</code> n'est bloqu\u00e9 \
+   par aucun pare-feu ou antivirus. \
+   Vous pouvez vous rendre sur la configuration du pare-feu windows via \
+   (D\u00e9marrer ->  Panneau de Configuration ->  Pare-feu Windows ).</ul> \
+   <p>  Il est \u00e9galement possible que vous soyez derri\u00e8re un proxy que l'application \
+   est incapable de d\u00e9tecter automatiquement. \
+   Si tel est le cas, veuillez saisir les r\u00e9glages proxy ci-dessous.</html>
+
+m.proxy_extra =<html>Si vous \u00eates certain de ne pas utiliser de proxy, il est \
+   possible qu'une interruption temporaire de la connexion internet emp\u00fbche la \
+   communication avec les serveurs. Dans ce cas, vous pouvez relancer \
+   l'installation ult\u00e9rieurement.</html>
+
+m.proxy_host = Proxy IP
+m.proxy_port = Proxy port
+m.proxy_username = Nom d'utilisateur
+m.proxy_password = Mot de passe
+m.proxy_auth_required = Identification requise
+m.proxy_ok = OK
+m.proxy_cancel = Annuler
+
+m.downloading_java = T\u00e9l\u00e9chargement en cours de la Machine Virtuelle Java
+m.unpacking_java = D\u00e9compression en cours de la Machine Virtuelle Java
+
+m.resolving = R\u00e9solution des t\u00e9l\u00e9chargements en cours
+m.downloading = T\u00e9l\u00e9chargement des donn\u00e9es en cours
+m.failure = \u00c9chec du t\u00e9l\u00e9chargement: {0}
+
+m.checking = V\u00e9rification de la mise-\u00e0-jour en cours
+m.validating = Validation en cours
+m.patching = Modification en cours
+m.launching = Lancement en cours
+
+m.patch_notes = Notes de mise-\u00e0-jour
+
+m.complete = Complet \u00e0 {0}%
+m.remain = {0} restant
+
+m.updating_metadata = T\u00e9l\u00e9chargement des fichiers de contr\u00f4les en cours
+
+m.init_failed = Notre fichier de configuration est perdu ou corrompu. T\u00e9l\u00e9chargement \
+   d'une nouvelle copie en cours ...
+
+m.java_download_failed = Impossible de t\u00e9l\u00e9charger automatiquement la \
+   version de Java n\u00e9cessaire.\n\n\
+   Veuillez vous rendre sur www.java.com et t\u00e9l\u00e9charger et installer la version \
+   la plus r\u00e9cente de Java, avant d'ex\u00e9cuter l'application \u00e0 nouveau.
+
+m.java_unpack_failed = Impossible de d\u00e9compresser la version de \
+   Java n\u00e9cessaire. Veuillez v\u00e9rifier que vous avez au moins 100 MB d'espace libre \
+   sur votre disque dur puis tenter d'ex\u00e9cuter l'application \u00e0 nouveau.\n\n\
+   Si le probl\u00e8me persiste, rendez vous www.java.com et t\u00e9l\u00e9chargez et \
+   installez la version plus r\u00e9cente de Java puis essayez de nouveau.
+
+m.unable_to_repair = Impossible de t\u00e9l\u00e9charger les fichiers n\u00e9cessaires apr\u00e8s \
+   cinq tentatives. Vous pouvez tenter d'ex\u00e9cuter l'application \u00e0 nouveau, mais il est \
+   possible qu'une d\u00e9sinstallation / r\u00e9installation soit n\u00e9cessaire.
+
+m.unknown_error = Une erreur inconnue a fait \u00e9chouer le lancement de l'application. \
+   Veuillez visiter\n{0} pour plus d'informations.
+m.init_error = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause de l'erreur \
+   suivante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
+
+m.readonly_error = Le r\u00e9pertoire d'installation de cette application: \
+   \n{0}\nest en lecture seule. Veuillez installer l'application dans un r\u00e9pertoire avec \
+   un acc\u00e8s en \u00e9criture.
+
+m.missing_resource = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause d'une \
+   ressource manquante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
+
+m.insufficient_permissions_error = Vous n'avez pas accepter la signature \
+  num\u00e9rique de cette application. Si vous souhaitez ex\u00e9cuter cette application, vous \
+  devez accepter sa signature num\u00e9rique.\n\nAfin de le faire, vous devez quitter votre \
+  navigateur, le red\u00e9marrer, retourner \u00e0 cette page puis relancer l'application. \
+  Une fois la bo\u00eete de dialogue de s\u00e9curit\u00e9 affich\u00e9e, cliquez sur le bouton \
+  pour accepter la signature num\u00e9rique et accorder les permissions n\u00e9cessaires au bon \
+  fonctionnement de l'application.
+
+m.corrupt_digest_signature_error = Nous ne pouvons pas v\u00e9rifier la signature num\u00e9rique \
+  de l'application.\nVeuillez v\u00e9rifier que vous lancez l'application \ndepuis \
+  la bonne adresse internet.
+
+m.default_install_error = la section de support du site
+
+m.another_getdown_running = Plusieurs instances d'installation de cette \
+  application sont d\u00e9j\u00e0 en cours d'ex\u00e9cution.  Cette instance va s'arr\u00eater \
+  afin de permettre aux autres d'aboutir.
+
+m.applet_stopped = L'appelet Getdown a \u00e9t\u00e9 stopp\u00e9e.
+
+# application/digest errors
+m.missing_appbase = Le fichier de configuration ne contient pas 'appbase'.
+m.invalid_version = Le fichier de configuration sp\u00e9cifie une version invalide.
+m.invalid_appbase = Le fichier de configuration sp\u00e9cifie un 'appbase' invalide.
+m.missing_class = Le fichier de configuration ne contient pas la classe de l'application.
+m.missing_code = Le fichier de configuration ne sp\u00e9cifie aucune ressource de code.
+m.invalid_digest_file = Le fichier digest est invalide.
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_it.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_it.properties
new file mode 100644 (file)
index 0000000..33b3260
--- /dev/null
@@ -0,0 +1,114 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Annullare l'installazione?
+m.abort_confirm = <html>Sei sicuro di voler annullare l'installazione? \
+  Potrai riprenderla in seguito, riavviando nuovamente l'applicazione.</html>
+m.abort_ok = Chiudi
+m.abort_cancel = Continua l'installazione
+
+m.detecting_proxy = Provo a recuperare le configurazioni del proxy
+
+m.configure_proxy = <html>Impossibile collegarsi al server per \
+  recuperare i dati. \
+  <ul><li> Se il Firewall di Windows o Norton Internet Security bloccano \
+  <code>javaw.exe</code> non si possono scaricare i dati. Devi \
+  permettere a <code>javaw.exe</code> di accedere a internet. Puoi provare \
+  di nuovo, ma dovresti abilitare javaw.exe nella tua configurazione \
+  del firewall ( Start -> Pannello di Controllo -> Windows Firewall ).</ul> \
+  <p> Il tuo computer potrebbe accedere a internet attraverso un proxy e \
+  questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \
+  tue impostazioni del proxy, puoi inserirle di seguito.</html>
+
+m.proxy_extra = <html>Se sei sicuro di non usare proxy  \
+  potrebbe essere un problema di internet o di collegamento con il server. \
+  In questo caso puoi annullare e ripetere l'installazione più tardi.</html>
+
+m.proxy_host = IP Proxy
+m.proxy_port = Porta Proxy
+m.proxy_username = Nome utente
+m.proxy_password = Parola d'ordine
+m.proxy_auth_required = Autenticazione richiesta
+m.proxy_ok = OK
+m.proxy_cancel = Annulla
+
+m.downloading_java = Scaricando la Java Virtual Machine
+m.unpacking_java = Scompattando la Java Virtual Machine
+
+m.resolving = Recuperando i file da scaricare
+m.downloading = Download dei dati
+m.failure = Download fallito: {0}
+
+m.checking = Sto controllando gli aggiornamenti
+m.validating = Validazione
+m.patching = Applico le patch
+m.launching = Avvio
+
+m.patch_notes = Note delle Patch
+m.play_again = Avvia Nuovamente
+
+m.complete = {0}% completato
+m.remain = {0} rimasto
+
+m.updating_metadata = Scarico i file di controllo
+
+m.init_failed = La configurazione Ã¨ corrotta o mancante. Provo a \
+  scaricarne una nuova copia...
+
+m.java_download_failed = Impossibile scaricare la versione corretta \
+  di Java per il tuo computer.\n\n\
+  Visita www.java.com e scarica l'ultima versione di \
+  Java, poi lancia di nuovo l'applicazione.
+
+m.java_unpack_failed = Impossibile scompattare l'aggiornamento di \
+  Java. Verifica di avere almeno 100 MB di spazio libero nel tuo \
+  hard disk e prova a rilanciare l'applicazione.\n\n\
+  Se l'errore persiste, vistia www.java.com, scarica e \
+  installa l'ultima versione di Java e riprova.
+
+m.unable_to_repair = Impossibile scaricare i file necessari dopo 5 \
+  tentativi. Puoi provare a rilanciare l'applicazione, ma se fallisce \
+  di nuovo potresti dover reinstallarla.
+
+m.unknown_error = L'applicazione non Ã¨ stata avviata a causa di uno strano \
+  errore che non conosco. Visita\n{0} per avere informazioni \
+  in merito.
+m.init_error = L'applicazione non Ã¨ stata avviata a causa del seguente \
+  errore:\n{0}\n\nVistita\n{1} per avere \
+  informazioni su come risolvere il problema.
+
+m.readonly_error = La directory dove l'applicazione Ã¨ installata: \
+  \n{0}\nè in sola lettura. Installa l'applicazione dove hai i diritti \
+  di scrittura.
+
+m.missing_resource = L'applicazione non Ã¨ stata avviata a causa di mancanza \
+  di risorse:\n{0}\n\nVisita\n{1} per avere informazioni su come risolvere \
+  questi problemi.
+
+m.insufficient_permissions_error = Non hai accettato la \
+ firma digitale. Se vuoi eseguire l'applicazione devi accettare la \
+ firma digitale.\n\nPer farlo, riavvia il tuo browser \
+ e ritorna in questa pagina per rilanciare l'applicazione. Quando l'avviso \
+ di sicurezza viene mostrato, clicca per accettare la firma digitale \
+ ed eseguire l'applicazione con i privilegi necessari.
+
+m.corrupt_digest_signature_error = Impossibile verificare la firma digitale dell'applicazione \
+ .\nControlla di aver lanciato l'applicazione dal\n\
+ sito web corretto.
+
+m.default_install_error = la sezione di supporto del sito
+
+m.another_getdown_running = E' già in esecuzione un'istanza del programma. \
+ Questa verrà chiusa.
+
+m.applet_stopped = L'applet di Getdown Ã¨ stata interrotta.
+
+# application/digest errors
+m.missing_appbase = Il tag "appbase" Ã¨ mancante.
+m.invalid_version = Il file di configurazione non contiene una versione valida (tag "version").
+m.invalid_appbase = Il tag "appbase" non Ã¨ valido.
+m.missing_class = Il file di configurazione non contiene la classe da eseguire (tag "class").
+m.missing_code = Il file di configurazione non contiene alcuna risorsa (tag "code").
+m.invalid_digest_file = Il file di digest non Ã¨ valido.
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_ja.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_ja.properties
new file mode 100644 (file)
index 0000000..c344c16
--- /dev/null
@@ -0,0 +1,107 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f 
+m.abort_confirm = <html>\u672c\u5f53\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f  \
+  \u5f8c\u3067\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u305f\u969b\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u518d\u958b\u3067\u304d\u307e\u3059\u3002</html> 
+m.abort_ok = \u4e2d\u6b62 
+m.abort_cancel = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7d9a\u884c 
+
+m.detecting_proxy = \u81ea\u52d5\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u5b9f\u884c\u4e2d
+
+m.configure_proxy = <html>\u30b5\u30fc\u30d0\u306b\u63a5\u7d9a\u3067\u304d\u306a\u3044\u305f\u3081\u3001\u30b2\u30fc\u30e0\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b \
+  \u5931\u6557\u3057\u307e\u3057\u305f\u3002  \
+  <ul><li>\u30a6\u30a3\u30f3\u30c9\u30a6\u30ba\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u307e\u305f\u306f\u30ce\u30fc\u30c8\u30f3\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u304c \
+  <code>javaw.exe</code>\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b\u3088\u3046\u8a2d\u5b9a\u3057\u3066\u3042\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30e0\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3002  \u8a2d\u5b9a\u3092 \
+  <code>javaw.exe</code>\u7d4c\u7531\u3067\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u3088\u3046\u306b\u5909\u66f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002  \u30b2\u30fc\u30e0\u3092\u518d\u8d77\u52d5 \
+  \u3057\u305f\u5f8c\u3001\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u306e\u8a2d\u5b9a\u304b\u3089javaw.exe \u3092\u524a\u9664 \
+  \u3057\u3066\u304f\u3060\u3055\u3044\uff08\u30b9\u30bf\u30fc\u30c8\u2192\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u2192\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\uff09\u3002</ul> \
+  <p>\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u81ea\u52d5\u691c\u51fa\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306f \
+  \u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u3078\u30a2\u30af\u30bb\u30b9\u3057\u3066\u3044\u307e\u3059\u3002  \u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u8a73\u7d30\u304c \
+  \u308f\u304b\u3063\u3066\u3044\u308b\u5834\u5408\u306f\u3001\u4e0b\u306b\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html> 
+
+m.proxy_extra = <html>\u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u4e00\u6642\u7684\u306a \
+  \u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306e\u4e0d\u5177\u5408\u306b\u3088\u308a\u3001\u30b5\u30fc\u30d0\u3068\u4ea4\u4fe1\u3067\u304d\u306a\u3044\u72b6\u614b\u306b\u3042\u308b \
+  \u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002  \u305d\u306e\u5834\u5408\u306f\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u30ad\u30e3\u30f3\u30bb\u30eb\u3057\u3066\u3001 \
+  \u5f8c\u307b\u3069\u6539\u3081\u3066\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html> 
+
+m.proxy_host = \u30d7\u30ed\u30ad\u30b7IP 
+m.proxy_port = \u30d7\u30ed\u30ad\u30b7\u30dd\u30fc\u30c8
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = OK  
+m.proxy_cancel = \u30ad\u30e3\u30f3\u30bb\u30eb 
+
+m.downloading_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+m.unpacking_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u89e3\u51cd\u4e2d
+
+m.resolving = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306e\u8a2d\u5b9a\u4e2d
+m.downloading = \u30c7\u30fc\u30bf\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+m.failure = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5931\u6557\uff1a  {0} 
+
+m.checking = \u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u306e\u78ba\u8a8d\u4e2d
+m.validating = \u8a8d\u8a3c\u4e2d
+m.patching = \u4fee\u6b63\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u5b9f\u884c\u4e2d
+m.launching = \u5b9f\u884c\u4e2d
+
+m.complete = {0}\uff05\u5b8c\u4e86 
+m.remain = \u3000\u6b8b\u308a{0} 
+
+m.updating_metadata = \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+
+m.init_failed = \u74b0\u5883\u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u307e\u305f\u306f\u58ca\u308c\u3066\u3044\u307e\u3059\u3002  \u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
+  \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d\u2026 
+
+m.java_download_failed = \u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u3001Java\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u6700\u65b0 \
+  \u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u81ea\u52d5\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\n \
+  www.java.com \u304b\u3089\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u624b\u52d5\u3067\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u3001 \
+  \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.java_unpack_failed = Java\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u89e3\u51cd \
+  \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u30cf\u30fc\u30c9\u30c9\u30e9\u30a4\u30d6\u306e\u30e1\u30e2\u30ea\u304c100MB\u4ee5\u4e0a\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304b\u3089 \
+  \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n \
+  \u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001www.java.com \u304b\u3089Java\u306e\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
+  \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u304b\u3089\u3001\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002 
+
+m.unable_to_repair = 5\u56de\u8a66\u884c\u3057\u307e\u3057\u305f\u304c\u3001\u5fc5\u8981\u306a\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9 \
+  \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u5f8c\u307b\u3069\u6539\u3081\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \
+  \u518d\u5ea6\u5931\u6557\u3057\u305f\u5834\u5408\u306f\u3001\u30a2\u30f3\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u5f8c\u306b\u518d\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.unknown_error = \u539f\u56e0\u4e0d\u660e\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c \
+  \u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u89e3\u6c7a\u65b9\u6cd5\u3092\n{0}\u3067 \
+  \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+m.init_error = \u6b21\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
+  \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067 \
+  \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.readonly_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u305f\u30d5\u30a9\u30eb\u30c0\u306f  \
+  \n{0}\n\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002  \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u66f8\u304d\u8fbc\u307f\u304c\u3067\u304d\u308b\u30d5\u30a9\u30eb\u30c0\u306b \
+  \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.missing_resource = \u30ea\u30bd\u30fc\u30b9\u4e0d\u660e\u306e\u305f\u3081\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
+  \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067\u78ba\u8a8d \
+  \u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.insufficient_permissions_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u62d2\u5426 \
+ \u3055\u308c\u307e\u3057\u305f\u3002  \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3059\u308b\u5834\u5408\u306f\u3001\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u306e\u627f\u8a8d\u304c \
+ \u5fc5\u8981\u3067\u3059\u3002\n\n\u627f\u8a8d\u306b\u306f\u3001\u30d6\u30e9\u30a6\u30b6\u3092\u9589\u3058\u3066\u304b\u3089\u518d\u5ea6\u958b\u304d\u3001 \
+ \u672c\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3092\u518d\u8868\u793a\u3057\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044  \u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u306e \
+ \u8b66\u544a\u304c\u8868\u793a\u3055\u308c\u305f\u6642\u306f\u3001\u5b9f\u884c\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u3092\u627f\u8a8d\u3057\u3001 \
+ \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.corrupt_digest_signature_error = \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u8a8d\u8a3c \
+ \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\u6307\u5b9a\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u304b\u3089\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u304b\n \
+ \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
+
+m.default_install_error = \u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3067\u306e\u30b5\u30dd\u30fc\u30c8\u8868\u793a 
+
+# application/digest errors
+m.missing_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306eappbase\u304c\u4e0d\u660e\u3067\u3059\u3002 
+m.invalid_version = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306f\u7121\u52b9\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 
+m.invalid_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u306aappbase\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 
+m.missing_class = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30af\u30e9\u30b9\u304c\u4e0d\u660e\u3067\u3059\u3002 
+m.missing_code = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u3067\u30b3\u30fc\u30c9\u30ea\u30bd\u30fc\u30b9\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 
+m.invalid_digest_file = \u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u3067\u3059\u3002 
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_ko.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_ko.properties
new file mode 100644 (file)
index 0000000..3f8a47f
--- /dev/null
@@ -0,0 +1,102 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?
+m.abort_confirm = <html>\uC815\uB9D0\uB85C \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \
+  \uB098\uC911\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uC5EC \uC124\uCE58\uB97C \uC7AC\uAC1C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624.</html>
+m.abort_ok = \uC911\uC9C0
+m.abort_cancel = \uACC4\uC18D\uD558\uC5EC \uC124\uCE58
+
+m.detecting_proxy = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
+
+m.configure_proxy = <html>\uAC8C\uC784 \uB370\uC774\uD130\uB97C \uBC1B\uAE30 \uC704\uD55C \uC11C\uBC84 \uC811\uC18D\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.\
+  <ul><li>\uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD \uB610\uB294 \uB178\uD134 \uC778\uD130\uB137 \uC2DC\uD050\uB9AC\uD2F0\uAC00 <code>javaw.exe</code>\uC774 \uC124\uC815\uC5D0\uC11C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC744 \uACBD\uC6B0, \
+  \uAC8C\uC784 \uB370\uC774\uD130\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
+  <code>javaw.exe</code>\uAC00 \uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD560 \uC218 \uC788\uB3C4\uB85D \uC124\uC815\uC744 \uBCC0\uACBD\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+  \uAC8C\uC784\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD55C \uD6C4, \uBC29\uD654\uBCBD \uC124\uC815\uC5D0\uC11C javaw.exe\uB97C \uC0AD\uC81C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+  ( \uC2DC\uC791 -> \uC81C\uC5B4\uD310 -> \uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD )</ul> \
+  <p> \uCEF4\uD4E8\uD130\uAC00 \uD504\uB85D\uC2DC \uC11C\uBC84\uB97C \uD1B5\uD574 \uC778\uD130\uB137\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB2E4\uBA74, \uD504\uB85D\uC2DC \uC124\uC815\uC758 \uC790\uB3D9 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC73C\uBBC0\uB85C, \
+  \uC0AC\uC6A9\uD558\uB294 \uD504\uB85D\uC2DC \uC124\uC815\uC744 \uC54C\uACE0 \uC788\uC744 \uACBD\uC6B0 \uC544\uB798\uC5D0 \uC785\uB825\uD558\uC5EC \uC8FC\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.</html>
+
+m.proxy_extra = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
+
+m.proxy_host = \uD504\uB85D\uC2DC IP
+m.proxy_port = \uD504\uB85D\uC2DC \uD3EC\uD2B8
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = OK
+m.proxy_cancel = \uCDE8\uC18C
+
+m.downloading_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uB2E4\uC6B4\uB85C\uB4DC \uC911
+m.unpacking_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uC555\uCD95\uC744 \uD574\uC81C\uD558\uB294 \uC911
+
+m.resolving = \uB2E4\uC6B4\uB85C\uB4DC \uBD84\uC11D \uC911
+m.downloading = \uB370\uC774\uD130 \uB2E4\uC6B4\uB85C\uB4DC \uC911
+m.failure = \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: {0}
+
+m.checking = \uC5C5\uB370\uC774\uD2B8 \uCCB4\uD06C
+m.validating = \uC720\uD6A8\uC131 \uAC80\uC0AC \uC911
+m.patching = \uD328\uCE58 \uC911
+m.launching = \uC2E4\uD589 \uC911
+
+m.patch_notes = \uD328\uCE58 \uB178\uD2B8
+m.play_again = \uB2E4\uC2DC \uC2E4\uD589
+
+m.complete = {0}% \uC644\uB8CC
+m.remain = {0} \uB0A8\uC74C
+
+m.updating_metadata = \uCEE8\uD2B8\uB864 \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911
+
+m.init_failed = \uC124\uC815 \uD30C\uC77C\uC774 \uB204\uB77D\uB418\uC5C8\uAC70\uB098 \uBCC0\uD615\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
+  \uC0C8\uB85C\uC6B4 \uBCF5\uC0AC\uBCF8\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911\uC785\uB2C8\uB2E4...
+
+m.java_download_failed = \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uD544\uC694\uD55C \uC0C8\uB85C\uC6B4 \uBC84\uC804\uC758 \uC790\uBC14\uB97C \uC790\uB3D9\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\n\
+  \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
+  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
+
+m.java_unpack_failed = \uC5C5\uB370\uC774\uD2B8\uB41C \uBC84\uC804\uC758 \uC790\uBC14\uC758 \uC555\uCD95\uC744 \uD480 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
+  \uD558\uB4DC\uB4DC\uB77C\uC774\uBE0C\uC5D0 \uCD5C\uC18C\uD55C 100MB\uC758 \uC6A9\uB7C9\uC744 \uD655\uBCF4\uD55C \uC774\uD6C4, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.\n\n\
+  \uB9CC\uC57D \uBB38\uC81C\uAC00 \uD574\uACB0\uB418\uC9C0 \uC54A\uB294\uB2E4\uBA74, \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
+  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
+
+m.unable_to_repair = \uB2E4\uC12F\uBC88\uC758 \uC2DC\uB3C4\uC5D0\uB3C4 \uD544\uC694\uD55C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \
+  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2DC\uC791\uD574\uBCF4\uC2DC\uACE0, \uADF8\uB798\uB3C4 \uB2E4\uC6B4\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD55C\uB2E4\uBA74, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC81C\uAC70\uD55C \uD6C4, \uB2E4\uC2DC \uC2E4\uD589\uD574\uBCF4\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.unknown_error = \uBCF5\uAD6C\uB420 \uC218 \uC5C6\uB294 \uC624\uB958\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
+  \n{0}\uC5D0 \uB300\uD55C \uBCF5\uAD6C \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.init_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC544\uB798\uC640 \uAC19\uC740 \uC5D0\uB7EC\uB85C \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC5D0\uB7EC:\
+  \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.readonly_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC124\uCE58\uB41C \uB514\uB809\uD1A0\uB9AC: \
+  \n{0}\n\uAC00 \uC77D\uAE30 \uC804\uC6A9\uC785\uB2C8\uB2E4. \uC77D\uAE30 \uAD8C\uD55C\uC774 \uC2B9\uC778\uB41C \uB809\uD1A0\uB9AC\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC124\uCE58\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.missing_resource = \uB9AC\uC18C\uC2A4\uC758 \uC190\uC2E4\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. : \
+  \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.insufficient_permissions_error = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \
+ \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uAE30 \uC704\uD574\uC11C \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+ \n\n\uADF8\uB9AC\uACE0 \uB098\uC11C \uC6F9 \uBE0C\uB77C\uC6B0\uC800\uB97C \uB2EB\uACE0 \uB2E4\uC2DC \uC2DC\uC791\uD558\uC5EC \uC6F9\uD398\uC774\uC9C0\uB85C \uB3CC\uC544\uC640 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC7AC\uC2DC\uC791\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. \
+ \uBCF4\uC548\uC5D0 \uB300\uD55C \uB300\uD654\uC0C1\uC790\uAC00 \uBCF4\uC774\uBA74, \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC5D0 \uB300\uD55C \uD655\uC778 \uBC84\uD2BC\uC744 \uD074\uB9AD\uD558\uACE0, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uAE30 \uC704\uD55C \
+ \uAD8C\uD55C\uC744 \uBD80\uC5EC\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.corrupt_digest_signature_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n \
+ \uC62C\uBC14\uB978 \uC6F9\uC0AC\uC774\uD2B8\uC5D0\uC11C \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uACE0 \uC788\uB294 \uC9C0 \uD655\uC778\uBC14\uB78D\uB2C8\uB2E4.
+
+m.default_install_error = \uC6F9\uC0AC\uC774\uD2B8\uC758 \uC9C0\uC6D0 \uBA54\uB274(support section)\uB97C \uD655\uC778\uD558\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
+
+m.another_getdown_running = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uC778\uC2A4\uD1A8\uB7EC\uC758 \uB2E4\uC911 \uC778\uC2A4\uD134\uC2A4\uAC00 \uC2E4\uD589\uC911\uC785\uB2C8\uB2E4. \
+ \uD558\uB098\uAC00 \uC644\uB8CC\uB420 \uB54C\uAE4C\uC9C0 \uC911\uB2E8\uB429\uB2C8\uB2E4.
+
+m.applet_stopped = Getdown \uC560\uD50C\uB9BF \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
+
+# application/digest errors
+m.missing_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 'appbase' \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
+m.invalid_version = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C \uBC84\uC804\uC774 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
+m.invalid_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C 'appbase'\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
+m.missing_class = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uD074\uB798\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
+m.missing_code = \uC124\uC815 \uD30C\uC77C\uC5D0 \uB9AC\uC18C\uC2A4\uC5D0 \uB300\uD55C \uCF54\uB4DC\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
+m.invalid_digest_file = \uB2E4\uC774\uC81C\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC798\uBABB\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_pt.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_pt.properties
new file mode 100644 (file)
index 0000000..47db91c
--- /dev/null
@@ -0,0 +1,118 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.abort_title = Cancelar a instala\u00E7\u00E3o?
+m.abort_confirm = <html>Tem certeza que deseja cancelar a instala\u00E7\u00E3o? \
+  Voc\u00EA pode continuar a instala\u00E7\u00E3o mais tarde, \
+  basta executar a aplica\u00E7\u00E3o novamente.</html>
+m.abort_ok = Sair
+m.abort_cancel = Continuar a instala\u00E7\u00E3o
+
+m.detecting_proxy = Tentando detectar automaticamente as configura\u00E7\u00F5es de proxy
+
+m.configure_proxy = <html>N\u00E3o foi poss\u00EDvel conectar aos nossos servidores para \
+  fazer o download dos dados. \
+  <ul><li> Se o Firewall do Windows ou o Norton Internet Security est\u00E1 configurado \
+  para bloquear o programa <code>javaw.exe</code> n\u00E3o ser\u00E1 poss\u00EDvel realizar \
+  o download. Voc\u00EA ter\u00E1 que permitir que o programa <code>javaw.exe</code> acesse \
+  a internet. Voc\u00EA pode tentar executar o programa novamente, mas voc\u00EA precisa \
+  remover o programa javaw.exe das configura\u00E7\u00F5es do firewall (Iniciar -> Painel \
+  de controle -> Firewall do Windows).</ul> \
+  <p> Seu computador pode estar acessando a internet atrav\u00E9s de um proxy e n\u00E3o foi \
+  capaz de detectar automaticamente as configura\u00E7\u00F5es de proxy. \
+  Voc\u00EA pode informar esses dados abaixo.</html>
+
+m.proxy_extra = <html>Se voc\u00EA tem certeza que n\u00E3o usa um proxy, ent\u00E3o pode ser \
+  que exista um problema tempor\u00E1rio que est\u00E1 impedindo a comunica\u00E7\u00E3o \
+  com os nossos servidores. Neste caso voc\u00EA pode cancelar e tentar instalar novamente \
+  mais tarde.</html>
+
+m.proxy_host = IP do Proxy
+m.proxy_port = Porta do Proxy
+m.proxy_username = Nome de usu\u00e1rio
+m.proxy_password = Senha
+m.proxy_auth_required = Autentifica\u00e7\u00e3o requerida
+m.proxy_ok = OK
+m.proxy_cancel = Cancelar
+
+m.downloading_java = Fazendo o download da m\u00E1quina virtual Java
+m.unpacking_java = Descompactando a m\u00E1quina virtual Java
+
+m.resolving = Resolvendo downloads
+m.downloading = Transferindo dados
+m.failure = Download falhou: {0}
+
+m.checking = Verificando atualiza\u00E7\u00F5es
+m.validating = Validando
+m.patching = Atualizando
+m.launching = Executando
+
+m.patch_notes = Corrigir notas
+m.play_again = Jogar de novo
+
+m.complete = {0}% completo
+m.remain = {0} Permanecer
+
+m.updating_metadata = Transferindo arquivos de controle
+
+m.init_failed = Nosso arquivo de configura\u00E7\u00E3o est\u00E1 ausente ou corrompido. Tente \
+  baixar uma nova c\u00F3pia...
+
+m.java_download_failed = N\u00E3o conseguimos baixar automaticamente a\
+  vers\u00E3o necess\u00E1ria do Java para o seu computador.\n\n\
+  Por favor, acesse www.java.com, baixe e instale a \u00FAltima vers\u00E3o do \
+  Java, em seguida, tente executar o aplicativo novamente.
+
+m.java_unpack_failed = N\u00E3o conseguimos descompactar uma vers\u00E3o atualizada do \
+  Java. Por favor, certifique-se de ter pelo menos 100 MB de espa\u00E7o livre em seu \
+  disco r\u00EDgido e tente executar o aplicativo novamente. \n\n\
+  Se isso n\u00E3o resolver o problema, acesse www.java.com,baixe e \
+  instale a \u00FAltima vers\u00E3o do Java e tente novamente.
+
+m.unable_to_repair = N\u00E3o conseguimos baixar os arquivos necess\u00E1rios depois de \
+  cinco tentativas. Voc\u00EA pode tentar executar o aplicativo novamente, mas se ele \
+  falhar pode ser necess\u00E1rio desinstalar e reinstalar.
+
+m.unknown_error = A aplica\u00E7\u00E3o falhou ao iniciar devido a algum erro estranho \
+  do qual n\u00E3o conseguimos recuperar. Por favor, visite \n{0} para obter \
+  informa\u00E7\u00F5es sobre como recuperar.
+m.init_error = A aplica\u00E7\u00E3o falhou ao iniciar devido ao seguinte \
+  erro:\n{0}\n\nPor favor visite \n{1} para \
+  informa\u00E7\u00F5es sobre como lidar com esse problema.
+
+m.readonly_error =O diret\u00F3rio no qual este aplicativo est\u00E1 instalado: \
+  \n{0}\n \u00E9 somente leitura. Por favor, instale o aplicativo em um diret\u00F3rio onde \
+  voc\u00EA tem acesso de grava\u00E7\u00E3o.
+
+m.missing_resource = A aplica\u00E7\u00E3o falhou ao iniciar devido a uma falta \
+  de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informa\u00E7\u00F5es sobre \
+  como lidar com tal problema.
+
+m.insufficient_permissions_error = Voc\u00EA n\u00E3o aceitou a assinatura digital \
+  do aplicativo. Se voc\u00EA quiser executar o aplicativo, voc\u00EA ter\u00E1 que aceitar \
+  a assinatura digital. \n\nPara fazer isso, voc\u00EA ter\u00E1 que sair do seu navegador, \
+  reinici\u00E1-lo, e retornar a esta p\u00E1gina web para executar a aplica\u00E7\u00E3o. \
+  Quando o di\u00E1logo de seguran\u00E7a aparecer, clique no bot\u00E3o para aceitar a \
+  assinatura digital e conceder a este aplicativo os privil\u00E9gios necess\u00E1rios \
+  para executar.
+
+m.corrupt_digest_signature_error = N\u00E3o conseguimos verificar a assinatura digital \
+  do aplicativo.\nPor favor, verifique se voc\u00EA est\u00E1 utilizando o aplicativo \nde um \
+  site correto.
+
+m.default_install_error = a se\u00E7\u00E3o de suporte do site
+
+m.another_getdown_running = V\u00E1rias inst\u00E2ncias desta aplica\u00E7\u00E3o \
+  est\u00E3o em execu\u00E7\u00E3o. Esta ir\u00E1 parar e deixar outra completar suas atividades.
+
+m.applet_stopped = Foi solicitado ao miniaplicativo GetDow que parasse de trabalhar.
+
+# application/digest errors
+m.missing_appbase = O arquivo de configura\u00E7\u00E3o n\u00E3o possui o 'AppBase'.
+m.invalid_version = O arquivo de configura\u00E7\u00E3o especifica uma vers\u00E3o inv\u00E1lida.
+m.invalid_appbase = O arquivo de configura\u00E7\u00E3o especifica um 'AppBase' inv\u00E1lido.
+m.missing_class = O arquivo de configura\u00E7\u00E3o n\u00E3o possui a classe de aplicativo.
+m.missing_code = O arquivo de configura\u00E7\u00E3o n\u00E3o especifica um recurso de c\u00F3digo.
+m.invalid_digest_file = O arquivo digest \u00E9 inv\u00E1lido.
diff --git a/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_zh.properties b/getdown/src/getdown/launcher/target/classes/com/threerings/getdown/messages_zh.properties
new file mode 100644 (file)
index 0000000..2c27543
--- /dev/null
@@ -0,0 +1,61 @@
+#
+# $Id$
+#
+# Getdown translation messages
+
+m.detecting_proxy = \u641c\u5bfb\u4ee3\u7406\u670d\u52a1\u5668
+
+m.configure_proxy = <html>\u6211\u4eec\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u4e0b\u8f7d\u6e38\u620f\u6570\u636e\u3002\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e \
+  \u60a8\u7684\u8ba1\u7b97\u673a\u662f\u901a\u8fc7\u4ee3\u7406\u670d\u52a1\u5668\u8fde\u63a5\u4e92\u8054\u7f51\u7684\uff0c\u5e76\u4e14\u6211\u4eec\u65e0\u6cd5\u81ea\u52a8\u83b7\u5f97\u4ee3\u7406\u670d\u52a1\u5668\u7684 \
+  \u8bbe\u7f6e\u3002\u5982\u679c\u60a8\u77e5\u9053\u60a8\u4ee3\u7406\u670d\u52a1\u5668\u7684\u8bbe\u7f6e\uff0c\u60a8\u53ef\u4ee5\u5728\u4e0b\u9762\u8f93\u5165\u3002</html>
+
+m.proxy_extra = <html>\u5982\u679c\u60a8\u786e\u5b9a\u60a8\u6ca1\u6709\u4f7f\u7528\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e\u6682\u65f6\u65e0\u6cd5 \
+  \u8fde\u63a5\u5230\u4e92\u8054\u7f51\uff0c\u5bfc\u81f4\u65e0\u6cd5\u548c\u670d\u52a1\u5668\u901a\u8baf\u3002\u8fd9\u79cd\u60c5\u51b5\uff0c\u60a8\u53ef\u4ee5\u53d6\u6d88\uff0c\u7a0d\u5019\u518d\u91cd\u65b0\u5b89\u88c5\u3002<br><br> \
+  \u5982\u679c\u60a8\u65e0\u6cd5\u786e\u5b9a\u60a8\u662f\u5426\u4f7f\u7528\u4e86\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8bf7\u8bbf\u95ee\u6211\u4eec\u7f51\u7ad9\u4e2d\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c \
+  \u4e86\u89e3\u5982\u4f55\u68c0\u6d4b\u60a8\u7684\u4ee3\u7406\u670d\u52a1\u5668\u8bbe\u7f6e\u3002</html>
+
+m.proxy_host = \u4ee3\u7406\u670d\u52a1\u5668\u7684IP\u5730\u5740
+m.proxy_port = \u4ee3\u7406\u670d\u52a1\u5668\u7684\u7aef\u53e3\u53f7
+m.proxy_username = Username
+m.proxy_password = Password
+m.proxy_auth_required = Authentication required
+m.proxy_ok = \u786e\u5b9a
+m.proxy_cancel = \u53d6\u6d88
+
+m.resolving = \u5206\u6790\u9700\u4e0b\u8f7d\u5185\u5bb9
+m.downloading = \u4e0b\u8f7d\u6570\u636e
+m.failure = \u4e0b\u8f7d\u5931\u8d25: {0}
+
+m.checking = \u68c0\u67e5\u66f4\u65b0\u5185\u5bb9
+m.validating = \u786e\u8ba4
+m.patching = \u5347\u7ea7
+m.launching = \u542f\u52a8
+
+m.complete = {0}% \u5b8c\u6210
+m.remain = {0} \u5269\u4f59\u65f6\u95f4
+
+m.updating_metadata = \u4e0b\u8f7d\u63a7\u5236\u6587\u4ef6
+
+m.init_failed = \u65e0\u6cd5\u627e\u5230\u914d\u7f6e\u6587\u4ef6\u6216\u5df2\u635f\u574f\u3002\u5c1d\u8bd5\u91cd\u65b0\u4e0b\u8f7d...
+
+m.unable_to_repair = \u7ecf\u8fc75\u6b21\u5c1d\u8bd5\uff0c\u4f9d\u7136\u65e0\u6cd5\u4e0b\u8f7d\u6240\u9700\u7684\u6587\u4ef6\u3002\
+\u60a8\u53ef\u4ee5\u91cd\u65b0\u8fd0\u884c\u7a0b\u5e8f\uff0c\u4f46\u662f\u5982\u679c\u4f9d\u7136\u5931\u8d25\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u53cd\u5b89\u88c5\u5e76\u91cd\u65b0\u5b89\u88c5\u3002
+
+
+m.unknown_error = \u7531\u4e8e\u4e00\u4e9b\u65e0\u6cd5\u56de\u590d\u7684\u4e25\u91cd\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\u3002\
+\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u89e3\u51b3\u95ee\u9898\u3002
+
+m.init_error = \u7531\u4e8e\u4e0b\u5217\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
+\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u9519\u8bef\u3002
+
+
+m.missing_resource = \u7531\u4e8e\u65e0\u6cd5\u627e\u5230\u4e0b\u5217\u8d44\u6e90\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
+\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u95ee\u9898\u3002
+
+# application/digest errors
+m.missing_appbase = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230 'appbase'\u3002
+m.invalid_version = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684\u7248\u672c\u3002
+m.invalid_appbase = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684 'appbase'\u3002
+m.missing_class = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u7a0b\u5e8f\u6587\u4ef6\u3002
+m.missing_code = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u6307\u5b9a\u7684\u8d44\u6e90\u3002
+m.invalid_digest_file = \u65e0\u6548\u7684\u914d\u7f6e\u6587\u4ef6\u3002
diff --git a/getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT.jar b/getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT.jar
new file mode 100644 (file)
index 0000000..f71b6ee
Binary files /dev/null and b/getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT.jar differ
diff --git a/getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT_proguard_base.jar b/getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT_proguard_base.jar
new file mode 100644 (file)
index 0000000..88428a4
Binary files /dev/null and b/getdown/src/getdown/launcher/target/getdown-launcher-1.8.3-SNAPSHOT_proguard_base.jar differ
diff --git a/getdown/src/getdown/launcher/target/maven-archiver/pom.properties b/getdown/src/getdown/launcher/target/maven-archiver/pom.properties
new file mode 100644 (file)
index 0000000..ab0c975
--- /dev/null
@@ -0,0 +1,4 @@
+#Created by Apache Maven 3.5.2
+version=1.8.3-SNAPSHOT
+groupId=com.threerings.getdown
+artifactId=getdown-launcher
diff --git a/getdown/src/getdown/launcher/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/getdown/src/getdown/launcher/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644 (file)
index 0000000..ecc6201
--- /dev/null
@@ -0,0 +1,28 @@
+com/threerings/getdown/launcher/GetdownApp$1$1.class
+com/threerings/getdown/launcher/Getdown$4.class
+com/threerings/getdown/launcher/RotatingBackgrounds$ImageLoader.class
+com/threerings/getdown/launcher/ProxyPanel$SaneTextField.class
+com/threerings/getdown/launcher/GetdownApp$2.class
+com/threerings/getdown/launcher/Getdown$2.class
+com/threerings/getdown/launcher/ProxyPanel.class
+com/threerings/getdown/launcher/GetdownApp.class
+com/threerings/getdown/launcher/Getdown$ProgressReporter.class
+com/threerings/getdown/launcher/ProxyPanel$SaneLabelField.class
+com/threerings/getdown/launcher/ProxyUtil$1.class
+com/threerings/getdown/launcher/ProxyPanel$SanePasswordField.class
+com/threerings/getdown/launcher/StatusPanel$1.class
+com/threerings/getdown/launcher/GetdownApp$1$3.class
+com/threerings/getdown/launcher/AbortPanel.class
+com/threerings/getdown/launcher/GetdownApp$1$2.class
+com/threerings/getdown/launcher/ProxyUtil.class
+com/threerings/getdown/launcher/ProxyPanel$1.class
+com/threerings/getdown/launcher/Getdown$5.class
+com/threerings/getdown/launcher/StatusPanel.class
+com/threerings/getdown/launcher/GetdownApp$1.class
+com/threerings/getdown/launcher/RotatingBackgrounds.class
+com/threerings/getdown/launcher/MultipleGetdownRunning.class
+com/threerings/getdown/launcher/Getdown$1.class
+com/threerings/getdown/launcher/Getdown.class
+com/threerings/getdown/launcher/Getdown$3.class
+com/threerings/getdown/launcher/Getdown$6.class
+com/threerings/getdown/launcher/Getdown$4$1.class
diff --git a/getdown/src/getdown/launcher/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/getdown/src/getdown/launcher/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644 (file)
index 0000000..5ad07dd
--- /dev/null
@@ -0,0 +1,8 @@
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/MultipleGetdownRunning.java
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
+/Users/bsoares/git/getdown2/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
diff --git a/getdown/src/getdown/launcher/target/proguard_map.txt b/getdown/src/getdown/launcher/target/proguard_map.txt
new file mode 100644 (file)
index 0000000..4ecff17
--- /dev/null
@@ -0,0 +1,1349 @@
+ca.beq.util.win32.registry.KeyIterator -> ca.beq.util.win32.registry.KeyIterator:
+    ca.beq.util.win32.registry.RegistryKey m_key -> m_key
+    int m_index -> m_index
+    int m_hkey -> m_hkey
+    int m_maxsize -> m_maxsize
+    int m_count -> m_count
+    void <init>(ca.beq.util.win32.registry.RegistryKey) -> <init>
+    void initializeFields() -> initializeFields
+    boolean hasNext() -> hasNext
+    java.lang.Object next() -> next
+    java.lang.String getNext() -> getNext
+    void remove() -> remove
+ca.beq.util.win32.registry.RegistryException -> ca.beq.util.win32.registry.RegistryException:
+    void <init>() -> <init>
+    void <init>(java.lang.String) -> <init>
+ca.beq.util.win32.registry.RegistryKey -> ca.beq.util.win32.registry.RegistryKey:
+    boolean c_initSucceeded -> c_initSucceeded
+    ca.beq.util.win32.registry.RootKey m_root -> m_root
+    java.lang.String m_path -> m_path
+    void testInitialized() -> testInitialized
+    void initialize() -> initialize
+    void initialize(java.lang.String) -> initialize
+    boolean isInitialized() -> isInitialized
+    void checkInitialized() -> checkInitialized
+    void <init>() -> <init>
+    void <init>(ca.beq.util.win32.registry.RootKey) -> <init>
+    void <init>(java.lang.String) -> <init>
+    void <init>(ca.beq.util.win32.registry.RootKey,java.lang.String) -> <init>
+    ca.beq.util.win32.registry.RootKey getRootKey() -> getRootKey
+    java.lang.String getPath() -> getPath
+    java.lang.String makePath(java.lang.String) -> makePath
+    java.lang.String getName() -> getName
+    boolean exists() -> exists
+    void create() -> create
+    ca.beq.util.win32.registry.RegistryKey createSubkey(java.lang.String) -> createSubkey
+    void delete() -> delete
+    boolean hasSubkeys() -> hasSubkeys
+    boolean hasSubkey(java.lang.String) -> hasSubkey
+    java.util.Iterator subkeys() -> subkeys
+    java.util.Iterator values() -> values
+    boolean hasValue(java.lang.String) -> hasValue
+    boolean hasValues() -> hasValues
+    ca.beq.util.win32.registry.RegistryValue getValue(java.lang.String) -> getValue
+    void setValue(ca.beq.util.win32.registry.RegistryValue) -> setValue
+    void deleteValue(java.lang.String) -> deleteValue
+    java.lang.String toString() -> toString
+    void <clinit>() -> <clinit>
+ca.beq.util.win32.registry.RegistryValue -> ca.beq.util.win32.registry.RegistryValue:
+    java.lang.String m_name -> m_name
+    ca.beq.util.win32.registry.ValueType m_type -> m_type
+    java.lang.Object m_data -> m_data
+    void <init>() -> <init>
+    void <init>(java.lang.Object) -> <init>
+    void <init>(java.lang.String,java.lang.Object) -> <init>
+    void <init>(java.lang.String,ca.beq.util.win32.registry.ValueType,java.lang.Object) -> <init>
+    void <init>(java.lang.String,boolean) -> <init>
+    void <init>(java.lang.String,byte) -> <init>
+    void <init>(java.lang.String,int) -> <init>
+    void <init>(java.lang.String,long) -> <init>
+    void <init>(java.lang.String,float) -> <init>
+    void <init>(java.lang.String,double) -> <init>
+    java.lang.String getName() -> getName
+    void setName(java.lang.String) -> setName
+    ca.beq.util.win32.registry.ValueType getType() -> getType
+    void setType(ca.beq.util.win32.registry.ValueType) -> setType
+    java.lang.Object getData() -> getData
+    void setData(java.lang.Object) -> setData
+    void setData(byte) -> setData
+    void setData(boolean) -> setData
+    void setData(int) -> setData
+    void setData(long) -> setData
+    void setData(float) -> setData
+    void setData(double) -> setData
+    java.lang.String getStringValue() -> getStringValue
+    java.lang.String toString() -> toString
+ca.beq.util.win32.registry.RootKey -> ca.beq.util.win32.registry.RootKey:
+    java.lang.String m_name -> m_name
+    int m_value -> m_value
+    ca.beq.util.win32.registry.RootKey HKEY_CLASSES_ROOT -> HKEY_CLASSES_ROOT
+    ca.beq.util.win32.registry.RootKey HKEY_CURRENT_USER -> HKEY_CURRENT_USER
+    ca.beq.util.win32.registry.RootKey HKEY_LOCAL_MACHINE -> HKEY_LOCAL_MACHINE
+    ca.beq.util.win32.registry.RootKey HKEY_USERS -> HKEY_USERS
+    ca.beq.util.win32.registry.RootKey HKEY_CURRENT_CONFIG -> HKEY_CURRENT_CONFIG
+    ca.beq.util.win32.registry.RootKey HKEY_PERFORMANCE_DATA -> HKEY_PERFORMANCE_DATA
+    ca.beq.util.win32.registry.RootKey HKEY_DYN_DATA -> HKEY_DYN_DATA
+    void <init>(java.lang.String,int) -> <init>
+    int getValue() -> getValue
+    java.lang.String toString() -> toString
+    void <clinit>() -> <clinit>
+ca.beq.util.win32.registry.ValueIterator -> ca.beq.util.win32.registry.ValueIterator:
+    ca.beq.util.win32.registry.RegistryKey m_key -> m_key
+    int m_index -> m_index
+    int m_hkey -> m_hkey
+    int m_maxsize -> m_maxsize
+    int m_count -> m_count
+    void <init>(ca.beq.util.win32.registry.RegistryKey) -> <init>
+    void initializeFields() -> initializeFields
+    boolean hasNext() -> hasNext
+    java.lang.Object next() -> next
+    java.lang.String getNext() -> getNext
+    void remove() -> remove
+ca.beq.util.win32.registry.ValueType -> ca.beq.util.win32.registry.ValueType:
+    java.lang.String m_name -> m_name
+    int m_value -> m_value
+    ca.beq.util.win32.registry.ValueType REG_NONE -> REG_NONE
+    ca.beq.util.win32.registry.ValueType REG_SZ -> REG_SZ
+    ca.beq.util.win32.registry.ValueType REG_EXPAND_SZ -> REG_EXPAND_SZ
+    ca.beq.util.win32.registry.ValueType REG_BINARY -> REG_BINARY
+    ca.beq.util.win32.registry.ValueType REG_DWORD -> REG_DWORD
+    ca.beq.util.win32.registry.ValueType REG_DWORD_LITTLE_ENDIAN -> REG_DWORD_LITTLE_ENDIAN
+    ca.beq.util.win32.registry.ValueType REG_DWORD_BIG_ENDIAN -> REG_DWORD_BIG_ENDIAN
+    ca.beq.util.win32.registry.ValueType REG_MULTI_SZ -> REG_MULTI_SZ
+    ca.beq.util.win32.registry.ValueType REG_RESOURCE_LIST -> REG_RESOURCE_LIST
+    ca.beq.util.win32.registry.ValueType REG_LINK -> REG_LINK
+    ca.beq.util.win32.registry.ValueType REG_FULL_RESOURCE_DESCRIPTOR -> REG_FULL_RESOURCE_DESCRIPTOR
+    ca.beq.util.win32.registry.ValueType REG_RESOURCE_REQUIREMENTS_LIST -> REG_RESOURCE_REQUIREMENTS_LIST
+    void <init>(java.lang.String,int) -> <init>
+    int getValue() -> getValue
+    java.lang.String toString() -> toString
+    void <clinit>() -> <clinit>
+com.samskivert.Log -> com.a.a:
+    com.samskivert.util.Logger log -> a
+    void <clinit>() -> <clinit>
+com.samskivert.swing.DimenInfo -> com.a.a.a:
+    int count -> a
+    int totwid -> b
+    int tothei -> c
+    int maxwid -> d
+    int maxhei -> e
+    int numfix -> f
+    int fixwid -> g
+    int fixhei -> h
+    int maxfreewid -> i
+    int maxfreehei -> j
+    int totweight -> k
+    java.awt.Dimension[] dimens -> l
+    void <init>() -> <init>
+    java.lang.String toString() -> toString
+    boolean equals(java.lang.Object,java.lang.Object) -> a
+com.samskivert.swing.GroupLayout -> com.a.a.b:
+    com.samskivert.swing.GroupLayout$Constraints FIXED -> a
+    com.samskivert.swing.GroupLayout$Policy NONE -> b
+    com.samskivert.swing.GroupLayout$Policy STRETCH -> c
+    com.samskivert.swing.GroupLayout$Policy EQUALIZE -> d
+    com.samskivert.swing.GroupLayout$Policy CONSTRAIN -> e
+    com.samskivert.swing.GroupLayout$Justification CENTER -> f
+    com.samskivert.swing.GroupLayout$Justification LEFT -> g
+    com.samskivert.swing.GroupLayout$Justification RIGHT -> h
+    com.samskivert.swing.GroupLayout$Justification TOP -> i
+    com.samskivert.swing.GroupLayout$Justification BOTTOM -> j
+    com.samskivert.swing.GroupLayout$Policy _policy -> k
+    com.samskivert.swing.GroupLayout$Policy _offpolicy -> l
+    int _gap -> m
+    com.samskivert.swing.GroupLayout$Justification _justification -> n
+    com.samskivert.swing.GroupLayout$Justification _offjust -> o
+    java.util.HashMap _constraints -> p
+    com.samskivert.swing.GroupLayout$Constraints DEFAULT_CONSTRAINTS -> q
+    void <init>() -> <init>
+    void addLayoutComponent(java.lang.String,java.awt.Component) -> addLayoutComponent
+    void removeLayoutComponent(java.awt.Component) -> removeLayoutComponent
+    void addLayoutComponent(java.awt.Component,java.lang.Object) -> addLayoutComponent
+    float getLayoutAlignmentX(java.awt.Container) -> getLayoutAlignmentX
+    float getLayoutAlignmentY(java.awt.Container) -> getLayoutAlignmentY
+    java.awt.Dimension minimumLayoutSize(java.awt.Container) -> minimumLayoutSize
+    java.awt.Dimension preferredLayoutSize(java.awt.Container) -> preferredLayoutSize
+    java.awt.Dimension maximumLayoutSize(java.awt.Container) -> maximumLayoutSize
+    java.awt.Dimension getLayoutSize(java.awt.Container,int) -> a
+    void invalidateLayout(java.awt.Container) -> invalidateLayout
+    com.samskivert.swing.GroupLayout$Constraints getConstraints(java.awt.Component) -> a
+    com.samskivert.swing.DimenInfo computeDimens(java.awt.Container,int) -> b
+    javax.swing.JPanel makeButtonBox(com.samskivert.swing.GroupLayout$Justification,java.awt.Component[]) -> a
+    void <clinit>() -> <clinit>
+com.samskivert.swing.GroupLayout$Constraints -> com.a.a.b$a:
+    int _weight -> a
+    void <init>(int) -> <init>
+    boolean isFixed() -> a
+    int getWeight() -> b
+com.samskivert.swing.GroupLayout$Justification -> com.a.a.b$b:
+    void <init>() -> <init>
+com.samskivert.swing.GroupLayout$Policy -> com.a.a.b$c:
+    void <init>() -> <init>
+com.samskivert.swing.HGroupLayout -> com.a.a.c:
+    void <init>(com.samskivert.swing.GroupLayout$Policy,com.samskivert.swing.GroupLayout$Justification) -> <init>
+    void <init>() -> <init>
+    java.awt.Dimension getLayoutSize(java.awt.Container,int) -> a
+    void layoutContainer(java.awt.Container) -> layoutContainer
+com.samskivert.swing.Label -> com.a.a.d:
+    java.util.regex.Pattern COLOR_PATTERN -> a
+    java.lang.String _text -> b
+    java.lang.String _rawText -> c
+    int _style -> d
+    int _align -> e
+    java.awt.Dimension _constraints -> f
+    java.awt.Dimension _size -> g
+    float[] _leaders -> h
+    java.awt.Font _font -> i
+    java.awt.font.TextLayout[] _layouts -> j
+    java.awt.geom.Rectangle2D[] _lbounds -> k
+    java.awt.Color _alternateColor -> l
+    java.awt.Color _textColor -> m
+    boolean _mainDraw -> n
+    java.util.regex.Pattern ESCAPED_PATTERN -> o
+    java.lang.String unescapeColors(java.lang.String,boolean) -> a
+    void <init>() -> <init>
+    void <init>(java.lang.String) -> <init>
+    void <init>(java.lang.String,java.awt.Color,java.awt.Font) -> <init>
+    void <init>(java.lang.String,int,java.awt.Color,java.awt.Color,java.awt.Font) -> <init>
+    void setAlternateColor(java.awt.Color) -> a
+    void setStyle(int) -> a
+    void setTargetWidth(int) -> b
+    java.awt.Dimension getSize() -> a
+    void layout(java.awt.Graphics2D) -> a
+    java.util.List computeLines(java.awt.font.LineBreakMeasurer,int,java.awt.Dimension,boolean) -> a
+    void render(java.awt.Graphics2D,float,float) -> a
+    java.text.AttributedCharacterIterator textIterator(java.awt.Graphics2D) -> b
+    void addAttributes(java.text.AttributedString) -> a
+    double getWidth(java.awt.geom.Rectangle2D) -> a
+    java.awt.geom.Rectangle2D getBounds(java.awt.font.TextLayout) -> a
+    float getHeight(java.awt.font.TextLayout) -> b
+    void <clinit>() -> <clinit>
+com.samskivert.swing.Spacer -> com.a.a.e:
+    void <init>(int,int) -> <init>
+    void <init>(java.awt.Dimension) -> <init>
+com.samskivert.swing.VGroupLayout -> com.a.a.f:
+    void <init>() -> <init>
+    java.awt.Dimension getLayoutSize(java.awt.Container,int) -> a
+    void layoutContainer(java.awt.Container) -> layoutContainer
+com.samskivert.swing.util.SwingUtil -> com.a.a.a.a:
+    boolean _defaultTextAntialiasing -> a
+    void centerWindow(java.awt.Window) -> a
+    java.lang.Object activateAntiAliasing(java.awt.Graphics2D) -> a
+    void restoreAntiAliasing(java.awt.Graphics2D,java.lang.Object) -> a
+    boolean getDefaultTextAntialiasing() -> a
+    void <clinit>() -> <clinit>
+com.samskivert.util.AbstractIntSet -> com.a.b.a:
+    void <init>() -> <init>
+    boolean contains(int) -> a
+    int size() -> size
+    boolean isEmpty() -> isEmpty
+    boolean remove(int) -> b
+    java.util.Iterator iterator() -> iterator
+    boolean contains(java.lang.Object) -> contains
+    boolean remove(java.lang.Object) -> remove
+    boolean equals(java.lang.Object) -> equals
+    int hashCode() -> hashCode
+    java.lang.String toString() -> toString
+    boolean containsAll(java.util.Collection) -> containsAll
+    boolean addAll(java.util.Collection) -> addAll
+    boolean removeAll(java.util.Collection) -> removeAll
+    boolean retainAll(java.util.Collection) -> retainAll
+    boolean add(java.lang.Object) -> add
+com.samskivert.util.AbstractInterator -> com.a.b.b:
+    void <init>() -> <init>
+    void remove() -> remove
+    java.lang.Object next() -> next
+com.samskivert.util.ArrayUtil -> com.a.b.c:
+    java.lang.String safeToString(java.lang.Object) -> a
+    void <clinit>() -> <clinit>
+com.samskivert.util.FormatterUtil -> com.a.b.d:
+    java.lang.String LINE_SEPARATOR -> a
+    void configureDefaultHandler(java.util.logging.Formatter) -> a
+    void <clinit>() -> <clinit>
+com.samskivert.util.HashIntMap -> com.a.b.e:
+    com.samskivert.util.HashIntMap$Record[] _buckets -> a
+    int _size -> b
+    float _loadFactor -> c
+    com.samskivert.util.IntSet _keySet -> d
+    void <init>(int,float) -> <init>
+    void <init>() -> <init>
+    int size() -> size
+    boolean containsKey(java.lang.Object) -> containsKey
+    boolean containsKey(int) -> a
+    boolean containsValue(java.lang.Object) -> containsValue
+    java.lang.Object get(java.lang.Object) -> get
+    java.lang.Object put(int,java.lang.Object) -> a
+    java.lang.Object remove(java.lang.Object) -> remove
+    com.samskivert.util.HashIntMap$Record getImpl(int) -> b
+    com.samskivert.util.HashIntMap$Record removeImpl(int,boolean) -> a
+    void clear() -> clear
+    void ensureCapacity(int) -> c
+    int keyToIndex(int) -> d
+    void checkShrink() -> a
+    void resizeBuckets(int) -> e
+    java.util.Set entrySet() -> entrySet
+    java.util.Set keySet() -> keySet
+    com.samskivert.util.HashIntMap clone() -> b
+    com.samskivert.util.HashIntMap$Record[] createBuckets(int) -> f
+    java.lang.Object clone() -> clone
+    java.lang.Object put(java.lang.Object,java.lang.Object) -> put
+com.samskivert.util.HashIntMap$1 -> com.a.b.f:
+    com.samskivert.util.HashIntMap this$0 -> a
+    void <init>(com.samskivert.util.HashIntMap) -> <init>
+    int size() -> size
+    java.util.Iterator iterator() -> iterator
+com.samskivert.util.HashIntMap$2 -> com.a.b.g:
+    com.samskivert.util.HashIntMap this$0 -> a
+    void <init>(com.samskivert.util.HashIntMap) -> <init>
+    int size() -> size
+    java.util.Iterator iterator() -> iterator
+com.samskivert.util.HashIntMap$3 -> com.a.b.h:
+    com.samskivert.util.HashIntMap this$0 -> a
+    void <init>(com.samskivert.util.HashIntMap) -> <init>
+    com.samskivert.util.Interator interator() -> a
+    int size() -> size
+    boolean contains(int) -> a
+    boolean remove(int) -> b
+com.samskivert.util.HashIntMap$3$1 -> com.a.b.i:
+    java.util.Iterator i -> a
+    com.samskivert.util.HashIntMap$3 this$1 -> b
+    void <init>(com.samskivert.util.HashIntMap$3) -> <init>
+    boolean hasNext() -> hasNext
+    int nextInt() -> a
+    void remove() -> remove
+com.samskivert.util.HashIntMap$IntEntryIterator -> com.a.b.e$a:
+    com.samskivert.util.HashIntMap this$0 -> a
+    void <init>(com.samskivert.util.HashIntMap) -> <init>
+    java.lang.Object next() -> next
+com.samskivert.util.HashIntMap$MapEntryIterator -> com.a.b.e$b:
+    com.samskivert.util.HashIntMap this$0 -> a
+    void <init>(com.samskivert.util.HashIntMap) -> <init>
+    java.lang.Object next() -> next
+com.samskivert.util.HashIntMap$Record -> com.a.b.e$c:
+    com.samskivert.util.HashIntMap$Record next -> a
+    int key -> b
+    java.lang.Object value -> c
+    void <init>(int,java.lang.Object) -> <init>
+    int getIntKey() -> a
+    java.lang.Object getValue() -> getValue
+    java.lang.Object setValue(java.lang.Object) -> setValue
+    boolean equals(java.lang.Object) -> equals
+    int hashCode() -> hashCode
+    java.lang.String toString() -> toString
+    com.samskivert.util.HashIntMap$Record clone() -> b
+    java.lang.Object clone() -> clone
+    java.lang.Object getKey() -> getKey
+com.samskivert.util.HashIntMap$RecordIterator -> com.a.b.e$d:
+    int _index -> a
+    com.samskivert.util.HashIntMap$Record _record -> b
+    com.samskivert.util.HashIntMap$Record _last -> c
+    com.samskivert.util.HashIntMap this$0 -> d
+    void <init>(com.samskivert.util.HashIntMap) -> <init>
+    boolean hasNext() -> hasNext
+    com.samskivert.util.HashIntMap$Record nextRecord() -> a
+    void remove() -> remove
+com.samskivert.util.IntMap -> com.a.b.j:
+    java.lang.Object put(int,java.lang.Object) -> a
+com.samskivert.util.IntMap$IntEntry -> com.a.b.j$a:
+    int getIntKey() -> a
+com.samskivert.util.IntSet -> com.a.b.k:
+    boolean contains(int) -> a
+    com.samskivert.util.Interator interator() -> a
+com.samskivert.util.Interable -> com.a.b.l:
+    com.samskivert.util.Interator interator() -> a
+com.samskivert.util.Interator -> com.a.b.m:
+    int nextInt() -> a
+com.samskivert.util.JDK14Logger -> com.a.b.n:
+    void <init>() -> <init>
+    void init() -> a
+    com.samskivert.util.Logger getLogger(java.lang.String) -> a
+com.samskivert.util.JDK14Logger$Impl -> com.a.b.n$a:
+    java.util.logging.Logger _impl -> a
+    java.util.logging.Level[] LEVELS -> b
+    void <init>(java.util.logging.Logger) -> <init>
+    boolean shouldLog(int) -> a
+    void doLog(int,java.lang.String,java.lang.Throwable) -> a
+    void <clinit>() -> <clinit>
+com.samskivert.util.Logger -> com.a.b.o:
+    com.samskivert.util.Logger$Factory _factory -> a
+    void <init>() -> <init>
+    com.samskivert.util.Logger getLogger(java.lang.String) -> a
+    void warning(java.lang.Object,java.lang.Object[]) -> a
+    boolean shouldLog(int) -> a
+    void doLog(int,java.lang.String,java.lang.Throwable) -> a
+    com.samskivert.util.Logger$Factory createConfiguredFactory() -> a
+    void <clinit>() -> <clinit>
+com.samskivert.util.Logger$Factory -> com.a.b.o$a:
+    void init() -> a
+    com.samskivert.util.Logger getLogger(java.lang.String) -> a
+com.samskivert.util.OneLineLogFormatter -> com.a.b.p:
+    boolean _showWhere -> a
+    java.util.Date _date -> b
+    java.text.SimpleDateFormat _format -> c
+    java.text.FieldPosition _fpos -> d
+    void <init>() -> <init>
+    void <init>(boolean) -> <init>
+    java.lang.String format(java.util.logging.LogRecord) -> format
+    void configureDefaultHandler() -> a
+com.samskivert.util.RunAnywhere -> com.a.b.q:
+    boolean _isMacOS -> a
+    boolean isMacOS() -> a
+    void <clinit>() -> <clinit>
+com.samskivert.util.StringUtil -> com.a.b.r:
+    java.text.NumberFormat _ffmt -> a
+    com.samskivert.util.IntMap _letterToBits -> b
+    boolean isBlank(java.lang.String) -> a
+    java.lang.String toString(java.lang.Object) -> a
+    void toString(java.lang.StringBuilder,java.lang.Object,java.lang.String,java.lang.String) -> a
+    void toString(java.lang.StringBuilder,java.lang.Object,java.lang.String,java.lang.String,java.lang.String) -> a
+    void coordsToString(java.lang.StringBuilder,int,int) -> a
+    void <clinit>() -> <clinit>
+com.samskivert.util.Throttle -> com.a.b.s:
+    long[] _ops -> a
+    int _lastOp -> b
+    long _period -> c
+    void <init>(int,long) -> <init>
+    boolean throttleOp() -> a
+    java.lang.String toString() -> toString
+com.samskivert.util.Tuple -> com.a.b.t:
+    java.lang.Object left -> a
+    java.lang.Object right -> b
+    void <init>(java.lang.Object,java.lang.Object) -> <init>
+    int hashCode() -> hashCode
+    boolean equals(java.lang.Object) -> equals
+    java.lang.String toString() -> toString
+com.threerings.getdown.Log -> com.threerings.getdown.Log:
+    com.threerings.getdown.Log$Shim log -> log
+    java.lang.String DATE_FORMAT -> DATE_FORMAT
+    java.util.logging.Level[] LEVELS -> LEVELS
+    void <init>() -> <init>
+    java.lang.String format(java.lang.Object,java.lang.Object[]) -> format
+    void <clinit>() -> <clinit>
+com.threerings.getdown.Log$OneLineFormatter -> com.threerings.getdown.Log$OneLineFormatter:
+    java.util.Date _date -> _date
+    java.text.SimpleDateFormat _format -> _format
+    java.text.FieldPosition _fpos -> _fpos
+    void <init>() -> <init>
+    java.lang.String format(java.util.logging.LogRecord) -> format
+com.threerings.getdown.Log$Shim -> com.threerings.getdown.Log$Shim:
+    java.util.logging.Logger _impl -> _impl
+    void <init>() -> <init>
+    void debug(java.lang.Object,java.lang.Object[]) -> debug
+    void info(java.lang.Object,java.lang.Object[]) -> info
+    void warning(java.lang.Object,java.lang.Object[]) -> warning
+    void error(java.lang.Object,java.lang.Object[]) -> error
+    void doLog(int,java.lang.Object,java.lang.Object[]) -> doLog
+com.threerings.getdown.cache.GarbageCollector -> com.threerings.getdown.cache.GarbageCollector:
+    void <init>() -> <init>
+    void collect(java.io.File,long) -> collect
+    void collectNative(java.io.File,long) -> collectNative
+    boolean shouldDelete(java.io.File,long) -> shouldDelete
+    java.io.File getLastAccessedFile(java.io.File) -> getLastAccessedFile
+    boolean isLastAccessedFile(java.io.File) -> isLastAccessedFile
+    java.io.File getCachedFile(java.io.File) -> getCachedFile
+    java.io.File access$000(java.io.File) -> access$000
+    java.io.File access$100(java.io.File) -> access$100
+    boolean access$200(java.io.File,long) -> access$200
+com.threerings.getdown.cache.GarbageCollector$1 -> com.threerings.getdown.cache.a:
+    long val$retentionPeriodMillis -> a
+    void <init>(long) -> <init>
+    void visit(java.io.File) -> visit
+com.threerings.getdown.cache.ResourceCache -> com.threerings.getdown.cache.ResourceCache:
+    java.io.File _cacheDir -> _cacheDir
+    java.lang.String LAST_ACCESSED_FILE_SUFFIX -> LAST_ACCESSED_FILE_SUFFIX
+    void <init>(java.io.File) -> <init>
+    void createDirectoryIfNecessary(java.io.File) -> createDirectoryIfNecessary
+    java.io.File cacheFile(java.io.File,java.lang.String,java.lang.String) -> cacheFile
+    void createNewFile(java.io.File) -> createNewFile
+    java.lang.String getFileSuffix(java.io.File) -> getFileSuffix
+com.threerings.getdown.data.Application -> com.threerings.getdown.data.Application:
+    java.lang.String CONFIG_FILE -> CONFIG_FILE
+    java.lang.String VERSION_FILE -> VERSION_FILE
+    java.lang.String PROP_PASSTHROUGH_PREFIX -> PROP_PASSTHROUGH_PREFIX
+    java.lang.String SIGNATURE_SUFFIX -> SIGNATURE_SUFFIX
+    java.lang.String MANIFEST_CLASS -> MANIFEST_CLASS
+    java.net.Proxy proxy -> proxy
+    com.threerings.getdown.data.EnvConfig _envc -> _envc
+    java.io.File _config -> _config
+    com.threerings.getdown.data.Digest _digest -> _digest
+    long _version -> _version
+    long _targetVersion -> _targetVersion
+    java.lang.String _appbase -> _appbase
+    java.net.URL _vappbase -> _vappbase
+    java.net.URL _latest -> _latest
+    java.lang.String _class -> _class
+    java.lang.String _dockName -> _dockName
+    java.lang.String _dockIconPath -> _dockIconPath
+    boolean _strictComments -> _strictComments
+    boolean _windebug -> _windebug
+    boolean _allowOffline -> _allowOffline
+    int _maxConcDownloads -> _maxConcDownloads
+    java.lang.String _trackingURL -> _trackingURL
+    java.util.Set _trackingPcts -> _trackingPcts
+    java.lang.String _trackingCookieName -> _trackingCookieName
+    java.lang.String _trackingCookieProperty -> _trackingCookieProperty
+    java.lang.String _trackingURLSuffix -> _trackingURLSuffix
+    java.lang.String _trackingGAHash -> _trackingGAHash
+    long _trackingStart -> _trackingStart
+    int _trackingId -> _trackingId
+    java.lang.String _javaVersionProp -> _javaVersionProp
+    java.lang.String _javaVersionRegex -> _javaVersionRegex
+    long _javaMinVersion -> _javaMinVersion
+    long _javaMaxVersion -> _javaMaxVersion
+    boolean _javaExactVersionRequired -> _javaExactVersionRequired
+    java.lang.String _javaLocation -> _javaLocation
+    java.util.List _codes -> _codes
+    java.util.List _resources -> _resources
+    boolean _useCodeCache -> _useCodeCache
+    int _codeCacheRetentionDays -> _codeCacheRetentionDays
+    java.util.Map _auxgroups -> _auxgroups
+    java.util.Map _auxactive -> _auxactive
+    java.util.List _jvmargs -> _jvmargs
+    java.util.List _appargs -> _appargs
+    java.lang.String[] _optimumJvmArgs -> _optimumJvmArgs
+    java.util.List _txtJvmArgs -> _txtJvmArgs
+    boolean _warnedAboutSetLastModified -> _warnedAboutSetLastModified
+    java.nio.channels.FileLock _lock -> _lock
+    java.nio.channels.FileChannel _lockChannel -> _lockChannel
+    java.util.Random _rando -> _rando
+    java.lang.String[] EMPTY_STRING_ARRAY -> EMPTY_STRING_ARRAY
+    java.lang.String ENV_VAR_PREFIX -> ENV_VAR_PREFIX
+    java.util.regex.Pattern ENV_VAR_PATTERN -> ENV_VAR_PATTERN
+    void <init>(com.threerings.getdown.data.EnvConfig) -> <init>
+    java.io.File getAppDir() -> getAppDir
+    boolean useCodeCache() -> useCodeCache
+    int getCodeCacheRetentionDays() -> getCodeCacheRetentionDays
+    int maxConcurrentDownloads() -> maxConcurrentDownloads
+    com.threerings.getdown.data.Resource getConfigResource() -> getConfigResource
+    java.util.List getCodeResources() -> getCodeResources
+    java.util.List getResources() -> getResources
+    java.lang.String getDigest(com.threerings.getdown.data.Resource) -> getDigest
+    java.util.List getAllActiveResources() -> getAllActiveResources
+    com.threerings.getdown.data.Application$AuxGroup getAuxGroup(java.lang.String) -> getAuxGroup
+    java.lang.Iterable getAuxGroups() -> getAuxGroups
+    boolean isAuxGroupActive(java.lang.String) -> isAuxGroupActive
+    java.util.List getActiveCodeResources() -> getActiveCodeResources
+    java.util.List getNativeResources() -> getNativeResources
+    java.util.List getActiveResources() -> getActiveResources
+    com.threerings.getdown.data.Resource getPatchResource(java.lang.String) -> getPatchResource
+    com.threerings.getdown.data.Resource getJavaVMResource() -> getJavaVMResource
+    com.threerings.getdown.data.Resource getFullResource() -> getFullResource
+    java.net.URL getTrackingURL(java.lang.String) -> getTrackingURL
+    java.net.URL getTrackingProgressURL(int) -> getTrackingProgressURL
+    java.lang.String getTrackingCookieName() -> getTrackingCookieName
+    java.lang.String getTrackingCookieProperty() -> getTrackingCookieProperty
+    com.threerings.getdown.util.Config init(boolean) -> init
+    void fillAssignmentListFromPairs(java.lang.String,java.util.List) -> fillAssignmentListFromPairs
+    java.net.URL getRemoteURL(java.lang.String) -> getRemoteURL
+    java.io.File getLocalPath(java.lang.String) -> getLocalPath
+    boolean haveValidJavaVersion() -> haveValidJavaVersion
+    boolean hasOptimumJvmArgs() -> hasOptimumJvmArgs
+    boolean allowOffline() -> allowOffline
+    void attemptRecovery(com.threerings.getdown.data.Application$StatusDisplay) -> attemptRecovery
+    void updateMetadata() -> updateMetadata
+    java.lang.Process createProcess(boolean) -> createProcess
+    java.lang.String[] createEnvironment() -> createEnvironment
+    void invokeDirect() -> invokeDirect
+    java.lang.String processArg(java.lang.String) -> processArg
+    boolean verifyMetadata(com.threerings.getdown.data.Application$StatusDisplay) -> verifyMetadata
+    void verifyResources(com.threerings.getdown.util.ProgressObserver,int[],java.util.Set,java.util.Set,java.util.Set) -> verifyResources
+    void verifyResource(com.threerings.getdown.data.Resource,com.threerings.getdown.util.ProgressObserver,int[],java.util.Set,java.util.Set,java.util.Set) -> verifyResource
+    void unpackResources(com.threerings.getdown.util.ProgressObserver,java.util.Set) -> unpackResources
+    void clearValidationMarkers() -> clearValidationMarkers
+    long getVersion() -> getVersion
+    java.net.URL createVAppBase(long) -> createVAppBase
+    void clearValidationMarkers(java.util.Iterator) -> clearValidationMarkers
+    void downloadConfigFile() -> downloadConfigFile
+    boolean lockForUpdates() -> lockForUpdates
+    void releaseLock() -> releaseLock
+    void downloadDigestFiles() -> downloadDigestFiles
+    void downloadControlFile(java.lang.String,int) -> downloadControlFile
+    java.io.File downloadFile(java.lang.String) -> downloadFile
+    com.threerings.getdown.data.Resource createResource(java.lang.String,java.util.EnumSet) -> createResource
+    void addAll(java.lang.String[],java.util.List) -> addAll
+    java.util.List intsToList(int[]) -> intsToList
+    java.util.List stringsToList(java.lang.String[]) -> stringsToList
+    void parseResources(com.threerings.getdown.util.Config,java.lang.String,java.util.EnumSet,java.util.List) -> parseResources
+    java.lang.String getGATrackingCode() -> getGATrackingCode
+    java.lang.String encodePath(java.lang.String) -> encodePath
+    java.io.File getLocalPath(java.io.File,java.lang.String) -> getLocalPath
+    void access$000(com.threerings.getdown.data.Application,com.threerings.getdown.data.Resource,com.threerings.getdown.util.ProgressObserver,int[],java.util.Set,java.util.Set,java.util.Set) -> access$000
+    void <clinit>() -> <clinit>
+com.threerings.getdown.data.Application$1 -> com.threerings.getdown.data.a:
+    com.threerings.getdown.data.Application this$0 -> a
+    void <init>(com.threerings.getdown.data.Application,java.net.URL[],java.lang.ClassLoader) -> <init>
+    java.security.PermissionCollection getPermissions(java.security.CodeSource) -> getPermissions
+com.threerings.getdown.data.Application$2 -> com.threerings.getdown.data.b:
+    java.util.concurrent.BlockingQueue val$actions -> b
+    com.threerings.getdown.util.ProgressObserver val$fobs -> a
+    com.threerings.getdown.data.Application this$0 -> c
+    void <init>(com.threerings.getdown.data.Application,java.util.concurrent.BlockingQueue,com.threerings.getdown.util.ProgressObserver) -> <init>
+    void progress(int) -> progress
+com.threerings.getdown.data.Application$2$1 -> com.threerings.getdown.data.c:
+    int val$percent -> a
+    com.threerings.getdown.data.Application$2 this$1 -> b
+    void <init>(com.threerings.getdown.data.Application$2,int) -> <init>
+    void run() -> run
+com.threerings.getdown.data.Application$3 -> com.threerings.getdown.data.d:
+    com.threerings.getdown.data.Resource val$rsrc -> b
+    com.threerings.getdown.util.ProgressAggregator val$pagg -> c
+    int val$index -> d
+    int[] val$fAlreadyValid -> e
+    java.util.Set val$unpackedAsync -> f
+    java.util.Set val$toInstallAsync -> g
+    java.util.Set val$toDownloadAsync -> h
+    java.util.concurrent.BlockingQueue val$actions -> i
+    int[] val$completed -> a
+    com.threerings.getdown.data.Application this$0 -> j
+    void <init>(com.threerings.getdown.data.Application,com.threerings.getdown.data.Resource,com.threerings.getdown.util.ProgressAggregator,int,int[],java.util.Set,java.util.Set,java.util.Set,java.util.concurrent.BlockingQueue,int[]) -> <init>
+    void run() -> run
+com.threerings.getdown.data.Application$3$1 -> com.threerings.getdown.data.e:
+    com.threerings.getdown.data.Application$3 this$1 -> a
+    void <init>(com.threerings.getdown.data.Application$3) -> <init>
+    void run() -> run
+com.threerings.getdown.data.Application$AuxGroup -> com.threerings.getdown.data.Application$AuxGroup:
+    java.lang.String name -> name
+    java.util.List codes -> codes
+    java.util.List rsrcs -> rsrcs
+    void <init>(java.lang.String,java.util.List,java.util.List) -> <init>
+com.threerings.getdown.data.Application$StatusDisplay -> com.threerings.getdown.data.Application$StatusDisplay:
+    void updateStatus(java.lang.String) -> updateStatus
+com.threerings.getdown.data.Application$UpdateInterface -> com.threerings.getdown.data.Application$UpdateInterface:
+    java.lang.String name -> name
+    int background -> background
+    java.util.List rotatingBackgrounds -> rotatingBackgrounds
+    java.lang.String errorBackground -> errorBackground
+    java.util.List iconImages -> iconImages
+    java.lang.String backgroundImage -> backgroundImage
+    java.lang.String progressImage -> progressImage
+    com.threerings.getdown.util.Rectangle progress -> progress
+    int progressText -> progressText
+    int progressBar -> progressBar
+    com.threerings.getdown.util.Rectangle status -> status
+    int statusText -> statusText
+    int textShadow -> textShadow
+    java.lang.String installError -> installError
+    com.threerings.getdown.util.Rectangle patchNotes -> patchNotes
+    java.lang.String patchNotesUrl -> patchNotesUrl
+    boolean hideDecorations -> hideDecorations
+    boolean hideProgressText -> hideProgressText
+    int minShowSeconds -> minShowSeconds
+    java.util.Map stepPercentages -> stepPercentages
+    java.lang.String toString() -> toString
+    void <init>(com.threerings.getdown.util.Config) -> <init>
+com.threerings.getdown.data.Application$UpdateInterface$Step -> com.threerings.getdown.data.Application$UpdateInterface$Step:
+    com.threerings.getdown.data.Application$UpdateInterface$Step UPDATE_JAVA -> UPDATE_JAVA
+    com.threerings.getdown.data.Application$UpdateInterface$Step VERIFY_METADATA -> VERIFY_METADATA
+    com.threerings.getdown.data.Application$UpdateInterface$Step DOWNLOAD -> DOWNLOAD
+    com.threerings.getdown.data.Application$UpdateInterface$Step PATCH -> PATCH
+    com.threerings.getdown.data.Application$UpdateInterface$Step VERIFY_RESOURCES -> VERIFY_RESOURCES
+    com.threerings.getdown.data.Application$UpdateInterface$Step REDOWNLOAD_RESOURCES -> REDOWNLOAD_RESOURCES
+    com.threerings.getdown.data.Application$UpdateInterface$Step UNPACK -> UNPACK
+    com.threerings.getdown.data.Application$UpdateInterface$Step LAUNCH -> LAUNCH
+    java.util.List defaultPercents -> defaultPercents
+    com.threerings.getdown.data.Application$UpdateInterface$Step[] $VALUES -> $VALUES
+    com.threerings.getdown.data.Application$UpdateInterface$Step[] values() -> values
+    com.threerings.getdown.data.Application$UpdateInterface$Step valueOf(java.lang.String) -> valueOf
+    void <init>(java.lang.String,int,int[]) -> <init>
+    void <clinit>() -> <clinit>
+com.threerings.getdown.data.Build -> com.threerings.getdown.data.Build:
+    void <init>() -> <init>
+    java.lang.String time() -> time
+    java.lang.String version() -> version
+    java.util.List hostWhitelist() -> hostWhitelist
+com.threerings.getdown.data.ClassPath -> com.threerings.getdown.data.ClassPath:
+    java.util.Set _classPathEntries -> _classPathEntries
+    void <init>(java.util.LinkedHashSet) -> <init>
+    java.lang.String asArgumentString() -> asArgumentString
+    java.net.URL[] asUrls() -> asUrls
+    java.util.Set getClassPathEntries() -> getClassPathEntries
+    java.net.URL getURL(java.io.File) -> getURL
+com.threerings.getdown.data.Digest -> com.threerings.getdown.data.Digest:
+    int VERSION -> VERSION
+    java.util.HashMap _digests -> _digests
+    java.lang.String _metaDigest -> _metaDigest
+    java.lang.String FILE_NAME -> FILE_NAME
+    java.lang.String FILE_SUFFIX -> FILE_SUFFIX
+    java.lang.String digestFile(int) -> digestFile
+    java.lang.String sigAlgorithm(int) -> sigAlgorithm
+    void createDigest(int,java.util.List,java.io.File) -> createDigest
+    java.security.MessageDigest getMessageDigest(int) -> getMessageDigest
+    void <init>(java.io.File,boolean) -> <init>
+    void <init>(java.io.File,int,boolean) -> <init>
+    java.lang.String getMetaDigest() -> getMetaDigest
+    boolean validateResource(com.threerings.getdown.data.Resource,com.threerings.getdown.util.ProgressObserver) -> validateResource
+    java.lang.String getDigest(com.threerings.getdown.data.Resource) -> getDigest
+    void note(java.lang.StringBuilder,java.lang.String,java.lang.String) -> note
+com.threerings.getdown.data.Digest$1 -> com.threerings.getdown.data.f:
+    int val$fversion -> a
+    java.util.Map val$digests -> b
+    com.threerings.getdown.data.Resource val$rsrc -> c
+    java.util.concurrent.BlockingQueue val$completed -> d
+    void <init>(int,java.util.Map,com.threerings.getdown.data.Resource,java.util.concurrent.BlockingQueue) -> <init>
+    void run() -> run
+com.threerings.getdown.data.EnvConfig -> com.threerings.getdown.data.EnvConfig:
+    java.io.File appDir -> appDir
+    java.lang.String appId -> appId
+    java.lang.String appBase -> appBase
+    java.util.List certs -> certs
+    java.util.List appArgs -> appArgs
+    java.lang.String USER_HOME_KEY -> USER_HOME_KEY
+    com.threerings.getdown.data.EnvConfig create(java.lang.String[],java.util.List) -> create
+    void <init>(java.io.File) -> <init>
+    void <init>(java.io.File,java.lang.String,java.lang.String,java.util.List,java.util.List) -> <init>
+com.threerings.getdown.data.EnvConfig$Note -> com.threerings.getdown.data.EnvConfig$Note:
+    com.threerings.getdown.data.EnvConfig$Note$Level level -> level
+    java.lang.String message -> message
+    com.threerings.getdown.data.EnvConfig$Note info(java.lang.String) -> info
+    com.threerings.getdown.data.EnvConfig$Note warn(java.lang.String) -> warn
+    com.threerings.getdown.data.EnvConfig$Note error(java.lang.String) -> error
+    void <init>(com.threerings.getdown.data.EnvConfig$Note$Level,java.lang.String) -> <init>
+com.threerings.getdown.data.EnvConfig$Note$Level -> com.threerings.getdown.data.EnvConfig$Note$Level:
+    com.threerings.getdown.data.EnvConfig$Note$Level INFO -> INFO
+    com.threerings.getdown.data.EnvConfig$Note$Level WARN -> WARN
+    com.threerings.getdown.data.EnvConfig$Note$Level ERROR -> ERROR
+    com.threerings.getdown.data.EnvConfig$Note$Level[] $VALUES -> $VALUES
+    com.threerings.getdown.data.EnvConfig$Note$Level[] values() -> values
+    com.threerings.getdown.data.EnvConfig$Note$Level valueOf(java.lang.String) -> valueOf
+    void <init>(java.lang.String,int) -> <init>
+    void <clinit>() -> <clinit>
+com.threerings.getdown.data.PathBuilder -> com.threerings.getdown.data.PathBuilder:
+    java.lang.String CODE_CACHE_DIR -> CODE_CACHE_DIR
+    java.lang.String NATIVE_CACHE_DIR -> NATIVE_CACHE_DIR
+    void <init>() -> <init>
+    com.threerings.getdown.data.ClassPath buildClassPath(com.threerings.getdown.data.Application) -> buildClassPath
+    com.threerings.getdown.data.ClassPath buildDefaultClassPath(com.threerings.getdown.data.Application) -> buildDefaultClassPath
+    com.threerings.getdown.data.ClassPath buildCachedClassPath(com.threerings.getdown.data.Application) -> buildCachedClassPath
+    com.threerings.getdown.data.ClassPath buildLibsPath(com.threerings.getdown.data.Application,boolean) -> buildLibsPath
+com.threerings.getdown.data.Properties -> com.threerings.getdown.data.Properties:
+    java.lang.String GETDOWN -> GETDOWN
+    java.lang.String CONNECT_PORT -> CONNECT_PORT
+    void <init>() -> <init>
+com.threerings.getdown.data.Resource -> com.threerings.getdown.data.Resource:
+    java.util.EnumSet NORMAL -> NORMAL
+    java.util.EnumSet UNPACK -> UNPACK
+    java.util.EnumSet EXEC -> EXEC
+    java.util.EnumSet PRELOAD -> PRELOAD
+    java.util.EnumSet NATIVE -> NATIVE
+    java.lang.String _path -> _path
+    java.net.URL _remote -> _remote
+    java.io.File _local -> _local
+    java.io.File _localNew -> _localNew
+    java.io.File _marker -> _marker
+    java.io.File _unpacked -> _unpacked
+    java.util.EnumSet _attrs -> _attrs
+    boolean _isJar -> _isJar
+    boolean _isPacked200Jar -> _isPacked200Jar
+    java.util.Comparator ENTRY_COMP -> ENTRY_COMP
+    int DIGEST_BUFFER_SIZE -> DIGEST_BUFFER_SIZE
+    java.lang.String computeDigest(int,java.io.File,java.security.MessageDigest,com.threerings.getdown.util.ProgressObserver) -> computeDigest
+    void <init>(java.lang.String,java.net.URL,java.io.File,java.util.EnumSet) -> <init>
+    java.lang.String getPath() -> getPath
+    java.io.File getLocal() -> getLocal
+    java.io.File getLocalNew() -> getLocalNew
+    java.io.File getUnpacked() -> getUnpacked
+    java.io.File getFinalTarget() -> getFinalTarget
+    java.net.URL getRemote() -> getRemote
+    boolean shouldUnpack() -> shouldUnpack
+    boolean shouldPredownload() -> shouldPredownload
+    boolean isNative() -> isNative
+    java.lang.String computeDigest(int,java.security.MessageDigest,com.threerings.getdown.util.ProgressObserver) -> computeDigest
+    boolean isMarkedValid() -> isMarkedValid
+    void markAsValid() -> markAsValid
+    void clearMarker() -> clearMarker
+    void install(boolean) -> install
+    void unpack() -> unpack
+    void applyAttrs() -> applyAttrs
+    void erase() -> erase
+    int compareTo(com.threerings.getdown.data.Resource) -> compareTo
+    boolean equals(java.lang.Object) -> equals
+    int hashCode() -> hashCode
+    java.lang.String toString() -> toString
+    void updateProgress(com.threerings.getdown.util.ProgressObserver,long,long) -> updateProgress
+    boolean isJar(java.lang.String) -> isJar
+    boolean isPacked200Jar(java.lang.String) -> isPacked200Jar
+    int compareTo(java.lang.Object) -> compareTo
+    void <clinit>() -> <clinit>
+com.threerings.getdown.data.Resource$1 -> com.threerings.getdown.data.g:
+    void <init>() -> <init>
+    int compare(java.lang.Object,java.lang.Object) -> compare
+com.threerings.getdown.data.Resource$Attr -> com.threerings.getdown.data.Resource$Attr:
+    com.threerings.getdown.data.Resource$Attr UNPACK -> UNPACK
+    com.threerings.getdown.data.Resource$Attr CLEAN -> CLEAN
+    com.threerings.getdown.data.Resource$Attr EXEC -> EXEC
+    com.threerings.getdown.data.Resource$Attr PRELOAD -> PRELOAD
+    com.threerings.getdown.data.Resource$Attr NATIVE -> NATIVE
+    com.threerings.getdown.data.Resource$Attr[] $VALUES -> $VALUES
+    com.threerings.getdown.data.Resource$Attr[] values() -> values
+    com.threerings.getdown.data.Resource$Attr valueOf(java.lang.String) -> valueOf
+    void <init>(java.lang.String,int) -> <init>
+    void <clinit>() -> <clinit>
+com.threerings.getdown.data.SysProps -> com.threerings.getdown.data.SysProps:
+    void <init>() -> <init>
+    java.lang.String appDir() -> appDir
+    java.lang.String appId() -> appId
+    java.lang.String appBase() -> appBase
+    boolean noLogRedir() -> noLogRedir
+    java.lang.String appbaseDomain() -> appbaseDomain
+    java.lang.String appbaseOverride() -> appbaseOverride
+    boolean silent() -> silent
+    boolean launchInSilent() -> launchInSilent
+    boolean noUpdate() -> noUpdate
+    boolean noInstall() -> noInstall
+    int startDelay() -> startDelay
+    boolean noUnpack() -> noUnpack
+    boolean direct() -> direct
+    int connectTimeout() -> connectTimeout
+    int readTimeout() -> readTimeout
+    int threadPoolSize() -> threadPoolSize
+    long parseJavaVersion(java.lang.String,java.lang.String) -> parseJavaVersion
+    java.lang.String overrideAppbase(java.lang.String) -> overrideAppbase
+    java.lang.String replaceDomain(java.lang.String) -> replaceDomain
+com.threerings.getdown.launcher.AbortPanel -> com.threerings.getdown.launcher.AbortPanel:
+    com.threerings.getdown.launcher.Getdown _getdown -> _getdown
+    java.util.ResourceBundle _msgs -> _msgs
+    void <init>(com.threerings.getdown.launcher.Getdown,java.util.ResourceBundle) -> <init>
+    java.awt.Dimension getPreferredSize() -> getPreferredSize
+    void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed
+    java.lang.String get(java.lang.String) -> get
+com.threerings.getdown.launcher.Getdown -> com.threerings.getdown.launcher.Getdown:
+    com.threerings.getdown.util.ProgressObserver _progobs -> _progobs
+    com.threerings.getdown.data.Application _app -> _app
+    com.threerings.getdown.data.Application$UpdateInterface _ifc -> _ifc
+    java.util.ResourceBundle _msgs -> _msgs
+    java.awt.Container _container -> _container
+    javax.swing.JLayeredPane _layers -> _layers
+    com.threerings.getdown.launcher.StatusPanel _status -> _status
+    javax.swing.JButton _patchNotes -> _patchNotes
+    com.threerings.getdown.launcher.AbortPanel _abort -> _abort
+    com.threerings.getdown.launcher.RotatingBackgrounds _background -> _background
+    boolean _dead -> _dead
+    boolean _silent -> _silent
+    boolean _launchInSilent -> _launchInSilent
+    boolean _noUpdate -> _noUpdate
+    long _startup -> _startup
+    java.util.Set _toInstallResources -> _toInstallResources
+    boolean _readyToInstall -> _readyToInstall
+    boolean _enableTracking -> _enableTracking
+    int _reportedProgress -> _reportedProgress
+    int _delay -> _delay
+    int _stepMaxPercent -> _stepMaxPercent
+    int _stepMinPercent -> _stepMinPercent
+    int _lastGlobalPercent -> _lastGlobalPercent
+    int _uiDisplayPercent -> _uiDisplayPercent
+    int MAX_LOOPS -> MAX_LOOPS
+    long FALLBACK_CHECK_TIME -> FALLBACK_CHECK_TIME
+    void <init>(com.threerings.getdown.data.EnvConfig) -> <init>
+    boolean isUpdateAvailable() -> isUpdateAvailable
+    void install() -> install
+    void run() -> run
+    void configProxy(java.lang.String,java.lang.String,java.lang.String,java.lang.String) -> configProxy
+    boolean detectProxy() -> detectProxy
+    void readConfig(boolean) -> readConfig
+    void doPredownloads(java.util.Collection) -> doPredownloads
+    void getdown() -> getdown
+    void updateStatus(java.lang.String) -> updateStatus
+    java.awt.image.BufferedImage loadImage(java.lang.String) -> loadImage
+    void updateJava() -> updateJava
+    void update() -> update
+    void download(java.util.Collection) -> download
+    void launch() -> launch
+    void createInterfaceAsync(boolean) -> createInterfaceAsync
+    void initInterface() -> initInterface
+    com.threerings.getdown.launcher.RotatingBackgrounds getBackground() -> getBackground
+    java.awt.Image getProgressImage() -> getProgressImage
+    void handleWindowClose() -> handleWindowClose
+    void fail(java.lang.String) -> fail
+    void setStep(com.threerings.getdown.data.Application$UpdateInterface$Step) -> setStep
+    int stepToGlobalPercent(int) -> stepToGlobalPercent
+    void setStatusAsync(java.lang.String,int,long,boolean) -> setStatusAsync
+    void reportTrackingEvent(java.lang.String,int) -> reportTrackingEvent
+    java.awt.Container createContainer() -> createContainer
+    void configureContainer() -> configureContainer
+    void showContainer() -> showContainer
+    void disposeContainer() -> disposeContainer
+    boolean invokeDirect() -> invokeDirect
+    void showDocument(java.lang.String) -> showDocument
+    void exit(int) -> exit
+    void copyStream(java.io.InputStream,java.io.PrintStream) -> copyStream
+    java.awt.Image loadImage(java.lang.String) -> loadImage
+com.threerings.getdown.launcher.Getdown$1 -> com.threerings.getdown.launcher.a:
+    com.threerings.getdown.launcher.Getdown this$0 -> a
+    void <init>(com.threerings.getdown.launcher.Getdown) -> <init>
+    void run() -> run
+com.threerings.getdown.launcher.Getdown$2 -> com.threerings.getdown.launcher.b:
+    int _lastCheck -> a
+    com.threerings.getdown.launcher.Getdown this$0 -> b
+    void <init>(com.threerings.getdown.launcher.Getdown,java.net.Proxy) -> <init>
+    void resolvingDownloads() -> resolvingDownloads
+    void downloadProgress(int,long) -> downloadProgress
+    void downloadFailed(com.threerings.getdown.data.Resource,java.lang.Exception) -> downloadFailed
+com.threerings.getdown.launcher.Getdown$3 -> com.threerings.getdown.launcher.c:
+    java.io.InputStream val$stderr -> a
+    com.threerings.getdown.launcher.Getdown this$0 -> b
+    void <init>(com.threerings.getdown.launcher.Getdown,java.io.InputStream) -> <init>
+    void run() -> run
+com.threerings.getdown.launcher.Getdown$4 -> com.threerings.getdown.launcher.d:
+    boolean val$reinit -> b
+    com.threerings.getdown.launcher.Getdown this$0 -> a
+    void <init>(com.threerings.getdown.launcher.Getdown,boolean) -> <init>
+    void run() -> run
+com.threerings.getdown.launcher.Getdown$4$1 -> com.threerings.getdown.launcher.e:
+    com.threerings.getdown.launcher.Getdown$4 this$1 -> a
+    void <init>(com.threerings.getdown.launcher.Getdown$4,java.lang.String) -> <init>
+    void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed
+com.threerings.getdown.launcher.Getdown$5 -> com.threerings.getdown.launcher.f:
+    java.lang.String val$message -> a
+    int val$percent -> b
+    long val$remaining -> c
+    com.threerings.getdown.launcher.Getdown this$0 -> d
+    void <init>(com.threerings.getdown.launcher.Getdown,java.lang.String,int,long) -> <init>
+    void run() -> run
+com.threerings.getdown.launcher.Getdown$6 -> com.threerings.getdown.launcher.g:
+    com.threerings.getdown.launcher.Getdown this$0 -> a
+    void <init>(com.threerings.getdown.launcher.Getdown) -> <init>
+    void progress(int) -> progress
+com.threerings.getdown.launcher.Getdown$ProgressReporter -> com.threerings.getdown.launcher.Getdown$ProgressReporter:
+    java.net.URL _url -> _url
+    com.threerings.getdown.launcher.Getdown this$0 -> this$0
+    void <init>(com.threerings.getdown.launcher.Getdown,java.net.URL) -> <init>
+    void run() -> run
+com.threerings.getdown.launcher.GetdownApp -> com.threerings.getdown.launcher.GetdownApp:
+    void <init>() -> <init>
+    void main(java.lang.String[]) -> main
+    com.threerings.getdown.launcher.Getdown start(java.lang.String[]) -> start
+com.threerings.getdown.launcher.GetdownApp$1 -> com.threerings.getdown.launcher.h:
+    javax.swing.JFrame _frame -> a
+    void <init>(com.threerings.getdown.data.EnvConfig) -> <init>
+    java.awt.Container createContainer() -> createContainer
+    void configureContainer() -> configureContainer
+    void showContainer() -> showContainer
+    void disposeContainer() -> disposeContainer
+    void showDocument(java.lang.String) -> showDocument
+    void exit(int) -> exit
+    void fail(java.lang.String) -> fail
+com.threerings.getdown.launcher.GetdownApp$1$1 -> com.threerings.getdown.launcher.i:
+    com.threerings.getdown.launcher.GetdownApp$1 this$0 -> a
+    void <init>(com.threerings.getdown.launcher.GetdownApp$1) -> <init>
+    void windowClosing(java.awt.event.WindowEvent) -> windowClosing
+com.threerings.getdown.launcher.GetdownApp$1$2 -> com.threerings.getdown.launcher.j:
+    com.threerings.getdown.launcher.GetdownApp$1 this$0 -> a
+    void <init>(com.threerings.getdown.launcher.GetdownApp$1) -> <init>
+    void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed
+com.threerings.getdown.launcher.GetdownApp$1$3 -> com.threerings.getdown.launcher.k:
+    com.threerings.getdown.launcher.GetdownApp$1 this$0 -> a
+    void <init>(com.threerings.getdown.launcher.GetdownApp$1) -> <init>
+    void run() -> run
+com.threerings.getdown.launcher.GetdownApp$2 -> com.threerings.getdown.launcher.l:
+    int[] $SwitchMap$com$threerings$getdown$data$EnvConfig$Note$Level -> a
+    void <clinit>() -> <clinit>
+com.threerings.getdown.launcher.MultipleGetdownRunning -> com.threerings.getdown.launcher.MultipleGetdownRunning:
+    void <init>() -> <init>
+com.threerings.getdown.launcher.ProxyPanel -> com.threerings.getdown.launcher.ProxyPanel:
+    com.threerings.getdown.launcher.Getdown _getdown -> _getdown
+    java.util.ResourceBundle _msgs -> _msgs
+    javax.swing.JTextField _host -> _host
+    javax.swing.JTextField _port -> _port
+    javax.swing.JCheckBox _useAuth -> _useAuth
+    javax.swing.JTextField _username -> _username
+    javax.swing.JPasswordField _password -> _password
+    void <init>(com.threerings.getdown.launcher.Getdown,java.util.ResourceBundle) -> <init>
+    void setProxy(java.lang.String,java.lang.String) -> setProxy
+    void addNotify() -> addNotify
+    java.awt.Dimension getPreferredSize() -> getPreferredSize
+    void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed
+    java.lang.String get(java.lang.String) -> get
+    java.awt.Dimension clampWidth(java.awt.Dimension,int) -> clampWidth
+com.threerings.getdown.launcher.ProxyPanel$1 -> com.threerings.getdown.launcher.m:
+    com.threerings.getdown.launcher.ProxyPanel this$0 -> a
+    void <init>(com.threerings.getdown.launcher.ProxyPanel) -> <init>
+    void itemStateChanged(java.awt.event.ItemEvent) -> itemStateChanged
+com.threerings.getdown.launcher.ProxyPanel$SaneLabelField -> com.threerings.getdown.launcher.ProxyPanel$SaneLabelField:
+    void <init>(java.lang.String) -> <init>
+    java.awt.Dimension getPreferredSize() -> getPreferredSize
+com.threerings.getdown.launcher.ProxyPanel$SanePasswordField -> com.threerings.getdown.launcher.ProxyPanel$SanePasswordField:
+    void <init>() -> <init>
+    java.awt.Dimension getPreferredSize() -> getPreferredSize
+com.threerings.getdown.launcher.ProxyPanel$SaneTextField -> com.threerings.getdown.launcher.ProxyPanel$SaneTextField:
+    void <init>() -> <init>
+    java.awt.Dimension getPreferredSize() -> getPreferredSize
+com.threerings.getdown.launcher.ProxyUtil -> com.threerings.getdown.launcher.ProxyUtil:
+    java.lang.String PROXY_REGISTRY -> PROXY_REGISTRY
+    void <init>() -> <init>
+    boolean autoDetectProxy(com.threerings.getdown.data.Application) -> autoDetectProxy
+    boolean canLoadWithoutProxy(java.net.URL) -> canLoadWithoutProxy
+    void configProxy(com.threerings.getdown.data.Application,java.lang.String,java.lang.String,java.lang.String,java.lang.String) -> configProxy
+    java.lang.String[] loadProxy(com.threerings.getdown.data.Application) -> loadProxy
+    void saveProxy(com.threerings.getdown.data.Application,java.lang.String,java.lang.String) -> saveProxy
+    void initProxy(com.threerings.getdown.data.Application,java.lang.String,java.lang.String,java.lang.String,java.lang.String) -> initProxy
+com.threerings.getdown.launcher.ProxyUtil$1 -> com.threerings.getdown.launcher.n:
+    java.lang.String val$fuser -> a
+    char[] val$fpass -> b
+    void <init>(java.lang.String,char[]) -> <init>
+    java.net.PasswordAuthentication getPasswordAuthentication() -> getPasswordAuthentication
+com.threerings.getdown.launcher.RotatingBackgrounds -> com.threerings.getdown.launcher.RotatingBackgrounds:
+    long currentDisplayStart -> currentDisplayStart
+    int current -> current
+    java.awt.Image[] images -> images
+    java.awt.Image errorImage -> errorImage
+    int[] percentages -> percentages
+    int[] minDisplayTime -> minDisplayTime
+    void <init>() -> <init>
+    void <init>(java.awt.Image) -> <init>
+    void <init>(java.util.List,java.lang.String,com.threerings.getdown.launcher.RotatingBackgrounds$ImageLoader) -> <init>
+    java.awt.Image getImage(int) -> getImage
+    java.awt.Image getErrorImage() -> getErrorImage
+    int getNumImages() -> getNumImages
+    void makeEmpty() -> makeEmpty
+com.threerings.getdown.launcher.RotatingBackgrounds$ImageLoader -> com.threerings.getdown.launcher.RotatingBackgrounds$ImageLoader:
+    java.awt.Image loadImage(java.lang.String) -> loadImage
+com.threerings.getdown.launcher.StatusPanel -> com.threerings.getdown.launcher.StatusPanel:
+    java.awt.Image _barimg -> _barimg
+    com.threerings.getdown.launcher.RotatingBackgrounds _bg -> _bg
+    java.awt.Dimension _psize -> _psize
+    java.util.ResourceBundle _msgs -> _msgs
+    int _progress -> _progress
+    java.lang.String _status -> _status
+    int _statusDots -> _statusDots
+    boolean _displayError -> _displayError
+    com.samskivert.swing.Label _label -> _label
+    com.samskivert.swing.Label _newlab -> _newlab
+    com.samskivert.swing.Label _plabel -> _plabel
+    com.samskivert.swing.Label _newplab -> _newplab
+    com.samskivert.swing.Label _rlabel -> _rlabel
+    com.samskivert.swing.Label _newrlab -> _newrlab
+    com.threerings.getdown.data.Application$UpdateInterface _ifc -> _ifc
+    javax.swing.Timer _timer -> _timer
+    long[] _remain -> _remain
+    int _ridx -> _ridx
+    com.samskivert.util.Throttle _rthrottle -> _rthrottle
+    java.awt.Font FONT -> FONT
+    void <init>(java.util.ResourceBundle) -> <init>
+    void init(com.threerings.getdown.data.Application$UpdateInterface,com.threerings.getdown.launcher.RotatingBackgrounds,java.awt.Image) -> init
+    boolean imageUpdate(java.awt.Image,int,int,int,int,int) -> imageUpdate
+    void setProgress(int,long) -> setProgress
+    void setStatus(java.lang.String,boolean) -> setStatus
+    void stopThrob() -> stopThrob
+    void addNotify() -> addNotify
+    void removeNotify() -> removeNotify
+    void paintComponent(java.awt.Graphics) -> paintComponent
+    java.awt.Dimension getPreferredSize() -> getPreferredSize
+    void updateStatusLabel() -> updateStatusLabel
+    int getStatusY(com.samskivert.swing.Label) -> getStatusY
+    com.samskivert.swing.Label createLabel(java.lang.String,java.awt.Color) -> createLabel
+    java.lang.String xlate(java.lang.String) -> xlate
+    java.lang.String get(java.lang.String,java.lang.String[]) -> get
+    java.lang.String get(java.lang.String) -> get
+    void <clinit>() -> <clinit>
+com.threerings.getdown.launcher.StatusPanel$1 -> com.threerings.getdown.launcher.o:
+    com.threerings.getdown.launcher.StatusPanel this$0 -> a
+    void <init>(com.threerings.getdown.launcher.StatusPanel) -> <init>
+    void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed
+com.threerings.getdown.net.Downloader -> com.threerings.getdown.net.Downloader:
+    java.util.Map _sizes -> _sizes
+    java.util.Map _downloaded -> _downloaded
+    long _start -> _start
+    long _bytesPerSecond -> _bytesPerSecond
+    long _lastUpdate -> _lastUpdate
+    com.threerings.getdown.net.Downloader$State _state -> _state
+    long UPDATE_DELAY -> UPDATE_DELAY
+    void <init>() -> <init>
+    boolean download(java.util.Collection,int) -> download
+    void abort() -> abort
+    void resolvingDownloads() -> resolvingDownloads
+    void downloadProgress(int,long) -> downloadProgress
+    void downloadFailed(com.threerings.getdown.data.Resource,java.lang.Exception) -> downloadFailed
+    long checkSize(com.threerings.getdown.data.Resource) -> checkSize
+    void reportProgress(com.threerings.getdown.data.Resource,long,long) -> reportProgress
+    long sum(java.lang.Iterable) -> sum
+    void download(com.threerings.getdown.data.Resource) -> download
+com.threerings.getdown.net.Downloader$1 -> com.threerings.getdown.net.a:
+    com.threerings.getdown.data.Resource val$rsrc -> a
+    com.threerings.getdown.net.Downloader this$0 -> b
+    void <init>(com.threerings.getdown.net.Downloader,com.threerings.getdown.data.Resource) -> <init>
+    void run() -> run
+com.threerings.getdown.net.Downloader$State -> com.threerings.getdown.net.Downloader$State:
+    com.threerings.getdown.net.Downloader$State DOWNLOADING -> DOWNLOADING
+    com.threerings.getdown.net.Downloader$State COMPLETE -> COMPLETE
+    com.threerings.getdown.net.Downloader$State FAILED -> FAILED
+    com.threerings.getdown.net.Downloader$State ABORTED -> ABORTED
+    com.threerings.getdown.net.Downloader$State[] $VALUES -> $VALUES
+    com.threerings.getdown.net.Downloader$State[] values() -> values
+    com.threerings.getdown.net.Downloader$State valueOf(java.lang.String) -> valueOf
+    void <init>(java.lang.String,int) -> <init>
+    void <clinit>() -> <clinit>
+com.threerings.getdown.net.HTTPDownloader -> com.threerings.getdown.net.HTTPDownloader:
+    java.net.Proxy _proxy -> _proxy
+    void <init>(java.net.Proxy) -> <init>
+    long checkSize(com.threerings.getdown.data.Resource) -> checkSize
+    void download(com.threerings.getdown.data.Resource) -> download
+com.threerings.getdown.spi.ProxyAuth -> com.threerings.getdown.spi.ProxyAuth:
+    com.threerings.getdown.spi.ProxyAuth$Credentials loadCredentials(java.lang.String) -> loadCredentials
+    void saveCredentials(java.lang.String,java.lang.String,java.lang.String) -> saveCredentials
+com.threerings.getdown.spi.ProxyAuth$Credentials -> com.threerings.getdown.spi.ProxyAuth$Credentials:
+    java.lang.String username -> username
+    java.lang.String password -> password
+    void <init>(java.lang.String,java.lang.String) -> <init>
+com.threerings.getdown.tools.Differ -> com.threerings.getdown.tools.Differ:
+    void <init>() -> <init>
+    void createDiff(java.io.File,java.io.File,boolean) -> createDiff
+    void createPatch(java.io.File,java.util.ArrayList,java.util.ArrayList,boolean) -> createPatch
+    java.io.File rebuildJar(java.io.File) -> rebuildJar
+    void jarDiff(java.io.File,java.io.File,java.util.jar.JarOutputStream) -> jarDiff
+    void main(java.lang.String[]) -> main
+    void pipe(java.io.File,java.util.jar.JarOutputStream) -> pipe
+com.threerings.getdown.tools.Digester -> com.threerings.getdown.tools.Digester:
+    void <init>() -> <init>
+    void main(java.lang.String[]) -> main
+    void createDigests(java.io.File,java.io.File,java.lang.String,java.lang.String) -> createDigests
+    void createDigest(int,java.io.File) -> createDigest
+    void signDigest(int,java.io.File,java.io.File,java.lang.String,java.lang.String) -> signDigest
+com.threerings.getdown.tools.JarDiff -> com.threerings.getdown.tools.JarDiff:
+    int DEFAULT_READ_SIZE -> DEFAULT_READ_SIZE
+    byte[] newBytes -> newBytes
+    byte[] oldBytes -> oldBytes
+    boolean _debug -> _debug
+    void <init>() -> <init>
+    void createPatch(java.lang.String,java.lang.String,java.io.OutputStream,boolean) -> createPatch
+    void createIndex(java.util.jar.JarOutputStream,java.util.List,java.util.Map) -> createIndex
+    java.io.Writer writeEscapedString(java.io.Writer,java.lang.String) -> writeEscapedString
+    void writeEntry(java.util.jar.JarOutputStream,java.util.jar.JarEntry,com.threerings.getdown.tools.JarDiff$JarFile2) -> writeEntry
+    byte[] access$000() -> access$000
+    byte[] access$100() -> access$100
+    boolean access$200() -> access$200
+    void <clinit>() -> <clinit>
+com.threerings.getdown.tools.JarDiff$JarFile2 -> com.threerings.getdown.tools.JarDiff$a:
+    java.util.jar.JarFile _jar -> a
+    java.util.List _entries -> b
+    java.util.HashMap _nameToEntryMap -> c
+    java.util.HashMap _crcToEntryMap -> d
+    void <init>(java.lang.String) -> <init>
+    java.util.jar.JarFile getJarFile() -> a
+    java.util.Iterator iterator() -> iterator
+    java.util.jar.JarEntry getEntryByName(java.lang.String) -> a
+    boolean differs(java.io.InputStream,java.io.InputStream) -> a
+    boolean contains(com.threerings.getdown.tools.JarDiff$JarFile2,java.util.jar.JarEntry) -> a
+    java.lang.String hasSameContent(com.threerings.getdown.tools.JarDiff$JarFile2,java.util.jar.JarEntry) -> b
+    void index() -> b
+    void close() -> close
+com.threerings.getdown.tools.JarDiffCodes -> com.threerings.getdown.tools.JarDiffCodes:
+    java.lang.String INDEX_NAME -> INDEX_NAME
+    java.lang.String VERSION_HEADER -> VERSION_HEADER
+    java.lang.String REMOVE_COMMAND -> REMOVE_COMMAND
+    java.lang.String MOVE_COMMAND -> MOVE_COMMAND
+com.threerings.getdown.tools.JarDiffPatcher -> com.threerings.getdown.tools.JarDiffPatcher:
+    int DEFAULT_READ_SIZE -> DEFAULT_READ_SIZE
+    byte[] newBytes -> newBytes
+    byte[] oldBytes -> oldBytes
+    void <init>() -> <init>
+    void patchJar(java.lang.String,java.lang.String,java.io.File,com.threerings.getdown.util.ProgressObserver) -> patchJar
+    void updateObserver(com.threerings.getdown.util.ProgressObserver,double,double) -> updateObserver
+    void determineNameMapping(java.util.jar.JarFile,java.util.Set,java.util.Map) -> determineNameMapping
+    java.util.List getSubpaths(java.lang.String) -> getSubpaths
+    void writeEntry(java.util.jar.JarOutputStream,java.util.jar.JarEntry,java.util.jar.JarFile) -> writeEntry
+    void writeEntry(java.util.jar.JarOutputStream,java.util.jar.JarEntry,java.io.InputStream) -> writeEntry
+    void <clinit>() -> <clinit>
+com.threerings.getdown.tools.Patcher -> com.threerings.getdown.tools.Patcher:
+    java.lang.String CREATE -> CREATE
+    java.lang.String PATCH -> PATCH
+    java.lang.String DELETE -> DELETE
+    com.threerings.getdown.util.ProgressObserver _obs -> _obs
+    long _complete -> _complete
+    long _plength -> _plength
+    byte[] _buffer -> _buffer
+    int COPY_BUFFER_SIZE -> COPY_BUFFER_SIZE
+    void <init>() -> <init>
+    void patch(java.io.File,java.io.File,com.threerings.getdown.util.ProgressObserver) -> patch
+    java.lang.String strip(java.lang.String,java.lang.String) -> strip
+    void createFile(java.util.jar.JarFile,java.util.zip.ZipEntry,java.io.File) -> createFile
+    void patchFile(java.util.jar.JarFile,java.util.zip.ZipEntry,java.io.File,java.lang.String) -> patchFile
+    void updateProgress(int) -> updateProgress
+    void main(java.lang.String[]) -> main
+com.threerings.getdown.tools.Patcher$1 -> com.threerings.getdown.tools.a:
+    long val$elength -> a
+    com.threerings.getdown.tools.Patcher this$0 -> b
+    void <init>(com.threerings.getdown.tools.Patcher,long) -> <init>
+    void progress(int) -> progress
+com.threerings.getdown.util.Base64 -> com.threerings.getdown.util.Base64:
+    int DEFAULT -> DEFAULT
+    int NO_PADDING -> NO_PADDING
+    int NO_WRAP -> NO_WRAP
+    int CRLF -> CRLF
+    int URL_SAFE -> URL_SAFE
+    int NO_CLOSE -> NO_CLOSE
+    boolean $assertionsDisabled -> $assertionsDisabled
+    byte[] decode(java.lang.String,int) -> decode
+    byte[] decode(byte[],int) -> decode
+    byte[] decode(byte[],int,int,int) -> decode
+    java.lang.String encodeToString(byte[],int) -> encodeToString
+    java.lang.String encodeToString(byte[],int,int,int) -> encodeToString
+    byte[] encode(byte[],int) -> encode
+    byte[] encode(byte[],int,int,int) -> encode
+    void <init>() -> <init>
+    void <clinit>() -> <clinit>
+com.threerings.getdown.util.Base64$Coder -> com.threerings.getdown.util.Base64$a:
+    byte[] output -> a
+    int op -> b
+    void <init>() -> <init>
+com.threerings.getdown.util.Base64$Decoder -> com.threerings.getdown.util.Base64$b:
+    int[] DECODE -> c
+    int[] DECODE_WEBSAFE -> d
+    int state -> e
+    int value -> f
+    int[] alphabet -> g
+    void <init>(int,byte[]) -> <init>
+    boolean process(byte[],int,int,boolean) -> a
+    void <clinit>() -> <clinit>
+com.threerings.getdown.util.Base64$Encoder -> com.threerings.getdown.util.Base64$c:
+    byte[] ENCODE -> f
+    byte[] ENCODE_WEBSAFE -> g
+    byte[] tail -> h
+    int tailLen -> i
+    int count -> j
+    boolean do_padding -> c
+    boolean do_newline -> d
+    boolean do_cr -> e
+    byte[] alphabet -> k
+    boolean $assertionsDisabled -> l
+    void <init>(int,byte[]) -> <init>
+    boolean process(byte[],int,int,boolean) -> a
+    void <clinit>() -> <clinit>
+com.threerings.getdown.util.Color -> com.threerings.getdown.util.Color:
+    int CLEAR -> CLEAR
+    int WHITE -> WHITE
+    int BLACK -> BLACK
+    float brightness(int) -> brightness
+    void <init>() -> <init>
+com.threerings.getdown.util.Config -> com.threerings.getdown.util.Config:
+    com.threerings.getdown.util.Config EMPTY -> EMPTY
+    java.util.Map _data -> _data
+    com.threerings.getdown.util.Config$ParseOpts createOpts(boolean) -> createOpts
+    java.util.List parsePairs(java.io.File,com.threerings.getdown.util.Config$ParseOpts) -> parsePairs
+    java.util.List parsePairs(java.io.Reader,com.threerings.getdown.util.Config$ParseOpts) -> parsePairs
+    com.threerings.getdown.util.Rectangle parseRect(java.lang.String,java.lang.String) -> parseRect
+    java.lang.Integer parseColor(java.lang.String) -> parseColor
+    com.threerings.getdown.util.Config parseConfig(java.io.File,com.threerings.getdown.util.Config$ParseOpts) -> parseConfig
+    void <init>(java.util.Map) -> <init>
+    boolean hasValue(java.lang.String) -> hasValue
+    java.lang.Object getRaw(java.lang.String) -> getRaw
+    java.lang.String getString(java.lang.String) -> getString
+    java.lang.String getString(java.lang.String,java.lang.String) -> getString
+    boolean getBoolean(java.lang.String) -> getBoolean
+    java.lang.String[] getMultiValue(java.lang.String) -> getMultiValue
+    com.threerings.getdown.util.Rectangle getRect(java.lang.String,com.threerings.getdown.util.Rectangle) -> getRect
+    int getInt(java.lang.String,int) -> getInt
+    long getLong(java.lang.String,long) -> getLong
+    int getColor(java.lang.String,int) -> getColor
+    java.lang.String[] getList(java.lang.String) -> getList
+    java.lang.String getUrl(java.lang.String,java.lang.String) -> getUrl
+    boolean checkQualifiers(java.lang.String,java.lang.String,java.lang.String) -> checkQualifiers
+    boolean checkQualifier(java.lang.String,java.lang.String,java.lang.String) -> checkQualifier
+    void <clinit>() -> <clinit>
+com.threerings.getdown.util.Config$ParseOpts -> com.threerings.getdown.util.Config$ParseOpts:
+    boolean biasToKey -> biasToKey
+    boolean strictComments -> strictComments
+    java.lang.String osname -> osname
+    java.lang.String osarch -> osarch
+    void <init>() -> <init>
+com.threerings.getdown.util.ConnectionUtil -> com.threerings.getdown.util.ConnectionUtil:
+    void <init>() -> <init>
+    java.net.URLConnection open(java.net.Proxy,java.net.URL,int,int) -> open
+    java.net.HttpURLConnection openHttp(java.net.Proxy,java.net.URL,int,int) -> openHttp
+com.threerings.getdown.util.FileUtil -> com.threerings.getdown.util.FileUtil:
+    void <init>() -> <init>
+    boolean renameTo(java.io.File,java.io.File) -> renameTo
+    boolean deleteHarder(java.io.File) -> deleteHarder
+    boolean deleteDirHarder(java.io.File) -> deleteDirHarder
+    java.util.List readLines(java.io.Reader) -> readLines
+    void unpackJar(java.util.jar.JarFile,java.io.File,boolean) -> unpackJar
+    void unpackPacked200Jar(java.io.File,java.io.File) -> unpackPacked200Jar
+    void copy(java.io.File,java.io.File) -> copy
+    void makeExecutable(java.io.File) -> makeExecutable
+    void walkTree(java.io.File,com.threerings.getdown.util.FileUtil$Visitor) -> walkTree
+com.threerings.getdown.util.FileUtil$Visitor -> com.threerings.getdown.util.FileUtil$Visitor:
+    void visit(java.io.File) -> visit
+com.threerings.getdown.util.HostWhitelist -> com.threerings.getdown.util.HostWhitelist:
+    void <init>() -> <init>
+    java.net.URL verify(java.net.URL) -> verify
+    java.net.URL verify(java.util.List,java.net.URL) -> verify
+com.threerings.getdown.util.LaunchUtil -> com.threerings.getdown.util.LaunchUtil:
+    java.lang.String LOCAL_JAVA_DIR -> LOCAL_JAVA_DIR
+    boolean _isWindows -> _isWindows
+    boolean _isMacOS -> _isMacOS
+    boolean _isLinux -> _isLinux
+    void <init>() -> <init>
+    boolean updateVersionAndRelaunch(java.io.File,java.lang.String,java.lang.String) -> updateVersionAndRelaunch
+    java.lang.String getJVMPath(java.io.File) -> getJVMPath
+    java.lang.String getJVMPath(java.io.File,boolean) -> getJVMPath
+    void upgradeGetdown(java.io.File,java.io.File,java.io.File) -> upgradeGetdown
+    boolean mustMonitorChildren() -> mustMonitorChildren
+    boolean isWindows() -> isWindows
+    boolean isMacOS() -> isMacOS
+    boolean isLinux() -> isLinux
+    java.lang.String checkJVMPath(java.lang.String,boolean) -> checkJVMPath
+    void <clinit>() -> <clinit>
+com.threerings.getdown.util.MessageUtil -> com.threerings.getdown.util.MessageUtil:
+    java.lang.String TAINT_CHAR -> TAINT_CHAR
+    void <init>() -> <init>
+    boolean isTainted(java.lang.String) -> isTainted
+    java.lang.String taint(java.lang.Object) -> taint
+    java.lang.String untaint(java.lang.String) -> untaint
+    java.lang.String compose(java.lang.String,java.lang.Object[]) -> compose
+    java.lang.String compose(java.lang.String,java.lang.String[]) -> compose
+    java.lang.String tcompose(java.lang.String,java.lang.Object[]) -> tcompose
+    java.lang.String tcompose(java.lang.String,java.lang.String[]) -> tcompose
+    java.lang.String escape(java.lang.String) -> escape
+    java.lang.String unescape(java.lang.String) -> unescape
+com.threerings.getdown.util.ProgressAggregator -> com.threerings.getdown.util.ProgressAggregator:
+    com.threerings.getdown.util.ProgressObserver _target -> _target
+    long[] _sizes -> _sizes
+    int[] _progress -> _progress
+    void <init>(com.threerings.getdown.util.ProgressObserver,long[]) -> <init>
+    com.threerings.getdown.util.ProgressObserver startElement(int) -> startElement
+    void updateAggProgress() -> updateAggProgress
+    long sum(long[]) -> sum
+com.threerings.getdown.util.ProgressAggregator$1 -> com.threerings.getdown.util.a:
+    int val$index -> a
+    com.threerings.getdown.util.ProgressAggregator this$0 -> b
+    void <init>(com.threerings.getdown.util.ProgressAggregator,int) -> <init>
+    void progress(int) -> progress
+com.threerings.getdown.util.ProgressObserver -> com.threerings.getdown.util.ProgressObserver:
+    void progress(int) -> progress
+com.threerings.getdown.util.Rectangle -> com.threerings.getdown.util.Rectangle:
+    int x -> x
+    int y -> y
+    int width -> width
+    int height -> height
+    void <init>(int,int,int,int) -> <init>
+    com.threerings.getdown.util.Rectangle union(com.threerings.getdown.util.Rectangle) -> union
+    java.lang.String toString() -> toString
+com.threerings.getdown.util.StreamUtil -> com.threerings.getdown.util.StreamUtil:
+    void <init>() -> <init>
+    void close(java.io.InputStream) -> close
+    void close(java.io.OutputStream) -> close
+    void close(java.io.Reader) -> close
+    void close(java.io.Writer) -> close
+    java.io.OutputStream copy(java.io.InputStream,java.io.OutputStream) -> copy
+    byte[] toByteArray(java.io.InputStream) -> toByteArray
+com.threerings.getdown.util.StringUtil -> com.threerings.getdown.util.StringUtil:
+    java.lang.String XLATE -> XLATE
+    void <init>() -> <init>
+    boolean couldBeValidUrl(java.lang.String) -> couldBeValidUrl
+    boolean isBlank(java.lang.String) -> isBlank
+    int[] parseIntArray(java.lang.String) -> parseIntArray
+    java.lang.String[] parseStringArray(java.lang.String) -> parseStringArray
+    java.lang.String[] parseStringArray(java.lang.String,boolean) -> parseStringArray
+    java.lang.String deNull(java.lang.String) -> deNull
+    java.lang.String hexlate(byte[],int) -> hexlate
+    java.lang.String hexlate(byte[]) -> hexlate
+    java.lang.String join(java.lang.Object[]) -> join
+    java.lang.String join(java.lang.Object[],boolean) -> join
+    java.lang.String join(java.lang.Object[],java.lang.String) -> join
+    java.lang.String join(java.lang.Object[],java.lang.String,boolean) -> join
+com.threerings.getdown.util.VersionUtil -> com.threerings.getdown.util.VersionUtil:
+    void <init>() -> <init>
+    long readVersion(java.io.File) -> readVersion
+    void writeVersion(java.io.File,long) -> writeVersion
+    long parseJavaVersion(java.lang.String,java.lang.String) -> parseJavaVersion
+    long readReleaseVersion(java.io.File,java.lang.String) -> readReleaseVersion
+    int parseInt(java.lang.String) -> parseInt
diff --git a/getdown/src/getdown/launcher/target/proguard_seed.txt b/getdown/src/getdown/launcher/target/proguard_seed.txt
new file mode 100644 (file)
index 0000000..39b31be
--- /dev/null
@@ -0,0 +1,879 @@
+ca.beq.util.win32.registry.KeyIterator
+ca.beq.util.win32.registry.KeyIterator: ca.beq.util.win32.registry.RegistryKey m_key
+ca.beq.util.win32.registry.KeyIterator: int m_index
+ca.beq.util.win32.registry.KeyIterator: int m_hkey
+ca.beq.util.win32.registry.KeyIterator: int m_maxsize
+ca.beq.util.win32.registry.KeyIterator: int m_count
+ca.beq.util.win32.registry.KeyIterator: KeyIterator(ca.beq.util.win32.registry.RegistryKey)
+ca.beq.util.win32.registry.KeyIterator: void initializeFields()
+ca.beq.util.win32.registry.KeyIterator: boolean hasNext()
+ca.beq.util.win32.registry.KeyIterator: java.lang.Object next()
+ca.beq.util.win32.registry.KeyIterator: java.lang.String getNext()
+ca.beq.util.win32.registry.KeyIterator: void remove()
+ca.beq.util.win32.registry.RegistryException
+ca.beq.util.win32.registry.RegistryException: RegistryException()
+ca.beq.util.win32.registry.RegistryException: RegistryException(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey
+ca.beq.util.win32.registry.RegistryKey: boolean c_initSucceeded
+ca.beq.util.win32.registry.RegistryKey: ca.beq.util.win32.registry.RootKey m_root
+ca.beq.util.win32.registry.RegistryKey: java.lang.String m_path
+ca.beq.util.win32.registry.RegistryKey: void testInitialized()
+ca.beq.util.win32.registry.RegistryKey: void initialize()
+ca.beq.util.win32.registry.RegistryKey: void initialize(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: boolean isInitialized()
+ca.beq.util.win32.registry.RegistryKey: void checkInitialized()
+ca.beq.util.win32.registry.RegistryKey: RegistryKey()
+ca.beq.util.win32.registry.RegistryKey: RegistryKey(ca.beq.util.win32.registry.RootKey)
+ca.beq.util.win32.registry.RegistryKey: RegistryKey(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: RegistryKey(ca.beq.util.win32.registry.RootKey,java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: ca.beq.util.win32.registry.RootKey getRootKey()
+ca.beq.util.win32.registry.RegistryKey: java.lang.String getPath()
+ca.beq.util.win32.registry.RegistryKey: java.lang.String makePath(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: java.lang.String getName()
+ca.beq.util.win32.registry.RegistryKey: boolean exists()
+ca.beq.util.win32.registry.RegistryKey: void create()
+ca.beq.util.win32.registry.RegistryKey: ca.beq.util.win32.registry.RegistryKey createSubkey(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: void delete()
+ca.beq.util.win32.registry.RegistryKey: boolean hasSubkeys()
+ca.beq.util.win32.registry.RegistryKey: boolean hasSubkey(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: java.util.Iterator subkeys()
+ca.beq.util.win32.registry.RegistryKey: java.util.Iterator values()
+ca.beq.util.win32.registry.RegistryKey: boolean hasValue(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: boolean hasValues()
+ca.beq.util.win32.registry.RegistryKey: ca.beq.util.win32.registry.RegistryValue getValue(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: void setValue(ca.beq.util.win32.registry.RegistryValue)
+ca.beq.util.win32.registry.RegistryKey: void deleteValue(java.lang.String)
+ca.beq.util.win32.registry.RegistryKey: java.lang.String toString()
+ca.beq.util.win32.registry.RegistryKey: void <clinit>()
+ca.beq.util.win32.registry.RegistryValue
+ca.beq.util.win32.registry.RegistryValue: java.lang.String m_name
+ca.beq.util.win32.registry.RegistryValue: ca.beq.util.win32.registry.ValueType m_type
+ca.beq.util.win32.registry.RegistryValue: java.lang.Object m_data
+ca.beq.util.win32.registry.RegistryValue: RegistryValue()
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.Object)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,java.lang.Object)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,ca.beq.util.win32.registry.ValueType,java.lang.Object)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,boolean)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,byte)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,int)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,long)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,float)
+ca.beq.util.win32.registry.RegistryValue: RegistryValue(java.lang.String,double)
+ca.beq.util.win32.registry.RegistryValue: java.lang.String getName()
+ca.beq.util.win32.registry.RegistryValue: void setName(java.lang.String)
+ca.beq.util.win32.registry.RegistryValue: ca.beq.util.win32.registry.ValueType getType()
+ca.beq.util.win32.registry.RegistryValue: void setType(ca.beq.util.win32.registry.ValueType)
+ca.beq.util.win32.registry.RegistryValue: java.lang.Object getData()
+ca.beq.util.win32.registry.RegistryValue: void setData(java.lang.Object)
+ca.beq.util.win32.registry.RegistryValue: void setData(byte)
+ca.beq.util.win32.registry.RegistryValue: void setData(boolean)
+ca.beq.util.win32.registry.RegistryValue: void setData(int)
+ca.beq.util.win32.registry.RegistryValue: void setData(long)
+ca.beq.util.win32.registry.RegistryValue: void setData(float)
+ca.beq.util.win32.registry.RegistryValue: void setData(double)
+ca.beq.util.win32.registry.RegistryValue: java.lang.String getStringValue()
+ca.beq.util.win32.registry.RegistryValue: java.lang.String toString()
+ca.beq.util.win32.registry.RootKey
+ca.beq.util.win32.registry.RootKey: java.lang.String m_name
+ca.beq.util.win32.registry.RootKey: int m_value
+ca.beq.util.win32.registry.RootKey: ca.beq.util.win32.registry.RootKey HKEY_CLASSES_ROOT
+ca.beq.util.win32.registry.RootKey: ca.beq.util.win32.registry.RootKey HKEY_CURRENT_USER
+ca.beq.util.win32.registry.RootKey: ca.beq.util.win32.registry.RootKey HKEY_LOCAL_MACHINE
+ca.beq.util.win32.registry.RootKey: ca.beq.util.win32.registry.RootKey HKEY_USERS
+ca.beq.util.win32.registry.RootKey: ca.beq.util.win32.registry.RootKey HKEY_CURRENT_CONFIG
+ca.beq.util.win32.registry.RootKey: ca.beq.util.win32.registry.RootKey HKEY_PERFORMANCE_DATA
+ca.beq.util.win32.registry.RootKey: ca.beq.util.win32.registry.RootKey HKEY_DYN_DATA
+ca.beq.util.win32.registry.RootKey: RootKey(java.lang.String,int)
+ca.beq.util.win32.registry.RootKey: int getValue()
+ca.beq.util.win32.registry.RootKey: java.lang.String toString()
+ca.beq.util.win32.registry.RootKey: void <clinit>()
+ca.beq.util.win32.registry.ValueIterator
+ca.beq.util.win32.registry.ValueIterator: ca.beq.util.win32.registry.RegistryKey m_key
+ca.beq.util.win32.registry.ValueIterator: int m_index
+ca.beq.util.win32.registry.ValueIterator: int m_hkey
+ca.beq.util.win32.registry.ValueIterator: int m_maxsize
+ca.beq.util.win32.registry.ValueIterator: int m_count
+ca.beq.util.win32.registry.ValueIterator: ValueIterator(ca.beq.util.win32.registry.RegistryKey)
+ca.beq.util.win32.registry.ValueIterator: void initializeFields()
+ca.beq.util.win32.registry.ValueIterator: boolean hasNext()
+ca.beq.util.win32.registry.ValueIterator: java.lang.Object next()
+ca.beq.util.win32.registry.ValueIterator: java.lang.String getNext()
+ca.beq.util.win32.registry.ValueIterator: void remove()
+ca.beq.util.win32.registry.ValueType
+ca.beq.util.win32.registry.ValueType: java.lang.String m_name
+ca.beq.util.win32.registry.ValueType: int m_value
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_NONE
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_SZ
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_EXPAND_SZ
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_BINARY
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_DWORD
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_DWORD_LITTLE_ENDIAN
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_DWORD_BIG_ENDIAN
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_MULTI_SZ
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_RESOURCE_LIST
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_LINK
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_FULL_RESOURCE_DESCRIPTOR
+ca.beq.util.win32.registry.ValueType: ca.beq.util.win32.registry.ValueType REG_RESOURCE_REQUIREMENTS_LIST
+ca.beq.util.win32.registry.ValueType: ValueType(java.lang.String,int)
+ca.beq.util.win32.registry.ValueType: int getValue()
+ca.beq.util.win32.registry.ValueType: java.lang.String toString()
+ca.beq.util.win32.registry.ValueType: void <clinit>()
+com.threerings.getdown.Log
+com.threerings.getdown.Log: com.threerings.getdown.Log$Shim log
+com.threerings.getdown.Log: java.lang.String DATE_FORMAT
+com.threerings.getdown.Log: java.util.logging.Level[] LEVELS
+com.threerings.getdown.Log: Log()
+com.threerings.getdown.Log: java.lang.String format(java.lang.Object,java.lang.Object[])
+com.threerings.getdown.Log: void <clinit>()
+com.threerings.getdown.Log$OneLineFormatter
+com.threerings.getdown.Log$OneLineFormatter: java.util.Date _date
+com.threerings.getdown.Log$OneLineFormatter: java.text.SimpleDateFormat _format
+com.threerings.getdown.Log$OneLineFormatter: java.text.FieldPosition _fpos
+com.threerings.getdown.Log$OneLineFormatter: Log$OneLineFormatter()
+com.threerings.getdown.Log$OneLineFormatter: java.lang.String format(java.util.logging.LogRecord)
+com.threerings.getdown.Log$Shim
+com.threerings.getdown.Log$Shim: java.util.logging.Logger _impl
+com.threerings.getdown.Log$Shim: Log$Shim()
+com.threerings.getdown.Log$Shim: void debug(java.lang.Object,java.lang.Object[])
+com.threerings.getdown.Log$Shim: void info(java.lang.Object,java.lang.Object[])
+com.threerings.getdown.Log$Shim: void warning(java.lang.Object,java.lang.Object[])
+com.threerings.getdown.Log$Shim: void error(java.lang.Object,java.lang.Object[])
+com.threerings.getdown.Log$Shim: void doLog(int,java.lang.Object,java.lang.Object[])
+com.threerings.getdown.cache.GarbageCollector
+com.threerings.getdown.cache.GarbageCollector: GarbageCollector()
+com.threerings.getdown.cache.GarbageCollector: void collect(java.io.File,long)
+com.threerings.getdown.cache.GarbageCollector: void collectNative(java.io.File,long)
+com.threerings.getdown.cache.GarbageCollector: boolean shouldDelete(java.io.File,long)
+com.threerings.getdown.cache.GarbageCollector: java.io.File getLastAccessedFile(java.io.File)
+com.threerings.getdown.cache.GarbageCollector: boolean isLastAccessedFile(java.io.File)
+com.threerings.getdown.cache.GarbageCollector: java.io.File getCachedFile(java.io.File)
+com.threerings.getdown.cache.GarbageCollector: java.io.File access$000(java.io.File)
+com.threerings.getdown.cache.GarbageCollector: java.io.File access$100(java.io.File)
+com.threerings.getdown.cache.GarbageCollector: boolean access$200(java.io.File,long)
+com.threerings.getdown.cache.ResourceCache
+com.threerings.getdown.cache.ResourceCache: java.io.File _cacheDir
+com.threerings.getdown.cache.ResourceCache: java.lang.String LAST_ACCESSED_FILE_SUFFIX
+com.threerings.getdown.cache.ResourceCache: ResourceCache(java.io.File)
+com.threerings.getdown.cache.ResourceCache: void createDirectoryIfNecessary(java.io.File)
+com.threerings.getdown.cache.ResourceCache: java.io.File cacheFile(java.io.File,java.lang.String,java.lang.String)
+com.threerings.getdown.cache.ResourceCache: void createNewFile(java.io.File)
+com.threerings.getdown.cache.ResourceCache: java.lang.String getFileSuffix(java.io.File)
+com.threerings.getdown.data.Application
+com.threerings.getdown.data.Application: java.lang.String CONFIG_FILE
+com.threerings.getdown.data.Application: java.lang.String VERSION_FILE
+com.threerings.getdown.data.Application: java.lang.String PROP_PASSTHROUGH_PREFIX
+com.threerings.getdown.data.Application: java.lang.String SIGNATURE_SUFFIX
+com.threerings.getdown.data.Application: java.lang.String MANIFEST_CLASS
+com.threerings.getdown.data.Application: java.net.Proxy proxy
+com.threerings.getdown.data.Application: com.threerings.getdown.data.EnvConfig _envc
+com.threerings.getdown.data.Application: java.io.File _config
+com.threerings.getdown.data.Application: com.threerings.getdown.data.Digest _digest
+com.threerings.getdown.data.Application: long _version
+com.threerings.getdown.data.Application: long _targetVersion
+com.threerings.getdown.data.Application: java.lang.String _appbase
+com.threerings.getdown.data.Application: java.net.URL _vappbase
+com.threerings.getdown.data.Application: java.net.URL _latest
+com.threerings.getdown.data.Application: java.lang.String _class
+com.threerings.getdown.data.Application: java.lang.String _dockName
+com.threerings.getdown.data.Application: java.lang.String _dockIconPath
+com.threerings.getdown.data.Application: boolean _strictComments
+com.threerings.getdown.data.Application: boolean _windebug
+com.threerings.getdown.data.Application: boolean _allowOffline
+com.threerings.getdown.data.Application: int _maxConcDownloads
+com.threerings.getdown.data.Application: java.lang.String _trackingURL
+com.threerings.getdown.data.Application: java.util.Set _trackingPcts
+com.threerings.getdown.data.Application: java.lang.String _trackingCookieName
+com.threerings.getdown.data.Application: java.lang.String _trackingCookieProperty
+com.threerings.getdown.data.Application: java.lang.String _trackingURLSuffix
+com.threerings.getdown.data.Application: java.lang.String _trackingGAHash
+com.threerings.getdown.data.Application: long _trackingStart
+com.threerings.getdown.data.Application: int _trackingId
+com.threerings.getdown.data.Application: java.lang.String _javaVersionProp
+com.threerings.getdown.data.Application: java.lang.String _javaVersionRegex
+com.threerings.getdown.data.Application: long _javaMinVersion
+com.threerings.getdown.data.Application: long _javaMaxVersion
+com.threerings.getdown.data.Application: boolean _javaExactVersionRequired
+com.threerings.getdown.data.Application: java.lang.String _javaLocation
+com.threerings.getdown.data.Application: java.util.List _codes
+com.threerings.getdown.data.Application: java.util.List _resources
+com.threerings.getdown.data.Application: boolean _useCodeCache
+com.threerings.getdown.data.Application: int _codeCacheRetentionDays
+com.threerings.getdown.data.Application: java.util.Map _auxgroups
+com.threerings.getdown.data.Application: java.util.Map _auxactive
+com.threerings.getdown.data.Application: java.util.List _jvmargs
+com.threerings.getdown.data.Application: java.util.List _appargs
+com.threerings.getdown.data.Application: java.lang.String[] _optimumJvmArgs
+com.threerings.getdown.data.Application: java.util.List _txtJvmArgs
+com.threerings.getdown.data.Application: boolean _warnedAboutSetLastModified
+com.threerings.getdown.data.Application: java.nio.channels.FileLock _lock
+com.threerings.getdown.data.Application: java.nio.channels.FileChannel _lockChannel
+com.threerings.getdown.data.Application: java.util.Random _rando
+com.threerings.getdown.data.Application: java.lang.String[] EMPTY_STRING_ARRAY
+com.threerings.getdown.data.Application: java.lang.String ENV_VAR_PREFIX
+com.threerings.getdown.data.Application: java.util.regex.Pattern ENV_VAR_PATTERN
+com.threerings.getdown.data.Application: Application(com.threerings.getdown.data.EnvConfig)
+com.threerings.getdown.data.Application: java.io.File getAppDir()
+com.threerings.getdown.data.Application: boolean useCodeCache()
+com.threerings.getdown.data.Application: int getCodeCacheRetentionDays()
+com.threerings.getdown.data.Application: int maxConcurrentDownloads()
+com.threerings.getdown.data.Application: com.threerings.getdown.data.Resource getConfigResource()
+com.threerings.getdown.data.Application: java.util.List getCodeResources()
+com.threerings.getdown.data.Application: java.util.List getResources()
+com.threerings.getdown.data.Application: java.lang.String getDigest(com.threerings.getdown.data.Resource)
+com.threerings.getdown.data.Application: java.util.List getAllActiveResources()
+com.threerings.getdown.data.Application: com.threerings.getdown.data.Application$AuxGroup getAuxGroup(java.lang.String)
+com.threerings.getdown.data.Application: java.lang.Iterable getAuxGroups()
+com.threerings.getdown.data.Application: boolean isAuxGroupActive(java.lang.String)
+com.threerings.getdown.data.Application: java.util.List getActiveCodeResources()
+com.threerings.getdown.data.Application: java.util.List getNativeResources()
+com.threerings.getdown.data.Application: java.util.List getActiveResources()
+com.threerings.getdown.data.Application: com.threerings.getdown.data.Resource getPatchResource(java.lang.String)
+com.threerings.getdown.data.Application: com.threerings.getdown.data.Resource getJavaVMResource()
+com.threerings.getdown.data.Application: com.threerings.getdown.data.Resource getFullResource()
+com.threerings.getdown.data.Application: java.net.URL getTrackingURL(java.lang.String)
+com.threerings.getdown.data.Application: java.net.URL getTrackingProgressURL(int)
+com.threerings.getdown.data.Application: java.lang.String getTrackingCookieName()
+com.threerings.getdown.data.Application: java.lang.String getTrackingCookieProperty()
+com.threerings.getdown.data.Application: com.threerings.getdown.util.Config init(boolean)
+com.threerings.getdown.data.Application: void fillAssignmentListFromPairs(java.lang.String,java.util.List)
+com.threerings.getdown.data.Application: java.net.URL getRemoteURL(java.lang.String)
+com.threerings.getdown.data.Application: java.io.File getLocalPath(java.lang.String)
+com.threerings.getdown.data.Application: boolean haveValidJavaVersion()
+com.threerings.getdown.data.Application: boolean hasOptimumJvmArgs()
+com.threerings.getdown.data.Application: boolean allowOffline()
+com.threerings.getdown.data.Application: void attemptRecovery(com.threerings.getdown.data.Application$StatusDisplay)
+com.threerings.getdown.data.Application: void updateMetadata()
+com.threerings.getdown.data.Application: java.lang.Process createProcess(boolean)
+com.threerings.getdown.data.Application: java.lang.String[] createEnvironment()
+com.threerings.getdown.data.Application: void invokeDirect()
+com.threerings.getdown.data.Application: java.lang.String processArg(java.lang.String)
+com.threerings.getdown.data.Application: boolean verifyMetadata(com.threerings.getdown.data.Application$StatusDisplay)
+com.threerings.getdown.data.Application: void verifyResources(com.threerings.getdown.util.ProgressObserver,int[],java.util.Set,java.util.Set,java.util.Set)
+com.threerings.getdown.data.Application: void verifyResource(com.threerings.getdown.data.Resource,com.threerings.getdown.util.ProgressObserver,int[],java.util.Set,java.util.Set,java.util.Set)
+com.threerings.getdown.data.Application: void unpackResources(com.threerings.getdown.util.ProgressObserver,java.util.Set)
+com.threerings.getdown.data.Application: void clearValidationMarkers()
+com.threerings.getdown.data.Application: long getVersion()
+com.threerings.getdown.data.Application: java.net.URL createVAppBase(long)
+com.threerings.getdown.data.Application: void clearValidationMarkers(java.util.Iterator)
+com.threerings.getdown.data.Application: void downloadConfigFile()
+com.threerings.getdown.data.Application: boolean lockForUpdates()
+com.threerings.getdown.data.Application: void releaseLock()
+com.threerings.getdown.data.Application: void downloadDigestFiles()
+com.threerings.getdown.data.Application: void downloadControlFile(java.lang.String,int)
+com.threerings.getdown.data.Application: java.io.File downloadFile(java.lang.String)
+com.threerings.getdown.data.Application: com.threerings.getdown.data.Resource createResource(java.lang.String,java.util.EnumSet)
+com.threerings.getdown.data.Application: void addAll(java.lang.String[],java.util.List)
+com.threerings.getdown.data.Application: java.util.List intsToList(int[])
+com.threerings.getdown.data.Application: java.util.List stringsToList(java.lang.String[])
+com.threerings.getdown.data.Application: void parseResources(com.threerings.getdown.util.Config,java.lang.String,java.util.EnumSet,java.util.List)
+com.threerings.getdown.data.Application: java.lang.String getGATrackingCode()
+com.threerings.getdown.data.Application: java.lang.String encodePath(java.lang.String)
+com.threerings.getdown.data.Application: java.io.File getLocalPath(java.io.File,java.lang.String)
+com.threerings.getdown.data.Application: void access$000(com.threerings.getdown.data.Application,com.threerings.getdown.data.Resource,com.threerings.getdown.util.ProgressObserver,int[],java.util.Set,java.util.Set,java.util.Set)
+com.threerings.getdown.data.Application: void <clinit>()
+com.threerings.getdown.data.Application$AuxGroup
+com.threerings.getdown.data.Application$AuxGroup: java.lang.String name
+com.threerings.getdown.data.Application$AuxGroup: java.util.List codes
+com.threerings.getdown.data.Application$AuxGroup: java.util.List rsrcs
+com.threerings.getdown.data.Application$AuxGroup: Application$AuxGroup(java.lang.String,java.util.List,java.util.List)
+com.threerings.getdown.data.Application$StatusDisplay
+com.threerings.getdown.data.Application$StatusDisplay: void updateStatus(java.lang.String)
+com.threerings.getdown.data.Application$UpdateInterface
+com.threerings.getdown.data.Application$UpdateInterface: java.lang.String name
+com.threerings.getdown.data.Application$UpdateInterface: int background
+com.threerings.getdown.data.Application$UpdateInterface: java.util.List rotatingBackgrounds
+com.threerings.getdown.data.Application$UpdateInterface: java.lang.String errorBackground
+com.threerings.getdown.data.Application$UpdateInterface: java.util.List iconImages
+com.threerings.getdown.data.Application$UpdateInterface: java.lang.String backgroundImage
+com.threerings.getdown.data.Application$UpdateInterface: java.lang.String progressImage
+com.threerings.getdown.data.Application$UpdateInterface: com.threerings.getdown.util.Rectangle progress
+com.threerings.getdown.data.Application$UpdateInterface: int progressText
+com.threerings.getdown.data.Application$UpdateInterface: int progressBar
+com.threerings.getdown.data.Application$UpdateInterface: com.threerings.getdown.util.Rectangle status
+com.threerings.getdown.data.Application$UpdateInterface: int statusText
+com.threerings.getdown.data.Application$UpdateInterface: int textShadow
+com.threerings.getdown.data.Application$UpdateInterface: java.lang.String installError
+com.threerings.getdown.data.Application$UpdateInterface: com.threerings.getdown.util.Rectangle patchNotes
+com.threerings.getdown.data.Application$UpdateInterface: java.lang.String patchNotesUrl
+com.threerings.getdown.data.Application$UpdateInterface: boolean hideDecorations
+com.threerings.getdown.data.Application$UpdateInterface: boolean hideProgressText
+com.threerings.getdown.data.Application$UpdateInterface: int minShowSeconds
+com.threerings.getdown.data.Application$UpdateInterface: java.util.Map stepPercentages
+com.threerings.getdown.data.Application$UpdateInterface: java.lang.String toString()
+com.threerings.getdown.data.Application$UpdateInterface: Application$UpdateInterface(com.threerings.getdown.util.Config)
+com.threerings.getdown.data.Application$UpdateInterface$Step
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step UPDATE_JAVA
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step VERIFY_METADATA
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step DOWNLOAD
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step PATCH
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step VERIFY_RESOURCES
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step REDOWNLOAD_RESOURCES
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step UNPACK
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step LAUNCH
+com.threerings.getdown.data.Application$UpdateInterface$Step: java.util.List defaultPercents
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step[] $VALUES
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step[] values()
+com.threerings.getdown.data.Application$UpdateInterface$Step: com.threerings.getdown.data.Application$UpdateInterface$Step valueOf(java.lang.String)
+com.threerings.getdown.data.Application$UpdateInterface$Step: Application$UpdateInterface$Step(java.lang.String,int,int[])
+com.threerings.getdown.data.Application$UpdateInterface$Step: void <clinit>()
+com.threerings.getdown.data.Build
+com.threerings.getdown.data.Build: Build()
+com.threerings.getdown.data.Build: java.lang.String time()
+com.threerings.getdown.data.Build: java.lang.String version()
+com.threerings.getdown.data.Build: java.util.List hostWhitelist()
+com.threerings.getdown.data.ClassPath
+com.threerings.getdown.data.ClassPath: java.util.Set _classPathEntries
+com.threerings.getdown.data.ClassPath: ClassPath(java.util.LinkedHashSet)
+com.threerings.getdown.data.ClassPath: java.lang.String asArgumentString()
+com.threerings.getdown.data.ClassPath: java.net.URL[] asUrls()
+com.threerings.getdown.data.ClassPath: java.util.Set getClassPathEntries()
+com.threerings.getdown.data.ClassPath: java.net.URL getURL(java.io.File)
+com.threerings.getdown.data.Digest
+com.threerings.getdown.data.Digest: int VERSION
+com.threerings.getdown.data.Digest: java.util.HashMap _digests
+com.threerings.getdown.data.Digest: java.lang.String _metaDigest
+com.threerings.getdown.data.Digest: java.lang.String FILE_NAME
+com.threerings.getdown.data.Digest: java.lang.String FILE_SUFFIX
+com.threerings.getdown.data.Digest: java.lang.String digestFile(int)
+com.threerings.getdown.data.Digest: java.lang.String sigAlgorithm(int)
+com.threerings.getdown.data.Digest: void createDigest(int,java.util.List,java.io.File)
+com.threerings.getdown.data.Digest: java.security.MessageDigest getMessageDigest(int)
+com.threerings.getdown.data.Digest: Digest(java.io.File,boolean)
+com.threerings.getdown.data.Digest: Digest(java.io.File,int,boolean)
+com.threerings.getdown.data.Digest: java.lang.String getMetaDigest()
+com.threerings.getdown.data.Digest: boolean validateResource(com.threerings.getdown.data.Resource,com.threerings.getdown.util.ProgressObserver)
+com.threerings.getdown.data.Digest: java.lang.String getDigest(com.threerings.getdown.data.Resource)
+com.threerings.getdown.data.Digest: void note(java.lang.StringBuilder,java.lang.String,java.lang.String)
+com.threerings.getdown.data.EnvConfig
+com.threerings.getdown.data.EnvConfig: java.io.File appDir
+com.threerings.getdown.data.EnvConfig: java.lang.String appId
+com.threerings.getdown.data.EnvConfig: java.lang.String appBase
+com.threerings.getdown.data.EnvConfig: java.util.List certs
+com.threerings.getdown.data.EnvConfig: java.util.List appArgs
+com.threerings.getdown.data.EnvConfig: java.lang.String USER_HOME_KEY
+com.threerings.getdown.data.EnvConfig: com.threerings.getdown.data.EnvConfig create(java.lang.String[],java.util.List)
+com.threerings.getdown.data.EnvConfig: EnvConfig(java.io.File)
+com.threerings.getdown.data.EnvConfig: EnvConfig(java.io.File,java.lang.String,java.lang.String,java.util.List,java.util.List)
+com.threerings.getdown.data.EnvConfig$Note
+com.threerings.getdown.data.EnvConfig$Note: com.threerings.getdown.data.EnvConfig$Note$Level level
+com.threerings.getdown.data.EnvConfig$Note: java.lang.String message
+com.threerings.getdown.data.EnvConfig$Note: com.threerings.getdown.data.EnvConfig$Note info(java.lang.String)
+com.threerings.getdown.data.EnvConfig$Note: com.threerings.getdown.data.EnvConfig$Note warn(java.lang.String)
+com.threerings.getdown.data.EnvConfig$Note: com.threerings.getdown.data.EnvConfig$Note error(java.lang.String)
+com.threerings.getdown.data.EnvConfig$Note: EnvConfig$Note(com.threerings.getdown.data.EnvConfig$Note$Level,java.lang.String)
+com.threerings.getdown.data.EnvConfig$Note$Level
+com.threerings.getdown.data.EnvConfig$Note$Level: com.threerings.getdown.data.EnvConfig$Note$Level INFO
+com.threerings.getdown.data.EnvConfig$Note$Level: com.threerings.getdown.data.EnvConfig$Note$Level WARN
+com.threerings.getdown.data.EnvConfig$Note$Level: com.threerings.getdown.data.EnvConfig$Note$Level ERROR
+com.threerings.getdown.data.EnvConfig$Note$Level: com.threerings.getdown.data.EnvConfig$Note$Level[] $VALUES
+com.threerings.getdown.data.EnvConfig$Note$Level: com.threerings.getdown.data.EnvConfig$Note$Level[] values()
+com.threerings.getdown.data.EnvConfig$Note$Level: com.threerings.getdown.data.EnvConfig$Note$Level valueOf(java.lang.String)
+com.threerings.getdown.data.EnvConfig$Note$Level: EnvConfig$Note$Level(java.lang.String,int)
+com.threerings.getdown.data.EnvConfig$Note$Level: void <clinit>()
+com.threerings.getdown.data.PathBuilder
+com.threerings.getdown.data.PathBuilder: java.lang.String CODE_CACHE_DIR
+com.threerings.getdown.data.PathBuilder: java.lang.String NATIVE_CACHE_DIR
+com.threerings.getdown.data.PathBuilder: PathBuilder()
+com.threerings.getdown.data.PathBuilder: com.threerings.getdown.data.ClassPath buildClassPath(com.threerings.getdown.data.Application)
+com.threerings.getdown.data.PathBuilder: com.threerings.getdown.data.ClassPath buildDefaultClassPath(com.threerings.getdown.data.Application)
+com.threerings.getdown.data.PathBuilder: com.threerings.getdown.data.ClassPath buildCachedClassPath(com.threerings.getdown.data.Application)
+com.threerings.getdown.data.PathBuilder: com.threerings.getdown.data.ClassPath buildLibsPath(com.threerings.getdown.data.Application,boolean)
+com.threerings.getdown.data.Properties
+com.threerings.getdown.data.Properties: java.lang.String GETDOWN
+com.threerings.getdown.data.Properties: java.lang.String CONNECT_PORT
+com.threerings.getdown.data.Properties: Properties()
+com.threerings.getdown.data.Resource
+com.threerings.getdown.data.Resource: java.util.EnumSet NORMAL
+com.threerings.getdown.data.Resource: java.util.EnumSet UNPACK
+com.threerings.getdown.data.Resource: java.util.EnumSet EXEC
+com.threerings.getdown.data.Resource: java.util.EnumSet PRELOAD
+com.threerings.getdown.data.Resource: java.util.EnumSet NATIVE
+com.threerings.getdown.data.Resource: java.lang.String _path
+com.threerings.getdown.data.Resource: java.net.URL _remote
+com.threerings.getdown.data.Resource: java.io.File _local
+com.threerings.getdown.data.Resource: java.io.File _localNew
+com.threerings.getdown.data.Resource: java.io.File _marker
+com.threerings.getdown.data.Resource: java.io.File _unpacked
+com.threerings.getdown.data.Resource: java.util.EnumSet _attrs
+com.threerings.getdown.data.Resource: boolean _isJar
+com.threerings.getdown.data.Resource: boolean _isPacked200Jar
+com.threerings.getdown.data.Resource: java.util.Comparator ENTRY_COMP
+com.threerings.getdown.data.Resource: int DIGEST_BUFFER_SIZE
+com.threerings.getdown.data.Resource: java.lang.String computeDigest(int,java.io.File,java.security.MessageDigest,com.threerings.getdown.util.ProgressObserver)
+com.threerings.getdown.data.Resource: Resource(java.lang.String,java.net.URL,java.io.File,java.util.EnumSet)
+com.threerings.getdown.data.Resource: java.lang.String getPath()
+com.threerings.getdown.data.Resource: java.io.File getLocal()
+com.threerings.getdown.data.Resource: java.io.File getLocalNew()
+com.threerings.getdown.data.Resource: java.io.File getUnpacked()
+com.threerings.getdown.data.Resource: java.io.File getFinalTarget()
+com.threerings.getdown.data.Resource: java.net.URL getRemote()
+com.threerings.getdown.data.Resource: boolean shouldUnpack()
+com.threerings.getdown.data.Resource: boolean shouldPredownload()
+com.threerings.getdown.data.Resource: boolean isNative()
+com.threerings.getdown.data.Resource: java.lang.String computeDigest(int,java.security.MessageDigest,com.threerings.getdown.util.ProgressObserver)
+com.threerings.getdown.data.Resource: boolean isMarkedValid()
+com.threerings.getdown.data.Resource: void markAsValid()
+com.threerings.getdown.data.Resource: void clearMarker()
+com.threerings.getdown.data.Resource: void install(boolean)
+com.threerings.getdown.data.Resource: void unpack()
+com.threerings.getdown.data.Resource: void applyAttrs()
+com.threerings.getdown.data.Resource: void erase()
+com.threerings.getdown.data.Resource: int compareTo(com.threerings.getdown.data.Resource)
+com.threerings.getdown.data.Resource: boolean equals(java.lang.Object)
+com.threerings.getdown.data.Resource: int hashCode()
+com.threerings.getdown.data.Resource: java.lang.String toString()
+com.threerings.getdown.data.Resource: void updateProgress(com.threerings.getdown.util.ProgressObserver,long,long)
+com.threerings.getdown.data.Resource: boolean isJar(java.lang.String)
+com.threerings.getdown.data.Resource: boolean isPacked200Jar(java.lang.String)
+com.threerings.getdown.data.Resource: int compareTo(java.lang.Object)
+com.threerings.getdown.data.Resource: void <clinit>()
+com.threerings.getdown.data.Resource$Attr
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr UNPACK
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr CLEAN
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr EXEC
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr PRELOAD
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr NATIVE
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr[] $VALUES
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr[] values()
+com.threerings.getdown.data.Resource$Attr: com.threerings.getdown.data.Resource$Attr valueOf(java.lang.String)
+com.threerings.getdown.data.Resource$Attr: Resource$Attr(java.lang.String,int)
+com.threerings.getdown.data.Resource$Attr: void <clinit>()
+com.threerings.getdown.data.SysProps
+com.threerings.getdown.data.SysProps: SysProps()
+com.threerings.getdown.data.SysProps: java.lang.String appDir()
+com.threerings.getdown.data.SysProps: java.lang.String appId()
+com.threerings.getdown.data.SysProps: java.lang.String appBase()
+com.threerings.getdown.data.SysProps: boolean noLogRedir()
+com.threerings.getdown.data.SysProps: java.lang.String appbaseDomain()
+com.threerings.getdown.data.SysProps: java.lang.String appbaseOverride()
+com.threerings.getdown.data.SysProps: boolean silent()
+com.threerings.getdown.data.SysProps: boolean launchInSilent()
+com.threerings.getdown.data.SysProps: boolean noUpdate()
+com.threerings.getdown.data.SysProps: boolean noInstall()
+com.threerings.getdown.data.SysProps: int startDelay()
+com.threerings.getdown.data.SysProps: boolean noUnpack()
+com.threerings.getdown.data.SysProps: boolean direct()
+com.threerings.getdown.data.SysProps: int connectTimeout()
+com.threerings.getdown.data.SysProps: int readTimeout()
+com.threerings.getdown.data.SysProps: int threadPoolSize()
+com.threerings.getdown.data.SysProps: long parseJavaVersion(java.lang.String,java.lang.String)
+com.threerings.getdown.data.SysProps: java.lang.String overrideAppbase(java.lang.String)
+com.threerings.getdown.data.SysProps: java.lang.String replaceDomain(java.lang.String)
+com.threerings.getdown.launcher.AbortPanel
+com.threerings.getdown.launcher.AbortPanel: com.threerings.getdown.launcher.Getdown _getdown
+com.threerings.getdown.launcher.AbortPanel: java.util.ResourceBundle _msgs
+com.threerings.getdown.launcher.AbortPanel: AbortPanel(com.threerings.getdown.launcher.Getdown,java.util.ResourceBundle)
+com.threerings.getdown.launcher.AbortPanel: java.awt.Dimension getPreferredSize()
+com.threerings.getdown.launcher.AbortPanel: void actionPerformed(java.awt.event.ActionEvent)
+com.threerings.getdown.launcher.AbortPanel: java.lang.String get(java.lang.String)
+com.threerings.getdown.launcher.Getdown
+com.threerings.getdown.launcher.Getdown: com.threerings.getdown.util.ProgressObserver _progobs
+com.threerings.getdown.launcher.Getdown: com.threerings.getdown.data.Application _app
+com.threerings.getdown.launcher.Getdown: com.threerings.getdown.data.Application$UpdateInterface _ifc
+com.threerings.getdown.launcher.Getdown: java.util.ResourceBundle _msgs
+com.threerings.getdown.launcher.Getdown: java.awt.Container _container
+com.threerings.getdown.launcher.Getdown: javax.swing.JLayeredPane _layers
+com.threerings.getdown.launcher.Getdown: com.threerings.getdown.launcher.StatusPanel _status
+com.threerings.getdown.launcher.Getdown: javax.swing.JButton _patchNotes
+com.threerings.getdown.launcher.Getdown: com.threerings.getdown.launcher.AbortPanel _abort
+com.threerings.getdown.launcher.Getdown: com.threerings.getdown.launcher.RotatingBackgrounds _background
+com.threerings.getdown.launcher.Getdown: boolean _dead
+com.threerings.getdown.launcher.Getdown: boolean _silent
+com.threerings.getdown.launcher.Getdown: boolean _launchInSilent
+com.threerings.getdown.launcher.Getdown: boolean _noUpdate
+com.threerings.getdown.launcher.Getdown: long _startup
+com.threerings.getdown.launcher.Getdown: java.util.Set _toInstallResources
+com.threerings.getdown.launcher.Getdown: boolean _readyToInstall
+com.threerings.getdown.launcher.Getdown: boolean _enableTracking
+com.threerings.getdown.launcher.Getdown: int _reportedProgress
+com.threerings.getdown.launcher.Getdown: int _delay
+com.threerings.getdown.launcher.Getdown: int _stepMaxPercent
+com.threerings.getdown.launcher.Getdown: int _stepMinPercent
+com.threerings.getdown.launcher.Getdown: int _lastGlobalPercent
+com.threerings.getdown.launcher.Getdown: int _uiDisplayPercent
+com.threerings.getdown.launcher.Getdown: int MAX_LOOPS
+com.threerings.getdown.launcher.Getdown: long FALLBACK_CHECK_TIME
+com.threerings.getdown.launcher.Getdown: Getdown(com.threerings.getdown.data.EnvConfig)
+com.threerings.getdown.launcher.Getdown: boolean isUpdateAvailable()
+com.threerings.getdown.launcher.Getdown: void install()
+com.threerings.getdown.launcher.Getdown: void run()
+com.threerings.getdown.launcher.Getdown: void configProxy(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
+com.threerings.getdown.launcher.Getdown: boolean detectProxy()
+com.threerings.getdown.launcher.Getdown: void readConfig(boolean)
+com.threerings.getdown.launcher.Getdown: void doPredownloads(java.util.Collection)
+com.threerings.getdown.launcher.Getdown: void getdown()
+com.threerings.getdown.launcher.Getdown: void updateStatus(java.lang.String)
+com.threerings.getdown.launcher.Getdown: java.awt.image.BufferedImage loadImage(java.lang.String)
+com.threerings.getdown.launcher.Getdown: void updateJava()
+com.threerings.getdown.launcher.Getdown: void update()
+com.threerings.getdown.launcher.Getdown: void download(java.util.Collection)
+com.threerings.getdown.launcher.Getdown: void launch()
+com.threerings.getdown.launcher.Getdown: void createInterfaceAsync(boolean)
+com.threerings.getdown.launcher.Getdown: void initInterface()
+com.threerings.getdown.launcher.Getdown: com.threerings.getdown.launcher.RotatingBackgrounds getBackground()
+com.threerings.getdown.launcher.Getdown: java.awt.Image getProgressImage()
+com.threerings.getdown.launcher.Getdown: void handleWindowClose()
+com.threerings.getdown.launcher.Getdown: void fail(java.lang.String)
+com.threerings.getdown.launcher.Getdown: void setStep(com.threerings.getdown.data.Application$UpdateInterface$Step)
+com.threerings.getdown.launcher.Getdown: int stepToGlobalPercent(int)
+com.threerings.getdown.launcher.Getdown: void setStatusAsync(java.lang.String,int,long,boolean)
+com.threerings.getdown.launcher.Getdown: void reportTrackingEvent(java.lang.String,int)
+com.threerings.getdown.launcher.Getdown: java.awt.Container createContainer()
+com.threerings.getdown.launcher.Getdown: void configureContainer()
+com.threerings.getdown.launcher.Getdown: void showContainer()
+com.threerings.getdown.launcher.Getdown: void disposeContainer()
+com.threerings.getdown.launcher.Getdown: boolean invokeDirect()
+com.threerings.getdown.launcher.Getdown: void showDocument(java.lang.String)
+com.threerings.getdown.launcher.Getdown: void exit(int)
+com.threerings.getdown.launcher.Getdown: void copyStream(java.io.InputStream,java.io.PrintStream)
+com.threerings.getdown.launcher.Getdown: java.awt.Image loadImage(java.lang.String)
+com.threerings.getdown.launcher.Getdown$ProgressReporter
+com.threerings.getdown.launcher.Getdown$ProgressReporter: java.net.URL _url
+com.threerings.getdown.launcher.Getdown$ProgressReporter: com.threerings.getdown.launcher.Getdown this$0
+com.threerings.getdown.launcher.Getdown$ProgressReporter: Getdown$ProgressReporter(com.threerings.getdown.launcher.Getdown,java.net.URL)
+com.threerings.getdown.launcher.Getdown$ProgressReporter: void run()
+com.threerings.getdown.launcher.GetdownApp
+com.threerings.getdown.launcher.GetdownApp: GetdownApp()
+com.threerings.getdown.launcher.GetdownApp: void main(java.lang.String[])
+com.threerings.getdown.launcher.GetdownApp: com.threerings.getdown.launcher.Getdown start(java.lang.String[])
+com.threerings.getdown.launcher.MultipleGetdownRunning
+com.threerings.getdown.launcher.MultipleGetdownRunning: MultipleGetdownRunning()
+com.threerings.getdown.launcher.ProxyPanel
+com.threerings.getdown.launcher.ProxyPanel: com.threerings.getdown.launcher.Getdown _getdown
+com.threerings.getdown.launcher.ProxyPanel: java.util.ResourceBundle _msgs
+com.threerings.getdown.launcher.ProxyPanel: javax.swing.JTextField _host
+com.threerings.getdown.launcher.ProxyPanel: javax.swing.JTextField _port
+com.threerings.getdown.launcher.ProxyPanel: javax.swing.JCheckBox _useAuth
+com.threerings.getdown.launcher.ProxyPanel: javax.swing.JTextField _username
+com.threerings.getdown.launcher.ProxyPanel: javax.swing.JPasswordField _password
+com.threerings.getdown.launcher.ProxyPanel: ProxyPanel(com.threerings.getdown.launcher.Getdown,java.util.ResourceBundle)
+com.threerings.getdown.launcher.ProxyPanel: void setProxy(java.lang.String,java.lang.String)
+com.threerings.getdown.launcher.ProxyPanel: void addNotify()
+com.threerings.getdown.launcher.ProxyPanel: java.awt.Dimension getPreferredSize()
+com.threerings.getdown.launcher.ProxyPanel: void actionPerformed(java.awt.event.ActionEvent)
+com.threerings.getdown.launcher.ProxyPanel: java.lang.String get(java.lang.String)
+com.threerings.getdown.launcher.ProxyPanel: java.awt.Dimension clampWidth(java.awt.Dimension,int)
+com.threerings.getdown.launcher.ProxyPanel$SaneLabelField
+com.threerings.getdown.launcher.ProxyPanel$SaneLabelField: ProxyPanel$SaneLabelField(java.lang.String)
+com.threerings.getdown.launcher.ProxyPanel$SaneLabelField: java.awt.Dimension getPreferredSize()
+com.threerings.getdown.launcher.ProxyPanel$SanePasswordField
+com.threerings.getdown.launcher.ProxyPanel$SanePasswordField: ProxyPanel$SanePasswordField()
+com.threerings.getdown.launcher.ProxyPanel$SanePasswordField: java.awt.Dimension getPreferredSize()
+com.threerings.getdown.launcher.ProxyPanel$SaneTextField
+com.threerings.getdown.launcher.ProxyPanel$SaneTextField: ProxyPanel$SaneTextField()
+com.threerings.getdown.launcher.ProxyPanel$SaneTextField: java.awt.Dimension getPreferredSize()
+com.threerings.getdown.launcher.ProxyUtil
+com.threerings.getdown.launcher.ProxyUtil: java.lang.String PROXY_REGISTRY
+com.threerings.getdown.launcher.ProxyUtil: ProxyUtil()
+com.threerings.getdown.launcher.ProxyUtil: boolean autoDetectProxy(com.threerings.getdown.data.Application)
+com.threerings.getdown.launcher.ProxyUtil: boolean canLoadWithoutProxy(java.net.URL)
+com.threerings.getdown.launcher.ProxyUtil: void configProxy(com.threerings.getdown.data.Application,java.lang.String,java.lang.String,java.lang.String,java.lang.String)
+com.threerings.getdown.launcher.ProxyUtil: java.lang.String[] loadProxy(com.threerings.getdown.data.Application)
+com.threerings.getdown.launcher.ProxyUtil: void saveProxy(com.threerings.getdown.data.Application,java.lang.String,java.lang.String)
+com.threerings.getdown.launcher.ProxyUtil: void initProxy(com.threerings.getdown.data.Application,java.lang.String,java.lang.String,java.lang.String,java.lang.String)
+com.threerings.getdown.launcher.RotatingBackgrounds
+com.threerings.getdown.launcher.RotatingBackgrounds: long currentDisplayStart
+com.threerings.getdown.launcher.RotatingBackgrounds: int current
+com.threerings.getdown.launcher.RotatingBackgrounds: java.awt.Image[] images
+com.threerings.getdown.launcher.RotatingBackgrounds: java.awt.Image errorImage
+com.threerings.getdown.launcher.RotatingBackgrounds: int[] percentages
+com.threerings.getdown.launcher.RotatingBackgrounds: int[] minDisplayTime
+com.threerings.getdown.launcher.RotatingBackgrounds: RotatingBackgrounds()
+com.threerings.getdown.launcher.RotatingBackgrounds: RotatingBackgrounds(java.awt.Image)
+com.threerings.getdown.launcher.RotatingBackgrounds: RotatingBackgrounds(java.util.List,java.lang.String,com.threerings.getdown.launcher.RotatingBackgrounds$ImageLoader)
+com.threerings.getdown.launcher.RotatingBackgrounds: java.awt.Image getImage(int)
+com.threerings.getdown.launcher.RotatingBackgrounds: java.awt.Image getErrorImage()
+com.threerings.getdown.launcher.RotatingBackgrounds: int getNumImages()
+com.threerings.getdown.launcher.RotatingBackgrounds: void makeEmpty()
+com.threerings.getdown.launcher.RotatingBackgrounds$ImageLoader
+com.threerings.getdown.launcher.RotatingBackgrounds$ImageLoader: java.awt.Image loadImage(java.lang.String)
+com.threerings.getdown.launcher.StatusPanel
+com.threerings.getdown.launcher.StatusPanel: java.awt.Image _barimg
+com.threerings.getdown.launcher.StatusPanel: com.threerings.getdown.launcher.RotatingBackgrounds _bg
+com.threerings.getdown.launcher.StatusPanel: java.awt.Dimension _psize
+com.threerings.getdown.launcher.StatusPanel: java.util.ResourceBundle _msgs
+com.threerings.getdown.launcher.StatusPanel: int _progress
+com.threerings.getdown.launcher.StatusPanel: java.lang.String _status
+com.threerings.getdown.launcher.StatusPanel: int _statusDots
+com.threerings.getdown.launcher.StatusPanel: boolean _displayError
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.swing.Label _label
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.swing.Label _newlab
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.swing.Label _plabel
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.swing.Label _newplab
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.swing.Label _rlabel
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.swing.Label _newrlab
+com.threerings.getdown.launcher.StatusPanel: com.threerings.getdown.data.Application$UpdateInterface _ifc
+com.threerings.getdown.launcher.StatusPanel: javax.swing.Timer _timer
+com.threerings.getdown.launcher.StatusPanel: long[] _remain
+com.threerings.getdown.launcher.StatusPanel: int _ridx
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.util.Throttle _rthrottle
+com.threerings.getdown.launcher.StatusPanel: java.awt.Font FONT
+com.threerings.getdown.launcher.StatusPanel: StatusPanel(java.util.ResourceBundle)
+com.threerings.getdown.launcher.StatusPanel: void init(com.threerings.getdown.data.Application$UpdateInterface,com.threerings.getdown.launcher.RotatingBackgrounds,java.awt.Image)
+com.threerings.getdown.launcher.StatusPanel: boolean imageUpdate(java.awt.Image,int,int,int,int,int)
+com.threerings.getdown.launcher.StatusPanel: void setProgress(int,long)
+com.threerings.getdown.launcher.StatusPanel: void setStatus(java.lang.String,boolean)
+com.threerings.getdown.launcher.StatusPanel: void stopThrob()
+com.threerings.getdown.launcher.StatusPanel: void addNotify()
+com.threerings.getdown.launcher.StatusPanel: void removeNotify()
+com.threerings.getdown.launcher.StatusPanel: void paintComponent(java.awt.Graphics)
+com.threerings.getdown.launcher.StatusPanel: java.awt.Dimension getPreferredSize()
+com.threerings.getdown.launcher.StatusPanel: void updateStatusLabel()
+com.threerings.getdown.launcher.StatusPanel: int getStatusY(com.samskivert.swing.Label)
+com.threerings.getdown.launcher.StatusPanel: com.samskivert.swing.Label createLabel(java.lang.String,java.awt.Color)
+com.threerings.getdown.launcher.StatusPanel: java.lang.String xlate(java.lang.String)
+com.threerings.getdown.launcher.StatusPanel: java.lang.String get(java.lang.String,java.lang.String[])
+com.threerings.getdown.launcher.StatusPanel: java.lang.String get(java.lang.String)
+com.threerings.getdown.launcher.StatusPanel: void <clinit>()
+com.threerings.getdown.net.Downloader
+com.threerings.getdown.net.Downloader: java.util.Map _sizes
+com.threerings.getdown.net.Downloader: java.util.Map _downloaded
+com.threerings.getdown.net.Downloader: long _start
+com.threerings.getdown.net.Downloader: long _bytesPerSecond
+com.threerings.getdown.net.Downloader: long _lastUpdate
+com.threerings.getdown.net.Downloader: com.threerings.getdown.net.Downloader$State _state
+com.threerings.getdown.net.Downloader: long UPDATE_DELAY
+com.threerings.getdown.net.Downloader: Downloader()
+com.threerings.getdown.net.Downloader: boolean download(java.util.Collection,int)
+com.threerings.getdown.net.Downloader: void abort()
+com.threerings.getdown.net.Downloader: void resolvingDownloads()
+com.threerings.getdown.net.Downloader: void downloadProgress(int,long)
+com.threerings.getdown.net.Downloader: void downloadFailed(com.threerings.getdown.data.Resource,java.lang.Exception)
+com.threerings.getdown.net.Downloader: long checkSize(com.threerings.getdown.data.Resource)
+com.threerings.getdown.net.Downloader: void reportProgress(com.threerings.getdown.data.Resource,long,long)
+com.threerings.getdown.net.Downloader: long sum(java.lang.Iterable)
+com.threerings.getdown.net.Downloader: void download(com.threerings.getdown.data.Resource)
+com.threerings.getdown.net.Downloader$State
+com.threerings.getdown.net.Downloader$State: com.threerings.getdown.net.Downloader$State DOWNLOADING
+com.threerings.getdown.net.Downloader$State: com.threerings.getdown.net.Downloader$State COMPLETE
+com.threerings.getdown.net.Downloader$State: com.threerings.getdown.net.Downloader$State FAILED
+com.threerings.getdown.net.Downloader$State: com.threerings.getdown.net.Downloader$State ABORTED
+com.threerings.getdown.net.Downloader$State: com.threerings.getdown.net.Downloader$State[] $VALUES
+com.threerings.getdown.net.Downloader$State: com.threerings.getdown.net.Downloader$State[] values()
+com.threerings.getdown.net.Downloader$State: com.threerings.getdown.net.Downloader$State valueOf(java.lang.String)
+com.threerings.getdown.net.Downloader$State: Downloader$State(java.lang.String,int)
+com.threerings.getdown.net.Downloader$State: void <clinit>()
+com.threerings.getdown.net.HTTPDownloader
+com.threerings.getdown.net.HTTPDownloader: java.net.Proxy _proxy
+com.threerings.getdown.net.HTTPDownloader: HTTPDownloader(java.net.Proxy)
+com.threerings.getdown.net.HTTPDownloader: long checkSize(com.threerings.getdown.data.Resource)
+com.threerings.getdown.net.HTTPDownloader: void download(com.threerings.getdown.data.Resource)
+com.threerings.getdown.spi.ProxyAuth
+com.threerings.getdown.spi.ProxyAuth: com.threerings.getdown.spi.ProxyAuth$Credentials loadCredentials(java.lang.String)
+com.threerings.getdown.spi.ProxyAuth: void saveCredentials(java.lang.String,java.lang.String,java.lang.String)
+com.threerings.getdown.spi.ProxyAuth$Credentials
+com.threerings.getdown.spi.ProxyAuth$Credentials: java.lang.String username
+com.threerings.getdown.spi.ProxyAuth$Credentials: java.lang.String password
+com.threerings.getdown.spi.ProxyAuth$Credentials: ProxyAuth$Credentials(java.lang.String,java.lang.String)
+com.threerings.getdown.tools.Differ
+com.threerings.getdown.tools.Differ: Differ()
+com.threerings.getdown.tools.Differ: void createDiff(java.io.File,java.io.File,boolean)
+com.threerings.getdown.tools.Differ: void createPatch(java.io.File,java.util.ArrayList,java.util.ArrayList,boolean)
+com.threerings.getdown.tools.Differ: java.io.File rebuildJar(java.io.File)
+com.threerings.getdown.tools.Differ: void jarDiff(java.io.File,java.io.File,java.util.jar.JarOutputStream)
+com.threerings.getdown.tools.Differ: void main(java.lang.String[])
+com.threerings.getdown.tools.Differ: void pipe(java.io.File,java.util.jar.JarOutputStream)
+com.threerings.getdown.tools.Digester
+com.threerings.getdown.tools.Digester: Digester()
+com.threerings.getdown.tools.Digester: void main(java.lang.String[])
+com.threerings.getdown.tools.Digester: void createDigests(java.io.File,java.io.File,java.lang.String,java.lang.String)
+com.threerings.getdown.tools.Digester: void createDigest(int,java.io.File)
+com.threerings.getdown.tools.Digester: void signDigest(int,java.io.File,java.io.File,java.lang.String,java.lang.String)
+com.threerings.getdown.tools.JarDiff
+com.threerings.getdown.tools.JarDiff: int DEFAULT_READ_SIZE
+com.threerings.getdown.tools.JarDiff: byte[] newBytes
+com.threerings.getdown.tools.JarDiff: byte[] oldBytes
+com.threerings.getdown.tools.JarDiff: boolean _debug
+com.threerings.getdown.tools.JarDiff: JarDiff()
+com.threerings.getdown.tools.JarDiff: void createPatch(java.lang.String,java.lang.String,java.io.OutputStream,boolean)
+com.threerings.getdown.tools.JarDiff: void createIndex(java.util.jar.JarOutputStream,java.util.List,java.util.Map)
+com.threerings.getdown.tools.JarDiff: java.io.Writer writeEscapedString(java.io.Writer,java.lang.String)
+com.threerings.getdown.tools.JarDiff: void writeEntry(java.util.jar.JarOutputStream,java.util.jar.JarEntry,com.threerings.getdown.tools.JarDiff$JarFile2)
+com.threerings.getdown.tools.JarDiff: byte[] access$000()
+com.threerings.getdown.tools.JarDiff: byte[] access$100()
+com.threerings.getdown.tools.JarDiff: boolean access$200()
+com.threerings.getdown.tools.JarDiff: void <clinit>()
+com.threerings.getdown.tools.JarDiffCodes
+com.threerings.getdown.tools.JarDiffCodes: java.lang.String INDEX_NAME
+com.threerings.getdown.tools.JarDiffCodes: java.lang.String VERSION_HEADER
+com.threerings.getdown.tools.JarDiffCodes: java.lang.String REMOVE_COMMAND
+com.threerings.getdown.tools.JarDiffCodes: java.lang.String MOVE_COMMAND
+com.threerings.getdown.tools.JarDiffPatcher
+com.threerings.getdown.tools.JarDiffPatcher: int DEFAULT_READ_SIZE
+com.threerings.getdown.tools.JarDiffPatcher: byte[] newBytes
+com.threerings.getdown.tools.JarDiffPatcher: byte[] oldBytes
+com.threerings.getdown.tools.JarDiffPatcher: JarDiffPatcher()
+com.threerings.getdown.tools.JarDiffPatcher: void patchJar(java.lang.String,java.lang.String,java.io.File,com.threerings.getdown.util.ProgressObserver)
+com.threerings.getdown.tools.JarDiffPatcher: void updateObserver(com.threerings.getdown.util.ProgressObserver,double,double)
+com.threerings.getdown.tools.JarDiffPatcher: void determineNameMapping(java.util.jar.JarFile,java.util.Set,java.util.Map)
+com.threerings.getdown.tools.JarDiffPatcher: java.util.List getSubpaths(java.lang.String)
+com.threerings.getdown.tools.JarDiffPatcher: void writeEntry(java.util.jar.JarOutputStream,java.util.jar.JarEntry,java.util.jar.JarFile)
+com.threerings.getdown.tools.JarDiffPatcher: void writeEntry(java.util.jar.JarOutputStream,java.util.jar.JarEntry,java.io.InputStream)
+com.threerings.getdown.tools.JarDiffPatcher: void <clinit>()
+com.threerings.getdown.tools.Patcher
+com.threerings.getdown.tools.Patcher: java.lang.String CREATE
+com.threerings.getdown.tools.Patcher: java.lang.String PATCH
+com.threerings.getdown.tools.Patcher: java.lang.String DELETE
+com.threerings.getdown.tools.Patcher: com.threerings.getdown.util.ProgressObserver _obs
+com.threerings.getdown.tools.Patcher: long _complete
+com.threerings.getdown.tools.Patcher: long _plength
+com.threerings.getdown.tools.Patcher: byte[] _buffer
+com.threerings.getdown.tools.Patcher: int COPY_BUFFER_SIZE
+com.threerings.getdown.tools.Patcher: Patcher()
+com.threerings.getdown.tools.Patcher: void patch(java.io.File,java.io.File,com.threerings.getdown.util.ProgressObserver)
+com.threerings.getdown.tools.Patcher: java.lang.String strip(java.lang.String,java.lang.String)
+com.threerings.getdown.tools.Patcher: void createFile(java.util.jar.JarFile,java.util.zip.ZipEntry,java.io.File)
+com.threerings.getdown.tools.Patcher: void patchFile(java.util.jar.JarFile,java.util.zip.ZipEntry,java.io.File,java.lang.String)
+com.threerings.getdown.tools.Patcher: void updateProgress(int)
+com.threerings.getdown.tools.Patcher: void main(java.lang.String[])
+com.threerings.getdown.util.Base64
+com.threerings.getdown.util.Base64: int DEFAULT
+com.threerings.getdown.util.Base64: int NO_PADDING
+com.threerings.getdown.util.Base64: int NO_WRAP
+com.threerings.getdown.util.Base64: int CRLF
+com.threerings.getdown.util.Base64: int URL_SAFE
+com.threerings.getdown.util.Base64: int NO_CLOSE
+com.threerings.getdown.util.Base64: boolean $assertionsDisabled
+com.threerings.getdown.util.Base64: byte[] decode(java.lang.String,int)
+com.threerings.getdown.util.Base64: byte[] decode(byte[],int)
+com.threerings.getdown.util.Base64: byte[] decode(byte[],int,int,int)
+com.threerings.getdown.util.Base64: java.lang.String encodeToString(byte[],int)
+com.threerings.getdown.util.Base64: java.lang.String encodeToString(byte[],int,int,int)
+com.threerings.getdown.util.Base64: byte[] encode(byte[],int)
+com.threerings.getdown.util.Base64: byte[] encode(byte[],int,int,int)
+com.threerings.getdown.util.Base64: Base64()
+com.threerings.getdown.util.Base64: void <clinit>()
+com.threerings.getdown.util.Color
+com.threerings.getdown.util.Color: int CLEAR
+com.threerings.getdown.util.Color: int WHITE
+com.threerings.getdown.util.Color: int BLACK
+com.threerings.getdown.util.Color: float brightness(int)
+com.threerings.getdown.util.Color: Color()
+com.threerings.getdown.util.Config
+com.threerings.getdown.util.Config: com.threerings.getdown.util.Config EMPTY
+com.threerings.getdown.util.Config: java.util.Map _data
+com.threerings.getdown.util.Config: com.threerings.getdown.util.Config$ParseOpts createOpts(boolean)
+com.threerings.getdown.util.Config: java.util.List parsePairs(java.io.File,com.threerings.getdown.util.Config$ParseOpts)
+com.threerings.getdown.util.Config: java.util.List parsePairs(java.io.Reader,com.threerings.getdown.util.Config$ParseOpts)
+com.threerings.getdown.util.Config: com.threerings.getdown.util.Rectangle parseRect(java.lang.String,java.lang.String)
+com.threerings.getdown.util.Config: java.lang.Integer parseColor(java.lang.String)
+com.threerings.getdown.util.Config: com.threerings.getdown.util.Config parseConfig(java.io.File,com.threerings.getdown.util.Config$ParseOpts)
+com.threerings.getdown.util.Config: Config(java.util.Map)
+com.threerings.getdown.util.Config: boolean hasValue(java.lang.String)
+com.threerings.getdown.util.Config: java.lang.Object getRaw(java.lang.String)
+com.threerings.getdown.util.Config: java.lang.String getString(java.lang.String)
+com.threerings.getdown.util.Config: java.lang.String getString(java.lang.String,java.lang.String)
+com.threerings.getdown.util.Config: boolean getBoolean(java.lang.String)
+com.threerings.getdown.util.Config: java.lang.String[] getMultiValue(java.lang.String)
+com.threerings.getdown.util.Config: com.threerings.getdown.util.Rectangle getRect(java.lang.String,com.threerings.getdown.util.Rectangle)
+com.threerings.getdown.util.Config: int getInt(java.lang.String,int)
+com.threerings.getdown.util.Config: long getLong(java.lang.String,long)
+com.threerings.getdown.util.Config: int getColor(java.lang.String,int)
+com.threerings.getdown.util.Config: java.lang.String[] getList(java.lang.String)
+com.threerings.getdown.util.Config: java.lang.String getUrl(java.lang.String,java.lang.String)
+com.threerings.getdown.util.Config: boolean checkQualifiers(java.lang.String,java.lang.String,java.lang.String)
+com.threerings.getdown.util.Config: boolean checkQualifier(java.lang.String,java.lang.String,java.lang.String)
+com.threerings.getdown.util.Config: void <clinit>()
+com.threerings.getdown.util.Config$ParseOpts
+com.threerings.getdown.util.Config$ParseOpts: boolean biasToKey
+com.threerings.getdown.util.Config$ParseOpts: boolean strictComments
+com.threerings.getdown.util.Config$ParseOpts: java.lang.String osname
+com.threerings.getdown.util.Config$ParseOpts: java.lang.String osarch
+com.threerings.getdown.util.Config$ParseOpts: Config$ParseOpts()
+com.threerings.getdown.util.ConnectionUtil
+com.threerings.getdown.util.ConnectionUtil: ConnectionUtil()
+com.threerings.getdown.util.ConnectionUtil: java.net.URLConnection open(java.net.Proxy,java.net.URL,int,int)
+com.threerings.getdown.util.ConnectionUtil: java.net.HttpURLConnection openHttp(java.net.Proxy,java.net.URL,int,int)
+com.threerings.getdown.util.FileUtil
+com.threerings.getdown.util.FileUtil: FileUtil()
+com.threerings.getdown.util.FileUtil: boolean renameTo(java.io.File,java.io.File)
+com.threerings.getdown.util.FileUtil: boolean deleteHarder(java.io.File)
+com.threerings.getdown.util.FileUtil: boolean deleteDirHarder(java.io.File)
+com.threerings.getdown.util.FileUtil: java.util.List readLines(java.io.Reader)
+com.threerings.getdown.util.FileUtil: void unpackJar(java.util.jar.JarFile,java.io.File,boolean)
+com.threerings.getdown.util.FileUtil: void unpackPacked200Jar(java.io.File,java.io.File)
+com.threerings.getdown.util.FileUtil: void copy(java.io.File,java.io.File)
+com.threerings.getdown.util.FileUtil: void makeExecutable(java.io.File)
+com.threerings.getdown.util.FileUtil: void walkTree(java.io.File,com.threerings.getdown.util.FileUtil$Visitor)
+com.threerings.getdown.util.FileUtil$Visitor
+com.threerings.getdown.util.FileUtil$Visitor: void visit(java.io.File)
+com.threerings.getdown.util.HostWhitelist
+com.threerings.getdown.util.HostWhitelist: HostWhitelist()
+com.threerings.getdown.util.HostWhitelist: java.net.URL verify(java.net.URL)
+com.threerings.getdown.util.HostWhitelist: java.net.URL verify(java.util.List,java.net.URL)
+com.threerings.getdown.util.LaunchUtil
+com.threerings.getdown.util.LaunchUtil: java.lang.String LOCAL_JAVA_DIR
+com.threerings.getdown.util.LaunchUtil: boolean _isWindows
+com.threerings.getdown.util.LaunchUtil: boolean _isMacOS
+com.threerings.getdown.util.LaunchUtil: boolean _isLinux
+com.threerings.getdown.util.LaunchUtil: LaunchUtil()
+com.threerings.getdown.util.LaunchUtil: boolean updateVersionAndRelaunch(java.io.File,java.lang.String,java.lang.String)
+com.threerings.getdown.util.LaunchUtil: java.lang.String getJVMPath(java.io.File)
+com.threerings.getdown.util.LaunchUtil: java.lang.String getJVMPath(java.io.File,boolean)
+com.threerings.getdown.util.LaunchUtil: void upgradeGetdown(java.io.File,java.io.File,java.io.File)
+com.threerings.getdown.util.LaunchUtil: boolean mustMonitorChildren()
+com.threerings.getdown.util.LaunchUtil: boolean isWindows()
+com.threerings.getdown.util.LaunchUtil: boolean isMacOS()
+com.threerings.getdown.util.LaunchUtil: boolean isLinux()
+com.threerings.getdown.util.LaunchUtil: java.lang.String checkJVMPath(java.lang.String,boolean)
+com.threerings.getdown.util.LaunchUtil: void <clinit>()
+com.threerings.getdown.util.MessageUtil
+com.threerings.getdown.util.MessageUtil: java.lang.String TAINT_CHAR
+com.threerings.getdown.util.MessageUtil: MessageUtil()
+com.threerings.getdown.util.MessageUtil: boolean isTainted(java.lang.String)
+com.threerings.getdown.util.MessageUtil: java.lang.String taint(java.lang.Object)
+com.threerings.getdown.util.MessageUtil: java.lang.String untaint(java.lang.String)
+com.threerings.getdown.util.MessageUtil: java.lang.String compose(java.lang.String,java.lang.Object[])
+com.threerings.getdown.util.MessageUtil: java.lang.String compose(java.lang.String,java.lang.String[])
+com.threerings.getdown.util.MessageUtil: java.lang.String tcompose(java.lang.String,java.lang.Object[])
+com.threerings.getdown.util.MessageUtil: java.lang.String tcompose(java.lang.String,java.lang.String[])
+com.threerings.getdown.util.MessageUtil: java.lang.String escape(java.lang.String)
+com.threerings.getdown.util.MessageUtil: java.lang.String unescape(java.lang.String)
+com.threerings.getdown.util.ProgressAggregator
+com.threerings.getdown.util.ProgressAggregator: com.threerings.getdown.util.ProgressObserver _target
+com.threerings.getdown.util.ProgressAggregator: long[] _sizes
+com.threerings.getdown.util.ProgressAggregator: int[] _progress
+com.threerings.getdown.util.ProgressAggregator: ProgressAggregator(com.threerings.getdown.util.ProgressObserver,long[])
+com.threerings.getdown.util.ProgressAggregator: com.threerings.getdown.util.ProgressObserver startElement(int)
+com.threerings.getdown.util.ProgressAggregator: void updateAggProgress()
+com.threerings.getdown.util.ProgressAggregator: long sum(long[])
+com.threerings.getdown.util.ProgressObserver
+com.threerings.getdown.util.ProgressObserver: void progress(int)
+com.threerings.getdown.util.Rectangle
+com.threerings.getdown.util.Rectangle: int x
+com.threerings.getdown.util.Rectangle: int y
+com.threerings.getdown.util.Rectangle: int width
+com.threerings.getdown.util.Rectangle: int height
+com.threerings.getdown.util.Rectangle: Rectangle(int,int,int,int)
+com.threerings.getdown.util.Rectangle: com.threerings.getdown.util.Rectangle union(com.threerings.getdown.util.Rectangle)
+com.threerings.getdown.util.Rectangle: java.lang.String toString()
+com.threerings.getdown.util.StreamUtil
+com.threerings.getdown.util.StreamUtil: StreamUtil()
+com.threerings.getdown.util.StreamUtil: void close(java.io.InputStream)
+com.threerings.getdown.util.StreamUtil: void close(java.io.OutputStream)
+com.threerings.getdown.util.StreamUtil: void close(java.io.Reader)
+com.threerings.getdown.util.StreamUtil: void close(java.io.Writer)
+com.threerings.getdown.util.StreamUtil: java.io.OutputStream copy(java.io.InputStream,java.io.OutputStream)
+com.threerings.getdown.util.StreamUtil: byte[] toByteArray(java.io.InputStream)
+com.threerings.getdown.util.StringUtil
+com.threerings.getdown.util.StringUtil: java.lang.String XLATE
+com.threerings.getdown.util.StringUtil: StringUtil()
+com.threerings.getdown.util.StringUtil: boolean couldBeValidUrl(java.lang.String)
+com.threerings.getdown.util.StringUtil: boolean isBlank(java.lang.String)
+com.threerings.getdown.util.StringUtil: int[] parseIntArray(java.lang.String)
+com.threerings.getdown.util.StringUtil: java.lang.String[] parseStringArray(java.lang.String)
+com.threerings.getdown.util.StringUtil: java.lang.String[] parseStringArray(java.lang.String,boolean)
+com.threerings.getdown.util.StringUtil: java.lang.String deNull(java.lang.String)
+com.threerings.getdown.util.StringUtil: java.lang.String hexlate(byte[],int)
+com.threerings.getdown.util.StringUtil: java.lang.String hexlate(byte[])
+com.threerings.getdown.util.StringUtil: java.lang.String join(java.lang.Object[])
+com.threerings.getdown.util.StringUtil: java.lang.String join(java.lang.Object[],boolean)
+com.threerings.getdown.util.StringUtil: java.lang.String join(java.lang.Object[],java.lang.String)
+com.threerings.getdown.util.StringUtil: java.lang.String join(java.lang.Object[],java.lang.String,boolean)
+com.threerings.getdown.util.VersionUtil
+com.threerings.getdown.util.VersionUtil: VersionUtil()
+com.threerings.getdown.util.VersionUtil: long readVersion(java.io.File)
+com.threerings.getdown.util.VersionUtil: void writeVersion(java.io.File,long)
+com.threerings.getdown.util.VersionUtil: long parseJavaVersion(java.lang.String,java.lang.String)
+com.threerings.getdown.util.VersionUtil: long readReleaseVersion(java.io.File,java.lang.String)
+com.threerings.getdown.util.VersionUtil: int parseInt(java.lang.String)
diff --git a/getdown/src/getdown/lib/SOURCE_HEADER b/getdown/src/getdown/lib/SOURCE_HEADER
new file mode 100644 (file)
index 0000000..43271fe
--- /dev/null
@@ -0,0 +1,5 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
diff --git a/getdown/src/getdown/lib/jRegistryKey.dll b/getdown/src/getdown/lib/jRegistryKey.dll
new file mode 100644 (file)
index 0000000..5746728
Binary files /dev/null and b/getdown/src/getdown/lib/jRegistryKey.dll differ
diff --git a/getdown/src/getdown/lib/jregistrykey/jregistrykey/1.0/jregistrykey-1.0.jar b/getdown/src/getdown/lib/jregistrykey/jregistrykey/1.0/jregistrykey-1.0.jar
new file mode 100644 (file)
index 0000000..5100795
Binary files /dev/null and b/getdown/src/getdown/lib/jregistrykey/jregistrykey/1.0/jregistrykey-1.0.jar differ
diff --git a/getdown/src/getdown/lib/jregistrykey/jregistrykey/1.0/jregistrykey-1.0.pom b/getdown/src/getdown/lib/jregistrykey/jregistrykey/1.0/jregistrykey-1.0.pom
new file mode 100644 (file)
index 0000000..226a7d7
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>jregistrykey</groupId>
+  <artifactId>jregistrykey</artifactId>
+  <version>1.0</version>
+  <description>POM was created from install:install-file</description>
+</project>
diff --git a/getdown/src/getdown/lib/jregistrykey/jregistrykey/maven-metadata-local.xml b/getdown/src/getdown/lib/jregistrykey/jregistrykey/maven-metadata-local.xml
new file mode 100644 (file)
index 0000000..1a8a725
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metadata>
+  <groupId>jregistrykey</groupId>
+  <artifactId>jregistrykey</artifactId>
+  <version>1.0</version>
+  <versioning>
+    <versions>
+      <version>1.0</version>
+    </versions>
+    <lastUpdated>20101118155146</lastUpdated>
+  </versioning>
+</metadata>
diff --git a/getdown/src/getdown/lib/manifest.mf b/getdown/src/getdown/lib/manifest.mf
new file mode 100644 (file)
index 0000000..3be50cc
--- /dev/null
@@ -0,0 +1,6 @@
+Main-Class: com.threerings.getdown.launcher.Getdown
+Permissions: all-permissions
+Application-Name: Getdown
+Codebase: *
+Application-Library-Allowable-Codebase: *
+Caller-Allowable-Codebase: *
diff --git a/getdown/src/getdown/pom.xml b/getdown/src/getdown/pom.xml
new file mode 100644 (file)
index 0000000..c6af03d
--- /dev/null
@@ -0,0 +1,180 @@
+<?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>org.sonatype.oss</groupId>
+    <artifactId>oss-parent</artifactId>
+    <version>7</version>
+  </parent>
+
+  <groupId>com.threerings.getdown</groupId>
+  <artifactId>getdown</artifactId>
+  <packaging>pom</packaging>
+  <version>1.8.3-SNAPSHOT</version>
+
+  <name>getdown</name>
+  <description>An application installer and updater.</description>
+  <url>https://github.com/threerings/getdown</url>
+  <issueManagement>
+    <url>https://github.com/threerings/getdown/issues</url>
+  </issueManagement>
+
+  <licenses>
+    <license>
+      <name>The (New) BSD License</name>
+      <url>http://www.opensource.org/licenses/bsd-license.php</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <developers>
+    <developer>
+      <id>samskivert</id>
+      <name>Michael Bayne</name>
+      <email>mdb@samskivert.com</email>
+    </developer>
+  </developers>
+
+  <scm>
+    <connection>scm:git:git://github.com/threerings/getdown.git</connection>
+    <developerConnection>scm:git:git@github.com:threerings/getdown.git</developerConnection>
+    <url>https://github.com/threerings/getdown</url>
+  </scm>
+
+  <modules>
+    <module>core</module>
+    <module>launcher</module>
+    <module>ant</module>
+  </modules>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.sonatype.plugins</groupId>
+        <artifactId>nexus-staging-maven-plugin</artifactId>
+        <version>1.6.8</version>
+        <extensions>true</extensions>
+        <inherited>false</inherited>
+        <configuration>
+          <serverId>ossrh-releases</serverId>
+          <nexusUrl>https://oss.sonatype.org/</nexusUrl>
+          <stagingProfileId>aa555c46fc37d0</stagingProfileId>
+        </configuration>
+      </plugin>
+    </plugins>
+
+    <!-- Common plugin configuration for all children -->
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.7.0</version>
+          <configuration>
+            <source>1.7</source>
+            <target>1.7</target>
+            <fork>true</fork>
+            <showDeprecation>true</showDeprecation>
+            <showWarnings>true</showWarnings>
+            <compilerArgs>
+              <arg>-Xlint</arg>
+              <arg>-Xlint:-serial</arg>
+              <arg>-Xlint:-path</arg>
+            </compilerArgs>
+          </configuration>
+        </plugin>
+
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-resources-plugin</artifactId>
+          <version>3.0.2</version>
+          <configuration>
+            <encoding>UTF-8</encoding>
+          </configuration>
+        </plugin>
+
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>3.0.0-M1</version>
+          <configuration>
+            <quiet>true</quiet>
+            <show>public</show>
+            <additionalparam>-Xdoclint:all -Xdoclint:-missing</additionalparam>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>eclipse</id>
+      <activation>
+        <property>
+          <name>m2e.version</name>
+        </property>
+      </activation>
+      <build>
+        <pluginManagement>
+          <plugins>
+            <plugin>
+              <!-- Tell m2eclipse to ignore the enforcer plugin from our parent. Otherwise it warns
+                   about not being able to run it. -->
+              <groupId>org.eclipse.m2e</groupId>
+              <artifactId>lifecycle-mapping</artifactId>
+              <version>1.0.0</version>
+              <configuration>
+                <lifecycleMappingMetadata>
+                  <pluginExecutions>
+                    <pluginExecution>
+                      <pluginExecutionFilter>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-enforcer-plugin</artifactId>
+                        <versionRange>[1.0,)</versionRange>
+                        <goals>
+                          <goal>enforce</goal>
+                        </goals>
+                      </pluginExecutionFilter>
+                      <action>
+                        <ignore />
+                      </action>
+                    </pluginExecution>
+                  </pluginExecutions>
+                </lifecycleMappingMetadata>
+              </configuration>
+            </plugin>
+          </plugins>
+        </pluginManagement>
+      </build>
+    </profile>
+
+    <profile>
+      <id>release-sign-artifacts</id>
+      <activation>
+        <property><name>performRelease</name><value>true</value></property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+            <version>1.6</version>
+            <executions>
+              <execution>
+                <id>sign-artifacts</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>sign</goal>
+                </goals>
+              </execution>
+            </executions>
+            <configuration>
+              <keyname>mdb@samskivert.com</keyname>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>