Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ umbrella [#908](https://github.com/IJHack/QtPass/issues/908)).
- Bounded and non-blocking reads where store content or a pasted value
could cost QtPass: an OTP configuration over 4096 characters is refused
before it is parsed; content search keeps at most 200 characters of a
matching line; a `.gpg-id.sig` must be a regular file of at most 64 KiB
before `gpg` is given it (a FIFO blocked QtPass); a key file is opened
without blocking on a FIFO; and the key generator refuses a name or
passphrase with a line break, which would have become a batch directive.
matching line, around the match and never half a surrogate pair; a
`.gpg-id.sig` must be a regular file of at most 64 KiB before `gpg` is
given it (a FIFO blocked QtPass); a key file is opened without blocking
on a FIFO; and the key generator refuses a name or passphrase with a
line break, which would have become a batch directive.
- An entry that decompresses into millions of lines no longer exhausts
memory below the 64 MiB plaintext limit. Showing one read every line into
a list of its own (gigabytes for an entry of newlines) and made a row of
Expand Down
15 changes: 11 additions & 4 deletions src/nativegrep.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,19 @@ auto NativeGrep::matchFile(const QProcessEnvironment &env,
// it; "…" marks what was left out on either side.
const qsizetype at =
qMax<qsizetype>(0, m.capturedStart() - (t.data() - line.data()));
const qsizetype start = qBound<qsizetype>(0, at - kMaxPreviewLength / 4,
t.size() - kMaxPreviewLength);
QString preview = t.mid(start, kMaxPreviewLength).toString();
qsizetype start = qBound<qsizetype>(0, at - kMaxPreviewLength / 4,
t.size() - kMaxPreviewLength);
qsizetype end = start + kMaxPreviewLength;
// Never half a surrogate pair (an emoji, CJK Extension B) at either
// edge: the window takes the whole character.
if (start > 0 && t[start].isLowSurrogate())
--start;
if (end < t.size() && t[end - 1].isHighSurrogate())
++end;
QString preview = t.sliced(start, end - start).toString();
if (start > 0)
preview.prepend(QChar(0x2026));
if (start + kMaxPreviewLength < t.size())
if (end < t.size())
preview.append(QChar(0x2026));
matches << preview;
}
Expand Down
9 changes: 6 additions & 3 deletions src/nativegrep.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ class NativeGrep : public QObject {
static constexpr int kMaxMatches = 10000;

/**
* @brief Longest preview of a matching line, in characters.
* @brief Longest preview of a matching line, in UTF-16 code units.
*
* A result shows where the pattern matched, not the line; a single line
* can be most of an entry's 64 MiB plaintext, and 10000 such copies would
* not fit in memory. A longer line is shown as a window of this many
* characters around the match, "…" where the line goes on.
* not fit in memory. A longer line is shown as a window of this many code
* units around the match (up to two more, one per edge, to keep a
* surrogate pair whole), "…" where the line goes on. A sequence of
* several code points (a ZWJ emoji, a flag) can still be cut between
* them.
*/
static constexpr qsizetype kMaxPreviewLength = 200;

Expand Down
37 changes: 37 additions & 0 deletions tests/auto/util/tst_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2967,6 +2967,43 @@ void tst_util::grepTakesABoundedNumberOfMatches() {
qPrintable(far.first()));
QVERIFY(far.first().startsWith(QChar(0x2026)));
QVERIFY(far.first().endsWith(QChar(0x2026)));

// Characters outside the BMP on both sides, the window's edges falling
// inside a surrogate pair: the preview keeps every pair whole.
const QString emoji = QString::fromUtf8("\xF0\x9F\x98\x80");
// The "x" puts the match an odd number of code units from the pairs
// before it, so the window starts inside one and ends inside another.
const QString wide = QStringLiteral("ab") + emoji.repeated(1000) +
QStringLiteral("xhit") + emoji.repeated(1000);
const QString plain = tmp.filePath(QStringLiteral("wide.txt"));
{
QFile f(plain);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(wide.toUtf8() + '\n');
}
QVERIFY(script.open(QIODevice::WriteOnly | QIODevice::Truncate));
// Next to the script, so no path is quoted into it (TMPDIR may hold a
// quote).
script.write("#!/bin/sh\ncat \"$(dirname \"$0\")/wide.txt\"\n");
script.close();
const QStringList pairs = NativeGrep::matchFile(
env, longGpg, QDir(store).filePath("e0000.gpg"), rx);
QCOMPARE(pairs.size(), 1);
const QString shown = pairs.first();
QVERIFY2(shown.contains(QStringLiteral("hit")), qPrintable(shown));
// Both edges fell inside a pair and took one more code unit, and "…"
// stands on either side.
QCOMPARE(shown.size(), NativeGrep::kMaxPreviewLength + 4);
for (qsizetype i = 0; i < shown.size(); ++i) {
if (shown.at(i).isHighSurrogate()) {
QVERIFY2(
i + 1 < shown.size() && shown.at(i + 1).isLowSurrogate(),
qPrintable(QStringLiteral("lone high surrogate at %1").arg(i)));
} else if (shown.at(i).isLowSurrogate()) {
QVERIFY2(i > 0 && shown.at(i - 1).isHighSurrogate(),
qPrintable(QStringLiteral("lone low surrogate at %1").arg(i)));
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

// Exactly the budget, every entry whole: complete, no marker anywhere.
Expand Down
Loading