| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | #include "config.h" |
| 7 | |
| 8 | #include "katetextbuffer.h" |
| 9 | #include "katetextloader.h" |
| 10 | |
| 11 | #include "katedocument.h" |
| 12 | |
| 13 | // this is unfortunate, but needed for performance |
| 14 | #include "katepartdebug.h" |
| 15 | #include "kateview.h" |
| 16 | |
| 17 | #ifndef Q_OS_WIN |
| 18 | #include <cerrno> |
| 19 | #include <unistd.h> |
| 20 | // sadly there seems to be no possibility in Qt to determine detailed error |
| 21 | // codes about e.g. file open errors, so we need to resort to evaluating |
| 22 | // errno directly on platforms that support this |
| 23 | #define CAN_USE_ERRNO |
| 24 | #endif |
| 25 | |
| 26 | #include <QBuffer> |
| 27 | #include <QCryptographicHash> |
| 28 | #include <QFile> |
| 29 | #include <QFileInfo> |
| 30 | #include <QScopeGuard> |
| 31 | #include <QStandardPaths> |
| 32 | #include <QStringEncoder> |
| 33 | #include <QTemporaryFile> |
| 34 | |
| 35 | #if HAVE_KAUTH |
| 36 | #include "katesecuretextbuffer_p.h" |
| 37 | #include <KAuth/Action> |
| 38 | #include <KAuth/ExecuteJob> |
| 39 | #endif |
| 40 | |
| 41 | #if 0 |
| 42 | #define BUFFER_DEBUG qCDebug(LOG_KTE) |
| 43 | #else |
| 44 | #define BUFFER_DEBUG \ |
| 45 | if (0) \ |
| 46 | qCDebug(LOG_KTE) |
| 47 | #endif |
| 48 | |
| 49 | namespace Kate |
| 50 | { |
| 51 | TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, bool alwaysUseKAuth) |
| 52 | : QObject(parent) |
| 53 | , m_document(parent) |
| 54 | , m_history(*this) |
| 55 | , m_lines(0) |
| 56 | , m_revision(-1) |
| 57 | , m_editingTransactions(0) |
| 58 | , m_editingLastRevision(0) |
| 59 | , m_editingLastLines(0) |
| 60 | , m_editingMinimalLineChanged(-1) |
| 61 | , m_editingMaximalLineChanged(-1) |
| 62 | , m_encodingProberType(KEncodingProber::Universal) |
| 63 | , m_generateByteOrderMark(false) |
| 64 | , m_endOfLineMode(eolUnix) |
| 65 | , m_lineLengthLimit(4096) |
| 66 | , m_alwaysUseKAuthForSave(alwaysUseKAuth) |
| 67 | { |
| 68 | // create initial state, this will set m_revision to 0 |
| 69 | clear(); |
| 70 | } |
| 71 | |
| 72 | TextBuffer::~TextBuffer() |
| 73 | { |
| 74 | // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect |
| 75 | m_document = nullptr; |
| 76 | |
| 77 | // not allowed during editing |
| 78 | Q_ASSERT(m_editingTransactions == 0); |
| 79 | |
| 80 | // invalidate all moving stuff |
| 81 | std::vector<Kate::TextRange *> rangesWithFeedback; |
| 82 | for (auto b : m_blocks) { |
| 83 | auto cursors = std::move(b->m_cursors); |
| 84 | for (auto it = cursors.begin(); it != cursors.end(); ++it) { |
| 85 | auto cursor = *it; |
| 86 | // update the block |
| 87 | cursor->m_block = nullptr; |
| 88 | cursor->m_line = cursor->m_column = -1; |
| 89 | cursor->m_buffer = nullptr; |
| 90 | if (auto r = cursor->kateRange()) { |
| 91 | r->m_buffer = nullptr; |
| 92 | if (r->feedback()) { |
| 93 | rangesWithFeedback.push_back(x: r); |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | // uniquify ranges |
| 100 | std::sort(first: rangesWithFeedback.begin(), last: rangesWithFeedback.end()); |
| 101 | auto it = std::unique(first: rangesWithFeedback.begin(), last: rangesWithFeedback.end()); |
| 102 | std::for_each(first: rangesWithFeedback.begin(), last: it, f: [](Kate::TextRange *range) { |
| 103 | range->feedback()->rangeInvalid(range); |
| 104 | }); |
| 105 | |
| 106 | // clean out all cursors and lines, only cursors belonging to range will survive |
| 107 | for (TextBlock *block : m_blocks) { |
| 108 | block->clearLines(); |
| 109 | } |
| 110 | |
| 111 | // delete all blocks, now that all cursors are really deleted |
| 112 | // else asserts in destructor of blocks will fail! |
| 113 | qDeleteAll(c: m_blocks); |
| 114 | m_blocks.clear(); |
| 115 | } |
| 116 | |
| 117 | void TextBuffer::invalidateRanges() |
| 118 | { |
| 119 | std::vector<Kate::TextRange *> ranges; |
| 120 | ranges.reserve(n: m_blocks.size()); |
| 121 | for (TextBlock *block : m_blocks) { |
| 122 | for (auto cursor : block->m_cursors) { |
| 123 | if (cursor->kateRange()) { |
| 124 | ranges.push_back(x: cursor->kateRange()); |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | // uniquify ranges |
| 129 | std::sort(first: ranges.begin(), last: ranges.end()); |
| 130 | auto it = std::unique(first: ranges.begin(), last: ranges.end()); |
| 131 | std::for_each(first: ranges.begin(), last: it, f: [](Kate::TextRange *range) { |
| 132 | range->setRange({KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()}); |
| 133 | }); |
| 134 | } |
| 135 | |
| 136 | void TextBuffer::clear() |
| 137 | { |
| 138 | // not allowed during editing |
| 139 | Q_ASSERT(m_editingTransactions == 0); |
| 140 | |
| 141 | m_multilineRanges.clear(); |
| 142 | invalidateRanges(); |
| 143 | |
| 144 | // new block for empty buffer |
| 145 | TextBlock *newBlock = new TextBlock(this, 0); |
| 146 | newBlock->appendLine(textOfLine: QString()); |
| 147 | |
| 148 | // clean out all cursors and lines, move them to newBlock if not belonging to a range |
| 149 | for (TextBlock *block : std::as_const(t&: m_blocks)) { |
| 150 | auto cursors = std::move(block->m_cursors); |
| 151 | for (auto it = cursors.begin(); it != cursors.end(); ++it) { |
| 152 | auto cursor = *it; |
| 153 | if (!cursor->kateRange()) { |
| 154 | // update the block |
| 155 | cursor->m_block = newBlock; |
| 156 | // move the cursor into the target block |
| 157 | cursor->m_line = cursor->m_column = 0; |
| 158 | newBlock->m_cursors.push_back(x: cursor); |
| 159 | // remove it and advance to next element |
| 160 | } |
| 161 | // skip cursors with ranges, we need to invalidate the ranges later |
| 162 | } |
| 163 | block->clearLines(); |
| 164 | } |
| 165 | std::sort(first: newBlock->m_cursors.begin(), last: newBlock->m_cursors.end()); |
| 166 | |
| 167 | // kill all buffer blocks |
| 168 | qDeleteAll(c: m_blocks); |
| 169 | // insert one block with one empty line |
| 170 | m_blocks = {newBlock}; |
| 171 | m_startLines = {0}; |
| 172 | m_blockSizes = {1}; |
| 173 | |
| 174 | // reset lines and last used block |
| 175 | m_lines = 1; |
| 176 | |
| 177 | // increment revision, we did reset it here in the past |
| 178 | // that is no good idea as we can mix up content variants after an reload |
| 179 | ++m_revision; |
| 180 | |
| 181 | // reset bom detection |
| 182 | m_generateByteOrderMark = false; |
| 183 | |
| 184 | // reset the filter device |
| 185 | m_mimeTypeForFilterDev = QStringLiteral("text/plain" ); |
| 186 | |
| 187 | // clear edit history |
| 188 | m_history.clear(); |
| 189 | |
| 190 | // we got cleared |
| 191 | Q_EMIT cleared(); |
| 192 | } |
| 193 | |
| 194 | TextLine TextBuffer::line(int line) const |
| 195 | { |
| 196 | // get block, this will assert on invalid line |
| 197 | int blockIndex = blockForLine(line); |
| 198 | |
| 199 | // get line |
| 200 | return m_blocks.at(n: blockIndex)->line(line: line - m_startLines[blockIndex]); |
| 201 | } |
| 202 | |
| 203 | void TextBuffer::setLineMetaData(int line, const TextLine &textLine) |
| 204 | { |
| 205 | // get block, this will assert on invalid line |
| 206 | int blockIndex = blockForLine(line); |
| 207 | |
| 208 | // get line |
| 209 | return m_blocks.at(n: blockIndex)->setLineMetaData(line: line - m_startLines[blockIndex], textLine); |
| 210 | } |
| 211 | |
| 212 | int TextBuffer::cursorToOffset(KTextEditor::Cursor c) const |
| 213 | { |
| 214 | if ((c.line() < 0) || (c.line() >= lines())) { |
| 215 | return -1; |
| 216 | } |
| 217 | |
| 218 | int off = 0; |
| 219 | const int blockIndex = blockForLine(line: c.line()); |
| 220 | for (auto it = m_blockSizes.begin(), end = m_blockSizes.begin() + blockIndex; it != end; ++it) { |
| 221 | off += *it; |
| 222 | } |
| 223 | |
| 224 | auto block = m_blocks[blockIndex]; |
| 225 | int start = block->startLine(); |
| 226 | int end = start + block->lines(); |
| 227 | for (int line = start; line < end; ++line) { |
| 228 | if (line >= c.line()) { |
| 229 | off += qMin(a: c.column(), b: block->lineLength(line)); |
| 230 | return off; |
| 231 | } |
| 232 | off += block->lineLength(line) + 1; |
| 233 | } |
| 234 | |
| 235 | Q_ASSERT(false); |
| 236 | return -1; |
| 237 | } |
| 238 | |
| 239 | KTextEditor::Cursor TextBuffer::offsetToCursor(int offset) const |
| 240 | { |
| 241 | if (offset >= 0) { |
| 242 | int off = 0; |
| 243 | int blockIdx = 0; |
| 244 | for (int blockSize : m_blockSizes) { |
| 245 | if (off + blockSize < offset) { |
| 246 | off += blockSize; |
| 247 | } else { |
| 248 | auto block = m_blocks[blockIdx]; |
| 249 | const int lines = block->lines(); |
| 250 | int start = block->startLine(); |
| 251 | int end = start + lines; |
| 252 | for (int line = start; line < end; ++line) { |
| 253 | const int len = block->lineLength(line); |
| 254 | if (off + len >= offset) { |
| 255 | return KTextEditor::Cursor(line, offset - off); |
| 256 | } |
| 257 | off += len + 1; |
| 258 | } |
| 259 | } |
| 260 | blockIdx++; |
| 261 | } |
| 262 | } |
| 263 | return KTextEditor::Cursor::invalid(); |
| 264 | } |
| 265 | |
| 266 | QString TextBuffer::text() const |
| 267 | { |
| 268 | QString text; |
| 269 | qsizetype size = 0; |
| 270 | for (int blockSize : m_blockSizes) { |
| 271 | size += blockSize; |
| 272 | } |
| 273 | text.reserve(asize: size); |
| 274 | size -= 1; // remove -1, last newline |
| 275 | |
| 276 | // combine all blocks |
| 277 | for (TextBlock *block : m_blocks) { |
| 278 | block->text(text); |
| 279 | } |
| 280 | text.chop(n: 1); // remove last \n |
| 281 | |
| 282 | Q_ASSERT(size == text.size()); |
| 283 | return text; |
| 284 | } |
| 285 | |
| 286 | bool TextBuffer::startEditing() |
| 287 | { |
| 288 | // increment transaction counter |
| 289 | ++m_editingTransactions; |
| 290 | |
| 291 | // if not first running transaction, do nothing |
| 292 | if (m_editingTransactions > 1) { |
| 293 | return false; |
| 294 | } |
| 295 | |
| 296 | // reset information about edit... |
| 297 | m_editingLastRevision = m_revision; |
| 298 | m_editingLastLines = m_lines; |
| 299 | m_editingMinimalLineChanged = -1; |
| 300 | m_editingMaximalLineChanged = -1; |
| 301 | |
| 302 | // transaction has started |
| 303 | Q_EMIT m_document->KTextEditor::Document::editingStarted(document: m_document); |
| 304 | |
| 305 | // first transaction started |
| 306 | return true; |
| 307 | } |
| 308 | |
| 309 | bool TextBuffer::finishEditing() |
| 310 | { |
| 311 | // only allowed if still transactions running |
| 312 | Q_ASSERT(m_editingTransactions > 0); |
| 313 | |
| 314 | // decrement counter |
| 315 | --m_editingTransactions; |
| 316 | |
| 317 | // if not last running transaction, do nothing |
| 318 | if (m_editingTransactions > 0) { |
| 319 | return false; |
| 320 | } |
| 321 | |
| 322 | // assert that if buffer changed, the line ranges are set and valid! |
| 323 | Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1)); |
| 324 | Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged)); |
| 325 | Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines)); |
| 326 | Q_ASSERT(!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines)); |
| 327 | |
| 328 | // transaction has finished |
| 329 | Q_EMIT m_document->KTextEditor::Document::editingFinished(document: m_document); |
| 330 | |
| 331 | // last transaction finished |
| 332 | return true; |
| 333 | } |
| 334 | |
| 335 | void TextBuffer::wrapLine(const KTextEditor::Cursor position) |
| 336 | { |
| 337 | // debug output for REAL low-level debugging |
| 338 | BUFFER_DEBUG << "wrapLine" << position; |
| 339 | |
| 340 | // only allowed if editing transaction running |
| 341 | Q_ASSERT(m_editingTransactions > 0); |
| 342 | |
| 343 | // get block, this will assert on invalid line |
| 344 | int blockIndex = blockForLine(line: position.line()); |
| 345 | |
| 346 | // let the block handle the wrapLine |
| 347 | // this can only lead to one more line in this block |
| 348 | // no other blocks will change |
| 349 | // this call will trigger fixStartLines |
| 350 | ++m_lines; // first alter the line counter, as functions called will need the valid one |
| 351 | m_blocks.at(n: blockIndex)->wrapLine(position, fixStartLinesStartIndex: blockIndex); |
| 352 | m_blockSizes[blockIndex] += 1; |
| 353 | |
| 354 | // remember changes |
| 355 | ++m_revision; |
| 356 | |
| 357 | // update changed line interval |
| 358 | if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { |
| 359 | m_editingMinimalLineChanged = position.line(); |
| 360 | } |
| 361 | |
| 362 | if (position.line() <= m_editingMaximalLineChanged) { |
| 363 | ++m_editingMaximalLineChanged; |
| 364 | } else { |
| 365 | m_editingMaximalLineChanged = position.line() + 1; |
| 366 | } |
| 367 | |
| 368 | // balance the changed block if needed |
| 369 | balanceBlock(index: blockIndex); |
| 370 | |
| 371 | // emit signal about done change |
| 372 | Q_EMIT m_document->KTextEditor::Document::lineWrapped(document: m_document, position); |
| 373 | } |
| 374 | |
| 375 | void TextBuffer::unwrapLine(int line) |
| 376 | { |
| 377 | // debug output for REAL low-level debugging |
| 378 | BUFFER_DEBUG << "unwrapLine" << line; |
| 379 | |
| 380 | // only allowed if editing transaction running |
| 381 | Q_ASSERT(m_editingTransactions > 0); |
| 382 | |
| 383 | // line 0 can't be unwrapped |
| 384 | Q_ASSERT(line > 0); |
| 385 | |
| 386 | // get block, this will assert on invalid line |
| 387 | int blockIndex = blockForLine(line); |
| 388 | |
| 389 | // is this the first line in the block? |
| 390 | const int blockStartLine = m_startLines[blockIndex]; |
| 391 | const bool firstLineInBlock = line == blockStartLine; |
| 392 | |
| 393 | // let the block handle the unwrapLine |
| 394 | // this can either lead to one line less in this block or the previous one |
| 395 | // the previous one could even end up with zero lines |
| 396 | // this call will trigger fixStartLines |
| 397 | |
| 398 | m_blocks.at(n: blockIndex) |
| 399 | ->unwrapLine(line: line - blockStartLine, previousBlock: (blockIndex > 0) ? m_blocks.at(n: blockIndex - 1) : nullptr, fixStartLinesStartIndex: firstLineInBlock ? (blockIndex - 1) : blockIndex); |
| 400 | --m_lines; |
| 401 | |
| 402 | // decrement index for later fixup, if we modified the block in front of the found one |
| 403 | if (firstLineInBlock) { |
| 404 | --blockIndex; |
| 405 | } |
| 406 | |
| 407 | // remember changes |
| 408 | ++m_revision; |
| 409 | |
| 410 | // update changed line interval |
| 411 | if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { |
| 412 | m_editingMinimalLineChanged = line - 1; |
| 413 | } |
| 414 | |
| 415 | if (line <= m_editingMaximalLineChanged) { |
| 416 | --m_editingMaximalLineChanged; |
| 417 | } else { |
| 418 | m_editingMaximalLineChanged = line - 1; |
| 419 | } |
| 420 | |
| 421 | // balance the changed block if needed |
| 422 | balanceBlock(index: blockIndex); |
| 423 | |
| 424 | // emit signal about done change |
| 425 | Q_EMIT m_document->KTextEditor::Document::lineUnwrapped(document: m_document, line); |
| 426 | } |
| 427 | |
| 428 | void TextBuffer::insertText(const KTextEditor::Cursor position, const QString &text) |
| 429 | { |
| 430 | // debug output for REAL low-level debugging |
| 431 | BUFFER_DEBUG << "insertText" << position << text; |
| 432 | |
| 433 | // only allowed if editing transaction running |
| 434 | Q_ASSERT(m_editingTransactions > 0); |
| 435 | |
| 436 | // skip work, if no text to insert |
| 437 | if (text.isEmpty()) { |
| 438 | return; |
| 439 | } |
| 440 | |
| 441 | // get block, this will assert on invalid line |
| 442 | int blockIndex = blockForLine(line: position.line()); |
| 443 | |
| 444 | // let the block handle the insertText |
| 445 | m_blocks.at(n: blockIndex)->insertText(position, text); |
| 446 | m_blockSizes[blockIndex] += text.size(); |
| 447 | |
| 448 | // remember changes |
| 449 | ++m_revision; |
| 450 | |
| 451 | // update changed line interval |
| 452 | if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { |
| 453 | m_editingMinimalLineChanged = position.line(); |
| 454 | } |
| 455 | |
| 456 | if (position.line() > m_editingMaximalLineChanged) { |
| 457 | m_editingMaximalLineChanged = position.line(); |
| 458 | } |
| 459 | |
| 460 | // emit signal about done change |
| 461 | Q_EMIT m_document->KTextEditor::Document::textInserted(document: m_document, position, text); |
| 462 | } |
| 463 | |
| 464 | void TextBuffer::removeText(KTextEditor::Range range) |
| 465 | { |
| 466 | // debug output for REAL low-level debugging |
| 467 | BUFFER_DEBUG << "removeText" << range; |
| 468 | |
| 469 | // only allowed if editing transaction running |
| 470 | Q_ASSERT(m_editingTransactions > 0); |
| 471 | |
| 472 | // only ranges on one line are supported |
| 473 | Q_ASSERT(range.start().line() == range.end().line()); |
| 474 | |
| 475 | // start column <= end column and >= 0 |
| 476 | Q_ASSERT(range.start().column() <= range.end().column()); |
| 477 | Q_ASSERT(range.start().column() >= 0); |
| 478 | |
| 479 | // skip work, if no text to remove |
| 480 | if (range.isEmpty()) { |
| 481 | return; |
| 482 | } |
| 483 | |
| 484 | // get block, this will assert on invalid line |
| 485 | int blockIndex = blockForLine(line: range.start().line()); |
| 486 | |
| 487 | // let the block handle the removeText, retrieve removed text |
| 488 | QString text; |
| 489 | m_blocks.at(n: blockIndex)->removeText(range, removedText&: text); |
| 490 | m_blockSizes[blockIndex] -= text.size(); |
| 491 | |
| 492 | // remember changes |
| 493 | ++m_revision; |
| 494 | |
| 495 | // update changed line interval |
| 496 | if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { |
| 497 | m_editingMinimalLineChanged = range.start().line(); |
| 498 | } |
| 499 | |
| 500 | if (range.start().line() > m_editingMaximalLineChanged) { |
| 501 | m_editingMaximalLineChanged = range.start().line(); |
| 502 | } |
| 503 | |
| 504 | // emit signal about done change |
| 505 | Q_EMIT m_document->KTextEditor::Document::textRemoved(document: m_document, range, text); |
| 506 | } |
| 507 | |
| 508 | int TextBuffer::blockForLine(int line) const |
| 509 | { |
| 510 | // only allow valid lines |
| 511 | if ((line < 0) || (line >= lines())) { |
| 512 | qFatal(msg: "out of range line requested in text buffer (%d out of [0, %d])" , line, lines()); |
| 513 | } |
| 514 | |
| 515 | size_t b = line / BufferBlockSize; |
| 516 | if (b >= m_blocks.size()) { |
| 517 | b = m_blocks.size() - 1; |
| 518 | } |
| 519 | |
| 520 | if (m_startLines[b] <= line && line < m_startLines[b] + m_blocks[b]->lines()) { |
| 521 | return b; |
| 522 | } |
| 523 | |
| 524 | if (m_startLines[b] > line) { |
| 525 | for (int i = b - 1; i >= 0; --i) { |
| 526 | if (m_startLines[i] <= line && line < m_startLines[i] + m_blocks[i]->lines()) { |
| 527 | return i; |
| 528 | } |
| 529 | } |
| 530 | } |
| 531 | |
| 532 | if (m_startLines[b] < line || (m_blocks[b]->lines() == 0)) { |
| 533 | for (size_t i = b + 1; i < m_blocks.size(); ++i) { |
| 534 | if (m_startLines[i] <= line && line < m_startLines[i] + m_blocks[i]->lines()) { |
| 535 | return i; |
| 536 | } |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | qFatal(msg: "line requested in text buffer (%d out of [0, %d[), no block found" , line, lines()); |
| 541 | return -1; |
| 542 | } |
| 543 | |
| 544 | void TextBuffer::fixStartLines(int startBlock, int value) |
| 545 | { |
| 546 | // only allow valid start block |
| 547 | Q_ASSERT(startBlock >= 0); |
| 548 | Q_ASSERT(startBlock <= (int)m_startLines.size()); |
| 549 | // fixup block |
| 550 | for (auto it = m_startLines.begin() + startBlock, end = m_startLines.end(); it != end; ++it) { |
| 551 | // move start line by given value |
| 552 | *it += value; |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | void TextBuffer::balanceBlock(int index) |
| 557 | { |
| 558 | auto check = qScopeGuard(f: [this] { |
| 559 | if (!(m_blocks.size() == m_startLines.size() && m_blocks.size() == m_blockSizes.size())) { |
| 560 | qFatal(msg: "blocks/startlines/blocksizes are not equal in size!" ); |
| 561 | } |
| 562 | }); |
| 563 | |
| 564 | // two cases, too big or too small block |
| 565 | TextBlock *blockToBalance = m_blocks.at(n: index); |
| 566 | |
| 567 | // first case, too big one, split it |
| 568 | if (blockToBalance->lines() >= 2 * BufferBlockSize) { |
| 569 | // half the block |
| 570 | int halfSize = blockToBalance->lines() / 2; |
| 571 | |
| 572 | // create and insert new block after current one, already set right start line |
| 573 | const int newBlockStartLine = m_startLines[index] + halfSize; |
| 574 | TextBlock *newBlock = new TextBlock(this, index + 1); |
| 575 | m_blocks.insert(position: m_blocks.begin() + index + 1, x: newBlock); |
| 576 | m_startLines.insert(position: m_startLines.begin() + index + 1, x: newBlockStartLine); |
| 577 | m_blockSizes.insert(position: m_blockSizes.begin() + index + 1, x: 0); |
| 578 | |
| 579 | // adjust block indexes |
| 580 | for (auto it = m_blocks.begin() + index, end = m_blocks.end(); it != end; ++it) { |
| 581 | (*it)->setBlockIndex(index++); |
| 582 | } |
| 583 | |
| 584 | blockToBalance->splitBlock(fromLine: halfSize, newBlock); |
| 585 | |
| 586 | // split is done |
| 587 | return; |
| 588 | } |
| 589 | |
| 590 | // second case: possibly too small block |
| 591 | |
| 592 | // if only one block, no chance to unite |
| 593 | // same if this is first block, we always append to previous one |
| 594 | if (index == 0) { |
| 595 | // remove the block if its empty |
| 596 | if (blockToBalance->lines() == 0) { |
| 597 | m_blocks.erase(position: m_blocks.begin()); |
| 598 | m_startLines.erase(position: m_startLines.begin()); |
| 599 | m_blockSizes.erase(position: m_blockSizes.begin()); |
| 600 | Q_ASSERT(m_startLines[0] == 0); |
| 601 | for (auto it = m_blocks.begin(), end = m_blocks.end(); it != end; ++it) { |
| 602 | (*it)->setBlockIndex(index++); |
| 603 | } |
| 604 | } |
| 605 | return; |
| 606 | } |
| 607 | |
| 608 | // block still large enough, do nothing |
| 609 | if (2 * blockToBalance->lines() > BufferBlockSize) { |
| 610 | return; |
| 611 | } |
| 612 | |
| 613 | // unite small block with predecessor |
| 614 | TextBlock *targetBlock = m_blocks.at(n: index - 1); |
| 615 | |
| 616 | // merge block |
| 617 | blockToBalance->mergeBlock(targetBlock); |
| 618 | m_blockSizes[index - 1] += m_blockSizes[index]; |
| 619 | |
| 620 | // delete old block |
| 621 | delete blockToBalance; |
| 622 | m_blocks.erase(position: m_blocks.begin() + index); |
| 623 | m_startLines.erase(position: m_startLines.begin() + index); |
| 624 | m_blockSizes.erase(position: m_blockSizes.begin() + index); |
| 625 | |
| 626 | for (auto it = m_blocks.begin() + index, end = m_blocks.end(); it != end; ++it) { |
| 627 | (*it)->setBlockIndex(index++); |
| 628 | } |
| 629 | |
| 630 | Q_ASSERT(index == (int)m_blocks.size()); |
| 631 | } |
| 632 | |
| 633 | void TextBuffer::debugPrint(const QString &title) const |
| 634 | { |
| 635 | // print header with title |
| 636 | printf(format: "%s (lines: %d)\n" , qPrintable(title), m_lines); |
| 637 | |
| 638 | // print all blocks |
| 639 | for (size_t i = 0; i < m_blocks.size(); ++i) { |
| 640 | m_blocks.at(n: i)->debugPrint(blockIndex: i); |
| 641 | } |
| 642 | } |
| 643 | |
| 644 | bool TextBuffer::load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec) |
| 645 | { |
| 646 | // fallback codec must exist |
| 647 | Q_ASSERT(!m_fallbackTextCodec.isEmpty()); |
| 648 | |
| 649 | // codec must be set! |
| 650 | Q_ASSERT(!m_textCodec.isEmpty()); |
| 651 | |
| 652 | // first: clear buffer in any case! |
| 653 | clear(); |
| 654 | |
| 655 | // construct the file loader for the given file, with correct prober type |
| 656 | Kate::TextLoader file(filename, m_encodingProberType, m_lineLengthLimit); |
| 657 | |
| 658 | // triple play, maximal three loading rounds |
| 659 | // 0) use the given encoding, be done, if no encoding errors happen |
| 660 | // 1) use BOM to decided if Unicode or if that fails, use encoding prober, if no encoding errors happen, be done |
| 661 | // 2) use fallback encoding, be done, if no encoding errors happen |
| 662 | // 3) use again given encoding, be done in any case |
| 663 | for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) { |
| 664 | // kill all blocks beside first one |
| 665 | for (size_t b = 1; b < m_blocks.size(); ++b) { |
| 666 | TextBlock *block = m_blocks.at(n: b); |
| 667 | block->clearLines(); |
| 668 | delete block; |
| 669 | } |
| 670 | m_blocks.resize(new_size: 1); |
| 671 | m_startLines.resize(new_size: 1); |
| 672 | m_blockSizes.resize(new_size: 1); |
| 673 | |
| 674 | // remove lines in first block |
| 675 | m_blocks.back()->clearLines(); |
| 676 | m_startLines.back() = 0; |
| 677 | m_blockSizes.back() = 0; |
| 678 | m_lines = 0; |
| 679 | |
| 680 | // reset error flags |
| 681 | tooLongLinesWrapped = false; |
| 682 | longestLineLoaded = 0; |
| 683 | |
| 684 | // try to open file, with given encoding |
| 685 | // in round 0 + 3 use the given encoding from user |
| 686 | // in round 1 use 0, to trigger detection |
| 687 | // in round 2 use fallback |
| 688 | QString codec = m_textCodec; |
| 689 | if (i == 1) { |
| 690 | codec.clear(); |
| 691 | } else if (i == 2) { |
| 692 | codec = m_fallbackTextCodec; |
| 693 | } |
| 694 | |
| 695 | if (!file.open(codec)) { |
| 696 | // create one dummy textline, in any case |
| 697 | m_blocks.back()->appendLine(textOfLine: QString()); |
| 698 | m_lines++; |
| 699 | m_blockSizes[0] = 1; |
| 700 | return false; |
| 701 | } |
| 702 | |
| 703 | // read in all lines... |
| 704 | encodingErrors = false; |
| 705 | while (!file.eof()) { |
| 706 | // read line |
| 707 | int offset = 0; |
| 708 | int length = 0; |
| 709 | bool currentError = !file.readLine(offset, length, tooLongLinesWrapped, longestLineLoaded); |
| 710 | encodingErrors = encodingErrors || currentError; |
| 711 | |
| 712 | // bail out on encoding error, if not last round! |
| 713 | if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) { |
| 714 | BUFFER_DEBUG << "Failed try to load file" << filename << "with codec" << file.textCodec(); |
| 715 | break; |
| 716 | } |
| 717 | |
| 718 | // ensure blocks aren't too large |
| 719 | if (m_blocks.back()->lines() >= BufferBlockSize) { |
| 720 | int index = (int)m_blocks.size(); |
| 721 | int startLine = m_blocks.back()->startLine() + m_blocks.back()->lines(); |
| 722 | m_blocks.push_back(x: new TextBlock(this, index)); |
| 723 | m_startLines.push_back(x: startLine); |
| 724 | m_blockSizes.push_back(x: 0); |
| 725 | } |
| 726 | |
| 727 | // append line to last block |
| 728 | m_blocks.back()->appendLine(textOfLine: QString(file.unicode() + offset, length)); |
| 729 | m_blockSizes.back() += length + 1; |
| 730 | ++m_lines; |
| 731 | } |
| 732 | |
| 733 | // if no encoding error, break out of reading loop |
| 734 | if (!encodingErrors) { |
| 735 | // remember used codec, might change bom setting |
| 736 | setTextCodec(file.textCodec()); |
| 737 | break; |
| 738 | } |
| 739 | } |
| 740 | |
| 741 | // save checksum of file on disk |
| 742 | setDigest(file.digest()); |
| 743 | |
| 744 | // remember if BOM was found |
| 745 | if (file.byteOrderMarkFound()) { |
| 746 | setGenerateByteOrderMark(true); |
| 747 | } |
| 748 | |
| 749 | // remember eol mode, if any found in file |
| 750 | if (file.eol() != eolUnknown) { |
| 751 | setEndOfLineMode(file.eol()); |
| 752 | } |
| 753 | |
| 754 | // remember mime type for filter device |
| 755 | m_mimeTypeForFilterDev = file.mimeTypeForFilterDev(); |
| 756 | |
| 757 | // assert that one line is there! |
| 758 | Q_ASSERT(m_lines > 0); |
| 759 | |
| 760 | // report CODEC + ERRORS |
| 761 | BUFFER_DEBUG << "Loaded file " << filename << "with codec" << m_textCodec << (encodingErrors ? "with" : "without" ) << "encoding errors" ; |
| 762 | |
| 763 | // report BOM |
| 764 | BUFFER_DEBUG << (file.byteOrderMarkFound() ? "Found" : "Didn't find" ) << "byte order mark" ; |
| 765 | |
| 766 | // report filter device mime-type |
| 767 | BUFFER_DEBUG << "used filter device for mime-type" << m_mimeTypeForFilterDev; |
| 768 | |
| 769 | // emit success |
| 770 | Q_EMIT loaded(filename, encodingErrors); |
| 771 | |
| 772 | // file loading worked, modulo encoding problems |
| 773 | return true; |
| 774 | } |
| 775 | |
| 776 | const QByteArray &TextBuffer::digest() const |
| 777 | { |
| 778 | return m_digest; |
| 779 | } |
| 780 | |
| 781 | void TextBuffer::setDigest(const QByteArray &checksum) |
| 782 | { |
| 783 | m_digest = checksum; |
| 784 | } |
| 785 | |
| 786 | void TextBuffer::setTextCodec(const QString &codec) |
| 787 | { |
| 788 | m_textCodec = codec; |
| 789 | |
| 790 | // enforce bom for some encodings |
| 791 | if (const auto setEncoding = QStringConverter::encodingForName(name: m_textCodec.toUtf8().constData())) { |
| 792 | for (const auto encoding : {QStringConverter::Utf16, |
| 793 | QStringConverter::Utf16BE, |
| 794 | QStringConverter::Utf16LE, |
| 795 | QStringConverter::Utf32, |
| 796 | QStringConverter::Utf32BE, |
| 797 | QStringConverter::Utf32LE}) { |
| 798 | if (setEncoding == encoding) { |
| 799 | setGenerateByteOrderMark(true); |
| 800 | break; |
| 801 | } |
| 802 | } |
| 803 | } |
| 804 | } |
| 805 | |
| 806 | bool TextBuffer::save(const QString &filename) |
| 807 | { |
| 808 | // codec must be set, else below we fail! |
| 809 | Q_ASSERT(!m_textCodec.isEmpty()); |
| 810 | |
| 811 | // ensure we do not kill symlinks, see bug 498589 |
| 812 | auto realFile = filename; |
| 813 | if (const auto realFileResolved = QFileInfo(realFile).canonicalFilePath(); !realFileResolved.isEmpty()) { |
| 814 | realFile = realFileResolved; |
| 815 | } |
| 816 | |
| 817 | const auto saveRes = saveBufferUnprivileged(filename: realFile); |
| 818 | if (saveRes == SaveResult::Failed) { |
| 819 | return false; |
| 820 | } |
| 821 | if (saveRes == SaveResult::MissingPermissions) { |
| 822 | // either unit-test mode or we're missing permissions to write to the |
| 823 | // file => use temporary file and try to use authhelper |
| 824 | if (!saveBufferEscalated(filename: realFile)) { |
| 825 | return false; |
| 826 | } |
| 827 | } |
| 828 | |
| 829 | // remember this revision as last saved |
| 830 | m_history.setLastSavedRevision(); |
| 831 | |
| 832 | // inform that we have saved the state |
| 833 | markModifiedLinesAsSaved(); |
| 834 | |
| 835 | // emit that file was saved and be done |
| 836 | Q_EMIT saved(filename); |
| 837 | return true; |
| 838 | } |
| 839 | |
| 840 | bool TextBuffer::saveBuffer(const QString &filename, KCompressionDevice &saveFile) |
| 841 | { |
| 842 | QStringEncoder encoder(m_textCodec.toUtf8().constData(), generateByteOrderMark() ? QStringConverter::Flag::WriteBom : QStringConverter::Flag::Default); |
| 843 | |
| 844 | // our loved eol string ;) |
| 845 | QString eol = QStringLiteral("\n" ); |
| 846 | if (endOfLineMode() == eolDos) { |
| 847 | eol = QStringLiteral("\r\n" ); |
| 848 | } else if (endOfLineMode() == eolMac) { |
| 849 | eol = QStringLiteral("\r" ); |
| 850 | } |
| 851 | |
| 852 | // just dump the lines out ;) |
| 853 | for (int i = 0; i < m_lines; ++i) { |
| 854 | // dump current line |
| 855 | saveFile.write(data: encoder.encode(str: line(line: i).text())); |
| 856 | |
| 857 | // append correct end of line string |
| 858 | if ((i + 1) < m_lines) { |
| 859 | saveFile.write(data: encoder.encode(str: eol)); |
| 860 | } |
| 861 | |
| 862 | // early out on stream errors |
| 863 | if (saveFile.error() != QFileDevice::NoError) { |
| 864 | return false; |
| 865 | } |
| 866 | } |
| 867 | |
| 868 | // TODO: this only writes bytes when there is text. This is a fine optimization for most cases, but this makes saving |
| 869 | // an empty file with the BOM set impossible (results to an empty file with 0 bytes, no BOM) |
| 870 | |
| 871 | // close the file, we might want to read from underlying buffer below |
| 872 | saveFile.close(); |
| 873 | |
| 874 | // did save work? |
| 875 | if (saveFile.error() != QFileDevice::NoError) { |
| 876 | BUFFER_DEBUG << "Saving file " << filename << "failed with error" << saveFile.errorString(); |
| 877 | return false; |
| 878 | } |
| 879 | |
| 880 | return true; |
| 881 | } |
| 882 | |
| 883 | TextBuffer::SaveResult TextBuffer::saveBufferUnprivileged(const QString &filename) |
| 884 | { |
| 885 | if (m_alwaysUseKAuthForSave) { |
| 886 | // unit-testing mode, simulate we need privileges |
| 887 | return SaveResult::MissingPermissions; |
| 888 | } |
| 889 | |
| 890 | // construct correct filter device |
| 891 | // we try to use the same compression as for opening |
| 892 | const KCompressionDevice::CompressionType type = KCompressionDevice::compressionTypeForMimeType(mimetype: m_mimeTypeForFilterDev); |
| 893 | auto saveFile = std::make_unique<KCompressionDevice>(args: filename, args: type); |
| 894 | |
| 895 | if (!saveFile->open(mode: QIODevice::WriteOnly)) { |
| 896 | #ifdef CAN_USE_ERRNO |
| 897 | if (errno != EACCES) { |
| 898 | return SaveResult::Failed; |
| 899 | } |
| 900 | #endif |
| 901 | return SaveResult::MissingPermissions; |
| 902 | } |
| 903 | |
| 904 | if (!saveBuffer(filename, saveFile&: *saveFile)) { |
| 905 | return SaveResult::Failed; |
| 906 | } |
| 907 | |
| 908 | return SaveResult::Success; |
| 909 | } |
| 910 | |
| 911 | bool TextBuffer::saveBufferEscalated(const QString &filename) |
| 912 | { |
| 913 | #if HAVE_KAUTH |
| 914 | // construct correct filter device |
| 915 | // we try to use the same compression as for opening |
| 916 | const KCompressionDevice::CompressionType type = KCompressionDevice::compressionTypeForMimeType(mimetype: m_mimeTypeForFilterDev); |
| 917 | auto saveFile = std::make_unique<KCompressionDevice>(args: filename, args: type); |
| 918 | uint ownerId = -2; |
| 919 | uint groupId = -2; |
| 920 | std::unique_ptr<QIODevice> temporaryBuffer; |
| 921 | |
| 922 | // Memorize owner and group. |
| 923 | const QFileInfo fileInfo(filename); |
| 924 | if (fileInfo.exists()) { |
| 925 | ownerId = fileInfo.ownerId(); |
| 926 | groupId = fileInfo.groupId(); |
| 927 | } |
| 928 | |
| 929 | // if that fails we need more privileges to save this file |
| 930 | // -> we write to a temporary file and then send its path to KAuth action for privileged save |
| 931 | temporaryBuffer = std::make_unique<QBuffer>(); |
| 932 | |
| 933 | // open buffer for write and read (read is used for checksum computing and writing to temporary file) |
| 934 | if (!temporaryBuffer->open(mode: QIODevice::ReadWrite)) { |
| 935 | return false; |
| 936 | } |
| 937 | |
| 938 | // we are now saving to a temporary buffer with potential compression proxy |
| 939 | saveFile = std::make_unique<KCompressionDevice>(args: temporaryBuffer.get(), args: false, args: type); |
| 940 | if (!saveFile->open(mode: QIODevice::WriteOnly)) { |
| 941 | return false; |
| 942 | } |
| 943 | |
| 944 | if (!saveBuffer(filename, saveFile&: *saveFile)) { |
| 945 | return false; |
| 946 | } |
| 947 | |
| 948 | // temporary buffer was used to save the file |
| 949 | // -> computing checksum |
| 950 | // -> saving to temporary file |
| 951 | // -> copying the temporary file to the original file location with KAuth action |
| 952 | QTemporaryFile tempFile; |
| 953 | if (!tempFile.open()) { |
| 954 | return false; |
| 955 | } |
| 956 | |
| 957 | // go to QBuffer start |
| 958 | temporaryBuffer->seek(pos: 0); |
| 959 | |
| 960 | // read contents of QBuffer and add them to checksum utility as well as to QTemporaryFile |
| 961 | char buffer[bufferLength]; |
| 962 | qint64 read = -1; |
| 963 | QCryptographicHash cryptographicHash(SecureTextBuffer::checksumAlgorithm); |
| 964 | while ((read = temporaryBuffer->read(data: buffer, maxlen: bufferLength)) > 0) { |
| 965 | cryptographicHash.addData(data: QByteArrayView(buffer, read)); |
| 966 | if (tempFile.write(data: buffer, len: read) == -1) { |
| 967 | return false; |
| 968 | } |
| 969 | } |
| 970 | if (!tempFile.flush()) { |
| 971 | return false; |
| 972 | } |
| 973 | |
| 974 | // prepare data for KAuth action |
| 975 | QVariantMap kAuthActionArgs; |
| 976 | kAuthActionArgs.insert(QStringLiteral("sourceFile" ), value: tempFile.fileName()); |
| 977 | kAuthActionArgs.insert(QStringLiteral("targetFile" ), value: filename); |
| 978 | kAuthActionArgs.insert(QStringLiteral("checksum" ), value: cryptographicHash.result()); |
| 979 | kAuthActionArgs.insert(QStringLiteral("ownerId" ), value: ownerId); |
| 980 | kAuthActionArgs.insert(QStringLiteral("groupId" ), value: groupId); |
| 981 | |
| 982 | // call save with elevated privileges |
| 983 | if (QStandardPaths::isTestModeEnabled()) { |
| 984 | // unit testing purposes only |
| 985 | if (!SecureTextBuffer::savefile(args: kAuthActionArgs).succeeded()) { |
| 986 | return false; |
| 987 | } |
| 988 | } else { |
| 989 | KAuth::Action kAuthSaveAction(QStringLiteral("org.kde.ktexteditor6.katetextbuffer.savefile" )); |
| 990 | kAuthSaveAction.setHelperId(QStringLiteral("org.kde.ktexteditor6.katetextbuffer" )); |
| 991 | kAuthSaveAction.setArguments(kAuthActionArgs); |
| 992 | KAuth::ExecuteJob *job = kAuthSaveAction.execute(); |
| 993 | if (!job->exec()) { |
| 994 | return false; |
| 995 | } |
| 996 | } |
| 997 | |
| 998 | return true; |
| 999 | #else |
| 1000 | Q_UNUSED(filename); |
| 1001 | return false; |
| 1002 | #endif |
| 1003 | } |
| 1004 | |
| 1005 | void TextBuffer::(KTextEditor::View *view, KTextEditor::LineRange lineRange, bool needsRepaint, TextRange *deleteRange) |
| 1006 | { |
| 1007 | // ignore calls if no document is around |
| 1008 | if (!m_document) { |
| 1009 | return; |
| 1010 | } |
| 1011 | |
| 1012 | // update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really |
| 1013 | // just create 20k ranges in a go and you wait seconds on a decent machine |
| 1014 | const QList<KTextEditor::View *> views = m_document->views(); |
| 1015 | for (KTextEditor::View *curView : views) { |
| 1016 | // filter wrong views |
| 1017 | if (view && view != curView && !deleteRange) { |
| 1018 | continue; |
| 1019 | } |
| 1020 | |
| 1021 | // notify view, it is really a kate view |
| 1022 | static_cast<KTextEditor::ViewPrivate *>(curView)->notifyAboutRangeChange(lineRange, needsRepaint, deleteRange); |
| 1023 | } |
| 1024 | } |
| 1025 | |
| 1026 | void TextBuffer::markModifiedLinesAsSaved() |
| 1027 | { |
| 1028 | for (TextBlock *block : std::as_const(t&: m_blocks)) { |
| 1029 | block->markModifiedLinesAsSaved(); |
| 1030 | } |
| 1031 | } |
| 1032 | |
| 1033 | void TextBuffer::(TextRange *range) |
| 1034 | { |
| 1035 | auto it = std::find(first: m_multilineRanges.begin(), last: m_multilineRanges.end(), val: range); |
| 1036 | if (it == m_multilineRanges.end()) { |
| 1037 | m_multilineRanges.push_back(x: range); |
| 1038 | return; |
| 1039 | } |
| 1040 | } |
| 1041 | |
| 1042 | void TextBuffer::(TextRange *range) |
| 1043 | { |
| 1044 | m_multilineRanges.erase(first: std::remove(first: m_multilineRanges.begin(), last: m_multilineRanges.end(), value: range), last: m_multilineRanges.end()); |
| 1045 | } |
| 1046 | |
| 1047 | bool TextBuffer::hasMultlineRange(KTextEditor::MovingRange *range) const |
| 1048 | { |
| 1049 | return std::find(first: m_multilineRanges.begin(), last: m_multilineRanges.end(), val: range) != m_multilineRanges.end(); |
| 1050 | } |
| 1051 | |
| 1052 | void TextBuffer::(int line, KTextEditor::View *view, bool rangesWithAttributeOnly, QList<TextRange *> &outRanges) const |
| 1053 | { |
| 1054 | outRanges.clear(); |
| 1055 | // get block, this will assert on invalid line |
| 1056 | const int blockIndex = blockForLine(line); |
| 1057 | m_blocks.at(n: blockIndex)->rangesForLine(line, view, rangesWithAttributeOnly, outRanges); |
| 1058 | // printf("Requested range for line %d, available %d\n", (int)line, (int)m_multilineRanges.size()); |
| 1059 | for (TextRange *range : std::as_const(t: m_multilineRanges)) { |
| 1060 | if (rangesWithAttributeOnly && !range->hasAttribute()) { |
| 1061 | continue; |
| 1062 | } |
| 1063 | |
| 1064 | // we want ranges for no view, but this one's attribute is only valid for views |
| 1065 | if (!view && range->attributeOnlyForViews()) { |
| 1066 | continue; |
| 1067 | } |
| 1068 | |
| 1069 | // the range's attribute is not valid for this view |
| 1070 | if (range->view() && range->view() != view) { |
| 1071 | continue; |
| 1072 | } |
| 1073 | |
| 1074 | // if line is in the range, ok |
| 1075 | if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) { |
| 1076 | outRanges.append(t: range); |
| 1077 | } |
| 1078 | } |
| 1079 | std::sort(first: outRanges.begin(), last: outRanges.end()); |
| 1080 | outRanges.erase(abegin: std::unique(first: outRanges.begin(), last: outRanges.end()), aend: outRanges.end()); |
| 1081 | } |
| 1082 | } |
| 1083 | |
| 1084 | #include "moc_katetextbuffer.cpp" |
| 1085 | |