/*
 * Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
 *
 * This program 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 2 or (at your option)
 * version 3 of the License.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "KdbxXmlReader.h"
#include "KeePass2RandomStream.h"
#include "core/Clock.h"
#include "core/DatabaseIcons.h"
#include "core/Endian.h"
#include "core/Entry.h"
#include "core/Global.h"
#include "core/Group.h"
#include "core/Tools.h"
#include "streams/QtIOCompressor"

#include <QBuffer>
#include <QFile>
#include <utility>

#define UUID_LENGTH 16

/**
 * @param version KDBX version
 */
KdbxXmlReader::KdbxXmlReader(quint32 version)
    : m_kdbxVersion(version)
{
}

/**
 * @param version KDBX version
 * @param binaryPool binary pool
 */
KdbxXmlReader::KdbxXmlReader(quint32 version, QHash<QString, QByteArray> binaryPool)
    : m_kdbxVersion(version)
    , m_binaryPool(std::move(binaryPool))
{
}

/**
 * Read XML contents from a file into a new database.
 *
 * @param device input file
 * @return pointer to the new database
 */
QSharedPointer<Database> KdbxXmlReader::readDatabase(const QString& filename)
{
    QFile file(filename);
    file.open(QIODevice::ReadOnly);
    return readDatabase(&file);
}

/**
 * Read XML stream from a device into a new database.
 *
 * @param device input device
 * @return pointer to the new database
 */
QSharedPointer<Database> KdbxXmlReader::readDatabase(QIODevice* device)
{
    auto db = QSharedPointer<Database>::create();
    readDatabase(device, db.data());
    return db;
}

/**
 * Read XML contents from a device into a given database using a \link KeePass2RandomStream.
 *
 * @param device input device
 * @param db database to read into
 * @param randomStream random stream to use for decryption
 */
void KdbxXmlReader::readDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream)
{
    m_error = false;
    m_errorStr.clear();

    m_xml.clear();
    m_xml.setDevice(device);

    m_db = db;
    m_meta = m_db->metadata();
    m_meta->setUpdateDatetime(false);

    m_randomStream = randomStream;
    m_headerHash.clear();

    m_tmpParent.reset(new Group());

    bool rootGroupParsed = false;

    if (m_xml.hasError()) {
        raiseError(tr("XML parsing failure: %1").arg(m_xml.error()));
        return;
    }

    if (m_xml.readNextStartElement() && m_xml.name() == "KeePassFile") {
        rootGroupParsed = parseKeePassFile();
    }

    if (!rootGroupParsed) {
        raiseError(tr("No root group"));
        return;
    }

    if (!m_tmpParent->children().isEmpty()) {
        qWarning("KdbxXmlReader::readDatabase: found %d invalid group reference(s)", m_tmpParent->children().size());
    }

    if (!m_tmpParent->entries().isEmpty()) {
        qWarning("KdbxXmlReader::readDatabase: found %d invalid entry reference(s)", m_tmpParent->children().size());
    }

    const QSet<QString> poolKeys = asConst(m_binaryPool).keys().toSet();
    const QSet<QString> entryKeys = asConst(m_binaryMap).keys().toSet();
    const QSet<QString> unmappedKeys = entryKeys - poolKeys;
    const QSet<QString> unusedKeys = poolKeys - entryKeys;

    if (!unmappedKeys.isEmpty()) {
        qWarning("Unmapped keys left.");
    }

    for (const QString& key : unusedKeys) {
        qWarning("KdbxXmlReader::readDatabase: found unused key \"%s\"", qPrintable(key));
    }

    QHash<QString, QPair<Entry*, QString>>::const_iterator i;
    for (i = m_binaryMap.constBegin(); i != m_binaryMap.constEnd(); ++i) {
        const QPair<Entry*, QString>& target = i.value();
        target.first->attachments()->set(target.second, m_binaryPool[i.key()]);
    }

    m_meta->setUpdateDatetime(true);

    QHash<QUuid, Group*>::const_iterator iGroup;
    for (iGroup = m_groups.constBegin(); iGroup != m_groups.constEnd(); ++iGroup) {
        iGroup.value()->setUpdateTimeinfo(true);
    }

    QHash<QUuid, Entry*>::const_iterator iEntry;
    for (iEntry = m_entries.constBegin(); iEntry != m_entries.constEnd(); ++iEntry) {
        iEntry.value()->setUpdateTimeinfo(true);

        const QList<Entry*> historyItems = iEntry.value()->historyItems();
        for (Entry* histEntry : historyItems) {
            histEntry->setUpdateTimeinfo(true);
        }
    }
}

bool KdbxXmlReader::strictMode() const
{
    return m_strictMode;
}

void KdbxXmlReader::setStrictMode(bool strictMode)
{
    m_strictMode = strictMode;
}

bool KdbxXmlReader::hasError() const
{
    return m_error || m_xml.hasError();
}

QString KdbxXmlReader::errorString() const
{
    if (m_error) {
        return m_errorStr;
    }
    if (m_xml.hasError()) {
        return tr("XML error:\n%1\nLine %2, column %3")
            .arg(m_xml.errorString())
            .arg(m_xml.lineNumber())
            .arg(m_xml.columnNumber());
    }
    return QString();
}

bool KdbxXmlReader::isTrueValue(const QStringRef& value)
{
    return value.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0 || value == "1";
}

void KdbxXmlReader::raiseError(const QString& errorMessage)
{
    m_error = true;
    m_errorStr = errorMessage;
}

QByteArray KdbxXmlReader::headerHash() const
{
    return m_headerHash;
}

bool KdbxXmlReader::parseKeePassFile()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "KeePassFile");

    bool rootElementFound = false;
    bool rootParsedSuccessfully = false;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Meta") {
            parseMeta();
            continue;
        }

        if (m_xml.name() == "Root") {
            if (rootElementFound) {
                rootParsedSuccessfully = false;
                qWarning("Multiple root elements");
            } else {
                rootParsedSuccessfully = parseRoot();
                rootElementFound = true;
            }
            continue;
        }

        skipCurrentElement();
    }

    return rootParsedSuccessfully;
}

void KdbxXmlReader::parseMeta()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Meta");

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Generator") {
            m_meta->setGenerator(readString());
        } else if (m_xml.name() == "HeaderHash") {
            m_headerHash = readBinary();
        } else if (m_xml.name() == "DatabaseName") {
            m_meta->setName(readString());
        } else if (m_xml.name() == "DatabaseNameChanged") {
            m_meta->setNameChanged(readDateTime());
        } else if (m_xml.name() == "DatabaseDescription") {
            m_meta->setDescription(readString());
        } else if (m_xml.name() == "DatabaseDescriptionChanged") {
            m_meta->setDescriptionChanged(readDateTime());
        } else if (m_xml.name() == "DefaultUserName") {
            m_meta->setDefaultUserName(readString());
        } else if (m_xml.name() == "DefaultUserNameChanged") {
            m_meta->setDefaultUserNameChanged(readDateTime());
        } else if (m_xml.name() == "MaintenanceHistoryDays") {
            m_meta->setMaintenanceHistoryDays(readNumber());
        } else if (m_xml.name() == "Color") {
            m_meta->setColor(readColor());
        } else if (m_xml.name() == "MasterKeyChanged") {
            m_meta->setDatabaseKeyChanged(readDateTime());
        } else if (m_xml.name() == "MasterKeyChangeRec") {
            m_meta->setMasterKeyChangeRec(readNumber());
        } else if (m_xml.name() == "MasterKeyChangeForce") {
            m_meta->setMasterKeyChangeForce(readNumber());
        } else if (m_xml.name() == "MemoryProtection") {
            parseMemoryProtection();
        } else if (m_xml.name() == "CustomIcons") {
            parseCustomIcons();
        } else if (m_xml.name() == "RecycleBinEnabled") {
            m_meta->setRecycleBinEnabled(readBool());
        } else if (m_xml.name() == "RecycleBinUUID") {
            m_meta->setRecycleBin(getGroup(readUuid()));
        } else if (m_xml.name() == "RecycleBinChanged") {
            m_meta->setRecycleBinChanged(readDateTime());
        } else if (m_xml.name() == "EntryTemplatesGroup") {
            m_meta->setEntryTemplatesGroup(getGroup(readUuid()));
        } else if (m_xml.name() == "EntryTemplatesGroupChanged") {
            m_meta->setEntryTemplatesGroupChanged(readDateTime());
        } else if (m_xml.name() == "LastSelectedGroup") {
            m_meta->setLastSelectedGroup(getGroup(readUuid()));
        } else if (m_xml.name() == "LastTopVisibleGroup") {
            m_meta->setLastTopVisibleGroup(getGroup(readUuid()));
        } else if (m_xml.name() == "HistoryMaxItems") {
            int value = readNumber();
            if (value >= -1) {
                m_meta->setHistoryMaxItems(value);
            } else {
                qWarning("HistoryMaxItems invalid number");
            }
        } else if (m_xml.name() == "HistoryMaxSize") {
            int value = readNumber();
            if (value >= -1) {
                m_meta->setHistoryMaxSize(value);
            } else {
                qWarning("HistoryMaxSize invalid number");
            }
        } else if (m_xml.name() == "Binaries") {
            parseBinaries();
        } else if (m_xml.name() == "CustomData") {
            parseCustomData(m_meta->customData());
        } else if (m_xml.name() == "SettingsChanged") {
            m_meta->setSettingsChanged(readDateTime());
        } else {
            skipCurrentElement();
        }
    }
}

void KdbxXmlReader::parseMemoryProtection()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "MemoryProtection");

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "ProtectTitle") {
            m_meta->setProtectTitle(readBool());
        } else if (m_xml.name() == "ProtectUserName") {
            m_meta->setProtectUsername(readBool());
        } else if (m_xml.name() == "ProtectPassword") {
            m_meta->setProtectPassword(readBool());
        } else if (m_xml.name() == "ProtectURL") {
            m_meta->setProtectUrl(readBool());
        } else if (m_xml.name() == "ProtectNotes") {
            m_meta->setProtectNotes(readBool());
        } else {
            skipCurrentElement();
        }
    }
}

void KdbxXmlReader::parseCustomIcons()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomIcons");

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Icon") {
            parseIcon();
        } else {
            skipCurrentElement();
        }
    }
}

void KdbxXmlReader::parseIcon()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Icon");

    QUuid uuid;
    QImage icon;
    bool uuidSet = false;
    bool iconSet = false;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "UUID") {
            uuid = readUuid();
            uuidSet = !uuid.isNull();
        } else if (m_xml.name() == "Data") {
            icon.loadFromData(readBinary());
            iconSet = true;
        } else {
            skipCurrentElement();
        }
    }

    if (uuidSet && iconSet) {
        // Check for duplicate UUID (corruption)
        if (m_meta->hasCustomIcon(uuid)) {
            uuid = QUuid::createUuid();
        }
        m_meta->addCustomIcon(uuid, icon);
        return;
    }

    raiseError(tr("Missing icon uuid or data"));
}

void KdbxXmlReader::parseBinaries()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binaries");

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() != "Binary") {
            skipCurrentElement();
            continue;
        }

        QXmlStreamAttributes attr = m_xml.attributes();
        QString id = attr.value("ID").toString();
        QByteArray data = isTrueValue(attr.value("Compressed")) ? readCompressedBinary() : readBinary();

        if (m_binaryPool.contains(id)) {
            qWarning("KdbxXmlReader::parseBinaries: overwriting binary item \"%s\"", qPrintable(id));
        }

        m_binaryPool.insert(id, data);
    }
}

void KdbxXmlReader::parseCustomData(CustomData* customData)
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomData");

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Item") {
            parseCustomDataItem(customData);
            continue;
        }
        skipCurrentElement();
    }
}

void KdbxXmlReader::parseCustomDataItem(CustomData* customData)
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Item");

    QString key;
    QString value;
    bool keySet = false;
    bool valueSet = false;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Key") {
            key = readString();
            keySet = true;
        } else if (m_xml.name() == "Value") {
            value = readString();
            valueSet = true;
        } else {
            skipCurrentElement();
        }
    }

    if (keySet && valueSet) {
        customData->set(key, value);
        return;
    }

    raiseError(tr("Missing custom data key or value"));
}

bool KdbxXmlReader::parseRoot()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Root");

    bool groupElementFound = false;
    bool groupParsedSuccessfully = false;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Group") {
            if (groupElementFound) {
                groupParsedSuccessfully = false;
                raiseError(tr("Multiple group elements"));
                continue;
            }

            Group* rootGroup = parseGroup();
            if (rootGroup) {
                Group* oldRoot = m_db->rootGroup();
                m_db->setRootGroup(rootGroup);
                delete oldRoot;
                groupParsedSuccessfully = true;
            }

            groupElementFound = true;
        } else if (m_xml.name() == "DeletedObjects") {
            parseDeletedObjects();
        } else {
            skipCurrentElement();
        }
    }

    return groupParsedSuccessfully;
}

Group* KdbxXmlReader::parseGroup()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Group");

    auto group = new Group();
    group->setUpdateTimeinfo(false);
    QList<Group*> children;
    QList<Entry*> entries;
    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "UUID") {
            QUuid uuid = readUuid();
            if (uuid.isNull()) {
                if (m_strictMode) {
                    raiseError(tr("Null group uuid"));
                } else {
                    group->setUuid(QUuid::createUuid());
                }
            } else {
                group->setUuid(uuid);
            }
            continue;
        }
        if (m_xml.name() == "Name") {
            group->setName(readString());
            continue;
        }
        if (m_xml.name() == "Notes") {
            group->setNotes(readString());
            continue;
        }
        if (m_xml.name() == "IconID") {
            int iconId = readNumber();
            if (iconId < 0) {
                if (m_strictMode) {
                    raiseError(tr("Invalid group icon number"));
                }
                iconId = 0;
            } else if (iconId >= databaseIcons()->count()) {
                qWarning("KdbxXmlReader::parseGroup: icon id \"%d\" not supported", iconId);
                iconId = databaseIcons()->count() - 1;
            }

            group->setIcon(iconId);
            continue;
        }
        if (m_xml.name() == "CustomIconUUID") {
            QUuid uuid = readUuid();
            if (!uuid.isNull()) {
                group->setIcon(uuid);
            }
            continue;
        }
        if (m_xml.name() == "Times") {
            group->setTimeInfo(parseTimes());
            continue;
        }
        if (m_xml.name() == "IsExpanded") {
            group->setExpanded(readBool());
            continue;
        }
        if (m_xml.name() == "DefaultAutoTypeSequence") {
            group->setDefaultAutoTypeSequence(readString());
            continue;
        }
        if (m_xml.name() == "EnableAutoType") {
            QString str = readString();

            if (str.compare("null", Qt::CaseInsensitive) == 0) {
                group->setAutoTypeEnabled(Group::Inherit);
            } else if (str.compare("true", Qt::CaseInsensitive) == 0) {
                group->setAutoTypeEnabled(Group::Enable);
            } else if (str.compare("false", Qt::CaseInsensitive) == 0) {
                group->setAutoTypeEnabled(Group::Disable);
            } else {
                raiseError(tr("Invalid EnableAutoType value"));
            }
            continue;
        }
        if (m_xml.name() == "EnableSearching") {
            QString str = readString();

            if (str.compare("null", Qt::CaseInsensitive) == 0) {
                group->setSearchingEnabled(Group::Inherit);
            } else if (str.compare("true", Qt::CaseInsensitive) == 0) {
                group->setSearchingEnabled(Group::Enable);
            } else if (str.compare("false", Qt::CaseInsensitive) == 0) {
                group->setSearchingEnabled(Group::Disable);
            } else {
                raiseError(tr("Invalid EnableSearching value"));
            }
            continue;
        }
        if (m_xml.name() == "LastTopVisibleEntry") {
            group->setLastTopVisibleEntry(getEntry(readUuid()));
            continue;
        }
        if (m_xml.name() == "Group") {
            Group* newGroup = parseGroup();
            if (newGroup) {
                children.append(newGroup);
            }
            continue;
        }
        if (m_xml.name() == "Entry") {
            Entry* newEntry = parseEntry(false);
            if (newEntry) {
                entries.append(newEntry);
            }
            continue;
        }
        if (m_xml.name() == "CustomData") {
            parseCustomData(group->customData());
            continue;
        }

        skipCurrentElement();
    }

    if (group->uuid().isNull() && !m_strictMode) {
        group->setUuid(QUuid::createUuid());
    }

    if (!group->uuid().isNull()) {
        Group* tmpGroup = group;
        group = getGroup(tmpGroup->uuid());
        group->copyDataFrom(tmpGroup);
        group->setUpdateTimeinfo(false);
        delete tmpGroup;
    } else if (!hasError()) {
        raiseError(tr("No group uuid found"));
    }

    for (Group* child : asConst(children)) {
        child->setParent(group);
    }

    for (Entry* entry : asConst(entries)) {
        entry->setGroup(group);
    }

    return group;
}

void KdbxXmlReader::parseDeletedObjects()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObjects");

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "DeletedObject") {
            parseDeletedObject();
        } else {
            skipCurrentElement();
        }
    }
}

void KdbxXmlReader::parseDeletedObject()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObject");

    DeletedObject delObj{{}, {}};

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "UUID") {
            QUuid uuid = readUuid();
            if (uuid.isNull()) {
                if (m_strictMode) {
                    raiseError(tr("Null DeleteObject uuid"));
                    return;
                }
                continue;
            }
            delObj.uuid = uuid;
            continue;
        }
        if (m_xml.name() == "DeletionTime") {
            delObj.deletionTime = readDateTime();
            continue;
        }
        skipCurrentElement();
    }

    if (!delObj.uuid.isNull() && !delObj.deletionTime.isNull()) {
        m_db->addDeletedObject(delObj);
        return;
    }

    if (m_strictMode) {
        raiseError(tr("Missing DeletedObject uuid or time"));
    }
}

Entry* KdbxXmlReader::parseEntry(bool history)
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Entry");

    auto entry = new Entry();
    entry->setUpdateTimeinfo(false);
    QList<Entry*> historyItems;
    QList<StringPair> binaryRefs;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "UUID") {
            QUuid uuid = readUuid();
            if (uuid.isNull()) {
                if (m_strictMode) {
                    raiseError(tr("Null entry uuid"));
                } else {
                    entry->setUuid(QUuid::createUuid());
                }
            } else {
                entry->setUuid(uuid);
            }
            continue;
        }
        if (m_xml.name() == "IconID") {
            int iconId = readNumber();
            if (iconId < 0) {
                if (m_strictMode) {
                    raiseError(tr("Invalid entry icon number"));
                }
                iconId = 0;
            }
            entry->setIcon(iconId);
            continue;
        }
        if (m_xml.name() == "CustomIconUUID") {
            QUuid uuid = readUuid();
            if (!uuid.isNull()) {
                entry->setIcon(uuid);
            }
            continue;
        }
        if (m_xml.name() == "ForegroundColor") {
            entry->setForegroundColor(readColor());
            continue;
        }
        if (m_xml.name() == "BackgroundColor") {
            entry->setBackgroundColor(readColor());
            continue;
        }
        if (m_xml.name() == "OverrideURL") {
            entry->setOverrideUrl(readString());
            continue;
        }
        if (m_xml.name() == "Tags") {
            entry->setTags(readString());
            continue;
        }
        if (m_xml.name() == "Times") {
            entry->setTimeInfo(parseTimes());
            continue;
        }
        if (m_xml.name() == "String") {
            parseEntryString(entry);
            continue;
        }
        if (m_xml.name() == "Binary") {
            QPair<QString, QString> ref = parseEntryBinary(entry);
            if (!ref.first.isEmpty() && !ref.second.isEmpty()) {
                binaryRefs.append(ref);
            }
            continue;
        }
        if (m_xml.name() == "AutoType") {
            parseAutoType(entry);
            continue;
        }
        if (m_xml.name() == "History") {
            if (history) {
                raiseError(tr("History element in history entry"));
            } else {
                historyItems = parseEntryHistory();
            }
            continue;
        }
        if (m_xml.name() == "CustomData") {
            parseCustomData(entry->customData());
            continue;
        }
        skipCurrentElement();
    }

    if (entry->uuid().isNull() && !m_strictMode) {
        entry->setUuid(QUuid::createUuid());
    }

    if (!entry->uuid().isNull()) {
        if (history) {
            entry->setUpdateTimeinfo(false);
        } else {
            Entry* tmpEntry = entry;

            entry = getEntry(tmpEntry->uuid());
            entry->copyDataFrom(tmpEntry);
            entry->setUpdateTimeinfo(false);

            delete tmpEntry;
        }
    } else if (!hasError()) {
        raiseError(tr("No entry uuid found"));
    }

    for (Entry* historyItem : asConst(historyItems)) {
        if (historyItem->uuid() != entry->uuid()) {
            if (m_strictMode) {
                raiseError(tr("History element with different uuid"));
            } else {
                historyItem->setUuid(entry->uuid());
            }
        }
        entry->addHistoryItem(historyItem);
    }

    for (const StringPair& ref : asConst(binaryRefs)) {
        m_binaryMap.insertMulti(ref.first, qMakePair(entry, ref.second));
    }

    return entry;
}

void KdbxXmlReader::parseEntryString(Entry* entry)
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "String");

    QString key;
    QString value;
    bool protect = false;
    bool keySet = false;
    bool valueSet = false;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Key") {
            key = readString();
            keySet = true;
            continue;
        }

        if (m_xml.name() == "Value") {
            QXmlStreamAttributes attr = m_xml.attributes();
            bool isProtected;
            bool protectInMemory;
            value = readString(isProtected, protectInMemory);
            protect = isProtected || protectInMemory;
            valueSet = true;
            continue;
        }

        skipCurrentElement();
    }

    if (keySet && valueSet) {
        // the default attributes are always there so additionally check if it's empty
        if (entry->attributes()->hasKey(key) && !entry->attributes()->value(key).isEmpty()) {
            raiseError(tr("Duplicate custom attribute found"));
            return;
        }
        entry->attributes()->set(key, value, protect);
        return;
    }

    raiseError(tr("Entry string key or value missing"));
}

QPair<QString, QString> KdbxXmlReader::parseEntryBinary(Entry* entry)
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binary");

    QPair<QString, QString> poolRef;

    QString key;
    QByteArray value;
    bool keySet = false;
    bool valueSet = false;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Key") {
            key = readString();
            keySet = true;
            continue;
        }
        if (m_xml.name() == "Value") {
            QXmlStreamAttributes attr = m_xml.attributes();

            if (attr.hasAttribute("Ref")) {
                poolRef = qMakePair(attr.value("Ref").toString(), key);
                m_xml.skipCurrentElement();
            } else {
                // format compatibility
                value = readBinary();
            }

            valueSet = true;
            continue;
        }
        skipCurrentElement();
    }

    if (keySet && valueSet) {
        if (entry->attachments()->hasKey(key) && entry->attachments()->value(key) != value) {
            // NOTE: This only impacts KDBX 3.x databases
            // Prepend a random string to the key to make it unique and prevent data loss
            key = key.prepend(QUuid::createUuid().toString().mid(1, 8) + "_");
            qWarning("Duplicate attachment name found, renamed to: %s", qPrintable(key));
        }
        entry->attachments()->set(key, value);
    } else {
        raiseError(tr("Entry binary key or value missing"));
    }

    return poolRef;
}

void KdbxXmlReader::parseAutoType(Entry* entry)
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "AutoType");

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Enabled") {
            entry->setAutoTypeEnabled(readBool());
        } else if (m_xml.name() == "DataTransferObfuscation") {
            entry->setAutoTypeObfuscation(readNumber());
        } else if (m_xml.name() == "DefaultSequence") {
            entry->setDefaultAutoTypeSequence(readString());
        } else if (m_xml.name() == "Association") {
            parseAutoTypeAssoc(entry);
        } else {
            skipCurrentElement();
        }
    }
}

void KdbxXmlReader::parseAutoTypeAssoc(Entry* entry)
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Association");

    AutoTypeAssociations::Association assoc;
    bool windowSet = false;
    bool sequenceSet = false;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Window") {
            assoc.window = readString();
            windowSet = true;
        } else if (m_xml.name() == "KeystrokeSequence") {
            assoc.sequence = readString();
            sequenceSet = true;
        } else {
            skipCurrentElement();
        }
    }

    if (windowSet && sequenceSet) {
        entry->autoTypeAssociations()->add(assoc);
        return;
    }
    raiseError(tr("Auto-type association window or sequence missing"));
}

QList<Entry*> KdbxXmlReader::parseEntryHistory()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "History");

    QList<Entry*> historyItems;

    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "Entry") {
            historyItems.append(parseEntry(true));
        } else {
            skipCurrentElement();
        }
    }

    return historyItems;
}

TimeInfo KdbxXmlReader::parseTimes()
{
    Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Times");

    TimeInfo timeInfo;
    while (!m_xml.hasError() && m_xml.readNextStartElement()) {
        if (m_xml.name() == "LastModificationTime") {
            timeInfo.setLastModificationTime(readDateTime());
        } else if (m_xml.name() == "CreationTime") {
            timeInfo.setCreationTime(readDateTime());
        } else if (m_xml.name() == "LastAccessTime") {
            timeInfo.setLastAccessTime(readDateTime());
        } else if (m_xml.name() == "ExpiryTime") {
            timeInfo.setExpiryTime(readDateTime());
        } else if (m_xml.name() == "Expires") {
            timeInfo.setExpires(readBool());
        } else if (m_xml.name() == "UsageCount") {
            timeInfo.setUsageCount(readNumber());
        } else if (m_xml.name() == "LocationChanged") {
            timeInfo.setLocationChanged(readDateTime());
        } else {
            skipCurrentElement();
        }
    }

    return timeInfo;
}

QString KdbxXmlReader::readString()
{
    bool isProtected;
    bool protectInMemory;

    return readString(isProtected, protectInMemory);
}

QString KdbxXmlReader::readString(bool& isProtected, bool& protectInMemory)
{
    QXmlStreamAttributes attr = m_xml.attributes();
    isProtected = isTrueValue(attr.value("Protected"));
    protectInMemory = isTrueValue(attr.value("ProtectInMemory"));
    QString value = m_xml.readElementText();

    if (isProtected && !value.isEmpty()) {
        QByteArray ciphertext = QByteArray::fromBase64(value.toLatin1());
        bool ok;
        QByteArray plaintext = m_randomStream->process(ciphertext, &ok);
        if (!ok) {
            value.clear();
            raiseError(m_randomStream->errorString());
            return value;
        }

        value = QString::fromUtf8(plaintext);
    }

    return value;
}

bool KdbxXmlReader::readBool()
{
    QString str = readString();

    if (str.compare("true", Qt::CaseInsensitive) == 0) {
        return true;
    }
    if (str.compare("false", Qt::CaseInsensitive) == 0) {
        return false;
    }
    if (str.length() == 0) {
        return false;
    }
    raiseError(tr("Invalid bool value"));
    return false;
}

QDateTime KdbxXmlReader::readDateTime()
{
    QString str = readString();
    if (Tools::isBase64(str.toLatin1())) {
        QByteArray secsBytes = QByteArray::fromBase64(str.toUtf8()).leftJustified(8, '\0', true).left(8);
        qint64 secs = Endian::bytesToSizedInt<quint64>(secsBytes, KeePass2::BYTEORDER);
        return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs);
    }

    QDateTime dt = Clock::parse(str, Qt::ISODate);
    if (dt.isValid()) {
        return dt;
    }

    if (m_strictMode) {
        raiseError(tr("Invalid date time value"));
    }

    return Clock::currentDateTimeUtc();
}

QString KdbxXmlReader::readColor()
{
    QString colorStr = readString();

    if (colorStr.isEmpty()) {
        return colorStr;
    }

    if (colorStr.length() != 7 || colorStr[0] != '#') {
        if (m_strictMode) {
            raiseError(tr("Invalid color value"));
        }
        return colorStr;
    }

    for (int i = 0; i <= 2; ++i) {
        QString rgbPartStr = colorStr.mid(1 + 2 * i, 2);
        bool ok;
        int rgbPart = rgbPartStr.toInt(&ok, 16);
        if (!ok || rgbPart > 255) {
            if (m_strictMode) {
                raiseError(tr("Invalid color rgb part"));
            }
            return colorStr;
        }
    }

    return colorStr;
}

int KdbxXmlReader::readNumber()
{
    bool ok;
    int result = readString().toInt(&ok);
    if (!ok) {
        raiseError(tr("Invalid number value"));
    }
    return result;
}

QUuid KdbxXmlReader::readUuid()
{
    QByteArray uuidBin = readBinary();
    if (uuidBin.isEmpty()) {
        return QUuid();
    }
    if (uuidBin.length() != UUID_LENGTH) {
        if (m_strictMode) {
            raiseError(tr("Invalid uuid value"));
        }
        return QUuid();
    }
    return QUuid::fromRfc4122(uuidBin);
}

QByteArray KdbxXmlReader::readBinary()
{
    QXmlStreamAttributes attr = m_xml.attributes();
    bool isProtected = isTrueValue(attr.value("Protected"));
    QString value = m_xml.readElementText();
    QByteArray data = QByteArray::fromBase64(value.toLatin1());

    if (isProtected && !data.isEmpty()) {
        bool ok;
        QByteArray plaintext = m_randomStream->process(data, &ok);
        if (!ok) {
            data.clear();
            raiseError(m_randomStream->errorString());
            return data;
        }

        data = plaintext;
    }

    return data;
}

QByteArray KdbxXmlReader::readCompressedBinary()
{
    QByteArray rawData = readBinary();

    QBuffer buffer(&rawData);
    buffer.open(QIODevice::ReadOnly);

    QtIOCompressor compressor(&buffer);
    compressor.setStreamFormat(QtIOCompressor::GzipFormat);
    compressor.open(QIODevice::ReadOnly);

    QByteArray result;
    if (!Tools::readAllFromDevice(&compressor, result)) {
        //: Translator meant is a binary data inside an entry
        raiseError(tr("Unable to decompress binary"));
    }
    return result;
}

Group* KdbxXmlReader::getGroup(const QUuid& uuid)
{
    if (uuid.isNull()) {
        return nullptr;
    }

    if (m_groups.contains(uuid)) {
        return m_groups.value(uuid);
    }

    auto group = new Group();
    group->setUpdateTimeinfo(false);
    group->setUuid(uuid);
    group->setParent(m_tmpParent.data());
    m_groups.insert(uuid, group);
    return group;
}

Entry* KdbxXmlReader::getEntry(const QUuid& uuid)
{
    if (uuid.isNull()) {
        return nullptr;
    }

    if (m_entries.contains(uuid)) {
        return m_entries.value(uuid);
    }

    auto entry = new Entry();
    entry->setUpdateTimeinfo(false);
    entry->setUuid(uuid);
    entry->setGroup(m_tmpParent.data());
    m_entries.insert(uuid, entry);
    return entry;
}

void KdbxXmlReader::skipCurrentElement()
{
    qWarning("KdbxXmlReader::skipCurrentElement: skip element \"%s\"", qPrintable(m_xml.name().toString()));
    m_xml.skipCurrentElement();
}
