File indexing completed on 2024-05-05 12:22: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 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"