Skip to content

Commit

Permalink
Add :corellium:shard:dump module
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-goral committed May 12, 2021
1 parent e2963f5 commit 80465d2
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 1 deletion.
66 changes: 66 additions & 0 deletions corellium/shard/dump/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Shard dump

Allows dumping shards as formatted json file.

## Example

The example output file structure:
```json
[
[
{
"name": "path/to/app1.apk",
"tests": [
{
"name": "path/to/app1-test1.apk",
"cases": [
{
"name": "app1.test1.Test#case1",
"duration": 10000
}
]
}
]
}
],
[
{
"name": "path/to/app1.apk",
"tests": [
{
"name": "app1-test1.apk",
"cases": [
{
"name": "app1.test1.Test#case2",
"duration": 2000
}
]
}
]
},
{
"name": "path/to/app2.apk",
"tests": [
{
"name": "path/to/app2-test1.apk",
"cases": [
{
"name": "app2.test1.Test2#case1",
"duration": 1000
}
]
},
{
"name": "path/to/app2-test1.apk",
"cases": [
{
"name": "app2.test1.Test2#case2",
"duration": 120
}
]
}
]
}
]
]
```
21 changes: 21 additions & 0 deletions corellium/shard/dump/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin(Plugins.Kotlin.PLUGIN_JVM)
}

repositories {
jcenter()
mavenCentral()
maven(url = "https://kotlin.bintray.com/kotlinx")
}

tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "1.8" }

dependencies {
implementation(Dependencies.KOTLIN_COROUTINES_CORE)
implementation(Dependencies.GSON)
api(project(":corellium:shard"))

testImplementation(Dependencies.JUNIT)
}
25 changes: 25 additions & 0 deletions corellium/shard/dump/src/main/kotlin/flank/corellium/shard/Dump.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package flank.corellium.shard

import flank.corellium.shard.internal.obfuscatePrettyGson
import flank.corellium.shard.internal.prettyGson
import java.nio.file.Files.newBufferedWriter
import java.nio.file.Paths.get

/**
* Dump shards as formatted json file.
*
* @receiver List of shards to dump.
* @param filePath Relative or absolut path the file.
* @param obfuscate Hash the test cases names. Use for security reasons.
*/
fun List<InstanceShard>.dumpToFile(
filePath: String,
obfuscate: Boolean = false
) {
val writer = newBufferedWriter(get(filePath))
when (obfuscate) {
true -> obfuscatePrettyGson
false -> prettyGson
}.toJson(this, writer)
writer.flush()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package flank.corellium.shard.internal

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type

internal val obfuscatePrettyGson: Gson = GsonBuilder()
.registerTypeAdapter(ListOfStringTypeToken.rawType, ObfuscatedAndroidJsonSerializer)
.registerTypeHierarchyAdapter(ListOfStringListTypeToken.rawType, ObfuscatedIosJsonSerializer)
.setPrettyPrinting()
.create()

private object ListOfStringTypeToken : TypeToken<List<String>>()

private object ObfuscatedAndroidJsonSerializer : JsonSerializer<List<String>> {
private val obfuscationContext by lazy { mutableMapOf<String, MutableMap<String, String>>() }

override fun serialize(
src: List<String>,
typeOfSrc: Type,
context: JsonSerializationContext
) = JsonArray().also { jsonArray ->
src.forEach {
val items = it.split(" ") // split for class and test name
jsonArray.add(items.first() + " " + obfuscationContext.obfuscateAndroidTestName(items.last()))
}
}
}

private object ListOfStringListTypeToken : TypeToken<CustomShardChunks>()
private typealias CustomShardChunks = List<Map<String, List<String>>>

private object ObfuscatedIosJsonSerializer : JsonSerializer<CustomShardChunks> {
private val obfuscationContext by lazy { mutableMapOf<String, MutableMap<String, String>>() }

override fun serialize(
src: CustomShardChunks,
typeOfSrc: Type,
context: JsonSerializationContext
) = JsonArray().also { jsonArray ->
src.forEach { xcTestMethodList ->
val xcTestMethodJson = JsonObject()
xcTestMethodList.forEach { entry ->
xcTestMethodJson.add(
obfuscationContext.obfuscateIosTestName(entry.key),
JsonArray().also { nestedJsonArray ->
entry.value.forEach { it ->
nestedJsonArray.add(obfuscationContext.obfuscateIosTestName(it))
}
}
)
}
jsonArray.add(xcTestMethodJson)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package flank.corellium.shard.internal

import com.google.gson.Gson
import com.google.gson.GsonBuilder

internal val prettyGson: Gson = GsonBuilder().setPrettyPrinting().create()
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package flank.corellium.shard.internal

private const val LOWER_CASE_CHARS = "abcdefghijklmnopqrstuvwxyz"
private const val UPPER_CASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
private const val ANDROID_TEST_METHOD_SEPARATOR = '#'
private const val ANDROID_PACKAGE_SEPARATOR = '.'
private const val IOS_TEST_METHOD_SEPARATOR = '/'

internal typealias ObfuscationContext = MutableMap<String, MutableMap<String, String>>

internal fun ObfuscationContext.obfuscateAndroidTestName(input: String): String {
val obfuscatedPackageNameWithClass =
obfuscateAndroidPackageAndClass(input.split(ANDROID_TEST_METHOD_SEPARATOR).first())

return obfuscatedPackageNameWithClass + obfuscateAndroidMethodIfPresent(input, obfuscatedPackageNameWithClass)
}

private fun ObfuscationContext.obfuscateAndroidPackageAndClass(packageNameWithClass: String) =
packageNameWithClass
.split(ANDROID_PACKAGE_SEPARATOR)
.fold("") { previous, next ->
val classChunk = getOrPut(previous) { linkedMapOf() }
val obfuscatedPart = classChunk.getOrPut(next) { nextSymbol(next, classChunk) }
if (previous.isBlank()) obfuscatedPart else "$previous$ANDROID_PACKAGE_SEPARATOR$obfuscatedPart"
}

private fun ObfuscationContext.obfuscateAndroidMethodIfPresent(
input: String,
obfuscatedPackageNameWithClass: String
) = if (input.contains(ANDROID_TEST_METHOD_SEPARATOR))
ANDROID_TEST_METHOD_SEPARATOR + obfuscateMethodName(
methodName = input.split(ANDROID_TEST_METHOD_SEPARATOR).last(),
context = getOrPut(obfuscatedPackageNameWithClass) { mutableMapOf() }
)
else ""

internal fun ObfuscationContext.obfuscateIosTestName(input: String): String {
val className = input.split(IOS_TEST_METHOD_SEPARATOR).first()
val obfuscatedClassName = getOrPut("") { mutableMapOf() }.run {
getOrPut(className) { nextSymbol(className, this) }
}
return obfuscatedClassName +
IOS_TEST_METHOD_SEPARATOR +
obfuscateMethodName(
methodName = input.split(IOS_TEST_METHOD_SEPARATOR).last(),
context = getOrPut(obfuscatedClassName) { linkedMapOf() }
)
}

private fun nextSymbol(key: String, context: Map<String, String>): String {
val isLowerCaseKey = key.first().isLowerCase()
val possibleSymbols = if (isLowerCaseKey) LOWER_CASE_CHARS else UPPER_CASE_CHARS

val currentContextItemCount = context.values.count {
if (isLowerCaseKey) it.first().isLowerCase() else it.first().isUpperCase()
}
val repeatSymbol = currentContextItemCount / possibleSymbols.length + 1

return possibleSymbols[currentContextItemCount % possibleSymbols.length].toString().repeat(repeatSymbol)
}

private fun obfuscateMethodName(methodName: String, context: MutableMap<String, String>) =
context.getOrPut(methodName) { nextSymbol(methodName, context) }
142 changes: 142 additions & 0 deletions corellium/shard/dump/src/test/kotlin/flank/corellium/shard/DumpTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package flank.corellium.shard

import org.junit.After
import org.junit.Assert
import org.junit.Test
import java.io.File

class DumpTest {

private val filePath = "dump-shards.json"
private val file = File(filePath)

private val shards = listOf(
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "app1-test1#case1",
duration = 10_000,
),
)
),
)
),
),
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "app1-test1#case2",
duration = 2_000,
),
)
),
)
),
Shard.App(
name = "app2",
tests = listOf(
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "app2-test1#case1",
duration = 1_000,
),
)
),
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "app2-test1#case2",
),
)
),
)
),
)
)

private val expected = """
[
[
{
"name": "app1",
"tests": [
{
"name": "app1-test1",
"cases": [
{
"name": "app1-test1#case1",
"duration": 10000
}
]
}
]
}
],
[
{
"name": "app1",
"tests": [
{
"name": "app1-test1",
"cases": [
{
"name": "app1-test1#case2",
"duration": 2000
}
]
}
]
},
{
"name": "app2",
"tests": [
{
"name": "app2-test1",
"cases": [
{
"name": "app2-test1#case1",
"duration": 1000
}
]
},
{
"name": "app2-test1",
"cases": [
{
"name": "app2-test1#case2",
"duration": 120
}
]
}
]
}
]
]
""".trimIndent()

@Test
fun testDumpToFile() {
shards.dumpToFile(filePath)

Assert.assertTrue(file.exists())
Assert.assertEquals(expected, file.readText().trim())
}

@After
fun cleanUp () {
file.delete()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object Shard {
/**
* Abstract representation for test case (test method).
* @property name An abstract name for identifying test case.
* @property duration The duration of the test case run. Use default if no previous duration was recorded.
* @property duration The duration of the test case run in milliseconds. Use default if no previous duration was recorded.
*/
class Case(
val name: String,
Expand Down
Loading

0 comments on commit 80465d2

Please sign in to comment.