File indexing completed on 2024-05-05 16:18:05

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