PasswordTreePayloadWriter.kt
package de.pflugradts.passbird.application.passwordtree
import de.pflugradts.kotlinextensions.Option
import de.pflugradts.passbird.application.util.copyBytes
import de.pflugradts.passbird.application.util.copyInt
import de.pflugradts.passbird.application.util.readBytes
import de.pflugradts.passbird.application.util.scramble
import de.pflugradts.passbird.domain.model.egg.Egg
import de.pflugradts.passbird.domain.model.egg.Protein
import de.pflugradts.passbird.domain.model.shell.EncryptedShell
import de.pflugradts.passbird.domain.model.shell.Shell
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.shellOf
class PasswordTreePayloadWriter constructor() {
fun write(snapshot: PasswordTreeSnapshot): Shell {
val contentSize = calcRequiredContentSize(snapshot)
val bytes = ByteArray(signatureSize() + contentSize + checksumBytes())
try {
var offset = copyBytes(signature(), bytes, 0, signatureSize())
snapshot.memory.forEach { memoryListOption ->
memoryListOption.ifPresent { memoryList ->
memoryList.forEach { memoryEntry ->
offset += memoryEntry.encryptedShellAsByteArray().copyToAndScramble(bytes, offset)
}
}
}
snapshot.favorites.forEach { favoriteListOption ->
favoriteListOption.ifPresent { favoriteList ->
favoriteList.forEach { favoriteEntry ->
offset += favoriteEntry.encryptedShellAsByteArray().copyToAndScramble(bytes, offset)
}
}
}
snapshot.nests.forEach { nestShell ->
offset += nestShell.nestAsByteArray().copyToAndScramble(bytes, offset)
}
snapshot.eggs.forEach { egg ->
offset += egg.eggAsByteArray().copyToAndScramble(bytes, offset)
}
val checksumBytes = byteArrayOf(if (contentSize > 0) bytes.contentChecksum(contentSize) else 0x0)
try {
copyBytes(checksumBytes, bytes, offset, checksumBytes())
} finally {
checksumBytes.scramble()
}
return shellOf(bytes)
} finally {
bytes.scramble()
}
}
private fun calcRequiredContentSize(snapshot: PasswordTreeSnapshot): Int {
val memorySize = 100 * Integer.BYTES + snapshot.memory.fold(0) { acc, inner ->
acc + inner.map { slots -> slots.sumOf { it.map(EncryptedShell::size).orElse(0) } }.orElse(0)
}
val favoriteSize = 100 * Integer.BYTES + snapshot.favorites.fold(0) { acc, inner ->
acc + inner.map { slots -> slots.sumOf { it.map(EncryptedShell::size).orElse(0) } }.orElse(0)
}
val eggDataSize = snapshot.eggs.sumOf { it.serializedDataSize() }
val eggMetaSize = snapshot.eggs.size * 22 * intBytes()
val nestSize = snapshot.nests.size * intBytes() + snapshot.nests.filter { !it.isEmpty }.sumOf { it.size }
return memorySize + favoriteSize + eggDataSize + eggMetaSize + nestSize
}
private fun Option<EncryptedShell>.encryptedShellAsByteArray() = if (isPresent) {
val shellBytesSize = get().size
val bytes = ByteArray(Integer.BYTES + shellBytesSize)
copyInt(shellBytesSize, bytes, 0)
get().toByteArray().copyToAndScramble(bytes, Integer.BYTES)
bytes
} else {
val bytes = ByteArray(Integer.BYTES)
copyInt(0, bytes, 0)
bytes
}
private fun Shell.nestAsByteArray(): ByteArray {
val nestBytesSize = size
val bytes = ByteArray(Integer.BYTES + nestBytesSize)
copyInt(nestBytesSize, bytes, 0)
if (!isEmpty) toByteArray().copyToAndScramble(bytes, Integer.BYTES)
return bytes
}
private fun Egg.eggAsByteArray(): ByteArray {
val eggIdShell = viewEggId()
val passwordShell = viewPassword()
val eggIdSize = eggIdShell.size
val passwordSize = passwordShell.size
val metaSize = 22 * Integer.BYTES
val proteinDataSize = proteins.filter { it.isPresent }.sumOf { it.get().serializedDataSize() }
val bytes = ByteArray(Integer.BYTES + eggIdSize + passwordSize + metaSize + proteinDataSize)
var completed = false
try {
var offset = copyInt(associatedNest().index(), bytes, 0)
offset += copyInt(eggIdSize, bytes, offset)
offset += eggIdShell.toByteArray().copyToAndScramble(bytes, offset)
offset += copyInt(passwordSize, bytes, offset)
offset += passwordShell.toByteArray().copyToAndScramble(bytes, offset)
proteins.forEach { slottedProtein ->
if (slottedProtein.isPresent) {
val typeShell = slottedProtein.get().viewType()
val structureShell = slottedProtein.get().viewStructure()
try {
offset += copyInt(typeShell.size, bytes, offset)
offset += typeShell.toByteArray().copyToAndScramble(bytes, offset)
offset += copyInt(structureShell.size, bytes, offset)
offset += structureShell.toByteArray().copyToAndScramble(bytes, offset)
} finally {
typeShell.scramble()
structureShell.scramble()
}
} else {
offset += copyInt(0, bytes, offset)
offset += copyInt(0, bytes, offset)
}
}
completed = true
return bytes
} finally {
eggIdShell.scramble()
passwordShell.scramble()
if (!completed) bytes.scramble()
}
}
private fun Egg.serializedDataSize(): Int {
val eggIdShell = viewEggId()
val passwordShell = viewPassword()
try {
return intBytes() + eggIdShell.size + passwordShell.size + proteins.filter { it.isPresent }.sumOf {
it.get().serializedDataSize()
}
} finally {
eggIdShell.scramble()
passwordShell.scramble()
}
}
private fun Protein.serializedDataSize(): Int {
val typeShell = viewType()
val structureShell = viewStructure()
try {
return typeShell.size + structureShell.size
} finally {
typeShell.scramble()
structureShell.scramble()
}
}
private fun ByteArray.copyToAndScramble(target: ByteArray, offset: Int) = try {
copyBytes(this, target, offset, size)
} finally {
scramble()
}
private fun ByteArray.contentChecksum(contentSize: Int): Byte {
val checksumSource = readBytes(this, signatureSize(), contentSize)
try {
return checksum(checksumSource)
} finally {
checksumSource.scramble()
}
}
}