diff --git a/src/buffer/katesecuretextbuffer.cpp b/src/buffer/katesecuretextbuffer.cpp index 979bb62f..98b96cef 100644 --- a/src/buffer/katesecuretextbuffer.cpp +++ b/src/buffer/katesecuretextbuffer.cpp @@ -1,156 +1,166 @@ /* This file is part of the KTextEditor project. * * Copyright (C) 2017 KDE Developers * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katesecuretextbuffer_p.h" #ifndef Q_OS_WIN #include #include #endif #include #include +#include +#include #include -#include KAUTH_HELPER_MAIN("org.kde.ktexteditor.katetextbuffer", SecureTextBuffer) ActionReply SecureTextBuffer::savefile(const QVariantMap &args) { - const ActionMode actionMode = static_cast(args[QLatin1String("actionMode")].toInt()); + const QString sourceFile = args[QLatin1String("sourceFile")].toString(); const QString targetFile = args[QLatin1String("targetFile")].toString(); + const QByteArray checksum = args[QLatin1String("checksum")].toByteArray(); const uint ownerId = (uint) args[QLatin1String("ownerId")].toInt(); + const uint groupId = (uint) args[QLatin1String("groupId")].toInt(); - if (actionMode == ActionMode::Prepare) { - - const QString temporaryFile = prepareTempFileInternal(targetFile, ownerId); - - if (temporaryFile.isEmpty()) { - return ActionReply::HelperErrorReply(); - } - - ActionReply reply; - reply.addData(QLatin1String("temporaryFile"), temporaryFile); - - return reply; - - } - - if (actionMode == ActionMode::Move) { - - const QString sourceFile = args[QLatin1String("sourceFile")].toString(); - const uint groupId = (uint) args[QLatin1String("groupId")].toInt(); - - if (moveFileInternal(sourceFile, targetFile, ownerId, groupId)) { - return ActionReply::SuccessReply(); - } + if (saveFileInternal(sourceFile, targetFile, checksum, ownerId, groupId)) { + return ActionReply::SuccessReply(); } return ActionReply::HelperErrorReply(); } -bool SecureTextBuffer::moveFileInternal(const QString &sourceFile, const QString &targetFile, const uint ownerId, const uint groupId) +bool SecureTextBuffer::saveFileInternal(const QString &sourceFile, const QString &targetFile, + const QByteArray &checksum, const uint ownerId, const uint groupId) { - const bool newFile = !QFile::exists(targetFile); - bool atomicRenameSucceeded = false; - - /** - * There is no atomic rename operation publicly exposed by Qt. - * - * We use std::rename for UNIX and for now no-op for windows (triggers fallback). - * - * As fallback we are copying source file to destination with the help of QSaveFile - * to ensure targetFile is overwritten safely. - */ -#ifndef Q_OS_WIN - const int result = std::rename(QFile::encodeName(sourceFile).constData(), QFile::encodeName(targetFile).constData()); - if (result == 0) { - syncToDisk(QFile(targetFile).handle()); - atomicRenameSucceeded = true; + QFileInfo targetFileInfo(targetFile); + if (!QDir::setCurrent(targetFileInfo.dir().path())) { + return false; } -#else - atomicRenameSucceeded = false; -#endif - if (!atomicRenameSucceeded) { - // as fallback copy the temporary file to the target with help of QSaveFile - QFile readFile(sourceFile); - QSaveFile saveFile(targetFile); - if (!readFile.open(QIODevice::ReadOnly) || !saveFile.open(QIODevice::WriteOnly)) { - return false; - } - char buffer[bufferLength]; - qint64 read = -1; - while ((read = readFile.read(buffer, bufferLength)) > 0) { - if (saveFile.write(buffer, read) == -1) { - return false; - } - } - if (read == -1 || !saveFile.commit()) { + // get information about target file + const QString targetFileName = targetFileInfo.fileName(); + targetFileInfo.setFile(targetFileName); + const bool newFile = !targetFileInfo.exists(); + + // open source and target file + QFile readFile(sourceFile); + //TODO use QSaveFile for saving contents and automatic atomic move on commit() when QSaveFile's security problem + // (default temporary file permissions) is fixed + // + // We will first generate temporary filename and then use it relatively to prevent an attacker + // to trick us to write contents to a different file by changing underlying directory. + QTemporaryFile tempFile(targetFileName); + if (!tempFile.open()) { + return false; + } + tempFile.close(); + QString tempFileName = QFileInfo(tempFile).fileName(); + tempFile.setFileName(tempFileName); + if (!readFile.open(QIODevice::ReadOnly) || !tempFile.open()) { + return false; + } + const int tempFileDescriptor = tempFile.handle(); + + // prepare checksum maker + QCryptographicHash cryptographicHash(checksumAlgorithm); + + // copy contents + char buffer[bufferLength]; + qint64 read = -1; + while ((read = readFile.read(buffer, bufferLength)) > 0) { + cryptographicHash.addData(buffer, read); + if (tempFile.write(buffer, read) == -1) { return false; } } - if (!newFile) { - // ensure file has the same owner and group as before - setOwner(targetFile, ownerId, groupId); + // check that copying was successful and checksum matched + QByteArray localChecksum = cryptographicHash.result(); + if (read == -1 || localChecksum != checksum || !tempFile.flush()) { + return false; } - return true; -} + tempFile.close(); -QString SecureTextBuffer::prepareTempFileInternal(const QString &targetFile, const uint ownerId) -{ - QTemporaryFile tempFile(targetFile); - if (!tempFile.open()) { - return QString(); + if (newFile) { + // ensure new file is readable by anyone + tempFile.setPermissions(tempFile.permissions() | QFile::Permission::ReadGroup | QFile::Permission::ReadOther); + } else { + // ensure the same file permissions + tempFile.setPermissions(targetFileInfo.permissions()); + // ensure file has the same owner and group as before + setOwner(tempFileDescriptor, ownerId, groupId); } - tempFile.setAutoRemove(false); - setOwner(tempFile.fileName(), ownerId, -1); - return tempFile.fileName(); + + // rename temporary file to the target file + if (moveFile(tempFileName, targetFileName)) { + // temporary file was renamed, there is nothing to remove anymore + tempFile.setAutoRemove(false); + return true; + } + return false; } -void SecureTextBuffer::setOwner(const QString &filename, const uint ownerId, const uint groupId) +void SecureTextBuffer::setOwner(const int filedes, const uint ownerId, const uint groupId) { #ifndef Q_OS_WIN if (ownerId != (uint)-2 && groupId != (uint)-2) { - const int result = chown(QFile::encodeName(filename).constData(), ownerId, groupId); + const int result = fchown(filedes, ownerId, groupId); // set at least correct group if owner cannot be changed if (result != 0 && errno == EPERM) { - chown(QFile::encodeName(filename).constData(), getuid(), groupId); + fchown(filedes, getuid(), groupId); } } #else // no-op for windows #endif } +bool SecureTextBuffer::moveFile(const QString &sourceFile, const QString &targetFile) +{ +#ifndef Q_OS_WIN + const int result = std::rename(QFile::encodeName(sourceFile).constData(), QFile::encodeName(targetFile).constData()); + if (result == 0) { + syncToDisk(QFile(targetFile).handle()); + return true; + } + return false; +#else + // use racy fallback for windows + QFile::remove(targetFile); + return QFile::rename(sourceFile, targetFile); +#endif +} + void SecureTextBuffer::syncToDisk(const int fd) { #ifndef Q_OS_WIN #ifdef HAVE_FDATASYNC fdatasync(fd); #else fsync(fd); #endif #else // no-op for windows #endif -} \ No newline at end of file +} + diff --git a/src/buffer/katesecuretextbuffer_p.h b/src/buffer/katesecuretextbuffer_p.h index b931aa27..a38285b6 100644 --- a/src/buffer/katesecuretextbuffer_p.h +++ b/src/buffer/katesecuretextbuffer_p.h @@ -1,89 +1,80 @@ /* This file is part of the KTextEditor project. * * Copyright (C) 2017 KDE Developers * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_SECURE_TEXTBUFFER_P_H #define KATE_SECURE_TEXTBUFFER_P_H #include #include +#include #include using namespace KAuth; /** * Class used as KAuth helper binary. * It is supposed to be called through KAuth action. * * It also contains couple of common methods intended to be used * directly by TextBuffer as well as from helper binary. * * This class should only be used by TextBuffer. */ class SecureTextBuffer : public QObject { Q_OBJECT public: - /** - * We support Prepare action for temporary file creation - * and Move action for moving final file to its destination - */ - enum ActionMode { - Prepare = 1, - Move = 2 - }; - SecureTextBuffer() {} ~SecureTextBuffer() {} /** - * Common helper methods + * Common helper method */ - static void setOwner(const QString &filename, const uint ownerId, const uint groupId); - static void syncToDisk(const int fd); + static void setOwner(const int filedes, const uint ownerId, const uint groupId); + + static const QCryptographicHash::Algorithm checksumAlgorithm = QCryptographicHash::Algorithm::Sha512; private: static const qint64 bufferLength = 4096; /** - * Creates temporary file based on given target file path. - * Temporary file is set to not be deleted on object destroy - * so KTextEditor can save contents in it. + * Saves file contents using sets permissions. */ - static QString prepareTempFileInternal(const QString &targetFile, const uint ownerId); + static bool saveFileInternal(const QString &sourceFile, const QString &targetFile, + const QByteArray &checksum, const uint ownerId, const uint groupId); - /** - * Move file to its given destination and set owner. - */ - static bool moveFileInternal(const QString &sourceFile, const QString &targetFile, const uint ownerId, const uint groupId); + static bool moveFile(const QString &sourceFile, const QString &targetFile); + + static void syncToDisk(const int fd); public Q_SLOTS: /** * KAuth action to perform both prepare or move work based on given parameters. * We keep this code in one method to prevent multiple KAuth user queries during one save action. */ static ActionReply savefile(const QVariantMap &args); }; #endif diff --git a/src/buffer/katetextbuffer.cpp b/src/buffer/katetextbuffer.cpp index 95603fa7..aa5a4555 100644 --- a/src/buffer/katetextbuffer.cpp +++ b/src/buffer/katetextbuffer.cpp @@ -1,1061 +1,1045 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #include "kateglobal.h" #include "katetextbuffer.h" #include "katesecuretextbuffer_p.h" #include "katetextloader.h" // this is unfortunate, but needed for performance #include "katedocument.h" #include "kateview.h" #include "katepartdebug.h" #ifndef Q_OS_WIN #include #endif #include #include -#include #include +#include +#include #if 0 #define BUFFER_DEBUG qCDebug(LOG_KTE) #else #define BUFFER_DEBUG if (0) qCDebug(LOG_KTE) #endif namespace Kate { TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize, bool alwaysUseKAuth) : QObject(parent) , m_document(parent) , m_history(*this) , m_blockSize(blockSize) , m_lines(0) , m_lastUsedBlock(0) , m_revision(0) , m_editingTransactions(0) , m_editingLastRevision(0) , m_editingLastLines(0) , m_editingMinimalLineChanged(-1) , m_editingMaximalLineChanged(-1) , m_encodingProberType(KEncodingProber::Universal) , m_fallbackTextCodec(nullptr) , m_textCodec(nullptr) , m_generateByteOrderMark(false) , m_endOfLineMode(eolUnix) , m_newLineAtEof(false) , m_lineLengthLimit(4096) , m_alwaysUseKAuthForSave(alwaysUseKAuth) { // minimal block size must be > 0 Q_ASSERT(m_blockSize > 0); // create initial state clear(); } TextBuffer::~TextBuffer() { // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect m_document = nullptr; // not allowed during editing Q_ASSERT(m_editingTransactions == 0); // kill all ranges, work on copy, they will remove themself from the hash QSet copyRanges = m_ranges; qDeleteAll(copyRanges); Q_ASSERT(m_ranges.empty()); // clean out all cursors and lines, only cursors belonging to range will survive foreach (TextBlock *block, m_blocks) { block->deleteBlockContent(); } // delete all blocks, now that all cursors are really deleted // else asserts in destructor of blocks will fail! qDeleteAll(m_blocks); m_blocks.clear(); // kill all invalid cursors, do this after block deletion, to uncover if they might be still linked in blocks QSet copyCursors = m_invalidCursors; qDeleteAll(copyCursors); Q_ASSERT(m_invalidCursors.empty()); } void TextBuffer::invalidateRanges() { // invalidate all ranges, work on copy, they might delete themself... QSet copyRanges = m_ranges; foreach (TextRange *range, copyRanges) { range->setRange(KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()); } } void TextBuffer::clear() { // not allowed during editing Q_ASSERT(m_editingTransactions == 0); invalidateRanges(); // new block for empty buffer TextBlock *newBlock = new TextBlock(this, 0); newBlock->appendLine(QString()); // clean out all cursors and lines, either move them to newBlock or invalidate them, if belonging to a range foreach (TextBlock *block, m_blocks) { block->clearBlockContent(newBlock); } // kill all buffer blocks qDeleteAll(m_blocks); m_blocks.clear(); // insert one block with one empty line m_blocks.append(newBlock); // reset lines and last used block m_lines = 1; m_lastUsedBlock = 0; // reset revision m_revision = 0; // reset bom detection m_generateByteOrderMark = false; // reset the filter device m_mimeTypeForFilterDev = QStringLiteral("text/plain"); // clear edit history m_history.clear(); // we got cleared emit cleared(); } TextLine TextBuffer::line(int line) const { // get block, this will assert on invalid line int blockIndex = blockForLine(line); // get line return m_blocks.at(blockIndex)->line(line); } QString TextBuffer::text() const { QString text; // combine all blocks foreach (TextBlock *block, m_blocks) { block->text(text); } // return generated string return text; } bool TextBuffer::startEditing() { // increment transaction counter ++m_editingTransactions; // if not first running transaction, do nothing if (m_editingTransactions > 1) { return false; } // reset information about edit... m_editingLastRevision = m_revision; m_editingLastLines = m_lines; m_editingMinimalLineChanged = -1; m_editingMaximalLineChanged = -1; // transaction has started emit editingStarted(); if (m_document) emit m_document->KTextEditor::Document::editingStarted(m_document); // first transaction started return true; } bool TextBuffer::finishEditing() { // only allowed if still transactions running Q_ASSERT(m_editingTransactions > 0); // decrement counter --m_editingTransactions; // if not last running transaction, do nothing if (m_editingTransactions > 0) { return false; } // assert that if buffer changed, the line ranges are set and valid! Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1)); Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged)); Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines)); Q_ASSERT(!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines)); // transaction has finished emit editingFinished(); if (m_document) emit m_document->KTextEditor::Document::editingFinished(m_document); // last transaction finished return true; } void TextBuffer::wrapLine(const KTextEditor::Cursor &position) { // debug output for REAL low-level debugging BUFFER_DEBUG << "wrapLine" << position; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // get block, this will assert on invalid line int blockIndex = blockForLine(position.line()); /** * let the block handle the wrapLine * this can only lead to one more line in this block * no other blocks will change * this call will trigger fixStartLines */ ++m_lines; // first alter the line counter, as functions called will need the valid one m_blocks.at(blockIndex)->wrapLine(position, blockIndex); // remember changes ++m_revision; // update changed line interval if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = position.line(); } if (position.line() <= m_editingMaximalLineChanged) { ++m_editingMaximalLineChanged; } else { m_editingMaximalLineChanged = position.line() + 1; } // balance the changed block if needed balanceBlock(blockIndex); // emit signal about done change emit lineWrapped(position); if (m_document) emit m_document->KTextEditor::Document::lineWrapped(m_document, position); } void TextBuffer::unwrapLine(int line) { // debug output for REAL low-level debugging BUFFER_DEBUG << "unwrapLine" << line; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // line 0 can't be unwrapped Q_ASSERT(line > 0); // get block, this will assert on invalid line int blockIndex = blockForLine(line); // is this the first line in the block? bool firstLineInBlock = (line == m_blocks.at(blockIndex)->startLine()); /** * let the block handle the unwrapLine * this can either lead to one line less in this block or the previous one * the previous one could even end up with zero lines * this call will trigger fixStartLines */ m_blocks.at(blockIndex)->unwrapLine(line, (blockIndex > 0) ? m_blocks.at(blockIndex - 1) : nullptr, firstLineInBlock ? (blockIndex - 1) : blockIndex); --m_lines; // decrement index for later fixup, if we modified the block in front of the found one if (firstLineInBlock) { --blockIndex; } // remember changes ++m_revision; // update changed line interval if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = line - 1; } if (line <= m_editingMaximalLineChanged) { --m_editingMaximalLineChanged; } else { m_editingMaximalLineChanged = line - 1; } // balance the changed block if needed balanceBlock(blockIndex); // emit signal about done change emit lineUnwrapped(line); if (m_document) emit m_document->KTextEditor::Document::lineUnwrapped(m_document, line); } void TextBuffer::insertText(const KTextEditor::Cursor &position, const QString &text) { // debug output for REAL low-level debugging BUFFER_DEBUG << "insertText" << position << text; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // skip work, if no text to insert if (text.isEmpty()) { return; } // get block, this will assert on invalid line int blockIndex = blockForLine(position.line()); // let the block handle the insertText m_blocks.at(blockIndex)->insertText(position, text); // remember changes ++m_revision; // update changed line interval if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = position.line(); } if (position.line() > m_editingMaximalLineChanged) { m_editingMaximalLineChanged = position.line(); } // emit signal about done change emit textInserted(position, text); if (m_document) emit m_document->KTextEditor::Document::textInserted(m_document, position, text); } void TextBuffer::removeText(const KTextEditor::Range &range) { // debug output for REAL low-level debugging BUFFER_DEBUG << "removeText" << range; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // only ranges on one line are supported Q_ASSERT(range.start().line() == range.end().line()); // start column <= end column and >= 0 Q_ASSERT(range.start().column() <= range.end().column()); Q_ASSERT(range.start().column() >= 0); // skip work, if no text to remove if (range.isEmpty()) { return; } // get block, this will assert on invalid line int blockIndex = blockForLine(range.start().line()); // let the block handle the removeText, retrieve removed text QString text; m_blocks.at(blockIndex)->removeText(range, text); // remember changes ++m_revision; // update changed line interval if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = range.start().line(); } if (range.start().line() > m_editingMaximalLineChanged) { m_editingMaximalLineChanged = range.start().line(); } // emit signal about done change emit textRemoved(range, text); if (m_document) emit m_document->KTextEditor::Document::textRemoved(m_document, range, text); } int TextBuffer::blockForLine(int line) const { // only allow valid lines if ((line < 0) || (line >= lines())) { qFatal("out of range line requested in text buffer (%d out of [0, %d[)", line, lines()); } // we need blocks and last used block should not be negative Q_ASSERT(!m_blocks.isEmpty()); Q_ASSERT(m_lastUsedBlock >= 0); /** * shortcut: try last block first */ if (m_lastUsedBlock < m_blocks.size()) { /** * check if block matches * if yes, just return again this block */ TextBlock *block = m_blocks[m_lastUsedBlock]; const int start = block->startLine(); const int lines = block->lines(); if (start <= line && line < (start + lines)) { return m_lastUsedBlock; } } /** * search for right block * use binary search * if we leave this loop not by returning the found element we have an error */ int blockStart = 0; int blockEnd = m_blocks.size() - 1; while (blockEnd >= blockStart) { // get middle and ensure it is OK int middle = blockStart + ((blockEnd - blockStart) / 2); Q_ASSERT(middle >= 0); Q_ASSERT(middle < m_blocks.size()); // facts bout this block TextBlock *block = m_blocks[middle]; const int start = block->startLine(); const int lines = block->lines(); // right block found, remember it and return it if (start <= line && line < (start + lines)) { m_lastUsedBlock = middle; return middle; } // half our stuff ;) if (line < start) { blockEnd = middle - 1; } else { blockStart = middle + 1; } } // we should always find a block qFatal("line requested in text buffer (%d out of [0, %d[), no block found", line, lines()); return -1; } void TextBuffer::fixStartLines(int startBlock) { // only allow valid start block Q_ASSERT(startBlock >= 0); Q_ASSERT(startBlock < m_blocks.size()); // new start line for next block TextBlock *block = m_blocks.at(startBlock); int newStartLine = block->startLine() + block->lines(); // fixup block for (int index = startBlock + 1; index < m_blocks.size(); ++index) { // set new start line block = m_blocks.at(index); block->setStartLine(newStartLine); // calculate next start line newStartLine += block->lines(); } } void TextBuffer::balanceBlock(int index) { /** * two cases, too big or too small block */ TextBlock *blockToBalance = m_blocks.at(index); // first case, too big one, split it if (blockToBalance->lines() >= 2 * m_blockSize) { // half the block int halfSize = blockToBalance->lines() / 2; // create and insert new block behind current one, already set right start line TextBlock *newBlock = blockToBalance->splitBlock(halfSize); Q_ASSERT(newBlock); m_blocks.insert(m_blocks.begin() + index + 1, newBlock); // split is done return; } // second case: possibly too small block // if only one block, no chance to unite // same if this is first block, we always append to previous one if (index == 0) { return; } // block still large enough, do nothing if (2 * blockToBalance->lines() > m_blockSize) { return; } // unite small block with predecessor TextBlock *targetBlock = m_blocks.at(index - 1); // merge block blockToBalance->mergeBlock(targetBlock); // delete old block delete blockToBalance; m_blocks.erase(m_blocks.begin() + index); } void TextBuffer::debugPrint(const QString &title) const { // print header with title printf("%s (lines: %d bs: %d)\n", qPrintable(title), m_lines, m_blockSize); // print all blocks for (int i = 0; i < m_blocks.size(); ++i) { m_blocks.at(i)->debugPrint(i); } } bool TextBuffer::load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec) { // fallback codec must exist Q_ASSERT(m_fallbackTextCodec); // codec must be set! Q_ASSERT(m_textCodec); /** * first: clear buffer in any case! */ clear(); /** * construct the file loader for the given file, with correct prober type */ Kate::TextLoader file(filename, m_encodingProberType); /** * triple play, maximal three loading rounds * 0) use the given encoding, be done, if no encoding errors happen * 1) use BOM to decided if Unicode or if that fails, use encoding prober, if no encoding errors happen, be done * 2) use fallback encoding, be done, if no encoding errors happen * 3) use again given encoding, be done in any case */ for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) { /** * kill all blocks beside first one */ for (int b = 1; b < m_blocks.size(); ++b) { TextBlock *block = m_blocks.at(b); block->clearLines(); delete block; } m_blocks.resize(1); /** * remove lines in first block */ m_blocks.last()->clearLines(); m_lines = 0; /** * try to open file, with given encoding * in round 0 + 3 use the given encoding from user * in round 1 use 0, to trigger detection * in round 2 use fallback */ QTextCodec *codec = m_textCodec; if (i == 1) { codec = nullptr; } else if (i == 2) { codec = m_fallbackTextCodec; } if (!file.open(codec)) { // create one dummy textline, in any case m_blocks.last()->appendLine(QString()); m_lines++; return false; } // read in all lines... encodingErrors = false; while (!file.eof()) { // read line int offset = 0, length = 0; bool currentError = !file.readLine(offset, length); encodingErrors = encodingErrors || currentError; // bail out on encoding error, if not last round! if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) { BUFFER_DEBUG << "Failed try to load file" << filename << "with codec" << (file.textCodec() ? file.textCodec()->name() : "(null)"); break; } // get Unicode data for this line const QChar *unicodeData = file.unicode() + offset; if (longestLineLoaded < length) longestLineLoaded=length; /** * split lines, if too large */ do { /** * calculate line length */ int lineLength = length; if ((m_lineLengthLimit > 0) && (lineLength > m_lineLengthLimit)) { /** * search for place to wrap */ int spacePosition = m_lineLengthLimit - 1; for (int testPosition = m_lineLengthLimit - 1; (testPosition >= 0) && (testPosition >= (m_lineLengthLimit - (m_lineLengthLimit / 10))); --testPosition) { /** * wrap place found? */ if (unicodeData[testPosition].isSpace() || unicodeData[testPosition].isPunct()) { spacePosition = testPosition; break; } } /** * wrap the line */ lineLength = spacePosition + 1; length -= lineLength; tooLongLinesWrapped = true; } else { /** * be done after this round */ length = 0; } /** * construct new text line with content from file * move data pointer */ QString textLine(unicodeData, lineLength); unicodeData += lineLength; /** * ensure blocks aren't too large */ if (m_blocks.last()->lines() >= m_blockSize) { m_blocks.append(new TextBlock(this, m_blocks.last()->startLine() + m_blocks.last()->lines())); } /** * append line to last block */ m_blocks.last()->appendLine(textLine); ++m_lines; } while (length > 0); } // if no encoding error, break out of reading loop if (!encodingErrors) { // remember used codec, might change bom setting setTextCodec(file.textCodec()); break; } } // save checksum of file on disk setDigest(file.digest()); // remember if BOM was found if (file.byteOrderMarkFound()) { setGenerateByteOrderMark(true); } // remember eol mode, if any found in file if (file.eol() != eolUnknown) { setEndOfLineMode(file.eol()); } // remember mime type for filter device m_mimeTypeForFilterDev = file.mimeTypeForFilterDev(); // assert that one line is there! Q_ASSERT(m_lines > 0); // report CODEC + ERRORS BUFFER_DEBUG << "Loaded file " << filename << "with codec" << m_textCodec->name() << (encodingErrors ? "with" : "without") << "encoding errors"; // report BOM BUFFER_DEBUG << (file.byteOrderMarkFound() ? "Found" : "Didn't find") << "byte order mark"; // report filter device mime-type BUFFER_DEBUG << "used filter device for mime-type" << m_mimeTypeForFilterDev; // emit success emit loaded(filename, encodingErrors); // file loading worked, modulo encoding problems return true; } const QByteArray &TextBuffer::digest() const { return m_digest; } void TextBuffer::setDigest(const QByteArray &checksum) { m_digest = checksum; } void TextBuffer::setTextCodec(QTextCodec *codec) { m_textCodec = codec; // enforce bom for some encodings int mib = m_textCodec->mibEnum(); if (mib == 1013 || mib == 1014 || mib == 1015) { // utf16 setGenerateByteOrderMark(true); } if (mib == 1017 || mib == 1018 || mib == 1019) { // utf32 setGenerateByteOrderMark(true); } } bool TextBuffer::save(const QString &filename) { // codec must be set! Q_ASSERT(m_textCodec); const bool newFile = !QFile::exists(filename); /** * Memorize owner and group. Due to design of QSaveFile we will have to re-set them after save is complete. */ uint ownerId = -2; uint groupId = -2; if (!newFile) { QFileInfo fileInfo(filename); ownerId = fileInfo.ownerId(); groupId = fileInfo.groupId(); } /** - * use QSaveFile for save write + rename + * use QSaveFile for file saving */ - QScopedPointer saveFile(new QSaveFile(filename)); + QScopedPointer saveFile(new QSaveFile(filename)); static_cast(saveFile.data())->setDirectWriteFallback(true); - bool usingTemporaryFile = false; - QScopedPointer kAuthActionArgs; - QScopedPointer kAuthSaveAction; + bool usingTemporaryBuffer = false; // open QSaveFile for write if (m_alwaysUseKAuthForSave || !saveFile->open(QIODevice::WriteOnly)) { // if that fails we need more privileges to save this file - // -> we write to temporary file and then move it to target location + // -> we write to a temporary file and then send its path to KAuth action for privileged save - usingTemporaryFile = true; + usingTemporaryBuffer = true; - QString targetFilename(filename); + // we are now saving to a temporary buffer + saveFile.reset(new QBuffer()); - kAuthActionArgs.reset(new QVariantMap()); - kAuthActionArgs->insert(QLatin1String("actionMode"), SecureTextBuffer::ActionMode::Prepare); - kAuthActionArgs->insert(QLatin1String("targetFile"), targetFilename); - kAuthActionArgs->insert(QLatin1String("ownerId"), getuid()); - - // call save with elevated privileges - if (KTextEditor::EditorPrivate::unitTestMode()) { - - // unit testing purposes only - ActionReply reply = SecureTextBuffer::savefile(*kAuthActionArgs); - if (!reply.succeeded()) { - return false; - } - targetFilename = reply.data()[QLatin1String("temporaryFile")].toString(); - - } else { - - // call action - kAuthSaveAction.reset(new KAuth::Action(QLatin1String("org.kde.ktexteditor.katetextbuffer.savefile"))); - kAuthSaveAction->setHelperId(QLatin1String("org.kde.ktexteditor.katetextbuffer")); - kAuthSaveAction->setArguments(*kAuthActionArgs); - KAuth::ExecuteJob *job = kAuthSaveAction->execute(); - if (!job->exec()) { - return false; - } - - // get temporary file path from the reply - targetFilename = job->data()[QLatin1String("temporaryFile")].toString(); - - } - - if (targetFilename.isEmpty()) { + // open buffer for write and read (read is used for checksum computing and writing to temporary file) + if (!saveFile->open(QIODevice::ReadWrite)) { return false; } - - // we are now saving to a prepared temporary file - saveFile.reset(new QFile(targetFilename)); - - // open QTemporaryFile for write - if (!saveFile->open(QIODevice::WriteOnly)) { - return false; - } - - if (!newFile) { - // set original file's permissions to temporary file (QSaveFile does this automatically) - saveFile->setPermissions(QFile(filename).permissions()); - } } /** * construct correct filter device and try to open */ KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(m_mimeTypeForFilterDev); KCompressionDevice file(saveFile.data(), false, type); if (!file.open(QIODevice::WriteOnly)) { return false; } /** * construct stream + disable Unicode headers */ QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-16")); // set the correct codec stream.setCodec(m_textCodec); // generate byte order mark? stream.setGenerateByteOrderMark(generateByteOrderMark()); // our loved eol string ;) QString eol = QStringLiteral("\n"); //m_doc->config()->eolString (); if (endOfLineMode() == eolDos) { eol = QStringLiteral("\r\n"); } else if (endOfLineMode() == eolMac) { eol = QStringLiteral("\r"); } // just dump the lines out ;) for (int i = 0; i < m_lines; ++i) { // get line to save Kate::TextLine textline = line(i); stream << textline->text(); // append correct end of line string if ((i + 1) < m_lines) { stream << eol; } } if (m_newLineAtEof) { Q_ASSERT(m_lines > 0); // see .h file const Kate::TextLine lastLine = line(m_lines - 1); const int firstChar = lastLine->firstChar(); if (firstChar > -1 || lastLine->length() > 0) { stream << eol; } } // flush stream stream.flush(); // close and delete file file.close(); // flush file - if (!saveFile->flush()) { - return false; - } - - if (usingTemporaryFile) { - // ensure that the file is written to disk - // just for temporary file (QSaveFile does this automatically in commit()) - SecureTextBuffer::syncToDisk(saveFile->handle()); + if (!usingTemporaryBuffer) { + static_cast(saveFile.data())->flush(); } // did save work? // only finalize if stream status == OK bool ok = (stream.status() == QTextStream::Ok); // commit changes if (ok) { - if (usingTemporaryFile) { + if (usingTemporaryBuffer) { - // temporary file was used to save the file - // -> moving this file to original location with KAuth action + // temporary buffer was used to save the file + // -> computing checksum + // -> saving to temporary file + // -> copying the temporary file to the original file location with KAuth action - kAuthActionArgs->insert(QLatin1String("actionMode"), SecureTextBuffer::ActionMode::Move); - kAuthActionArgs->insert(QLatin1String("sourceFile"), saveFile->fileName()); - kAuthActionArgs->insert(QLatin1String("targetFile"), filename); - kAuthActionArgs->insert(QLatin1String("ownerId"), ownerId); - kAuthActionArgs->insert(QLatin1String("groupId"), groupId); + QTemporaryFile tempFile; + if (!tempFile.open()) { + return false; + } + QCryptographicHash cryptographicHash(SecureTextBuffer::checksumAlgorithm); + + // go to QBuffer start + saveFile->seek(0); + + // read contents of QBuffer and add them to checksum utility as well as to QTemporaryFile + char buffer[bufferLength]; + qint64 read = -1; + while ((read = saveFile->read(buffer, bufferLength)) > 0) { + cryptographicHash.addData(buffer, read); + if (tempFile.write(buffer, read) == -1) { + return false; + } + } + if (!tempFile.flush()) { + return false; + } + + // compute checksum + QByteArray checksum = cryptographicHash.result(); + + // prepare data for KAuth action + QVariantMap kAuthActionArgs; + kAuthActionArgs.insert(QLatin1String("sourceFile"), tempFile.fileName()); + kAuthActionArgs.insert(QLatin1String("targetFile"), filename); + kAuthActionArgs.insert(QLatin1String("checksum"), checksum); + kAuthActionArgs.insert(QLatin1String("ownerId"), ownerId); + kAuthActionArgs.insert(QLatin1String("groupId"), groupId); // call save with elevated privileges if (KTextEditor::EditorPrivate::unitTestMode()) { // unit testing purposes only - ok = SecureTextBuffer::savefile(*kAuthActionArgs).succeeded(); + ok = SecureTextBuffer::savefile(kAuthActionArgs).succeeded(); } else { - kAuthSaveAction->setArguments(*kAuthActionArgs); - KAuth::ExecuteJob *job = kAuthSaveAction->execute(); + KAuth::Action kAuthSaveAction(QLatin1String("org.kde.ktexteditor.katetextbuffer.savefile")); + kAuthSaveAction.setHelperId(QLatin1String("org.kde.ktexteditor.katetextbuffer")); + kAuthSaveAction.setArguments(kAuthActionArgs); + KAuth::ExecuteJob *job = kAuthSaveAction.execute(); ok = job->exec(); } } else { // standard save without elevated privileges - ok = static_cast(saveFile.data())->commit(); + QSaveFile *saveFileLocal = static_cast(saveFile.data()); - if (ok && !newFile) { + if (!newFile) { // ensure correct owner - SecureTextBuffer::setOwner(filename, ownerId, groupId); + SecureTextBuffer::setOwner(saveFileLocal->handle(), ownerId, groupId); } + ok = saveFileLocal->commit(); + } } // remember this revision as last saved if we had success! if (ok) { m_history.setLastSavedRevision(); } // report CODEC + ERRORS BUFFER_DEBUG << "Saved file " << filename << "with codec" << m_textCodec->name() << (ok ? "without" : "with") << "errors"; if (ok) { markModifiedLinesAsSaved(); } // emit signal on success if (ok) { emit saved(filename); } // return success or not return ok; } void TextBuffer::notifyAboutRangeChange(KTextEditor::View *view, int startLine, int endLine, bool rangeWithAttribute) { /** * ignore calls if no document is around */ if (!m_document) { return; } /** * update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really * just create 20k ranges in a go and you wait seconds on a decent machine */ const QList &views = m_document->views(); foreach (KTextEditor::View *curView, views) { // filter wrong views if (view && view != curView) { continue; } // notify view, it is really a kate view static_cast(curView)->notifyAboutRangeChange(startLine, endLine, rangeWithAttribute); } } void TextBuffer::markModifiedLinesAsSaved() { foreach (TextBlock *block, m_blocks) { block->markModifiedLinesAsSaved(); } } QList TextBuffer::rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const { // get block, this will assert on invalid line const int blockIndex = blockForLine(line); // get the ranges of the right block QList rightRanges; foreach (const QSet &ranges, m_blocks.at(blockIndex)->rangesForLine(line)) { foreach (TextRange *const range, ranges) { /** * we want only ranges with attributes, but this one has none */ if (rangesWithAttributeOnly && !range->hasAttribute()) { continue; } /** * we want ranges for no view, but this one's attribute is only valid for views */ if (!view && range->attributeOnlyForViews()) { continue; } /** * the range's attribute is not valid for this view */ if (range->view() && range->view() != view) { continue; } /** * if line is in the range, ok */ if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) { rightRanges.append(range); } } } // return right ranges return rightRanges; } } diff --git a/src/buffer/katetextbuffer.h b/src/buffer/katetextbuffer.h index 8ec356fe..f8912f24 100644 --- a/src/buffer/katetextbuffer.h +++ b/src/buffer/katetextbuffer.h @@ -1,656 +1,661 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_TEXTBUFFER_H #define KATE_TEXTBUFFER_H #include #include #include #include #include #include #include "katedocument.h" #include #include "katetextblock.h" #include "katetextcursor.h" #include "katetextrange.h" #include "katetexthistory.h" // encoding prober #include namespace Kate { /** * Class representing a text buffer. * The interface is line based, internally the text will be stored in blocks of text lines. */ class KTEXTEDITOR_EXPORT TextBuffer : public QObject { friend class TextCursor; friend class TextRange; friend class TextBlock; Q_OBJECT public: /** * End of line mode */ enum EndOfLineMode { eolUnknown = -1 , eolUnix = 0 , eolDos = 1 , eolMac = 2 }; /** * Construct an empty text buffer. * Empty means one empty line in one block. * @param parent parent qobject * @param blockSize block size in lines the buffer should try to hold, default 64 lines */ TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize = 64, bool alwaysUseKAuth = false); /** * Destruct the text buffer * Virtual, we allow inheritance */ virtual ~TextBuffer(); /** * Clears the buffer, reverts to initial empty state. * Empty means one empty line in one block. * Virtual, can be overwritten. */ virtual void clear(); /** * Set encoding prober type for this buffer to use for load. * @param proberType prober type to use for encoding */ void setEncodingProberType(KEncodingProber::ProberType proberType) { m_encodingProberType = proberType; } /** * Get encoding prober type for this buffer * @return currently in use prober type of this buffer */ KEncodingProber::ProberType encodingProberType() const { return m_encodingProberType; } /** * Set fallback codec for this buffer to use for load. * @param codec fallback QTextCodec to use for encoding */ void setFallbackTextCodec(QTextCodec *codec) { m_fallbackTextCodec = codec; } /** * Get fallback codec for this buffer * @return currently in use fallback codec of this buffer */ QTextCodec *fallbackTextCodec() const { return m_fallbackTextCodec; } /** * Set codec for this buffer to use for load/save. * Loading might overwrite this, if it encounters problems and finds a better codec. * Might change BOM setting. * @param codec QTextCodec to use for encoding */ void setTextCodec(QTextCodec *codec); /** * Get codec for this buffer * @return currently in use codec of this buffer */ QTextCodec *textCodec() const { return m_textCodec; } /** * Generate byte order mark on save. * Loading might overwrite this setting, if there is a BOM found inside the file. * @param generateByteOrderMark should BOM be generated? */ void setGenerateByteOrderMark(bool generateByteOrderMark) { m_generateByteOrderMark = generateByteOrderMark; } /** * Generate byte order mark on save? * @return should BOM be generated? */ bool generateByteOrderMark() const { return m_generateByteOrderMark; } /** * Set end of line mode for this buffer, not allowed to be set to unknown. * Loading might overwrite this setting, if there is a eol found inside the file. * @param endOfLineMode new eol mode */ void setEndOfLineMode(EndOfLineMode endOfLineMode) { Q_ASSERT(endOfLineMode != eolUnknown); m_endOfLineMode = endOfLineMode; } /** * Get end of line mode * @return end of line mode */ EndOfLineMode endOfLineMode() const { return m_endOfLineMode; } /** * Set whether to insert a newline character on save at the end of the file * @param newlineAtEof should newline be added if non-existing */ void setNewLineAtEof(bool newlineAtEof) { m_newLineAtEof = newlineAtEof; } /** * Set line length limit * @param lineLengthLimit new line length limit */ void setLineLengthLimit(int lineLengthLimit) { m_lineLengthLimit = lineLengthLimit; } /** * Load the given file. This will first clear the buffer and then load the file. * Even on error during loading the buffer will still be cleared. * Before calling this, setTextCodec must have been used to set codec! * @param filename file to open * @param encodingErrors were there problems occurred while decoding the file? * @param tooLongLinesWrapped were too long lines found and wrapped? * @param longestLineLoaded the longest line in the file (before wrapping) * @param enforceTextCodec enforce to use only the set text codec * @return success, the file got loaded, perhaps with encoding errors * Virtual, can be overwritten. */ virtual bool load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec); /** * Save the current buffer content to the given file. * Before calling this, setTextCodec and setFallbackTextCodec must have been used to set codec! * @param filename file to save * @return success * Virtual, can be overwritten. */ virtual bool save(const QString &filename); /** * Lines currently stored in this buffer. * This is never 0, even clear will let one empty line remain. */ int lines() const { Q_ASSERT(m_lines > 0); return m_lines; } /** * Revision of this buffer. Is set to 0 on construction, clear() (load will trigger clear()). * Is incremented on each change to the buffer. * @return current revision */ qint64 revision() const { return m_revision; } /** * Retrieve a text line. * @param line wanted line number * @return text line */ TextLine line(int line) const; /** * Retrieve text of complete buffer. * @return text for this buffer, lines separated by '\n' */ QString text() const; /** * Start an editing transaction, the wrapLine/unwrapLine/insertText and removeText functions * are only allowed to be called inside a editing transaction. * Editing transactions can stack. The number of startEdit and endEdit calls must match. * @return returns true, if no transaction was already running * Virtual, can be overwritten. */ virtual bool startEditing(); /** * Finish an editing transaction. Only allowed to be called if editing transaction is started. * @return returns true, if this finished last running transaction * Virtual, can be overwritten. */ virtual bool finishEditing(); /** * Query the number of editing transactions running atm. * @return number of running transactions */ int editingTransactions() const { return m_editingTransactions; } /** * Query the revsion of this buffer before the ongoing editing transactions. * @return revision of buffer before current editing transaction altered it */ qint64 editingLastRevision() const { return m_editingLastRevision; } /** * Query the number of lines of this buffer before the ongoing editing transactions. * @return number of lines of buffer before current editing transaction altered it */ int editingLastLines() const { return m_editingLastLines; } /** * Query information from the last editing transaction: was the content of the buffer changed? * This is checked by comparing the editingLastRevision() with the current revision(). * @return content of buffer was changed in last transaction? */ bool editingChangedBuffer() const { return editingLastRevision() != revision(); } /** * Query information from the last editing transaction: was the number of lines of the buffer changed? * This is checked by comparing the editingLastLines() with the current lines(). * @return content of buffer was changed in last transaction? */ bool editingChangedNumberOfLines() const { return editingLastLines() != lines(); } /** * Get minimal line number changed by last editing transaction * @return maximal line number changed by last editing transaction, or -1, if none changed */ int editingMinimalLineChanged() const { return m_editingMinimalLineChanged; } /** * Get maximal line number changed by last editing transaction * @return maximal line number changed by last editing transaction, or -1, if none changed */ int editingMaximalLineChanged() const { return m_editingMaximalLineChanged; } /** * Wrap line at given cursor position. * @param position line/column as cursor where to wrap * Virtual, can be overwritten. */ virtual void wrapLine(const KTextEditor::Cursor &position); /** * Unwrap given line. * @param line line to unwrap * Virtual, can be overwritten. */ virtual void unwrapLine(int line); /** * Insert text at given cursor position. Does nothing if text is empty, beside some consistency checks. * @param position position where to insert text * @param text text to insert * Virtual, can be overwritten. */ virtual void insertText(const KTextEditor::Cursor &position, const QString &text); /** * Remove text at given range. Does nothing if range is empty, beside some consistency checks. * @param range range of text to remove, must be on one line only. * Virtual, can be overwritten. */ virtual void removeText(const KTextEditor::Range &range); /** * TextHistory of this buffer * @return text history for this buffer */ TextHistory &history() { return m_history; } Q_SIGNALS: /** * Buffer got cleared. This is emitted when constructor or load have called clear() internally, * or when the user of the buffer has called clear() itself. */ void cleared(); /** * Buffer loaded successfully a file * @param filename file which was loaded * @param encodingErrors were there problems occurred while decoding the file? */ void loaded(const QString &filename, bool encodingErrors); /** * Buffer saved successfully a file * @param filename file which was saved */ void saved(const QString &filename); /** * Editing transaction has started. */ void editingStarted(); /** * Editing transaction has finished. */ void editingFinished(); /** * A line got wrapped. * @param position position where the wrap occurred */ void lineWrapped(const KTextEditor::Cursor &position); /** * A line got unwrapped. * @param line line where the unwrap occurred */ void lineUnwrapped(int line); /** * Text got inserted. * @param position position where the insertion occurred * @param text inserted text */ void textInserted(const KTextEditor::Cursor &position, const QString &text); /** * Text got removed. * @param range range where the removal occurred * @param text removed text */ void textRemoved(const KTextEditor::Range &range, const QString &text); private: /** * Find block containing given line. * @param line we want to find block for this line * @return index of found block */ int blockForLine(int line) const; /** * Fix start lines of all blocks after the given one * @param startBlock index of block from which we start to fix */ void fixStartLines(int startBlock); /** * Balance the given block. Look if it is too small or too large. * @param index block to balance */ void balanceBlock(int index); /** * Block for given index in block list. * @param index block index * @return block matching this index */ TextBlock *blockForIndex(int index) { return m_blocks[index]; } /** * A range changed, notify the views, in case of attributes or feedback. * @param view which view is affected? 0 for all views * @param startLine start line of change * @param endLine end line of change * @param rangeWithAttribute attribute changed or is active, this will perhaps lead to repaints */ void notifyAboutRangeChange(KTextEditor::View *view, int startLine, int endLine, bool rangeWithAttribute); /** * Mark all modified lines as lines saved on disk (modified line system). */ void markModifiedLinesAsSaved(); public: /** * Gets the document to which this buffer is bound. * \return a pointer to the document */ KTextEditor::DocumentPrivate *document() const { return m_document; } /** * Debug output, print whole buffer content with line numbers and line length * @param title title for this output */ void debugPrint(const QString &title) const; /** * Return the ranges which affect the given line. * @param line line to look at * @param view only return ranges associated with given view * @param rangesWithAttributeOnly only return ranges which have a attribute set * @return list of ranges affecting this line */ QList rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const; /** * Check if the given range pointer is still valid. * @return range pointer still belongs to range for this buffer */ bool rangePointerValid(TextRange *range) const { return m_ranges.contains(range); } /** * Invalidate all ranges in this buffer. */ void invalidateRanges(); // // checksum handling // public: /** * Checksum of the document on disk, set either through file loading * in openFile() or in KTextEditor::DocumentPrivate::saveFile() * @return git compatible sha1 checksum for this document */ const QByteArray &digest() const; /** * Set the checksum of this buffer. Make sure this checksum is up-to-date * when reading digest(). * @param checksum git compatible sha1 digest for the document on disk */ void setDigest(const QByteArray &checksum); private: QByteArray m_digest; private: /** * parent document */ KTextEditor::DocumentPrivate *m_document; /** * text history */ TextHistory m_history; /** * block size in lines the buffer will try to hold */ const int m_blockSize; /** * List of blocks which contain the lines of this buffer */ QVector m_blocks; /** * Number of lines in buffer */ int m_lines; /** * Last used block in the buffer. Is used for speeding up blockForLine. * May contain invalid index, must be checked before using. */ mutable int m_lastUsedBlock; /** * Revision of the buffer. */ qint64 m_revision; /** * Current number of running edit transactions */ int m_editingTransactions; /** * Revision remembered at start of current editing transaction */ qint64 m_editingLastRevision; /** * Number of lines remembered at start of current editing transaction */ int m_editingLastLines; /** * minimal line number changed by last editing transaction */ int m_editingMinimalLineChanged; /** * maximal line number changed by last editing transaction */ int m_editingMaximalLineChanged; /** * Set of invalid cursors for this whole buffer. * Valid cursors are inside the block the belong to. */ QSet m_invalidCursors; /** * Set of ranges of this whole buffer. */ QSet m_ranges; /** * Encoding prober type to use */ KEncodingProber::ProberType m_encodingProberType; /** * Fallback text codec to use */ QTextCodec *m_fallbackTextCodec; /** * Text codec to use */ QTextCodec *m_textCodec; /** * Mime-Type used for transparent compression/decompression support * Set by load(), reset by clear() */ QString m_mimeTypeForFilterDev; /** * Should byte order mark be created? */ bool m_generateByteOrderMark; /** * End of line mode, default is Unix */ EndOfLineMode m_endOfLineMode; /** * Insert newline character at the end of the file? */ bool m_newLineAtEof; /** * Limit for line length, longer lines will be wrapped on load */ int m_lineLengthLimit; /** * For unit-testing purposes only. */ bool m_alwaysUseKAuthForSave; + + /** + * For copying QBuffer -> QTemporaryFile while saving document in privileged mode + */ + static const qint64 bufferLength = 4096; }; } #endif