/*
 * Copyright 2019 Jeremy Jamet / Kunzisoft.
 *     
 * This file is part of KeePassDX.
 *
 *  KeePassDX is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  KeePassDX is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with KeePassDX.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.kunzisoft.keepass.database.file.input

import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.StreamCipher
import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.HmacBlock
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.CustomDataItem
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DateInstant.Companion.fromDotNetSeconds
import com.kunzisoft.keepass.database.element.DateInstant.Companion.fromISO8601Format
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.CorruptedDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.database.exception.InvalidAlgorithmDatabaseException
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.database.exception.KDFMemoryDatabaseException
import com.kunzisoft.keepass.database.exception.NoMemoryDatabaseException
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.stream.HashedBlockInputStream
import com.kunzisoft.keepass.stream.HmacBlockInputStream
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.keepass.utils.bytes16ToUuid
import com.kunzisoft.keepass.utils.bytes64ToLong
import com.kunzisoft.keepass.utils.readBytes
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import com.kunzisoft.keepass.utils.readBytesLength
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.text.ParseException
import java.util.Arrays
import java.util.Stack
import java.util.UUID
import java.util.zip.GZIPInputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.Mac
import kotlin.math.min

class DatabaseInputKDBX(database: DatabaseKDBX)
    : DatabaseInput<DatabaseKDBX>(database) {

    private var randomStream: StreamCipher? = null

    private var hashOfHeader: ByteArray? = null

    private var readNextNode = true
    private val ctxGroups = Stack<GroupKDBX>()
    private var ctxGroup: GroupKDBX? = null
    private var ctxEntry: EntryKDBX? = null
    private var ctxStringName: String? = null
    private var ctxStringValue: ProtectedString? = null
    private var ctxBinaryName: String? = null
    private var ctxBinaryValue: BinaryData? = null
    private var ctxATName: String? = null
    private var ctxATSeq: String? = null
    private var entryInHistory = false
    private var ctxHistoryBase: EntryKDBX? = null
    private var ctxDeletedObject: DeletedObject? = null
    private var customIconID = DatabaseVersioned.UUID_ZERO
    private var customIconName: String = ""
    private var customIconLastModificationTime: DateInstant? = null
    private var customIconData: ByteArray? = null
    private var customDataKey: String? = null
    private var customDataValue: String? = null
    private var customDataLastModificationTime: DateInstant? = null
    private var groupCustomDataKey: String? = null
    private var groupCustomDataValue: String? = null
    private var groupCustomDataLastModificationTime: DateInstant? = null
    private var entryCustomDataKey: String? = null
    private var entryCustomDataValue: String? = null
    private var entryCustomDataLastModificationTime: DateInstant? = null

    private var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}

    fun setMethodToCheckIfRAMIsSufficient(method: (memoryWanted: Long) -> Boolean) {
        this.isRAMSufficient = method
    }

    @Throws(DatabaseInputException::class)
    override fun openDatabase(databaseInputStream: InputStream,
                              progressTaskUpdater: ProgressTaskUpdater?,
                              assignMasterKey: (() -> Unit)): DatabaseKDBX {
        try {
            startKeyTimer(progressTaskUpdater)

            val header = DatabaseHeaderKDBX(mDatabase)

            val headerAndHash = header.loadFromFile(databaseInputStream)
            mDatabase.kdbxVersion = header.version

            hashOfHeader = headerAndHash.hash
            val pbHeader = headerAndHash.header

            val transformSeed = header.transformSeed
            mDatabase.transformSeed = transformSeed
            assignMasterKey.invoke()
            mDatabase.makeFinalKey(header.masterSeed)

            stopKeyTimer()
            startContentTimer(progressTaskUpdater)

            val cipher: Cipher
            try {
                val engine: CipherEngine = mDatabase.encryptionAlgorithm.cipherEngine
                engine.forcePaddingCompatibility = true
                cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
                engine.forcePaddingCompatibility = false
            } catch (e: Exception) {
                throw InvalidAlgorithmDatabaseException(e)
            }

            val plainInputStream: InputStream
            if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {

                val dataDecrypted = CipherInputStream(databaseInputStream, cipher)
                val storedStartBytes: ByteArray?
                try {
                    storedStartBytes = dataDecrypted.readBytesLength(32)
                    if (storedStartBytes.size != 32) {
                        throw InvalidCredentialsDatabaseException()
                    }
                } catch (e: IOException) {
                    throw InvalidCredentialsDatabaseException()
                }

                if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
                    throw InvalidCredentialsDatabaseException()
                }

                plainInputStream = HashedBlockInputStream(dataDecrypted)
            } else { // KDBX 4
                val storedHash = databaseInputStream.readBytesLength(32)
                if (!storedHash.contentEquals(hashOfHeader)) {
                    throw InvalidCredentialsDatabaseException()
                }

                val hmacKey = mDatabase.hmacKey ?: throw DatabaseInputException()

                val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
                val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
                val headerHmac = hmac.doFinal(pbHeader)

                val storedHmac = databaseInputStream.readBytesLength(32)
                if (storedHmac.size != 32) {
                    throw InvalidCredentialsDatabaseException()
                }
                // Mac doesn't match
                if (!headerHmac.contentEquals(storedHmac)) {
                    throw InvalidCredentialsDatabaseException()
                }

                val hmIs = HmacBlockInputStream(databaseInputStream, true, hmacKey)

                plainInputStream = CipherInputStream(hmIs, cipher)
            }

            val inputStreamXml: InputStream = when (mDatabase.compressionAlgorithm) {
                CompressionAlgorithm.GZIP -> GZIPInputStream(plainInputStream)
                else -> plainInputStream
            }

            if (!mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
                readInnerHeader(inputStreamXml, header)
            }

            try {
                randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
            } catch (e: Exception) {
                throw DatabaseInputException(e)
            }

            val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply {
                isNamespaceAware = false
            }
            val xmlPullParser = xmlPullParserFactory.newPullParser().apply {
                setInput(inputStreamXml, null)
            }
            readDocumentStreamed(xmlPullParser)

            stopContentTimer()

        } catch (e: Error) {
            if (e is OutOfMemoryError)
                throw NoMemoryDatabaseException(e)
            if (e.message?.contains("Hash failed with code") == true)
                throw KDFMemoryDatabaseException(e)
            throw DatabaseInputException(e)
        }

        return mDatabase
    }

    @Throws(IOException::class)
    private fun readInnerHeader(dataInputStream: InputStream,
                                header: DatabaseHeaderKDBX) {

        var readStream = true
        while (readStream) {
            val fieldId = dataInputStream.read().toByte()

            val size = dataInputStream.readBytes4ToUInt().toKotlinInt()
            if (size < 0) throw CorruptedDatabaseException()

            var data = ByteArray(0)
            try {
                if (size > 0) {
                    if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
                        data = dataInputStream.readBytesLength(size)
                    }
                }
            } catch (e: Exception) {
                // OOM only if corrupted file
                throw CorruptedDatabaseException()
            }

            readStream = true
            when (fieldId) {
                DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader -> {
                    readStream = false
                }
                DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID -> {
                    header.setRandomStreamID(data)
                }
                DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey -> {
                    header.innerRandomStreamKey = data
                }
                DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
                    // Read in a file
                    val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
                    val byteLength = size - 1
                    // No compression at this level
                    val protectedBinary = mDatabase.buildNewBinaryAttachment(
                            isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
                    protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
                        dataInputStream.readBytes(byteLength) { buffer ->
                            outputStream.write(buffer)
                        }
                    }
                }
            }
        }
    }

    private enum class KdbContext {
        Null,
        KeePassFile,
        Meta,
        Root,
        MemoryProtection,
        CustomIcons,
        CustomIcon,
        CustomData,
        CustomDataItem,
        RootDeletedObjects,
        DeletedObject,
        Group,
        GroupTimes,
        GroupCustomData,
        GroupCustomDataItem,
        Entry,
        EntryTimes,
        EntryString,
        EntryBinary,
        EntryAutoType,
        EntryAutoTypeItem,
        EntryHistory,
        EntryCustomData,
        EntryCustomDataItem,
        Binaries
    }

    @Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
    private fun readDocumentStreamed(xpp: XmlPullParser) {

        ctxGroups.clear()

        var ctx = KdbContext.Null

        readNextNode = true

        while (true) {
            if (readNextNode) {
                if (xpp.next() == XmlPullParser.END_DOCUMENT) break
            } else {
                readNextNode = true
            }

            when (xpp.eventType) {
                XmlPullParser.START_TAG -> ctx = readXmlElement(ctx, xpp)

                XmlPullParser.END_TAG -> ctx = endXmlElement(ctx, xpp)

                else -> {
                }
            }
        }

        // Error checks
        if (ctx != KdbContext.Null) throw XMLMalformedDatabaseException()
        if (ctxGroups.size != 0) throw XMLMalformedDatabaseException()
    }

    @Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
    private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext {
        val name = xpp.name
        when (ctx) {
            KdbContext.Null -> if (name.equals(DatabaseKDBXXML.ElemDocNode, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.KeePassFile, xpp)
            } else
                readUnknown(xpp)

            KdbContext.KeePassFile -> if (name.equals(DatabaseKDBXXML.ElemMeta, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.Meta, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemRoot, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.Root, xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.Meta -> if (name.equals(DatabaseKDBXXML.ElemGenerator, ignoreCase = true)) {
                readString(xpp) // Ignore
            } else if (name.equals(DatabaseKDBXXML.ElemHeaderHash, ignoreCase = true)) {
                val encodedHash = readString(xpp)
                if (encodedHash.isNotEmpty() && hashOfHeader != null) {
                    val hash = Base64.decode(encodedHash, BASE64_FLAG)
                    if (!Arrays.equals(hash, hashOfHeader)) {
                        throw DatabaseInputException()
                    }
                }
            } else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) {
                mDatabase.settingsChanged = readDateInstant(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbName, ignoreCase = true)) {
                mDatabase.name = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbNameChanged, ignoreCase = true)) {
                mDatabase.nameChanged = readDateInstant(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbDesc, ignoreCase = true)) {
                mDatabase.description = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbDescChanged, ignoreCase = true)) {
                mDatabase.descriptionChanged = readDateInstant(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbDefaultUser, ignoreCase = true)) {
                mDatabase.defaultUserName = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbDefaultUserChanged, ignoreCase = true)) {
                mDatabase.defaultUserNameChanged = readDateInstant(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbColor, ignoreCase = true)) {
                mDatabase.color = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbMntncHistoryDays, ignoreCase = true)) {
                mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS)
            } else if (name.equals(DatabaseKDBXXML.ElemDbKeyChanged, ignoreCase = true)) {
                mDatabase.keyLastChanged = readDateInstant(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDbKeyChangeRec, ignoreCase = true)) {
                mDatabase.keyChangeRecDays = readLong(xpp, -1)
            } else if (name.equals(DatabaseKDBXXML.ElemDbKeyChangeForce, ignoreCase = true)) {
                mDatabase.keyChangeForceDays = readLong(xpp, -1)
            } else if (name.equals(DatabaseKDBXXML.ElemDbKeyChangeForceOnce, ignoreCase = true)) {
                mDatabase.isKeyChangeForceOnce = readBool(xpp, false)
            } else if (name.equals(DatabaseKDBXXML.ElemMemoryProt, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.MemoryProtection, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemCustomIcons, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.CustomIcons, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemRecycleBinEnabled, ignoreCase = true)) {
                mDatabase.isRecycleBinEnabled = readBool(xpp, true)
            } else if (name.equals(DatabaseKDBXXML.ElemRecycleBinUuid, ignoreCase = true)) {
                mDatabase.recycleBinUUID = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemRecycleBinChanged, ignoreCase = true)) {
                mDatabase.recycleBinChanged = readDateInstant(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemEntryTemplatesGroup, ignoreCase = true)) {
                mDatabase.entryTemplatesGroup = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, ignoreCase = true)) {
                mDatabase.entryTemplatesGroupChanged = readDateInstant(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemHistoryMaxItems, ignoreCase = true)) {
                mDatabase.historyMaxItems = readInt(xpp, -1)
            } else if (name.equals(DatabaseKDBXXML.ElemHistoryMaxSize, ignoreCase = true)) {
                mDatabase.historyMaxSize = readLong(xpp, -1)
            } else if (name.equals(DatabaseKDBXXML.ElemLastSelectedGroup, ignoreCase = true)) {
                mDatabase.lastSelectedGroupUUID = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemLastTopVisibleGroup, ignoreCase = true)) {
                mDatabase.lastTopVisibleGroupUUID = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemBinaries, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.Binaries, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.CustomData, xpp)
            }

            KdbContext.MemoryProtection -> if (name.equals(DatabaseKDBXXML.ElemProtTitle, ignoreCase = true)) {
                mDatabase.memoryProtection.protectTitle = readBool(xpp, MemoryProtectionConfig.DEFAULT_PROTECT_TITLE)
            } else if (name.equals(DatabaseKDBXXML.ElemProtUserName, ignoreCase = true)) {
                mDatabase.memoryProtection.protectUserName = readBool(xpp, MemoryProtectionConfig.DEFAULT_PROTECT_USERNAME)
            } else if (name.equals(DatabaseKDBXXML.ElemProtPassword, ignoreCase = true)) {
                mDatabase.memoryProtection.protectPassword = readBool(xpp, MemoryProtectionConfig.DEFAULT_PROTECT_PASSWORD)
            } else if (name.equals(DatabaseKDBXXML.ElemProtURL, ignoreCase = true)) {
                mDatabase.memoryProtection.protectUrl = readBool(xpp, MemoryProtectionConfig.DEFAULT_PROTECT_URL)
            } else if (name.equals(DatabaseKDBXXML.ElemProtNotes, ignoreCase = true)) {
                mDatabase.memoryProtection.protectNotes = readBool(xpp, MemoryProtectionConfig.DEFAULT_PROTECT_NOTES)
            } else if (name.equals(DatabaseKDBXXML.ElemProtAutoHide, ignoreCase = true)) {
                mDatabase.memoryProtection.autoEnableVisualHiding = readBool(xpp, MemoryProtectionConfig.DEFAULT_AUTO_ENABLE_VISUAL_HIDING)
            } else {
                readUnknown(xpp)
            }

            KdbContext.CustomIcons -> if (name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.CustomIcon, xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.CustomIcon -> if (name.equals(DatabaseKDBXXML.ElemCustomIconItemID, ignoreCase = true)) {
                customIconID = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemCustomIconItemData, ignoreCase = true)) {
                val strData = readString(xpp)
                if (strData.isNotEmpty()) {
                    customIconData = Base64.decode(strData, BASE64_FLAG)
                }
            } else if (name.equals(DatabaseKDBXXML.ElemName, ignoreCase = true)) {
                customIconName = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true)) {
                customIconLastModificationTime = readDateInstant(xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
                readBinary(xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.CustomData -> if (name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.CustomDataItem, xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.CustomDataItem -> if (name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true)) {
                customDataKey = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true)) {
                customDataValue = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true)) {
                customDataLastModificationTime = readDateInstant(xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.Root -> if (name.equals(DatabaseKDBXXML.ElemGroup, ignoreCase = true)) {
                if (ctxGroups.size != 0)
                    throw IOException("Group list should be empty.")

                mDatabase.rootGroup = mDatabase.createGroup()
                ctxGroups.push(mDatabase.rootGroup)
                ctxGroup = ctxGroups.peek()

                return switchContext(ctx, KdbContext.Group, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDeletedObjects, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.RootDeletedObjects, xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.Group -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
                ctxGroup?.nodeId = NodeIdUUID(readUuid(xpp))
                ctxGroup?.let { mDatabase.addGroupIndex(it) }
            } else if (name.equals(DatabaseKDBXXML.ElemName, ignoreCase = true)) {
                ctxGroup?.title = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
                ctxGroup?.notes = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
                ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
            } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
                val iconUUID = readUuid(xpp)
                ctxGroup?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
            } else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
                ctxGroup?.tags = readTags(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
                ctxGroup?.previousParentGroup = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.GroupTimes, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) {
                ctxGroup?.isExpanded = readBool(xpp, true)
            } else if (name.equals(DatabaseKDBXXML.ElemGroupDefaultAutoTypeSeq, ignoreCase = true)) {
                ctxGroup?.defaultAutoTypeSequence = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemEnableAutoType, ignoreCase = true)) {
                ctxGroup?.enableAutoType = readOptionalBool(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemEnableSearching, ignoreCase = true)) {
                ctxGroup?.enableSearching = readOptionalBool(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemLastTopVisibleEntry, ignoreCase = true)) {
                ctxGroup?.lastTopVisibleEntry = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.GroupCustomData, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemGroup, ignoreCase = true)) {
                ctxGroup = mDatabase.createGroup()
                val groupPeek = ctxGroups.peek()
                ctxGroup?.let {
                    groupPeek.addChildGroup(it)
                    it.parent = groupPeek
                    ctxGroups.push(it)
                }

                return switchContext(ctx, KdbContext.Group, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemEntry, ignoreCase = true)) {
                ctxEntry = mDatabase.createEntry()
                ctxEntry?.let {
                    ctxGroup?.addChildEntry(it)
                    it.parent = ctxGroup
                }

                entryInHistory = false
                return switchContext(ctx, KdbContext.Entry, xpp)
            } else {
                readUnknown(xpp)
            }
            KdbContext.GroupCustomData -> if (name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.GroupCustomDataItem, xpp)
            } else {
                readUnknown(xpp)
            }
            KdbContext.GroupCustomDataItem -> when {
                name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true) -> groupCustomDataKey = readString(xpp)
                name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true) -> groupCustomDataValue = readString(xpp)
                name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> groupCustomDataLastModificationTime = readDateInstant(xpp)
                else -> readUnknown(xpp)
            }


            KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
                ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
            } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
                ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
            } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
                val iconUUID = readUuid(xpp)
                ctxEntry?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
            } else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
                ctxEntry?.foregroundColor = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
                ctxEntry?.backgroundColor = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemOverrideUrl, ignoreCase = true)) {
                ctxEntry?.overrideURL = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemQualityCheck, ignoreCase = true)) {
                ctxEntry?.qualityCheck = readBool(xpp, true)
            } else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
                ctxEntry?.tags = readTags(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
                ctxEntry?.previousParentGroup = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.EntryTimes, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemString, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.EntryString, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.EntryBinary, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemAutoType, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.EntryAutoType, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.EntryCustomData, xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemHistory, ignoreCase = true)) {
                if (!entryInHistory) {
                    ctxHistoryBase = ctxEntry
                    return switchContext(ctx, KdbContext.EntryHistory, xpp)
                } else {
                    readUnknown(xpp)
                }
            } else {
                readUnknown(xpp)
            }
            KdbContext.EntryCustomData -> if (name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.EntryCustomDataItem, xpp)
            } else {
                readUnknown(xpp)
            }
            KdbContext.EntryCustomDataItem -> when {
                name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true) -> entryCustomDataKey = readString(xpp)
                name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true) -> entryCustomDataValue = readString(xpp)
                name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> entryCustomDataLastModificationTime = readDateInstant(xpp)
                else -> readUnknown(xpp)
            }

            KdbContext.GroupTimes, KdbContext.EntryTimes -> {
                val tl: NodeKDBXInterface? =
                        if (ctx == KdbContext.GroupTimes) {
                            ctxGroup
                        } else {
                            ctxEntry
                        }

                when {
                    name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> tl?.lastModificationTime = readDateInstant(xpp)
                    name.equals(DatabaseKDBXXML.ElemCreationTime, ignoreCase = true) -> tl?.creationTime = readDateInstant(xpp)
                    name.equals(DatabaseKDBXXML.ElemLastAccessTime, ignoreCase = true) -> tl?.lastAccessTime = readDateInstant(xpp)
                    name.equals(DatabaseKDBXXML.ElemExpiryTime, ignoreCase = true) -> tl?.expiryTime = readDateInstant(xpp)
                    name.equals(DatabaseKDBXXML.ElemExpires, ignoreCase = true) -> tl?.expires = readBool(xpp, false)
                    name.equals(DatabaseKDBXXML.ElemUsageCount, ignoreCase = true) -> tl?.usageCount = readULong(xpp, UnsignedLong(0))
                    name.equals(DatabaseKDBXXML.ElemLocationChanged, ignoreCase = true) -> tl?.locationChanged = readDateInstant(xpp)
                    else -> readUnknown(xpp)
                }
            }

            KdbContext.EntryString -> if (name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true)) {
                ctxStringName = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true)) {
                ctxStringValue = readProtectedString(xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.EntryBinary -> if (name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true)) {
                ctxBinaryName = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true)) {
                ctxBinaryValue = readBinary(xpp)
            }

            KdbContext.EntryAutoType -> if (name.equals(DatabaseKDBXXML.ElemAutoTypeEnabled, ignoreCase = true)) {
                ctxEntry?.autoType?.enabled = readBool(xpp, true)
            } else if (name.equals(DatabaseKDBXXML.ElemAutoTypeObfuscation, ignoreCase = true)) {
                ctxEntry?.autoType?.obfuscationOptions = readUInt(xpp, UnsignedInt(0))
            } else if (name.equals(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, ignoreCase = true)) {
                ctxEntry?.autoType?.defaultSequence = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemAutoTypeItem, ignoreCase = true)) {
                return switchContext(ctx, KdbContext.EntryAutoTypeItem, xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.EntryAutoTypeItem -> if (name.equals(DatabaseKDBXXML.ElemWindow, ignoreCase = true)) {
                ctxATName = readString(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemKeystrokeSequence, ignoreCase = true)) {
                ctxATSeq = readString(xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.EntryHistory -> if (name.equals(DatabaseKDBXXML.ElemEntry, ignoreCase = true)) {
                ctxEntry = EntryKDBX()
                ctxEntry?.let { ctxHistoryBase?.addEntryToHistory(it) }

                entryInHistory = true
                return switchContext(ctx, KdbContext.Entry, xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.RootDeletedObjects -> if (name.equals(DatabaseKDBXXML.ElemDeletedObject, ignoreCase = true)) {
                ctxDeletedObject = DeletedObject()
                ctxDeletedObject?.let { mDatabase.addDeletedObject(it) }

                return switchContext(ctx, KdbContext.DeletedObject, xpp)
            } else {
                readUnknown(xpp)
            }

            KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
                ctxDeletedObject?.uuid = readUuid(xpp)
            } else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
                ctxDeletedObject?.deletionTime = readDateInstant(xpp)
            } else {
                readUnknown(xpp)
            }
        }

        return ctx
    }

    @Throws(XmlPullParserException::class)
    private fun endXmlElement(ctx: KdbContext?, xpp: XmlPullParser): KdbContext {
        // (xpp.getEventType() == XmlPullParser.END_TAG);

        val name = xpp.name
        if (ctx == KdbContext.KeePassFile && name.equals(DatabaseKDBXXML.ElemDocNode, ignoreCase = true)) {
            return KdbContext.Null
        } else if (ctx == KdbContext.Meta && name.equals(DatabaseKDBXXML.ElemMeta, ignoreCase = true)) {
            return KdbContext.KeePassFile
        } else if (ctx == KdbContext.Root && name.equals(DatabaseKDBXXML.ElemRoot, ignoreCase = true)) {
            return KdbContext.KeePassFile
        } else if (ctx == KdbContext.MemoryProtection && name.equals(DatabaseKDBXXML.ElemMemoryProt, ignoreCase = true)) {
            return KdbContext.Meta
        } else if (ctx == KdbContext.CustomIcons && name.equals(DatabaseKDBXXML.ElemCustomIcons, ignoreCase = true)) {
            return KdbContext.Meta
        } else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
            val iconData = customIconData
            if (customIconID != DatabaseVersioned.UUID_ZERO && iconData != null) {
                mDatabase.addCustomIcon(customIconID,
                        customIconName,
                        customIconLastModificationTime,
                        isRAMSufficient.invoke(iconData.size.toLong())) { _, binary ->
                    binary?.getOutputDataStream(mDatabase.binaryCache)?.use { outputStream ->
                        outputStream.write(iconData)
                    }
                }
            }
            customIconID = DatabaseVersioned.UUID_ZERO
            customIconName = ""
            customIconLastModificationTime = null
            customIconData = null
            return KdbContext.CustomIcons
        } else if (ctx == KdbContext.Binaries && name.equals(DatabaseKDBXXML.ElemBinaries, ignoreCase = true)) {
            return KdbContext.Meta
        } else if (ctx == KdbContext.CustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
            return KdbContext.Meta
        } else if (ctx == KdbContext.CustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
            customDataKey?.let { dataKey ->
                customDataValue?.let { dataValue ->
                    mDatabase.customData.put(CustomDataItem(dataKey,
                            dataValue, customDataLastModificationTime))
                }
            }
            customDataKey = null
            customDataValue = null
            customDataLastModificationTime = null
            return KdbContext.CustomData
        } else if (ctx == KdbContext.Group && name.equals(DatabaseKDBXXML.ElemGroup, ignoreCase = true)) {
            if (ctxGroup != null && ctxGroup?.id == DatabaseVersioned.UUID_ZERO) {
                ctxGroup?.nodeId = mDatabase.newGroupId()
                mDatabase.addGroupIndex(ctxGroup!!)
            }

            ctxGroups.pop()

            if (ctxGroups.size == 0) {
                ctxGroup = null
                return KdbContext.Root
            } else {
                ctxGroup = ctxGroups.peek()
                return KdbContext.Group
            }
        } else if (ctx == KdbContext.GroupTimes && name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
            return KdbContext.Group
        } else if (ctx == KdbContext.GroupCustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
            return KdbContext.Group
        } else if (ctx == KdbContext.GroupCustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
            groupCustomDataKey?.let { customDataKey ->
                groupCustomDataValue?.let { customDataValue ->
                    ctxGroup?.customData?.put(CustomDataItem(customDataKey, customDataValue, groupCustomDataLastModificationTime))
                }
            }
            groupCustomDataKey = null
            groupCustomDataValue = null
            groupCustomDataLastModificationTime = null
            return KdbContext.GroupCustomData

        } else if (ctx == KdbContext.Entry && name.equals(DatabaseKDBXXML.ElemEntry, ignoreCase = true)) {

            if (ctxEntry?.id == DatabaseVersioned.UUID_ZERO)
                ctxEntry?.nodeId = mDatabase.newEntryId()

            if (entryInHistory) {
                ctxEntry = ctxHistoryBase
                return KdbContext.EntryHistory
            } else if (ctxEntry != null) {
                // Add entry to the index only when close the XML element
                mDatabase.addEntryIndex(ctxEntry!!)
            }

            return KdbContext.Group
        } else if (ctx == KdbContext.EntryTimes && name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
            return KdbContext.Entry
        } else if (ctx == KdbContext.EntryString && name.equals(DatabaseKDBXXML.ElemString, ignoreCase = true)) {
            if (ctxStringName != null && ctxStringValue != null)
                ctxEntry?.putField(ctxStringName!!, ctxStringValue!!)
            ctxStringName = null
            ctxStringValue = null

            return KdbContext.Entry
        } else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
            if (ctxBinaryName != null && ctxBinaryValue != null) {
                ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.attachmentPool)
            }
            ctxBinaryName = null
            ctxBinaryValue = null

            return KdbContext.Entry
        } else if (ctx == KdbContext.EntryAutoType && name.equals(DatabaseKDBXXML.ElemAutoType, ignoreCase = true)) {
            return KdbContext.Entry
        } else if (ctx == KdbContext.EntryAutoTypeItem && name.equals(DatabaseKDBXXML.ElemAutoTypeItem, ignoreCase = true)) {
            if (ctxATName != null && ctxATSeq != null)
                ctxEntry?.autoType?.add(ctxATName!!, ctxATSeq!!)
            ctxATName = null
            ctxATSeq = null

            return KdbContext.EntryAutoType
        } else if (ctx == KdbContext.EntryCustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
            return KdbContext.Entry
        } else if (ctx == KdbContext.EntryCustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
            entryCustomDataKey?.let { customDataKey ->
                entryCustomDataValue?.let { customDataValue ->
                    ctxEntry?.customData?.put(CustomDataItem(customDataKey, customDataValue, entryCustomDataLastModificationTime))
                }
            }
            entryCustomDataKey = null
            entryCustomDataValue = null
            entryCustomDataLastModificationTime = null
            return KdbContext.EntryCustomData
        } else if (ctx == KdbContext.EntryHistory && name.equals(DatabaseKDBXXML.ElemHistory, ignoreCase = true)) {
            entryInHistory = false
            return KdbContext.Entry
        } else if (ctx == KdbContext.RootDeletedObjects && name.equals(DatabaseKDBXXML.ElemDeletedObjects, ignoreCase = true)) {
            return KdbContext.Root
        } else if (ctx == KdbContext.DeletedObject && name.equals(DatabaseKDBXXML.ElemDeletedObject, ignoreCase = true)) {
            ctxDeletedObject = null
            return KdbContext.RootDeletedObjects
        } else {
            var contextName = ""
            if (ctx != null) {
                contextName = ctx.name
            }
            throw XMLMalformedDatabaseException("Invalid end element: Context " + contextName + "End element: " + name)
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readDateInstant(xpp: XmlPullParser): DateInstant {
        val sDate = readString(xpp)
        var utcDate = DateInstant()
        if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
            try {
                utcDate = sDate.fromISO8601Format()
            } catch (e: ParseException) {
                // Catch with null test below
            }
        } else {
            var buf = Base64.decode(sDate, BASE64_FLAG)
            if (buf.size != 8) {
                val buf8 = ByteArray(8)
                System.arraycopy(buf, 0, buf8, 0, min(buf.size, 8))
                buf = buf8
            }
            val seconds = bytes64ToLong(buf)
            utcDate = seconds.fromDotNetSeconds()
        }
        return utcDate
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readTags(xpp: XmlPullParser): Tags {
        val tags = Tags(readString(xpp))
        mDatabase.tagPool.put(tags)
        return tags
    }

    @Throws(XmlPullParserException::class, IOException::class)
    private fun readUnknown(xpp: XmlPullParser) {
        if (xpp.isEmptyElementTag) return

        readProtectedBase64String(xpp)
        while (xpp.next() != XmlPullParser.END_DOCUMENT) {
            if (xpp.eventType == XmlPullParser.END_TAG) break
            if (xpp.eventType == XmlPullParser.START_TAG) continue

            readUnknown(xpp)
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readBool(xpp: XmlPullParser, bDefault: Boolean): Boolean {
        val str = readString(xpp)

        return when {
            str.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true) -> true
            str.equals(DatabaseKDBXXML.ValFalse, ignoreCase = true) -> false
            else -> bDefault
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readOptionalBool(xpp: XmlPullParser, bDefault: Boolean? = null): Boolean? {
        val str = readString(xpp)

        return when {
            str.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true) -> true
            str.equals(DatabaseKDBXXML.ValFalse, ignoreCase = true) -> false
            str.equals(DatabaseKDBXXML.ValNull, ignoreCase = true) -> null
            else -> bDefault
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readUuid(xpp: XmlPullParser): UUID {
        val encoded = readString(xpp)

        if (encoded.isEmpty()) {
            return DatabaseVersioned.UUID_ZERO
        }

        return try {
            val buf = Base64.decode(encoded, BASE64_FLAG)
            bytes16ToUuid(buf)
        } catch (e: Exception) {
            Log.e(TAG, "Unable to read base 64 UUID, create a random one", e)
            UUID.randomUUID()
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readInt(xpp: XmlPullParser, default: Int): Int {
        return try {
            readString(xpp).toInt()
        } catch (e: Exception) {
            default
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readUInt(xpp: XmlPullParser, default: UnsignedInt): UnsignedInt {
        return try {
            UnsignedInt(readString(xpp).toInt())
        } catch (e: Exception) {
            default
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readLong(xpp: XmlPullParser, default: Long): Long {
        return try {
            readString(xpp).toLong()
        } catch (e: Exception) {
            default
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readULong(xpp: XmlPullParser, default: UnsignedLong): UnsignedLong {
        return try {
            UnsignedLong(readString(xpp).toLong())
        } catch (e: Exception) {
            default
        }
    }

    @Throws(XmlPullParserException::class, IOException::class)
    private fun readProtectedString(xpp: XmlPullParser): ProtectedString {
        val buf = readProtectedBase64String(xpp)

        if (buf != null) {
            try {
                return ProtectedString(true, String(buf, Charset.forName("UTF-8")))
            } catch (e: UnsupportedEncodingException) {
                e.printStackTrace()
                throw IOException(e.localizedMessage)
            }
        }

        return ProtectedString(false, readString(xpp))
    }

    @Throws(XmlPullParserException::class, IOException::class)
    private fun readBinary(xpp: XmlPullParser): BinaryData? {

        // Reference Id to a binary already present in binary pool
        val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
        // New id to a binary
        val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)

        return when {
            ref != null -> {
                xpp.next() // Consume end tag
                val id = Integer.parseInt(ref)
                // A ref is not necessarily an index in Database V3.1
                var binaryRetrieve = mDatabase.attachmentPool[id]
                // Create empty binary if not retrieved in pool
                if (binaryRetrieve == null) {
                    binaryRetrieve = mDatabase.buildNewBinaryAttachment(
                            smallSize = false,
                            compression = false,
                            protection = false,
                            binaryPoolId = id)
                }
                return binaryRetrieve
            }
            key != null -> {
                createBinary(key.toIntOrNull(), xpp)
            }
            else -> {
                // New binary to retrieve
                createBinary(null, xpp)
            }
        }
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryData? {
        var compressed = false
        var protected = true

        if (xpp.attributeCount > 0) {
            val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
            if (compress != null) {
                compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
            }

            val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
            if (protect != null) {
                protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
            }
        }

        val base64 = readString(xpp)
        if (base64.isEmpty())
            return null

        // Build the new binary and compress
        val binaryAttachment = mDatabase.buildNewBinaryAttachment(
                isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
        try {
            binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
                outputStream.write(Base64.decode(base64, BASE64_FLAG))
            }
        } catch (e: Exception) {
            Log.e(TAG, "Unable to read base 64 attachment", e)
            binaryAttachment.isCorrupted = true
            binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
                outputStream.write(base64.toByteArray())
            }
        }
        return binaryAttachment
    }

    @Throws(IOException::class, XmlPullParserException::class)
    private fun readString(xpp: XmlPullParser): String {
        val buf = readProtectedBase64String(xpp)

        if (buf != null) {
            try {
                return String(buf, Charset.forName("UTF-8"))
            } catch (e: UnsupportedEncodingException) {
                throw IOException(e)
            }
        }

        return xpp.safeNextText()
    }


    @Throws(XmlPullParserException::class, IOException::class)
    private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? {
        if (xpp.attributeCount > 0) {
            val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
            if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
                Base64.decode(xpp.safeNextText(), BASE64_FLAG)?.let { data ->
                    return randomStream?.processBytes(data)
                }
                return ByteArray(0)
            }
        }
        return null
    }

    @Throws(XmlPullParserException::class, IOException::class)
    private fun switchContext(ctxCurrent: KdbContext, ctxNew: KdbContext,
                              xpp: XmlPullParser): KdbContext {

        if (xpp.isEmptyElementTag) {
            xpp.next()  // Consume the end tag
            return ctxCurrent
        }
        return ctxNew
    }

    companion object {

        private val TAG = DatabaseInputKDBX::class.java.name

        private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
    }

}

@Throws(IOException::class, XmlPullParserException::class)
fun XmlPullParser.safeNextText(): String {
    val result = nextText()
    if (eventType != XmlPullParser.END_TAG) {
        nextTag()
    }
    return result
}
