File indexing completed on 2024-04-21 03:57:18
0001 /* 0002 SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 #include "config.h" 0007 #include "kateglobal.h" 0008 0009 #include "katetextbuffer.h" 0010 #include "katetextloader.h" 0011 0012 #include "katedocument.h" 0013 0014 // this is unfortunate, but needed for performance 0015 #include "katepartdebug.h" 0016 #include "kateview.h" 0017 0018 #ifndef Q_OS_WIN 0019 #include <cerrno> 0020 #include <unistd.h> 0021 // sadly there seems to be no possibility in Qt to determine detailed error 0022 // codes about e.g. file open errors, so we need to resort to evaluating 0023 // errno directly on platforms that support this 0024 #define CAN_USE_ERRNO 0025 #endif 0026 0027 #include <QBuffer> 0028 #include <QCryptographicHash> 0029 #include <QFile> 0030 #include <QFileInfo> 0031 #include <QStringEncoder> 0032 #include <QTemporaryFile> 0033 0034 #if HAVE_KAUTH 0035 #include "katesecuretextbuffer_p.h" 0036 #include <KAuth/Action> 0037 #include <KAuth/ExecuteJob> 0038 #endif 0039 0040 #if 0 0041 #define BUFFER_DEBUG qCDebug(LOG_KTE) 0042 #else 0043 #define BUFFER_DEBUG \ 0044 if (0) \ 0045 qCDebug(LOG_KTE) 0046 #endif 0047 0048 namespace Kate 0049 { 0050 TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, bool alwaysUseKAuth) 0051 : QObject(parent) 0052 , m_document(parent) 0053 , m_history(*this) 0054 , m_lines(0) 0055 , m_revision(0) 0056 , m_editingTransactions(0) 0057 , m_editingLastRevision(0) 0058 , m_editingLastLines(0) 0059 , m_editingMinimalLineChanged(-1) 0060 , m_editingMaximalLineChanged(-1) 0061 , m_encodingProberType(KEncodingProber::Universal) 0062 , m_generateByteOrderMark(false) 0063 , m_endOfLineMode(eolUnix) 0064 , m_lineLengthLimit(4096) 0065 , m_alwaysUseKAuthForSave(alwaysUseKAuth) 0066 { 0067 // create initial state 0068 clear(); 0069 } 0070 0071 TextBuffer::~TextBuffer() 0072 { 0073 // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect 0074 m_document = nullptr; 0075 0076 // not allowed during editing 0077 Q_ASSERT(m_editingTransactions == 0); 0078 0079 // kill all ranges, work on copy, they will remove themself from the hash 0080 QSet<TextRange *> copyRanges = m_ranges; 0081 qDeleteAll(copyRanges); 0082 Q_ASSERT(m_ranges.empty()); 0083 0084 // clean out all cursors and lines, only cursors belonging to range will survive 0085 for (TextBlock *block : std::as_const(m_blocks)) { 0086 block->deleteBlockContent(); 0087 } 0088 0089 // delete all blocks, now that all cursors are really deleted 0090 // else asserts in destructor of blocks will fail! 0091 qDeleteAll(m_blocks); 0092 m_blocks.clear(); 0093 0094 // kill all invalid cursors, do this after block deletion, to uncover if they might be still linked in blocks 0095 QSet<TextCursor *> copyCursors = m_invalidCursors; 0096 qDeleteAll(copyCursors); 0097 Q_ASSERT(m_invalidCursors.empty()); 0098 } 0099 0100 void TextBuffer::invalidateRanges() 0101 { 0102 // invalidate all ranges, work on copy, they might delete themself... 0103 const QSet<TextRange *> copyRanges = m_ranges; 0104 for (TextRange *range : copyRanges) { 0105 range->setRange(KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()); 0106 } 0107 } 0108 0109 void TextBuffer::clear() 0110 { 0111 // not allowed during editing 0112 Q_ASSERT(m_editingTransactions == 0); 0113 0114 invalidateRanges(); 0115 0116 // new block for empty buffer 0117 TextBlock *newBlock = new TextBlock(this, 0); 0118 newBlock->appendLine(QString()); 0119 0120 // clean out all cursors and lines, either move them to newBlock or invalidate them, if belonging to a range 0121 for (TextBlock *block : std::as_const(m_blocks)) { 0122 block->clearBlockContent(newBlock); 0123 } 0124 0125 // kill all buffer blocks 0126 qDeleteAll(m_blocks); 0127 m_blocks.clear(); 0128 0129 // insert one block with one empty line 0130 m_blocks.push_back(newBlock); 0131 0132 // reset lines and last used block 0133 m_lines = 1; 0134 0135 // reset revision 0136 m_revision = 0; 0137 0138 // reset bom detection 0139 m_generateByteOrderMark = false; 0140 0141 // reset the filter device 0142 m_mimeTypeForFilterDev = QStringLiteral("text/plain"); 0143 0144 // clear edit history 0145 m_history.clear(); 0146 0147 // we got cleared 0148 Q_EMIT cleared(); 0149 } 0150 0151 TextLine TextBuffer::line(int line) const 0152 { 0153 // get block, this will assert on invalid line 0154 int blockIndex = blockForLine(line); 0155 0156 // get line 0157 return m_blocks.at(blockIndex)->line(line); 0158 } 0159 0160 void TextBuffer::setLineMetaData(int line, const TextLine &textLine) 0161 { 0162 // get block, this will assert on invalid line 0163 int blockIndex = blockForLine(line); 0164 0165 // get line 0166 return m_blocks.at(blockIndex)->setLineMetaData(line, textLine); 0167 } 0168 0169 int TextBuffer::cursorToOffset(KTextEditor::Cursor c) const 0170 { 0171 if (!c.isValid() || c > document()->documentEnd()) { 0172 return -1; 0173 } 0174 0175 int off = 0; 0176 int line = 0; 0177 for (auto block : m_blocks) { 0178 if (block->startLine() + block->lines() < c.line()) { 0179 off += block->blockSize(); 0180 line += block->lines(); 0181 } else { 0182 const int lines = block->lines(); 0183 for (int i = 0; i < lines; ++i) { 0184 if (line >= c.line()) { 0185 off += qMin(c.column(), block->lineLength(line)); 0186 return off; 0187 } 0188 off += block->lineLength(line) + 1; 0189 line++; 0190 } 0191 } 0192 } 0193 0194 Q_ASSERT(false); 0195 return -1; 0196 } 0197 0198 KTextEditor::Cursor TextBuffer::offsetToCursor(int offset) const 0199 { 0200 if (offset >= 0) { 0201 int off = 0; 0202 for (auto block : m_blocks) { 0203 if (off + block->blockSize() < offset) { 0204 off += block->blockSize(); 0205 } else { 0206 const int lines = block->lines(); 0207 int start = block->startLine(); 0208 int end = start + lines; 0209 for (int line = start; line < end; ++line) { 0210 const int len = block->lineLength(line); 0211 if (off + len >= offset) { 0212 return KTextEditor::Cursor(line, offset - off); 0213 } 0214 off += len + 1; 0215 } 0216 } 0217 } 0218 } 0219 return KTextEditor::Cursor::invalid(); 0220 } 0221 0222 QString TextBuffer::text() const 0223 { 0224 QString text; 0225 qsizetype size = 0; 0226 for (auto b : m_blocks) { 0227 size += b->blockSize(); 0228 } 0229 size -= 1; // remove -1, last newline 0230 text.reserve(size); 0231 0232 // combine all blocks 0233 for (TextBlock *block : std::as_const(m_blocks)) { 0234 block->text(text); 0235 } 0236 0237 Q_ASSERT(size == text.size()); 0238 return text; 0239 } 0240 0241 bool TextBuffer::startEditing() 0242 { 0243 // increment transaction counter 0244 ++m_editingTransactions; 0245 0246 // if not first running transaction, do nothing 0247 if (m_editingTransactions > 1) { 0248 return false; 0249 } 0250 0251 // reset information about edit... 0252 m_editingLastRevision = m_revision; 0253 m_editingLastLines = m_lines; 0254 m_editingMinimalLineChanged = -1; 0255 m_editingMaximalLineChanged = -1; 0256 0257 // transaction has started 0258 Q_EMIT m_document->KTextEditor::Document::editingStarted(m_document); 0259 0260 // first transaction started 0261 return true; 0262 } 0263 0264 bool TextBuffer::finishEditing() 0265 { 0266 // only allowed if still transactions running 0267 Q_ASSERT(m_editingTransactions > 0); 0268 0269 // decrement counter 0270 --m_editingTransactions; 0271 0272 // if not last running transaction, do nothing 0273 if (m_editingTransactions > 0) { 0274 return false; 0275 } 0276 0277 // assert that if buffer changed, the line ranges are set and valid! 0278 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1)); 0279 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged)); 0280 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines)); 0281 Q_ASSERT(!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines)); 0282 0283 // transaction has finished 0284 Q_EMIT m_document->KTextEditor::Document::editingFinished(m_document); 0285 0286 // last transaction finished 0287 return true; 0288 } 0289 0290 void TextBuffer::wrapLine(const KTextEditor::Cursor position) 0291 { 0292 // debug output for REAL low-level debugging 0293 BUFFER_DEBUG << "wrapLine" << position; 0294 0295 // only allowed if editing transaction running 0296 Q_ASSERT(m_editingTransactions > 0); 0297 0298 // get block, this will assert on invalid line 0299 int blockIndex = blockForLine(position.line()); 0300 0301 // let the block handle the wrapLine 0302 // this can only lead to one more line in this block 0303 // no other blocks will change 0304 // this call will trigger fixStartLines 0305 ++m_lines; // first alter the line counter, as functions called will need the valid one 0306 m_blocks.at(blockIndex)->wrapLine(position, blockIndex); 0307 0308 // remember changes 0309 ++m_revision; 0310 0311 // update changed line interval 0312 if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { 0313 m_editingMinimalLineChanged = position.line(); 0314 } 0315 0316 if (position.line() <= m_editingMaximalLineChanged) { 0317 ++m_editingMaximalLineChanged; 0318 } else { 0319 m_editingMaximalLineChanged = position.line() + 1; 0320 } 0321 0322 // balance the changed block if needed 0323 balanceBlock(blockIndex); 0324 0325 // emit signal about done change 0326 Q_EMIT m_document->KTextEditor::Document::lineWrapped(m_document, position); 0327 } 0328 0329 void TextBuffer::unwrapLine(int line) 0330 { 0331 // debug output for REAL low-level debugging 0332 BUFFER_DEBUG << "unwrapLine" << line; 0333 0334 // only allowed if editing transaction running 0335 Q_ASSERT(m_editingTransactions > 0); 0336 0337 // line 0 can't be unwrapped 0338 Q_ASSERT(line > 0); 0339 0340 // get block, this will assert on invalid line 0341 int blockIndex = blockForLine(line); 0342 0343 // is this the first line in the block? 0344 bool firstLineInBlock = (line == m_blocks.at(blockIndex)->startLine()); 0345 0346 // let the block handle the unwrapLine 0347 // this can either lead to one line less in this block or the previous one 0348 // the previous one could even end up with zero lines 0349 // this call will trigger fixStartLines 0350 m_blocks.at(blockIndex)->unwrapLine(line, (blockIndex > 0) ? m_blocks.at(blockIndex - 1) : nullptr, firstLineInBlock ? (blockIndex - 1) : blockIndex); 0351 --m_lines; 0352 0353 // decrement index for later fixup, if we modified the block in front of the found one 0354 if (firstLineInBlock) { 0355 --blockIndex; 0356 } 0357 0358 // remember changes 0359 ++m_revision; 0360 0361 // update changed line interval 0362 if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { 0363 m_editingMinimalLineChanged = line - 1; 0364 } 0365 0366 if (line <= m_editingMaximalLineChanged) { 0367 --m_editingMaximalLineChanged; 0368 } else { 0369 m_editingMaximalLineChanged = line - 1; 0370 } 0371 0372 // balance the changed block if needed 0373 balanceBlock(blockIndex); 0374 0375 // emit signal about done change 0376 Q_EMIT m_document->KTextEditor::Document::lineUnwrapped(m_document, line); 0377 } 0378 0379 void TextBuffer::insertText(const KTextEditor::Cursor position, const QString &text) 0380 { 0381 // debug output for REAL low-level debugging 0382 BUFFER_DEBUG << "insertText" << position << text; 0383 0384 // only allowed if editing transaction running 0385 Q_ASSERT(m_editingTransactions > 0); 0386 0387 // skip work, if no text to insert 0388 if (text.isEmpty()) { 0389 return; 0390 } 0391 0392 // get block, this will assert on invalid line 0393 int blockIndex = blockForLine(position.line()); 0394 0395 // let the block handle the insertText 0396 m_blocks.at(blockIndex)->insertText(position, text); 0397 0398 // remember changes 0399 ++m_revision; 0400 0401 // update changed line interval 0402 if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { 0403 m_editingMinimalLineChanged = position.line(); 0404 } 0405 0406 if (position.line() > m_editingMaximalLineChanged) { 0407 m_editingMaximalLineChanged = position.line(); 0408 } 0409 0410 // emit signal about done change 0411 Q_EMIT m_document->KTextEditor::Document::textInserted(m_document, position, text); 0412 } 0413 0414 void TextBuffer::removeText(KTextEditor::Range range) 0415 { 0416 // debug output for REAL low-level debugging 0417 BUFFER_DEBUG << "removeText" << range; 0418 0419 // only allowed if editing transaction running 0420 Q_ASSERT(m_editingTransactions > 0); 0421 0422 // only ranges on one line are supported 0423 Q_ASSERT(range.start().line() == range.end().line()); 0424 0425 // start column <= end column and >= 0 0426 Q_ASSERT(range.start().column() <= range.end().column()); 0427 Q_ASSERT(range.start().column() >= 0); 0428 0429 // skip work, if no text to remove 0430 if (range.isEmpty()) { 0431 return; 0432 } 0433 0434 // get block, this will assert on invalid line 0435 int blockIndex = blockForLine(range.start().line()); 0436 0437 // let the block handle the removeText, retrieve removed text 0438 QString text; 0439 m_blocks.at(blockIndex)->removeText(range, text); 0440 0441 // remember changes 0442 ++m_revision; 0443 0444 // update changed line interval 0445 if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { 0446 m_editingMinimalLineChanged = range.start().line(); 0447 } 0448 0449 if (range.start().line() > m_editingMaximalLineChanged) { 0450 m_editingMaximalLineChanged = range.start().line(); 0451 } 0452 0453 // emit signal about done change 0454 Q_EMIT m_document->KTextEditor::Document::textRemoved(m_document, range, text); 0455 } 0456 0457 int TextBuffer::blockForLine(int line) const 0458 { 0459 // only allow valid lines 0460 if ((line < 0) || (line >= lines())) { 0461 qFatal("out of range line requested in text buffer (%d out of [0, %d])", line, lines()); 0462 } 0463 0464 size_t b = line / BufferBlockSize; 0465 if (b >= m_blocks.size()) { 0466 b = m_blocks.size() - 1; 0467 } 0468 0469 auto block = m_blocks[b]; 0470 if (block->startLine() <= line && line < block->startLine() + block->lines()) { 0471 return b; 0472 } 0473 0474 if (block->startLine() > line) { 0475 for (int i = b - 1; i >= 0; --i) { 0476 auto block = m_blocks[i]; 0477 if (block->startLine() <= line && line < block->startLine() + block->lines()) { 0478 return i; 0479 } 0480 } 0481 } 0482 0483 if (block->startLine() < line || (block->lines() == 0)) { 0484 for (size_t i = b + 1; i < m_blocks.size(); ++i) { 0485 auto block = m_blocks[i]; 0486 if (block->startLine() <= line && line < block->startLine() + block->lines()) { 0487 return i; 0488 } 0489 } 0490 } 0491 0492 qFatal("line requested in text buffer (%d out of [0, %d[), no block found", line, lines()); 0493 return -1; 0494 } 0495 0496 void TextBuffer::fixStartLines(int startBlock) 0497 { 0498 // only allow valid start block 0499 Q_ASSERT(startBlock >= 0); 0500 Q_ASSERT(startBlock < (int)m_blocks.size()); 0501 0502 // new start line for next block 0503 TextBlock *block = m_blocks.at(startBlock); 0504 int newStartLine = block->startLine() + block->lines(); 0505 0506 // fixup block 0507 for (size_t index = startBlock + 1; index < m_blocks.size(); ++index) { 0508 // set new start line 0509 block = m_blocks.at(index); 0510 block->setStartLine(newStartLine); 0511 0512 // calculate next start line 0513 newStartLine += block->lines(); 0514 } 0515 } 0516 0517 void TextBuffer::balanceBlock(int index) 0518 { 0519 // two cases, too big or too small block 0520 TextBlock *blockToBalance = m_blocks.at(index); 0521 0522 // first case, too big one, split it 0523 if (blockToBalance->lines() >= 2 * BufferBlockSize) { 0524 // half the block 0525 int halfSize = blockToBalance->lines() / 2; 0526 0527 // create and insert new block behind current one, already set right start line 0528 TextBlock *newBlock = blockToBalance->splitBlock(halfSize); 0529 Q_ASSERT(newBlock); 0530 m_blocks.insert(m_blocks.begin() + index + 1, newBlock); 0531 0532 // split is done 0533 return; 0534 } 0535 0536 // second case: possibly too small block 0537 0538 // if only one block, no chance to unite 0539 // same if this is first block, we always append to previous one 0540 if (index == 0) { 0541 return; 0542 } 0543 0544 // block still large enough, do nothing 0545 if (2 * blockToBalance->lines() > BufferBlockSize) { 0546 return; 0547 } 0548 0549 // unite small block with predecessor 0550 TextBlock *targetBlock = m_blocks.at(index - 1); 0551 0552 // merge block 0553 blockToBalance->mergeBlock(targetBlock); 0554 0555 // delete old block 0556 delete blockToBalance; 0557 m_blocks.erase(m_blocks.begin() + index); 0558 } 0559 0560 void TextBuffer::debugPrint(const QString &title) const 0561 { 0562 // print header with title 0563 printf("%s (lines: %d)\n", qPrintable(title), m_lines); 0564 0565 // print all blocks 0566 for (size_t i = 0; i < m_blocks.size(); ++i) { 0567 m_blocks.at(i)->debugPrint(i); 0568 } 0569 } 0570 0571 bool TextBuffer::load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec) 0572 { 0573 // fallback codec must exist 0574 Q_ASSERT(!m_fallbackTextCodec.isEmpty()); 0575 0576 // codec must be set! 0577 Q_ASSERT(!m_textCodec.isEmpty()); 0578 0579 // first: clear buffer in any case! 0580 clear(); 0581 0582 // construct the file loader for the given file, with correct prober type 0583 Kate::TextLoader file(filename, m_encodingProberType); 0584 0585 // triple play, maximal three loading rounds 0586 // 0) use the given encoding, be done, if no encoding errors happen 0587 // 1) use BOM to decided if Unicode or if that fails, use encoding prober, if no encoding errors happen, be done 0588 // 2) use fallback encoding, be done, if no encoding errors happen 0589 // 3) use again given encoding, be done in any case 0590 for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) { 0591 // kill all blocks beside first one 0592 for (size_t b = 1; b < m_blocks.size(); ++b) { 0593 TextBlock *block = m_blocks.at(b); 0594 block->clearLines(); 0595 delete block; 0596 } 0597 m_blocks.resize(1); 0598 0599 // remove lines in first block 0600 m_blocks.back()->clearLines(); 0601 m_lines = 0; 0602 0603 // try to open file, with given encoding 0604 // in round 0 + 3 use the given encoding from user 0605 // in round 1 use 0, to trigger detection 0606 // in round 2 use fallback 0607 QString codec = m_textCodec; 0608 if (i == 1) { 0609 codec.clear(); 0610 } else if (i == 2) { 0611 codec = m_fallbackTextCodec; 0612 } 0613 0614 if (!file.open(codec)) { 0615 // create one dummy textline, in any case 0616 m_blocks.back()->appendLine(QString()); 0617 m_lines++; 0618 return false; 0619 } 0620 0621 // read in all lines... 0622 encodingErrors = false; 0623 while (!file.eof()) { 0624 // read line 0625 int offset = 0; 0626 int length = 0; 0627 bool currentError = !file.readLine(offset, length); 0628 encodingErrors = encodingErrors || currentError; 0629 0630 // bail out on encoding error, if not last round! 0631 if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) { 0632 BUFFER_DEBUG << "Failed try to load file" << filename << "with codec" << file.textCodec(); 0633 break; 0634 } 0635 0636 // get Unicode data for this line 0637 const QChar *unicodeData = file.unicode() + offset; 0638 0639 if (longestLineLoaded < length) { 0640 longestLineLoaded = length; 0641 } 0642 0643 // split lines, if too large 0644 do { 0645 // calculate line length 0646 int lineLength = length; 0647 if ((m_lineLengthLimit > 0) && (lineLength > m_lineLengthLimit)) { 0648 // search for place to wrap 0649 int spacePosition = m_lineLengthLimit - 1; 0650 for (int testPosition = m_lineLengthLimit - 1; (testPosition >= 0) && (testPosition >= (m_lineLengthLimit - (m_lineLengthLimit / 10))); 0651 --testPosition) { 0652 // wrap place found? 0653 if (unicodeData[testPosition].isSpace() || unicodeData[testPosition].isPunct()) { 0654 spacePosition = testPosition; 0655 break; 0656 } 0657 } 0658 0659 // wrap the line 0660 lineLength = spacePosition + 1; 0661 length -= lineLength; 0662 tooLongLinesWrapped = true; 0663 } else { 0664 // be done after this round 0665 length = 0; 0666 } 0667 0668 // construct new text line with content from file 0669 // move data pointer 0670 QString textLine(unicodeData, lineLength); 0671 unicodeData += lineLength; 0672 0673 // ensure blocks aren't too large 0674 if (m_blocks.back()->lines() >= BufferBlockSize) { 0675 m_blocks.push_back(new TextBlock(this, m_blocks.back()->startLine() + m_blocks.back()->lines())); 0676 } 0677 0678 // append line to last block 0679 m_blocks.back()->appendLine(textLine); 0680 ++m_lines; 0681 } while (length > 0); 0682 } 0683 0684 // if no encoding error, break out of reading loop 0685 if (!encodingErrors) { 0686 // remember used codec, might change bom setting 0687 setTextCodec(file.textCodec()); 0688 break; 0689 } 0690 } 0691 0692 // save checksum of file on disk 0693 setDigest(file.digest()); 0694 0695 // remember if BOM was found 0696 if (file.byteOrderMarkFound()) { 0697 setGenerateByteOrderMark(true); 0698 } 0699 0700 // remember eol mode, if any found in file 0701 if (file.eol() != eolUnknown) { 0702 setEndOfLineMode(file.eol()); 0703 } 0704 0705 // remember mime type for filter device 0706 m_mimeTypeForFilterDev = file.mimeTypeForFilterDev(); 0707 0708 // assert that one line is there! 0709 Q_ASSERT(m_lines > 0); 0710 0711 // report CODEC + ERRORS 0712 BUFFER_DEBUG << "Loaded file " << filename << "with codec" << m_textCodec << (encodingErrors ? "with" : "without") << "encoding errors"; 0713 0714 // report BOM 0715 BUFFER_DEBUG << (file.byteOrderMarkFound() ? "Found" : "Didn't find") << "byte order mark"; 0716 0717 // report filter device mime-type 0718 BUFFER_DEBUG << "used filter device for mime-type" << m_mimeTypeForFilterDev; 0719 0720 // emit success 0721 Q_EMIT loaded(filename, encodingErrors); 0722 0723 // file loading worked, modulo encoding problems 0724 return true; 0725 } 0726 0727 const QByteArray &TextBuffer::digest() const 0728 { 0729 return m_digest; 0730 } 0731 0732 void TextBuffer::setDigest(const QByteArray &checksum) 0733 { 0734 m_digest = checksum; 0735 } 0736 0737 void TextBuffer::setTextCodec(const QString &codec) 0738 { 0739 m_textCodec = codec; 0740 0741 // enforce bom for some encodings 0742 if (const auto setEncoding = QStringConverter::encodingForName(m_textCodec.toUtf8().constData())) { 0743 for (const auto encoding : {QStringConverter::Utf16, 0744 QStringConverter::Utf16BE, 0745 QStringConverter::Utf16LE, 0746 QStringConverter::Utf32, 0747 QStringConverter::Utf32BE, 0748 QStringConverter::Utf32LE}) { 0749 if (setEncoding == encoding) { 0750 setGenerateByteOrderMark(true); 0751 break; 0752 } 0753 } 0754 } 0755 } 0756 0757 bool TextBuffer::save(const QString &filename) 0758 { 0759 // codec must be set, else below we fail! 0760 Q_ASSERT(!m_textCodec.isEmpty()); 0761 0762 SaveResult saveRes = saveBufferUnprivileged(filename); 0763 0764 if (saveRes == SaveResult::Failed) { 0765 return false; 0766 } else if (saveRes == SaveResult::MissingPermissions) { 0767 // either unit-test mode or we're missing permissions to write to the 0768 // file => use temporary file and try to use authhelper 0769 if (!saveBufferEscalated(filename)) { 0770 return false; 0771 } 0772 } 0773 0774 // remember this revision as last saved 0775 m_history.setLastSavedRevision(); 0776 0777 // inform that we have saved the state 0778 markModifiedLinesAsSaved(); 0779 0780 // emit that file was saved and be done 0781 Q_EMIT saved(filename); 0782 return true; 0783 } 0784 0785 bool TextBuffer::saveBuffer(const QString &filename, KCompressionDevice &saveFile) 0786 { 0787 QStringEncoder encoder(m_textCodec.toUtf8().constData(), generateByteOrderMark() ? QStringConverter::Flag::WriteBom : QStringConverter::Flag::Default); 0788 0789 // our loved eol string ;) 0790 QString eol = QStringLiteral("\n"); 0791 if (endOfLineMode() == eolDos) { 0792 eol = QStringLiteral("\r\n"); 0793 } else if (endOfLineMode() == eolMac) { 0794 eol = QStringLiteral("\r"); 0795 } 0796 0797 // just dump the lines out ;) 0798 for (int i = 0; i < m_lines; ++i) { 0799 // dump current line 0800 saveFile.write(encoder.encode(line(i).text())); 0801 0802 // append correct end of line string 0803 if ((i + 1) < m_lines) { 0804 saveFile.write(encoder.encode(eol)); 0805 } 0806 0807 // early out on stream errors 0808 if (saveFile.error() != QFileDevice::NoError) { 0809 return false; 0810 } 0811 } 0812 0813 // TODO: this only writes bytes when there is text. This is a fine optimization for most cases, but this makes saving 0814 // an empty file with the BOM set impossible (results to an empty file with 0 bytes, no BOM) 0815 0816 // close the file, we might want to read from underlying buffer below 0817 saveFile.close(); 0818 0819 // did save work? 0820 if (saveFile.error() != QFileDevice::NoError) { 0821 BUFFER_DEBUG << "Saving file " << filename << "failed with error" << saveFile.errorString(); 0822 return false; 0823 } 0824 0825 return true; 0826 } 0827 0828 TextBuffer::SaveResult TextBuffer::saveBufferUnprivileged(const QString &filename) 0829 { 0830 if (m_alwaysUseKAuthForSave) { 0831 // unit-testing mode, simulate we need privileges 0832 return SaveResult::MissingPermissions; 0833 } 0834 0835 // construct correct filter device 0836 // we try to use the same compression as for opening 0837 const KCompressionDevice::CompressionType type = KCompressionDevice::compressionTypeForMimeType(m_mimeTypeForFilterDev); 0838 auto saveFile = std::make_unique<KCompressionDevice>(filename, type); 0839 0840 if (!saveFile->open(QIODevice::WriteOnly)) { 0841 #ifdef CAN_USE_ERRNO 0842 if (errno != EACCES) { 0843 return SaveResult::Failed; 0844 } 0845 #endif 0846 return SaveResult::MissingPermissions; 0847 } 0848 0849 if (!saveBuffer(filename, *saveFile)) { 0850 return SaveResult::Failed; 0851 } 0852 0853 return SaveResult::Success; 0854 } 0855 0856 bool TextBuffer::saveBufferEscalated(const QString &filename) 0857 { 0858 #if HAVE_KAUTH 0859 // construct correct filter device 0860 // we try to use the same compression as for opening 0861 const KCompressionDevice::CompressionType type = KCompressionDevice::compressionTypeForMimeType(m_mimeTypeForFilterDev); 0862 auto saveFile = std::make_unique<KCompressionDevice>(filename, type); 0863 uint ownerId = -2; 0864 uint groupId = -2; 0865 std::unique_ptr<QIODevice> temporaryBuffer; 0866 0867 // Memorize owner and group. 0868 const QFileInfo fileInfo(filename); 0869 if (fileInfo.exists()) { 0870 ownerId = fileInfo.ownerId(); 0871 groupId = fileInfo.groupId(); 0872 } 0873 0874 // if that fails we need more privileges to save this file 0875 // -> we write to a temporary file and then send its path to KAuth action for privileged save 0876 temporaryBuffer = std::make_unique<QBuffer>(); 0877 0878 // open buffer for write and read (read is used for checksum computing and writing to temporary file) 0879 if (!temporaryBuffer->open(QIODevice::ReadWrite)) { 0880 return false; 0881 } 0882 0883 // we are now saving to a temporary buffer with potential compression proxy 0884 saveFile = std::make_unique<KCompressionDevice>(temporaryBuffer.get(), false, type); 0885 if (!saveFile->open(QIODevice::WriteOnly)) { 0886 return false; 0887 } 0888 0889 if (!saveBuffer(filename, *saveFile)) { 0890 return false; 0891 } 0892 0893 // temporary buffer was used to save the file 0894 // -> computing checksum 0895 // -> saving to temporary file 0896 // -> copying the temporary file to the original file location with KAuth action 0897 QTemporaryFile tempFile; 0898 if (!tempFile.open()) { 0899 return false; 0900 } 0901 0902 // go to QBuffer start 0903 temporaryBuffer->seek(0); 0904 0905 // read contents of QBuffer and add them to checksum utility as well as to QTemporaryFile 0906 char buffer[bufferLength]; 0907 qint64 read = -1; 0908 QCryptographicHash cryptographicHash(SecureTextBuffer::checksumAlgorithm); 0909 while ((read = temporaryBuffer->read(buffer, bufferLength)) > 0) { 0910 cryptographicHash.addData(QByteArrayView(buffer, read)); 0911 if (tempFile.write(buffer, read) == -1) { 0912 return false; 0913 } 0914 } 0915 if (!tempFile.flush()) { 0916 return false; 0917 } 0918 0919 // prepare data for KAuth action 0920 QVariantMap kAuthActionArgs; 0921 kAuthActionArgs.insert(QStringLiteral("sourceFile"), tempFile.fileName()); 0922 kAuthActionArgs.insert(QStringLiteral("targetFile"), filename); 0923 kAuthActionArgs.insert(QStringLiteral("checksum"), cryptographicHash.result()); 0924 kAuthActionArgs.insert(QStringLiteral("ownerId"), ownerId); 0925 kAuthActionArgs.insert(QStringLiteral("groupId"), groupId); 0926 0927 // call save with elevated privileges 0928 if (KTextEditor::EditorPrivate::unitTestMode()) { 0929 // unit testing purposes only 0930 if (!SecureTextBuffer::savefile(kAuthActionArgs).succeeded()) { 0931 return false; 0932 } 0933 } else { 0934 KAuth::Action kAuthSaveAction(QStringLiteral("org.kde.ktexteditor6.katetextbuffer.savefile")); 0935 kAuthSaveAction.setHelperId(QStringLiteral("org.kde.ktexteditor6.katetextbuffer")); 0936 kAuthSaveAction.setArguments(kAuthActionArgs); 0937 KAuth::ExecuteJob *job = kAuthSaveAction.execute(); 0938 if (!job->exec()) { 0939 return false; 0940 } 0941 } 0942 0943 return true; 0944 #else 0945 Q_UNUSED(filename); 0946 return false; 0947 #endif 0948 } 0949 0950 void TextBuffer::notifyAboutRangeChange(KTextEditor::View *view, KTextEditor::LineRange lineRange, bool needsRepaint) 0951 { 0952 // ignore calls if no document is around 0953 if (!m_document) { 0954 return; 0955 } 0956 0957 // update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really 0958 // just create 20k ranges in a go and you wait seconds on a decent machine 0959 const QList<KTextEditor::View *> views = m_document->views(); 0960 for (KTextEditor::View *curView : views) { 0961 // filter wrong views 0962 if (view && view != curView) { 0963 continue; 0964 } 0965 0966 // notify view, it is really a kate view 0967 static_cast<KTextEditor::ViewPrivate *>(curView)->notifyAboutRangeChange(lineRange, needsRepaint); 0968 } 0969 } 0970 0971 void TextBuffer::markModifiedLinesAsSaved() 0972 { 0973 for (TextBlock *block : std::as_const(m_blocks)) { 0974 block->markModifiedLinesAsSaved(); 0975 } 0976 } 0977 } 0978 0979 #include "moc_katetextbuffer.cpp"