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"