File indexing completed on 2024-05-12 05:44:35

0001 /***************************************************************************
0002  *   Copyright (C) 2005-2009 by Rajko Albrecht  ral@alwins-world.de        *
0003  *   https://kde.org/applications/development/org.kde.kdesvn               *
0004  *                                                                         *
0005  * This program is free software; you can redistribute it and/or           *
0006  * modify it under the terms of the GNU Lesser General Public              *
0007  * License as published by the Free Software Foundation; either            *
0008  * version 2.1 of the License, or (at your option) any later version.      *
0009  *                                                                         *
0010  * This program is distributed in the hope that it will be useful,         *
0011  * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
0012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       *
0013  * Lesser General Public License for more details.                         *
0014  *                                                                         *
0015  * You should have received a copy of the GNU Lesser General Public        *
0016  * License along with this program (in the file LGPL.txt); if not,         *
0017  * write to the Free Software Foundation, Inc., 51 Franklin St,            *
0018  * Fifth Floor, Boston, MA  02110-1301  USA                                *
0019  *                                                                         *
0020  * This software consists of voluntary contributions made by many          *
0021  * individuals.  For exact contribution history, see the revision          *
0022  * history and logs, available at https://commits.kde.org/kdesvn.          *
0023  ***************************************************************************/
0024 #include "LogCache.h"
0025 
0026 #include <QDebug>
0027 #include <QDir>
0028 #include <QMap>
0029 #include <QMutex>
0030 #include <QSqlDatabase>
0031 #include <QSqlError>
0032 #include <QSqlQuery>
0033 #include <QThreadStorage>
0034 #include <QVariant>
0035 
0036 #include "svnqt/cache/DatabaseException.h"
0037 #include "svnqt/path.h"
0038 
0039 static QString SQLTYPE()
0040 {
0041     return QStringLiteral("QSQLITE");
0042 }
0043 static QString SQLMAIN()
0044 {
0045     return QStringLiteral("logmain-logcache");
0046 }
0047 static QString SQLMAINTABLE()
0048 {
0049     return QStringLiteral("logdb");
0050 }
0051 static QString SQLTMPDB()
0052 {
0053     return QStringLiteral("tmpdb");
0054 }
0055 static QString SQLREPOSPARAMETER()
0056 {
0057     return QStringLiteral("repoparameter");
0058 }
0059 static QString SQLSTATUS()
0060 {
0061     return QStringLiteral("logstatus");
0062 }
0063 
0064 namespace svn
0065 {
0066 namespace cache
0067 {
0068 
0069 LogCache *LogCache::mSelf = nullptr;
0070 
0071 class ThreadDBStore
0072 {
0073 public:
0074     ThreadDBStore()
0075     {
0076         m_DB = QSqlDatabase();
0077     }
0078     ~ThreadDBStore()
0079     {
0080         m_DB.commit();
0081         m_DB.close();
0082         m_DB = QSqlDatabase();
0083         for (const QString &dbName : reposCacheNames) {
0084             if (QSqlDatabase::database(dbName).isOpen()) {
0085                 QSqlDatabase::database(dbName).commit();
0086                 QSqlDatabase::database(dbName).close();
0087             }
0088             QSqlDatabase::removeDatabase(dbName);
0089         }
0090         QSqlDatabase::removeDatabase(key);
0091     }
0092 
0093     void deleteDb(const QString &path)
0094     {
0095         for (auto it = reposCacheNames.begin(); it != reposCacheNames.end(); ++it) {
0096             QSqlDatabase _db = QSqlDatabase::database(it.value());
0097             if (_db.databaseName() == path) {
0098                 qDebug() << "Removing database " << _db.databaseName() << Qt::endl;
0099                 if (_db.isOpen()) {
0100                     _db.commit();
0101                     _db.close();
0102                 }
0103                 _db = QSqlDatabase();
0104                 QSqlDatabase::removeDatabase(it.value());
0105                 reposCacheNames.erase(it);
0106                 break;
0107             }
0108         }
0109     }
0110     QSqlDatabase m_DB;
0111     QString key;
0112     QMap<QString, QString> reposCacheNames;
0113 };
0114 
0115 class LogCacheData
0116 {
0117 protected:
0118     QMutex m_singleDbMutex;
0119 
0120 public:
0121     LogCacheData()
0122     {
0123     }
0124     ~LogCacheData()
0125     {
0126         if (m_mainDB.hasLocalData()) {
0127             m_mainDB.localData()->m_DB.close();
0128             m_mainDB.setLocalData(nullptr);
0129         }
0130     }
0131 
0132     QString idToPath(const QString &id) const
0133     {
0134         return m_BasePath + QLatin1Char('/') + id + QLatin1String(".db");
0135     }
0136 
0137     bool deleteRepository(const QString &aRepository)
0138     {
0139         const QString id = getReposId(aRepository);
0140 
0141         static const QString s_q(QLatin1String("delete from ") + SQLREPOSPARAMETER() + QLatin1String(" where id = ?"));
0142         static const QString r_q(QLatin1String("delete from ") + SQLMAINTABLE() + QLatin1String(" where id = ?"));
0143         QSqlDatabase mainDB = getMainDB();
0144         if (!mainDB.isValid()) {
0145             qWarning("Failed to open main database.");
0146             return false;
0147         }
0148         qDebug() << m_mainDB.localData()->reposCacheNames;
0149         m_mainDB.localData()->deleteDb(idToPath(id));
0150         qDebug() << m_mainDB.localData()->reposCacheNames;
0151         QFile fi(idToPath(id));
0152         if (fi.exists()) {
0153             if (!fi.remove()) {
0154                 qWarning() << "Could not delete " << fi.fileName();
0155                 return false;
0156             }
0157         }
0158         qDebug() << "Removed " << fi.fileName() << Qt::endl;
0159         mainDB.transaction();
0160         QSqlQuery _q(mainDB);
0161         _q.prepare(s_q);
0162         _q.bindValue(0, id);
0163         if (!_q.exec()) {
0164             qDebug() << "Error delete value: " << _q.lastError().text() << "(" << _q.lastQuery() << ")";
0165             _q.finish();
0166             mainDB.rollback();
0167             return false;
0168         }
0169         _q.prepare(r_q);
0170         _q.bindValue(0, id);
0171         if (!_q.exec()) {
0172             qDebug() << "Error delete value: " << _q.lastError().text() << "(" << _q.lastQuery() << ")";
0173             _q.finish();
0174             mainDB.rollback();
0175             return false;
0176         }
0177         mainDB.commit();
0178         return true;
0179     }
0180 
0181     bool checkReposDb(QSqlDatabase aDb)
0182     {
0183         if (!aDb.open()) {
0184             return false;
0185         }
0186 
0187         QSqlQuery _q(aDb);
0188         QStringList list = aDb.tables();
0189 
0190         aDb.transaction();
0191         if (!list.contains(QStringLiteral("logentries"))) {
0192             _q.exec(
0193                 QStringLiteral("CREATE TABLE \"logentries\" (\"idx\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"revision\" INTEGER UNIQUE,\"date\" "
0194                                "INTEGER,\"author\" TEXT, \"message\" TEXT)"));
0195         }
0196         if (!list.contains(QStringLiteral("changeditems"))) {
0197             _q.exec(
0198                 QStringLiteral("CREATE TABLE \"changeditems\" (\"idx\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"revision\" INTEGER,\"changeditem\" "
0199                                "TEXT,\"action\" TEXT,\"copyfrom\" TEXT,\"copyfromrev\" INTEGER, UNIQUE(revision,changeditem,action))"));
0200         }
0201         if (!list.contains(QStringLiteral("mergeditems"))) {
0202             _q.exec(QStringLiteral("CREATE TABLE \"mergeditems\" (\"revision\" INTEGER,\"mergeditems\" TEXT, PRIMARY KEY(revision))"));
0203         }
0204         if (!list.contains(QStringLiteral("dbversion"))) {
0205             _q.exec(QStringLiteral("CREATE TABLE \"dbversion\" (\"version\" INTEGER)"));
0206             qDebug() << _q.lastError();
0207             _q.exec(QStringLiteral("INSERT INTO \"dbversion\" (version) VALUES(0)"));
0208         }
0209         aDb.commit();
0210         list = aDb.tables();
0211         if (!list.contains(QStringLiteral("logentries")) || !list.contains(QStringLiteral("changeditems")) || !list.contains(QStringLiteral("mergeditems"))
0212             || !list.contains(QStringLiteral("dbversion"))) {
0213             qDebug() << "lists: " << list;
0214             return false;
0215         }
0216         _q.exec(QStringLiteral("SELECT VERSION from dbversion limit 1"));
0217         if (_q.lastError().type() == QSqlError::NoError && _q.next()) {
0218             int version = _q.value(0).toInt();
0219             if (version == 0) {
0220                 _q.exec(QStringLiteral("create index if not exists main.authorindex on logentries(author)"));
0221                 if (_q.lastError().type() != QSqlError::NoError) {
0222                     qDebug() << _q.lastError();
0223                 } else {
0224                     _q.exec(QStringLiteral("UPDATE dbversion SET VERSION=1"));
0225                 }
0226                 ++version;
0227             }
0228             if (version == 1) {
0229                 _q.exec(QStringLiteral("create index if not exists main.dateindex on logentries(date)"));
0230                 if (_q.lastError().type() != QSqlError::NoError) {
0231                     qDebug() << _q.lastError();
0232                 } else {
0233                     _q.exec(QStringLiteral("UPDATE dbversion SET VERSION=2"));
0234                 }
0235                 ++version;
0236             }
0237         } else {
0238             qDebug() << "Select: " << _q.lastError();
0239         }
0240         return true;
0241     }
0242 
0243     QString createReposDB(const svn::Path &reposroot)
0244     {
0245         QMutexLocker locker(&m_singleDbMutex);
0246 
0247         QSqlDatabase _mdb = getMainDB();
0248 
0249         _mdb.transaction();
0250         QSqlQuery query(_mdb);
0251         QString q(QLatin1String("insert into ") + SQLMAINTABLE() + QLatin1String(" (reposroot) VALUES('") + reposroot.path() + QLatin1String("')"));
0252 
0253         if (!query.exec(q)) {
0254             return QString();
0255         }
0256 
0257         _mdb.commit();
0258         query.prepare(reposSelect());
0259         query.bindValue(0, reposroot.native());
0260         QString db;
0261         if (query.exec() && query.next()) {
0262             db = query.value(0).toString();
0263         } else {
0264             // qDebug() << "Error select_01: " << query.lastError().text() << "(" << query.lastQuery() << ")";
0265         }
0266         if (!db.isEmpty()) {
0267             QString fulldb = idToPath(db);
0268             QSqlDatabase _db = QSqlDatabase::addDatabase(SQLTYPE(), SQLTMPDB());
0269             _db.setDatabaseName(fulldb);
0270             if (!checkReposDb(_db)) { }
0271             _db = QSqlDatabase();
0272             QSqlDatabase::removeDatabase(SQLTMPDB());
0273         }
0274         return db;
0275     }
0276 
0277     QString getReposId(const svn::Path &reposroot)
0278     {
0279         if (!getMainDB().isValid()) {
0280             return QString();
0281         }
0282         QSqlQuery c(getMainDB());
0283         c.prepare(reposSelect());
0284         c.bindValue(0, reposroot.native());
0285 
0286         // only the first one
0287         if (c.exec() && c.next()) {
0288             return c.value(0).toString();
0289         }
0290         return QString();
0291     }
0292 
0293     QSqlDatabase getReposDB(const svn::Path &reposroot)
0294     {
0295         if (!getMainDB().isValid()) {
0296             return QSqlDatabase();
0297         }
0298         QString dbFile = getReposId(reposroot);
0299 
0300         if (dbFile.isEmpty()) {
0301             dbFile = createReposDB(reposroot);
0302             if (dbFile.isEmpty()) {
0303                 return QSqlDatabase();
0304             }
0305         }
0306         if (m_mainDB.localData()->reposCacheNames.find(dbFile) != m_mainDB.localData()->reposCacheNames.end()) {
0307             QSqlDatabase db = QSqlDatabase::database(m_mainDB.localData()->reposCacheNames.value(dbFile));
0308             checkReposDb(db);
0309             return db;
0310         }
0311         unsigned i = 0;
0312         QString _key = dbFile;
0313         while (QSqlDatabase::contains(_key)) {
0314             _key = QStringLiteral("%1-%2").arg(dbFile).arg(i++);
0315         }
0316         const QString fulldb = idToPath(dbFile);
0317         QSqlDatabase db = QSqlDatabase::addDatabase(SQLTYPE(), _key);
0318         db.setDatabaseName(fulldb);
0319         if (!checkReposDb(db)) {
0320             db = QSqlDatabase();
0321         } else {
0322             m_mainDB.localData()->reposCacheNames[dbFile] = _key;
0323         }
0324         return db;
0325     }
0326 
0327     QSqlDatabase getMainDB() const
0328     {
0329         if (!m_mainDB.hasLocalData()) {
0330             unsigned i = 0;
0331             QString _key = SQLMAIN();
0332             while (QSqlDatabase::contains(_key)) {
0333                 _key = QStringLiteral("%1-%2").arg(SQLMAIN()).arg(i++);
0334             }
0335             QSqlDatabase db = QSqlDatabase::addDatabase(SQLTYPE(), _key);
0336             db.setDatabaseName(m_BasePath + QLatin1String("/maindb.db"));
0337             if (db.open()) {
0338                 m_mainDB.setLocalData(new ThreadDBStore);
0339                 m_mainDB.localData()->key = _key;
0340                 m_mainDB.localData()->m_DB = db;
0341             }
0342         }
0343         if (m_mainDB.hasLocalData()) {
0344             return m_mainDB.localData()->m_DB;
0345         } else {
0346             return QSqlDatabase();
0347         }
0348     }
0349     QString m_BasePath;
0350 
0351     mutable QThreadStorage<ThreadDBStore *> m_mainDB;
0352 
0353     static QString reposSelect()
0354     {
0355         return QStringLiteral("SELECT id from ") + SQLMAINTABLE() + QStringLiteral(" where reposroot=? ORDER by id DESC");
0356     }
0357 };
0358 
0359 /*!
0360     \fn svn::cache::LogCache::LogCache()
0361  */
0362 LogCache::LogCache()
0363     : m_BasePath(QDir::homePath() + QLatin1String("/.svnqt"))
0364 {
0365     setupCachePath();
0366 }
0367 
0368 LogCache::LogCache(const QString &aBasePath)
0369 {
0370     delete mSelf;
0371     mSelf = this;
0372     if (aBasePath.isEmpty()) {
0373         m_BasePath = QDir::homePath() + QLatin1String("/.svnqt");
0374     } else {
0375         m_BasePath = aBasePath;
0376     }
0377     setupCachePath();
0378 }
0379 
0380 LogCache::~LogCache()
0381 {
0382 }
0383 
0384 /*!
0385     \fn svn::cache::LogCache::setupCachePath()
0386  */
0387 void LogCache::setupCachePath()
0388 {
0389     m_CacheData.reset(new LogCacheData);
0390     m_CacheData->m_BasePath = m_BasePath;
0391     QDir d;
0392     if (!d.exists(m_BasePath)) {
0393         d.mkdir(m_BasePath);
0394     }
0395     m_BasePath = m_BasePath + QLatin1Char('/') + QLatin1String("logcache");
0396     if (!d.exists(m_BasePath)) {
0397         d.mkdir(m_BasePath);
0398     }
0399     m_CacheData->m_BasePath = m_BasePath;
0400     if (d.exists(m_BasePath)) {
0401         setupMainDb();
0402     }
0403 }
0404 
0405 void LogCache::setupMainDb()
0406 {
0407     QSqlDatabase mainDB = m_CacheData->getMainDB();
0408     if (!mainDB.isValid()) {
0409         qWarning("Failed to open main database.");
0410     } else {
0411         const QStringList list = mainDB.tables();
0412         QSqlQuery q(mainDB);
0413         if (!list.contains(SQLSTATUS())) {
0414             mainDB.transaction();
0415             if (q.exec(QLatin1String("CREATE TABLE \"") + SQLSTATUS() + QLatin1String("\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"value\" TEXT);"))) {
0416                 q.exec(QLatin1String("INSERT INTO \"") + SQLSTATUS() + QLatin1String("\" (key,value) values(\"version\",\"0\");"));
0417             }
0418             mainDB.commit();
0419         }
0420         int version = databaseVersion();
0421         if (version == 0) {
0422             mainDB.transaction();
0423             if (!list.contains(SQLMAINTABLE())) {
0424                 q.exec(QLatin1String("CREATE TABLE IF NOT EXISTS \"") + SQLMAINTABLE()
0425                        + QLatin1String("\" (\"reposroot\" TEXT,\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);"));
0426             } /* else {
0427                  q.exec("CREATE TABLE IF NOT EXISTS \""+QString(SQLMAINTABLE)+"new\" (\"reposroot\" TEXT,\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);");
0428                  q.exec("insert into \""+QString(SQLMAINTABLE)+"new\" select \"reposroot\",\"id\" from \""+QString(SQLMAINTABLE)+"\");");
0429                  q.exec("drop table \""+QString(SQLMAINTABLE)+"\";");
0430                  q.exec("alter table \""+QString(SQLMAINTABLE)+"new\" to \""+QString(SQLMAINTABLE)+"\";");
0431              }*/
0432             ++version;
0433         }
0434         if (version == 1) {
0435             mainDB.transaction();
0436             if (!q.exec(QLatin1String("CREATE TABLE IF NOT EXISTS \"") + SQLREPOSPARAMETER()
0437                         + QLatin1String("\" (\"id\" INTEGER NOT NULL, \"parameter\" TEXT, \"value\" TEXT, PRIMARY KEY(\"id\",\"parameter\"));"))) {
0438                 qDebug() << "Error create: " << q.lastError().text() << "(" << q.lastQuery() << ")";
0439             }
0440             mainDB.commit();
0441             ++version;
0442         }
0443         databaseVersion(version);
0444     }
0445 }
0446 
0447 void LogCache::databaseVersion(int newversion)
0448 {
0449     QSqlDatabase mainDB = m_CacheData->getMainDB();
0450     if (!mainDB.isValid()) {
0451         return;
0452     }
0453     static const QString _qs(QLatin1String("update \"") + SQLSTATUS() + QLatin1String("\" SET value = ? WHERE \"key\" = \"version\""));
0454     QSqlQuery cur(mainDB);
0455     cur.prepare(_qs);
0456     cur.bindValue(0, newversion);
0457     if (!cur.exec()) {
0458         qDebug() << "Error set version: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
0459     }
0460 }
0461 
0462 int LogCache::databaseVersion() const
0463 {
0464     QSqlDatabase mainDB = m_CacheData->getMainDB();
0465     if (!mainDB.isValid()) {
0466         return -1;
0467     }
0468     static const QString _qs(QLatin1String("select value from \"") + SQLSTATUS() + QLatin1String("\" WHERE \"key\" = \"version\""));
0469     QSqlQuery cur(mainDB);
0470     cur.prepare(_qs);
0471     if (!cur.exec()) {
0472         qDebug() << "Error select version: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
0473         return -1;
0474     }
0475     if (cur.isActive() && cur.next()) {
0476         // qDebug("Sel result: %s",_q.value(0).toString().toUtf8().data());
0477         return cur.value(0).toInt();
0478     }
0479     return -1;
0480 }
0481 
0482 QVariant LogCache::getRepositoryParameter(const svn::Path &repository, const QString &key) const
0483 {
0484     QSqlDatabase mainDB = m_CacheData->getMainDB();
0485     if (!mainDB.isValid()) {
0486         return QVariant();
0487     }
0488     static const QString qs(QLatin1String("select \"value\",\"repoparameter\".\"parameter\" as \"key\" from \"") + SQLREPOSPARAMETER()
0489                             + QLatin1String("\" INNER JOIN \"") + SQLMAINTABLE() + QLatin1String("\" ON (\"") + SQLREPOSPARAMETER()
0490                             + QLatin1String("\".id = \"") + SQLMAINTABLE() + QLatin1String("\".id and \"") + SQLMAINTABLE()
0491                             + QLatin1String("\".reposroot = ?)  WHERE \"parameter\" = ?;"));
0492     QSqlQuery cur(mainDB);
0493     cur.prepare(qs);
0494     cur.bindValue(0, repository.native());
0495     cur.bindValue(1, key);
0496     if (!cur.exec()) {
0497         qWarning() << "Error select: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
0498         return QVariant();
0499     }
0500     if (cur.isActive() && cur.next()) {
0501         return cur.value(0);
0502     }
0503     return QVariant();
0504 }
0505 
0506 bool LogCache::setRepositoryParameter(const svn::Path &repository, const QString &key, const QVariant &value)
0507 {
0508     QSqlDatabase mainDB = m_CacheData->getMainDB();
0509     if (!mainDB.isValid()) {
0510         return false;
0511     }
0512     QString id = m_CacheData->getReposId(repository);
0513     if (id.isEmpty()) {
0514         return false;
0515     }
0516     static const QString qs(QLatin1String("INSERT OR REPLACE INTO \"") + SQLREPOSPARAMETER()
0517                             + QLatin1String("\" (\"id\",\"parameter\",\"value\") values (\"%1\",\"%2\",?);"));
0518     static const QString dqs(QLatin1String("DELETE FROM \"") + SQLREPOSPARAMETER() + QLatin1String("\" WHERE \"id\"=? and \"parameter\" = ?"));
0519     mainDB.transaction();
0520     QSqlQuery cur(mainDB);
0521     if (value.isValid()) {
0522         QString _qs = qs.arg(id, key); //.arg(value.toByteArray());
0523         cur.prepare(_qs);
0524         cur.bindValue(0, value);
0525         if (!cur.exec()) {
0526             qDebug() << "Error insert new value: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
0527             cur.finish();
0528             mainDB.rollback();
0529             return false;
0530         }
0531     } else {
0532         cur.prepare(dqs);
0533         cur.bindValue(0, id);
0534         cur.bindValue(1, key);
0535         if (!cur.exec()) {
0536             qDebug() << "Error delete value: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
0537             cur.finish();
0538             mainDB.rollback();
0539             return false;
0540         }
0541     }
0542     mainDB.commit();
0543     return true;
0544 }
0545 
0546 }
0547 }
0548 
0549 /*!
0550     \fn svn::cache::LogCache::self()
0551  */
0552 svn::cache::LogCache *svn::cache::LogCache::self()
0553 {
0554     if (!mSelf) {
0555         mSelf = new LogCache();
0556     }
0557     return mSelf;
0558 }
0559 
0560 /*!
0561     \fn svn::cache::LogCache::reposDb()
0562  */
0563 QSqlDatabase svn::cache::LogCache::reposDb(const QString &aRepository)
0564 {
0565     //    //qDebug("reposDB");
0566     return m_CacheData->getReposDB(aRepository);
0567 }
0568 
0569 /*!
0570     \fn svn::cache::LogCache::cachedRepositories()const
0571  */
0572 QStringList svn::cache::LogCache::cachedRepositories() const
0573 {
0574     static const QString s_q(QLatin1String("select \"reposroot\" from ") + SQLMAINTABLE() + QLatin1String(" order by reposroot"));
0575     QSqlDatabase mainDB = m_CacheData->getMainDB();
0576     QStringList _res;
0577     if (!mainDB.isValid()) {
0578         qWarning("Failed to open main database.");
0579         return _res;
0580     }
0581     QSqlQuery cur(mainDB);
0582     cur.prepare(s_q);
0583     if (!cur.exec()) {
0584         throw svn::cache::DatabaseException(QLatin1String("Could not retrieve values: ") + cur.lastError().text());
0585     }
0586     while (cur.next()) {
0587         _res.append(cur.value(0).toString());
0588     }
0589 
0590     return _res;
0591 }
0592 
0593 bool svn::cache::LogCache::valid() const
0594 {
0595     return m_CacheData->getMainDB().isValid();
0596 }
0597 
0598 bool svn::cache::LogCache::deleteRepository(const QString &aRepository)
0599 {
0600     return m_CacheData->deleteRepository(aRepository);
0601 }