QtPass Developer documentation
QtPass test coverage
Current view: top level - src/nativegrep.h Coverage Total Hit
Test: QtPass Lines: 100.0 % 1 1
Test Date: 2026-11-14 13:19:18 Functions: - 0 0

            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_
        

Generated by: LCOV version 2.0-1