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
49namespace Kate
50{
51TextBuffer::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
72TextBuffer::~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
117void 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
136void 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
194TextLine 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
203void 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
212int 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
239KTextEditor::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
266QString 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
286bool 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
309bool 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
335void 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
375void 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
428void 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
464void 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
508int 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
544void 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
556void 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
633void 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
644bool 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
776const QByteArray &TextBuffer::digest() const
777{
778 return m_digest;
779}
780
781void TextBuffer::setDigest(const QByteArray &checksum)
782{
783 m_digest = checksum;
784}
785
786void 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
806bool 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
840bool 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
883TextBuffer::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
911bool 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
1005void TextBuffer::notifyAboutRangeChange(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
1026void TextBuffer::markModifiedLinesAsSaved()
1027{
1028 for (TextBlock *block : std::as_const(t&: m_blocks)) {
1029 block->markModifiedLinesAsSaved();
1030 }
1031}
1032
1033void TextBuffer::addMultilineRange(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
1042void TextBuffer::removeMultilineRange(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
1047bool 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
1052void TextBuffer::rangesForLine(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

source code of ktexteditor/src/buffer/katetextbuffer.cpp