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

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