File indexing completed on 2024-04-14 05:41:03

0001 /**
0002  * SPDX-FileCopyrightText: 2021 Sebastian Engel <kde@sebastianengel.eu>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "weaver.h"
0008 
0009 
0010 // TODO include libBasket instead of hardcoded link
0011 #include "../../src/archive.h"
0012 
0013 #include <KLocalizedString>
0014 #include <QDebug>
0015 #include <QDir>
0016 #include <QFile>
0017 #include <QFileInfo>
0018 #include <QMimeDatabase>
0019 #include <QMimeType>
0020 #include <QXmlStreamReader>
0021 
0022 void translateErrorCode(const Archive::IOErrorCode code);
0023 
0024 Weaver::Weaver(QCommandLineParser *parser,
0025                const QCommandLineOption &mode_weave,
0026                const QCommandLineOption &mode_unweave,
0027                const QCommandLineOption &output,
0028                const QCommandLineOption &basename,
0029                const QCommandLineOption &previewImg,
0030                const QCommandLineOption &force)
0031     : m_parser(parser)
0032     , m_weave(mode_weave)
0033     , m_unweave(mode_unweave)
0034     , m_output(output)
0035     , m_basename(basename)
0036     , m_previewImg(previewImg)
0037     , m_force(force)
0038     , m_preview(QString())
0039 {
0040 }
0041 
0042 int Weaver::runMain()
0043 {
0044     if (!m_parser->isSet(m_unweave) && !m_parser->isSet(m_weave)) {
0045         qCritical().noquote() << i18n("You need to provide at least one --weave/-w or --unweave/-u option");
0046         return 1;
0047     }
0048 
0049     if (m_parser->isSet(m_unweave) && m_parser->isSet(m_weave)) {
0050         qCritical().noquote() << i18n("You cannot use --weave/-w and --unweave/-u options in conjunction.");
0051         return 1;
0052     }
0053 
0054     if (m_parser->values(m_unweave).size() > 1 || m_parser->values(m_weave).size() > 1) {
0055         qWarning().noquote() << i18n(
0056             "Multiple --weave/-w or --unweave/-u input options found. All but the first input"
0057             " are going to be ignored.");
0058     }
0059 
0060     bool ret = true;
0061 
0062     if (m_parser->isSet(m_weave)) {
0063         ret = weave();
0064     } else if (m_parser->isSet(m_unweave)) {
0065         ret = unweave();
0066     }
0067 
0068     if (!ret) {
0069         return 1;
0070     }
0071 
0072     return 0;
0073 }
0074 
0075 bool Weaver::unweave()
0076 {
0077     m_in = m_parser->value(m_unweave);
0078 
0079     if (!isBasketFile(m_in)) {
0080         qCritical().noquote() << i18n("The source seems to be an invalid .baskets file");
0081         return false;
0082     }
0083 
0084     if (m_parser->isSet(m_output)) {
0085         m_out = m_parser->value(m_output);
0086         if (!QFileInfo::exists(m_out)) {
0087             qCritical().noquote() << i18n("Output directory does not exist.");
0088             return false;
0089         }
0090     } else {
0091         m_out = QFileInfo(m_in).absoluteDir().path();
0092     }
0093 
0094     QString destination = m_out + QDir::separator();
0095     if (m_parser->isSet(m_basename)) {
0096         destination += m_parser->value(m_basename);
0097     } else {
0098         destination += QFileInfo(m_in).baseName() + "_baskets";
0099     }
0100 
0101     Archive::IOErrorCode errorCode = Archive::extractArchive(m_in, destination, !m_parser->isSet(m_force));
0102 
0103     translateErrorCode(errorCode);
0104 
0105     return errorCode == Archive::IOErrorCode::NoError;
0106 }
0107 
0108 bool Weaver::weave()
0109 {
0110     m_in = m_parser->value(m_weave);
0111     m_preview = m_parser->value(m_previewImg);
0112 
0113     if (!isBasketSourceValid(m_in)) {
0114         qCritical().noquote() << i18n("The source seems to be invalid.");
0115         if (!m_parser->isSet(m_force)) {
0116             return false;
0117         }
0118     }
0119 
0120     m_preview = m_parser->value(m_previewImg);
0121     if (!isPreviewValid(m_preview)) {
0122         m_preview = QString();
0123     }
0124 
0125     if (m_parser->isSet(m_output)) {
0126         m_out = m_parser->value(m_output);
0127         if (!QFileInfo::exists(m_out)) {
0128             qCritical().noquote() << i18n("Output directory does not exist.");
0129             return false;
0130         }
0131     } else {
0132         QDir inputPath(m_in);
0133         inputPath.cdUp();
0134         m_out = inputPath.absolutePath();
0135     }
0136 
0137     QString destination = m_out + QDir::separator();
0138     if (m_parser->isSet(m_basename)) {
0139         destination += m_parser->value(m_basename);
0140     } else {
0141         destination += QFileInfo(m_in).baseName();
0142     }
0143 
0144     const QString extenstion = QStringLiteral(".baskets");
0145     if (!destination.endsWith(extenstion)) {
0146         destination += extenstion;
0147     }
0148 
0149     Archive::IOErrorCode errorCode = Archive::createArchiveFromSource(m_in, m_preview, destination, !m_parser->isSet(m_force));
0150 
0151     translateErrorCode(errorCode);
0152 
0153     return errorCode == Archive::IOErrorCode::NoError;
0154 }
0155 
0156 bool Weaver::isBasketSourceValid(const QString &basketsDirectory)
0157 {
0158     QFileInfo dirInfo(basketsDirectory);
0159     if (!dirInfo.isDir() || !dirInfo.exists()) {
0160         return false;
0161     }
0162 
0163     // test the existence of /baskets/baskets.xml
0164     const QString basketTreePath = basketsDirectory + QDir::separator() + "baskets" + QDir::separator() + "baskets.xml";
0165     if (!QFileInfo::exists(basketTreePath)) {
0166         return false;
0167     }
0168 
0169     QStringList containedBaskets;
0170     QFile basketTree(basketTreePath);
0171     if (basketTree.open(QIODevice::ReadOnly)) {
0172         QXmlStreamReader xml(&basketTree);
0173 
0174         // find the beginning of the basketTree element
0175         while (!xml.atEnd()) {
0176             xml.readNextStartElement();
0177             if (xml.name() == QStringLiteral("basketTree")) {
0178                 break;
0179             }
0180         }
0181         if (xml.atEnd()) {
0182             basketTree.close();
0183             return false;
0184         }
0185         // collect all referenced baskets
0186         while (!xml.atEnd()) {
0187             xml.readNextStartElement();
0188             if (xml.name() == QStringLiteral("basket")) {
0189                 if (xml.attributes().hasAttribute(QStringLiteral("folderName"))) {
0190                     containedBaskets.append(xml.attributes().value(QStringLiteral("folderName")).toString());
0191                     xml.skipCurrentElement();
0192                 } else {
0193                     basketTree.close();
0194                     return false;
0195                 }
0196             } else {
0197                 break;
0198             }
0199         }
0200     }
0201     basketTree.close();
0202 
0203     // test whether the referenced subdirectories exist
0204     for (const QString &bskt : containedBaskets) {
0205         const QString bsktPath = basketsDirectory + QDir::separator() + "baskets" + QDir::separator() + bskt;
0206         if (!QFileInfo::exists(bsktPath)) {
0207             return false;
0208         }
0209     }
0210 
0211     return true;
0212 }
0213 
0214 bool Weaver::isBasketFile(const QString &basketsFile)
0215 {
0216     QFile file(basketsFile);
0217 
0218     if (!file.exists()) {
0219         return false;
0220     }
0221 
0222     if (file.open(QIODevice::ReadOnly)) {
0223         QTextStream stream(&file);
0224         QString line = stream.readLine();
0225         stream.setCodec("ISO-8859-1");
0226         if (line != QStringLiteral("BasKetNP:archive")) {
0227             file.close();
0228             return false;
0229         }
0230 
0231         while (!stream.atEnd()) {
0232             line = stream.readLine();
0233             int index = line.indexOf(':');
0234             QString key;
0235             QString value;
0236             if (index >= 0) {
0237                 key = line.left(index);
0238                 value = line.right(line.length() - index - 1);
0239             } else {
0240                 key = line;
0241                 value = QString();
0242             }
0243             // only test existence of keywords
0244             if (key == QStringLiteral("version") || key == QStringLiteral("read-compatible") ||
0245                 key == QStringLiteral("write-compatible")) {
0246                 continue;
0247             }
0248             // test for existence, then skip block of given size
0249             if (key == QStringLiteral("preview*")) {
0250                 bool ok = false;
0251                 const qint64 size = value.toULong(&ok);
0252                 if (!ok) {
0253                     file.close();
0254                     return false;
0255                 }
0256                 stream.seek(stream.pos() + size);
0257             }
0258             // test for existence, then skip block of given size
0259             else if (key == QStringLiteral("archive*")) {
0260                 bool ok = false;
0261                 qint64 size = value.toULong(&ok);
0262                 if (!ok) {
0263                     file.close();
0264                     return false;
0265                 }
0266                 stream.seek(stream.pos() + size);
0267             }
0268             // test unknown embedded file, then skip block of given size
0269             else if (key.endsWith('*')) {
0270                 bool ok = false;
0271                 qint64 size = value.toULong(&ok);
0272                 if (!ok) {
0273                     file.close();
0274                     return false;
0275                 }
0276                 stream.seek(stream.pos() + size);
0277             }
0278         }
0279         file.close();
0280 
0281     } else {
0282         qCritical().noquote() << i18n("Could not open file.");
0283         return false;
0284     }
0285 
0286     return true;
0287 }
0288 
0289 bool Weaver::isPreviewValid(const QString &previewFile)
0290 {
0291     if (previewFile.isEmpty() || !QFileInfo::exists(previewFile)) {
0292         return false;
0293     }
0294 
0295     // is the given file a png?
0296     QMimeDatabase db;
0297     QMimeType mime = db.mimeTypeForFile(previewFile);
0298 
0299     return mime.inherits(QStringLiteral("image/png"));
0300 }
0301 
0302 void translateErrorCode(const Archive::IOErrorCode code)
0303 {
0304     switch (code) {
0305     case Archive::IOErrorCode::FailedToOpenResource:
0306         qCritical().noquote() << i18n("Failed to open a file resource.");
0307         break;
0308     case Archive::IOErrorCode::NotABasketArchive:
0309         qCritical().noquote() << i18n("This file is not a basket archive.");
0310         break;
0311     case Archive::IOErrorCode::CorruptedBasketArchive:
0312         qCritical().noquote() << i18n("This file is corrupted. It can not be opened.");
0313         break;
0314     case Archive::IOErrorCode::DestinationExists:
0315         qCritical().noquote() << i18n("The destination path already exists.");
0316         break;
0317     case Archive::IOErrorCode::IncompatibleBasketVersion:
0318         qCritical().noquote() << i18n("This file supplied file format is not supported");
0319         break;
0320     case Archive::IOErrorCode::PossiblyCompatibleBasketVersion:
0321         qWarning().noquote() << i18n("This file was created with a more recent version of BasKet Note Pads. It might not be fully supported");
0322         [[fallthrough]];
0323     case Archive::IOErrorCode::NoError:
0324         break;
0325     }
0326 }