119 lines
5.7 KiB
Kotlin
119 lines
5.7 KiB
Kotlin
package com.gacy.android
|
|||
|
|
|
||
|
|
import com.gacy.android.data.GoRuntimeInstaller
|
||
|
|
import org.json.JSONObject
|
||
|
|
import org.junit.Assert.*
|
||
|
|
import org.junit.Rule
|
||
|
|
import org.junit.Test
|
||
|
|
import org.junit.rules.TemporaryFolder
|
||
|
|
import java.io.ByteArrayInputStream
|
||
|
|
import java.io.ByteArrayOutputStream
|
||
|
|
import java.io.File
|
||
|
|
import java.util.concurrent.CountDownLatch
|
||
|
|
import java.util.concurrent.Executors
|
||
|
|
import java.util.concurrent.TimeUnit
|
||
|
|
import java.util.concurrent.atomic.AtomicInteger
|
||
|
|
import java.util.zip.ZipEntry
|
||
|
|
import java.util.zip.ZipOutputStream
|
||
|
|
|
||
|
|
class GoRuntimeInstallerTest {
|
||
|
|
@get:Rule val tmp = TemporaryFolder()
|
||
|
|
|
||
|
|
private fun archive(resource: String, hotfix: String, extra: Pair<String, String>? = null, versions: String = "{}"): ByteArray {
|
||
|
|
val entries = linkedMapOf(
|
||
|
|
"persistent" to "original-save", "freesr-data.json" to "original-roster",
|
||
|
|
"server.json" to "server-config", "hotfix.json" to hotfix, "versions.json" to versions,
|
||
|
|
"res.json" to resource, "teleports.json" to "{}", "resources/MapEntrance.json" to "{}"
|
||
|
|
)
|
||
|
|
if (extra != null) entries[extra.first] = extra.second
|
||
|
|
val output = ByteArrayOutputStream()
|
||
|
|
ZipOutputStream(output).use { zip ->
|
||
|
|
entries.forEach { (name, content) ->
|
||
|
|
zip.putNextEntry(ZipEntry(name)); zip.write(content.toByteArray()); zip.closeEntry()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return output.toByteArray()
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test fun updatePreservesUserDataAndRepairsMissingResources() {
|
||
|
|
val directory = tmp.newFolder("runtime")
|
||
|
|
val old = archive("old", """{"existing":{"asset":"bundled"}}""")
|
||
|
|
GoRuntimeInstaller(directory) { ByteArrayInputStream(old) }.prepare()
|
||
|
|
File(directory, "persistent").writeText("user-save")
|
||
|
|
File(directory, "freesr-data.json").writeText("user-roster")
|
||
|
|
File(directory, "hotfix.json").writeText("""{"existing":{"asset":"user"}}""")
|
||
|
|
File(directory, "versions.json").writeText("""{"existing":{"asset":"user"}}""")
|
||
|
|
val updated = archive("new", """{"existing":{"asset":"new"},"added":{"asset":"new"}}""", versions = """{"existing":{"asset":"new"},"added":{"asset":"new"}}""")
|
||
|
|
val installer = GoRuntimeInstaller(directory) { ByteArrayInputStream(updated) }
|
||
|
|
installer.prepare()
|
||
|
|
assertEquals("user-save", File(directory, "persistent").readText())
|
||
|
|
assertEquals("user-roster", File(directory, "freesr-data.json").readText())
|
||
|
|
assertEquals("new", File(directory, "res.json").readText())
|
||
|
|
val hotfix = JSONObject(File(directory, "hotfix.json").readText())
|
||
|
|
assertEquals("user", hotfix.getJSONObject("existing").getString("asset"))
|
||
|
|
assertEquals("new", hotfix.getJSONObject("added").getString("asset"))
|
||
|
|
val versions = JSONObject(File(directory, "versions.json").readText())
|
||
|
|
assertEquals("user", versions.getJSONObject("existing").getString("asset"))
|
||
|
|
assertEquals("new", versions.getJSONObject("added").getString("asset"))
|
||
|
|
File(directory, "resources/MapEntrance.json").delete()
|
||
|
|
installer.prepare()
|
||
|
|
assertTrue(File(directory, "resources/MapEntrance.json").isFile)
|
||
|
|
assertEquals("user-save", File(directory, "persistent").readText())
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test fun rejectsPathsOutsideRuntimeDirectory() {
|
||
|
|
val directory = tmp.newFolder("runtime")
|
||
|
|
val zip = archive("data", "{}", "../outside" to "bad")
|
||
|
|
try {
|
||
|
|
GoRuntimeInstaller(directory) { ByteArrayInputStream(zip) }.prepare()
|
||
|
|
fail("Path traversal should be rejected")
|
||
|
|
} catch (_: IllegalArgumentException) { }
|
||
|
|
assertFalse(File(directory.parentFile, "outside").exists())
|
||
|
|
assertFalse(File(directory, ".asset-version").exists())
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test fun installerUpgradeMergesVersionsEvenWhenArchiveIsUnchanged() {
|
||
|
|
val directory = tmp.newFolder("migration")
|
||
|
|
val zip = archive("data", "{}", versions = """{"new":{"asset":"bundle"}}""")
|
||
|
|
val installer = GoRuntimeInstaller(directory) { ByteArrayInputStream(zip) }
|
||
|
|
installer.prepare()
|
||
|
|
File(directory, "versions.json").writeText("""{"custom":{"asset":"user"}}""")
|
||
|
|
val marker = File(directory, ".asset-version")
|
||
|
|
marker.writeText(marker.readText().removePrefix("2:"))
|
||
|
|
installer.prepare()
|
||
|
|
val versions = JSONObject(File(directory, "versions.json").readText())
|
||
|
|
assertEquals("user", versions.getJSONObject("custom").getString("asset"))
|
||
|
|
assertEquals("bundle", versions.getJSONObject("new").getString("asset"))
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test fun simultaneousStartsSerializeExtraction() {
|
||
|
|
val directory = tmp.newFolder("runtime")
|
||
|
|
val zip = archive("data", "{}")
|
||
|
|
val firstOpened = CountDownLatch(1)
|
||
|
|
val release = CountDownLatch(1)
|
||
|
|
val secondOpened = CountDownLatch(1)
|
||
|
|
val opens = AtomicInteger()
|
||
|
|
val installer = GoRuntimeInstaller(directory) {
|
||
|
|
if (opens.incrementAndGet() == 1) {
|
||
|
|
firstOpened.countDown()
|
||
|
|
check(release.await(5, TimeUnit.SECONDS))
|
||
|
|
} else {
|
||
|
|
secondOpened.countDown()
|
||
|
|
}
|
||
|
|
ByteArrayInputStream(zip)
|
||
|
|
}
|
||
|
|
val workers = Executors.newFixedThreadPool(2)
|
||
|
|
try {
|
||
|
|
val first = workers.submit { installer.prepare() }
|
||
|
|
assertTrue(firstOpened.await(5, TimeUnit.SECONDS))
|
||
|
|
val second = workers.submit { installer.prepare() }
|
||
|
|
assertFalse("a second install entered before the first was released", secondOpened.await(200, TimeUnit.MILLISECONDS))
|
||
|
|
release.countDown()
|
||
|
|
first.get(10, TimeUnit.SECONDS); second.get(10, TimeUnit.SECONDS)
|
||
|
|
assertEquals("data", File(directory, "res.json").readText())
|
||
|
|
} finally {
|
||
|
|
release.countDown()
|
||
|
|
workers.shutdownNow()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|