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 | |
48 | namespace Kate |
49 | { |
50 | TextBuffer::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 | |
71 | TextBuffer::~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 | |
100 | void 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 | |
109 | void 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 | |
151 | TextLine 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 | |
160 | void 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 | |
169 | int 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 | |
198 | KTextEditor::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 | |
222 | QString 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 | |
241 | bool 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 | |
264 | bool 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 | |
290 | void 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 | |
329 | void 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 | |
379 | void 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 | |
414 | void 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 | |
457 | int 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 | |
496 | void 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 | |
517 | void 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 | |
560 | void 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 | |
571 | bool 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 | |
693 | const QByteArray &TextBuffer::digest() const |
694 | { |
695 | return m_digest; |
696 | } |
697 | |
698 | void TextBuffer::setDigest(const QByteArray &checksum) |
699 | { |
700 | m_digest = checksum; |
701 | } |
702 | |
703 | void 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 | |
723 | bool 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 | |
751 | bool 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 | |
794 | TextBuffer::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 | |
822 | bool 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 | |
916 | void 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 | |
937 | void 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 | |