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 }