QtPass Developer documentation
QtPass test coverage
Current view: top level - src/imitatepass.h (source / functions) Coverage Total Hit
Test: QtPass Lines: 71.4 % 7 5
Test Date: 2026-11-14 13:19:18 Functions: 0.0 % 1 0

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief ImitatePass, the backend that does what `pass` does by running gpg
       6              :  * and git itself.
       7              :  */
       8              : #ifndef SRC_IMITATEPASS_H_
       9              : #define SRC_IMITATEPASS_H_
      10              : 
      11              : #include "gpgidsigner.h"
      12              : #include "nativegrep.h"
      13              : #include "pass.h"
      14              : #include "simpletransaction.h"
      15              : #include <QQueue>
      16              : #include <QTemporaryDir>
      17              : 
      18              : #include <atomic>
      19              : #include <memory>
      20              : #include <optional>
      21              : 
      22              : class QRegularExpression;
      23              : class QThread;
      24              : 
      25              : /**
      26              :  * @class ImitatePass
      27              :  * @ingroup backend
      28              :  * @brief Implementation that imitates 'pass' when the real tool is unavailable.
      29              :  *
      30              :  * ImitatePass provides a complete password store implementation using direct
      31              :  * GPG operations when 'pass' is not installed or not enabled. It handles:
      32              :  * - Direct GPG encryption/decryption
      33              :  * - Git operations via direct subprocess calls
      34              :  * - GPG key management and recipient lists
      35              :  * - File-based password storage (.gpg files)
      36              :  * - Re-encryption when recipients change
      37              :  *
      38              :  * This is used as a fallback when RealPass cannot be initialized.
      39              :  */
      40              : class ImitatePass : public Pass {
      41          413 :   Q_OBJECT
      42              : 
      43              :   friend class tst_util;
      44              :   friend class tst_imitatepass;
      45              : 
      46              : protected:
      47              :   /**
      48              :    * @brief The signer for this store's `.gpg-id`, bound to the configured gpg
      49              :    * and signing keys and to execBlocking(), so a verification on the
      50              :    * re-encryption worker can be interrupted like every other gpg run there.
      51              :    * @return A signer; cheap to build, valid while m_settings is.
      52              :    */
      53              :   auto gpgIdSigner() -> GpgIdSigner;
      54              :   /**
      55              :    * @brief Read a `.gpg-id`, verify its detached signature and parse the
      56              :    * recipients from the bytes that were verified.
      57              :    *
      58              :    * One read serves both the check and the parse, so a file swapped in
      59              :    * between cannot smuggle in a recipient the signature never covered.
      60              :    * @param gpgIdFile The `.gpg-id`.
      61              :    * @param recipients Receives the recipients; empty on failure.
      62              :    * @param why Receives, if not null, what to tell the user on failure: a
      63              :    *        bad signature, or a signed list older than one accepted before
      64              :    *        (GpgIdGeneration).
      65              :    * @return false when the file cannot be read, the signature is bad or the
      66              :    *         list is a rollback (with a signing key configured). true
      67              :    *         otherwise, also for an empty list; the caller decides what an
      68              :    *         empty list means.
      69              :    */
      70              :   auto loadVerifiedRecipients(const QString &gpgIdFile, QStringList *recipients,
      71              :                               QString *why = nullptr) -> bool;
      72              :   /**
      73              :    * @brief Write the enabled recipients to a .gpg-id, atomically: a write
      74              :    * that fails halfway (full disk, dead network share) leaves the previous
      75              :    * file in place instead of a truncated list that would then be signed
      76              :    * and encrypted to.
      77              :    * @param gpgIdFile Path to .gpg-id file.
      78              :    * @param users List of recipients.
      79              :    * @param written Receives the exact bytes written, if not null: what the
      80              :    *        signature is then made over.
      81              :    * @return true when the file was written; false after reporting through
      82              :    *         critical().
      83              :    */
      84              :   auto writeGpgIdFile(const QString &gpgIdFile, const QList<UserInfo> &users,
      85              :                       QByteArray *written = nullptr) -> bool;
      86              :   /**
      87              :    * @brief Sign a `.gpg-id` with the configured key and verify the result;
      88              :    * failures are reported through critical().
      89              :    * @param gpgIdFile Path to .gpg-id file.
      90              :    * @param contents The bytes just written as that file: the signature is
      91              :    *        made over these, not over whatever is under the name by then.
      92              :    * @return true on success, false on failure.
      93              :    */
      94              :   auto signGpgIdFile(const QString &gpgIdFile, const QByteArray &contents)
      95              :       -> bool;
      96              :   /**
      97              :    * @brief Refuse a folder, or a `.gpg-id`/`.gpg-id.sig` in it, that is a
      98              :    * link: writing there would reach outside the store.
      99              :    * @param path The folder, with its trailing separator.
     100              :    * @return Whether it was refused (and the user told).
     101              :    */
     102              :   auto refuseLinkedGpgIdFolder(const QString &path) -> bool;
     103              :   /**
     104              :    * @brief The recipients to encrypt @p file to: its folder's `.gpg-id`,
     105              :    * verified when a signing key is set.
     106              :    * @param file The entry, absolute.
     107              :    * @param recipients Receives the key IDs.
     108              :    * @return false when there are none to use (the user was told why).
     109              :    */
     110              :   auto recipientsForEntry(const QString &file, QStringList *recipients) -> bool;
     111              :   /**
     112              :    * @brief The file a copy of @p src to @p dest writes: like `pass cp`, into
     113              :    * an existing folder with `.gpg` appended. Refused (the user told) when it
     114              :    * is a link, the source itself, or taken and @p force is off.
     115              :    * @param src The entry to copy.
     116              :    * @param dest Where to, a file or a folder.
     117              :    * @param force Whether an existing entry may be replaced.
     118              :    * @return The file, or an empty string when refused.
     119              :    */
     120              :   auto copyDestination(const QString &src, const QString &dest, bool force)
     121              :       -> QString;
     122              :   /**
     123              :    * @brief Put the generation header on a list that is about to be signed:
     124              :    * reserve a generation above what this device accepted and what the
     125              :    * verified list on disk says, and bind the list to its folder, so no older
     126              :    * or relocated signed list comes back.
     127              :    * @param gpgIdFile The `.gpg-id` being written.
     128              :    * @param signer Its signer, which verifies the list on disk.
     129              :    * @param contents The key IDs, one per line; receives the header too.
     130              :    * @return false when no generation could be reserved (the user was told);
     131              :    * an unrecordable list must not be written, it would let the old one back.
     132              :    */
     133              :   auto addGenerationHeader(const QString &gpgIdFile, const GpgIdSigner &signer,
     134              :                            QByteArray *contents) -> bool;
     135              :   /**
     136              :    * @brief Bring the signature in line with the list just written: a new one
     137              :    * when signing is on, the old one removed when it is off (pass and other
     138              :    * clients would reject the new list under it).
     139              :    * @param gpgIdFile The `.gpg-id` that was written.
     140              :    * @param written Its bytes, which are what gets signed.
     141              :    * @param signer Whether and with which keys to sign.
     142              :    * @param useGit Whether the store is a Git repository QtPass manages.
     143              :    * @param sigToCommit Receives the signature file to commit, if any.
     144              :    * @return false when signing or removing failed (the user was told).
     145              :    */
     146              :   auto settleSignature(const QString &gpgIdFile, const QByteArray &written,
     147              :                        const GpgIdSigner &signer, bool useGit,
     148              :                        QString *sigToCommit) -> bool;
     149              :   /**
     150              :    * @brief Stage a `.gpg-id` and, when given, its `.sig`, and commit both in
     151              :    * one commit, so no commit in the history has a recipient list without
     152              :    * the signature that covers it. Nothing is committed when neither file
     153              :    * changed.
     154              :    * @param gpgIdFile Absolute path of the `.gpg-id`.
     155              :    * @param gpgIdSigFile Absolute path of the `.gpg-id.sig`, or empty when no
     156              :    *        signing key is configured.
     157              :    * @param out Receives the concatenated stdout of the git commands.
     158              :    * @param err Receives the concatenated stderr of the git commands.
     159              :    * @return Exit code of the first failing git command, 0 on success.
     160              :    */
     161              :   auto gitAddGpgId(const QString &gpgIdFile, const QString &gpgIdSigFile,
     162              :                    QString *out, QString *err) -> int;
     163              :   /**
     164              :    * @brief Check whether git already tracks a file in the store.
     165              :    * @param file Absolute path inside the password store.
     166              :    * @return true when the file is in the git index.
     167              :    */
     168              :   auto gitTracks(const QString &file) -> bool;
     169              :   /**
     170              :    * @brief The start of a `git commit` for the store: "commit", and "-S"
     171              :    * when git config pass.signcommits is true there, as pass signs its own.
     172              :    * @return The arguments, to which the message and paths are appended.
     173              :    */
     174              :   auto commitArgs() -> QStringList;
     175              :   /**
     176              :    * @brief Recipients for the directory of @p file, from its verified
     177              :    * `.gpg-id`.
     178              :    * @param file Password file path.
     179              :    * @param verified Cache of `.gpg-id` path to the recipients its verified
     180              :    *        contents held; a hit skips gpg and reuses exactly that list.
     181              :    * @param gpgId Output parameter for recipient key IDs, sorted.
     182              :    * @return true on success, false when the file is unreadable or its
     183              :    *         signature is bad (reported through critical()).
     184              :    */
     185              :   auto verifyGpgIdForDir(const QString &file,
     186              :                          QHash<QString, QStringList> &verified,
     187              :                          QStringList &gpgId) -> bool;
     188              :   /**
     189              :    * @brief Create git backup commit before re-encryption.
     190              :    * @return true if backup created or not needed, false if backup failed.
     191              :    */
     192              :   auto createBackupCommit() -> bool;
     193              :   /**
     194              :    * @brief Read recipients from file.
     195              :    * @param fileName Path to file.
     196              :    * @return List of key IDs.
     197              :    */
     198              :   auto getKeysFromFile(const QString &fileName) -> QStringList;
     199              :   /**
     200              :    * @brief Re-encrypt single file with new recipients.
     201              :    * @param fileName File to re-encrypt.
     202              :    * @param recipients New recipient key IDs.
     203              :    * @param why Receives, for a failure the user can act on (the new
     204              :    * ciphertext could not be put under the entry's name), the reason for the
     205              :    * run's summary; left empty for a gpg failure, which is logged.
     206              :    * @return true on success, false on failure.
     207              :    */
     208              :   auto reencryptSingleFile(const QString &fileName,
     209              :                            const QStringList &recipients, QString *why) -> bool;
     210              :   /**
     211              :    * @brief Decrypt @p fileName for re-encryption.
     212              :    * @param fileName The entry to read.
     213              :    * @param plaintext Receives what gpg printed, with a trailing newline.
     214              :    * @return Whether gpg decrypted it to something.
     215              :    */
     216              :   auto decryptEntry(const QString &fileName, QString *plaintext) -> bool;
     217              :   /**
     218              :    * @brief Encrypt @p plaintext to @p recipients, into @p output.
     219              :    * @param output Where gpg writes the ciphertext; outside the store.
     220              :    * @param recipients The keys to encrypt to; gpg.conf adds none.
     221              :    * @param plaintext What to encrypt, fed on stdin.
     222              :    * @return Whether gpg wrote the ciphertext.
     223              :    */
     224              :   auto encryptFor(const QString &output, const QStringList &recipients,
     225              :                   const QString &plaintext) -> bool;
     226              :   /**
     227              :    * @brief Decrypt @p ciphertext again and compare it with @p plaintext, so
     228              :    * nothing replaces an entry until it is known to hold what it held.
     229              :    * @param ciphertext The file gpg just wrote.
     230              :    * @param plaintext What it must decrypt to.
     231              :    * @return Whether it does.
     232              :    */
     233              :   auto ciphertextHolds(const QString &ciphertext, const QString &plaintext)
     234              :       -> bool;
     235              :   /**
     236              :    * @brief Stage and commit a re-encrypted entry, when the store is a Git
     237              :    * repository QtPass manages.
     238              :    * @param fileName The entry that was replaced.
     239              :    * @return Whether the repository is up to date with the file; false makes
     240              :    * the run count the file as failed, so a partial store is not pushed.
     241              :    */
     242              :   auto commitReencrypted(const QString &fileName) -> bool;
     243              :   /**
     244              :    * @brief Resolve destination for move operation.
     245              :    * @param src Source path.
     246              :    * @param dest Destination path.
     247              :    * @param force Overwrite existing.
     248              :    * @return Resolved destination path.
     249              :    */
     250              :   auto resolveMoveDestination(const QString &src, const QString &dest,
     251              :                               bool force) -> QString;
     252              : 
     253              : public:
     254              :   /**
     255              :    * @brief Execute git move operation.
     256              :    * @param src Source path.
     257              :    * @param destFile Destination path.
     258              :    * @param force Overwrite existing.
     259              :    */
     260              :   void executeMoveGit(const QString &src, const QString &destFile, bool force);
     261              : 
     262              :   /**
     263              :    * @brief Commit changes to git.
     264              :    * @param file Changed file path.
     265              :    * @param msg Commit message.
     266              :    */
     267              :   void gitCommit(const QString &file, const QString &msg);
     268              : 
     269              :   /**
     270              :    * @brief Execute git command.
     271              :    * @param id Process identifier.
     272              :    * @param args Git arguments.
     273              :    * @param input Input to stdin.
     274              :    * @param readStdout Capture stdout.
     275              :    * @param readStderr Capture stderr.
     276              :    */
     277              :   void executeGit(PROCESS id, const QStringList &args,
     278              :                   QString input = QString(), bool readStdout = true,
     279              :                   bool readStderr = true);
     280              :   /**
     281              :    * @brief Git is enabled and an executable is configured.
     282              :    * @return true when git commands can actually run.
     283              :    */
     284              :   auto gitConfigured() const -> bool;
     285              :   /**
     286              :    * @brief gitConfigured() plus a status message when git is enabled but
     287              :    * no executable is configured.
     288              :    * @return true when git commands can actually run.
     289              :    */
     290              :   auto gitReady() -> bool;
     291              :   /**
     292              :    * @brief Execute GPG command.
     293              :    * @param id Process identifier.
     294              :    * @param args GPG arguments.
     295              :    * @param input Input to stdin.
     296              :    * @param readStdout Capture stdout.
     297              :    * @param readStderr Capture stderr.
     298              :    */
     299              :   void executeGpg(PROCESS id, const QStringList &args,
     300              :                   QString input = QString(), bool readStdout = true,
     301              :                   bool readStderr = true);
     302              : 
     303              :   /**
     304              :    * @class TransactionHelper
     305              :    * @brief RAII helper for wrapping operations in transactions.
     306              :    */
     307              :   class TransactionHelper {
     308              :     /** @brief The transaction being bracketed; not owned. */
     309              :     SimpleTransaction *m_transaction;
     310              :     /** @brief What the transaction reports as once its last step ends. */
     311              :     PROCESS m_result;
     312              : 
     313              :   public:
     314              :     /**
     315              :      * @brief Start transaction.
     316              :      * @param trans Transaction object.
     317              :      * @param result Result code on commit.
     318              :      */
     319              :     TransactionHelper(SimpleTransaction *trans, PROCESS result)
     320              :         : m_transaction(trans), m_result(result) {
     321          101 :       m_transaction->transactionStart();
     322           64 :     }
     323              :     /**
     324              :      * @brief End transaction on destruction.
     325              :      */
     326          101 :     ~TransactionHelper() { m_transaction->transactionEnd(m_result); }
     327              :   };
     328              : 
     329              : protected:
     330              :   /// storeEnvironment() as it was when the worker started; the worker's
     331              :   /// processes run in it.
     332              :   QProcessEnvironment m_reencryptEnv;
     333              :   /**
     334              :    * @brief Executor::executeBlocking() for the re-encryption helpers.
     335              :    *
     336              :    * Runs in the store's environment (storeEnvironment(); on the worker, the
     337              :    * copy taken when it started), which carries the git ceiling QtPass's own
     338              :    * does not. On the owning thread this is a plain blocking run. On the worker
     339              :    * thread it hands m_reencryptCancel to the cancellable executeBlocking()
     340              :    * overload: nothing is started once the flag is set, and a process that is
     341              :    * running when it gets set is terminated (then killed) by the worker thread,
     342              :    * which is what lets cancelReencryptPath() and the destructor interrupt an
     343              :    * active gpg or git without touching its QProcess or pid.
     344              :    * @param app The program.
     345              :    * @param args Its arguments.
     346              :    * @param input What to write to its stdin.
     347              :    * @param process_out Receives stdout, if not null.
     348              :    * @param process_err Receives stderr, if not null.
     349              :    * @return Exit code, or -1 when refused, failed to start or interrupted.
     350              :    */
     351              :   [[nodiscard]] auto execBlocking(const QString &app, const QStringList &args,
     352              :                                   const QString &input = QString(),
     353              :                                   QString *process_out = nullptr,
     354              :                                   QString *process_err = nullptr) -> int;
     355              :   /**
     356              :    * @brief Overload capturing stdout (and stderr) without stdin input.
     357              :    * @param app The program.
     358              :    * @param args Its arguments.
     359              :    * @param process_out Receives stdout.
     360              :    * @param process_err Receives stderr, if not null.
     361              :    * @return As the overload above.
     362              :    */
     363              :   [[nodiscard]] auto execBlocking(const QString &app, const QStringList &args,
     364              :                                   QString *process_out,
     365              :                                   QString *process_err = nullptr) -> int;
     366              :   /**
     367              :    * @brief Run git with `-C <store>` in the store, blocking.
     368              :    * @param args The git arguments (without the executable or `-C`).
     369              :    * @param out Receives stdout; may be null.
     370              :    * @return true when git ran and exited 0 (git on and configured).
     371              :    */
     372              :   auto storeGit(const QStringList &args, QString *out) -> bool override;
     373              :   /**
     374              :    * @brief Handle process completion.
     375              :    * @param id Process identifier.
     376              :    * @param exitCode Process exit code.
     377              :    * @param out Standard output from the process.
     378              :    * @param err Standard error from the process.
     379              :    */
     380              :   void finished(int id, int exitCode, const QString &out,
     381              :                 const QString &err) override;
     382              : 
     383              :   /**
     384              :    * @brief Whether the failed insert had placed its entry (see
     385              :    * placeEncryptedFile()) before a later git step failed.
     386              :    * @param err Not needed: the placement is known, not inferred.
     387              :    * @return true when the entry is in the store.
     388              :    */
     389            0 :   [[nodiscard]] auto insertWritten(const QString &err) const -> bool override {
     390              :     Q_UNUSED(err);
     391            0 :     return m_insertPlaced;
     392              :   }
     393              : 
     394              :   /**
     395              :    * @brief Open a transaction for every execution.
     396              :    *
     397              :    * In native (imitate) mode each wrapped command is a git/gpg transaction;
     398              :    * register it before the subprocess runs.
     399              :    * @param id Process identifier.
     400              :    */
     401              :   void beforeExecute(PROCESS id) override;
     402              : 
     403              : public:
     404              :   /**
     405              :    * @brief Construct the backend and connect its content search.
     406              :    */
     407              :   ImitatePass();
     408              :   /**
     409              :    * @brief Cancel a running content search, and cancel and join a running
     410              :    * re-encryption worker, before the members they use go away.
     411              :    */
     412              :   ~ImitatePass() override;
     413              : 
     414              :   // Git operations
     415              :   /**
     416              :    * @brief Initialize Git repository.
     417              :    */
     418              :   void GitInit() override;
     419              :   /**
     420              :    * @brief Pull from remote.
     421              :    */
     422              :   void GitPull() override;
     423              :   /**
     424              :    * @brief Pull with rebase.
     425              :    */
     426              :   void GitPull_b() override;
     427              :   /**
     428              :    * @brief Push to remote.
     429              :    */
     430              :   void GitPush() override;
     431              : 
     432              :   // Password operations
     433              :   /**
     434              :    * @brief Show decrypted password.
     435              :    * @param file Path to the password file relative to store root.
     436              :    */
     437              :   void Show(QString file) override;
     438              :   /**
     439              :    * @brief Insert new password.
     440              :    * @param file Path to the password file.
     441              :    * @param newValue Password content to store.
     442              :    * @param overwrite true to overwrite an existing file.
     443              :    */
     444              :   void Insert(QString file, QString newValue, bool overwrite) override;
     445              :   /**
     446              :    * @brief Remove password.
     447              :    * @param file Path to the file or directory to remove.
     448              :    * @param isDir true if removing a directory.
     449              :    */
     450              :   void Remove(QString file, bool isDir) override;
     451              :   /**
     452              :    * @brief Initialize store.
     453              :    *
     454              :    * Writes (and signs) the folder's `.gpg-id`, commits it when Git is used
     455              :    * and re-encrypts the folder to it. When that commit fails the entries are
     456              :    * still re-encrypted, without any Git step, and a critical() afterwards
     457              :    * says the change is not committed and whether the re-encryption
     458              :    * completed.
     459              :    * @param path Root path of the password store.
     460              :    * @param users List of recipient GPG keys.
     461              :    */
     462              :   void Init(QString path, const QList<UserInfo> &users) override;
     463              : 
     464              :   /**
     465              :    * @brief Re-encrypt entire directory.
     466              :    *
     467              :    * Emits startReencryptPath() synchronously, then runs the scan and the
     468              :    * per-file gpg calls on a worker thread. Progress is reported through
     469              :    * reencryptProgress(); per-file failures are collected and reported in a
     470              :    * single critical() once the run is over, followed by endReencryptPath().
     471              :    * A second call while a run is active is ignored.
     472              :    * @param dir Directory path.
     473              :    */
     474              :   void reencryptPath(const QString &dir);
     475              :   /**
     476              :    * @brief Re-encrypt @p dir under @p settings, the current profile's.
     477              :    *
     478              :    * This backend re-encrypts also when pass is the active one, and then is
     479              :    * not re-initialised on a profile switch: without this it would decrypt
     480              :    * the current profile's entries and encrypt them to the recipients and
     481              :    * signing policy of the profile it last ran for. The settings are taken
     482              :    * only when no run is active, so a running one keeps its own.
     483              :    * @param dir Directory path.
     484              :    * @param settings The current settings.
     485              :    */
     486              :   void reencryptPath(const QString &dir, const AppSettings &settings);
     487              :   /**
     488              :    * @brief Ask a running reencryptPath() to stop.
     489              :    *
     490              :    * Only sets a flag. The worker starts no further gpg or git process, and
     491              :    * the one it is blocked on (gpg waiting on pinentry, say) is terminated by
     492              :    * the worker itself, and killed if it is still running after a short grace
     493              :    * period, so the cancel takes effect promptly rather than after the current
     494              :    * file. The store stays
     495              :    * consistent: reencryptSingleFile() only replaces a file once the new
     496              :    * ciphertext is verified, so an interrupted file is left untouched, or
     497              :    * already re-encrypted but not yet committed when git was the process that
     498              :    * was interrupted. The remaining files are left untouched and the
     499              :    * interrupted file is not reported as a failure. endReencryptPath() is
     500              :    * still emitted when the worker has stopped.
     501              :    */
     502              :   void cancelReencryptPath();
     503              : 
     504              : signals:
     505              :   /**
     506              :    * @brief A reencryptPath() run is about to start its worker thread.
     507              :    *
     508              :    * The main window disables the interface and shows a progress dialog
     509              :    * with a Cancel button on it. Always followed by endReencryptPath().
     510              :    */
     511              :   void startReencryptPath();
     512              :   /**
     513              :    * @brief A reencryptPath() run is over, whether it finished, failed, was
     514              :    * cancelled or was refused; the main window releases the interface.
     515              :    *
     516              :    * Also emitted without a preceding startReencryptPath() when the folder
     517              :    * is refused before anything starts.
     518              :    */
     519              :   void endReencryptPath();
     520              :   /**
     521              :    * @brief Re-encryption progress: @p current of @p total files checked.
     522              :    *
     523              :    * Emitted from the worker thread; connect with a queued (or automatic)
     524              :    * connection.
     525              :    * @param current Number of files checked so far, including this one.
     526              :    * @param total Total number of files found by the scan.
     527              :    */
     528              :   void reencryptProgress(int current, int total);
     529              : 
     530              :   // Pass interface
     531              : public:
     532              :   /**
     533              :    * @brief Move password file.
     534              :    * @param src Source path.
     535              :    * @param dest Destination path.
     536              :    * @param force true to overwrite existing destination.
     537              :    */
     538              :   void Move(const QString src, const QString dest, const bool force) override;
     539              :   /**
     540              :    * @brief Copy password file.
     541              :    * @param src Source path.
     542              :    * @param dest Destination path.
     543              :    * @param force true to overwrite existing destination.
     544              :    */
     545              :   void Copy(const QString src, const QString dest, const bool force) override;
     546              :   /**
     547              :    * @brief Search all password content by GPG-decrypting each .gpg file.
     548              :    *
     549              :    * Pattern is interpreted as a QRegularExpression (PCRE-like); RealPass::Grep
     550              :    * uses the same search, so both backends match alike.
     551              :    *
     552              :    * @param pattern Search pattern (QRegularExpression).
     553              :    * @param caseInsensitive true for case-insensitive search.
     554              :    */
     555              :   void Grep(QString pattern, bool caseInsensitive) override;
     556              : 
     557              : private:
     558              :   /// Groups the git/gpg processes of one operation (Insert, Remove, Move,
     559              :   /// Copy) into a single completion; see finished() and TransactionHelper.
     560              :   SimpleTransaction m_transaction;
     561              :   /// Background search over the decrypted store; relays to finishedGrep.
     562              :   NativeGrep m_grep;
     563              :   /**
     564              :    * @brief stdout of the steps of the transaction in flight, collected by
     565              :    * finished() and handed on, then cleared, when its last step ends.
     566              :    */
     567              :   QString m_transactionOutput;
     568              : 
     569              :   /// An Insert() whose gpg is running or queued: the private directory gpg
     570              :   /// writes into (removed with it), the entry it is for, and whether an
     571              :   /// existing entry may be replaced. Consumed by finished() when the gpg
     572              :   /// step ends; one per Insert(), in order.
     573              :   struct PendingInsert {
     574              :     /** @brief The private directory gpg writes into; removed with this. */
     575              :     std::shared_ptr<QTemporaryDir> scratch;
     576              :     /** @brief The ciphertext file gpg writes, inside @ref scratch. */
     577              :     QString output;
     578              :     /** @brief The entry's path in the store, where @ref output goes. */
     579              :     QString file;
     580              :     /** @brief Whether an entry already at @ref file may be replaced. */
     581              :     bool overwrite = false;
     582              :   };
     583              :   /** @brief The inserts whose gpg step has not ended yet, oldest first. */
     584              :   QQueue<PendingInsert> m_pendingInserts;
     585              :   /// The insert whose gpg step ended last placed its entry in the store; its
     586              :   /// git steps may still fail. Set per gpg completion in finished(), not per
     587              :   /// Insert() call, so a second queued insert's failure is not reported with
     588              :   /// an earlier insert's answer.
     589              :   bool m_insertPlaced = false;
     590              : 
     591              :   /**
     592              :    * @brief Bring the ciphertext gpg wrote to @p output into the store as
     593              :    * @p file (Util::copyFileReplacing): through a temporary created next to
     594              :    * the entry and written by its open handle, then the operating system's
     595              :    * rename, which replaces the entry under that name (or, without
     596              :    * @p overwrite, fails when one has appeared) and follows nothing. Reports
     597              :    * nothing itself: Insert()'s finished() routes the reason through the
     598              :    * failed-operation path once the queued git steps are cancelled, and
     599              :    * re-encryption lists it in the run's summary.
     600              :    * @param output The file gpg wrote, in a directory of QtPass's own.
     601              :    * @param file The entry's path.
     602              :    * @param overwrite Whether an entry already there may be replaced.
     603              :    * @param error Receives why @p file could not be written, if not.
     604              :    * @return Whether @p file now holds the new ciphertext.
     605              :    */
     606              :   auto placeEncryptedFile(const QString &output, const QString &file,
     607              :                           bool overwrite, QString *error) -> bool;
     608              : 
     609              :   /**
     610              :    * @brief Outcome of one reencryptPath() run; filled on the worker thread and
     611              :    * consumed by finishReencrypt() on the owning thread.
     612              :    */
     613              :   struct ReencryptResult;
     614              :   /// Set while a re-encryption run is pending or on the worker thread.
     615              :   bool m_reencryptActive = false;
     616              :   /// Set by cancelReencryptPath() and the destructor; read by the worker
     617              :   /// between files and polled by Executor::executeBlocking() while the worker
     618              :   /// waits on a process, which the worker then ends itself. The flag is the
     619              :   /// only thing shared with the worker: its QProcess is not thread-safe and
     620              :   /// is never touched from another thread.
     621              :   std::atomic<bool> m_reencryptCancel{false};
     622              :   /**
     623              :    * @brief The re-encryption worker while it runs, null otherwise. The
     624              :    * destructor sets m_reencryptCancel and joins it.
     625              :    */
     626              :   QThread *m_reencryptThread = nullptr;
     627              :   /**
     628              :    * @brief Start the worker thread once the Executor queue is idle.
     629              :    * @param dir Directory passed to reencryptPath().
     630              :    */
     631              :   void startReencryptWorker(const QString &dir);
     632              :   /**
     633              :    * @brief Worker-thread body: scan @p dir and re-encrypt stale files.
     634              :    * @param dir Directory passed to reencryptPath().
     635              :    * @return Counts and the list of files that could not be re-encrypted.
     636              :    */
     637              :   auto reencryptFiles(const QString &dir) -> ReencryptResult;
     638              :   /**
     639              :    * @brief Pull before a run, when the store is a Git repository and
     640              :    * automatic pulling is on.
     641              :    * @return Whether re-encrypting may go ahead: false when a failed pull
     642              :    * left unmerged files or the check for unmerged files fails.
     643              :    */
     644              :   auto pullBeforeReencrypt() -> bool;
     645              :   /**
     646              :    * @brief The entries under @p dir a run considers, telling the user about
     647              :    * anything skipped for not being a regular file.
     648              :    * @param dir Directory to walk, recursively.
     649              :    * @return The `.gpg` files, links and special files left out.
     650              :    */
     651              :   auto entriesToReencrypt(const QString &dir) -> QStringList;
     652              :   /**
     653              :    * @brief The verified recipients for the folder @p fileName sits in.
     654              :    * @param fileName An entry in the folder.
     655              :    * @param verified Cache of the `.gpg-id` files already checked.
     656              :    * @param gpgId Receives the recipients to encrypt that folder to.
     657              :    * @param result Marked cancelled or aborted when the answer is no.
     658              :    * @return Whether the run may go on with this folder.
     659              :    */
     660              :   auto recipientsForDir(const QString &fileName,
     661              :                         QHash<QString, QStringList> &verified,
     662              :                         QStringList &gpgId, ReencryptResult &result) -> bool;
     663              :   /**
     664              :    * @brief Deal with what a crashed or failed earlier run left under @p dir
     665              :    * before touching anything: stale temporaries (today's staged
     666              :    * `.qtpass-XXXXXX.tmp`, 1.8.x's `<entry>.reencrypt.tmp`, the
     667              :    * `<entry>.XXXXXX.tmp` of builds in between) are removed unless they were
     668              :    * modified within the hour, where they may be another QtPass's write in
     669              :    * flight on a shared store; a 1.8.x `<entry>.reencrypt.bak` whose original
     670              :    * is missing is put back, one next to a present original is reported and
     671              :    * left alone (both are valid ciphertexts, the choice is the user's).
     672              :    * @param dir Directory to scan, recursively.
     673              :    * @return false when something was reported that needs a human first.
     674              :    */
     675              :   auto recoverReencryptLeftovers(const QString &dir) -> bool;
     676              :   /**
     677              :    * @brief Remove a stale temporary a crashed run left, unless it was
     678              :    * modified within the hour: on a shared store it may be another QtPass's
     679              :    * write in flight under the same name.
     680              :    * @param path The temporary, a regular file.
     681              :    */
     682              :   void dropStaleTemporary(const QString &path);
     683              :   /**
     684              :    * @brief Put a 1.8.x backup back in its entry's place when the entry is
     685              :    * missing; report it when both exist (both are valid ciphertexts, the
     686              :    * choice is the user's) or when the rename fails.
     687              :    * @param path The `<entry>.reencrypt.bak` file.
     688              :    * @return Whether nothing needs a human first.
     689              :    */
     690              :   auto restoreBackup(const QString &path) -> bool;
     691              :   /**
     692              :    * @brief Tell the user a leftover is not a regular file and was left.
     693              :    * @param path The leftover.
     694              :    */
     695              :   void reportUnrestorable(const QString &path);
     696              :   /**
     697              :    * @brief Report the outcome of a run and emit endReencryptPath().
     698              :    * @param result Outcome produced by reencryptFiles().
     699              :    */
     700              :   void finishReencrypt(const ReencryptResult &result);
     701              : 
     702              :   /**
     703              :    * @brief A `.gpg-id` Init() wrote but could not commit.
     704              :    */
     705            6 :   struct UncommittedGpgId {
     706              :     QString folder;   ///< The folder whose recipients changed.
     707              :     QString gitError; ///< What Git said when the commit failed.
     708              :   };
     709              :   /// Set by reencryptWithoutGit() for the run it starts and cleared by
     710              :   /// finishReencrypt(): that run does no Git at all (no pull, backup
     711              :   /// commit, per-entry commit or push) and ends with
     712              :   /// reportUncommittedGpgId(). Written only on the owning thread while no
     713              :   /// worker runs; the worker only reads it, through reencryptUsesGit().
     714              :   std::optional<UncommittedGpgId> m_uncommittedGpgId;
     715              :   /**
     716              :    * @brief gitConfigured(), unless the current run is one
     717              :    * reencryptWithoutGit() started.
     718              :    * @return Whether the re-encryption run may call Git.
     719              :    */
     720              :   auto reencryptUsesGit() const -> bool;
     721              :   /**
     722              :    * @brief Re-encrypt @p dir to the `.gpg-id` Init() wrote although its
     723              :    * commit failed, without any Git step (they would fail the same way), and
     724              :    * tell the user afterwards that the change is not committed.
     725              :    * @param dir The folder whose recipients changed.
     726              :    * @param gitError What Git said when the commit failed.
     727              :    */
     728              :   void reencryptWithoutGit(const QString &dir, const QString &gitError);
     729              :   /**
     730              :    * @brief Tell the user that the recipients of @p dir changed on disk but
     731              :    * the change is not committed, and whether the entries were re-encrypted.
     732              :    * @param dir The folder whose recipients changed.
     733              :    * @param gitError What Git said when the commit failed.
     734              :    * @param reencrypted Whether every entry was re-encrypted to the new
     735              :    *        recipients; false means some or all are still encrypted to the
     736              :    *        previous ones.
     737              :    */
     738              :   void reportUncommittedGpgId(const QString &dir, const QString &gitError,
     739              :                               bool reencrypted);
     740              : 
     741              :   /**
     742              :    * @brief Translate @p path for the given @p exe when WSL-routed: wraps in
     743              :    * wslpath substitution, otherwise returns @p path unchanged.
     744              :    * @param path Native filesystem path.
     745              :    * @param exe Executable path (checked for "wsl " prefix).
     746              :    * @return Path suitable for the given executable.
     747              :    */
     748              :   auto translatePathForWsl(const QString &path, const QString &exe) const
     749              :       -> QString;
     750              :   /**
     751              :    * @brief Translate @p path for git when WSL-routed: wraps in wslpath
     752              :    * substitution, otherwise returns @p path unchanged.
     753              :    * @param path Native filesystem path.
     754              :    * @return Path suitable for the configured git executable.
     755              :    */
     756              :   auto pgit(const QString &path) const -> QString;
     757              :   /**
     758              :    * @brief Translate @p path for gpg when WSL-routed: wraps in wslpath
     759              :    * substitution, otherwise returns @p path unchanged.
     760              :    * @param path Native filesystem path.
     761              :    * @return Path suitable for the configured gpg executable.
     762              :    */
     763              :   auto pgpg(const QString &path) const -> QString;
     764              : };
     765              : 
     766              : #endif // SRC_IMITATEPASS_H_
        

Generated by: LCOV version 2.0-1