File indexing completed on 2024-04-21 03:57:42

0001 /*
0002     SPDX-FileCopyrightText: 2010-2018 Dominik Haumann <dhaumann@kde.org>
0003     SPDX-FileCopyrightText: 2010 Diana-Victoria Tiriplica <diana.tiriplica@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "config.h"
0009 
0010 #include "katebuffer.h"
0011 #include "kateconfig.h"
0012 #include "katedocument.h"
0013 #include "katepartdebug.h"
0014 #include "kateswapdiffcreator.h"
0015 #include "kateswapfile.h"
0016 #include "katetextbuffer.h"
0017 #include "kateundomanager.h"
0018 #include "ktexteditor/message.h"
0019 #include <ktexteditor/view.h>
0020 
0021 #include <KLocalizedString>
0022 #include <KStandardGuiItem>
0023 
0024 #include <QApplication>
0025 #include <QCryptographicHash>
0026 #include <QDir>
0027 #include <QFileInfo>
0028 
0029 #ifndef Q_OS_WIN
0030 #include <unistd.h>
0031 #endif
0032 
0033 // swap file version header
0034 const static char swapFileVersionString[] = "Kate Swap File 2.0";
0035 
0036 // tokens for swap files
0037 const static qint8 EA_StartEditing = 'S';
0038 const static qint8 EA_FinishEditing = 'E';
0039 const static qint8 EA_WrapLine = 'W';
0040 const static qint8 EA_UnwrapLine = 'U';
0041 const static qint8 EA_InsertText = 'I';
0042 const static qint8 EA_RemoveText = 'R';
0043 
0044 namespace Kate
0045 {
0046 QTimer *SwapFile::s_timer = nullptr;
0047 
0048 SwapFile::SwapFile(KTextEditor::DocumentPrivate *document)
0049     : QObject(document)
0050     , m_document(document)
0051     , m_trackingEnabled(false)
0052     , m_recovered(false)
0053     , m_needSync(false)
0054 {
0055     // fixed version of serialisation
0056     m_stream.setVersion(QDataStream::Qt_4_6);
0057 
0058     // connect the timer
0059     connect(syncTimer(), &QTimer::timeout, this, &Kate::SwapFile::writeFileToDisk, Qt::DirectConnection);
0060 
0061     // connecting the signals
0062     connect(&m_document->buffer(), &KateBuffer::saved, this, &Kate::SwapFile::fileSaved);
0063     connect(&m_document->buffer(), &KateBuffer::loaded, this, &Kate::SwapFile::fileLoaded);
0064     connect(m_document, &KTextEditor::Document::configChanged, this, &SwapFile::configChanged);
0065 
0066     // tracking on!
0067     setTrackingEnabled(true);
0068 }
0069 
0070 SwapFile::~SwapFile()
0071 {
0072     // only remove swap file after data recovery (bug #304576)
0073     if (!shouldRecover()) {
0074         removeSwapFile();
0075     }
0076 }
0077 
0078 void SwapFile::configChanged()
0079 {
0080 }
0081 
0082 void SwapFile::setTrackingEnabled(bool enable)
0083 {
0084     if (m_trackingEnabled == enable) {
0085         return;
0086     }
0087 
0088     m_trackingEnabled = enable;
0089 
0090     if (m_trackingEnabled) {
0091         connect(m_document, &KTextEditor::Document::editingStarted, this, &Kate::SwapFile::startEditing);
0092         connect(m_document, &KTextEditor::Document::editingFinished, this, &Kate::SwapFile::finishEditing);
0093         connect(m_document, &KTextEditor::DocumentPrivate::modifiedChanged, this, &SwapFile::modifiedChanged);
0094 
0095         connect(m_document, &KTextEditor::Document::lineWrapped, this, &Kate::SwapFile::wrapLine);
0096         connect(m_document, &KTextEditor::Document::lineUnwrapped, this, &Kate::SwapFile::unwrapLine);
0097         connect(m_document, &KTextEditor::Document::textInserted, this, &Kate::SwapFile::insertText);
0098         connect(m_document, &KTextEditor::Document::textRemoved, this, &Kate::SwapFile::removeText);
0099     } else {
0100         disconnect(m_document, &KTextEditor::Document::editingStarted, this, &Kate::SwapFile::startEditing);
0101         disconnect(m_document, &KTextEditor::Document::editingFinished, this, &Kate::SwapFile::finishEditing);
0102         disconnect(m_document, &KTextEditor::DocumentPrivate::modifiedChanged, this, &SwapFile::modifiedChanged);
0103 
0104         disconnect(m_document, &KTextEditor::Document::lineWrapped, this, &Kate::SwapFile::wrapLine);
0105         disconnect(m_document, &KTextEditor::Document::lineUnwrapped, this, &Kate::SwapFile::unwrapLine);
0106         disconnect(m_document, &KTextEditor::Document::textInserted, this, &Kate::SwapFile::insertText);
0107         disconnect(m_document, &KTextEditor::Document::textRemoved, this, &Kate::SwapFile::removeText);
0108     }
0109 }
0110 
0111 void SwapFile::fileClosed()
0112 {
0113     // remove old swap file, file is now closed
0114     if (!shouldRecover()) {
0115         removeSwapFile();
0116     } else {
0117         m_document->setReadWrite(true);
0118     }
0119 
0120     // purge filename
0121     updateFileName();
0122 }
0123 
0124 KTextEditor::DocumentPrivate *SwapFile::document()
0125 {
0126     return m_document;
0127 }
0128 
0129 bool SwapFile::isValidSwapFile(QDataStream &stream, bool checkDigest) const
0130 {
0131     // read and check header
0132     QByteArray header;
0133     stream >> header;
0134 
0135     if (header != swapFileVersionString) {
0136         qCWarning(LOG_KTE) << "Can't open swap file, wrong version";
0137         return false;
0138     }
0139 
0140     // read checksum
0141     QByteArray checksum;
0142     stream >> checksum;
0143     // qCDebug(LOG_KTE) << "DIGEST:" << checksum << m_document->checksum();
0144     if (checkDigest && checksum != m_document->checksum()) {
0145         qCWarning(LOG_KTE) << "Can't recover from swap file, checksum of document has changed";
0146         return false;
0147     }
0148 
0149     return true;
0150 }
0151 
0152 void SwapFile::fileLoaded(const QString &)
0153 {
0154     // look for swap file
0155     if (!updateFileName()) {
0156         return;
0157     }
0158 
0159     if (!m_swapfile.exists()) {
0160         // qCDebug(LOG_KTE) << "No swap file";
0161         return;
0162     }
0163 
0164     if (!QFileInfo(m_swapfile).isReadable()) {
0165         qCWarning(LOG_KTE) << "Can't open swap file (missing permissions)";
0166         return;
0167     }
0168 
0169     // sanity check
0170     QFile peekFile(fileName());
0171     if (peekFile.open(QIODevice::ReadOnly)) {
0172         QDataStream stream(&peekFile);
0173         if (!isValidSwapFile(stream, true)) {
0174             removeSwapFile();
0175             return;
0176         }
0177         peekFile.close();
0178     } else {
0179         qCWarning(LOG_KTE) << "Can't open swap file:" << fileName();
0180         return;
0181     }
0182 
0183     // show swap file message
0184     m_document->setReadWrite(false);
0185     showSwapFileMessage();
0186 }
0187 
0188 void SwapFile::modifiedChanged()
0189 {
0190     if (!m_document->isModified() && !shouldRecover()) {
0191         m_needSync = false;
0192         // the file is not modified and we are not in recover mode
0193         removeSwapFile();
0194     }
0195 }
0196 
0197 void SwapFile::recover()
0198 {
0199     m_document->setReadWrite(true);
0200 
0201     // if isOpen() returns true, the swap file likely changed already (appended data)
0202     // Example: The document was falsely marked as writable and the user changed
0203     // text even though the recover bar was visible. In this case, a replay of
0204     // the swap file across wrong document content would happen -> certainly wrong
0205     if (m_swapfile.isOpen()) {
0206         qCWarning(LOG_KTE) << "Attempt to recover an already modified document. Aborting";
0207         removeSwapFile();
0208         return;
0209     }
0210 
0211     // if the file doesn't exist, abort (user might have deleted it, or use two editor instances)
0212     if (!m_swapfile.open(QIODevice::ReadOnly)) {
0213         qCWarning(LOG_KTE) << "Can't open swap file";
0214         return;
0215     }
0216 
0217     // remember that the file has recovered
0218     m_recovered = true;
0219 
0220     // open data stream
0221     m_stream.setDevice(&m_swapfile);
0222 
0223     // replay the swap file
0224     bool success = recover(m_stream);
0225 
0226     // close swap file
0227     m_stream.setDevice(nullptr);
0228     m_swapfile.close();
0229 
0230     if (!success) {
0231         removeSwapFile();
0232     }
0233 
0234     // recover can also be called through the KTE::RecoveryInterface.
0235     // Make sure, the message is hidden in this case as well.
0236     if (m_swapMessage) {
0237         m_swapMessage->deleteLater();
0238     }
0239 }
0240 
0241 bool SwapFile::recover(QDataStream &stream, bool checkDigest)
0242 {
0243     if (!isValidSwapFile(stream, checkDigest)) {
0244         return false;
0245     }
0246 
0247     // disconnect current signals
0248     setTrackingEnabled(false);
0249 
0250     // needed to set undo/redo cursors in a sane way
0251     bool firstEditInGroup = false;
0252     KTextEditor::Cursor undoCursor = KTextEditor::Cursor::invalid();
0253     KTextEditor::Cursor redoCursor = KTextEditor::Cursor::invalid();
0254 
0255     // replay swapfile
0256     bool editRunning = false;
0257     bool brokenSwapFile = false;
0258     while (!stream.atEnd()) {
0259         if (brokenSwapFile) {
0260             break;
0261         }
0262 
0263         qint8 type;
0264         stream >> type;
0265         switch (type) {
0266         case EA_StartEditing: {
0267             m_document->editStart();
0268             editRunning = true;
0269             firstEditInGroup = true;
0270             undoCursor = KTextEditor::Cursor::invalid();
0271             redoCursor = KTextEditor::Cursor::invalid();
0272             break;
0273         }
0274         case EA_FinishEditing: {
0275             m_document->editEnd();
0276 
0277             // empty editStart() / editEnd() groups exist: only set cursor if required
0278             if (!firstEditInGroup) {
0279                 // set undo/redo cursor of last KateUndoGroup of the undo manager
0280                 m_document->undoManager()->setUndoRedoCursorsOfLastGroup(undoCursor, redoCursor);
0281                 m_document->undoManager()->undoSafePoint();
0282             }
0283             firstEditInGroup = false;
0284             editRunning = false;
0285             break;
0286         }
0287         case EA_WrapLine: {
0288             if (!editRunning) {
0289                 brokenSwapFile = true;
0290                 break;
0291             }
0292 
0293             int line = 0;
0294             int column = 0;
0295             stream >> line >> column;
0296 
0297             // emulate buffer unwrapLine with document
0298             m_document->editWrapLine(line, column, true);
0299 
0300             // track undo/redo cursor
0301             if (firstEditInGroup) {
0302                 firstEditInGroup = false;
0303                 undoCursor = KTextEditor::Cursor(line, column);
0304             }
0305             redoCursor = KTextEditor::Cursor(line + 1, 0);
0306 
0307             break;
0308         }
0309         case EA_UnwrapLine: {
0310             if (!editRunning) {
0311                 brokenSwapFile = true;
0312                 break;
0313             }
0314 
0315             int line = 0;
0316             stream >> line;
0317 
0318             // assert valid line
0319             Q_ASSERT(line > 0);
0320 
0321             const int undoColumn = m_document->lineLength(line - 1);
0322 
0323             // emulate buffer unwrapLine with document
0324             m_document->editUnWrapLine(line - 1, true, 0);
0325 
0326             // track undo/redo cursor
0327             if (firstEditInGroup) {
0328                 firstEditInGroup = false;
0329                 undoCursor = KTextEditor::Cursor(line, 0);
0330             }
0331             redoCursor = KTextEditor::Cursor(line - 1, undoColumn);
0332 
0333             break;
0334         }
0335         case EA_InsertText: {
0336             if (!editRunning) {
0337                 brokenSwapFile = true;
0338                 break;
0339             }
0340 
0341             int line;
0342             int column;
0343             QByteArray text;
0344             stream >> line >> column >> text;
0345             m_document->insertText(KTextEditor::Cursor(line, column), QString::fromUtf8(text.data(), text.size()));
0346 
0347             // track undo/redo cursor
0348             if (firstEditInGroup) {
0349                 firstEditInGroup = false;
0350                 undoCursor = KTextEditor::Cursor(line, column);
0351             }
0352             redoCursor = KTextEditor::Cursor(line, column + text.size());
0353 
0354             break;
0355         }
0356         case EA_RemoveText: {
0357             if (!editRunning) {
0358                 brokenSwapFile = true;
0359                 break;
0360             }
0361 
0362             int line;
0363             int startColumn;
0364             int endColumn;
0365             stream >> line >> startColumn >> endColumn;
0366             m_document->removeText(KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn)));
0367 
0368             // track undo/redo cursor
0369             if (firstEditInGroup) {
0370                 firstEditInGroup = false;
0371                 undoCursor = KTextEditor::Cursor(line, endColumn);
0372             }
0373             redoCursor = KTextEditor::Cursor(line, startColumn);
0374 
0375             break;
0376         }
0377         default: {
0378             qCWarning(LOG_KTE) << "Unknown type:" << type;
0379         }
0380         }
0381     }
0382 
0383     // balanced editStart and editEnd?
0384     if (editRunning) {
0385         brokenSwapFile = true;
0386         m_document->editEnd();
0387     }
0388 
0389     // warn the user if the swap file is not complete
0390     if (brokenSwapFile) {
0391         qCWarning(LOG_KTE) << "Some data might be lost";
0392     } else {
0393         // set sane final cursor, if possible
0394         KTextEditor::View *view = m_document->activeView();
0395         redoCursor = m_document->undoManager()->lastRedoCursor();
0396         if (view && redoCursor.isValid()) {
0397             view->setCursorPosition(redoCursor);
0398         }
0399     }
0400 
0401     // reconnect the signals
0402     setTrackingEnabled(true);
0403 
0404     return true;
0405 }
0406 
0407 void SwapFile::fileSaved(const QString &)
0408 {
0409     m_needSync = false;
0410 
0411     // remove old swap file (e.g. if a file A was "saved as" B)
0412     removeSwapFile();
0413 
0414     // set the name for the new swap file
0415     updateFileName();
0416 }
0417 
0418 void SwapFile::startEditing()
0419 {
0420     // no swap file, no work
0421     if (m_swapfile.fileName().isEmpty()) {
0422         return;
0423     }
0424 
0425     // if swap file doesn't exists, open it in WriteOnly mode
0426     // if it does, append the data to the existing swap file,
0427     // in case you recover and start editing again
0428     if (!m_swapfile.exists()) {
0429         // create path if not there
0430         if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory
0431             && !QDir(KateDocumentConfig::global()->swapDirectory()).exists()) {
0432             QDir().mkpath(KateDocumentConfig::global()->swapDirectory());
0433         }
0434 
0435         m_swapfile.open(QIODevice::WriteOnly);
0436         m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
0437         m_stream.setDevice(&m_swapfile);
0438 
0439         // write file header
0440         m_stream << QByteArray(swapFileVersionString);
0441 
0442         // write checksum
0443         m_stream << m_document->checksum();
0444     } else if (m_stream.device() == nullptr) {
0445         m_swapfile.open(QIODevice::Append);
0446         m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
0447         m_stream.setDevice(&m_swapfile);
0448     }
0449 
0450     // format: qint8
0451     m_stream << EA_StartEditing;
0452 }
0453 
0454 void SwapFile::finishEditing()
0455 {
0456     // skip if not open
0457     if (!m_swapfile.isOpen()) {
0458         return;
0459     }
0460 
0461     // write the file to the disk every 15 seconds (default)
0462     // skip this if we disabled that
0463     if (m_document->config()->swapSyncInterval() != 0 && !syncTimer()->isActive()) {
0464         // important: we store the interval as seconds, start wants milliseconds!
0465         syncTimer()->start(m_document->config()->swapSyncInterval() * 1000);
0466     }
0467 
0468     // format: qint8
0469     m_stream << EA_FinishEditing;
0470     m_swapfile.flush();
0471 }
0472 
0473 void SwapFile::wrapLine(KTextEditor::Document *, const KTextEditor::Cursor position)
0474 {
0475     // skip if not open
0476     if (!m_swapfile.isOpen()) {
0477         return;
0478     }
0479 
0480     // format: qint8, int, int
0481     m_stream << EA_WrapLine << position.line() << position.column();
0482 
0483     m_needSync = true;
0484 }
0485 
0486 void SwapFile::unwrapLine(KTextEditor::Document *, int line)
0487 {
0488     // skip if not open
0489     if (!m_swapfile.isOpen()) {
0490         return;
0491     }
0492 
0493     // format: qint8, int
0494     m_stream << EA_UnwrapLine << line;
0495 
0496     m_needSync = true;
0497 }
0498 
0499 void SwapFile::insertText(KTextEditor::Document *, const KTextEditor::Cursor position, const QString &text)
0500 {
0501     // skip if not open
0502     if (!m_swapfile.isOpen()) {
0503         return;
0504     }
0505 
0506     // format: qint8, int, int, bytearray
0507     m_stream << EA_InsertText << position.line() << position.column() << text.toUtf8();
0508 
0509     m_needSync = true;
0510 }
0511 
0512 void SwapFile::removeText(KTextEditor::Document *, KTextEditor::Range range, const QString &)
0513 {
0514     // skip if not open
0515     if (!m_swapfile.isOpen()) {
0516         return;
0517     }
0518 
0519     // format: qint8, int, int, int
0520     Q_ASSERT(range.start().line() == range.end().line());
0521     m_stream << EA_RemoveText << range.start().line() << range.start().column() << range.end().column();
0522 
0523     m_needSync = true;
0524 }
0525 
0526 bool SwapFile::shouldRecover() const
0527 {
0528     // should not recover if the file has already recovered in another view
0529     if (m_recovered) {
0530         return false;
0531     }
0532 
0533     return !m_swapfile.fileName().isEmpty() && m_swapfile.exists() && m_stream.device() == nullptr;
0534 }
0535 
0536 void SwapFile::discard()
0537 {
0538     m_document->setReadWrite(true);
0539     removeSwapFile();
0540 
0541     // discard can also be called through the KTE::RecoveryInterface.
0542     // Make sure, the message is hidden in this case as well.
0543     if (m_swapMessage) {
0544         m_swapMessage->deleteLater();
0545     }
0546 }
0547 
0548 void SwapFile::removeSwapFile()
0549 {
0550     if (!m_swapfile.fileName().isEmpty() && m_swapfile.exists()) {
0551         m_stream.setDevice(nullptr);
0552         m_swapfile.close();
0553         m_swapfile.remove();
0554     }
0555 }
0556 
0557 bool SwapFile::updateFileName()
0558 {
0559     // first clear filename
0560     m_swapfile.setFileName(QString());
0561 
0562     // get the new path
0563     QString path = fileName();
0564     if (path.isNull()) {
0565         return false;
0566     }
0567 
0568     m_swapfile.setFileName(path);
0569     return true;
0570 }
0571 
0572 QString SwapFile::fileName()
0573 {
0574     const QUrl &url = m_document->url();
0575     if (url.isEmpty() || !url.isLocalFile()) {
0576         return QString();
0577     }
0578 
0579     const QString fullLocalPath(url.toLocalFile());
0580     QString path;
0581     if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory) {
0582         path = KateDocumentConfig::global()->swapDirectory();
0583         path.append(QLatin1Char('/'));
0584 
0585         // append the sha1 sum of the full path + filename, to avoid "too long" paths created
0586         path.append(QString::fromLatin1(QCryptographicHash::hash(fullLocalPath.toUtf8(), QCryptographicHash::Sha1).toHex()));
0587         path.append(QLatin1Char('-'));
0588         path.append(QFileInfo(fullLocalPath).fileName());
0589 
0590         path.append(QLatin1String(".kate-swp"));
0591     } else {
0592         path = fullLocalPath;
0593         int poz = path.lastIndexOf(QLatin1Char('/'));
0594         path.insert(poz + 1, QLatin1Char('.'));
0595         path.append(QLatin1String(".kate-swp"));
0596     }
0597 
0598     return path;
0599 }
0600 
0601 QTimer *SwapFile::syncTimer()
0602 {
0603     if (s_timer == nullptr) {
0604         s_timer = new QTimer(QApplication::instance());
0605         s_timer->setSingleShot(true);
0606     }
0607 
0608     return s_timer;
0609 }
0610 
0611 void SwapFile::writeFileToDisk()
0612 {
0613     if (m_needSync) {
0614         m_needSync = false;
0615 
0616 #ifndef Q_OS_WIN
0617         // ensure that the file is written to disk
0618 #if HAVE_FDATASYNC
0619         fdatasync(m_swapfile.handle());
0620 #else
0621         fsync(m_swapfile.handle());
0622 #endif
0623 #endif
0624     }
0625 }
0626 
0627 void SwapFile::showSwapFileMessage()
0628 {
0629     m_swapMessage = new KTextEditor::Message(i18n("The file was not closed properly."), KTextEditor::Message::Warning);
0630     m_swapMessage->setWordWrap(true);
0631 
0632     QAction *diffAction = new QAction(QIcon::fromTheme(QStringLiteral("split")), i18n("View Changes"), nullptr);
0633     QAction *recoverAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo")), i18n("Recover Data"), nullptr);
0634     QAction *discardAction = new QAction(KStandardGuiItem::discard().icon(), i18n("Discard"), nullptr);
0635 
0636     m_swapMessage->addAction(diffAction, false);
0637     m_swapMessage->addAction(recoverAction);
0638     m_swapMessage->addAction(discardAction);
0639 
0640     connect(diffAction, &QAction::triggered, this, &SwapFile::showDiff);
0641     connect(recoverAction, &QAction::triggered, this, qOverload<>(&Kate::SwapFile::recover), Qt::QueuedConnection);
0642     connect(discardAction, &QAction::triggered, this, &SwapFile::discard, Qt::QueuedConnection);
0643 
0644     m_document->postMessage(m_swapMessage);
0645 }
0646 
0647 void SwapFile::showDiff()
0648 {
0649     // the diff creator deletes itself through deleteLater() when it's done
0650     SwapDiffCreator *diffCreator = new SwapDiffCreator(this);
0651     diffCreator->viewDiff();
0652 }
0653 
0654 }