From c51098e2cfe314b311a613d10dfaf17d1bcefbf7 Mon Sep 17 00:00:00 2001 From: Florian Geyer Date: Wed, 16 Dec 2015 21:29:09 +0100 Subject: [PATCH 1/8] Flush temporary file before opening attachment. Closes #390 --- src/gui/entry/EditEntryWidget.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index beeda2ab8..cd4d51ce4 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -671,6 +671,14 @@ void EditEntryWidget::openAttachment(const QModelIndex& index) return; } + if (!file->flush()) { + MessageBox::warning(this, tr("Error"), + tr("Unable to save the attachment:\n").append(file->errorString())); + return; + } + + file->close(); + QDesktopServices::openUrl(QUrl::fromLocalFile(file->fileName())); } From 5e6b17aba4d75ef1fabda9bb8d1b34c3679ccfb3 Mon Sep 17 00:00:00 2001 From: Florian Geyer Date: Fri, 22 Jan 2016 22:37:07 +0100 Subject: [PATCH 2/8] Disable password generator button when showing entry in history mode. Closes #422 --- src/gui/entry/EditEntryWidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index cd4d51ce4..8cad92462 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -294,6 +294,7 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_mainUi->expireDatePicker->setReadOnly(m_history); m_mainUi->notesEdit->setReadOnly(m_history); m_mainUi->tooglePasswordGeneratorButton->setChecked(false); + m_mainUi->tooglePasswordGeneratorButton->setDisabled(m_history); m_mainUi->passwordGenerator->reset(); m_advancedUi->addAttachmentButton->setEnabled(!m_history); updateAttachmentButtonsEnabled(m_advancedUi->attachmentsView->currentIndex()); From 2d741afe3e025442ecbb37e1d3b8adbb9b09994c Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Sun, 24 Jan 2016 17:20:16 +0100 Subject: [PATCH 3/8] Strip invalid XML chars when writing databases. These characters are unprintable or just plain invalid. QXmlStreamReader throws and error when reading XML documents with such chars. Closes #392 --- src/format/KeePass2XmlWriter.cpp | 21 +++++++++++++++++++-- src/format/KeePass2XmlWriter.h | 1 + tests/TestKeePass2XmlReader.cpp | 31 +++++++++++++++++++++++++++++++ tests/TestKeePass2XmlReader.h | 1 + 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/format/KeePass2XmlWriter.cpp b/src/format/KeePass2XmlWriter.cpp index 313607c24..0d83935a0 100644 --- a/src/format/KeePass2XmlWriter.cpp +++ b/src/format/KeePass2XmlWriter.cpp @@ -374,7 +374,7 @@ void KeePass2XmlWriter::writeEntry(const Entry* entry) } if (!value.isEmpty()) { - m_xml.writeCharacters(value); + m_xml.writeCharacters(stripInvalidXml10Chars(value)); } m_xml.writeEndElement(); @@ -445,7 +445,7 @@ void KeePass2XmlWriter::writeString(const QString& qualifiedName, const QString& m_xml.writeEmptyElement(qualifiedName); } else { - m_xml.writeTextElement(qualifiedName, string); + m_xml.writeTextElement(qualifiedName, stripInvalidXml10Chars(string)); } } @@ -549,6 +549,23 @@ QString KeePass2XmlWriter::colorPartToString(int value) return str; } +QString KeePass2XmlWriter::stripInvalidXml10Chars(QString str) +{ + for (int i = str.size() - 1; i >= 0; i--) { + const ushort uc = str.at(i).unicode(); + + if ((uc < 0x20 && uc != 0x09 && uc != 0x0A && uc != 0x0D) + || (uc > 0xD7FF && uc < 0xE000) + || (uc > 0xFFFD)) + { + qWarning("Stripping invalid XML 1.0 codepoint %x", uc); + str.remove(i, 1); + } + } + + return str; +} + void KeePass2XmlWriter::raiseError(const QString& errorMessage) { m_error = true; diff --git a/src/format/KeePass2XmlWriter.h b/src/format/KeePass2XmlWriter.h index ea6212448..48dde6af3 100644 --- a/src/format/KeePass2XmlWriter.h +++ b/src/format/KeePass2XmlWriter.h @@ -73,6 +73,7 @@ class KeePass2XmlWriter void writeColor(const QString& qualifiedName, const QColor& color); void writeTriState(const QString& qualifiedName, Group::TriState triState); QString colorPartToString(int value); + QString stripInvalidXml10Chars(QString str); void raiseError(const QString& errorMessage); diff --git a/tests/TestKeePass2XmlReader.cpp b/tests/TestKeePass2XmlReader.cpp index 81690009e..f8ca29614 100644 --- a/tests/TestKeePass2XmlReader.cpp +++ b/tests/TestKeePass2XmlReader.cpp @@ -17,6 +17,7 @@ #include "TestKeePass2XmlReader.h" +#include #include #include @@ -26,6 +27,7 @@ #include "core/Metadata.h" #include "crypto/Crypto.h" #include "format/KeePass2XmlReader.h" +#include "format/KeePass2XmlWriter.h" #include "config-keepassx-tests.h" QTEST_GUILESS_MAIN(TestKeePass2XmlReader) @@ -408,6 +410,35 @@ void TestKeePass2XmlReader::testEmptyUuids() QVERIFY(!reader.hasError()); } +void TestKeePass2XmlReader::testInvalidXmlChars() +{ + QScopedPointer dbWrite(new Database()); + + Entry* entry = new Entry(); + entry->setUuid(Uuid::random()); + entry->setNotes(QString("a %1 b %2 c %3").arg(QChar(0x02)).arg(QChar(0xD800)).arg(QChar(0xFFFE))); + entry->setGroup(dbWrite->rootGroup()); + + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + KeePass2XmlWriter writer; + writer.writeDatabase(&buffer, dbWrite.data()); + QVERIFY(!writer.hasError()); + buffer.seek(0); + + KeePass2XmlReader reader; + reader.setStrictMode(true); + QScopedPointer dbRead(reader.readDatabase(&buffer)); + if (reader.hasError()) { + qWarning("Database read error: %s", qPrintable(reader.errorString())); + } + QVERIFY(!reader.hasError()); + QVERIFY(!dbRead.isNull()); + QCOMPARE(dbRead->rootGroup()->entries().size(), 1); + // check that the invalid codepoints have been stripped + QCOMPARE(dbRead->rootGroup()->entries().first()->notes(), QString("a b c ")); +} + void TestKeePass2XmlReader::cleanupTestCase() { delete m_db; diff --git a/tests/TestKeePass2XmlReader.h b/tests/TestKeePass2XmlReader.h index 651ac5614..815dbcd65 100644 --- a/tests/TestKeePass2XmlReader.h +++ b/tests/TestKeePass2XmlReader.h @@ -42,6 +42,7 @@ private Q_SLOTS: void testBroken(); void testBroken_data(); void testEmptyUuids(); + void testInvalidXmlChars(); void cleanupTestCase(); private: From 4752adf9d3347f73cfedbec589bf3ffbe43a81c8 Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Sun, 24 Jan 2016 17:56:35 +0100 Subject: [PATCH 4/8] Move pixmap caching to Metadata. --- src/core/Entry.cpp | 15 +++++---------- src/core/Entry.h | 2 -- src/core/Group.cpp | 15 +++++---------- src/core/Group.h | 1 - src/core/Metadata.cpp | 22 ++++++++++++++++++++++ src/core/Metadata.h | 4 ++++ 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 4f977915b..0634e20b5 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -114,13 +114,12 @@ QPixmap Entry::iconPixmap() const else { Q_ASSERT(database()); - QPixmap pixmap; - if (database() && !QPixmapCache::find(m_pixmapCacheKey, &pixmap)) { - pixmap = QPixmap::fromImage(database()->metadata()->customIcon(m_data.customIcon)); - m_pixmapCacheKey = QPixmapCache::insert(pixmap); + if (database()) { + return database()->metadata()->customIconPixmap(m_data.customIcon); + } + else { + return QPixmap(); } - - return pixmap; } } @@ -248,8 +247,6 @@ void Entry::setIcon(int iconNumber) m_data.iconNumber = iconNumber; m_data.customIcon = Uuid(); - m_pixmapCacheKey = QPixmapCache::Key(); - Q_EMIT modified(); emitDataChanged(); } @@ -263,8 +260,6 @@ void Entry::setIcon(const Uuid& uuid) m_data.customIcon = uuid; m_data.iconNumber = 0; - m_pixmapCacheKey = QPixmapCache::Key(); - Q_EMIT modified(); emitDataChanged(); } diff --git a/src/core/Entry.h b/src/core/Entry.h index ae07ed453..4b3502f74 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -169,7 +168,6 @@ private Q_SLOTS: Entry* m_tmpHistoryItem; bool m_modifiedSinceBegin; QPointer m_group; - mutable QPixmapCache::Key m_pixmapCacheKey; bool m_updateTimeinfo; }; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 57e0bae95..ece40df74 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -134,13 +134,12 @@ QPixmap Group::iconPixmap() const else { Q_ASSERT(m_db); - QPixmap pixmap; - if (m_db && !QPixmapCache::find(m_pixmapCacheKey, &pixmap)) { - pixmap = QPixmap::fromImage(m_db->metadata()->customIcon(m_data.customIcon)); - m_pixmapCacheKey = QPixmapCache::insert(pixmap); + if (m_db) { + return m_db->metadata()->customIconPixmap(m_data.customIcon); + } + else { + return QPixmap(); } - - return pixmap; } } @@ -214,8 +213,6 @@ void Group::setIcon(int iconNumber) m_data.iconNumber = iconNumber; m_data.customIcon = Uuid(); - m_pixmapCacheKey = QPixmapCache::Key(); - updateTimeinfo(); Q_EMIT modified(); Q_EMIT dataChanged(this); @@ -230,8 +227,6 @@ void Group::setIcon(const Uuid& uuid) m_data.customIcon = uuid; m_data.iconNumber = 0; - m_pixmapCacheKey = QPixmapCache::Key(); - updateTimeinfo(); Q_EMIT modified(); Q_EMIT dataChanged(this); diff --git a/src/core/Group.h b/src/core/Group.h index 3d3618044..2bc20b7b0 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -155,7 +155,6 @@ class Group : public QObject QList m_entries; QPointer m_parent; - mutable QPixmapCache::Key m_pixmapCacheKey; bool m_updateTimeinfo; diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index 3256ca05e..73074e827 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -167,6 +167,24 @@ QImage Metadata::customIcon(const Uuid& uuid) const return m_customIcons.value(uuid); } +QPixmap Metadata::customIconPixmap(const Uuid& uuid) const +{ + QPixmap pixmap; + + if (!m_customIcons.contains(uuid)) { + return pixmap; + } + + QPixmapCache::Key& cacheKey = m_customIconCacheKeys[uuid]; + + if (!QPixmapCache::find(cacheKey, &pixmap)) { + pixmap = QPixmap::fromImage(m_customIcons.value(uuid)); + cacheKey = QPixmapCache::insert(pixmap); + } + + return pixmap; +} + bool Metadata::containsCustomIcon(const Uuid& uuid) const { return m_customIcons.contains(uuid); @@ -338,6 +356,8 @@ void Metadata::addCustomIcon(const Uuid& uuid, const QImage& icon) Q_ASSERT(!m_customIcons.contains(uuid)); m_customIcons.insert(uuid, icon); + // reset cache in case there is also an icon with that uuid + m_customIconCacheKeys[uuid] = QPixmapCache::Key(); m_customIconsOrder.append(uuid); Q_ASSERT(m_customIcons.count() == m_customIconsOrder.count()); Q_EMIT modified(); @@ -365,6 +385,8 @@ void Metadata::removeCustomIcon(const Uuid& uuid) Q_ASSERT(m_customIcons.contains(uuid)); m_customIcons.remove(uuid); + QPixmapCache::remove(m_customIconCacheKeys.value(uuid)); + m_customIconCacheKeys.remove(uuid); m_customIconsOrder.removeAll(uuid); Q_ASSERT(m_customIcons.count() == m_customIconsOrder.count()); Q_EMIT modified(); diff --git a/src/core/Metadata.h b/src/core/Metadata.h index 4164fb63e..a15e3e0e7 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include "core/Global.h" @@ -78,6 +80,7 @@ class Metadata : public QObject bool protectNotes() const; // bool autoEnableVisualHiding() const; QImage customIcon(const Uuid& uuid) const; + QPixmap customIconPixmap(const Uuid& uuid) const; bool containsCustomIcon(const Uuid& uuid) const; QHash customIcons() const; QList customIconsOrder() const; @@ -153,6 +156,7 @@ class Metadata : public QObject MetadataData m_data; QHash m_customIcons; + mutable QHash m_customIconCacheKeys; QList m_customIconsOrder; QPointer m_recycleBin; From 1f33e6f04448c912b7112373a0b43300ab65a57f Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Sun, 24 Jan 2016 18:42:27 +0100 Subject: [PATCH 5/8] Add Metadata::customIconScaledPixmap(). --- src/core/Metadata.cpp | 22 ++++++++++++++++++++++ src/core/Metadata.h | 2 ++ 2 files changed, 24 insertions(+) diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index 73074e827..c27c196a2 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -185,6 +185,25 @@ QPixmap Metadata::customIconPixmap(const Uuid& uuid) const return pixmap; } +QPixmap Metadata::customIconScaledPixmap(const Uuid& uuid) const +{ + QPixmap pixmap; + + if (!m_customIcons.contains(uuid)) { + return pixmap; + } + + QPixmapCache::Key& cacheKey = m_customIconScaledCacheKeys[uuid]; + + if (!QPixmapCache::find(cacheKey, &pixmap)) { + QImage image = m_customIcons.value(uuid).scaled(16, 16, Qt::KeepAspectRatio, Qt::SmoothTransformation); + pixmap = QPixmap::fromImage(image); + cacheKey = QPixmapCache::insert(pixmap); + } + + return pixmap; +} + bool Metadata::containsCustomIcon(const Uuid& uuid) const { return m_customIcons.contains(uuid); @@ -358,6 +377,7 @@ void Metadata::addCustomIcon(const Uuid& uuid, const QImage& icon) m_customIcons.insert(uuid, icon); // reset cache in case there is also an icon with that uuid m_customIconCacheKeys[uuid] = QPixmapCache::Key(); + m_customIconScaledCacheKeys[uuid] = QPixmapCache::Key(); m_customIconsOrder.append(uuid); Q_ASSERT(m_customIcons.count() == m_customIconsOrder.count()); Q_EMIT modified(); @@ -387,6 +407,8 @@ void Metadata::removeCustomIcon(const Uuid& uuid) m_customIcons.remove(uuid); QPixmapCache::remove(m_customIconCacheKeys.value(uuid)); m_customIconCacheKeys.remove(uuid); + QPixmapCache::remove(m_customIconScaledCacheKeys.value(uuid)); + m_customIconScaledCacheKeys.remove(uuid); m_customIconsOrder.removeAll(uuid); Q_ASSERT(m_customIcons.count() == m_customIconsOrder.count()); Q_EMIT modified(); diff --git a/src/core/Metadata.h b/src/core/Metadata.h index a15e3e0e7..b6e25c288 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -81,6 +81,7 @@ class Metadata : public QObject // bool autoEnableVisualHiding() const; QImage customIcon(const Uuid& uuid) const; QPixmap customIconPixmap(const Uuid& uuid) const; + QPixmap customIconScaledPixmap(const Uuid& uuid) const; bool containsCustomIcon(const Uuid& uuid) const; QHash customIcons() const; QList customIconsOrder() const; @@ -157,6 +158,7 @@ class Metadata : public QObject QHash m_customIcons; mutable QHash m_customIconCacheKeys; + mutable QHash m_customIconScaledCacheKeys; QList m_customIconsOrder; QPointer m_recycleBin; From 38245aa2a949a77aa68d2c14231d7e04cd879745 Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Sun, 24 Jan 2016 18:45:10 +0100 Subject: [PATCH 6/8] Add iconScaledPixmap() convenience functions. --- src/core/Entry.cpp | 13 +++++++++++++ src/core/Entry.h | 1 + src/core/Group.cpp | 18 ++++++++++++++++++ src/core/Group.h | 1 + 4 files changed, 33 insertions(+) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 0634e20b5..784b489cd 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -123,6 +123,19 @@ QPixmap Entry::iconPixmap() const } } +QPixmap Entry::iconScaledPixmap() const +{ + if (m_data.customIcon.isNull()) { + // built-in icons are 16x16 so don't need to be scaled + return databaseIcons()->iconPixmap(m_data.iconNumber); + } + else { + Q_ASSERT(database()); + + return database()->metadata()->customIconScaledPixmap(m_data.customIcon); + } +} + int Entry::iconNumber() const { return m_data.iconNumber; diff --git a/src/core/Entry.h b/src/core/Entry.h index 4b3502f74..3044dc81f 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -60,6 +60,7 @@ class Entry : public QObject Uuid uuid() const; QImage icon() const; QPixmap iconPixmap() const; + QPixmap iconScaledPixmap() const; int iconNumber() const; Uuid iconUuid() const; QColor foregroundColor() const; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index ece40df74..371f3e4d1 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -143,6 +143,24 @@ QPixmap Group::iconPixmap() const } } +QPixmap Group::iconScaledPixmap() const +{ + if (m_data.customIcon.isNull()) { + // built-in icons are 16x16 so don't need to be scaled + return databaseIcons()->iconPixmap(m_data.iconNumber); + } + else { + Q_ASSERT(m_db); + + if (m_db) { + return m_db->metadata()->customIconScaledPixmap(m_data.customIcon); + } + else { + return QPixmap(); + } + } +} + int Group::iconNumber() const { return m_data.iconNumber; diff --git a/src/core/Group.h b/src/core/Group.h index 2bc20b7b0..3881ed246 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -58,6 +58,7 @@ class Group : public QObject QString notes() const; QImage icon() const; QPixmap iconPixmap() const; + QPixmap iconScaledPixmap() const; int iconNumber() const; Uuid iconUuid() const; TimeInfo timeInfo() const; From 93585aded73b122107897a27246d58630c3c0ed3 Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Sun, 24 Jan 2016 19:03:50 +0100 Subject: [PATCH 7/8] Always display scaled custom icons. Closes #322 --- src/core/Metadata.cpp | 11 +++++++++++ src/core/Metadata.h | 1 + src/gui/EditWidgetIcons.cpp | 6 +++--- src/gui/IconModels.cpp | 2 +- src/gui/IconModels.h | 6 +++--- src/gui/entry/EntryModel.cpp | 4 ++-- src/gui/group/GroupModel.cpp | 2 +- tests/TestEntryModel.cpp | 7 +++---- 8 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index c27c196a2..0c67bbac1 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -214,6 +214,17 @@ QHash Metadata::customIcons() const return m_customIcons; } +QHash Metadata::customIconsScaledPixmaps() const +{ + QHash result; + + Q_FOREACH (const Uuid& uuid, m_customIconsOrder) { + result.insert(uuid, customIconScaledPixmap(uuid)); + } + + return result; +} + QList Metadata::customIconsOrder() const { return m_customIconsOrder; diff --git a/src/core/Metadata.h b/src/core/Metadata.h index b6e25c288..062e55216 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -86,6 +86,7 @@ class Metadata : public QObject QHash customIcons() const; QList customIconsOrder() const; bool recycleBinEnabled() const; + QHash customIconsScaledPixmaps() const; Group* recycleBin(); const Group* recycleBin() const; QDateTime recycleBinChanged() const; diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index 5ea4db736..c408940e3 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -98,7 +98,7 @@ void EditWidgetIcons::load(Uuid currentUuid, Database* database, IconStruct icon m_database = database; m_currentUuid = currentUuid; - m_customIconModel->setIcons(database->metadata()->customIcons(), + m_customIconModel->setIcons(database->metadata()->customIconsScaledPixmaps(), database->metadata()->customIconsOrder()); Uuid iconUuid = iconStruct.uuid; @@ -133,7 +133,7 @@ void EditWidgetIcons::addCustomIcon() if (!image.isNull()) { Uuid uuid = Uuid::random(); m_database->metadata()->addCustomIconScaled(uuid, image); - m_customIconModel->setIcons(m_database->metadata()->customIcons(), + m_customIconModel->setIcons(m_database->metadata()->customIconsScaledPixmaps(), m_database->metadata()->customIconsOrder()); QModelIndex index = m_customIconModel->indexFromUuid(uuid); m_ui->customIconsView->setCurrentIndex(index); @@ -183,7 +183,7 @@ void EditWidgetIcons::removeCustomIcon() } m_database->metadata()->removeCustomIcon(iconUuid); - m_customIconModel->setIcons(m_database->metadata()->customIcons(), + m_customIconModel->setIcons(m_database->metadata()->customIconsScaledPixmaps(), m_database->metadata()->customIconsOrder()); if (m_customIconModel->rowCount() > 0) { m_ui->customIconsView->setCurrentIndex(m_customIconModel->index(0, 0)); diff --git a/src/gui/IconModels.cpp b/src/gui/IconModels.cpp index c169e875b..1c181eae2 100644 --- a/src/gui/IconModels.cpp +++ b/src/gui/IconModels.cpp @@ -54,7 +54,7 @@ CustomIconModel::CustomIconModel(QObject* parent) { } -void CustomIconModel::setIcons(const QHash& icons, const QList& iconsOrder) +void CustomIconModel::setIcons(const QHash& icons, const QList& iconsOrder) { beginResetModel(); diff --git a/src/gui/IconModels.h b/src/gui/IconModels.h index a04efa209..8a247070f 100644 --- a/src/gui/IconModels.h +++ b/src/gui/IconModels.h @@ -19,7 +19,7 @@ #define KEEPASSX_ICONMODELS_H #include -#include +#include #include "core/Global.h" #include "core/Uuid.h" @@ -44,12 +44,12 @@ class CustomIconModel : public QAbstractListModel virtual int rowCount(const QModelIndex& parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; - void setIcons(const QHash& icons, const QList& iconsOrder); + void setIcons(const QHash& icons, const QList& iconsOrder); Uuid uuidFromIndex(const QModelIndex& index) const; QModelIndex indexFromUuid(const Uuid& uuid) const; private: - QHash m_icons; + QHash m_icons; QList m_iconsOrder; }; diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index c75d38a1b..084ad72c7 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -146,7 +146,7 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const switch (index.column()) { case ParentGroup: if (entry->group()) { - return entry->group()->iconPixmap(); + return entry->group()->iconScaledPixmap(); } break; case Title: @@ -154,7 +154,7 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const return databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex); } else { - return entry->iconPixmap(); + return entry->iconScaledPixmap(); } } } diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index 21f8c7e3b..1244ad78f 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -136,7 +136,7 @@ QVariant GroupModel::data(const QModelIndex& index, int role) const return databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex); } else { - return group->iconPixmap(); + return group->iconScaledPixmap(); } } else if (role == Qt::FontRole) { diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp index 7ba886bc2..b323a733c 100644 --- a/tests/TestEntryModel.cpp +++ b/tests/TestEntryModel.cpp @@ -210,17 +210,16 @@ void TestEntryModel::testCustomIconModel() QCOMPARE(model->rowCount(), 0); - QHash icons; + QHash icons; QList iconsOrder; Uuid iconUuid(QByteArray(16, '2')); - QImage icon; - icons.insert(iconUuid, icon); + icons.insert(iconUuid, QPixmap()); iconsOrder << iconUuid; Uuid iconUuid2(QByteArray(16, '1')); QImage icon2; - icons.insert(iconUuid2, icon2); + icons.insert(iconUuid2, QPixmap()); iconsOrder << iconUuid2; model->setIcons(icons, iconsOrder); From eb56bd8973859e49384ada8757639466f483c22c Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Thu, 28 Jan 2016 23:07:04 +0100 Subject: [PATCH 8/8] Add repair functionality to strip invalid XML chars. Refs #392 --- src/CMakeLists.txt | 3 + src/format/KeePass2Reader.cpp | 14 +++- src/format/KeePass2Reader.h | 3 +- src/format/KeePass2Repair.cpp | 107 +++++++++++++++++++++++++++++++ src/format/KeePass2Repair.h | 50 +++++++++++++++ src/gui/DatabaseRepairWidget.cpp | 103 +++++++++++++++++++++++++++++ src/gui/DatabaseRepairWidget.h | 41 ++++++++++++ src/gui/MainWindow.cpp | 37 +++++++++++ src/gui/MainWindow.h | 1 + src/gui/MainWindow.ui | 6 ++ tests/TestKeePass2Writer.cpp | 30 +++++++++ tests/TestKeePass2Writer.h | 1 + tests/data/bug392.kdbx | Bin 0 -> 1374 bytes 13 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 src/format/KeePass2Repair.cpp create mode 100644 src/format/KeePass2Repair.h create mode 100644 src/gui/DatabaseRepairWidget.cpp create mode 100644 src/gui/DatabaseRepairWidget.h create mode 100644 tests/data/bug392.kdbx diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1270d346e..da8b9ec1e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -78,6 +78,7 @@ set(keepassx_SOURCES format/KeePass2.h format/KeePass2RandomStream.cpp format/KeePass2Reader.cpp + format/KeePass2Repair.cpp format/KeePass2Writer.cpp format/KeePass2XmlReader.cpp format/KeePass2XmlWriter.cpp @@ -86,6 +87,7 @@ set(keepassx_SOURCES gui/ChangeMasterKeyWidget.cpp gui/Clipboard.cpp gui/DatabaseOpenWidget.cpp + gui/DatabaseRepairWidget.cpp gui/DatabaseSettingsWidget.cpp gui/DatabaseTabWidget.cpp gui/DatabaseWidget.cpp @@ -180,6 +182,7 @@ set(keepassx_MOC gui/ChangeMasterKeyWidget.h gui/Clipboard.h gui/DatabaseOpenWidget.h + gui/DatabaseRepairWidget.h gui/DatabaseSettingsWidget.h gui/DatabaseTabWidget.h gui/DatabaseWidget.h diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index adde8cdbe..d1ca9ed9c 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -43,7 +43,7 @@ KeePass2Reader::KeePass2Reader() { } -Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& key) +Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& key, bool keepDatabase) { QScopedPointer db(new Database()); m_db = db.data(); @@ -178,7 +178,12 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke if (xmlReader.hasError()) { raiseError(xmlReader.errorString()); - return Q_NULLPTR; + if (keepDatabase) { + return db.take(); + } + else { + return Q_NULLPTR; + } } Q_ASSERT(version < 0x00030001 || !xmlReader.headerHash().isEmpty()); @@ -232,6 +237,11 @@ QByteArray KeePass2Reader::xmlData() return m_xmlData; } +QByteArray KeePass2Reader::streamKey() +{ + return m_protectedStreamKey; +} + void KeePass2Reader::raiseError(const QString& errorMessage) { m_error = true; diff --git a/src/format/KeePass2Reader.h b/src/format/KeePass2Reader.h index 2c9daab2d..827e671cd 100644 --- a/src/format/KeePass2Reader.h +++ b/src/format/KeePass2Reader.h @@ -31,12 +31,13 @@ class KeePass2Reader public: KeePass2Reader(); - Database* readDatabase(QIODevice* device, const CompositeKey& key); + Database* readDatabase(QIODevice* device, const CompositeKey& key, bool keepDatabase = false); Database* readDatabase(const QString& filename, const CompositeKey& key); bool hasError(); QString errorString(); void setSaveXml(bool save); QByteArray xmlData(); + QByteArray streamKey(); private: void raiseError(const QString& errorMessage); diff --git a/src/format/KeePass2Repair.cpp b/src/format/KeePass2Repair.cpp new file mode 100644 index 000000000..4eff3d806 --- /dev/null +++ b/src/format/KeePass2Repair.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016 Felix Geyer + * + * 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 . + */ + +#include "KeePass2Repair.h" + +#include +#include + +#include "format/KeePass2RandomStream.h" +#include "format/KeePass2Reader.h" +#include "format/KeePass2XmlReader.h" + +KeePass2Repair::KeePass2Repair() + : m_db(Q_NULLPTR) +{ +} + +KeePass2Repair::RepairResult KeePass2Repair::repairDatabase(QIODevice* device, const CompositeKey& key) +{ + m_db = Q_NULLPTR; + m_errorStr.clear(); + + KeePass2Reader reader; + reader.setSaveXml(true); + + Database* db = reader.readDatabase(device, key, true); + if (!reader.hasError()) { + delete db; + return NothingTodo; + } + + QByteArray xmlData = reader.xmlData(); + if (!db || xmlData.isEmpty()) { + delete db; + m_errorStr = reader.errorString(); + return UnableToOpen; + } + + bool repairAction = false; + + QString xmlStart = QString::fromLatin1(xmlData.constData(), qMin(100, xmlData.size())); + QRegExp encodingRegExp("encoding=\"([^\"]+)\"", Qt::CaseInsensitive, QRegExp::RegExp2); + if (encodingRegExp.indexIn(xmlStart) != -1) { + if (encodingRegExp.cap(1).compare("utf-8", Qt::CaseInsensitive) != 0 + && encodingRegExp.cap(1).compare("utf8", Qt::CaseInsensitive) != 0) + { + // database is not utf-8 encoded, we don't support repairing that + delete db; + return RepairFailed; + } + } + + // try to fix broken databases because of bug #392 + for (int i = (xmlData.size() - 1); i >= 0; i--) { + char ch = xmlData.at(i); + if (ch < 0x20 && ch != 0x09 && ch != 0x0A && ch != 0x0D) { + xmlData.remove(i, 1); + repairAction = true; + } + } + + if (!repairAction) { + // we were unable to find the problem + delete db; + return RepairFailed; + } + + KeePass2RandomStream randomStream; + randomStream.init(reader.streamKey()); + KeePass2XmlReader xmlReader; + QBuffer buffer(&xmlData); + buffer.open(QIODevice::ReadOnly); + xmlReader.readDatabase(&buffer, db, &randomStream); + + if (xmlReader.hasError()) { + delete db; + return RepairFailed; + } + else { + m_db = db; + return RepairSuccess; + } +} + +Database* KeePass2Repair::database() const +{ + return m_db; +} + +QString KeePass2Repair::errorString() const +{ + return m_errorStr; +} diff --git a/src/format/KeePass2Repair.h b/src/format/KeePass2Repair.h new file mode 100644 index 000000000..fe2f9dbfe --- /dev/null +++ b/src/format/KeePass2Repair.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 Felix Geyer + * + * 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 . + */ + +#ifndef KEEPASSX_KEEPASS2REPAIR_H +#define KEEPASSX_KEEPASS2REPAIR_H + +#include +#include + +#include "core/Database.h" +#include "keys/CompositeKey.h" + +class KeePass2Repair +{ + Q_DECLARE_TR_FUNCTIONS(KeePass2Repair) + +public: + enum RepairResult + { + NothingTodo, + UnableToOpen, + RepairSuccess, + RepairFailed + }; + + KeePass2Repair(); + RepairResult repairDatabase(QIODevice* device, const CompositeKey& key); + Database* database() const; + QString errorString() const; + +private: + Database* m_db; + QString m_errorStr; +}; + +#endif // KEEPASSX_KEEPASS2REPAIR_H diff --git a/src/gui/DatabaseRepairWidget.cpp b/src/gui/DatabaseRepairWidget.cpp new file mode 100644 index 000000000..b7eeac212 --- /dev/null +++ b/src/gui/DatabaseRepairWidget.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2016 Felix Geyer + * + * 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 . + */ + +#include "DatabaseRepairWidget.h" + +#include +#include + +#include "ui_DatabaseOpenWidget.h" +#include "core/Database.h" +#include "core/Metadata.h" +#include "format/KeePass2Repair.h" +#include "gui/MessageBox.h" +#include "keys/FileKey.h" +#include "keys/PasswordKey.h" + +DatabaseRepairWidget::DatabaseRepairWidget(QWidget* parent) + : DatabaseOpenWidget(parent) +{ + m_ui->labelHeadline->setText(tr("Repair database")); + + connect(this, SIGNAL(editFinished(bool)), this, SLOT(processEditFinished(bool))); +} + +void DatabaseRepairWidget::openDatabase() +{ + CompositeKey masterKey; + + if (m_ui->checkPassword->isChecked()) { + masterKey.addKey(PasswordKey(m_ui->editPassword->text())); + } + + if (m_ui->checkKeyFile->isChecked()) { + FileKey key; + QString keyFilename = m_ui->comboKeyFile->currentText(); + QString errorMsg; + if (!key.load(keyFilename, &errorMsg)) { + MessageBox::warning(this, tr("Error"), tr("Can't open key file").append(":\n").append(errorMsg)); + Q_EMIT editFinished(false); + } + masterKey.addKey(key); + } + + KeePass2Repair repair; + + QFile file(m_filename); + if (!file.open(QIODevice::ReadOnly)) { + // TODO: error message + Q_EMIT editFinished(false); + return; + } + if (m_db) { + delete m_db; + } + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + KeePass2Repair::RepairResult repairResult = repair.repairDatabase(&file, masterKey); + QApplication::restoreOverrideCursor(); + + switch (repairResult) { + case KeePass2Repair::NothingTodo: + MessageBox::information(this, tr("Error"), tr("Database opened fine. Nothing to do.")); + Q_EMIT editFinished(false); + return; + case KeePass2Repair::UnableToOpen: + MessageBox::warning(this, tr("Error"), tr("Unable to open the database.").append("\n") + .append(repair.errorString())); + Q_EMIT editFinished(false); + return; + case KeePass2Repair::RepairSuccess: + m_db = repair.database(); + MessageBox::warning(this, tr("Success"), tr("The database has been successfully repaired\nYou can now save it.")); + Q_EMIT editFinished(true); + return; + case KeePass2Repair::RepairFailed: + MessageBox::warning(this, tr("Error"), tr("Unable to repair the database.")); + Q_EMIT editFinished(false); + return; + } +} + +void DatabaseRepairWidget::processEditFinished(bool result) +{ + if (result) { + Q_EMIT success(); + } + else { + Q_EMIT error(); + } +} diff --git a/src/gui/DatabaseRepairWidget.h b/src/gui/DatabaseRepairWidget.h new file mode 100644 index 000000000..56c5c9609 --- /dev/null +++ b/src/gui/DatabaseRepairWidget.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 Felix Geyer + * + * 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 . + */ + +#ifndef KEEPASSX_DATABASEREPAIRWIDGET_H +#define KEEPASSX_DATABASEREPAIRWIDGET_H + +#include "gui/DatabaseOpenWidget.h" + +class DatabaseRepairWidget : public DatabaseOpenWidget +{ + Q_OBJECT + +public: + explicit DatabaseRepairWidget(QWidget* parent = Q_NULLPTR); + +Q_SIGNALS: + void success(); + void error(); + +protected: + void openDatabase() Q_DECL_OVERRIDE; + +private Q_SLOTS: + void processEditFinished(bool result); +}; + +#endif // KEEPASSX_DATABASEREPAIRWIDGET_H diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index cf26f1818..b6dc1df14 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -27,8 +27,12 @@ #include "core/FilePath.h" #include "core/InactivityTimer.h" #include "core/Metadata.h" +#include "format/KeePass2Writer.h" #include "gui/AboutDialog.h" #include "gui/DatabaseWidget.h" +#include "gui/DatabaseRepairWidget.h" +#include "gui/FileDialog.h" +#include "gui/MessageBox.h" const QString MainWindow::BaseWindowTitle = "KeePassX"; @@ -163,6 +167,8 @@ MainWindow::MainWindow() SLOT(changeDatabaseSettings())); connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); + connect(m_ui->actionRepairDatabase, SIGNAL(triggered()), this, + SLOT(repairDatabase())); connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, @@ -366,6 +372,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionImportKeePass1->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionRepairDatabase->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); } @@ -598,6 +605,36 @@ void MainWindow::lockDatabasesAfterInactivity() m_ui->tabWidget->lockDatabases(); } +void MainWindow::repairDatabase() +{ + QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files")); + QString fileName = fileDialog()->getOpenFileName(this, tr("Open database"), QString(), + filter); + if (fileName.isEmpty()) { + return; + } + + QScopedPointer dialog(new QDialog(this)); + DatabaseRepairWidget* dbRepairWidget = new DatabaseRepairWidget(dialog.data()); + connect(dbRepairWidget, SIGNAL(success()), dialog.data(), SLOT(accept())); + connect(dbRepairWidget, SIGNAL(error()), dialog.data(), SLOT(reject())); + dbRepairWidget->load(fileName); + if (dialog->exec() == QDialog::Accepted && dbRepairWidget->database()) { + QString saveFileName = fileDialog()->getSaveFileName(this, tr("Save repaired database"), QString(), + tr("KeePass 2 Database").append(" (*.kdbx)"), + Q_NULLPTR, 0, "kdbx"); + + if (!saveFileName.isEmpty()) { + KeePass2Writer writer; + writer.writeDatabase(saveFileName, dbRepairWidget->database()); + if (writer.hasError()) { + MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" + + writer.errorString()); + } + } + } +} + bool MainWindow::isTrayIconEnabled() const { #ifdef Q_OS_MAC diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 695c20d41..5aa39ca0c 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -66,6 +66,7 @@ private Q_SLOTS: void trayIconTriggered(QSystemTrayIcon::ActivationReason reason); void toggleWindow(); void lockDatabasesAfterInactivity(); + void repairDatabase(); private: static void setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback = 0); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 92c806347..66c040e99 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -121,6 +121,7 @@ + @@ -422,6 +423,11 @@ Export to CSV file + + + Repair database + + diff --git a/tests/TestKeePass2Writer.cpp b/tests/TestKeePass2Writer.cpp index fd478a875..638d4002e 100644 --- a/tests/TestKeePass2Writer.cpp +++ b/tests/TestKeePass2Writer.cpp @@ -20,6 +20,7 @@ #include #include +#include "config-keepassx-tests.h" #include "tests.h" #include "FailDevice.h" #include "core/Database.h" @@ -27,6 +28,7 @@ #include "core/Metadata.h" #include "crypto/Crypto.h" #include "format/KeePass2Reader.h" +#include "format/KeePass2Repair.h" #include "format/KeePass2Writer.h" #include "format/KeePass2XmlWriter.h" #include "keys/PasswordKey.h" @@ -127,6 +129,34 @@ void TestKeePass2Writer::testDeviceFailure() delete db; } +void TestKeePass2Writer::testRepair() +{ + QString brokenDbFilename = QString(KEEPASSX_TEST_DATA_DIR).append("/bug392.kdbx"); + // master password = test + // entry username: testuser\x10 + // entry password: testpw + CompositeKey key; + key.addKey(PasswordKey("test")); + + // test that we can't open the broken database + KeePass2Reader reader; + Database* dbBroken = reader.readDatabase(brokenDbFilename, key); + QVERIFY(!dbBroken); + QVERIFY(reader.hasError()); + + // test if we can repair the database + KeePass2Repair repair; + QFile file(brokenDbFilename); + file.open(QIODevice::ReadOnly); + QCOMPARE(repair.repairDatabase(&file, key), KeePass2Repair::RepairSuccess); + Database* dbRepaired = repair.database(); + QVERIFY(dbRepaired); + + QCOMPARE(dbRepaired->rootGroup()->entries().size(), 1); + QCOMPARE(dbRepaired->rootGroup()->entries().at(0)->username(), QString("testuser")); + QCOMPARE(dbRepaired->rootGroup()->entries().at(0)->password(), QString("testpw")); +} + void TestKeePass2Writer::cleanupTestCase() { delete m_dbOrg; diff --git a/tests/TestKeePass2Writer.h b/tests/TestKeePass2Writer.h index eb3f1095c..822883823 100644 --- a/tests/TestKeePass2Writer.h +++ b/tests/TestKeePass2Writer.h @@ -33,6 +33,7 @@ private Q_SLOTS: void testAttachments(); void testNonAsciiPasswords(); void testDeviceFailure(); + void testRepair(); void cleanupTestCase(); private: diff --git a/tests/data/bug392.kdbx b/tests/data/bug392.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..c649f8dc240e704f65b36e7127144e75cc77a10b GIT binary patch literal 1374 zcmV-k1)=%_*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZaI3;bW zT9+*$Y75=Zd7VSD_ah^)Vpzc&(j&9umm)}J1t0)xcKq_FJwh6x_fz4qKgcSEwh~vH z7IRQ(1ofU88op}=2mqjl0RR91000LN03jz_4Q$!y$eU#Y`b0KZlL#OHOiVvnkbA#z zo>(B(oZK=k#JtoLN0^~q!p4`U&0|O`2_OKNoGKRWn`B)~xf(CJ1$Lz<2enOBTiLH@ z0=&>y9AHEW1ONg60000401XNa3T97g9;>B_Oi(w&&5GR6puQpbNv;8hugv#m30jt> z>zUM*f_}_WNR63iSBq_tqE%!z0#Uh8RROD$67psz+lk@PM72ihoUf5toVhN{5~p>5 zHCy;wS5c4dY7?2uBOxIk6j#oC(;U24nhub31qGg3EjC0g5FIVJksDRg=c~Yxt-EJp{ z2<`sx!*Pj6p5&Rg%-y{3*A3-A1`qp&7TbN8Rs3ws`aJ}XWyZT26A~&;r?a-VarJ`g zVm1CFbaa6!Wa)Z*6p@0z-}ee~^Zwc4`Kfz;Nw^$^fIoC1G;nk$!N6g#4>VlA#BpqP zts0w6r_q2)7GQRq01OmSSkK4Z3EK3ZzJxLg0FtB1JYkooiPqG7v&%gvfHwtx{s}$0 z)qqTyL<|yFH`j8A=j0n>tl|4H{rHkR6if2Q7?HiIb%sm2V-AEUB$kAG?TPJ@EUGV% zwVXaH63YaRo}RCZypj{sVKfwELRb~jGgOzZ9MIJZI)GefqZVyjbPfft;!&*U>T&6Z z-mY;JV%X@7-SY0#e;{)rVlm}nbQ8CelTxUbH5b+H1d%O#+%fZ)WK7NY3#O~GvbxE% zj%;{PL_~S)7gqDgu6_b{irgxb$xB;w+In!N#;Ne?RIS6}QKYT=j0p2YQU6M(x?Dr1q%-%CV;#luwzN_iacabOx2 zsaPv$I#@{~m>73KygCZ%Zrqj@k(Q#M_jh)MSx%I9KcY`#{|9(OdhDY()Gu*Y3fHV3 zx@Wygw>?@SYGr|$G37jzH0@SjqdkU&PZz*jcOb1|Q~kq>2BrJa^ezKfmI!_XTc-ii z1*O`s7T&|1j|LlovS)Z2rX0^B~x?96)Z9u5(OOWms0 zJ?A9dQmNU22HR5!-VRF(_axnbMN^Em_U!7)jAH=E^bO})Fw|?F3+D8)VI-i8Xc;b3 zy;2V@P94j;^kTc(>Jo6C0gaTdTzPwocOuNYunl!ZM>cix=IhS$9I0@i(S2;Fblo@+ z%F>O@-a(r3`nKD_5VylPJGho{qq}$-1s*n%;?zj1UWr94q#X{FU7Y654m2Rc3>CRaOxdMHVZ!+4VL9ZdcaXBC zst7ix6g$19VR*DrSz?+_NMb4nNsx;44sv5sdrnAo;NYY%fNG7y%M*=-DIUdIv=H|K gQ2u&UW1v_&1L9Z3*^bcTR7=uutm$R3ME0rOx$&-!tpET3 literal 0 HcmV?d00001