Line data Source code
1 : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief NativeGrep, the content search that decrypts every entry on a worker
6 : * thread instead of running `pass grep`.
7 : */
8 : #ifndef SRC_NATIVEGREP_H_
9 : #define SRC_NATIVEGREP_H_
10 :
11 : #include <QList>
12 : #include <QObject>
13 : #include <QPair>
14 : #include <QProcessEnvironment>
15 : #include <QString>
16 : #include <QStringList>
17 : #include <atomic>
18 : #include <memory>
19 :
20 : class QRegularExpression;
21 : class QThread;
22 :
23 : /**
24 : * @class NativeGrep
25 : * @ingroup backend
26 : * @brief Full-text search over a password store without `pass grep`: every
27 : * `.gpg` file is decrypted with gpg on a worker thread and its lines matched
28 : * against a QRegularExpression. Only the newest search may report; a search
29 : * started while another runs asks that one to stop and supersedes it.
30 : */
31 : class NativeGrep : public QObject {
32 : Q_OBJECT
33 :
34 : public:
35 : /**
36 : * @brief Create a searcher.
37 : * @param parent Owner; results are delivered on its thread.
38 : */
39 : explicit NativeGrep(QObject *parent = nullptr);
40 :
41 : /**
42 : * @brief Most matching lines taken from one entry.
43 : *
44 : * A compressed entry can expand into millions of lines within the
45 : * plaintext limit, each a result and a row in the results list; past this
46 : * many, the entry gets one line saying there are more.
47 : */
48 : static constexpr int kMaxMatchesPerEntry = 100;
49 :
50 : /**
51 : * @brief Most matching lines a search collects over the whole store.
52 : *
53 : * The search stops once it has this many; the window says so.
54 : */
55 : static constexpr int kMaxMatches = 10000;
56 :
57 : /**
58 : * @brief Longest preview of a matching line, in UTF-16 code units.
59 : *
60 : * A result shows where the pattern matched, not the line; a single line
61 : * can be most of an entry's 64 MiB plaintext, and 10000 such copies would
62 : * not fit in memory. A longer line is shown as a window of this many code
63 : * units around the match (up to two more, one per edge, to keep a
64 : * surrogate pair whole), "…" where the line goes on. A sequence of
65 : * several code points (a ZWJ emoji, a flag) can still be cut between
66 : * them.
67 : */
68 : static constexpr qsizetype kMaxPreviewLength = 200;
69 :
70 : /**
71 : * @brief Whether a line in a result is the marker for "more matches in
72 : * this entry" rather than a match.
73 : *
74 : * A match is a trimmed, non-empty line, so an empty one can only be the
75 : * marker, which matchFile() and scanStore() put last. Not a match: not
76 : * counted, not content to hide.
77 : * @param line A line of a result.
78 : * @return Whether it is the marker.
79 : */
80 : [[nodiscard]] static auto isMoreMarker(const QString &line) -> bool {
81 : return line.isEmpty();
82 : }
83 :
84 : /**
85 : * @brief Whether a result is the marker for "the search stopped at
86 : * kMaxMatches, more entries were not searched".
87 : *
88 : * No entry has an empty name, so a result without one can only be the
89 : * marker, which scanStore() puts last.
90 : * @param result A result.
91 : * @return Whether it is the marker.
92 : */
93 : [[nodiscard]] static auto
94 : isStoppedMarker(const QPair<QString, QStringList> &result) -> bool {
95 : return result.first.isEmpty();
96 : }
97 : /**
98 : * @brief Interrupt running searches and wait up to five seconds for them.
99 : */
100 : ~NativeGrep() override;
101 :
102 : /**
103 : * @brief Start a search; finished() follows asynchronously, also for an
104 : * empty or invalid pattern (with no results).
105 : * @param pattern PCRE pattern.
106 : * @param caseInsensitive Match regardless of case.
107 : * @param gpgExe The gpg executable (a `wsl ` prefix routes paths through
108 : * wslpath).
109 : * @param storeDir The store to walk.
110 : * @param env Environment for gpg (GNUPGHOME and friends).
111 : */
112 : void search(const QString &pattern, bool caseInsensitive,
113 : const QString &gpgExe, const QString &storeDir,
114 : const QProcessEnvironment &env);
115 :
116 : /**
117 : * @brief Ask running searches to stop without waiting for them: the walk
118 : * ends at the next file and a gpg that is decrypting is terminated by its
119 : * own worker thread.
120 : */
121 : void cancel();
122 :
123 : /**
124 : * @brief Decrypt one .gpg file and return its trimmed lines matching @p rx.
125 : * @param env Environment for gpg.
126 : * @param gpgExe The gpg executable.
127 : * @param filePath The encrypted file.
128 : * @param rx The pattern.
129 : * @param cancel When set, gpg is not started, or is terminated while it
130 : * runs; nullptr to run to completion.
131 : * @return Matching lines, at most kMaxMatchesPerEntry and then a marker
132 : * (isMoreMarker()) when there are more; empty when gpg fails, is
133 : * cancelled or nothing matches.
134 : */
135 : static auto matchFile(const QProcessEnvironment &env, const QString &gpgExe,
136 : const QString &filePath, const QRegularExpression &rx,
137 : const std::atomic_bool *cancel = nullptr)
138 : -> QStringList;
139 : /**
140 : * @brief Walk @p storeDir, decrypt every .gpg file and collect the matches.
141 : * @param env Environment for gpg.
142 : * @param gpgExe The gpg executable.
143 : * @param storeDir The store.
144 : * @param rx The pattern.
145 : * @param cancel Stops the walk and the running gpg when set; nullptr to
146 : * rely on QThread::isInterruptionRequested() alone.
147 : * @return (entry name without .gpg, matching lines) per file with matches,
148 : * at most kMaxMatches lines in all, an entry cut short ending in a
149 : * marker (isMoreMarker()); when the walk stopped at that budget
150 : * with entries left, a last result that is a marker
151 : * (isStoppedMarker()). Empty when the search is stopped.
152 : */
153 : static auto scanStore(const QProcessEnvironment &env, const QString &gpgExe,
154 : const QString &storeDir, const QRegularExpression &rx,
155 : const std::atomic_bool *cancel = nullptr)
156 : -> QList<QPair<QString, QStringList>>;
157 :
158 : signals:
159 : /**
160 : * @brief The newest search is over.
161 : * @param results (entry, matching lines) pairs.
162 : */
163 : void finished(const QList<QPair<QString, QStringList>> &results);
164 :
165 : private:
166 : /// A running search: its thread and the flag its gpg runs poll. The flag
167 : /// is shared with the thread's lambda so it outlives this object if a
168 : /// search is still winding down when it is destroyed.
169 41 : struct Worker {
170 : /** @brief The thread the search runs on. */
171 : QThread *thread;
172 : /** @brief Set to ask the search to stop; polled by its gpg runs. */
173 : std::shared_ptr<std::atomic_bool> cancel;
174 : };
175 : /**
176 : * @brief Number of the newest search. A worker reports through finished()
177 : * only when its own number still equals this.
178 : */
179 : int m_seq = 0;
180 : /** @brief Searches still running, superseded ones included. */
181 : QList<Worker> m_workers;
182 : };
183 :
184 : #endif // SRC_NATIVEGREP_H_
|