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