unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
@ 2019-06-02  9:11 Damien Cassou
  2019-06-07  0:43 ` Noam Postavsky
  0 siblings, 1 reply; 13+ messages in thread
From: Damien Cassou @ 2019-06-02  9:11 UTC (permalink / raw)
  To: 36052
  Cc: Magnus Henoch, Nicolas Petton, Ted Zlatanov, Iku Iwasa, galaunay,
	Keith Amidon

[-- Attachment #1: Type: text/plain, Size: 1678 bytes --]

Hi,

auth-source-pass users have worked hard to improve the package. Everyone
is on CC.

3 have signed the FSF paperwork:

- Keith Amidon:
  https://github.com/DamienCassou/auth-password-store/pull/80#issuecomment-480853577
- Iku Iwasa:
  https://github.com/DamienCassou/auth-password-store/pull/87#issuecomment-480816595
- galaunay:
  https://github.com/DamienCassou/auth-password-store/pull/78#issuecomment-453862089

Magnus Henoch hasn't signed but his contribution is rather small (4
lines of a new unit-test and 2 lines of code). See patch 0001.

Here is a summary of the attached patches.

Damien Cassou (5):
  * lisp/auth-source-pass.el: Version 4.0.2
  * lisp/auth-source-pass.el (auth-source-pass-get): Add autoload
  Refactoring of auth-source-pass
  * lisp/auth-source-pass.el: Version 5.0.0
  * etc/NEWS: Describe changes to auth-source-pass

Iku Iwasa (1):
  Add auth-source-pass-port-separator option

Keith Amidon (4):
  Fix auth-source-pass to search for hostname:port/username
  Split out the attribute retrieval form auth-source-pass-get
  Minimize entry parsing in auth-source-pass
  * lisp/auth-source-pass.el: Add Keith Amidon to authors

Magnus Henoch (1):
  Fix auth-source-pass to return nil if no entry found

galaunay (1):
  Add auth-source-pass-path option

 etc/NEWS                            |  19 ++
 lisp/auth-source-pass.el            | 224 ++++++++-----
 test/lisp/auth-source-pass-tests.el | 473 +++++++++++++++++++---------
 3 files changed, 491 insertions(+), 225 deletions(-)

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Fix-auth-source-pass-to-return-nil-if-no-entry-found.patch --]
[-- Type: text/x-patch, Size: 1952 bytes --]

From 34961a0cb5220aefa3a432463d18c15a87bc61ba Mon Sep 17 00:00:00 2001
From: Magnus Henoch <magnus.henoch@gmail.com>
Date: Fri, 2 Nov 2018 21:51:59 +0000
Subject: [PATCH 01/12] Fix auth-source-pass to return nil if no entry found

* lisp/auth-source-pass.el (auth-source-pass-search): If there is no
matching entry, auth-source-pass-search should return nil, not (nil).
This lets auth-source fall back to other backends in the auth-sources
list.
* test/lisp/auth-source-pass-tests.el: Add corresponding test.
---
 lisp/auth-source-pass.el            | 3 ++-
 test/lisp/auth-source-pass-tests.el | 5 +++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4283ed0392..c82c90167a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -56,7 +56,8 @@
          ;; Do not build a result, as none will match when HOST is nil
          nil)
         (t
-         (list (auth-source-pass--build-result host port user)))))
+         (when-let ((result (auth-source-pass--build-result host port user)))
+           (list result)))))
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index d1e486ad6b..ab9ef92c14 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -83,6 +83,11 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host nil))))
 
+(ert-deftest auth-source-pass-not-found ()
+  (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
+                                  ("bar"))
+    (should-not (auth-source-pass-search :host "baz"))))
+
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
   (auth-source-pass--with-store '(("foo"))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-lisp-auth-source-pass.el-Version-4.0.2.patch --]
[-- Type: text/x-patch, Size: 745 bytes --]

From a4b63f10e436614e2b2ac5449307fc8371853600 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sat, 3 Nov 2018 09:06:42 +0100
Subject: [PATCH 02/12] * lisp/auth-source-pass.el: Version 4.0.2

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index c82c90167a..d092292e51 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,7 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
-;; Version: 4.0.1
+;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-lisp-auth-source-pass.el-auth-source-pass-get-Add-au.patch --]
[-- Type: text/x-patch, Size: 758 bytes --]

From e65766da015376fc9cfef75ece0a8ae614e11e27 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 6 Nov 2018 14:45:20 +0100
Subject: [PATCH 03/12] * lisp/auth-source-pass.el (auth-source-pass-get): Add
 autoload

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index d092292e51..4fcb1015e7 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -98,6 +98,7 @@ auth-source-pass-backend-parse
   (advice-add 'auth-source-backend-parse :before-until #'auth-source-pass-backend-parse))
 
 \f
+;;;###autoload
 (defun auth-source-pass-get (key entry)
   "Return the value associated to KEY in the password-store entry ENTRY.
 
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Add-auth-source-pass-path-option.patch --]
[-- Type: text/x-patch, Size: 2054 bytes --]

From d46e8dd1bbc3a1c7ac6506e3e6cfe3d87e57e99e Mon Sep 17 00:00:00 2001
From: galaunay <gaby.launay@tutanota.com>
Date: Sun, 13 Jan 2019 21:30:53 +0000
Subject: [PATCH 04/12] Add auth-source-pass-path option

* lisp/auth-source-pass.el (auth-source-pass):
(auth-source-pass-path): Add option to specify a customized
password-store path.
(auth-source-pass--read-entry):
(auth-source-pass-entries): Use the new option instead of hard-coded
`~/.password-store'.
---
 lisp/auth-source-pass.el | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4fcb1015e7..a029946f6c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -38,6 +38,15 @@
 (require 'auth-source)
 (require 'url-parse)
 
+(defgroup auth-source-pass nil
+  "password-store integration within auth-source."
+  :prefix "auth-source-pass-"
+  :group 'auth-source)
+
+(defcustom auth-source-pass-path "~/.password-store"
+  "Path to the password-store folder."
+  :type 'directory)
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -121,7 +130,7 @@ auth-source-pass--read-entry
   (with-temp-buffer
     (insert-file-contents (expand-file-name
                            (format "%s.gpg" entry)
-                           "~/.password-store"))
+                           auth-source-pass-path))
     (buffer-substring-no-properties (point-min) (point-max))))
 
 (defun auth-source-pass-parse-entry (entry)
@@ -188,7 +197,7 @@ auth-source-pass--entry-valid-p
 ;; in Emacs
 (defun auth-source-pass-entries ()
   "Return a list of all password store entries."
-  (let ((store-dir (expand-file-name "~/.password-store/")))
+  (let ((store-dir (expand-file-name auth-source-pass-path)))
     (mapcar
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Add-auth-source-pass-port-separator-option.patch --]
[-- Type: text/x-patch, Size: 3393 bytes --]

From f2107a0c03b2aa0421d5ee4e7ccc74c7cfd4371c Mon Sep 17 00:00:00 2001
From: Iku Iwasa <iku.iwasa@gmail.com>
Date: Sun, 7 Apr 2019 17:59:59 +0900
Subject: [PATCH 05/12] Add auth-source-pass-port-separator option

* lisp/auth-source-pass.el (auth-source-pass-port-separator): New
option to specify separator between host and port, default to
colon (":").
(auth-source-pass--find-match-unambiguous): Adapt to make use of the
new variable.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            | 16 +++++++++++++---
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index a029946f6c..e80149900b 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -47,6 +47,10 @@ auth-source-pass-path
   "Path to the password-store folder."
   :type 'directory)
 
+(defcustom auth-source-pass-port-separator ":"
+  "Separator string between host and port in entry filename."
+  :type 'string)
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -252,9 +256,15 @@ auth-source-pass--find-match-unambiguous
 
 HOSTNAME should not contain any username or port number."
   (or
-   (and user port (auth-source-pass--find-one-by-entry-name (format "%s@%s:%s" user hostname port) user))
-   (and user (auth-source-pass--find-one-by-entry-name (format "%s@%s" user hostname) user))
-   (and port (auth-source-pass--find-one-by-entry-name (format "%s:%s" hostname port) nil))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
+                   user))
+   (and user (auth-source-pass--find-one-by-entry-name
+              (format "%s@%s" user hostname)
+              user))
+   (and port (auth-source-pass--find-one-by-entry-name
+              (format "%s%s%s" hostname auth-source-pass-port-separator port)
+              nil))
    (auth-source-pass--find-one-by-entry-name hostname user)
    ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
    (let ((components (split-string hostname "\\.")))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ab9ef92c14..ae7a696bc6 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -186,6 +186,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
                    "host.com"))))
 
+(ert-deftest auth-source-pass-find-host-with-port ()
+  (auth-source-pass--with-store '(("host.com:443"))
+    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                   "host.com:443"))))
+
+(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("host.com#443"))
+      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                     "host.com#443")))))
+
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
   (declare (indent 1))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Fix-auth-source-pass-to-search-for-hostname-port-use.patch --]
[-- Type: text/x-patch, Size: 2729 bytes --]

From 4012ad2d3572c5f9ecc29c22106aba137d2f244d Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Thu, 14 Feb 2019 10:27:31 -0800
Subject: [PATCH 06/12] Fix auth-source-pass to search for
 hostname:port/username

auth-source-pass supports entries with username either prefixed to the
hostname with an @ as separator or in a subdirectory under the
hostname.  This was true when there was no port or service included in
the name, but got broken with the introduction of
auth-source-pass-port-separator.

* lisp/auth-source-pass.el (auth-source-pass--find-match-unambiguous): Fix
to match hostname:port/username.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            |  3 +++
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 14 insertions(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index e80149900b..9ad371ea7c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -259,6 +259,9 @@ auth-source-pass--find-match-unambiguous
    (and user port (auth-source-pass--find-one-by-entry-name
                    (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
                    user))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
+                   user))
    (and user (auth-source-pass--find-one-by-entry-name
               (format "%s@%s" user hostname)
               user))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ae7a696bc6..1539d9611f 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -144,6 +144,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
                    nil))))
 
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+  (auth-source-pass--with-store '(("bar.com:443/someone"))
+    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                   "bar.com:443/someone"))))
+
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("bar.com#443/someone"))
+      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                     "bar.com#443/someone")))))
+
 (ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
   (auth-source-pass--with-store '(("foo.com/bar"))
     (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-Split-out-the-attribute-retrieval-form-auth-source-p.patch --]
[-- Type: text/x-patch, Size: 3194 bytes --]

From 9e77cc4bf4131ef0fda205465cdf5ffdd6ef0a87 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Tue, 30 Apr 2019 07:52:14 -0700
Subject: [PATCH 07/12] Split out the attribute retrieval form
 auth-source-pass-get

Eliminate the need to repeatedly retrieve and parse the data for the
entry. This is generally a good thing since it eliminates repetitions
of the same crypto and parsing operations. It is especially valuable
when protecting an entry with a yubikey with touch required for crypto
operations as it eliminates the need to touch the yubikey sensor for
each attribute retrieved.

* lisp/auth-source-pass.el (auth-source-pass-get): Extract some code to
`auth-source-pass--get-attr'.
(auth-source-pass--get-attr): New function to get a field value from a
parsed entry.
(auth-source-pass--build-result): Make use of
`auth-source-pass--get-attr` to avoid repeated parsing.
---
 lisp/auth-source-pass.el | 31 +++++++++++++++++++++++--------
 1 file changed, 23 insertions(+), 8 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 9ad371ea7c..2203290d8c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -76,11 +76,12 @@ auth-source-pass--build-result
   "Build auth-source-pass entry matching HOST, PORT and USER."
   (let ((entry (auth-source-pass--find-match host user port)))
     (when entry
-      (let ((retval (list
-                     :host host
-                     :port (or (auth-source-pass-get "port" entry) port)
-                     :user (or (auth-source-pass-get "user" entry) user)
-                     :secret (lambda () (auth-source-pass-get 'secret entry)))))
+      (let* ((entry-data (auth-source-pass-parse-entry entry))
+             (retval (list
+                      :host host
+                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -125,9 +126,23 @@ auth-source-pass-get
 key1: value1
 key2: value2"
   (let ((data (auth-source-pass-parse-entry entry)))
-    (or (cdr (assoc key data))
-        (and (string= key "user")
-             (cdr (assoc "username" data))))))
+    (auth-source-pass--get-attr key data)))
+
+(defun auth-source-pass--get-attr (key entry-data)
+  "Return the value associated to KEY in data from an already parsed entry.
+
+ENTRY-DATA is the data from a parsed password-store entry.
+The key used to retrieve the password is the symbol `secret'.
+
+The convention used as the format for a password-store file is
+the following (see http://www.passwordstore.org/#organization):
+
+secret
+key1: value1
+key2: value2"
+  (or (cdr (assoc key entry-data))
+      (and (string= key "user")
+           (cdr (assoc "username" entry-data)))))
 
 (defun auth-source-pass--read-entry (entry)
   "Return a string with the file content of ENTRY."
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-Minimize-entry-parsing-in-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 33646 bytes --]

From 997d3c0ef848983fb85ffa36f7a9301904171850 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sun, 5 May 2019 20:21:43 -0700
Subject: [PATCH 08/12] Minimize entry parsing in auth-source-pass

Prior to this commit, while searching for the most applicable entry
password-store entries were decrypted and parsed to ensure they were
valid. The entries were parsed in the order they were found on the
filesystem and all applicable entries would be decrypted and parsed,
which varied based on the contents of the password-store and the entry
to be found.

This is fine when the GPG key is cached and each entry can be
decrypted without user interaction. However, for security some people
have their GPG on a hardware token like a Yubikey setup so that they
have to touch a sensor on the toke for every cryptographic operation,
in which case it becomes inconvenient as each attempt to find an entry
requires a variable number of touches of the hardware token.

The implementation already assumes that names which contain more of
the information in the search key should be preferred so there is an
ordering of preference of applicable entries. If the decrypt and
parsing is removed from the initial identification of applicable
entries in the store then in most cases a single decrypt and parse of
the most preferred entry will suffice, improving the experience for
hardware token users that require interaction with the token.

This commit implements that strategy. It is in spirit a refactor of
the existing code. The core of the change is the function
auth-source-pass--applicable-entries, which generates an ordered list
of regular expression matchers for all possible names that could be in
the password-store for the entry to be found and then makes a pass
over the password-store entry names accumulating the matching entries
in a list after the regexp that matched. This implementation ensures
the password-store entry list still only has to be scanned once.

The existing auth-source-pass--find-match-unambiguous was modified to
use this new function to obtain candidate entries and then parse them
one by one until an entry containing the desired information is
located. When complete it now returns the parsed data of the entry
instead of the entry name so that the information can be used directly
to construct the auth-source response.

* lisp/auth-source-pass.el: Private functions were refactored to
reduce the number of decryption operations.
* test/lisp/auth-source-pass-tests.el: One test case was added to the
test suite to verify that only the minimal number of entries are
parsed in common cases. The
auth-source-pass-only-return-entries-that-can-be-open test case had to
be re-implemented because the function it used eliminated as the
functionality is provided elsewhere. All the other fairly substantial
changes to the test suite are the result of mechanical changes that
were required to adapt to auth-source-pass--find-match returning the
data from a parsed password-store entry instead of the entry name.
---
 lisp/auth-source-pass.el            | 210 ++++++++++++----------
 test/lisp/auth-source-pass-tests.el | 264 +++++++++++++++++-----------
 2 files changed, 282 insertions(+), 192 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 2203290d8c..bb090eae40 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -74,14 +74,13 @@ auth-source-pass-port-separator
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
-  (let ((entry (auth-source-pass--find-match host user port)))
-    (when entry
-      (let* ((entry-data (auth-source-pass-parse-entry entry))
-             (retval (list
-                      :host host
-                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
-                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
-                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
+  (let ((entry-data (auth-source-pass--find-match host user port)))
+    (when entry-data
+      (let ((retval (list
+                     :host host
+                     :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                     :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                     :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -185,33 +184,6 @@ auth-source-pass--do-debug
          (cons (concat "auth-source-pass: " (car msg))
                (cdr msg))))
 
-(defun auth-source-pass--select-one-entry (entries user)
-  "Select one entry from ENTRIES by searching for a field matching USER."
-  (let ((number (length entries))
-        (entry-with-user
-         (and user
-              (seq-find (lambda (entry)
-                          (string-equal (auth-source-pass-get "user" entry) user))
-                        entries))))
-    (auth-source-pass--do-debug "found %s matches: %s" number
-                                (mapconcat #'identity entries ", "))
-    (if entry-with-user
-        (progn
-          (auth-source-pass--do-debug "return %s as it contains matching user field"
-                                      entry-with-user)
-          entry-with-user)
-      (auth-source-pass--do-debug "return %s as it is the first one" (car entries))
-      (car entries))))
-
-(defun auth-source-pass--entry-valid-p (entry)
-  "Return t iff ENTRY can be opened.
-Also displays a warning if not.  This function is slow, don't call it too
-often."
-  (if (auth-source-pass-parse-entry entry)
-      t
-    (auth-source-pass--do-debug "entry '%s' is not valid" entry)
-    nil))
-
 ;; TODO: add tests for that when `assess-with-filesystem' is included
 ;; in Emacs
 (defun auth-source-pass-entries ()
@@ -221,37 +193,8 @@ auth-source-pass-entries
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
 
-(defun auth-source-pass--find-all-by-entry-name (entryname user)
-  "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME.
-Only return valid entries as of `auth-source-pass--entry-valid-p'."
-  (seq-filter (lambda (entry)
-                (and
-                 (or
-                  (let ((components-host-user
-                         (member entryname (split-string entry "/"))))
-                    (and (= (length components-host-user) 2)
-                         (string-equal user (cadr components-host-user))))
-                  (string-equal entryname (file-name-nondirectory entry)))
-                 (auth-source-pass--entry-valid-p entry)))
-              (auth-source-pass-entries)))
-
-(defun auth-source-pass--find-one-by-entry-name (entryname user)
-  "Search the store for an entry matching ENTRYNAME.
-If USER is non nil, give precedence to entries containing a user field
-matching USER."
-  (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
-                              entryname
-                              user)
-  (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user)))
-    (pcase (length matching-entries)
-      (0 (auth-source-pass--do-debug "no match found")
-         nil)
-      (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries))
-         (car matching-entries))
-      (_ (auth-source-pass--select-one-entry matching-entries user)))))
-
 (defun auth-source-pass--find-match (host user port)
-  "Return a password-store entry name matching HOST, USER and PORT.
+  "Return password-store entry data matching HOST, USER and PORT.
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
@@ -265,33 +208,124 @@ auth-source-pass--find-match
      (or port (number-to-string (url-port url))))))
 
 (defun auth-source-pass--find-match-unambiguous (hostname user port)
-  "Return a password-store entry name matching HOSTNAME, USER and PORT.
+  "Return password-store entry data matching HOSTNAME, USER and PORT.
 If many matches are found, return the first one.  If no match is found,
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (or
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
-                   user))
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
-                   user))
-   (and user (auth-source-pass--find-one-by-entry-name
-              (format "%s@%s" user hostname)
-              user))
-   (and port (auth-source-pass--find-one-by-entry-name
-              (format "%s%s%s" hostname auth-source-pass-port-separator port)
-              nil))
-   (auth-source-pass--find-one-by-entry-name hostname user)
-   ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
-   (let ((components (split-string hostname "\\.")))
-     (when (= (length components) 3)
-       ;; start from scratch
-       (auth-source-pass--find-match-unambiguous
-        (mapconcat 'identity (cdr components) ".")
-        user
-        port)))))
+  (cl-reduce
+   (lambda (result entries)
+     (or result
+         (pcase (length entries)
+           (0 nil)
+           (1 (auth-source-pass-parse-entry (car entries)))
+           (_ (auth-source-pass--select-from-entries entries user)))))
+   (auth-source-pass--matching-entries hostname user port)
+   :initial-value nil))
+
+(defun auth-source-pass--select-from-entries (entries user)
+  "Return best matching password-store entry data from ENTRIES.
+
+If USER is non nil, give precedence to entries containing a user field
+matching USER."
+  (cl-reduce
+   (lambda (result entry)
+     (let ((entry-data (auth-source-pass-parse-entry entry)))
+       (cond ((equal (auth-source-pass--get-attr "user" result) user)
+              result)
+             ((equal (auth-source-pass--get-attr "user" entry-data) user)
+              entry-data)
+             (t
+              result))))
+   entries
+   :initial-value (auth-source-pass-parse-entry (car entries))))
+
+(defun auth-source-pass--matching-entries (hostname user port)
+  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
+
+The result is a list of lists of password-store entries, where
+each sublist contains entries that actually exist in the
+password-store matching one of the entry name formats that
+auth-source-pass expects, most specific to least specific."
+  (let* ((entries-lists (mapcar
+                         #'cdr
+                         (auth-source-pass--accumulate-matches hostname user port)))
+         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
+    (if entries
+        (auth-source-pass--do-debug (format "found: %S" entries))
+      (auth-source-pass--do-debug "no matches found"))
+    entries-lists))
+
+(defun auth-source-pass--accumulate-matches (hostname user port)
+  "Accumulate matching password-store entries into sublists.
+
+Entries matching supported formats that combine HOSTNAME, USER, &
+PORT are accumulated into sublists where the car of each sublist
+is a regular expression for matching paths in the password-store
+and the remainder is the list of matching entries."
+  (let ((suffix-match-lists
+         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
+                 (auth-source-pass--generate-entry-suffixes hostname user port))))
+    (cl-reduce #'auth-source-pass--entry-reducer
+               (auth-source-pass-entries)
+               :initial-value suffix-match-lists)))
+
+(defun auth-source-pass--entry-reducer (match-lists entry)
+  "Match MATCH-LISTS sublists against ENTRY.
+
+The result is a copy of match-lists with the entry added to the
+end of any sublists for which the regular expression at the head
+of the list matches the entry name."
+  (mapcar (lambda (match-list)
+            (if (string-match (car match-list) entry)
+                (append match-list (list entry))
+              match-list))
+          match-lists))
+
+(defun auth-source-pass--generate-entry-suffixes (hostname user port)
+  "Return a list of possible entry path suffixes in the password-store.
+
+Based on the supported pathname patterns for HOSTNAME, USER, &
+PORT, return a list of possible suffixes for matching entries in
+the password-store."
+  (let ((domains (auth-source-pass--domains (split-string hostname "\\."))))
+    (seq-mapcat (lambda (n)
+                  (auth-source-pass--name-port-user-suffixes n user port))
+                domains)))
+
+(defun auth-source-pass--domains (name-components)
+  "Return a list of possible domain names matching the hostname.
+
+This function takes a list of NAME-COMPONENTS, the strings
+separated by periods in the hostname, and returns a list of full
+domain names containing the trailing sequences of those
+components, from longest to shortest."
+  (when (> (length name-components) 0)
+    (cons (mapconcat 'identity name-components ".")
+          (auth-source-pass--domains (cdr name-components)))))
+
+(defun auth-source-pass--name-port-user-suffixes (name user port)
+  "Return a list of possible path suffixes for NAME, USER, & PORT.
+
+The resulting list is ordered from most specifc to least
+specific, with paths matching all of NAME, USER, & PORT first,
+then NAME & USER, then NAME & PORT, then just NAME."
+  (seq-mapcat
+   #'identity
+   (list
+    (when (and user port)
+      (list
+       (format "%s@%s%s%s" user name auth-source-pass-port-separator port)
+       (format "%s%s%s/%s" name auth-source-pass-port-separator port user)))
+    (when user
+      (list
+       (format "%s@%s" user name)
+       (format "%s/%s" name user)))
+    (when port
+      (list
+       (format "%s%s%s" name auth-source-pass-port-separator port)))
+    (list
+     (format "%s" name)))))
 
 (provide 'auth-source-pass)
 ;;; auth-source-pass.el ends here
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 1539d9611f..2c28f79945 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -63,14 +63,19 @@ auth-source-pass--debug
 This function is intended to be set to `auth-source-debug`."
   (add-to-list 'auth-source-pass--debug-log (apply #'format msg) t))
 
+(defvar auth-source-pass--parse-log nil)
+
 (defmacro auth-source-pass--with-store (store &rest body)
   "Use STORE as password-store while executing BODY."
   (declare (indent 1))
-  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry) (lambda (entry) (cdr (cl-find entry ,store :key #'car :test #'string=))) )
-             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store)))
-             ((symbol-function 'auth-source-pass--entry-valid-p) (lambda (_entry) t)))
+  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+              (lambda (entry)
+                (add-to-list 'auth-source-pass--parse-log entry)
+                (cdr (cl-find entry ,store :key #'car :test #'string=))))
+             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store))))
      (let ((auth-source-debug #'auth-source-pass--debug)
-           (auth-source-pass--debug-log nil))
+           (auth-source-pass--debug-log nil)
+           (auth-source-pass--parse-log nil))
        ,@body)))
 
 (ert-deftest auth-source-pass-any-host ()
@@ -88,125 +93,184 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass-find-match-minimal-parsing ()
+  (let ((store-contents
+         '(("baz" ("secret" . "baz password"))
+           ("baz:123" ("secret" . "baz:123 password"))
+           ("baz/foo" ("secret" . "baz/foo password"))
+           ("foo@baz" ("secret" . "foo@baz password"))
+           ("baz:123/foo" ("secret" . "baz:123/foo password"))
+           ("foo@baz:123" ("secret" . "foo@baz:123 password"))
+           ("bar.baz" ("secret" . "bar.baz password"))
+           ("bar.baz:123" ("secret" . "bar.baz:123 password"))
+           ("bar.baz/foo" ("secret" . "bar.baz/foo password"))
+           ("foo@bar.baz" ("secret" . "foo@bar.baz password"))
+           ("bar.baz:123/foo" ("secret" . "bar.baz:123/foo password"))
+           ("foo@bar.baz:123" ("secret" . "foo@bar.baz:123 password")))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" "123")
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" nil)
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil "123")
+      (should (equal auth-source-pass--parse-log '("bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("baz"))))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store '(("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store '(("SomeUser@foo") ("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
+        ("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store '(("foo") ("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
+
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil ("bar.com") nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
   (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   "someone@bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-bad-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
   (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-prefer-full ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
   (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "foo.bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(("foo.bar.com") ("bar.com") nil)))))
 
 (ert-deftest auth-source-pass-dont-match-at-folder-name ()
   (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
   (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                   "bar.com:443/someone"))))
+    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                   '(nil ("bar.com:443/someone") nil nil nil nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                     "bar.com#443/someone")))))
-
-(ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
-  (auth-source-pass--with-store '(("foo.com/bar"))
-    (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-                   "foo.com/bar"))))
-
-(ert-deftest auth-source-pass-search-with-user-first ()
+      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                     '(nil ("bar.com#443/someone") nil nil nil nil
+                           nil nil nil nil nil nil))))))
+
+(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
+  (auth-source-pass--with-store
+      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
+    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo.com/bar password")))))
+
+(ert-deftest auth-source-pass-matching-entries-with-user-first ()
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--find-match "foo" "user" nil)
-                   "user@foo"))
-    (auth-source-pass--should-have-message-containing "Found 1 match")))
+    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
+                   '(("user@foo") nil ("foo"))))
+    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo" ("user" . "someone")))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "subdir/foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "subdir/foo password"))
+      (should (equal (auth-source-pass--get-attr "user" result)
+                     "someone")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store '(("foo" ("user" . "someone")) ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password") ("user" . "someone"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "the first one")))
-
-(ert-deftest auth-source-pass-make-divansantana-happy ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
+
+(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
   (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "smtp.host.com" "myusername@host.co.za" nil)
-                   "host.com"))))
+    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
+                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
 
 (ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
-                   "host.com"))))
+  (auth-source-pass--with-store
+      '(("host.com" ("secret" . "host.com password")))
+    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "host.com password")))))
 
-(ert-deftest auth-source-pass-find-host-with-port ()
+(ert-deftest auth-source-pass-matching-entries-host-with-port ()
   (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                   "host.com:443"))))
+    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                   '(nil nil nil nil ("host.com:443") nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                     "host.com#443")))))
+      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                     '(nil nil nil nil ("host.com#443") nil
+                           nil nil nil nil nil nil))))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -218,7 +282,8 @@ auth-source-pass--with-store-find-foo
        ,@body)))
 
 (ert-deftest auth-source-pass-build-result-return-parameters ()
-  (auth-source-pass--with-store-find-foo '(("foo"))
+  (auth-source-pass--with-store-find-foo
+      '(("foo" ("secret" . "foo password")))
     (let ((result (auth-source-pass--build-result "foo" 512 "user")))
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "user")))))
@@ -238,7 +303,9 @@ auth-source-pass--with-store-find-foo
 (ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
   (let (passed-host)
     (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port) (setq passed-host host))))
+               (lambda (host _user _port)
+                 (setq passed-host host)
+                 nil)))
       (auth-source-pass--build-result "https://user@host.com:123" nil nil)
       (should (equal passed-host "https://user@host.com:123"))
       (auth-source-pass--build-result "https://user@host.com" nil nil)
@@ -249,27 +316,16 @@ auth-source-pass--with-store-find-foo
       (should (equal passed-host "user@host.com:443")))))
 
 (ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (cl-letf (((symbol-function 'auth-source-pass-entries)
-             (lambda () '("foo.site.com" "bar.site.com" "mail/baz.site.com/scott")))
-            ((symbol-function 'auth-source-pass--entry-valid-p)
-             ;; only foo.site.com and "mail/baz.site.com/scott" are valid
-             (lambda (entry) (member entry '("foo.site.com" "mail/baz.site.com/scott")))))
-    (should (equal (auth-source-pass--find-all-by-entry-name "foo.site.com" "someuser")
-                   '("foo.site.com")))
-    (should (equal (auth-source-pass--find-all-by-entry-name "bar.site.com" "someuser")
-                   '()))
-    (should (equal (auth-source-pass--find-all-by-entry-name "baz.site.com" "scott")
-                   '("mail/baz.site.com/scott")))))
-
-(ert-deftest auth-source-pass-entry-is-not-valid-when-unreadable ()
-  (cl-letf (((symbol-function 'auth-source-pass--read-entry)
-             (lambda (entry)
-               ;; only foo is a valid entry
-               (if (string-equal entry "foo")
-                   "password"
-                 nil))))
-    (should (auth-source-pass--entry-valid-p "foo"))
-    (should-not (auth-source-pass--entry-valid-p "bar"))))
+  (auth-source-pass--with-store
+      '(("foo.site.com" ("secret" . "foo.site.com password"))
+        ("bar.site.com") ; An entry name with no data is invalid
+        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
+    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
+                   '(("secret" . "foo.site.com password"))))
+    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
+                   nil))
+    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
+                   '(("secret" . "mail/baz.site.com/scott password"))))))
 
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-lisp-auth-source-pass.el-Add-Keith-Amidon-to-authors.patch --]
[-- Type: text/x-patch, Size: 755 bytes --]

From 90829466467c97f8eb350c13a8ad21ef566f7108 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sat, 11 May 2019 08:22:56 -0700
Subject: [PATCH 09/12] * lisp/auth-source-pass.el: Add Keith Amidon to authors

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index bb090eae40..5f05a63c06 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,6 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
+;;         Keith Amidon <camalot@picnicpark.org>
 ;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0010-Refactoring-of-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 34944 bytes --]

From 7d3db58d79d593314ba599855613305f39bf2bdb Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 14 May 2019 05:50:59 +0200
Subject: [PATCH 10/12] Refactoring of auth-source-pass

* lisp/auth-source-pass.el: Refactoring.
* test/lisp/auth-source-pass-tests.el: Refactoring and adding some
more tests.
---
 lisp/auth-source-pass.el            | 108 +++----
 test/lisp/auth-source-pass-tests.el | 464 +++++++++++++++++-----------
 2 files changed, 331 insertions(+), 241 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 5f05a63c06..1daafdb7e6 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -199,10 +199,17 @@ auth-source-pass--find-match
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
+  (apply #'auth-source-pass--find-match-unambiguous (auth-source-pass--disambiguate host user port)))
+
+(defun auth-source-pass--disambiguate (host &optional user port)
+  "Return (HOST USER PORT) after disambiguation.
+Disambiguate between having user provided inside HOST (e.g.,
+user@server.com) and inside USER by giving priority to USER.
+Same for PORT."
   (let* ((url (url-generic-parse-url (if (string-match-p ".*://" host)
                                          host
                                        (format "https://%s" host)))))
-    (auth-source-pass--find-match-unambiguous
+    (list
      (or (url-host url) host)
      (or user (url-user url))
      ;; url-port returns 443 (because of the https:// above) by default
@@ -214,74 +221,49 @@ auth-source-pass--find-match-unambiguous
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (cl-reduce
-   (lambda (result entries)
-     (or result
-         (pcase (length entries)
-           (0 nil)
-           (1 (auth-source-pass-parse-entry (car entries)))
-           (_ (auth-source-pass--select-from-entries entries user)))))
-   (auth-source-pass--matching-entries hostname user port)
-   :initial-value nil))
+  (let ((all-entries (auth-source-pass-entries))
+        (suffixes (auth-source-pass--generate-entry-suffixes hostname user port)))
+    (auth-source-pass--do-debug "searching for entries matching hostname=%S, user=%S, port=%S"
+                                hostname (or user "") (or port ""))
+    (auth-source-pass--do-debug "corresponding suffixes to search for: %S" suffixes)
+    (catch 'auth-source-pass-break
+      (dolist (suffix suffixes)
+        (let* ((matching-entries (auth-source-pass--entries-matching-suffix suffix all-entries))
+               (best-entry-data (auth-source-pass--select-from-entries matching-entries user)))
+          (pcase (length matching-entries)
+            (0 (auth-source-pass--do-debug "found no entries matching %S" suffix))
+            (1 (auth-source-pass--do-debug "found 1 entry matching %S: %S"
+                                           suffix
+                                           (car matching-entries)))
+            (_ (auth-source-pass--do-debug "found %s entries matching %S: %S"
+                                           (length matching-entries)
+                                           suffix
+                                           matching-entries)))
+          (when best-entry-data
+            (throw 'auth-source-pass-break best-entry-data)))))))
 
 (defun auth-source-pass--select-from-entries (entries user)
   "Return best matching password-store entry data from ENTRIES.
 
 If USER is non nil, give precedence to entries containing a user field
 matching USER."
-  (cl-reduce
-   (lambda (result entry)
-     (let ((entry-data (auth-source-pass-parse-entry entry)))
-       (cond ((equal (auth-source-pass--get-attr "user" result) user)
-              result)
-             ((equal (auth-source-pass--get-attr "user" entry-data) user)
-              entry-data)
-             (t
-              result))))
-   entries
-   :initial-value (auth-source-pass-parse-entry (car entries))))
-
-(defun auth-source-pass--matching-entries (hostname user port)
-  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
-
-The result is a list of lists of password-store entries, where
-each sublist contains entries that actually exist in the
-password-store matching one of the entry name formats that
-auth-source-pass expects, most specific to least specific."
-  (let* ((entries-lists (mapcar
-                         #'cdr
-                         (auth-source-pass--accumulate-matches hostname user port)))
-         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
-    (if entries
-        (auth-source-pass--do-debug (format "found: %S" entries))
-      (auth-source-pass--do-debug "no matches found"))
-    entries-lists))
-
-(defun auth-source-pass--accumulate-matches (hostname user port)
-  "Accumulate matching password-store entries into sublists.
-
-Entries matching supported formats that combine HOSTNAME, USER, &
-PORT are accumulated into sublists where the car of each sublist
-is a regular expression for matching paths in the password-store
-and the remainder is the list of matching entries."
-  (let ((suffix-match-lists
-         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
-                 (auth-source-pass--generate-entry-suffixes hostname user port))))
-    (cl-reduce #'auth-source-pass--entry-reducer
-               (auth-source-pass-entries)
-               :initial-value suffix-match-lists)))
-
-(defun auth-source-pass--entry-reducer (match-lists entry)
-  "Match MATCH-LISTS sublists against ENTRY.
-
-The result is a copy of match-lists with the entry added to the
-end of any sublists for which the regular expression at the head
-of the list matches the entry name."
-  (mapcar (lambda (match-list)
-            (if (string-match (car match-list) entry)
-                (append match-list (list entry))
-              match-list))
-          match-lists))
+  (let (fallback)
+    (catch 'auth-source-pass-break
+      (dolist (entry entries fallback)
+        (let ((entry-data (auth-source-pass-parse-entry entry)))
+          (when (and entry-data (not fallback))
+            (setq fallback entry-data)
+            (when (or (not user) (equal (auth-source-pass--get-attr "user" entry-data) user))
+              (throw 'auth-source-pass-break entry-data))))))))
+
+(defun auth-source-pass--entries-matching-suffix (suffix entries)
+  "Return entries matching SUFFIX.
+If ENTRIES is nil, use the result of calling `auth-source-pass-entries' instead."
+  (cl-remove-if-not
+   (lambda (entry) (string-match-p
+               (format "\\(^\\|/\\)%s$" (regexp-quote suffix))
+               entry))
+   (or entries (auth-source-pass-entries))))
 
 (defun auth-source-pass--generate-entry-suffixes (hostname user port)
   "Return a list of possible entry path suffixes in the password-store.
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 2c28f79945..e9d4cde4d2 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -52,11 +52,21 @@
 (defvar auth-source-pass--debug-log nil
   "Contains a list of all messages passed to `auth-source-do-debug`.")
 
-(defun auth-source-pass--should-have-message-containing (regexp)
-  "Assert that at least one `auth-source-do-debug` matched REGEXP."
-  (should (seq-find (lambda (message)
-                      (string-match regexp message))
-                    auth-source-pass--debug-log)))
+(defun auth-source-pass--have-message-matching (regexp)
+  "Return non-nil iff at least one `auth-source-do-debug` match REGEXP."
+  (seq-find (lambda (message)
+              (string-match regexp message))
+            auth-source-pass--debug-log))
+
+(defun auth-source-pass--explain--have-message-matching (regexp)
+  "Explainer function for `auth-source-pass--have-message-matching'.
+REGEXP is the same as in `auth-source-pass--have-message-matching'."
+  `(regexp
+    ,regexp
+    messages
+    ,(mapconcat #'identity auth-source-pass--debug-log "\n- ")))
+
+(put #'auth-source-pass--have-message-matching 'ert-explainer #'auth-source-pass--explain--have-message-matching)
 
 (defun auth-source-pass--debug (&rest msg)
   "Format MSG and add that to `auth-source-pass--debug-log`.
@@ -78,6 +88,82 @@ auth-source-pass--with-store
            (auth-source-pass--parse-log nil))
        ,@body)))
 
+(defun auth-source-pass--explain-match-entry-p (entry hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-entry-p'.
+
+ENTRY, HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-entry-p'."
+  `(entry
+    ,entry
+    store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-entry-p 'ert-explainer #'auth-source-pass--explain-match-entry-p)
+
+(defun auth-source-pass--includes-sorted-entries (entries hostname &optional user port)
+  "Return non-nil iff ENTRIES matching the parameters are found in store.
+ENTRIES should be sorted from most specific to least specific.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (if (seq-empty-p entries)
+      t
+    (and
+     (auth-source-pass-match-entry-p (car entries) hostname user port)
+     (auth-source-pass--includes-sorted-entries (cdr entries) hostname user port))))
+
+(defun auth-source-pass--explain-includes-sorted-entries (entries hostname &optional user port)
+  "Explainer function for `auth-source-pass--includes-sorted-entries'.
+
+ENTRIES, HOSTNAME, USER and PORT are the same as in `auth-source-pass--includes-sorted-entries'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)
+    entries
+    ,entries))
+
+(put 'auth-source-pass--includes-sorted-entries 'ert-explainer #'auth-source-pass--explain-includes-sorted-entries)
+
+(defun auth-source-pass--explain-match-any-entry-p (hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-any-entry-p'.
+
+HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-any-entry-p'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-any-entry-p 'ert-explainer #'auth-source-pass--explain-match-any-entry-p)
+
+(defun auth-source-pass--matching-entries (hostname &optional user port)
+  "Return password-store entries matching HOSTNAME, USER, PORT.
+
+The result is a list of lists of password-store entries.  Each
+sublist contains the password-store entries whose names match a
+suffix in `auth-source-pass--generate-entry-suffixes'.  The
+result is ordered the same way as the suffixes."
+  (let ((entries (auth-source-pass-entries)))
+    (mapcar (lambda (suffix) (auth-source-pass--entries-matching-suffix suffix entries))
+            (auth-source-pass--generate-entry-suffixes hostname user port))))
+
+(defun auth-source-pass-match-entry-p (entry hostname &optional user port)
+  "Return non-nil iff an ENTRY matching the parameters is found in store.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if
+   (lambda (entries) (cl-find entry entries :test #'string=))
+   (auth-source-pass--matching-entries hostname user port)))
+
+(defun auth-source-pass-match-any-entry-p (hostname &optional user port)
+  "Return non-nil iff there is at least one entry matching the parameters.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if #'identity (auth-source-pass--matching-entries hostname user port)))
+
 (ert-deftest auth-source-pass-any-host ()
   (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
                                   ("bar"))
@@ -93,6 +179,101 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass--disambiguate-extract-host-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo")) "foo"))
+  ;; only user
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo")) "foo"))
+  ;; only port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80")) "foo"))
+  ;; both user and port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80")) "foo"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80/path")) "foo")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-user-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo")) nil))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo")) "user"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80")) nil))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80")) "user"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path")) "user")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-user-parameter ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo" "user2")) "user2"))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo" "user2")) "user2"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80" "user2")) "user2"))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80" "user2")) "user2"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path" "user2")) "user2")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-port-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo")) "443"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo")) "443"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80")) "80"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80")) "80"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path")) "80"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path")) "80")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-port-parameter ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo" "user2" "8080")) "8080"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo" "user2" "8080")) "8080"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80" "user2" "8080")) "8080"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80" "user2" "8080")) "8080"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path" "user2" "8080")) "8080")))
+
 (ert-deftest auth-source-pass-find-match-minimal-parsing ()
   (let ((store-contents
          '(("baz" ("secret" . "baz password"))
@@ -121,156 +302,92 @@ auth-source-pass--with-store
       (should (equal auth-source-pass--parse-log '("bar.baz"))))
     (auth-source-pass--with-store store-contents
       (auth-source-pass--find-match "baz" nil nil)
-      (should (equal auth-source-pass--parse-log '("baz"))))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
-        ("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
+      (should (equal auth-source-pass--parse-log '("baz"))))
+    (auth-source-pass--with-store
+        '(("dir1/bar.com" ("key" . "val"))
+          ("dir2/bar.com" ("key" . "val"))
+          ("dir3/bar.com" ("key" . "val")))
+      (auth-source-pass--find-match "bar.com" nil nil)
+      (should (= (length auth-source-pass--parse-log) 1)))))
+
+(ert-deftest auth-source-pass--find-match-return-parsed-data ()
+  (auth-source-pass--with-store '(("bar.com" ("key" . "val")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil))))
+  (auth-source-pass--with-store '(("dir1/bar.com" ("key1" . "val1")) ("dir2/bar.com" ("key2" . "val2")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
-  (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
-  (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
-  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(("foo.bar.com") ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-dont-match-at-folder-name ()
-  (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
-  (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                   '(nil ("bar.com:443/someone") nil nil nil nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com"))
+    ;; match even if sub-domain is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "foo.bar.com"))
+    ;; match even if a user is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user"))
+    ;; match even if user as an @ sign
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user@someplace"))
+    ;; match even if a port is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" nil "8080"))
+    ;; match even if a user and a port are asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user" "8080"))
+    ;; don't match if a '.' is replaced with another character
+    (auth-source-pass--with-store '(("barXcom"))
+      (should-not (auth-source-pass-match-any-entry-p "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-username ()
+  (auth-source-pass--with-store '(("user@foo"))
+    (should (auth-source-pass-match-entry-p "user@foo" "foo" "user")))
+  ;; match even if sub-domain is asked for
+  (auth-source-pass--with-store '(("user@bar.com"))
+    (should (auth-source-pass-match-entry-p "user@bar.com" "foo.bar.com" "user")))
+  ;; don't match if no user is asked for
+  (auth-source-pass--with-store '(("user@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo")))
+  ;; don't match if user is different
+  (auth-source-pass--with-store '(("user1@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo" "user2")))
+  ;; don't match if sub-domain is asked for but user is different
+  (auth-source-pass--with-store '(("user1@bar.com"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.bar.com" "user2"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-port ()
+  (auth-source-pass--with-store '(("bar.com:8080"))
+    (should (auth-source-pass-match-entry-p "bar.com:8080" "bar.com" nil "8080"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-slash ()
+  ;; match if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com/user"))
+    (should (auth-source-pass-match-entry-p "foo.com/user" "foo.com" "user")))
+  ;; match with port if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com:8080/user"))
+    (should (auth-source-pass-match-entry-p "foo.com:8080/user" "foo.com" "user" "8080")))
+  ;; don't match if entry filename doesn't match user
+  (auth-source-pass--with-store '(("foo.com/baz"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.com" "user"))))
+
+(ert-deftest auth-source-pass-matching-entries-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                     '(nil ("bar.com#443/someone") nil nil nil nil
-                           nil nil nil nil nil nil))))))
-
-(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
-  (auth-source-pass--with-store
-      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
-    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo.com/bar password")))))
-
-(ert-deftest auth-source-pass-matching-entries-with-user-first ()
+      (should (auth-source-pass-match-entry-p "bar.com#443/someone" "bar.com" "someone" "443")))))
+
+(ert-deftest auth-source-pass--matching-entries-sort-results ()
+  (auth-source-pass--with-store '(("user@foo") ("foo"))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; same, but store is reversed
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
-                   '(("user@foo") nil ("foo"))))
-    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "subdir/foo password"))
-      (should (equal (auth-source-pass--get-attr "user" result)
-                     "someone")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password") ("user" . "someone"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
-                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
-
-(ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store
-      '(("host.com" ("secret" . "host.com password")))
-    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "host.com password")))))
-
-(ert-deftest auth-source-pass-matching-entries-host-with-port ()
-  (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                   '(nil nil nil nil ("host.com:443") nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
-  (let ((auth-source-pass-port-separator "#"))
-    (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                     '(nil nil nil nil ("host.com#443") nil
-                           nil nil nil nil nil nil))))))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; with sub-domain
+  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
+    (should (auth-source-pass--includes-sorted-entries '("foo.bar.com" "bar.com") "foo.bar.com")))
+  ;; matching user in the entry data takes priority
+  (auth-source-pass--with-store '(("dir1/bar.com") ("dir2/bar.com" ("user" . "user")))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user")))
+  ;; same, but store is reversed
+  (auth-source-pass--with-store '(("dir2/bar.com" ("user" . "user")) ("dir1/bar.com"))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user"))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -300,33 +417,6 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "anuser")))))
 
-(ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
-  (let (passed-host)
-    (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port)
-                 (setq passed-host host)
-                 nil)))
-      (auth-source-pass--build-result "https://user@host.com:123" nil nil)
-      (should (equal passed-host "https://user@host.com:123"))
-      (auth-source-pass--build-result "https://user@host.com" nil nil)
-      (should (equal passed-host "https://user@host.com"))
-      (auth-source-pass--build-result "user@host.com" nil nil)
-      (should (equal passed-host "user@host.com"))
-      (auth-source-pass--build-result "user@host.com:443" nil nil)
-      (should (equal passed-host "user@host.com:443")))))
-
-(ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (auth-source-pass--with-store
-      '(("foo.site.com" ("secret" . "foo.site.com password"))
-        ("bar.site.com") ; An entry name with no data is invalid
-        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
-    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
-                   '(("secret" . "foo.site.com password"))))
-    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
-                   nil))
-    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
-                   '(("secret" . "mail/baz.site.com/scott password"))))))
-
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
     (auth-source-pass-enable)
@@ -334,6 +424,24 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :user) "someone"))
       (should (equal (plist-get result :host) "gitlab.com")))))
 
+(ert-deftest auth-source-pass-prints-meaningful-debug-log ()
+  (auth-source-pass--with-store '()
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "entries matching hostname=\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "corresponding suffixes to search for: .*\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "found no entries matching \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 1 entry matching \"gitlab.com\": \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("a/gitlab.com") ("b/gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 2 entries matching \"gitlab.com\": (\"a/gitlab.com\" \"b/gitlab.com\")"))))
+
 (provide 'auth-source-pass-tests)
 
 ;;; auth-source-pass-tests.el ends here
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0011-lisp-auth-source-pass.el-Version-5.0.0.patch --]
[-- Type: text/x-patch, Size: 794 bytes --]

From be1914d3c6ceab3f0712088547545bdf5121d204 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 28 May 2019 08:46:41 +0200
Subject: [PATCH 11/12] * lisp/auth-source-pass.el: Version 5.0.0

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 1daafdb7e6..a5c1a26a88 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -5,7 +5,7 @@
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
 ;;         Keith Amidon <camalot@picnicpark.org>
-;; Version: 4.0.2
+;; Version: 5.0.0
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0012-etc-NEWS-Describe-changes-to-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 1351 bytes --]

From 84f2050e78c0f19dacb67d50dbe4d1dc114f5e3e Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sun, 2 Jun 2019 11:08:40 +0200
Subject: [PATCH 12/12] * etc/NEWS: Describe changes to auth-source-pass

---
 etc/NEWS | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index 975fab495a..5dcdac2668 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1485,6 +1485,25 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
 value.  Auto Revert mode can use this information to avoid polling the
 buffer periodically when 'auto-revert-avoid-polling' is non-nil.
 
+** auth-source-pass
+
+*** New customizable variable 'auth-source-pass-path' for the path to
+the password-store.  This defaults to ~/.password-store.
+
+*** New customizable variable 'auth-source-pass-port-separator' to
+specify separator between host and port.  This defaults to colon
+":".
+
+*** auth-source-pass.el and auth-source-pass-tests.el have been
+massively rewritten to minimize parsing of password-store entries.
+This makes the package usable with physical tokens requiring touching
+a sensor for every decryption.
+
+*** 'auth-source-pass-get' has an autoload cookie now.
+
+*** 'auth-source-pass-search' now correctly returns nil if no entry
+found.
+
 \f
 * New Modes and Packages in Emacs 27.1
 
-- 
2.21.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-02  9:11 bug#36052: 26.2.50; [PATCH] Improve auth-source-pass Damien Cassou
@ 2019-06-07  0:43 ` Noam Postavsky
  2019-06-08 15:47   ` Damien Cassou
  0 siblings, 1 reply; 13+ messages in thread
From: Noam Postavsky @ 2019-06-07  0:43 UTC (permalink / raw)
  To: Damien Cassou
  Cc: Magnus Henoch, Nicolas Petton, Iku Iwasa, Keith Amidon, galaunay,
	36052, Ted Zlatanov

Damien Cassou <damien@cassou.me> writes:

> Magnus Henoch hasn't signed but his contribution is rather small (4
> lines of a new unit-test and 2 lines of code). See patch 0001.

His patch needs the line

    Copyright-paperwork-exempt: yes

> Subject: [PATCH 04/12] Add auth-source-pass-path option

I think auth-source-pass-filename would be the correct name to conform
with GNU naming conventions.

> Subject: [PATCH 07/12] Split out the attribute retrieval form
>  auth-source-pass-get
>
> Eliminate the need to repeatedly retrieve and parse the data for the
> entry. This is generally a good thing since it eliminates repetitions
> of the same crypto and parsing operations. It is especially valuable
> when protecting an entry with a yubikey with touch required for crypto
> operations as it eliminates the need to touch the yubikey sensor for
> each attribute retrieved.

Missing double spacing at end of sentence here.

> +(defun auth-source-pass--get-attr (key entry-data)
> +  "Return the value associated to KEY in data from an already parsed entry.
> +
> +ENTRY-DATA is the data from a parsed password-store entry.
> +The key used to retrieve the password is the symbol `secret'.
> +
> +The convention used as the format for a password-store file is
> +the following (see http://www.passwordstore.org/#organization):
> +
> +secret
> +key1: value1
> +key2: value2"

I think the end of the docstring could be replaced with

    See `auth-source-pass-get'.

It seems a little silly to duplicate this info so close by.

> Subject: [PATCH 08/12] Minimize entry parsing in auth-source-pass
>
> Prior to this commit, while searching for the most applicable entry
> password-store entries were decrypted and parsed to ensure they were
> valid. The entries were parsed in the order they were found on the
> filesystem and all applicable entries would be decrypted and parsed,
> which varied based on the contents of the password-store and the entry
> to be found.
>
> This is fine when the GPG key is cached and each entry can be
> decrypted without user interaction. However, for security some people
> have their GPG on a hardware token like a Yubikey setup so that they
> have to touch a sensor on the toke for every cryptographic operation,
> in which case it becomes inconvenient as each attempt to find an entry
> requires a variable number of touches of the hardware token.
>
> The implementation already assumes that names which contain more of
> the information in the search key should be preferred so there is an
> ordering of preference of applicable entries. If the decrypt and
> parsing is removed from the initial identification of applicable
> entries in the store then in most cases a single decrypt and parse of
> the most preferred entry will suffice, improving the experience for
> hardware token users that require interaction with the token.
>
> This commit implements that strategy. It is in spirit a refactor of
> the existing code. The core of the change is the function
> auth-source-pass--applicable-entries, which generates an ordered list
> of regular expression matchers for all possible names that could be in
> the password-store for the entry to be found and then makes a pass
> over the password-store entry names accumulating the matching entries
> in a list after the regexp that matched. This implementation ensures
> the password-store entry list still only has to be scanned once.
>
> The existing auth-source-pass--find-match-unambiguous was modified to
> use this new function to obtain candidate entries and then parse them
> one by one until an entry containing the desired information is
> located. When complete it now returns the parsed data of the entry
> instead of the entry name so that the information can be used directly
> to construct the auth-source response.
>
> * lisp/auth-source-pass.el: Private functions were refactored to
> reduce the number of decryption operations.

Double spacing, and this ChangeLog entry is a little sparse.  It looks
like the last two prose paragraphs could be easily made into ChangeLog
entries, since they're already talking about specific functions.

> +  (when (> (length name-components) 0)
> +    (cons (mapconcat 'identity name-components ".")
> +          (auth-source-pass--domains (cdr name-components)))))

I suggest instead:

    (cl-maplist (lambda (components) (mapconcat #'identity components "."))
                name-components)

> Subject: [PATCH 10/12] Refactoring of auth-source-pass
>
> * lisp/auth-source-pass.el: Refactoring.

This one's a little empty too.

> Subject: [PATCH 12/12] * etc/NEWS: Describe changes to auth-source-pass
>
> ---
>  etc/NEWS | 19 +++++++++++++++++++
>  1 file changed, 19 insertions(+)
>
> diff --git a/etc/NEWS b/etc/NEWS
> index 975fab495a..5dcdac2668 100644
> --- a/etc/NEWS
> +++ b/etc/NEWS
> @@ -1485,6 +1485,25 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
>  value.  Auto Revert mode can use this information to avoid polling the
>  buffer periodically when 'auto-revert-avoid-polling' is non-nil.
>  
> +** auth-source-pass
> +
> +*** New customizable variable 'auth-source-pass-path' for the path to
> +the password-store.  This defaults to ~/.password-store.

It's better to have NEWS entries have the first sentence in one line.
Something like

    *** New customizable variable 'auth-source-pass-path'.
    Allows setting the path to the password-store, defaults to ~/.password-store.

> +*** New customizable variable 'auth-source-pass-port-separator' to
> +specify separator between host and port.  This defaults to colon
> +":".

    *** New customizable variable 'auth-source-pass-port-separator'.
    Specifies separator between host and port, defaults to colon ":".

> +*** auth-source-pass.el and auth-source-pass-tests.el have been
> +massively rewritten to minimize parsing of password-store entries.
> +This makes the package usable with physical tokens requiring touching
> +a sensor for every decryption.

This one puts too much emphasis on the rewrite which is an
implementation detail.

   *** Minimize the number of decryptions during password lookup.
   This makes the package usable with physical tokens requiring touching
   a sensor for every decryption.
   

> +*** 'auth-source-pass-get' has an autoload cookie now.

Maybe just say "is now autoloaded".

> +*** 'auth-source-pass-search' now correctly returns nil if no entry
> +found.

We don't put bug fixes in NEWS, so this one can be left out.





^ permalink raw reply	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-07  0:43 ` Noam Postavsky
@ 2019-06-08 15:47   ` Damien Cassou
  2019-06-08 16:02     ` Eli Zaretskii
                       ` (2 more replies)
  0 siblings, 3 replies; 13+ messages in thread
From: Damien Cassou @ 2019-06-08 15:47 UTC (permalink / raw)
  To: Noam Postavsky
  Cc: Magnus Henoch, Nicolas Petton, Iku Iwasa, Keith Amidon, galaunay,
	36052, Ted Zlatanov

[-- Attachment #1: Type: text/plain, Size: 1969 bytes --]

Hi Noam and everyone,

thank you *very* much for your review. I appreciate the effort to review
such a series of patches. Please find attached the new patches. I took
all your comments into account except for what follows:

Noam Postavsky <npostavs@gmail.com> writes:
> Damien Cassou <damien@cassou.me> writes:
>> * lisp/auth-source-pass.el: Private functions were refactored to
>> reduce the number of decryption operations.
>
> Double spacing, and this ChangeLog entry is a little sparse.  It looks
> like the last two prose paragraphs could be easily made into ChangeLog
> entries, since they're already talking about specific functions.


I thought about doing that as well but didn't. If you insist, I will do
the requested changes but here are my reasons for not doing it right
away:

- The changes are on private methods (with the "--" naming convention)
  and I'm not sure how much of private changes should be in the
  ChangeLog.

- The commit message you mention modifies functions that are modified
  again by a later patch (named "Refactoring of auth-source-pass"). I
  usually only send the latest version of my code and not the whole
  history but, this time, I'm not the author of the original version and
  I believe the author deserves to have his name in Emacs' git history
  because of the massive work he did for the package.

- I have tried not to rewrite too much of contributor's code and text in
  their own commit so that the authorship makes sense. I have no problem
  changing contributions in a later commit though (as I've shown in the
  patch "Refactoring of auth-source-pass").


What do you think?

>> * lisp/auth-source-pass.el: Refactoring.
>
> This one's a little empty too.


Changes are again on private functions. What do you suggest?

Thank you again for your work!

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Fix-auth-source-pass-to-return-nil-if-no-entry-found.patch --]
[-- Type: text/x-patch, Size: 1985 bytes --]

From 4b1bb2d4b1af3457181ac8be6f149f787f2e45fe Mon Sep 17 00:00:00 2001
From: Magnus Henoch <magnus.henoch@gmail.com>
Date: Fri, 2 Nov 2018 21:51:59 +0000
Subject: [PATCH 01/12] Fix auth-source-pass to return nil if no entry found

* lisp/auth-source-pass.el (auth-source-pass-search): If there is no
matching entry, auth-source-pass-search should return nil, not (nil).
This lets auth-source fall back to other backends in the auth-sources
list.
* test/lisp/auth-source-pass-tests.el: Add corresponding test.

Copyright-paperwork-exempt: yes
---
 lisp/auth-source-pass.el            | 3 ++-
 test/lisp/auth-source-pass-tests.el | 5 +++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4283ed0392..c82c90167a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -56,7 +56,8 @@
          ;; Do not build a result, as none will match when HOST is nil
          nil)
         (t
-         (list (auth-source-pass--build-result host port user)))))
+         (when-let ((result (auth-source-pass--build-result host port user)))
+           (list result)))))
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index d1e486ad6b..ab9ef92c14 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -83,6 +83,11 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host nil))))
 
+(ert-deftest auth-source-pass-not-found ()
+  (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
+                                  ("bar"))
+    (should-not (auth-source-pass-search :host "baz"))))
+
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
   (auth-source-pass--with-store '(("foo"))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-lisp-auth-source-pass.el-Version-4.0.2.patch --]
[-- Type: text/x-patch, Size: 745 bytes --]

From 926a0f7898ebd2607e717ece6cb8784ebd0f267a Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sat, 3 Nov 2018 09:06:42 +0100
Subject: [PATCH 02/12] * lisp/auth-source-pass.el: Version 4.0.2

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index c82c90167a..d092292e51 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,7 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
-;; Version: 4.0.1
+;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-lisp-auth-source-pass.el-auth-source-pass-get-Add-au.patch --]
[-- Type: text/x-patch, Size: 758 bytes --]

From b154934aa2c16ece1a440a65a43d74ed71a1a820 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 6 Nov 2018 14:45:20 +0100
Subject: [PATCH 03/12] * lisp/auth-source-pass.el (auth-source-pass-get): Add
 autoload

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index d092292e51..4fcb1015e7 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -98,6 +98,7 @@ auth-source-pass-backend-parse
   (advice-add 'auth-source-backend-parse :before-until #'auth-source-pass-backend-parse))
 
 \f
+;;;###autoload
 (defun auth-source-pass-get (key entry)
   "Return the value associated to KEY in the password-store entry ENTRY.
 
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Add-auth-source-pass-filename-option.patch --]
[-- Type: text/x-patch, Size: 2074 bytes --]

From e5e37fb1ddc6aba0f673c4dd9e1c54cb5783ce0a Mon Sep 17 00:00:00 2001
From: galaunay <gaby.launay@tutanota.com>
Date: Sun, 13 Jan 2019 21:30:53 +0000
Subject: [PATCH 04/12] Add auth-source-pass-filename option

* lisp/auth-source-pass.el (auth-source-pass):
(auth-source-pass-filename): Add option to specify a customized
password-store path.
(auth-source-pass--read-entry):
(auth-source-pass-entries): Use the new option instead of hard-coded
`~/.password-store'.
---
 lisp/auth-source-pass.el | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4fcb1015e7..ce6aea682e 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -38,6 +38,15 @@
 (require 'auth-source)
 (require 'url-parse)
 
+(defgroup auth-source-pass nil
+  "password-store integration within auth-source."
+  :prefix "auth-source-pass-"
+  :group 'auth-source)
+
+(defcustom auth-source-pass-filename "~/.password-store"
+  "Path to the password-store folder."
+  :type 'directory)
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -121,7 +130,7 @@ auth-source-pass--read-entry
   (with-temp-buffer
     (insert-file-contents (expand-file-name
                            (format "%s.gpg" entry)
-                           "~/.password-store"))
+                           auth-source-pass-filename))
     (buffer-substring-no-properties (point-min) (point-max))))
 
 (defun auth-source-pass-parse-entry (entry)
@@ -188,7 +197,7 @@ auth-source-pass--entry-valid-p
 ;; in Emacs
 (defun auth-source-pass-entries ()
   "Return a list of all password store entries."
-  (let ((store-dir (expand-file-name "~/.password-store/")))
+  (let ((store-dir (expand-file-name auth-source-pass-filename)))
     (mapcar
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-Add-auth-source-pass-path-option.patch --]
[-- Type: text/x-patch, Size: 2054 bytes --]

From d46e8dd1bbc3a1c7ac6506e3e6cfe3d87e57e99e Mon Sep 17 00:00:00 2001
From: galaunay <gaby.launay@tutanota.com>
Date: Sun, 13 Jan 2019 21:30:53 +0000
Subject: [PATCH 04/12] Add auth-source-pass-path option

* lisp/auth-source-pass.el (auth-source-pass):
(auth-source-pass-path): Add option to specify a customized
password-store path.
(auth-source-pass--read-entry):
(auth-source-pass-entries): Use the new option instead of hard-coded
`~/.password-store'.
---
 lisp/auth-source-pass.el | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4fcb1015e7..a029946f6c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -38,6 +38,15 @@
 (require 'auth-source)
 (require 'url-parse)
 
+(defgroup auth-source-pass nil
+  "password-store integration within auth-source."
+  :prefix "auth-source-pass-"
+  :group 'auth-source)
+
+(defcustom auth-source-pass-path "~/.password-store"
+  "Path to the password-store folder."
+  :type 'directory)
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -121,7 +130,7 @@ auth-source-pass--read-entry
   (with-temp-buffer
     (insert-file-contents (expand-file-name
                            (format "%s.gpg" entry)
-                           "~/.password-store"))
+                           auth-source-pass-path))
     (buffer-substring-no-properties (point-min) (point-max))))
 
 (defun auth-source-pass-parse-entry (entry)
@@ -188,7 +197,7 @@ auth-source-pass--entry-valid-p
 ;; in Emacs
 (defun auth-source-pass-entries ()
   "Return a list of all password store entries."
-  (let ((store-dir (expand-file-name "~/.password-store/")))
+  (let ((store-dir (expand-file-name auth-source-pass-path)))
     (mapcar
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-Add-auth-source-pass-port-separator-option.patch --]
[-- Type: text/x-patch, Size: 3397 bytes --]

From c35f5f760e652f56f61b3ec919d1a25e8234d02c Mon Sep 17 00:00:00 2001
From: Iku Iwasa <iku.iwasa@gmail.com>
Date: Sun, 7 Apr 2019 17:59:59 +0900
Subject: [PATCH 05/12] Add auth-source-pass-port-separator option

* lisp/auth-source-pass.el (auth-source-pass-port-separator): New
option to specify separator between host and port, default to
colon (":").
(auth-source-pass--find-match-unambiguous): Adapt to make use of the
new variable.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            | 16 +++++++++++++---
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index ce6aea682e..28c5b032f0 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -47,6 +47,10 @@ auth-source-pass-filename
   "Path to the password-store folder."
   :type 'directory)
 
+(defcustom auth-source-pass-port-separator ":"
+  "Separator string between host and port in entry filename."
+  :type 'string)
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -252,9 +256,15 @@ auth-source-pass--find-match-unambiguous
 
 HOSTNAME should not contain any username or port number."
   (or
-   (and user port (auth-source-pass--find-one-by-entry-name (format "%s@%s:%s" user hostname port) user))
-   (and user (auth-source-pass--find-one-by-entry-name (format "%s@%s" user hostname) user))
-   (and port (auth-source-pass--find-one-by-entry-name (format "%s:%s" hostname port) nil))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
+                   user))
+   (and user (auth-source-pass--find-one-by-entry-name
+              (format "%s@%s" user hostname)
+              user))
+   (and port (auth-source-pass--find-one-by-entry-name
+              (format "%s%s%s" hostname auth-source-pass-port-separator port)
+              nil))
    (auth-source-pass--find-one-by-entry-name hostname user)
    ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
    (let ((components (split-string hostname "\\.")))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ab9ef92c14..ae7a696bc6 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -186,6 +186,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
                    "host.com"))))
 
+(ert-deftest auth-source-pass-find-host-with-port ()
+  (auth-source-pass--with-store '(("host.com:443"))
+    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                   "host.com:443"))))
+
+(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("host.com#443"))
+      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                     "host.com#443")))))
+
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
   (declare (indent 1))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0006-Fix-auth-source-pass-to-search-for-hostname-port-use.patch --]
[-- Type: text/x-patch, Size: 2729 bytes --]

From 59610ffd617d7de103f0a79ab5b5128ac31dbe50 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Thu, 14 Feb 2019 10:27:31 -0800
Subject: [PATCH 06/12] Fix auth-source-pass to search for
 hostname:port/username

auth-source-pass supports entries with username either prefixed to the
hostname with an @ as separator or in a subdirectory under the
hostname.  This was true when there was no port or service included in
the name, but got broken with the introduction of
auth-source-pass-port-separator.

* lisp/auth-source-pass.el (auth-source-pass--find-match-unambiguous): Fix
to match hostname:port/username.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            |  3 +++
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 14 insertions(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 28c5b032f0..328070f3b6 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -259,6 +259,9 @@ auth-source-pass--find-match-unambiguous
    (and user port (auth-source-pass--find-one-by-entry-name
                    (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
                    user))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
+                   user))
    (and user (auth-source-pass--find-one-by-entry-name
               (format "%s@%s" user hostname)
               user))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ae7a696bc6..1539d9611f 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -144,6 +144,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
                    nil))))
 
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+  (auth-source-pass--with-store '(("bar.com:443/someone"))
+    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                   "bar.com:443/someone"))))
+
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("bar.com#443/someone"))
+      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                     "bar.com#443/someone")))))
+
 (ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
   (auth-source-pass--with-store '(("foo.com/bar"))
     (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0007-Split-out-the-attribute-retrieval-form-auth-source-p.patch --]
[-- Type: text/x-patch, Size: 3053 bytes --]

From 8919de231e88c93cb89fdbb1f1d2cb0cea008d09 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Tue, 30 Apr 2019 07:52:14 -0700
Subject: [PATCH 07/12] Split out the attribute retrieval form
 auth-source-pass-get

Eliminate the need to repeatedly retrieve and parse the data for the
entry.  This is generally a good thing since it eliminates repetitions
of the same crypto and parsing operations.  It is especially valuable
when protecting an entry with a yubikey with touch required for crypto
operations as it eliminates the need to touch the yubikey sensor for
each attribute retrieved.

* lisp/auth-source-pass.el (auth-source-pass-get): Extract some code to
`auth-source-pass--get-attr'.
(auth-source-pass--get-attr): New function to get a field value from a
parsed entry.
(auth-source-pass--build-result): Make use of
`auth-source-pass--get-attr` to avoid repeated parsing.
---
 lisp/auth-source-pass.el | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 328070f3b6..665c50f6bf 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -76,11 +76,12 @@ auth-source-pass--build-result
   "Build auth-source-pass entry matching HOST, PORT and USER."
   (let ((entry (auth-source-pass--find-match host user port)))
     (when entry
-      (let ((retval (list
-                     :host host
-                     :port (or (auth-source-pass-get "port" entry) port)
-                     :user (or (auth-source-pass-get "user" entry) user)
-                     :secret (lambda () (auth-source-pass-get 'secret entry)))))
+      (let* ((entry-data (auth-source-pass-parse-entry entry))
+             (retval (list
+                      :host host
+                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -125,9 +126,18 @@ auth-source-pass-get
 key1: value1
 key2: value2"
   (let ((data (auth-source-pass-parse-entry entry)))
-    (or (cdr (assoc key data))
-        (and (string= key "user")
-             (cdr (assoc "username" data))))))
+    (auth-source-pass--get-attr key data)))
+
+(defun auth-source-pass--get-attr (key entry-data)
+  "Return the value associated to KEY in data from an already parsed entry.
+
+ENTRY-DATA is the data from a parsed password-store entry.
+The key used to retrieve the password is the symbol `secret'.
+
+See `auth-source-pass-get'."
+  (or (cdr (assoc key entry-data))
+      (and (string= key "user")
+           (cdr (assoc "username" entry-data)))))
 
 (defun auth-source-pass--read-entry (entry)
   "Return a string with the file content of ENTRY."
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0008-Minimize-entry-parsing-in-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 33610 bytes --]

From e9943bb20632071b07232926bc62052b94788978 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sun, 5 May 2019 20:21:43 -0700
Subject: [PATCH 08/12] Minimize entry parsing in auth-source-pass

Prior to this commit, while searching for the most applicable entry
password-store entries were decrypted and parsed to ensure they were
valid.  The entries were parsed in the order they were found on the
filesystem and all applicable entries would be decrypted and parsed,
which varied based on the contents of the password-store and the entry
to be found.

This is fine when the GPG key is cached and each entry can be
decrypted without user interaction.  However, for security some people
have their GPG on a hardware token like a Yubikey setup so that they
have to touch a sensor on the toke for every cryptographic operation,
in which case it becomes inconvenient as each attempt to find an entry
requires a variable number of touches of the hardware token.

The implementation already assumes that names which contain more of
the information in the search key should be preferred so there is an
ordering of preference of applicable entries.  If the decrypt and
parsing is removed from the initial identification of applicable
entries in the store then in most cases a single decrypt and parse of
the most preferred entry will suffice, improving the experience for
hardware token users that require interaction with the token.

This commit implements that strategy.  It is in spirit a refactor of
the existing code.  The core of the change is the function
auth-source-pass--applicable-entries, which generates an ordered list
of regular expression matchers for all possible names that could be in
the password-store for the entry to be found and then makes a pass
over the password-store entry names accumulating the matching entries
in a list after the regexp that matched.  This implementation ensures
the password-store entry list still only has to be scanned once.

The existing auth-source-pass--find-match-unambiguous was modified to
use this new function to obtain candidate entries and then parse them
one by one until an entry containing the desired information is
located.  When complete it now returns the parsed data of the entry
instead of the entry name so that the information can be used directly
to construct the auth-source response.

* lisp/auth-source-pass.el: Private functions were refactored to
reduce the number of decryption operations.
* test/lisp/auth-source-pass-tests.el: One test case was added to the
test suite to verify that only the minimal number of entries are
parsed in common cases.  The
auth-source-pass-only-return-entries-that-can-be-open test case had to
be re-implemented because the function it was used eliminated as the
functionality is provided elsewhere.  All the other fairly substantial
changes to the test suite are the result of mechanical changes that
were required to adapt to auth-source-pass--find-match returning the
data from a parsed password-store entry instead of the entry name.
---
 lisp/auth-source-pass.el            | 209 ++++++++++++----------
 test/lisp/auth-source-pass-tests.el | 264 +++++++++++++++++-----------
 2 files changed, 281 insertions(+), 192 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 665c50f6bf..fd727ec74d 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -74,14 +74,13 @@ auth-source-pass-port-separator
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
-  (let ((entry (auth-source-pass--find-match host user port)))
-    (when entry
-      (let* ((entry-data (auth-source-pass-parse-entry entry))
-             (retval (list
-                      :host host
-                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
-                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
-                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
+  (let ((entry-data (auth-source-pass--find-match host user port)))
+    (when entry-data
+      (let ((retval (list
+                     :host host
+                     :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                     :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                     :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -180,33 +179,6 @@ auth-source-pass--do-debug
          (cons (concat "auth-source-pass: " (car msg))
                (cdr msg))))
 
-(defun auth-source-pass--select-one-entry (entries user)
-  "Select one entry from ENTRIES by searching for a field matching USER."
-  (let ((number (length entries))
-        (entry-with-user
-         (and user
-              (seq-find (lambda (entry)
-                          (string-equal (auth-source-pass-get "user" entry) user))
-                        entries))))
-    (auth-source-pass--do-debug "found %s matches: %s" number
-                                (mapconcat #'identity entries ", "))
-    (if entry-with-user
-        (progn
-          (auth-source-pass--do-debug "return %s as it contains matching user field"
-                                      entry-with-user)
-          entry-with-user)
-      (auth-source-pass--do-debug "return %s as it is the first one" (car entries))
-      (car entries))))
-
-(defun auth-source-pass--entry-valid-p (entry)
-  "Return t iff ENTRY can be opened.
-Also displays a warning if not.  This function is slow, don't call it too
-often."
-  (if (auth-source-pass-parse-entry entry)
-      t
-    (auth-source-pass--do-debug "entry '%s' is not valid" entry)
-    nil))
-
 ;; TODO: add tests for that when `assess-with-filesystem' is included
 ;; in Emacs
 (defun auth-source-pass-entries ()
@@ -216,37 +188,8 @@ auth-source-pass-entries
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
 
-(defun auth-source-pass--find-all-by-entry-name (entryname user)
-  "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME.
-Only return valid entries as of `auth-source-pass--entry-valid-p'."
-  (seq-filter (lambda (entry)
-                (and
-                 (or
-                  (let ((components-host-user
-                         (member entryname (split-string entry "/"))))
-                    (and (= (length components-host-user) 2)
-                         (string-equal user (cadr components-host-user))))
-                  (string-equal entryname (file-name-nondirectory entry)))
-                 (auth-source-pass--entry-valid-p entry)))
-              (auth-source-pass-entries)))
-
-(defun auth-source-pass--find-one-by-entry-name (entryname user)
-  "Search the store for an entry matching ENTRYNAME.
-If USER is non nil, give precedence to entries containing a user field
-matching USER."
-  (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
-                              entryname
-                              user)
-  (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user)))
-    (pcase (length matching-entries)
-      (0 (auth-source-pass--do-debug "no match found")
-         nil)
-      (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries))
-         (car matching-entries))
-      (_ (auth-source-pass--select-one-entry matching-entries user)))))
-
 (defun auth-source-pass--find-match (host user port)
-  "Return a password-store entry name matching HOST, USER and PORT.
+  "Return password-store entry data matching HOST, USER and PORT.
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
@@ -260,33 +203,123 @@ auth-source-pass--find-match
      (or port (number-to-string (url-port url))))))
 
 (defun auth-source-pass--find-match-unambiguous (hostname user port)
-  "Return a password-store entry name matching HOSTNAME, USER and PORT.
+  "Return password-store entry data matching HOSTNAME, USER and PORT.
 If many matches are found, return the first one.  If no match is found,
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (or
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
-                   user))
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
-                   user))
-   (and user (auth-source-pass--find-one-by-entry-name
-              (format "%s@%s" user hostname)
-              user))
-   (and port (auth-source-pass--find-one-by-entry-name
-              (format "%s%s%s" hostname auth-source-pass-port-separator port)
-              nil))
-   (auth-source-pass--find-one-by-entry-name hostname user)
-   ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
-   (let ((components (split-string hostname "\\.")))
-     (when (= (length components) 3)
-       ;; start from scratch
-       (auth-source-pass--find-match-unambiguous
-        (mapconcat 'identity (cdr components) ".")
-        user
-        port)))))
+  (cl-reduce
+   (lambda (result entries)
+     (or result
+         (pcase (length entries)
+           (0 nil)
+           (1 (auth-source-pass-parse-entry (car entries)))
+           (_ (auth-source-pass--select-from-entries entries user)))))
+   (auth-source-pass--matching-entries hostname user port)
+   :initial-value nil))
+
+(defun auth-source-pass--select-from-entries (entries user)
+  "Return best matching password-store entry data from ENTRIES.
+
+If USER is non nil, give precedence to entries containing a user field
+matching USER."
+  (cl-reduce
+   (lambda (result entry)
+     (let ((entry-data (auth-source-pass-parse-entry entry)))
+       (cond ((equal (auth-source-pass--get-attr "user" result) user)
+              result)
+             ((equal (auth-source-pass--get-attr "user" entry-data) user)
+              entry-data)
+             (t
+              result))))
+   entries
+   :initial-value (auth-source-pass-parse-entry (car entries))))
+
+(defun auth-source-pass--matching-entries (hostname user port)
+  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
+
+The result is a list of lists of password-store entries, where
+each sublist contains entries that actually exist in the
+password-store matching one of the entry name formats that
+auth-source-pass expects, most specific to least specific."
+  (let* ((entries-lists (mapcar
+                         #'cdr
+                         (auth-source-pass--accumulate-matches hostname user port)))
+         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
+    (if entries
+        (auth-source-pass--do-debug (format "found: %S" entries))
+      (auth-source-pass--do-debug "no matches found"))
+    entries-lists))
+
+(defun auth-source-pass--accumulate-matches (hostname user port)
+  "Accumulate matching password-store entries into sublists.
+
+Entries matching supported formats that combine HOSTNAME, USER, &
+PORT are accumulated into sublists where the car of each sublist
+is a regular expression for matching paths in the password-store
+and the remainder is the list of matching entries."
+  (let ((suffix-match-lists
+         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
+                 (auth-source-pass--generate-entry-suffixes hostname user port))))
+    (cl-reduce #'auth-source-pass--entry-reducer
+               (auth-source-pass-entries)
+               :initial-value suffix-match-lists)))
+
+(defun auth-source-pass--entry-reducer (match-lists entry)
+  "Match MATCH-LISTS sublists against ENTRY.
+
+The result is a copy of match-lists with the entry added to the
+end of any sublists for which the regular expression at the head
+of the list matches the entry name."
+  (mapcar (lambda (match-list)
+            (if (string-match (car match-list) entry)
+                (append match-list (list entry))
+              match-list))
+          match-lists))
+
+(defun auth-source-pass--generate-entry-suffixes (hostname user port)
+  "Return a list of possible entry path suffixes in the password-store.
+
+Based on the supported pathname patterns for HOSTNAME, USER, &
+PORT, return a list of possible suffixes for matching entries in
+the password-store."
+  (let ((domains (auth-source-pass--domains (split-string hostname "\\."))))
+    (seq-mapcat (lambda (n)
+                  (auth-source-pass--name-port-user-suffixes n user port))
+                domains)))
+
+(defun auth-source-pass--domains (name-components)
+  "Return a list of possible domain names matching the hostname.
+
+This function takes a list of NAME-COMPONENTS, the strings
+separated by periods in the hostname, and returns a list of full
+domain names containing the trailing sequences of those
+components, from longest to shortest."
+  (cl-maplist (lambda (components) (mapconcat #'identity components "."))
+              name-components))
+
+(defun auth-source-pass--name-port-user-suffixes (name user port)
+  "Return a list of possible path suffixes for NAME, USER, & PORT.
+
+The resulting list is ordered from most specifc to least
+specific, with paths matching all of NAME, USER, & PORT first,
+then NAME & USER, then NAME & PORT, then just NAME."
+  (seq-mapcat
+   #'identity
+   (list
+    (when (and user port)
+      (list
+       (format "%s@%s%s%s" user name auth-source-pass-port-separator port)
+       (format "%s%s%s/%s" name auth-source-pass-port-separator port user)))
+    (when user
+      (list
+       (format "%s@%s" user name)
+       (format "%s/%s" name user)))
+    (when port
+      (list
+       (format "%s%s%s" name auth-source-pass-port-separator port)))
+    (list
+     (format "%s" name)))))
 
 (provide 'auth-source-pass)
 ;;; auth-source-pass.el ends here
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 1539d9611f..2c28f79945 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -63,14 +63,19 @@ auth-source-pass--debug
 This function is intended to be set to `auth-source-debug`."
   (add-to-list 'auth-source-pass--debug-log (apply #'format msg) t))
 
+(defvar auth-source-pass--parse-log nil)
+
 (defmacro auth-source-pass--with-store (store &rest body)
   "Use STORE as password-store while executing BODY."
   (declare (indent 1))
-  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry) (lambda (entry) (cdr (cl-find entry ,store :key #'car :test #'string=))) )
-             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store)))
-             ((symbol-function 'auth-source-pass--entry-valid-p) (lambda (_entry) t)))
+  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+              (lambda (entry)
+                (add-to-list 'auth-source-pass--parse-log entry)
+                (cdr (cl-find entry ,store :key #'car :test #'string=))))
+             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store))))
      (let ((auth-source-debug #'auth-source-pass--debug)
-           (auth-source-pass--debug-log nil))
+           (auth-source-pass--debug-log nil)
+           (auth-source-pass--parse-log nil))
        ,@body)))
 
 (ert-deftest auth-source-pass-any-host ()
@@ -88,125 +93,184 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass-find-match-minimal-parsing ()
+  (let ((store-contents
+         '(("baz" ("secret" . "baz password"))
+           ("baz:123" ("secret" . "baz:123 password"))
+           ("baz/foo" ("secret" . "baz/foo password"))
+           ("foo@baz" ("secret" . "foo@baz password"))
+           ("baz:123/foo" ("secret" . "baz:123/foo password"))
+           ("foo@baz:123" ("secret" . "foo@baz:123 password"))
+           ("bar.baz" ("secret" . "bar.baz password"))
+           ("bar.baz:123" ("secret" . "bar.baz:123 password"))
+           ("bar.baz/foo" ("secret" . "bar.baz/foo password"))
+           ("foo@bar.baz" ("secret" . "foo@bar.baz password"))
+           ("bar.baz:123/foo" ("secret" . "bar.baz:123/foo password"))
+           ("foo@bar.baz:123" ("secret" . "foo@bar.baz:123 password")))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" "123")
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" nil)
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil "123")
+      (should (equal auth-source-pass--parse-log '("bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("baz"))))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store '(("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store '(("SomeUser@foo") ("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
+        ("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store '(("foo") ("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
+
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil ("bar.com") nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
   (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   "someone@bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-bad-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
   (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-prefer-full ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
   (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "foo.bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(("foo.bar.com") ("bar.com") nil)))))
 
 (ert-deftest auth-source-pass-dont-match-at-folder-name ()
   (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
   (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                   "bar.com:443/someone"))))
+    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                   '(nil ("bar.com:443/someone") nil nil nil nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                     "bar.com#443/someone")))))
-
-(ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
-  (auth-source-pass--with-store '(("foo.com/bar"))
-    (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-                   "foo.com/bar"))))
-
-(ert-deftest auth-source-pass-search-with-user-first ()
+      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                     '(nil ("bar.com#443/someone") nil nil nil nil
+                           nil nil nil nil nil nil))))))
+
+(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
+  (auth-source-pass--with-store
+      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
+    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo.com/bar password")))))
+
+(ert-deftest auth-source-pass-matching-entries-with-user-first ()
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--find-match "foo" "user" nil)
-                   "user@foo"))
-    (auth-source-pass--should-have-message-containing "Found 1 match")))
+    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
+                   '(("user@foo") nil ("foo"))))
+    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo" ("user" . "someone")))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "subdir/foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "subdir/foo password"))
+      (should (equal (auth-source-pass--get-attr "user" result)
+                     "someone")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store '(("foo" ("user" . "someone")) ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password") ("user" . "someone"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "the first one")))
-
-(ert-deftest auth-source-pass-make-divansantana-happy ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
+
+(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
   (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "smtp.host.com" "myusername@host.co.za" nil)
-                   "host.com"))))
+    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
+                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
 
 (ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
-                   "host.com"))))
+  (auth-source-pass--with-store
+      '(("host.com" ("secret" . "host.com password")))
+    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "host.com password")))))
 
-(ert-deftest auth-source-pass-find-host-with-port ()
+(ert-deftest auth-source-pass-matching-entries-host-with-port ()
   (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                   "host.com:443"))))
+    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                   '(nil nil nil nil ("host.com:443") nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                     "host.com#443")))))
+      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                     '(nil nil nil nil ("host.com#443") nil
+                           nil nil nil nil nil nil))))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -218,7 +282,8 @@ auth-source-pass--with-store-find-foo
        ,@body)))
 
 (ert-deftest auth-source-pass-build-result-return-parameters ()
-  (auth-source-pass--with-store-find-foo '(("foo"))
+  (auth-source-pass--with-store-find-foo
+      '(("foo" ("secret" . "foo password")))
     (let ((result (auth-source-pass--build-result "foo" 512 "user")))
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "user")))))
@@ -238,7 +303,9 @@ auth-source-pass--with-store-find-foo
 (ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
   (let (passed-host)
     (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port) (setq passed-host host))))
+               (lambda (host _user _port)
+                 (setq passed-host host)
+                 nil)))
       (auth-source-pass--build-result "https://user@host.com:123" nil nil)
       (should (equal passed-host "https://user@host.com:123"))
       (auth-source-pass--build-result "https://user@host.com" nil nil)
@@ -249,27 +316,16 @@ auth-source-pass--with-store-find-foo
       (should (equal passed-host "user@host.com:443")))))
 
 (ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (cl-letf (((symbol-function 'auth-source-pass-entries)
-             (lambda () '("foo.site.com" "bar.site.com" "mail/baz.site.com/scott")))
-            ((symbol-function 'auth-source-pass--entry-valid-p)
-             ;; only foo.site.com and "mail/baz.site.com/scott" are valid
-             (lambda (entry) (member entry '("foo.site.com" "mail/baz.site.com/scott")))))
-    (should (equal (auth-source-pass--find-all-by-entry-name "foo.site.com" "someuser")
-                   '("foo.site.com")))
-    (should (equal (auth-source-pass--find-all-by-entry-name "bar.site.com" "someuser")
-                   '()))
-    (should (equal (auth-source-pass--find-all-by-entry-name "baz.site.com" "scott")
-                   '("mail/baz.site.com/scott")))))
-
-(ert-deftest auth-source-pass-entry-is-not-valid-when-unreadable ()
-  (cl-letf (((symbol-function 'auth-source-pass--read-entry)
-             (lambda (entry)
-               ;; only foo is a valid entry
-               (if (string-equal entry "foo")
-                   "password"
-                 nil))))
-    (should (auth-source-pass--entry-valid-p "foo"))
-    (should-not (auth-source-pass--entry-valid-p "bar"))))
+  (auth-source-pass--with-store
+      '(("foo.site.com" ("secret" . "foo.site.com password"))
+        ("bar.site.com") ; An entry name with no data is invalid
+        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
+    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
+                   '(("secret" . "foo.site.com password"))))
+    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
+                   nil))
+    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
+                   '(("secret" . "mail/baz.site.com/scott password"))))))
 
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0009-lisp-auth-source-pass.el-Add-Keith-Amidon-to-authors.patch --]
[-- Type: text/x-patch, Size: 755 bytes --]

From d56fa7d36aa16ae0651f98380ff4a69d3bf2dfdd Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sat, 11 May 2019 08:22:56 -0700
Subject: [PATCH 09/12] * lisp/auth-source-pass.el: Add Keith Amidon to authors

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index fd727ec74d..029d845d12 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,6 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
+;;         Keith Amidon <camalot@picnicpark.org>
 ;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0010-Refactoring-of-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 34944 bytes --]

From 8a68e8d8b5a864a212133cec9573ec547c72dbcc Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 14 May 2019 05:50:59 +0200
Subject: [PATCH 10/12] Refactoring of auth-source-pass

* lisp/auth-source-pass.el: Refactoring.
* test/lisp/auth-source-pass-tests.el: Refactoring and adding some
more tests.
---
 lisp/auth-source-pass.el            | 108 +++----
 test/lisp/auth-source-pass-tests.el | 464 +++++++++++++++++-----------
 2 files changed, 331 insertions(+), 241 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 029d845d12..3aeb26a7ea 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -194,10 +194,17 @@ auth-source-pass--find-match
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
+  (apply #'auth-source-pass--find-match-unambiguous (auth-source-pass--disambiguate host user port)))
+
+(defun auth-source-pass--disambiguate (host &optional user port)
+  "Return (HOST USER PORT) after disambiguation.
+Disambiguate between having user provided inside HOST (e.g.,
+user@server.com) and inside USER by giving priority to USER.
+Same for PORT."
   (let* ((url (url-generic-parse-url (if (string-match-p ".*://" host)
                                          host
                                        (format "https://%s" host)))))
-    (auth-source-pass--find-match-unambiguous
+    (list
      (or (url-host url) host)
      (or user (url-user url))
      ;; url-port returns 443 (because of the https:// above) by default
@@ -209,74 +216,49 @@ auth-source-pass--find-match-unambiguous
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (cl-reduce
-   (lambda (result entries)
-     (or result
-         (pcase (length entries)
-           (0 nil)
-           (1 (auth-source-pass-parse-entry (car entries)))
-           (_ (auth-source-pass--select-from-entries entries user)))))
-   (auth-source-pass--matching-entries hostname user port)
-   :initial-value nil))
+  (let ((all-entries (auth-source-pass-entries))
+        (suffixes (auth-source-pass--generate-entry-suffixes hostname user port)))
+    (auth-source-pass--do-debug "searching for entries matching hostname=%S, user=%S, port=%S"
+                                hostname (or user "") (or port ""))
+    (auth-source-pass--do-debug "corresponding suffixes to search for: %S" suffixes)
+    (catch 'auth-source-pass-break
+      (dolist (suffix suffixes)
+        (let* ((matching-entries (auth-source-pass--entries-matching-suffix suffix all-entries))
+               (best-entry-data (auth-source-pass--select-from-entries matching-entries user)))
+          (pcase (length matching-entries)
+            (0 (auth-source-pass--do-debug "found no entries matching %S" suffix))
+            (1 (auth-source-pass--do-debug "found 1 entry matching %S: %S"
+                                           suffix
+                                           (car matching-entries)))
+            (_ (auth-source-pass--do-debug "found %s entries matching %S: %S"
+                                           (length matching-entries)
+                                           suffix
+                                           matching-entries)))
+          (when best-entry-data
+            (throw 'auth-source-pass-break best-entry-data)))))))
 
 (defun auth-source-pass--select-from-entries (entries user)
   "Return best matching password-store entry data from ENTRIES.
 
 If USER is non nil, give precedence to entries containing a user field
 matching USER."
-  (cl-reduce
-   (lambda (result entry)
-     (let ((entry-data (auth-source-pass-parse-entry entry)))
-       (cond ((equal (auth-source-pass--get-attr "user" result) user)
-              result)
-             ((equal (auth-source-pass--get-attr "user" entry-data) user)
-              entry-data)
-             (t
-              result))))
-   entries
-   :initial-value (auth-source-pass-parse-entry (car entries))))
-
-(defun auth-source-pass--matching-entries (hostname user port)
-  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
-
-The result is a list of lists of password-store entries, where
-each sublist contains entries that actually exist in the
-password-store matching one of the entry name formats that
-auth-source-pass expects, most specific to least specific."
-  (let* ((entries-lists (mapcar
-                         #'cdr
-                         (auth-source-pass--accumulate-matches hostname user port)))
-         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
-    (if entries
-        (auth-source-pass--do-debug (format "found: %S" entries))
-      (auth-source-pass--do-debug "no matches found"))
-    entries-lists))
-
-(defun auth-source-pass--accumulate-matches (hostname user port)
-  "Accumulate matching password-store entries into sublists.
-
-Entries matching supported formats that combine HOSTNAME, USER, &
-PORT are accumulated into sublists where the car of each sublist
-is a regular expression for matching paths in the password-store
-and the remainder is the list of matching entries."
-  (let ((suffix-match-lists
-         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
-                 (auth-source-pass--generate-entry-suffixes hostname user port))))
-    (cl-reduce #'auth-source-pass--entry-reducer
-               (auth-source-pass-entries)
-               :initial-value suffix-match-lists)))
-
-(defun auth-source-pass--entry-reducer (match-lists entry)
-  "Match MATCH-LISTS sublists against ENTRY.
-
-The result is a copy of match-lists with the entry added to the
-end of any sublists for which the regular expression at the head
-of the list matches the entry name."
-  (mapcar (lambda (match-list)
-            (if (string-match (car match-list) entry)
-                (append match-list (list entry))
-              match-list))
-          match-lists))
+  (let (fallback)
+    (catch 'auth-source-pass-break
+      (dolist (entry entries fallback)
+        (let ((entry-data (auth-source-pass-parse-entry entry)))
+          (when (and entry-data (not fallback))
+            (setq fallback entry-data)
+            (when (or (not user) (equal (auth-source-pass--get-attr "user" entry-data) user))
+              (throw 'auth-source-pass-break entry-data))))))))
+
+(defun auth-source-pass--entries-matching-suffix (suffix entries)
+  "Return entries matching SUFFIX.
+If ENTRIES is nil, use the result of calling `auth-source-pass-entries' instead."
+  (cl-remove-if-not
+   (lambda (entry) (string-match-p
+               (format "\\(^\\|/\\)%s$" (regexp-quote suffix))
+               entry))
+   (or entries (auth-source-pass-entries))))
 
 (defun auth-source-pass--generate-entry-suffixes (hostname user port)
   "Return a list of possible entry path suffixes in the password-store.
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 2c28f79945..e9d4cde4d2 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -52,11 +52,21 @@
 (defvar auth-source-pass--debug-log nil
   "Contains a list of all messages passed to `auth-source-do-debug`.")
 
-(defun auth-source-pass--should-have-message-containing (regexp)
-  "Assert that at least one `auth-source-do-debug` matched REGEXP."
-  (should (seq-find (lambda (message)
-                      (string-match regexp message))
-                    auth-source-pass--debug-log)))
+(defun auth-source-pass--have-message-matching (regexp)
+  "Return non-nil iff at least one `auth-source-do-debug` match REGEXP."
+  (seq-find (lambda (message)
+              (string-match regexp message))
+            auth-source-pass--debug-log))
+
+(defun auth-source-pass--explain--have-message-matching (regexp)
+  "Explainer function for `auth-source-pass--have-message-matching'.
+REGEXP is the same as in `auth-source-pass--have-message-matching'."
+  `(regexp
+    ,regexp
+    messages
+    ,(mapconcat #'identity auth-source-pass--debug-log "\n- ")))
+
+(put #'auth-source-pass--have-message-matching 'ert-explainer #'auth-source-pass--explain--have-message-matching)
 
 (defun auth-source-pass--debug (&rest msg)
   "Format MSG and add that to `auth-source-pass--debug-log`.
@@ -78,6 +88,82 @@ auth-source-pass--with-store
            (auth-source-pass--parse-log nil))
        ,@body)))
 
+(defun auth-source-pass--explain-match-entry-p (entry hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-entry-p'.
+
+ENTRY, HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-entry-p'."
+  `(entry
+    ,entry
+    store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-entry-p 'ert-explainer #'auth-source-pass--explain-match-entry-p)
+
+(defun auth-source-pass--includes-sorted-entries (entries hostname &optional user port)
+  "Return non-nil iff ENTRIES matching the parameters are found in store.
+ENTRIES should be sorted from most specific to least specific.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (if (seq-empty-p entries)
+      t
+    (and
+     (auth-source-pass-match-entry-p (car entries) hostname user port)
+     (auth-source-pass--includes-sorted-entries (cdr entries) hostname user port))))
+
+(defun auth-source-pass--explain-includes-sorted-entries (entries hostname &optional user port)
+  "Explainer function for `auth-source-pass--includes-sorted-entries'.
+
+ENTRIES, HOSTNAME, USER and PORT are the same as in `auth-source-pass--includes-sorted-entries'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)
+    entries
+    ,entries))
+
+(put 'auth-source-pass--includes-sorted-entries 'ert-explainer #'auth-source-pass--explain-includes-sorted-entries)
+
+(defun auth-source-pass--explain-match-any-entry-p (hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-any-entry-p'.
+
+HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-any-entry-p'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-any-entry-p 'ert-explainer #'auth-source-pass--explain-match-any-entry-p)
+
+(defun auth-source-pass--matching-entries (hostname &optional user port)
+  "Return password-store entries matching HOSTNAME, USER, PORT.
+
+The result is a list of lists of password-store entries.  Each
+sublist contains the password-store entries whose names match a
+suffix in `auth-source-pass--generate-entry-suffixes'.  The
+result is ordered the same way as the suffixes."
+  (let ((entries (auth-source-pass-entries)))
+    (mapcar (lambda (suffix) (auth-source-pass--entries-matching-suffix suffix entries))
+            (auth-source-pass--generate-entry-suffixes hostname user port))))
+
+(defun auth-source-pass-match-entry-p (entry hostname &optional user port)
+  "Return non-nil iff an ENTRY matching the parameters is found in store.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if
+   (lambda (entries) (cl-find entry entries :test #'string=))
+   (auth-source-pass--matching-entries hostname user port)))
+
+(defun auth-source-pass-match-any-entry-p (hostname &optional user port)
+  "Return non-nil iff there is at least one entry matching the parameters.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if #'identity (auth-source-pass--matching-entries hostname user port)))
+
 (ert-deftest auth-source-pass-any-host ()
   (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
                                   ("bar"))
@@ -93,6 +179,101 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass--disambiguate-extract-host-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo")) "foo"))
+  ;; only user
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo")) "foo"))
+  ;; only port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80")) "foo"))
+  ;; both user and port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80")) "foo"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80/path")) "foo")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-user-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo")) nil))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo")) "user"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80")) nil))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80")) "user"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path")) "user")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-user-parameter ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo" "user2")) "user2"))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo" "user2")) "user2"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80" "user2")) "user2"))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80" "user2")) "user2"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path" "user2")) "user2")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-port-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo")) "443"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo")) "443"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80")) "80"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80")) "80"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path")) "80"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path")) "80")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-port-parameter ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo" "user2" "8080")) "8080"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo" "user2" "8080")) "8080"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80" "user2" "8080")) "8080"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80" "user2" "8080")) "8080"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path" "user2" "8080")) "8080")))
+
 (ert-deftest auth-source-pass-find-match-minimal-parsing ()
   (let ((store-contents
          '(("baz" ("secret" . "baz password"))
@@ -121,156 +302,92 @@ auth-source-pass--with-store
       (should (equal auth-source-pass--parse-log '("bar.baz"))))
     (auth-source-pass--with-store store-contents
       (auth-source-pass--find-match "baz" nil nil)
-      (should (equal auth-source-pass--parse-log '("baz"))))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
-        ("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
+      (should (equal auth-source-pass--parse-log '("baz"))))
+    (auth-source-pass--with-store
+        '(("dir1/bar.com" ("key" . "val"))
+          ("dir2/bar.com" ("key" . "val"))
+          ("dir3/bar.com" ("key" . "val")))
+      (auth-source-pass--find-match "bar.com" nil nil)
+      (should (= (length auth-source-pass--parse-log) 1)))))
+
+(ert-deftest auth-source-pass--find-match-return-parsed-data ()
+  (auth-source-pass--with-store '(("bar.com" ("key" . "val")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil))))
+  (auth-source-pass--with-store '(("dir1/bar.com" ("key1" . "val1")) ("dir2/bar.com" ("key2" . "val2")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
-  (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
-  (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
-  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(("foo.bar.com") ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-dont-match-at-folder-name ()
-  (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
-  (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                   '(nil ("bar.com:443/someone") nil nil nil nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com"))
+    ;; match even if sub-domain is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "foo.bar.com"))
+    ;; match even if a user is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user"))
+    ;; match even if user as an @ sign
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user@someplace"))
+    ;; match even if a port is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" nil "8080"))
+    ;; match even if a user and a port are asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user" "8080"))
+    ;; don't match if a '.' is replaced with another character
+    (auth-source-pass--with-store '(("barXcom"))
+      (should-not (auth-source-pass-match-any-entry-p "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-username ()
+  (auth-source-pass--with-store '(("user@foo"))
+    (should (auth-source-pass-match-entry-p "user@foo" "foo" "user")))
+  ;; match even if sub-domain is asked for
+  (auth-source-pass--with-store '(("user@bar.com"))
+    (should (auth-source-pass-match-entry-p "user@bar.com" "foo.bar.com" "user")))
+  ;; don't match if no user is asked for
+  (auth-source-pass--with-store '(("user@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo")))
+  ;; don't match if user is different
+  (auth-source-pass--with-store '(("user1@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo" "user2")))
+  ;; don't match if sub-domain is asked for but user is different
+  (auth-source-pass--with-store '(("user1@bar.com"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.bar.com" "user2"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-port ()
+  (auth-source-pass--with-store '(("bar.com:8080"))
+    (should (auth-source-pass-match-entry-p "bar.com:8080" "bar.com" nil "8080"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-slash ()
+  ;; match if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com/user"))
+    (should (auth-source-pass-match-entry-p "foo.com/user" "foo.com" "user")))
+  ;; match with port if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com:8080/user"))
+    (should (auth-source-pass-match-entry-p "foo.com:8080/user" "foo.com" "user" "8080")))
+  ;; don't match if entry filename doesn't match user
+  (auth-source-pass--with-store '(("foo.com/baz"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.com" "user"))))
+
+(ert-deftest auth-source-pass-matching-entries-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                     '(nil ("bar.com#443/someone") nil nil nil nil
-                           nil nil nil nil nil nil))))))
-
-(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
-  (auth-source-pass--with-store
-      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
-    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo.com/bar password")))))
-
-(ert-deftest auth-source-pass-matching-entries-with-user-first ()
+      (should (auth-source-pass-match-entry-p "bar.com#443/someone" "bar.com" "someone" "443")))))
+
+(ert-deftest auth-source-pass--matching-entries-sort-results ()
+  (auth-source-pass--with-store '(("user@foo") ("foo"))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; same, but store is reversed
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
-                   '(("user@foo") nil ("foo"))))
-    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "subdir/foo password"))
-      (should (equal (auth-source-pass--get-attr "user" result)
-                     "someone")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password") ("user" . "someone"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
-                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
-
-(ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store
-      '(("host.com" ("secret" . "host.com password")))
-    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "host.com password")))))
-
-(ert-deftest auth-source-pass-matching-entries-host-with-port ()
-  (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                   '(nil nil nil nil ("host.com:443") nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
-  (let ((auth-source-pass-port-separator "#"))
-    (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                     '(nil nil nil nil ("host.com#443") nil
-                           nil nil nil nil nil nil))))))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; with sub-domain
+  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
+    (should (auth-source-pass--includes-sorted-entries '("foo.bar.com" "bar.com") "foo.bar.com")))
+  ;; matching user in the entry data takes priority
+  (auth-source-pass--with-store '(("dir1/bar.com") ("dir2/bar.com" ("user" . "user")))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user")))
+  ;; same, but store is reversed
+  (auth-source-pass--with-store '(("dir2/bar.com" ("user" . "user")) ("dir1/bar.com"))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user"))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -300,33 +417,6 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "anuser")))))
 
-(ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
-  (let (passed-host)
-    (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port)
-                 (setq passed-host host)
-                 nil)))
-      (auth-source-pass--build-result "https://user@host.com:123" nil nil)
-      (should (equal passed-host "https://user@host.com:123"))
-      (auth-source-pass--build-result "https://user@host.com" nil nil)
-      (should (equal passed-host "https://user@host.com"))
-      (auth-source-pass--build-result "user@host.com" nil nil)
-      (should (equal passed-host "user@host.com"))
-      (auth-source-pass--build-result "user@host.com:443" nil nil)
-      (should (equal passed-host "user@host.com:443")))))
-
-(ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (auth-source-pass--with-store
-      '(("foo.site.com" ("secret" . "foo.site.com password"))
-        ("bar.site.com") ; An entry name with no data is invalid
-        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
-    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
-                   '(("secret" . "foo.site.com password"))))
-    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
-                   nil))
-    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
-                   '(("secret" . "mail/baz.site.com/scott password"))))))
-
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
     (auth-source-pass-enable)
@@ -334,6 +424,24 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :user) "someone"))
       (should (equal (plist-get result :host) "gitlab.com")))))
 
+(ert-deftest auth-source-pass-prints-meaningful-debug-log ()
+  (auth-source-pass--with-store '()
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "entries matching hostname=\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "corresponding suffixes to search for: .*\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "found no entries matching \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 1 entry matching \"gitlab.com\": \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("a/gitlab.com") ("b/gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 2 entries matching \"gitlab.com\": (\"a/gitlab.com\" \"b/gitlab.com\")"))))
+
 (provide 'auth-source-pass-tests)
 
 ;;; auth-source-pass-tests.el ends here
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0011-lisp-auth-source-pass.el-Version-5.0.0.patch --]
[-- Type: text/x-patch, Size: 794 bytes --]

From 4f574f31867b2d2aa69d5e8d15ff22c132ca91a0 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 28 May 2019 08:46:41 +0200
Subject: [PATCH 11/12] * lisp/auth-source-pass.el: Version 5.0.0

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 3aeb26a7ea..18cca0095a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -5,7 +5,7 @@
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
 ;;         Keith Amidon <camalot@picnicpark.org>
-;; Version: 4.0.2
+;; Version: 5.0.0
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #14: 0012-etc-NEWS-Describe-changes-to-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 1194 bytes --]

From ac493f969c7ce910ec0aa73bd8caccb015ff06cb Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sun, 2 Jun 2019 11:08:40 +0200
Subject: [PATCH 12/12] * etc/NEWS: Describe changes to auth-source-pass

---
 etc/NEWS | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index 975fab495a..b89a3399cb 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1485,6 +1485,21 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
 value.  Auto Revert mode can use this information to avoid polling the
 buffer periodically when 'auto-revert-avoid-polling' is non-nil.
 
+** auth-source-pass
+
+*** New customizable variable 'auth-source-pass-filename'.
+Allows setting the path to the password-store, defaults to
+~/.password-store.
+
+*** New customizable variable 'auth-source-pass-port-separator'.
+Specifies separator between host and port, defaults to colon ":".
+
+*** Minimize the number of decryptions during password lookup.
+This makes the package usable with physical tokens requiring touching
+a sensor for every decryption.
+
+*** 'auth-source-pass-get' is now autoloaded.
+
 \f
 * New Modes and Packages in Emacs 27.1
 
-- 
2.21.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-08 15:47   ` Damien Cassou
@ 2019-06-08 16:02     ` Eli Zaretskii
  2019-06-08 22:38     ` Noam Postavsky
  2019-06-13 19:59     ` Damien Cassou
  2 siblings, 0 replies; 13+ messages in thread
From: Eli Zaretskii @ 2019-06-08 16:02 UTC (permalink / raw)
  To: Damien Cassou
  Cc: magnus.henoch, tzz, npostavs, iku.iwasa, camalot, gaby.launay,
	36052, nicolas

> From: Damien Cassou <damien@cassou.me>
> Date: Sat, 08 Jun 2019 17:47:53 +0200
> Cc: Magnus Henoch <magnus.henoch@gmail.com>, Nicolas Petton <nicolas@petton.fr>,
>  Iku Iwasa <iku.iwasa@gmail.com>, Keith Amidon <camalot@picnicpark.org>,
>  galaunay <gaby.launay@tutanota.com>, 36052@debbugs.gnu.org,
>  Ted Zlatanov <tzz@lifelogs.com>
> 
> +(defcustom auth-source-pass-filename "~/.password-store"
> +  "Path to the password-store folder."
> +  :type 'directory)

Please add/update a :version tag to any defcustom that you introduce
or modify.  This is the basis for a "C-h v" and a few other features
that report on options introduced/changed in a certain Emacs version.

Also, GNU Coding Standards frown on calling "path" anything that is
not colon-separated PATH-style list of directories.

> >From d46e8dd1bbc3a1c7ac6506e3e6cfe3d87e57e99e Mon Sep 17 00:00:00 2001
> From: galaunay <gaby.launay@tutanota.com>
> Date: Sun, 13 Jan 2019 21:30:53 +0000
> Subject: [PATCH 04/12] Add auth-source-pass-path option

This patch was included twice, it seems.

> +(defun auth-source-pass--get-attr (key entry-data)
> +  "Return the value associated to KEY in data from an already parsed entry.

We prefer that the first line of a function's doc references all of
the arguments.  In this case, I suggest to reword as follows:

  Return value associated with KEY in an ENTRY-DATA.

What ENTRY-DATA is is explained in the very next line"

> +ENTRY-DATA is the data from a parsed password-store entry.

So you don't need to repeat that in the first line.

> --- a/etc/NEWS
> +++ b/etc/NEWS
> @@ -1485,6 +1485,21 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
>  value.  Auto Revert mode can use this information to avoid polling the
>  buffer periodically when 'auto-revert-avoid-polling' is non-nil.
>  
> +** auth-source-pass
> +
> +*** New customizable variable 'auth-source-pass-filename'.
> +Allows setting the path to the password-store, defaults to
> +~/.password-store.
> +
> +*** New customizable variable 'auth-source-pass-port-separator'.
> +Specifies separator between host and port, defaults to colon ":".
> +
> +*** Minimize the number of decryptions during password lookup.
> +This makes the package usable with physical tokens requiring touching
> +a sensor for every decryption.
> +
> +*** 'auth-source-pass-get' is now autoloaded.

Do these changes warrant changes in auth.texi manual?

Thanks for working on this.





^ permalink raw reply	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-08 15:47   ` Damien Cassou
  2019-06-08 16:02     ` Eli Zaretskii
@ 2019-06-08 22:38     ` Noam Postavsky
  2019-06-13 19:59     ` Damien Cassou
  2 siblings, 0 replies; 13+ messages in thread
From: Noam Postavsky @ 2019-06-08 22:38 UTC (permalink / raw)
  To: Damien Cassou
  Cc: Magnus Henoch, Nicolas Petton, Iku Iwasa, galaunay, Ted Zlatanov,
	36052, Keith Amidon

Damien Cassou <damien@cassou.me> writes:
>>
>> Double spacing, and this ChangeLog entry is a little sparse.  It looks
>> like the last two prose paragraphs could be easily made into ChangeLog
>> entries, since they're already talking about specific functions.
>
>
> I thought about doing that as well but didn't. If you insist, I will do
> the requested changes but here are my reasons for not doing it right
> away:
>
> - The changes are on private methods (with the "--" naming convention)
>   and I'm not sure how much of private changes should be in the
>   ChangeLog.

AFAIK, we don't keep "private" functions out of the ChangeLog.  Same
with even more internal C functions.

> - The commit message you mention modifies functions that are modified
>   again by a later patch (named "Refactoring of auth-source-pass"). I
>   usually only send the latest version of my code and not the whole
>   history but, this time, I'm not the author of the original version and
>   I believe the author deserves to have his name in Emacs' git history
>   because of the massive work he did for the package.
>
> - I have tried not to rewrite too much of contributor's code and text in
>   their own commit so that the authorship makes sense. I have no problem
>   changing contributions in a later commit though (as I've shown in the
>   patch "Refactoring of auth-source-pass").

Sure that make sense, but I think the commit message only needs some
pretty minor formatting (although it seems that the original patch
message has a typo in the function name mentioned, it should be
auth-source-pass--matching-entries rather than
auth-source-pass--applicable-entries), e.g., just change the last
two paragraphs into:

    This commit implements that strategy.  It is in spirit a refactor of
    the existing code.
    * lisp/auth-source-pass.el (auth-source-pass--matching-entries): New
    function, generate an ordered list of regular expression matchers for
    all possible names that could be in the password-store for the entry to
    be found and then makes a pass over the password-store entry names
    accumulating the matching entries in a list after the regexp that
    matched.  This implementation ensures the password-store entry list
    still only has to be scanned once.
    (auth-source-pass--find-match-unambiguous): Use it to obtain candidate
    entries and then parse them one by one until an entry containing the
    desired information is located.  When complete, return the parsed data of
    the entry instead of the entry name so that the information can be used
    directly to construct the auth-source response.

and then at least mention the other functions:

    (auth-source-pass--build-result): Update accordingly.
    (auth-source-pass--select-one-entry)
    (auth-source-pass--entry-valid-p)
    (auth-source-pass--find-all-by-entry-name) 
    (auth-source-pass--find-one-by-entry-name): Remove.
    (auth-source-pass--select-from-entries) 
    (auth-source-pass--accumulate-matches) 
    (auth-source-pass--entry-reducer) 
    (auth-source-pass--generate-entry-suffixes) 
    (auth-source-pass--domains) 
    (auth-source-pass--name-port-user-suffixes): New functions.





^ permalink raw reply	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-08 15:47   ` Damien Cassou
  2019-06-08 16:02     ` Eli Zaretskii
  2019-06-08 22:38     ` Noam Postavsky
@ 2019-06-13 19:59     ` Damien Cassou
  2019-06-13 21:23       ` Noam Postavsky
                         ` (2 more replies)
  2 siblings, 3 replies; 13+ messages in thread
From: Damien Cassou @ 2019-06-13 19:59 UTC (permalink / raw)
  To: 36052
  Cc: Magnus Henoch, Nicolas Petton, Noam Postavsky, Iku Iwasa,
	Keith Amidon, galaunay, Ted Zlatanov

[-- Attachment #1: Type: text/plain, Size: 417 bytes --]

Hi everyone,

thank you so much to Eli and Noam for their detailed review. Here is a
new patch series that address every concern received so far. This series
also includes a new patch rewriting the "The Unix password store"
section of auth.texi.

Best,

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Fix-auth-source-pass-to-return-nil-if-no-entry-found.patch --]
[-- Type: text/x-patch, Size: 1985 bytes --]

From 5780a03cece9433fe186592ddd8c983b63795edb Mon Sep 17 00:00:00 2001
From: Magnus Henoch <magnus.henoch@gmail.com>
Date: Fri, 2 Nov 2018 21:51:59 +0000
Subject: [PATCH 01/13] Fix auth-source-pass to return nil if no entry found

* lisp/auth-source-pass.el (auth-source-pass-search): If there is no
matching entry, auth-source-pass-search should return nil, not (nil).
This lets auth-source fall back to other backends in the auth-sources
list.
* test/lisp/auth-source-pass-tests.el: Add corresponding test.

Copyright-paperwork-exempt: yes
---
 lisp/auth-source-pass.el            | 3 ++-
 test/lisp/auth-source-pass-tests.el | 5 +++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4283ed0392..c82c90167a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -56,7 +56,8 @@
          ;; Do not build a result, as none will match when HOST is nil
          nil)
         (t
-         (list (auth-source-pass--build-result host port user)))))
+         (when-let ((result (auth-source-pass--build-result host port user)))
+           (list result)))))
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index d1e486ad6b..ab9ef92c14 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -83,6 +83,11 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host nil))))
 
+(ert-deftest auth-source-pass-not-found ()
+  (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
+                                  ("bar"))
+    (should-not (auth-source-pass-search :host "baz"))))
+
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
   (auth-source-pass--with-store '(("foo"))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-lisp-auth-source-pass.el-Version-4.0.2.patch --]
[-- Type: text/x-patch, Size: 745 bytes --]

From dc0b4b5b587c2401e9da1f68d1c69e3af05989f8 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sat, 3 Nov 2018 09:06:42 +0100
Subject: [PATCH 02/13] * lisp/auth-source-pass.el: Version 4.0.2

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index c82c90167a..d092292e51 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,7 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
-;; Version: 4.0.1
+;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-lisp-auth-source-pass.el-auth-source-pass-get-Add-au.patch --]
[-- Type: text/x-patch, Size: 758 bytes --]

From f7d724373fa4193cb148616c1bfb55e671d85649 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 6 Nov 2018 14:45:20 +0100
Subject: [PATCH 03/13] * lisp/auth-source-pass.el (auth-source-pass-get): Add
 autoload

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index d092292e51..4fcb1015e7 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -98,6 +98,7 @@ auth-source-pass-backend-parse
   (advice-add 'auth-source-backend-parse :before-until #'auth-source-pass-backend-parse))
 
 \f
+;;;###autoload
 (defun auth-source-pass-get (key entry)
   "Return the value associated to KEY in the password-store entry ENTRY.
 
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Add-auth-source-pass-filename-option.patch --]
[-- Type: text/x-patch, Size: 2114 bytes --]

From 60cbc0c6641dcef0b7b9ec970f516e78825a704f Mon Sep 17 00:00:00 2001
From: galaunay <gaby.launay@tutanota.com>
Date: Sun, 13 Jan 2019 21:30:53 +0000
Subject: [PATCH 04/13] Add auth-source-pass-filename option

* lisp/auth-source-pass.el (auth-source-pass):
(auth-source-pass-filename): Add option to specify a customized
password-store path.
(auth-source-pass--read-entry):
(auth-source-pass-entries): Use the new option instead of hard-coded
`~/.password-store'.
---
 lisp/auth-source-pass.el | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4fcb1015e7..03bafa7a26 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -38,6 +38,17 @@
 (require 'auth-source)
 (require 'url-parse)
 
+(defgroup auth-source-pass nil
+  "password-store integration within auth-source."
+  :prefix "auth-source-pass-"
+  :group 'auth-source
+  :version "27.1")
+
+(defcustom auth-source-pass-filename "~/.password-store"
+  "Path to the password-store folder."
+  :type 'directory
+  :version "27.1")
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -121,7 +132,7 @@ auth-source-pass--read-entry
   (with-temp-buffer
     (insert-file-contents (expand-file-name
                            (format "%s.gpg" entry)
-                           "~/.password-store"))
+                           auth-source-pass-filename))
     (buffer-substring-no-properties (point-min) (point-max))))
 
 (defun auth-source-pass-parse-entry (entry)
@@ -188,7 +199,7 @@ auth-source-pass--entry-valid-p
 ;; in Emacs
 (defun auth-source-pass-entries ()
   "Return a list of all password store entries."
-  (let ((store-dir (expand-file-name "~/.password-store/")))
+  (let ((store-dir (expand-file-name auth-source-pass-filename)))
     (mapcar
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Add-auth-source-pass-port-separator-option.patch --]
[-- Type: text/x-patch, Size: 3396 bytes --]

From f74b1a5df38d6e8fc4cdf39d0f04f75ddfe49a77 Mon Sep 17 00:00:00 2001
From: Iku Iwasa <iku.iwasa@gmail.com>
Date: Sun, 7 Apr 2019 17:59:59 +0900
Subject: [PATCH 05/13] Add auth-source-pass-port-separator option

* lisp/auth-source-pass.el (auth-source-pass-port-separator): New
option to specify separator between host and port, default to
colon (":").
(auth-source-pass--find-match-unambiguous): Adapt to make use of the
new variable.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            | 17 ++++++++++++++---
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 03bafa7a26..4dea6709b9 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -49,6 +49,11 @@ auth-source-pass-filename
   :type 'directory
   :version "27.1")
 
+(defcustom auth-source-pass-port-separator ":"
+  "Separator string between host and port in entry filename."
+  :type 'string
+  :version "27.1")
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -254,9 +259,15 @@ auth-source-pass--find-match-unambiguous
 
 HOSTNAME should not contain any username or port number."
   (or
-   (and user port (auth-source-pass--find-one-by-entry-name (format "%s@%s:%s" user hostname port) user))
-   (and user (auth-source-pass--find-one-by-entry-name (format "%s@%s" user hostname) user))
-   (and port (auth-source-pass--find-one-by-entry-name (format "%s:%s" hostname port) nil))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
+                   user))
+   (and user (auth-source-pass--find-one-by-entry-name
+              (format "%s@%s" user hostname)
+              user))
+   (and port (auth-source-pass--find-one-by-entry-name
+              (format "%s%s%s" hostname auth-source-pass-port-separator port)
+              nil))
    (auth-source-pass--find-one-by-entry-name hostname user)
    ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
    (let ((components (split-string hostname "\\.")))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ab9ef92c14..ae7a696bc6 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -186,6 +186,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
                    "host.com"))))
 
+(ert-deftest auth-source-pass-find-host-with-port ()
+  (auth-source-pass--with-store '(("host.com:443"))
+    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                   "host.com:443"))))
+
+(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("host.com#443"))
+      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                     "host.com#443")))))
+
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
   (declare (indent 1))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Fix-auth-source-pass-to-search-for-hostname-port-use.patch --]
[-- Type: text/x-patch, Size: 2729 bytes --]

From 437845fb314694b2b31b5b128da5b5c7f3eaaf4c Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Thu, 14 Feb 2019 10:27:31 -0800
Subject: [PATCH 06/13] Fix auth-source-pass to search for
 hostname:port/username

auth-source-pass supports entries with username either prefixed to the
hostname with an @ as separator or in a subdirectory under the
hostname.  This was true when there was no port or service included in
the name, but got broken with the introduction of
auth-source-pass-port-separator.

* lisp/auth-source-pass.el (auth-source-pass--find-match-unambiguous): Fix
to match hostname:port/username.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            |  3 +++
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 14 insertions(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4dea6709b9..52ff312369 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -262,6 +262,9 @@ auth-source-pass--find-match-unambiguous
    (and user port (auth-source-pass--find-one-by-entry-name
                    (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
                    user))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
+                   user))
    (and user (auth-source-pass--find-one-by-entry-name
               (format "%s@%s" user hostname)
               user))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ae7a696bc6..1539d9611f 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -144,6 +144,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
                    nil))))
 
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+  (auth-source-pass--with-store '(("bar.com:443/someone"))
+    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                   "bar.com:443/someone"))))
+
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("bar.com#443/someone"))
+      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                     "bar.com#443/someone")))))
+
 (ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
   (auth-source-pass--with-store '(("foo.com/bar"))
     (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-Split-out-the-attribute-retrieval-form-auth-source-p.patch --]
[-- Type: text/x-patch, Size: 3031 bytes --]

From f3bf765271c04491d49c81d71186efefe9bb5891 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Tue, 30 Apr 2019 07:52:14 -0700
Subject: [PATCH 07/13] Split out the attribute retrieval form
 auth-source-pass-get

Eliminate the need to repeatedly retrieve and parse the data for the
entry.  This is generally a good thing since it eliminates repetitions
of the same crypto and parsing operations.  It is especially valuable
when protecting an entry with a yubikey with touch required for crypto
operations as it eliminates the need to touch the yubikey sensor for
each attribute retrieved.

* lisp/auth-source-pass.el (auth-source-pass-get): Extract some code to
`auth-source-pass--get-attr'.
(auth-source-pass--get-attr): New function to get a field value from a
parsed entry.
(auth-source-pass--build-result): Make use of
`auth-source-pass--get-attr` to avoid repeated parsing.
---
 lisp/auth-source-pass.el | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 52ff312369..c83d695b2e 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -79,11 +79,12 @@ auth-source-pass--build-result
   "Build auth-source-pass entry matching HOST, PORT and USER."
   (let ((entry (auth-source-pass--find-match host user port)))
     (when entry
-      (let ((retval (list
-                     :host host
-                     :port (or (auth-source-pass-get "port" entry) port)
-                     :user (or (auth-source-pass-get "user" entry) user)
-                     :secret (lambda () (auth-source-pass-get 'secret entry)))))
+      (let* ((entry-data (auth-source-pass-parse-entry entry))
+             (retval (list
+                      :host host
+                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -128,9 +129,18 @@ auth-source-pass-get
 key1: value1
 key2: value2"
   (let ((data (auth-source-pass-parse-entry entry)))
-    (or (cdr (assoc key data))
-        (and (string= key "user")
-             (cdr (assoc "username" data))))))
+    (auth-source-pass--get-attr key data)))
+
+(defun auth-source-pass--get-attr (key entry-data)
+  "Return value associated with KEY in an ENTRY-DATA.
+
+ENTRY-DATA is the data from a parsed password-store entry.
+The key used to retrieve the password is the symbol `secret'.
+
+See `auth-source-pass-get'."
+  (or (cdr (assoc key entry-data))
+      (and (string= key "user")
+           (cdr (assoc "username" entry-data)))))
 
 (defun auth-source-pass--read-entry (entry)
   "Return a string with the file content of ENTRY."
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-Minimize-entry-parsing-in-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 33974 bytes --]

From e0011902bc07290beff860089d08fddcf60e9153 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sun, 5 May 2019 20:21:43 -0700
Subject: [PATCH 08/13] Minimize entry parsing in auth-source-pass

Prior to this commit, while searching for the most applicable entry
password-store entries were decrypted and parsed to ensure they were
valid.  The entries were parsed in the order they were found on the
filesystem and all applicable entries would be decrypted and parsed,
which varied based on the contents of the password-store and the entry
to be found.

This is fine when the GPG key is cached and each entry can be
decrypted without user interaction.  However, for security some people
have their GPG on a hardware token like a Yubikey setup so that they
have to touch a sensor on the toke for every cryptographic operation,
in which case it becomes inconvenient as each attempt to find an entry
requires a variable number of touches of the hardware token.

The implementation already assumes that names which contain more of
the information in the search key should be preferred so there is an
ordering of preference of applicable entries.  If the decrypt and
parsing is removed from the initial identification of applicable
entries in the store then in most cases a single decrypt and parse of
the most preferred entry will suffice, improving the experience for
hardware token users that require interaction with the token.

This commit implements that strategy.  It is in spirit a refactor of
the existing code.

* lisp/auth-source-pass.el (auth-source-pass--matching-entries): New
function, generate an ordered list of regular expression matchers for
all possible names that could be in the password-store for the entry
to be found and then makes a pass over the password-store entry names
accumulating the matching entries in a list after the regexp that
matched.  This implementation ensures the password-store entry list
still only has to be scanned once.
(auth-source-pass--find-match-unambiguous): Use it to obtain candidate
entries and then parse them one by one until an entry containing the
desired information is located.  When complete, return the parsed data
of the entry instead of the entry name so that the information can be
used directly to construct the auth-source response.
(auth-source-pass--build-result): Update accordingly.
(auth-source-pass--find-match): Update docstring accordingly.
(auth-source-pass--select-one-entry)
(auth-source-pass--entry-valid-p)
(auth-source-pass--find-all-by-entry-name)
(auth-source-pass--find-one-by-entry-name): Remove.
(auth-source-pass--select-from-entries)
(auth-source-pass--accumulate-matches)
(auth-source-pass--entry-reducer)
(auth-source-pass--generate-entry-suffixes)
(auth-source-pass--domains)
(auth-source-pass--name-port-user-suffixes): New functions.

* test/lisp/auth-source-pass-tests.el: One test case was added to the
test suite to verify that only the minimal number of entries are
parsed in common cases.  The
auth-source-pass-only-return-entries-that-can-be-open test case had to
be re-implemented because the function it was used eliminated as the
functionality is provided elsewhere.  All the other fairly substantial
changes to the test suite are the result of mechanical changes that
were required to adapt to auth-source-pass--find-match returning the
data from a parsed password-store entry instead of the entry name.
---
 lisp/auth-source-pass.el            | 209 ++++++++++++----------
 test/lisp/auth-source-pass-tests.el | 264 +++++++++++++++++-----------
 2 files changed, 281 insertions(+), 192 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index c83d695b2e..d0cde90ab7 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -77,14 +77,13 @@ auth-source-pass-port-separator
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
-  (let ((entry (auth-source-pass--find-match host user port)))
-    (when entry
-      (let* ((entry-data (auth-source-pass-parse-entry entry))
-             (retval (list
-                      :host host
-                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
-                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
-                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
+  (let ((entry-data (auth-source-pass--find-match host user port)))
+    (when entry-data
+      (let ((retval (list
+                     :host host
+                     :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                     :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                     :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -183,33 +182,6 @@ auth-source-pass--do-debug
          (cons (concat "auth-source-pass: " (car msg))
                (cdr msg))))
 
-(defun auth-source-pass--select-one-entry (entries user)
-  "Select one entry from ENTRIES by searching for a field matching USER."
-  (let ((number (length entries))
-        (entry-with-user
-         (and user
-              (seq-find (lambda (entry)
-                          (string-equal (auth-source-pass-get "user" entry) user))
-                        entries))))
-    (auth-source-pass--do-debug "found %s matches: %s" number
-                                (mapconcat #'identity entries ", "))
-    (if entry-with-user
-        (progn
-          (auth-source-pass--do-debug "return %s as it contains matching user field"
-                                      entry-with-user)
-          entry-with-user)
-      (auth-source-pass--do-debug "return %s as it is the first one" (car entries))
-      (car entries))))
-
-(defun auth-source-pass--entry-valid-p (entry)
-  "Return t iff ENTRY can be opened.
-Also displays a warning if not.  This function is slow, don't call it too
-often."
-  (if (auth-source-pass-parse-entry entry)
-      t
-    (auth-source-pass--do-debug "entry '%s' is not valid" entry)
-    nil))
-
 ;; TODO: add tests for that when `assess-with-filesystem' is included
 ;; in Emacs
 (defun auth-source-pass-entries ()
@@ -219,37 +191,8 @@ auth-source-pass-entries
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
 
-(defun auth-source-pass--find-all-by-entry-name (entryname user)
-  "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME.
-Only return valid entries as of `auth-source-pass--entry-valid-p'."
-  (seq-filter (lambda (entry)
-                (and
-                 (or
-                  (let ((components-host-user
-                         (member entryname (split-string entry "/"))))
-                    (and (= (length components-host-user) 2)
-                         (string-equal user (cadr components-host-user))))
-                  (string-equal entryname (file-name-nondirectory entry)))
-                 (auth-source-pass--entry-valid-p entry)))
-              (auth-source-pass-entries)))
-
-(defun auth-source-pass--find-one-by-entry-name (entryname user)
-  "Search the store for an entry matching ENTRYNAME.
-If USER is non nil, give precedence to entries containing a user field
-matching USER."
-  (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
-                              entryname
-                              user)
-  (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user)))
-    (pcase (length matching-entries)
-      (0 (auth-source-pass--do-debug "no match found")
-         nil)
-      (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries))
-         (car matching-entries))
-      (_ (auth-source-pass--select-one-entry matching-entries user)))))
-
 (defun auth-source-pass--find-match (host user port)
-  "Return a password-store entry name matching HOST, USER and PORT.
+  "Return password-store entry data matching HOST, USER and PORT.
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
@@ -263,33 +206,123 @@ auth-source-pass--find-match
      (or port (number-to-string (url-port url))))))
 
 (defun auth-source-pass--find-match-unambiguous (hostname user port)
-  "Return a password-store entry name matching HOSTNAME, USER and PORT.
+  "Return password-store entry data matching HOSTNAME, USER and PORT.
 If many matches are found, return the first one.  If no match is found,
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (or
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
-                   user))
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
-                   user))
-   (and user (auth-source-pass--find-one-by-entry-name
-              (format "%s@%s" user hostname)
-              user))
-   (and port (auth-source-pass--find-one-by-entry-name
-              (format "%s%s%s" hostname auth-source-pass-port-separator port)
-              nil))
-   (auth-source-pass--find-one-by-entry-name hostname user)
-   ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
-   (let ((components (split-string hostname "\\.")))
-     (when (= (length components) 3)
-       ;; start from scratch
-       (auth-source-pass--find-match-unambiguous
-        (mapconcat 'identity (cdr components) ".")
-        user
-        port)))))
+  (cl-reduce
+   (lambda (result entries)
+     (or result
+         (pcase (length entries)
+           (0 nil)
+           (1 (auth-source-pass-parse-entry (car entries)))
+           (_ (auth-source-pass--select-from-entries entries user)))))
+   (auth-source-pass--matching-entries hostname user port)
+   :initial-value nil))
+
+(defun auth-source-pass--select-from-entries (entries user)
+  "Return best matching password-store entry data from ENTRIES.
+
+If USER is non nil, give precedence to entries containing a user field
+matching USER."
+  (cl-reduce
+   (lambda (result entry)
+     (let ((entry-data (auth-source-pass-parse-entry entry)))
+       (cond ((equal (auth-source-pass--get-attr "user" result) user)
+              result)
+             ((equal (auth-source-pass--get-attr "user" entry-data) user)
+              entry-data)
+             (t
+              result))))
+   entries
+   :initial-value (auth-source-pass-parse-entry (car entries))))
+
+(defun auth-source-pass--matching-entries (hostname user port)
+  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
+
+The result is a list of lists of password-store entries, where
+each sublist contains entries that actually exist in the
+password-store matching one of the entry name formats that
+auth-source-pass expects, most specific to least specific."
+  (let* ((entries-lists (mapcar
+                         #'cdr
+                         (auth-source-pass--accumulate-matches hostname user port)))
+         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
+    (if entries
+        (auth-source-pass--do-debug (format "found: %S" entries))
+      (auth-source-pass--do-debug "no matches found"))
+    entries-lists))
+
+(defun auth-source-pass--accumulate-matches (hostname user port)
+  "Accumulate matching password-store entries into sublists.
+
+Entries matching supported formats that combine HOSTNAME, USER, &
+PORT are accumulated into sublists where the car of each sublist
+is a regular expression for matching paths in the password-store
+and the remainder is the list of matching entries."
+  (let ((suffix-match-lists
+         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
+                 (auth-source-pass--generate-entry-suffixes hostname user port))))
+    (cl-reduce #'auth-source-pass--entry-reducer
+               (auth-source-pass-entries)
+               :initial-value suffix-match-lists)))
+
+(defun auth-source-pass--entry-reducer (match-lists entry)
+  "Match MATCH-LISTS sublists against ENTRY.
+
+The result is a copy of match-lists with the entry added to the
+end of any sublists for which the regular expression at the head
+of the list matches the entry name."
+  (mapcar (lambda (match-list)
+            (if (string-match (car match-list) entry)
+                (append match-list (list entry))
+              match-list))
+          match-lists))
+
+(defun auth-source-pass--generate-entry-suffixes (hostname user port)
+  "Return a list of possible entry path suffixes in the password-store.
+
+Based on the supported pathname patterns for HOSTNAME, USER, &
+PORT, return a list of possible suffixes for matching entries in
+the password-store."
+  (let ((domains (auth-source-pass--domains (split-string hostname "\\."))))
+    (seq-mapcat (lambda (n)
+                  (auth-source-pass--name-port-user-suffixes n user port))
+                domains)))
+
+(defun auth-source-pass--domains (name-components)
+  "Return a list of possible domain names matching the hostname.
+
+This function takes a list of NAME-COMPONENTS, the strings
+separated by periods in the hostname, and returns a list of full
+domain names containing the trailing sequences of those
+components, from longest to shortest."
+  (cl-maplist (lambda (components) (mapconcat #'identity components "."))
+              name-components))
+
+(defun auth-source-pass--name-port-user-suffixes (name user port)
+  "Return a list of possible path suffixes for NAME, USER, & PORT.
+
+The resulting list is ordered from most specifc to least
+specific, with paths matching all of NAME, USER, & PORT first,
+then NAME & USER, then NAME & PORT, then just NAME."
+  (seq-mapcat
+   #'identity
+   (list
+    (when (and user port)
+      (list
+       (format "%s@%s%s%s" user name auth-source-pass-port-separator port)
+       (format "%s%s%s/%s" name auth-source-pass-port-separator port user)))
+    (when user
+      (list
+       (format "%s@%s" user name)
+       (format "%s/%s" name user)))
+    (when port
+      (list
+       (format "%s%s%s" name auth-source-pass-port-separator port)))
+    (list
+     (format "%s" name)))))
 
 (provide 'auth-source-pass)
 ;;; auth-source-pass.el ends here
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 1539d9611f..2c28f79945 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -63,14 +63,19 @@ auth-source-pass--debug
 This function is intended to be set to `auth-source-debug`."
   (add-to-list 'auth-source-pass--debug-log (apply #'format msg) t))
 
+(defvar auth-source-pass--parse-log nil)
+
 (defmacro auth-source-pass--with-store (store &rest body)
   "Use STORE as password-store while executing BODY."
   (declare (indent 1))
-  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry) (lambda (entry) (cdr (cl-find entry ,store :key #'car :test #'string=))) )
-             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store)))
-             ((symbol-function 'auth-source-pass--entry-valid-p) (lambda (_entry) t)))
+  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+              (lambda (entry)
+                (add-to-list 'auth-source-pass--parse-log entry)
+                (cdr (cl-find entry ,store :key #'car :test #'string=))))
+             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store))))
      (let ((auth-source-debug #'auth-source-pass--debug)
-           (auth-source-pass--debug-log nil))
+           (auth-source-pass--debug-log nil)
+           (auth-source-pass--parse-log nil))
        ,@body)))
 
 (ert-deftest auth-source-pass-any-host ()
@@ -88,125 +93,184 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass-find-match-minimal-parsing ()
+  (let ((store-contents
+         '(("baz" ("secret" . "baz password"))
+           ("baz:123" ("secret" . "baz:123 password"))
+           ("baz/foo" ("secret" . "baz/foo password"))
+           ("foo@baz" ("secret" . "foo@baz password"))
+           ("baz:123/foo" ("secret" . "baz:123/foo password"))
+           ("foo@baz:123" ("secret" . "foo@baz:123 password"))
+           ("bar.baz" ("secret" . "bar.baz password"))
+           ("bar.baz:123" ("secret" . "bar.baz:123 password"))
+           ("bar.baz/foo" ("secret" . "bar.baz/foo password"))
+           ("foo@bar.baz" ("secret" . "foo@bar.baz password"))
+           ("bar.baz:123/foo" ("secret" . "bar.baz:123/foo password"))
+           ("foo@bar.baz:123" ("secret" . "foo@bar.baz:123 password")))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" "123")
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" nil)
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil "123")
+      (should (equal auth-source-pass--parse-log '("bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("baz"))))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store '(("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store '(("SomeUser@foo") ("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
+        ("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store '(("foo") ("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
+
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil ("bar.com") nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
   (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   "someone@bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-bad-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
   (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-prefer-full ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
   (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "foo.bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(("foo.bar.com") ("bar.com") nil)))))
 
 (ert-deftest auth-source-pass-dont-match-at-folder-name ()
   (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
   (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                   "bar.com:443/someone"))))
+    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                   '(nil ("bar.com:443/someone") nil nil nil nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                     "bar.com#443/someone")))))
-
-(ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
-  (auth-source-pass--with-store '(("foo.com/bar"))
-    (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-                   "foo.com/bar"))))
-
-(ert-deftest auth-source-pass-search-with-user-first ()
+      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                     '(nil ("bar.com#443/someone") nil nil nil nil
+                           nil nil nil nil nil nil))))))
+
+(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
+  (auth-source-pass--with-store
+      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
+    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo.com/bar password")))))
+
+(ert-deftest auth-source-pass-matching-entries-with-user-first ()
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--find-match "foo" "user" nil)
-                   "user@foo"))
-    (auth-source-pass--should-have-message-containing "Found 1 match")))
+    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
+                   '(("user@foo") nil ("foo"))))
+    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo" ("user" . "someone")))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "subdir/foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "subdir/foo password"))
+      (should (equal (auth-source-pass--get-attr "user" result)
+                     "someone")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store '(("foo" ("user" . "someone")) ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password") ("user" . "someone"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "the first one")))
-
-(ert-deftest auth-source-pass-make-divansantana-happy ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
+
+(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
   (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "smtp.host.com" "myusername@host.co.za" nil)
-                   "host.com"))))
+    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
+                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
 
 (ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
-                   "host.com"))))
+  (auth-source-pass--with-store
+      '(("host.com" ("secret" . "host.com password")))
+    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "host.com password")))))
 
-(ert-deftest auth-source-pass-find-host-with-port ()
+(ert-deftest auth-source-pass-matching-entries-host-with-port ()
   (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                   "host.com:443"))))
+    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                   '(nil nil nil nil ("host.com:443") nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                     "host.com#443")))))
+      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                     '(nil nil nil nil ("host.com#443") nil
+                           nil nil nil nil nil nil))))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -218,7 +282,8 @@ auth-source-pass--with-store-find-foo
        ,@body)))
 
 (ert-deftest auth-source-pass-build-result-return-parameters ()
-  (auth-source-pass--with-store-find-foo '(("foo"))
+  (auth-source-pass--with-store-find-foo
+      '(("foo" ("secret" . "foo password")))
     (let ((result (auth-source-pass--build-result "foo" 512 "user")))
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "user")))))
@@ -238,7 +303,9 @@ auth-source-pass--with-store-find-foo
 (ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
   (let (passed-host)
     (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port) (setq passed-host host))))
+               (lambda (host _user _port)
+                 (setq passed-host host)
+                 nil)))
       (auth-source-pass--build-result "https://user@host.com:123" nil nil)
       (should (equal passed-host "https://user@host.com:123"))
       (auth-source-pass--build-result "https://user@host.com" nil nil)
@@ -249,27 +316,16 @@ auth-source-pass--with-store-find-foo
       (should (equal passed-host "user@host.com:443")))))
 
 (ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (cl-letf (((symbol-function 'auth-source-pass-entries)
-             (lambda () '("foo.site.com" "bar.site.com" "mail/baz.site.com/scott")))
-            ((symbol-function 'auth-source-pass--entry-valid-p)
-             ;; only foo.site.com and "mail/baz.site.com/scott" are valid
-             (lambda (entry) (member entry '("foo.site.com" "mail/baz.site.com/scott")))))
-    (should (equal (auth-source-pass--find-all-by-entry-name "foo.site.com" "someuser")
-                   '("foo.site.com")))
-    (should (equal (auth-source-pass--find-all-by-entry-name "bar.site.com" "someuser")
-                   '()))
-    (should (equal (auth-source-pass--find-all-by-entry-name "baz.site.com" "scott")
-                   '("mail/baz.site.com/scott")))))
-
-(ert-deftest auth-source-pass-entry-is-not-valid-when-unreadable ()
-  (cl-letf (((symbol-function 'auth-source-pass--read-entry)
-             (lambda (entry)
-               ;; only foo is a valid entry
-               (if (string-equal entry "foo")
-                   "password"
-                 nil))))
-    (should (auth-source-pass--entry-valid-p "foo"))
-    (should-not (auth-source-pass--entry-valid-p "bar"))))
+  (auth-source-pass--with-store
+      '(("foo.site.com" ("secret" . "foo.site.com password"))
+        ("bar.site.com") ; An entry name with no data is invalid
+        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
+    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
+                   '(("secret" . "foo.site.com password"))))
+    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
+                   nil))
+    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
+                   '(("secret" . "mail/baz.site.com/scott password"))))))
 
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-lisp-auth-source-pass.el-Add-Keith-Amidon-to-authors.patch --]
[-- Type: text/x-patch, Size: 755 bytes --]

From 6520b693247147a5ee73f377a61309af63ef9d48 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sat, 11 May 2019 08:22:56 -0700
Subject: [PATCH 09/13] * lisp/auth-source-pass.el: Add Keith Amidon to authors

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index d0cde90ab7..2e596de030 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,6 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
+;;         Keith Amidon <camalot@picnicpark.org>
 ;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0010-Refactoring-of-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 36679 bytes --]

From 7e1b60e4dc1d32fbf6fcccdf14d306a5a1c55800 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 14 May 2019 05:50:59 +0200
Subject: [PATCH 10/13] Refactoring of auth-source-pass

* lisp/auth-source-pass.el (auth-source-pass--find-match): Refactor by
moving some code to auth-source-pass--disambiguate.
(auth-source-pass--disambiguate):
(auth-source-pass--entries-matching-suffix): New function.
(auth-source-pass--find-match-unambiguous):
(auth-source-pass--select-from-entries):
(auth-source-pass--entry-reducer): Refactor to simplify and improve
logging.
(auth-source-pass--matching-entries):
(auth-source-pass--accumulate-matches): Remove
* test/lisp/auth-source-pass-tests.el: Complete rewrite to facilitate
maintenance.
(auth-source-pass--have-message-containing): Remove
(auth-source-pass--have-message-matching):
(auth-source-pass--explain--have-message-matching):
(auth-source-pass--explain-match-entry-p):
(auth-source-pass--includes-sorted-entries):
(auth-source-pass--explain-includes-sorted-entries):
(auth-source-pass--explain-match-any-entry-p):
(auth-source-pass--matching-entries):
(auth-source-pass-match-entry-p):
(auth-source-pass-match-any-entry-p): New function.
---
 lisp/auth-source-pass.el            | 108 +++----
 test/lisp/auth-source-pass-tests.el | 482 ++++++++++++++++++----------
 2 files changed, 349 insertions(+), 241 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 2e596de030..dea0dcb61a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -197,10 +197,17 @@ auth-source-pass--find-match
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
+  (apply #'auth-source-pass--find-match-unambiguous (auth-source-pass--disambiguate host user port)))
+
+(defun auth-source-pass--disambiguate (host &optional user port)
+  "Return (HOST USER PORT) after disambiguation.
+Disambiguate between having user provided inside HOST (e.g.,
+user@server.com) and inside USER by giving priority to USER.
+Same for PORT."
   (let* ((url (url-generic-parse-url (if (string-match-p ".*://" host)
                                          host
                                        (format "https://%s" host)))))
-    (auth-source-pass--find-match-unambiguous
+    (list
      (or (url-host url) host)
      (or user (url-user url))
      ;; url-port returns 443 (because of the https:// above) by default
@@ -212,74 +219,49 @@ auth-source-pass--find-match-unambiguous
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (cl-reduce
-   (lambda (result entries)
-     (or result
-         (pcase (length entries)
-           (0 nil)
-           (1 (auth-source-pass-parse-entry (car entries)))
-           (_ (auth-source-pass--select-from-entries entries user)))))
-   (auth-source-pass--matching-entries hostname user port)
-   :initial-value nil))
+  (let ((all-entries (auth-source-pass-entries))
+        (suffixes (auth-source-pass--generate-entry-suffixes hostname user port)))
+    (auth-source-pass--do-debug "searching for entries matching hostname=%S, user=%S, port=%S"
+                                hostname (or user "") (or port ""))
+    (auth-source-pass--do-debug "corresponding suffixes to search for: %S" suffixes)
+    (catch 'auth-source-pass-break
+      (dolist (suffix suffixes)
+        (let* ((matching-entries (auth-source-pass--entries-matching-suffix suffix all-entries))
+               (best-entry-data (auth-source-pass--select-from-entries matching-entries user)))
+          (pcase (length matching-entries)
+            (0 (auth-source-pass--do-debug "found no entries matching %S" suffix))
+            (1 (auth-source-pass--do-debug "found 1 entry matching %S: %S"
+                                           suffix
+                                           (car matching-entries)))
+            (_ (auth-source-pass--do-debug "found %s entries matching %S: %S"
+                                           (length matching-entries)
+                                           suffix
+                                           matching-entries)))
+          (when best-entry-data
+            (throw 'auth-source-pass-break best-entry-data)))))))
 
 (defun auth-source-pass--select-from-entries (entries user)
   "Return best matching password-store entry data from ENTRIES.
 
 If USER is non nil, give precedence to entries containing a user field
 matching USER."
-  (cl-reduce
-   (lambda (result entry)
-     (let ((entry-data (auth-source-pass-parse-entry entry)))
-       (cond ((equal (auth-source-pass--get-attr "user" result) user)
-              result)
-             ((equal (auth-source-pass--get-attr "user" entry-data) user)
-              entry-data)
-             (t
-              result))))
-   entries
-   :initial-value (auth-source-pass-parse-entry (car entries))))
-
-(defun auth-source-pass--matching-entries (hostname user port)
-  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
-
-The result is a list of lists of password-store entries, where
-each sublist contains entries that actually exist in the
-password-store matching one of the entry name formats that
-auth-source-pass expects, most specific to least specific."
-  (let* ((entries-lists (mapcar
-                         #'cdr
-                         (auth-source-pass--accumulate-matches hostname user port)))
-         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
-    (if entries
-        (auth-source-pass--do-debug (format "found: %S" entries))
-      (auth-source-pass--do-debug "no matches found"))
-    entries-lists))
-
-(defun auth-source-pass--accumulate-matches (hostname user port)
-  "Accumulate matching password-store entries into sublists.
-
-Entries matching supported formats that combine HOSTNAME, USER, &
-PORT are accumulated into sublists where the car of each sublist
-is a regular expression for matching paths in the password-store
-and the remainder is the list of matching entries."
-  (let ((suffix-match-lists
-         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
-                 (auth-source-pass--generate-entry-suffixes hostname user port))))
-    (cl-reduce #'auth-source-pass--entry-reducer
-               (auth-source-pass-entries)
-               :initial-value suffix-match-lists)))
-
-(defun auth-source-pass--entry-reducer (match-lists entry)
-  "Match MATCH-LISTS sublists against ENTRY.
-
-The result is a copy of match-lists with the entry added to the
-end of any sublists for which the regular expression at the head
-of the list matches the entry name."
-  (mapcar (lambda (match-list)
-            (if (string-match (car match-list) entry)
-                (append match-list (list entry))
-              match-list))
-          match-lists))
+  (let (fallback)
+    (catch 'auth-source-pass-break
+      (dolist (entry entries fallback)
+        (let ((entry-data (auth-source-pass-parse-entry entry)))
+          (when (and entry-data (not fallback))
+            (setq fallback entry-data)
+            (when (or (not user) (equal (auth-source-pass--get-attr "user" entry-data) user))
+              (throw 'auth-source-pass-break entry-data))))))))
+
+(defun auth-source-pass--entries-matching-suffix (suffix entries)
+  "Return entries matching SUFFIX.
+If ENTRIES is nil, use the result of calling `auth-source-pass-entries' instead."
+  (cl-remove-if-not
+   (lambda (entry) (string-match-p
+               (format "\\(^\\|/\\)%s$" (regexp-quote suffix))
+               entry))
+   (or entries (auth-source-pass-entries))))
 
 (defun auth-source-pass--generate-entry-suffixes (hostname user port)
   "Return a list of possible entry path suffixes in the password-store.
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 2c28f79945..6f0d308ceb 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -52,11 +52,21 @@
 (defvar auth-source-pass--debug-log nil
   "Contains a list of all messages passed to `auth-source-do-debug`.")
 
-(defun auth-source-pass--should-have-message-containing (regexp)
-  "Assert that at least one `auth-source-do-debug` matched REGEXP."
-  (should (seq-find (lambda (message)
-                      (string-match regexp message))
-                    auth-source-pass--debug-log)))
+(defun auth-source-pass--have-message-matching (regexp)
+  "Return non-nil iff at least one `auth-source-do-debug` match REGEXP."
+  (seq-find (lambda (message)
+              (string-match regexp message))
+            auth-source-pass--debug-log))
+
+(defun auth-source-pass--explain--have-message-matching (regexp)
+  "Explainer function for `auth-source-pass--have-message-matching'.
+REGEXP is the same as in `auth-source-pass--have-message-matching'."
+  `(regexp
+    ,regexp
+    messages
+    ,(mapconcat #'identity auth-source-pass--debug-log "\n- ")))
+
+(put #'auth-source-pass--have-message-matching 'ert-explainer #'auth-source-pass--explain--have-message-matching)
 
 (defun auth-source-pass--debug (&rest msg)
   "Format MSG and add that to `auth-source-pass--debug-log`.
@@ -78,6 +88,82 @@ auth-source-pass--with-store
            (auth-source-pass--parse-log nil))
        ,@body)))
 
+(defun auth-source-pass--explain-match-entry-p (entry hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-entry-p'.
+
+ENTRY, HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-entry-p'."
+  `(entry
+    ,entry
+    store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-entry-p 'ert-explainer #'auth-source-pass--explain-match-entry-p)
+
+(defun auth-source-pass--includes-sorted-entries (entries hostname &optional user port)
+  "Return non-nil iff ENTRIES matching the parameters are found in store.
+ENTRIES should be sorted from most specific to least specific.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (if (seq-empty-p entries)
+      t
+    (and
+     (auth-source-pass-match-entry-p (car entries) hostname user port)
+     (auth-source-pass--includes-sorted-entries (cdr entries) hostname user port))))
+
+(defun auth-source-pass--explain-includes-sorted-entries (entries hostname &optional user port)
+  "Explainer function for `auth-source-pass--includes-sorted-entries'.
+
+ENTRIES, HOSTNAME, USER and PORT are the same as in `auth-source-pass--includes-sorted-entries'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)
+    entries
+    ,entries))
+
+(put 'auth-source-pass--includes-sorted-entries 'ert-explainer #'auth-source-pass--explain-includes-sorted-entries)
+
+(defun auth-source-pass--explain-match-any-entry-p (hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-any-entry-p'.
+
+HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-any-entry-p'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-any-entry-p 'ert-explainer #'auth-source-pass--explain-match-any-entry-p)
+
+(defun auth-source-pass--matching-entries (hostname &optional user port)
+  "Return password-store entries matching HOSTNAME, USER, PORT.
+
+The result is a list of lists of password-store entries.  Each
+sublist contains the password-store entries whose names match a
+suffix in `auth-source-pass--generate-entry-suffixes'.  The
+result is ordered the same way as the suffixes."
+  (let ((entries (auth-source-pass-entries)))
+    (mapcar (lambda (suffix) (auth-source-pass--entries-matching-suffix suffix entries))
+            (auth-source-pass--generate-entry-suffixes hostname user port))))
+
+(defun auth-source-pass-match-entry-p (entry hostname &optional user port)
+  "Return non-nil iff an ENTRY matching the parameters is found in store.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if
+   (lambda (entries) (cl-find entry entries :test #'string=))
+   (auth-source-pass--matching-entries hostname user port)))
+
+(defun auth-source-pass-match-any-entry-p (hostname &optional user port)
+  "Return non-nil iff there is at least one entry matching the parameters.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if #'identity (auth-source-pass--matching-entries hostname user port)))
+
 (ert-deftest auth-source-pass-any-host ()
   (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
                                   ("bar"))
@@ -93,6 +179,101 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass--disambiguate-extract-host-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo")) "foo"))
+  ;; only user
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo")) "foo"))
+  ;; only port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80")) "foo"))
+  ;; both user and port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80")) "foo"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80/path")) "foo")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-user-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo")) nil))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo")) "user"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80")) nil))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80")) "user"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path")) "user")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-user-parameter ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo" "user2")) "user2"))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo" "user2")) "user2"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80" "user2")) "user2"))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80" "user2")) "user2"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path" "user2")) "user2")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-port-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo")) "443"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo")) "443"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80")) "80"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80")) "80"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path")) "80"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path")) "80")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-port-parameter ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo" "user2" "8080")) "8080"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo" "user2" "8080")) "8080"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80" "user2" "8080")) "8080"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80" "user2" "8080")) "8080"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path" "user2" "8080")) "8080")))
+
 (ert-deftest auth-source-pass-find-match-minimal-parsing ()
   (let ((store-contents
          '(("baz" ("secret" . "baz password"))
@@ -121,156 +302,110 @@ auth-source-pass--with-store
       (should (equal auth-source-pass--parse-log '("bar.baz"))))
     (auth-source-pass--with-store store-contents
       (auth-source-pass--find-match "baz" nil nil)
-      (should (equal auth-source-pass--parse-log '("baz"))))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
-        ("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
+      (should (equal auth-source-pass--parse-log '("baz"))))
+    (auth-source-pass--with-store
+        '(("dir1/bar.com" ("key" . "val"))
+          ("dir2/bar.com" ("key" . "val"))
+          ("dir3/bar.com" ("key" . "val")))
+      (auth-source-pass--find-match "bar.com" nil nil)
+      (should (= (length auth-source-pass--parse-log) 1)))))
+
+(ert-deftest auth-source-pass--find-match-return-parsed-data ()
+  (auth-source-pass--with-store '(("bar.com" ("key" . "val")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil))))
+  (auth-source-pass--with-store '(("dir1/bar.com" ("key1" . "val1")) ("dir2/bar.com" ("key2" . "val2")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
-  (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
-  (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
-  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(("foo.bar.com") ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-dont-match-at-folder-name ()
-  (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
-  (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                   '(nil ("bar.com:443/someone") nil nil nil nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com"))
+    ;; match even if sub-domain is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "foo.bar.com"))
+    ;; match even if a user is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user"))
+    ;; match even if user as an @ sign
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user@someplace"))
+    ;; match even if a port is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" nil "8080"))
+    ;; match even if a user and a port are asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user" "8080"))
+    ;; don't match if a '.' is replaced with another character
+    (auth-source-pass--with-store '(("barXcom"))
+      (should-not (auth-source-pass-match-any-entry-p "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-username ()
+  (auth-source-pass--with-store '(("user@foo"))
+    (should (auth-source-pass-match-entry-p "user@foo" "foo" "user")))
+  ;; match even if sub-domain is asked for
+  (auth-source-pass--with-store '(("user@bar.com"))
+    (should (auth-source-pass-match-entry-p "user@bar.com" "foo.bar.com" "user")))
+  ;; don't match if no user is asked for
+  (auth-source-pass--with-store '(("user@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo")))
+  ;; don't match if user is different
+  (auth-source-pass--with-store '(("user1@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo" "user2")))
+  ;; don't match if sub-domain is asked for but user is different
+  (auth-source-pass--with-store '(("user1@bar.com"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.bar.com" "user2"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-port ()
+  (auth-source-pass--with-store '(("bar.com:8080"))
+    (should (auth-source-pass-match-entry-p "bar.com:8080" "bar.com" nil "8080"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-slash ()
+  ;; match if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com/user"))
+    (should (auth-source-pass-match-entry-p "foo.com/user" "foo.com" "user")))
+  ;; match with port if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com:8080/user"))
+    (should (auth-source-pass-match-entry-p "foo.com:8080/user" "foo.com" "user" "8080")))
+  ;; don't match if entry filename doesn't match user
+  (auth-source-pass--with-store '(("foo.com/baz"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.com" "user"))))
+
+(ert-deftest auth-source-pass-matching-entries-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                     '(nil ("bar.com#443/someone") nil nil nil nil
-                           nil nil nil nil nil nil))))))
-
-(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
-  (auth-source-pass--with-store
-      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
-    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo.com/bar password")))))
-
-(ert-deftest auth-source-pass-matching-entries-with-user-first ()
+      (should (auth-source-pass-match-entry-p "bar.com#443/someone" "bar.com" "someone" "443")))))
+
+(ert-deftest auth-source-pass--matching-entries-sort-results ()
+  (auth-source-pass--with-store '(("user@foo") ("foo"))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; same, but store is reversed
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
-                   '(("user@foo") nil ("foo"))))
-    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "subdir/foo password"))
-      (should (equal (auth-source-pass--get-attr "user" result)
-                     "someone")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password") ("user" . "someone"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
-                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
-
-(ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store
-      '(("host.com" ("secret" . "host.com password")))
-    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "host.com password")))))
-
-(ert-deftest auth-source-pass-matching-entries-host-with-port ()
-  (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                   '(nil nil nil nil ("host.com:443") nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
-  (let ((auth-source-pass-port-separator "#"))
-    (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                     '(nil nil nil nil ("host.com#443") nil
-                           nil nil nil nil nil nil))))))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; with sub-domain
+  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
+    (should (auth-source-pass--includes-sorted-entries '("foo.bar.com" "bar.com") "foo.bar.com")))
+  ;; matching user in the entry data takes priority
+  (auth-source-pass--with-store '(("dir1/bar.com") ("dir2/bar.com" ("user" . "user")))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user")))
+  ;; same, but store is reversed
+  (auth-source-pass--with-store '(("dir2/bar.com" ("user" . "user")) ("dir1/bar.com"))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user"))))
+
+(ert-deftest auth-source-pass-all-supported-organizations ()
+  ;; test every possible entry to store this data: user=rms host=gnu.org port=22
+  (dolist (entry '(;; only host name
+                   "gnu.org"
+                   ;; hostname + user
+                   "gnu.org/rms" "rms@gnu.org"
+                   ;; hostname + port
+                   "gnu.org:22"
+                   ;; hostname + user + port
+                   "gnu.org:22/rms" "rms@gnu.org:22"
+                   ;; all of the above in a random folder
+                   "a/b/gnu.org"
+                   "a/b/gnu.org/rms" "a/b/rms@gnu.org"
+                   "a/b/gnu.org:22"
+                   "a/b/gnu.org:22/rms" "a/b/rms@gnu.org:22"))
+    (auth-source-pass--with-store `((,entry))
+      (should (auth-source-pass-match-entry-p entry "gnu.org" "rms" "22")))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -300,33 +435,6 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "anuser")))))
 
-(ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
-  (let (passed-host)
-    (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port)
-                 (setq passed-host host)
-                 nil)))
-      (auth-source-pass--build-result "https://user@host.com:123" nil nil)
-      (should (equal passed-host "https://user@host.com:123"))
-      (auth-source-pass--build-result "https://user@host.com" nil nil)
-      (should (equal passed-host "https://user@host.com"))
-      (auth-source-pass--build-result "user@host.com" nil nil)
-      (should (equal passed-host "user@host.com"))
-      (auth-source-pass--build-result "user@host.com:443" nil nil)
-      (should (equal passed-host "user@host.com:443")))))
-
-(ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (auth-source-pass--with-store
-      '(("foo.site.com" ("secret" . "foo.site.com password"))
-        ("bar.site.com") ; An entry name with no data is invalid
-        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
-    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
-                   '(("secret" . "foo.site.com password"))))
-    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
-                   nil))
-    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
-                   '(("secret" . "mail/baz.site.com/scott password"))))))
-
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
     (auth-source-pass-enable)
@@ -334,6 +442,24 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :user) "someone"))
       (should (equal (plist-get result :host) "gitlab.com")))))
 
+(ert-deftest auth-source-pass-prints-meaningful-debug-log ()
+  (auth-source-pass--with-store '()
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "entries matching hostname=\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "corresponding suffixes to search for: .*\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "found no entries matching \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 1 entry matching \"gitlab.com\": \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("a/gitlab.com") ("b/gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 2 entries matching \"gitlab.com\": (\"a/gitlab.com\" \"b/gitlab.com\")"))))
+
 (provide 'auth-source-pass-tests)
 
 ;;; auth-source-pass-tests.el ends here
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0011-lisp-auth-source-pass.el-Version-5.0.0.patch --]
[-- Type: text/x-patch, Size: 794 bytes --]

From c3f49c38607c2a3f9bd64a563c0d9f19ce1c9514 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 28 May 2019 08:46:41 +0200
Subject: [PATCH 11/13] * lisp/auth-source-pass.el: Version 5.0.0

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index dea0dcb61a..624b7f621c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -5,7 +5,7 @@
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
 ;;         Keith Amidon <camalot@picnicpark.org>
-;; Version: 4.0.2
+;; Version: 5.0.0
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0012-etc-NEWS-Describe-changes-to-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 1170 bytes --]

From 4735a94344887320acf84c5b57773c8f48dbb8b5 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sun, 2 Jun 2019 11:08:40 +0200
Subject: [PATCH 12/13] * etc/NEWS: Describe changes to auth-source-pass

---
 etc/NEWS | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index 95d7e08074..f5781fb7e5 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1512,6 +1512,21 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
 value.  Auto Revert mode can use this information to avoid polling the
 buffer periodically when 'auto-revert-avoid-polling' is non-nil.
 
+** auth-source-pass
+
+*** New customizable variable 'auth-source-pass-filename'.
+Allows setting the path to the password-store, defaults to
+~/.password-store.
+
+*** New customizable variable 'auth-source-pass-port-separator'.
+Specifies separator between host and port, defaults to colon ":".
+
+*** Minimize the number of decryptions during password lookup.
+This makes the package usable with physical tokens requiring touching
+a sensor for every decryption.
+
+*** 'auth-source-pass-get' is now autoloaded.
+
 ** Bookmarks
 
 ---
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #14: 0013-doc-misc-auth.texi-The-Unix-password-store-Complete-.patch --]
[-- Type: text/x-patch, Size: 3678 bytes --]

From 7165f66b01ef0ff786a2416e3b4dab9db3c71c91 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Thu, 13 Jun 2019 21:54:21 +0200
Subject: [PATCH 13/13] * doc/misc/auth.texi (The Unix password store):
 Complete rewrite

---
 doc/misc/auth.texi | 47 +++++++++++++++++++++++++++++++++++-----------
 1 file changed, 36 insertions(+), 11 deletions(-)

diff --git a/doc/misc/auth.texi b/doc/misc/auth.texi
index a46e3d73fc..8d5971c137 100644
--- a/doc/misc/auth.texi
+++ b/doc/misc/auth.texi
@@ -445,19 +445,34 @@ The Unix password store
 
 @uref{http://www.passwordstore.org,,The standard unix password
 manager} (or just @code{pass}) stores your passwords in
-@code{gpg}-protected files following the Unix philosophy.
+@code{gpg}-protected files following the Unix philosophy. The store
+location (any directory) must be specified in the
+@code{auth-source-pass-filename} variable which defaults to
+@code{"~/.password-store"}.
 
-Emacs integration of @code{pass} follows the first approach suggested
-by the pass project itself for data organization to find data. This
-means that the filename of the file containing the password for a user
-on a particular host must contain the host name.  The file itself must
-contain the password on the first line, as well as a @code{username}
-field containing the username on a subsequent line. A @code{port}
-field can be used to differentiate the authentication data for several
-services with the same username on the same host.
+Emacs integration of @code{pass} follows the approach suggested by the
+pass project itself for data organization to find data. In particular,
+to store a password for the user @code{rms} on the host @code{gnu.org}
+on port @code{22}, you should use one of the following filenames.
 
-Users of @code{pass} may also be interested in functionality provided
-by other Emacs packages dealing with pass:
+@itemize
+@item @code{"gnu.org.gpg"} : No username or port in the filename means that any username and port will match.
+@item @code{"gnu.org/rms.gpg"} : The username to match can be expressed as filename inside a directory whose name matches the host. This is useful if the store has passwords for several users on the same host.
+@item @code{"rms@@gnu.org.gpg"} : The username can also be expressed as a prefix, separated from the host with an at-sign (@code{@@}).
+@item @code{"gnu.org:22.gpg"} : The port (aka. service) to match can only be expressed after the host and separated with a colon (@code{:}). The separator can be changed through the @code{auth-source-pass-port-separator} variable.
+@item @code{"gnu.org:22/rms.gpg"}
+@item @code{"rms@@gnu.org:22.gpg"}
+@item @code{"a/b/gnu.org.gpg"} : Entries can be stored in arbitrary directories.
+@item @code{"a/b/gnu.org/rms.gpg"}
+@item @code{"a/b/rms@@gnu.org.gpg"}
+@item @code{"a/b/gnu.org:22.gpg"}
+@item @code{"a/b/gnu.org:22/rms.gpg"}
+@item @code{"a/b/rms@@gnu.org:22.gpg"}
+@end itemize
+
+If several entries match, the one matching the most is
+preferred. Users of @code{pass} may also be interested in
+functionality provided by other Emacs packages:
 
 @itemize
 @item
@@ -468,6 +483,16 @@ The Unix password store
 @uref{https://github.com/jabranham/helm-pass,,helm-pass}: helm interface for pass.
 @end itemize
 
+@defvar auth-source-pass-filename
+Set this variable to a string locating the password store on the
+disk. Defaults to @code{"~/.password-store"}.
+@end defvar
+
+@defvar auth-source-pass-port-separator
+Set this variable to a string that should separate an host name from a
+port in an entry. Defaults to @code{":"}.
+@end defvar
+
 @node Help for developers
 @chapter Help for developers
 
-- 
2.21.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-13 19:59     ` Damien Cassou
@ 2019-06-13 21:23       ` Noam Postavsky
  2019-06-14  7:10       ` Damien Cassou
  2019-06-14  7:47       ` Eli Zaretskii
  2 siblings, 0 replies; 13+ messages in thread
From: Noam Postavsky @ 2019-06-13 21:23 UTC (permalink / raw)
  To: Damien Cassou
  Cc: Magnus Henoch, Nicolas Petton, Iku Iwasa, galaunay, Ted Zlatanov,
	36052, Keith Amidon

Damien Cassou <damien@cassou.me> writes:

> thank you so much to Eli and Noam for their detailed review. Here is a
> new patch series that address every concern received so far. This series
> also includes a new patch rewriting the "The Unix password store"
> section of auth.texi.

A couple more minor points, and some formatting nitpicks.

> Subject: [PATCH 04/13] Add auth-source-pass-filename option

> +(defcustom auth-source-pass-filename "~/.password-store"
> +  "Path to the password-store folder."

As Eli mentioned, the docstring should say Filename rather than Path too
(I guess this got missed/mixed up with my similar point about the
variable name).

> Subject: [PATCH 10/13] Refactoring of auth-source-pass
>
> * lisp/auth-source-pass.el (auth-source-pass--find-match): Refactor by
> moving some code to auth-source-pass--disambiguate.
> (auth-source-pass--disambiguate):
> (auth-source-pass--entries-matching-suffix): New function.

When you have two or more ChangeLog entries from the same file sharing
the same message, don't leave a colon at the end of the blank lines (see
also the example in CONTRIBUTE).  I.e., this should be

    (auth-source-pass--disambiguate)
    (auth-source-pass--entries-matching-suffix): New function.

Same for the other entries below.

> (auth-source-pass--find-match-unambiguous):
> (auth-source-pass--select-from-entries):
> (auth-source-pass--entry-reducer): Refactor to simplify and improve
> logging.
> (auth-source-pass--matching-entries):
> (auth-source-pass--accumulate-matches): Remove
                                                ^
> * test/lisp/auth-source-pass-tests.el: Complete rewrite to facilitate
> maintenance.
> (auth-source-pass--have-message-containing): Remove
                                                     ^
Period at the end of sentence.

> (auth-source-pass--have-message-matching):
> (auth-source-pass--explain--have-message-matching):
> (auth-source-pass--explain-match-entry-p):
> (auth-source-pass--includes-sorted-entries):
> (auth-source-pass--explain-includes-sorted-entries):
> (auth-source-pass--explain-match-any-entry-p):
> (auth-source-pass--matching-entries):
> (auth-source-pass-match-entry-p):
> (auth-source-pass-match-any-entry-p): New function.

> Subject: [PATCH 12/13] * etc/NEWS: Describe changes to auth-source-pass

> +** auth-source-pass
> +
> +*** New customizable variable 'auth-source-pass-filename'.
> +Allows setting the path to the password-store, defaults to
> +~/.password-store.
> +
> +*** New customizable variable 'auth-source-pass-port-separator'.
> +Specifies separator between host and port, defaults to colon ":".

I guess these NEWS entries should be marked +++ since you have added
text to the manual about them.

> +*** Minimize the number of decryptions during password lookup.
> +This makes the package usable with physical tokens requiring touching
> +a sensor for every decryption.
> +
> +*** 'auth-source-pass-get' is now autoloaded.

And these should have ---.

> Subject: [PATCH 13/13] * doc/misc/auth.texi (The Unix password store):
>  Complete rewrite

> --- a/doc/misc/auth.texi
> +++ b/doc/misc/auth.texi
> @@ -445,19 +445,34 @@ The Unix password store
>  
>  @uref{http://www.passwordstore.org,,The standard unix password
>  manager} (or just @code{pass}) stores your passwords in

> +@code{gpg}-protected files following the Unix philosophy. The store
                                                            ^ 
Double spacing at the end of sentence.  Same for the rest of this patch.

> +Emacs integration of @code{pass} follows the approach suggested by the
> +pass project itself for data organization to find data. In particular,
> +to store a password for the user @code{rms} on the host @code{gnu.org}
> +on port @code{22}, you should use one of the following filenames.

> +@itemize
> +@item @code{"gnu.org.gpg"} : No username or port in the filename means that any username and port will match.
> +@item @code{"gnu.org/rms.gpg"} : The username to match can be expressed as filename inside a directory whose name matches the host. This is useful if the store has passwords for several users on the same host.
> +@item @code{"rms@@gnu.org.gpg"} : The username can also be expressed as a prefix, separated from the host with an at-sign (@code{@@}).
> +@item @code{"gnu.org:22.gpg"} : The port (aka. service) to match can only be expressed after the host and separated with a colon (@code{:}). The separator can be changed through the @code{auth-source-pass-port-separator} variable.

> +If several entries match, the one matching the most is
> +preferred.

"the most" meaning highest number of matching items (where an "item" is
one of username, port or filename)?  The current wording could be
understood as "longest match" (counting characters) which I don't think
is what you meant.





^ permalink raw reply	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-13 19:59     ` Damien Cassou
  2019-06-13 21:23       ` Noam Postavsky
@ 2019-06-14  7:10       ` Damien Cassou
  2019-06-14  7:47       ` Eli Zaretskii
  2 siblings, 0 replies; 13+ messages in thread
From: Damien Cassou @ 2019-06-14  7:10 UTC (permalink / raw)
  To: 36052
  Cc: Magnus Henoch, Nicolas Petton, Noam Postavsky, Iku Iwasa,
	Keith Amidon, galaunay, Ted Zlatanov

[-- Attachment #1: Type: text/plain, Size: 369 bytes --]

Hi everyone,

after yet another detailed review from Noam, here is my new series of
patch. I hopefully took care of every feedback I got.

Thank you so much to the reviewers, you are doing an amazing job.

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Fix-auth-source-pass-to-return-nil-if-no-entry-found.patch --]
[-- Type: text/x-patch, Size: 1985 bytes --]

From e6948470878682577a976dcca41d7ae7af2c39c9 Mon Sep 17 00:00:00 2001
From: Magnus Henoch <magnus.henoch@gmail.com>
Date: Fri, 2 Nov 2018 21:51:59 +0000
Subject: [PATCH 01/13] Fix auth-source-pass to return nil if no entry found

* lisp/auth-source-pass.el (auth-source-pass-search): If there is no
matching entry, auth-source-pass-search should return nil, not (nil).
This lets auth-source fall back to other backends in the auth-sources
list.
* test/lisp/auth-source-pass-tests.el: Add corresponding test.

Copyright-paperwork-exempt: yes
---
 lisp/auth-source-pass.el            | 3 ++-
 test/lisp/auth-source-pass-tests.el | 5 +++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4283ed0392..c82c90167a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -56,7 +56,8 @@
          ;; Do not build a result, as none will match when HOST is nil
          nil)
         (t
-         (list (auth-source-pass--build-result host port user)))))
+         (when-let ((result (auth-source-pass--build-result host port user)))
+           (list result)))))
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index d1e486ad6b..ab9ef92c14 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -83,6 +83,11 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host nil))))
 
+(ert-deftest auth-source-pass-not-found ()
+  (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
+                                  ("bar"))
+    (should-not (auth-source-pass-search :host "baz"))))
+
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
   (auth-source-pass--with-store '(("foo"))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-lisp-auth-source-pass.el-Version-4.0.2.patch --]
[-- Type: text/x-patch, Size: 745 bytes --]

From b4778d0e47c8b6fea2f5c805bb830dd169cb8b75 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sat, 3 Nov 2018 09:06:42 +0100
Subject: [PATCH 02/13] * lisp/auth-source-pass.el: Version 4.0.2

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index c82c90167a..d092292e51 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,7 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
-;; Version: 4.0.1
+;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-lisp-auth-source-pass.el-auth-source-pass-get-Add-au.patch --]
[-- Type: text/x-patch, Size: 758 bytes --]

From fe947369b636e85adf0649124c09800076698567 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 6 Nov 2018 14:45:20 +0100
Subject: [PATCH 03/13] * lisp/auth-source-pass.el (auth-source-pass-get): Add
 autoload

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index d092292e51..4fcb1015e7 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -98,6 +98,7 @@ auth-source-pass-backend-parse
   (advice-add 'auth-source-backend-parse :before-until #'auth-source-pass-backend-parse))
 
 \f
+;;;###autoload
 (defun auth-source-pass-get (key entry)
   "Return the value associated to KEY in the password-store entry ENTRY.
 
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Add-auth-source-pass-filename-option.patch --]
[-- Type: text/x-patch, Size: 2116 bytes --]

From 9609227fb0b1c38471835ff9bd81217e3568562c Mon Sep 17 00:00:00 2001
From: galaunay <gaby.launay@tutanota.com>
Date: Sun, 13 Jan 2019 21:30:53 +0000
Subject: [PATCH 04/13] Add auth-source-pass-filename option

* lisp/auth-source-pass.el (auth-source-pass)
(auth-source-pass-filename): Add option to specify a customized
password-store path.
(auth-source-pass--read-entry)
(auth-source-pass-entries): Use the new option instead of hard-coded
`~/.password-store'.
---
 lisp/auth-source-pass.el | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4fcb1015e7..1fda698232 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -38,6 +38,17 @@
 (require 'auth-source)
 (require 'url-parse)
 
+(defgroup auth-source-pass nil
+  "password-store integration within auth-source."
+  :prefix "auth-source-pass-"
+  :group 'auth-source
+  :version "27.1")
+
+(defcustom auth-source-pass-filename "~/.password-store"
+  "Filename of the password-store folder."
+  :type 'directory
+  :version "27.1")
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -121,7 +132,7 @@ auth-source-pass--read-entry
   (with-temp-buffer
     (insert-file-contents (expand-file-name
                            (format "%s.gpg" entry)
-                           "~/.password-store"))
+                           auth-source-pass-filename))
     (buffer-substring-no-properties (point-min) (point-max))))
 
 (defun auth-source-pass-parse-entry (entry)
@@ -188,7 +199,7 @@ auth-source-pass--entry-valid-p
 ;; in Emacs
 (defun auth-source-pass-entries ()
   "Return a list of all password store entries."
-  (let ((store-dir (expand-file-name "~/.password-store/")))
+  (let ((store-dir (expand-file-name auth-source-pass-filename)))
     (mapcar
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Add-auth-source-pass-port-separator-option.patch --]
[-- Type: text/x-patch, Size: 3396 bytes --]

From b403aad81204d92505b78aadb7fb16541247c22d Mon Sep 17 00:00:00 2001
From: Iku Iwasa <iku.iwasa@gmail.com>
Date: Sun, 7 Apr 2019 17:59:59 +0900
Subject: [PATCH 05/13] Add auth-source-pass-port-separator option

* lisp/auth-source-pass.el (auth-source-pass-port-separator): New
option to specify separator between host and port, default to
colon (":").
(auth-source-pass--find-match-unambiguous): Adapt to make use of the
new variable.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            | 17 ++++++++++++++---
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 1fda698232..626dbf842c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -49,6 +49,11 @@ auth-source-pass-filename
   :type 'directory
   :version "27.1")
 
+(defcustom auth-source-pass-port-separator ":"
+  "Separator string between host and port in entry filename."
+  :type 'string
+  :version "27.1")
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -254,9 +259,15 @@ auth-source-pass--find-match-unambiguous
 
 HOSTNAME should not contain any username or port number."
   (or
-   (and user port (auth-source-pass--find-one-by-entry-name (format "%s@%s:%s" user hostname port) user))
-   (and user (auth-source-pass--find-one-by-entry-name (format "%s@%s" user hostname) user))
-   (and port (auth-source-pass--find-one-by-entry-name (format "%s:%s" hostname port) nil))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
+                   user))
+   (and user (auth-source-pass--find-one-by-entry-name
+              (format "%s@%s" user hostname)
+              user))
+   (and port (auth-source-pass--find-one-by-entry-name
+              (format "%s%s%s" hostname auth-source-pass-port-separator port)
+              nil))
    (auth-source-pass--find-one-by-entry-name hostname user)
    ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
    (let ((components (split-string hostname "\\.")))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ab9ef92c14..ae7a696bc6 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -186,6 +186,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
                    "host.com"))))
 
+(ert-deftest auth-source-pass-find-host-with-port ()
+  (auth-source-pass--with-store '(("host.com:443"))
+    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                   "host.com:443"))))
+
+(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("host.com#443"))
+      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                     "host.com#443")))))
+
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
   (declare (indent 1))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Fix-auth-source-pass-to-search-for-hostname-port-use.patch --]
[-- Type: text/x-patch, Size: 2729 bytes --]

From 2171a0c8e4e33e1791d6f194370f695452a6037c Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Thu, 14 Feb 2019 10:27:31 -0800
Subject: [PATCH 06/13] Fix auth-source-pass to search for
 hostname:port/username

auth-source-pass supports entries with username either prefixed to the
hostname with an @ as separator or in a subdirectory under the
hostname.  This was true when there was no port or service included in
the name, but got broken with the introduction of
auth-source-pass-port-separator.

* lisp/auth-source-pass.el (auth-source-pass--find-match-unambiguous): Fix
to match hostname:port/username.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            |  3 +++
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 14 insertions(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 626dbf842c..4aa0853be9 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -262,6 +262,9 @@ auth-source-pass--find-match-unambiguous
    (and user port (auth-source-pass--find-one-by-entry-name
                    (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
                    user))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
+                   user))
    (and user (auth-source-pass--find-one-by-entry-name
               (format "%s@%s" user hostname)
               user))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ae7a696bc6..1539d9611f 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -144,6 +144,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
                    nil))))
 
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+  (auth-source-pass--with-store '(("bar.com:443/someone"))
+    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                   "bar.com:443/someone"))))
+
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("bar.com#443/someone"))
+      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                     "bar.com#443/someone")))))
+
 (ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
   (auth-source-pass--with-store '(("foo.com/bar"))
     (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-Split-out-the-attribute-retrieval-form-auth-source-p.patch --]
[-- Type: text/x-patch, Size: 3031 bytes --]

From 4263c5673e0df4231921578d371396eec80c8f04 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Tue, 30 Apr 2019 07:52:14 -0700
Subject: [PATCH 07/13] Split out the attribute retrieval form
 auth-source-pass-get

Eliminate the need to repeatedly retrieve and parse the data for the
entry.  This is generally a good thing since it eliminates repetitions
of the same crypto and parsing operations.  It is especially valuable
when protecting an entry with a yubikey with touch required for crypto
operations as it eliminates the need to touch the yubikey sensor for
each attribute retrieved.

* lisp/auth-source-pass.el (auth-source-pass-get): Extract some code to
`auth-source-pass--get-attr'.
(auth-source-pass--get-attr): New function to get a field value from a
parsed entry.
(auth-source-pass--build-result): Make use of
`auth-source-pass--get-attr` to avoid repeated parsing.
---
 lisp/auth-source-pass.el | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4aa0853be9..a0b0841e1f 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -79,11 +79,12 @@ auth-source-pass--build-result
   "Build auth-source-pass entry matching HOST, PORT and USER."
   (let ((entry (auth-source-pass--find-match host user port)))
     (when entry
-      (let ((retval (list
-                     :host host
-                     :port (or (auth-source-pass-get "port" entry) port)
-                     :user (or (auth-source-pass-get "user" entry) user)
-                     :secret (lambda () (auth-source-pass-get 'secret entry)))))
+      (let* ((entry-data (auth-source-pass-parse-entry entry))
+             (retval (list
+                      :host host
+                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -128,9 +129,18 @@ auth-source-pass-get
 key1: value1
 key2: value2"
   (let ((data (auth-source-pass-parse-entry entry)))
-    (or (cdr (assoc key data))
-        (and (string= key "user")
-             (cdr (assoc "username" data))))))
+    (auth-source-pass--get-attr key data)))
+
+(defun auth-source-pass--get-attr (key entry-data)
+  "Return value associated with KEY in an ENTRY-DATA.
+
+ENTRY-DATA is the data from a parsed password-store entry.
+The key used to retrieve the password is the symbol `secret'.
+
+See `auth-source-pass-get'."
+  (or (cdr (assoc key entry-data))
+      (and (string= key "user")
+           (cdr (assoc "username" entry-data)))))
 
 (defun auth-source-pass--read-entry (entry)
   "Return a string with the file content of ENTRY."
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-Minimize-entry-parsing-in-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 33974 bytes --]

From fc79728f4d54249902175a5dd389b84d1352ad1b Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sun, 5 May 2019 20:21:43 -0700
Subject: [PATCH 08/13] Minimize entry parsing in auth-source-pass

Prior to this commit, while searching for the most applicable entry
password-store entries were decrypted and parsed to ensure they were
valid.  The entries were parsed in the order they were found on the
filesystem and all applicable entries would be decrypted and parsed,
which varied based on the contents of the password-store and the entry
to be found.

This is fine when the GPG key is cached and each entry can be
decrypted without user interaction.  However, for security some people
have their GPG on a hardware token like a Yubikey setup so that they
have to touch a sensor on the toke for every cryptographic operation,
in which case it becomes inconvenient as each attempt to find an entry
requires a variable number of touches of the hardware token.

The implementation already assumes that names which contain more of
the information in the search key should be preferred so there is an
ordering of preference of applicable entries.  If the decrypt and
parsing is removed from the initial identification of applicable
entries in the store then in most cases a single decrypt and parse of
the most preferred entry will suffice, improving the experience for
hardware token users that require interaction with the token.

This commit implements that strategy.  It is in spirit a refactor of
the existing code.

* lisp/auth-source-pass.el (auth-source-pass--matching-entries): New
function, generate an ordered list of regular expression matchers for
all possible names that could be in the password-store for the entry
to be found and then makes a pass over the password-store entry names
accumulating the matching entries in a list after the regexp that
matched.  This implementation ensures the password-store entry list
still only has to be scanned once.
(auth-source-pass--find-match-unambiguous): Use it to obtain candidate
entries and then parse them one by one until an entry containing the
desired information is located.  When complete, return the parsed data
of the entry instead of the entry name so that the information can be
used directly to construct the auth-source response.
(auth-source-pass--build-result): Update accordingly.
(auth-source-pass--find-match): Update docstring accordingly.
(auth-source-pass--select-one-entry)
(auth-source-pass--entry-valid-p)
(auth-source-pass--find-all-by-entry-name)
(auth-source-pass--find-one-by-entry-name): Remove.
(auth-source-pass--select-from-entries)
(auth-source-pass--accumulate-matches)
(auth-source-pass--entry-reducer)
(auth-source-pass--generate-entry-suffixes)
(auth-source-pass--domains)
(auth-source-pass--name-port-user-suffixes): New functions.

* test/lisp/auth-source-pass-tests.el: One test case was added to the
test suite to verify that only the minimal number of entries are
parsed in common cases.  The
auth-source-pass-only-return-entries-that-can-be-open test case had to
be re-implemented because the function it was used eliminated as the
functionality is provided elsewhere.  All the other fairly substantial
changes to the test suite are the result of mechanical changes that
were required to adapt to auth-source-pass--find-match returning the
data from a parsed password-store entry instead of the entry name.
---
 lisp/auth-source-pass.el            | 209 ++++++++++++----------
 test/lisp/auth-source-pass-tests.el | 264 +++++++++++++++++-----------
 2 files changed, 281 insertions(+), 192 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index a0b0841e1f..bcb215a6ac 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -77,14 +77,13 @@ auth-source-pass-port-separator
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
-  (let ((entry (auth-source-pass--find-match host user port)))
-    (when entry
-      (let* ((entry-data (auth-source-pass-parse-entry entry))
-             (retval (list
-                      :host host
-                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
-                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
-                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
+  (let ((entry-data (auth-source-pass--find-match host user port)))
+    (when entry-data
+      (let ((retval (list
+                     :host host
+                     :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                     :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                     :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -183,33 +182,6 @@ auth-source-pass--do-debug
          (cons (concat "auth-source-pass: " (car msg))
                (cdr msg))))
 
-(defun auth-source-pass--select-one-entry (entries user)
-  "Select one entry from ENTRIES by searching for a field matching USER."
-  (let ((number (length entries))
-        (entry-with-user
-         (and user
-              (seq-find (lambda (entry)
-                          (string-equal (auth-source-pass-get "user" entry) user))
-                        entries))))
-    (auth-source-pass--do-debug "found %s matches: %s" number
-                                (mapconcat #'identity entries ", "))
-    (if entry-with-user
-        (progn
-          (auth-source-pass--do-debug "return %s as it contains matching user field"
-                                      entry-with-user)
-          entry-with-user)
-      (auth-source-pass--do-debug "return %s as it is the first one" (car entries))
-      (car entries))))
-
-(defun auth-source-pass--entry-valid-p (entry)
-  "Return t iff ENTRY can be opened.
-Also displays a warning if not.  This function is slow, don't call it too
-often."
-  (if (auth-source-pass-parse-entry entry)
-      t
-    (auth-source-pass--do-debug "entry '%s' is not valid" entry)
-    nil))
-
 ;; TODO: add tests for that when `assess-with-filesystem' is included
 ;; in Emacs
 (defun auth-source-pass-entries ()
@@ -219,37 +191,8 @@ auth-source-pass-entries
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
 
-(defun auth-source-pass--find-all-by-entry-name (entryname user)
-  "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME.
-Only return valid entries as of `auth-source-pass--entry-valid-p'."
-  (seq-filter (lambda (entry)
-                (and
-                 (or
-                  (let ((components-host-user
-                         (member entryname (split-string entry "/"))))
-                    (and (= (length components-host-user) 2)
-                         (string-equal user (cadr components-host-user))))
-                  (string-equal entryname (file-name-nondirectory entry)))
-                 (auth-source-pass--entry-valid-p entry)))
-              (auth-source-pass-entries)))
-
-(defun auth-source-pass--find-one-by-entry-name (entryname user)
-  "Search the store for an entry matching ENTRYNAME.
-If USER is non nil, give precedence to entries containing a user field
-matching USER."
-  (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
-                              entryname
-                              user)
-  (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user)))
-    (pcase (length matching-entries)
-      (0 (auth-source-pass--do-debug "no match found")
-         nil)
-      (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries))
-         (car matching-entries))
-      (_ (auth-source-pass--select-one-entry matching-entries user)))))
-
 (defun auth-source-pass--find-match (host user port)
-  "Return a password-store entry name matching HOST, USER and PORT.
+  "Return password-store entry data matching HOST, USER and PORT.
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
@@ -263,33 +206,123 @@ auth-source-pass--find-match
      (or port (number-to-string (url-port url))))))
 
 (defun auth-source-pass--find-match-unambiguous (hostname user port)
-  "Return a password-store entry name matching HOSTNAME, USER and PORT.
+  "Return password-store entry data matching HOSTNAME, USER and PORT.
 If many matches are found, return the first one.  If no match is found,
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (or
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
-                   user))
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
-                   user))
-   (and user (auth-source-pass--find-one-by-entry-name
-              (format "%s@%s" user hostname)
-              user))
-   (and port (auth-source-pass--find-one-by-entry-name
-              (format "%s%s%s" hostname auth-source-pass-port-separator port)
-              nil))
-   (auth-source-pass--find-one-by-entry-name hostname user)
-   ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
-   (let ((components (split-string hostname "\\.")))
-     (when (= (length components) 3)
-       ;; start from scratch
-       (auth-source-pass--find-match-unambiguous
-        (mapconcat 'identity (cdr components) ".")
-        user
-        port)))))
+  (cl-reduce
+   (lambda (result entries)
+     (or result
+         (pcase (length entries)
+           (0 nil)
+           (1 (auth-source-pass-parse-entry (car entries)))
+           (_ (auth-source-pass--select-from-entries entries user)))))
+   (auth-source-pass--matching-entries hostname user port)
+   :initial-value nil))
+
+(defun auth-source-pass--select-from-entries (entries user)
+  "Return best matching password-store entry data from ENTRIES.
+
+If USER is non nil, give precedence to entries containing a user field
+matching USER."
+  (cl-reduce
+   (lambda (result entry)
+     (let ((entry-data (auth-source-pass-parse-entry entry)))
+       (cond ((equal (auth-source-pass--get-attr "user" result) user)
+              result)
+             ((equal (auth-source-pass--get-attr "user" entry-data) user)
+              entry-data)
+             (t
+              result))))
+   entries
+   :initial-value (auth-source-pass-parse-entry (car entries))))
+
+(defun auth-source-pass--matching-entries (hostname user port)
+  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
+
+The result is a list of lists of password-store entries, where
+each sublist contains entries that actually exist in the
+password-store matching one of the entry name formats that
+auth-source-pass expects, most specific to least specific."
+  (let* ((entries-lists (mapcar
+                         #'cdr
+                         (auth-source-pass--accumulate-matches hostname user port)))
+         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
+    (if entries
+        (auth-source-pass--do-debug (format "found: %S" entries))
+      (auth-source-pass--do-debug "no matches found"))
+    entries-lists))
+
+(defun auth-source-pass--accumulate-matches (hostname user port)
+  "Accumulate matching password-store entries into sublists.
+
+Entries matching supported formats that combine HOSTNAME, USER, &
+PORT are accumulated into sublists where the car of each sublist
+is a regular expression for matching paths in the password-store
+and the remainder is the list of matching entries."
+  (let ((suffix-match-lists
+         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
+                 (auth-source-pass--generate-entry-suffixes hostname user port))))
+    (cl-reduce #'auth-source-pass--entry-reducer
+               (auth-source-pass-entries)
+               :initial-value suffix-match-lists)))
+
+(defun auth-source-pass--entry-reducer (match-lists entry)
+  "Match MATCH-LISTS sublists against ENTRY.
+
+The result is a copy of match-lists with the entry added to the
+end of any sublists for which the regular expression at the head
+of the list matches the entry name."
+  (mapcar (lambda (match-list)
+            (if (string-match (car match-list) entry)
+                (append match-list (list entry))
+              match-list))
+          match-lists))
+
+(defun auth-source-pass--generate-entry-suffixes (hostname user port)
+  "Return a list of possible entry path suffixes in the password-store.
+
+Based on the supported pathname patterns for HOSTNAME, USER, &
+PORT, return a list of possible suffixes for matching entries in
+the password-store."
+  (let ((domains (auth-source-pass--domains (split-string hostname "\\."))))
+    (seq-mapcat (lambda (n)
+                  (auth-source-pass--name-port-user-suffixes n user port))
+                domains)))
+
+(defun auth-source-pass--domains (name-components)
+  "Return a list of possible domain names matching the hostname.
+
+This function takes a list of NAME-COMPONENTS, the strings
+separated by periods in the hostname, and returns a list of full
+domain names containing the trailing sequences of those
+components, from longest to shortest."
+  (cl-maplist (lambda (components) (mapconcat #'identity components "."))
+              name-components))
+
+(defun auth-source-pass--name-port-user-suffixes (name user port)
+  "Return a list of possible path suffixes for NAME, USER, & PORT.
+
+The resulting list is ordered from most specifc to least
+specific, with paths matching all of NAME, USER, & PORT first,
+then NAME & USER, then NAME & PORT, then just NAME."
+  (seq-mapcat
+   #'identity
+   (list
+    (when (and user port)
+      (list
+       (format "%s@%s%s%s" user name auth-source-pass-port-separator port)
+       (format "%s%s%s/%s" name auth-source-pass-port-separator port user)))
+    (when user
+      (list
+       (format "%s@%s" user name)
+       (format "%s/%s" name user)))
+    (when port
+      (list
+       (format "%s%s%s" name auth-source-pass-port-separator port)))
+    (list
+     (format "%s" name)))))
 
 (provide 'auth-source-pass)
 ;;; auth-source-pass.el ends here
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 1539d9611f..2c28f79945 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -63,14 +63,19 @@ auth-source-pass--debug
 This function is intended to be set to `auth-source-debug`."
   (add-to-list 'auth-source-pass--debug-log (apply #'format msg) t))
 
+(defvar auth-source-pass--parse-log nil)
+
 (defmacro auth-source-pass--with-store (store &rest body)
   "Use STORE as password-store while executing BODY."
   (declare (indent 1))
-  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry) (lambda (entry) (cdr (cl-find entry ,store :key #'car :test #'string=))) )
-             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store)))
-             ((symbol-function 'auth-source-pass--entry-valid-p) (lambda (_entry) t)))
+  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+              (lambda (entry)
+                (add-to-list 'auth-source-pass--parse-log entry)
+                (cdr (cl-find entry ,store :key #'car :test #'string=))))
+             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store))))
      (let ((auth-source-debug #'auth-source-pass--debug)
-           (auth-source-pass--debug-log nil))
+           (auth-source-pass--debug-log nil)
+           (auth-source-pass--parse-log nil))
        ,@body)))
 
 (ert-deftest auth-source-pass-any-host ()
@@ -88,125 +93,184 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass-find-match-minimal-parsing ()
+  (let ((store-contents
+         '(("baz" ("secret" . "baz password"))
+           ("baz:123" ("secret" . "baz:123 password"))
+           ("baz/foo" ("secret" . "baz/foo password"))
+           ("foo@baz" ("secret" . "foo@baz password"))
+           ("baz:123/foo" ("secret" . "baz:123/foo password"))
+           ("foo@baz:123" ("secret" . "foo@baz:123 password"))
+           ("bar.baz" ("secret" . "bar.baz password"))
+           ("bar.baz:123" ("secret" . "bar.baz:123 password"))
+           ("bar.baz/foo" ("secret" . "bar.baz/foo password"))
+           ("foo@bar.baz" ("secret" . "foo@bar.baz password"))
+           ("bar.baz:123/foo" ("secret" . "bar.baz:123/foo password"))
+           ("foo@bar.baz:123" ("secret" . "foo@bar.baz:123 password")))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" "123")
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" nil)
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil "123")
+      (should (equal auth-source-pass--parse-log '("bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("baz"))))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store '(("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store '(("SomeUser@foo") ("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
+        ("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store '(("foo") ("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
+
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil ("bar.com") nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
   (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   "someone@bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-bad-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
   (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-prefer-full ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
   (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "foo.bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(("foo.bar.com") ("bar.com") nil)))))
 
 (ert-deftest auth-source-pass-dont-match-at-folder-name ()
   (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
   (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                   "bar.com:443/someone"))))
+    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                   '(nil ("bar.com:443/someone") nil nil nil nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                     "bar.com#443/someone")))))
-
-(ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
-  (auth-source-pass--with-store '(("foo.com/bar"))
-    (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-                   "foo.com/bar"))))
-
-(ert-deftest auth-source-pass-search-with-user-first ()
+      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                     '(nil ("bar.com#443/someone") nil nil nil nil
+                           nil nil nil nil nil nil))))))
+
+(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
+  (auth-source-pass--with-store
+      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
+    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo.com/bar password")))))
+
+(ert-deftest auth-source-pass-matching-entries-with-user-first ()
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--find-match "foo" "user" nil)
-                   "user@foo"))
-    (auth-source-pass--should-have-message-containing "Found 1 match")))
+    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
+                   '(("user@foo") nil ("foo"))))
+    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo" ("user" . "someone")))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "subdir/foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "subdir/foo password"))
+      (should (equal (auth-source-pass--get-attr "user" result)
+                     "someone")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store '(("foo" ("user" . "someone")) ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password") ("user" . "someone"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "the first one")))
-
-(ert-deftest auth-source-pass-make-divansantana-happy ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
+
+(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
   (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "smtp.host.com" "myusername@host.co.za" nil)
-                   "host.com"))))
+    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
+                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
 
 (ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
-                   "host.com"))))
+  (auth-source-pass--with-store
+      '(("host.com" ("secret" . "host.com password")))
+    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "host.com password")))))
 
-(ert-deftest auth-source-pass-find-host-with-port ()
+(ert-deftest auth-source-pass-matching-entries-host-with-port ()
   (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                   "host.com:443"))))
+    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                   '(nil nil nil nil ("host.com:443") nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                     "host.com#443")))))
+      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                     '(nil nil nil nil ("host.com#443") nil
+                           nil nil nil nil nil nil))))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -218,7 +282,8 @@ auth-source-pass--with-store-find-foo
        ,@body)))
 
 (ert-deftest auth-source-pass-build-result-return-parameters ()
-  (auth-source-pass--with-store-find-foo '(("foo"))
+  (auth-source-pass--with-store-find-foo
+      '(("foo" ("secret" . "foo password")))
     (let ((result (auth-source-pass--build-result "foo" 512 "user")))
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "user")))))
@@ -238,7 +303,9 @@ auth-source-pass--with-store-find-foo
 (ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
   (let (passed-host)
     (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port) (setq passed-host host))))
+               (lambda (host _user _port)
+                 (setq passed-host host)
+                 nil)))
       (auth-source-pass--build-result "https://user@host.com:123" nil nil)
       (should (equal passed-host "https://user@host.com:123"))
       (auth-source-pass--build-result "https://user@host.com" nil nil)
@@ -249,27 +316,16 @@ auth-source-pass--with-store-find-foo
       (should (equal passed-host "user@host.com:443")))))
 
 (ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (cl-letf (((symbol-function 'auth-source-pass-entries)
-             (lambda () '("foo.site.com" "bar.site.com" "mail/baz.site.com/scott")))
-            ((symbol-function 'auth-source-pass--entry-valid-p)
-             ;; only foo.site.com and "mail/baz.site.com/scott" are valid
-             (lambda (entry) (member entry '("foo.site.com" "mail/baz.site.com/scott")))))
-    (should (equal (auth-source-pass--find-all-by-entry-name "foo.site.com" "someuser")
-                   '("foo.site.com")))
-    (should (equal (auth-source-pass--find-all-by-entry-name "bar.site.com" "someuser")
-                   '()))
-    (should (equal (auth-source-pass--find-all-by-entry-name "baz.site.com" "scott")
-                   '("mail/baz.site.com/scott")))))
-
-(ert-deftest auth-source-pass-entry-is-not-valid-when-unreadable ()
-  (cl-letf (((symbol-function 'auth-source-pass--read-entry)
-             (lambda (entry)
-               ;; only foo is a valid entry
-               (if (string-equal entry "foo")
-                   "password"
-                 nil))))
-    (should (auth-source-pass--entry-valid-p "foo"))
-    (should-not (auth-source-pass--entry-valid-p "bar"))))
+  (auth-source-pass--with-store
+      '(("foo.site.com" ("secret" . "foo.site.com password"))
+        ("bar.site.com") ; An entry name with no data is invalid
+        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
+    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
+                   '(("secret" . "foo.site.com password"))))
+    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
+                   nil))
+    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
+                   '(("secret" . "mail/baz.site.com/scott password"))))))
 
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-lisp-auth-source-pass.el-Add-Keith-Amidon-to-authors.patch --]
[-- Type: text/x-patch, Size: 755 bytes --]

From 61ca0e36e192ebaa389900366fb03432ed342d7a Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sat, 11 May 2019 08:22:56 -0700
Subject: [PATCH 09/13] * lisp/auth-source-pass.el: Add Keith Amidon to authors

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index bcb215a6ac..af421645cb 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,6 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
+;;         Keith Amidon <camalot@picnicpark.org>
 ;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0010-Refactoring-of-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 36669 bytes --]

From 380780bdccecee625c61f27e46754c14058b0e2b Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 14 May 2019 05:50:59 +0200
Subject: [PATCH 10/13] Refactoring of auth-source-pass

* lisp/auth-source-pass.el (auth-source-pass--find-match): Refactor by
moving some code to auth-source-pass--disambiguate.
(auth-source-pass--disambiguate)
(auth-source-pass--entries-matching-suffix): New function.
(auth-source-pass--find-match-unambiguous)
(auth-source-pass--select-from-entries)
(auth-source-pass--entry-reducer): Refactor to simplify and improve
logging.
(auth-source-pass--matching-entries)
(auth-source-pass--accumulate-matches): Remove.
* test/lisp/auth-source-pass-tests.el: Complete rewrite to facilitate
maintenance.
(auth-source-pass--have-message-containing): Remove.
(auth-source-pass--have-message-matching)
(auth-source-pass--explain--have-message-matching)
(auth-source-pass--explain-match-entry-p)
(auth-source-pass--includes-sorted-entries)
(auth-source-pass--explain-includes-sorted-entries)
(auth-source-pass--explain-match-any-entry-p)
(auth-source-pass--matching-entries)
(auth-source-pass-match-entry-p)
(auth-source-pass-match-any-entry-p): New function.
---
 lisp/auth-source-pass.el            | 108 +++----
 test/lisp/auth-source-pass-tests.el | 482 ++++++++++++++++++----------
 2 files changed, 349 insertions(+), 241 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index af421645cb..295bda507b 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -197,10 +197,17 @@ auth-source-pass--find-match
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
+  (apply #'auth-source-pass--find-match-unambiguous (auth-source-pass--disambiguate host user port)))
+
+(defun auth-source-pass--disambiguate (host &optional user port)
+  "Return (HOST USER PORT) after disambiguation.
+Disambiguate between having user provided inside HOST (e.g.,
+user@server.com) and inside USER by giving priority to USER.
+Same for PORT."
   (let* ((url (url-generic-parse-url (if (string-match-p ".*://" host)
                                          host
                                        (format "https://%s" host)))))
-    (auth-source-pass--find-match-unambiguous
+    (list
      (or (url-host url) host)
      (or user (url-user url))
      ;; url-port returns 443 (because of the https:// above) by default
@@ -212,74 +219,49 @@ auth-source-pass--find-match-unambiguous
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (cl-reduce
-   (lambda (result entries)
-     (or result
-         (pcase (length entries)
-           (0 nil)
-           (1 (auth-source-pass-parse-entry (car entries)))
-           (_ (auth-source-pass--select-from-entries entries user)))))
-   (auth-source-pass--matching-entries hostname user port)
-   :initial-value nil))
+  (let ((all-entries (auth-source-pass-entries))
+        (suffixes (auth-source-pass--generate-entry-suffixes hostname user port)))
+    (auth-source-pass--do-debug "searching for entries matching hostname=%S, user=%S, port=%S"
+                                hostname (or user "") (or port ""))
+    (auth-source-pass--do-debug "corresponding suffixes to search for: %S" suffixes)
+    (catch 'auth-source-pass-break
+      (dolist (suffix suffixes)
+        (let* ((matching-entries (auth-source-pass--entries-matching-suffix suffix all-entries))
+               (best-entry-data (auth-source-pass--select-from-entries matching-entries user)))
+          (pcase (length matching-entries)
+            (0 (auth-source-pass--do-debug "found no entries matching %S" suffix))
+            (1 (auth-source-pass--do-debug "found 1 entry matching %S: %S"
+                                           suffix
+                                           (car matching-entries)))
+            (_ (auth-source-pass--do-debug "found %s entries matching %S: %S"
+                                           (length matching-entries)
+                                           suffix
+                                           matching-entries)))
+          (when best-entry-data
+            (throw 'auth-source-pass-break best-entry-data)))))))
 
 (defun auth-source-pass--select-from-entries (entries user)
   "Return best matching password-store entry data from ENTRIES.
 
 If USER is non nil, give precedence to entries containing a user field
 matching USER."
-  (cl-reduce
-   (lambda (result entry)
-     (let ((entry-data (auth-source-pass-parse-entry entry)))
-       (cond ((equal (auth-source-pass--get-attr "user" result) user)
-              result)
-             ((equal (auth-source-pass--get-attr "user" entry-data) user)
-              entry-data)
-             (t
-              result))))
-   entries
-   :initial-value (auth-source-pass-parse-entry (car entries))))
-
-(defun auth-source-pass--matching-entries (hostname user port)
-  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
-
-The result is a list of lists of password-store entries, where
-each sublist contains entries that actually exist in the
-password-store matching one of the entry name formats that
-auth-source-pass expects, most specific to least specific."
-  (let* ((entries-lists (mapcar
-                         #'cdr
-                         (auth-source-pass--accumulate-matches hostname user port)))
-         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
-    (if entries
-        (auth-source-pass--do-debug (format "found: %S" entries))
-      (auth-source-pass--do-debug "no matches found"))
-    entries-lists))
-
-(defun auth-source-pass--accumulate-matches (hostname user port)
-  "Accumulate matching password-store entries into sublists.
-
-Entries matching supported formats that combine HOSTNAME, USER, &
-PORT are accumulated into sublists where the car of each sublist
-is a regular expression for matching paths in the password-store
-and the remainder is the list of matching entries."
-  (let ((suffix-match-lists
-         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
-                 (auth-source-pass--generate-entry-suffixes hostname user port))))
-    (cl-reduce #'auth-source-pass--entry-reducer
-               (auth-source-pass-entries)
-               :initial-value suffix-match-lists)))
-
-(defun auth-source-pass--entry-reducer (match-lists entry)
-  "Match MATCH-LISTS sublists against ENTRY.
-
-The result is a copy of match-lists with the entry added to the
-end of any sublists for which the regular expression at the head
-of the list matches the entry name."
-  (mapcar (lambda (match-list)
-            (if (string-match (car match-list) entry)
-                (append match-list (list entry))
-              match-list))
-          match-lists))
+  (let (fallback)
+    (catch 'auth-source-pass-break
+      (dolist (entry entries fallback)
+        (let ((entry-data (auth-source-pass-parse-entry entry)))
+          (when (and entry-data (not fallback))
+            (setq fallback entry-data)
+            (when (or (not user) (equal (auth-source-pass--get-attr "user" entry-data) user))
+              (throw 'auth-source-pass-break entry-data))))))))
+
+(defun auth-source-pass--entries-matching-suffix (suffix entries)
+  "Return entries matching SUFFIX.
+If ENTRIES is nil, use the result of calling `auth-source-pass-entries' instead."
+  (cl-remove-if-not
+   (lambda (entry) (string-match-p
+               (format "\\(^\\|/\\)%s$" (regexp-quote suffix))
+               entry))
+   (or entries (auth-source-pass-entries))))
 
 (defun auth-source-pass--generate-entry-suffixes (hostname user port)
   "Return a list of possible entry path suffixes in the password-store.
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 2c28f79945..6f0d308ceb 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -52,11 +52,21 @@
 (defvar auth-source-pass--debug-log nil
   "Contains a list of all messages passed to `auth-source-do-debug`.")
 
-(defun auth-source-pass--should-have-message-containing (regexp)
-  "Assert that at least one `auth-source-do-debug` matched REGEXP."
-  (should (seq-find (lambda (message)
-                      (string-match regexp message))
-                    auth-source-pass--debug-log)))
+(defun auth-source-pass--have-message-matching (regexp)
+  "Return non-nil iff at least one `auth-source-do-debug` match REGEXP."
+  (seq-find (lambda (message)
+              (string-match regexp message))
+            auth-source-pass--debug-log))
+
+(defun auth-source-pass--explain--have-message-matching (regexp)
+  "Explainer function for `auth-source-pass--have-message-matching'.
+REGEXP is the same as in `auth-source-pass--have-message-matching'."
+  `(regexp
+    ,regexp
+    messages
+    ,(mapconcat #'identity auth-source-pass--debug-log "\n- ")))
+
+(put #'auth-source-pass--have-message-matching 'ert-explainer #'auth-source-pass--explain--have-message-matching)
 
 (defun auth-source-pass--debug (&rest msg)
   "Format MSG and add that to `auth-source-pass--debug-log`.
@@ -78,6 +88,82 @@ auth-source-pass--with-store
            (auth-source-pass--parse-log nil))
        ,@body)))
 
+(defun auth-source-pass--explain-match-entry-p (entry hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-entry-p'.
+
+ENTRY, HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-entry-p'."
+  `(entry
+    ,entry
+    store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-entry-p 'ert-explainer #'auth-source-pass--explain-match-entry-p)
+
+(defun auth-source-pass--includes-sorted-entries (entries hostname &optional user port)
+  "Return non-nil iff ENTRIES matching the parameters are found in store.
+ENTRIES should be sorted from most specific to least specific.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (if (seq-empty-p entries)
+      t
+    (and
+     (auth-source-pass-match-entry-p (car entries) hostname user port)
+     (auth-source-pass--includes-sorted-entries (cdr entries) hostname user port))))
+
+(defun auth-source-pass--explain-includes-sorted-entries (entries hostname &optional user port)
+  "Explainer function for `auth-source-pass--includes-sorted-entries'.
+
+ENTRIES, HOSTNAME, USER and PORT are the same as in `auth-source-pass--includes-sorted-entries'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)
+    entries
+    ,entries))
+
+(put 'auth-source-pass--includes-sorted-entries 'ert-explainer #'auth-source-pass--explain-includes-sorted-entries)
+
+(defun auth-source-pass--explain-match-any-entry-p (hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-any-entry-p'.
+
+HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-any-entry-p'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-any-entry-p 'ert-explainer #'auth-source-pass--explain-match-any-entry-p)
+
+(defun auth-source-pass--matching-entries (hostname &optional user port)
+  "Return password-store entries matching HOSTNAME, USER, PORT.
+
+The result is a list of lists of password-store entries.  Each
+sublist contains the password-store entries whose names match a
+suffix in `auth-source-pass--generate-entry-suffixes'.  The
+result is ordered the same way as the suffixes."
+  (let ((entries (auth-source-pass-entries)))
+    (mapcar (lambda (suffix) (auth-source-pass--entries-matching-suffix suffix entries))
+            (auth-source-pass--generate-entry-suffixes hostname user port))))
+
+(defun auth-source-pass-match-entry-p (entry hostname &optional user port)
+  "Return non-nil iff an ENTRY matching the parameters is found in store.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if
+   (lambda (entries) (cl-find entry entries :test #'string=))
+   (auth-source-pass--matching-entries hostname user port)))
+
+(defun auth-source-pass-match-any-entry-p (hostname &optional user port)
+  "Return non-nil iff there is at least one entry matching the parameters.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if #'identity (auth-source-pass--matching-entries hostname user port)))
+
 (ert-deftest auth-source-pass-any-host ()
   (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
                                   ("bar"))
@@ -93,6 +179,101 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass--disambiguate-extract-host-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo")) "foo"))
+  ;; only user
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo")) "foo"))
+  ;; only port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80")) "foo"))
+  ;; both user and port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80")) "foo"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80/path")) "foo")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-user-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo")) nil))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo")) "user"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80")) nil))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80")) "user"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path")) "user")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-user-parameter ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo" "user2")) "user2"))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo" "user2")) "user2"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80" "user2")) "user2"))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80" "user2")) "user2"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path" "user2")) "user2")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-port-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo")) "443"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo")) "443"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80")) "80"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80")) "80"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path")) "80"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path")) "80")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-port-parameter ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo" "user2" "8080")) "8080"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo" "user2" "8080")) "8080"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80" "user2" "8080")) "8080"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80" "user2" "8080")) "8080"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path" "user2" "8080")) "8080")))
+
 (ert-deftest auth-source-pass-find-match-minimal-parsing ()
   (let ((store-contents
          '(("baz" ("secret" . "baz password"))
@@ -121,156 +302,110 @@ auth-source-pass--with-store
       (should (equal auth-source-pass--parse-log '("bar.baz"))))
     (auth-source-pass--with-store store-contents
       (auth-source-pass--find-match "baz" nil nil)
-      (should (equal auth-source-pass--parse-log '("baz"))))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
-        ("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
+      (should (equal auth-source-pass--parse-log '("baz"))))
+    (auth-source-pass--with-store
+        '(("dir1/bar.com" ("key" . "val"))
+          ("dir2/bar.com" ("key" . "val"))
+          ("dir3/bar.com" ("key" . "val")))
+      (auth-source-pass--find-match "bar.com" nil nil)
+      (should (= (length auth-source-pass--parse-log) 1)))))
+
+(ert-deftest auth-source-pass--find-match-return-parsed-data ()
+  (auth-source-pass--with-store '(("bar.com" ("key" . "val")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil))))
+  (auth-source-pass--with-store '(("dir1/bar.com" ("key1" . "val1")) ("dir2/bar.com" ("key2" . "val2")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
-  (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
-  (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
-  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(("foo.bar.com") ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-dont-match-at-folder-name ()
-  (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
-  (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                   '(nil ("bar.com:443/someone") nil nil nil nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com"))
+    ;; match even if sub-domain is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "foo.bar.com"))
+    ;; match even if a user is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user"))
+    ;; match even if user as an @ sign
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user@someplace"))
+    ;; match even if a port is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" nil "8080"))
+    ;; match even if a user and a port are asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user" "8080"))
+    ;; don't match if a '.' is replaced with another character
+    (auth-source-pass--with-store '(("barXcom"))
+      (should-not (auth-source-pass-match-any-entry-p "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-username ()
+  (auth-source-pass--with-store '(("user@foo"))
+    (should (auth-source-pass-match-entry-p "user@foo" "foo" "user")))
+  ;; match even if sub-domain is asked for
+  (auth-source-pass--with-store '(("user@bar.com"))
+    (should (auth-source-pass-match-entry-p "user@bar.com" "foo.bar.com" "user")))
+  ;; don't match if no user is asked for
+  (auth-source-pass--with-store '(("user@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo")))
+  ;; don't match if user is different
+  (auth-source-pass--with-store '(("user1@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo" "user2")))
+  ;; don't match if sub-domain is asked for but user is different
+  (auth-source-pass--with-store '(("user1@bar.com"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.bar.com" "user2"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-port ()
+  (auth-source-pass--with-store '(("bar.com:8080"))
+    (should (auth-source-pass-match-entry-p "bar.com:8080" "bar.com" nil "8080"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-slash ()
+  ;; match if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com/user"))
+    (should (auth-source-pass-match-entry-p "foo.com/user" "foo.com" "user")))
+  ;; match with port if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com:8080/user"))
+    (should (auth-source-pass-match-entry-p "foo.com:8080/user" "foo.com" "user" "8080")))
+  ;; don't match if entry filename doesn't match user
+  (auth-source-pass--with-store '(("foo.com/baz"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.com" "user"))))
+
+(ert-deftest auth-source-pass-matching-entries-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                     '(nil ("bar.com#443/someone") nil nil nil nil
-                           nil nil nil nil nil nil))))))
-
-(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
-  (auth-source-pass--with-store
-      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
-    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo.com/bar password")))))
-
-(ert-deftest auth-source-pass-matching-entries-with-user-first ()
+      (should (auth-source-pass-match-entry-p "bar.com#443/someone" "bar.com" "someone" "443")))))
+
+(ert-deftest auth-source-pass--matching-entries-sort-results ()
+  (auth-source-pass--with-store '(("user@foo") ("foo"))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; same, but store is reversed
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
-                   '(("user@foo") nil ("foo"))))
-    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "subdir/foo password"))
-      (should (equal (auth-source-pass--get-attr "user" result)
-                     "someone")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password") ("user" . "someone"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
-                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
-
-(ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store
-      '(("host.com" ("secret" . "host.com password")))
-    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "host.com password")))))
-
-(ert-deftest auth-source-pass-matching-entries-host-with-port ()
-  (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                   '(nil nil nil nil ("host.com:443") nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
-  (let ((auth-source-pass-port-separator "#"))
-    (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                     '(nil nil nil nil ("host.com#443") nil
-                           nil nil nil nil nil nil))))))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; with sub-domain
+  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
+    (should (auth-source-pass--includes-sorted-entries '("foo.bar.com" "bar.com") "foo.bar.com")))
+  ;; matching user in the entry data takes priority
+  (auth-source-pass--with-store '(("dir1/bar.com") ("dir2/bar.com" ("user" . "user")))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user")))
+  ;; same, but store is reversed
+  (auth-source-pass--with-store '(("dir2/bar.com" ("user" . "user")) ("dir1/bar.com"))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user"))))
+
+(ert-deftest auth-source-pass-all-supported-organizations ()
+  ;; test every possible entry to store this data: user=rms host=gnu.org port=22
+  (dolist (entry '(;; only host name
+                   "gnu.org"
+                   ;; hostname + user
+                   "gnu.org/rms" "rms@gnu.org"
+                   ;; hostname + port
+                   "gnu.org:22"
+                   ;; hostname + user + port
+                   "gnu.org:22/rms" "rms@gnu.org:22"
+                   ;; all of the above in a random folder
+                   "a/b/gnu.org"
+                   "a/b/gnu.org/rms" "a/b/rms@gnu.org"
+                   "a/b/gnu.org:22"
+                   "a/b/gnu.org:22/rms" "a/b/rms@gnu.org:22"))
+    (auth-source-pass--with-store `((,entry))
+      (should (auth-source-pass-match-entry-p entry "gnu.org" "rms" "22")))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -300,33 +435,6 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "anuser")))))
 
-(ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
-  (let (passed-host)
-    (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port)
-                 (setq passed-host host)
-                 nil)))
-      (auth-source-pass--build-result "https://user@host.com:123" nil nil)
-      (should (equal passed-host "https://user@host.com:123"))
-      (auth-source-pass--build-result "https://user@host.com" nil nil)
-      (should (equal passed-host "https://user@host.com"))
-      (auth-source-pass--build-result "user@host.com" nil nil)
-      (should (equal passed-host "user@host.com"))
-      (auth-source-pass--build-result "user@host.com:443" nil nil)
-      (should (equal passed-host "user@host.com:443")))))
-
-(ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (auth-source-pass--with-store
-      '(("foo.site.com" ("secret" . "foo.site.com password"))
-        ("bar.site.com") ; An entry name with no data is invalid
-        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
-    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
-                   '(("secret" . "foo.site.com password"))))
-    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
-                   nil))
-    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
-                   '(("secret" . "mail/baz.site.com/scott password"))))))
-
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
     (auth-source-pass-enable)
@@ -334,6 +442,24 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :user) "someone"))
       (should (equal (plist-get result :host) "gitlab.com")))))
 
+(ert-deftest auth-source-pass-prints-meaningful-debug-log ()
+  (auth-source-pass--with-store '()
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "entries matching hostname=\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "corresponding suffixes to search for: .*\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "found no entries matching \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 1 entry matching \"gitlab.com\": \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("a/gitlab.com") ("b/gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 2 entries matching \"gitlab.com\": (\"a/gitlab.com\" \"b/gitlab.com\")"))))
+
 (provide 'auth-source-pass-tests)
 
 ;;; auth-source-pass-tests.el ends here
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0011-lisp-auth-source-pass.el-Version-5.0.0.patch --]
[-- Type: text/x-patch, Size: 794 bytes --]

From fe04bfbb0bdeb58b1396578d6ece2bfcbb624b39 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 28 May 2019 08:46:41 +0200
Subject: [PATCH 11/13] * lisp/auth-source-pass.el: Version 5.0.0

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 295bda507b..f16d910890 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -5,7 +5,7 @@
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
 ;;         Keith Amidon <camalot@picnicpark.org>
-;; Version: 4.0.2
+;; Version: 5.0.0
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0012-etc-NEWS-Describe-changes-to-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 1194 bytes --]

From 3cff601ee272dad609a5fb7099f562b470a6ac7a Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sun, 2 Jun 2019 11:08:40 +0200
Subject: [PATCH 12/13] * etc/NEWS: Describe changes to auth-source-pass

---
 etc/NEWS | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index 95d7e08074..747e195b1b 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1512,6 +1512,25 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
 value.  Auto Revert mode can use this information to avoid polling the
 buffer periodically when 'auto-revert-avoid-polling' is non-nil.
 
+** auth-source-pass
+
++++
+*** New customizable variable 'auth-source-pass-filename'.
+Allows setting the path to the password-store, defaults to
+~/.password-store.
+
++++
+*** New customizable variable 'auth-source-pass-port-separator'.
+Specifies separator between host and port, defaults to colon ":".
+
+---
+*** Minimize the number of decryptions during password lookup.
+This makes the package usable with physical tokens requiring touching
+a sensor for every decryption.
+
+---
+*** 'auth-source-pass-get' is now autoloaded.
+
 ** Bookmarks
 
 ---
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #14: 0013-doc-misc-auth.texi-The-Unix-password-store-Complete-.patch --]
[-- Type: text/x-patch, Size: 3875 bytes --]

From 1b51160e1e7d9cee8d89b652c9495c6af8bcc215 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Thu, 13 Jun 2019 21:54:21 +0200
Subject: [PATCH 13/13] * doc/misc/auth.texi (The Unix password store):
 Complete rewrite

---
 doc/misc/auth.texi | 50 ++++++++++++++++++++++++++++++++++++----------
 1 file changed, 40 insertions(+), 10 deletions(-)

diff --git a/doc/misc/auth.texi b/doc/misc/auth.texi
index a46e3d73fc..5b4914e8f5 100644
--- a/doc/misc/auth.texi
+++ b/doc/misc/auth.texi
@@ -445,19 +445,39 @@ The Unix password store
 
 @uref{http://www.passwordstore.org,,The standard unix password
 manager} (or just @code{pass}) stores your passwords in
-@code{gpg}-protected files following the Unix philosophy.
+@code{gpg}-protected files following the Unix philosophy.  The store
+location (any directory) must be specified in the
+@code{auth-source-pass-filename} variable which defaults to
+@code{"~/.password-store"}.
 
-Emacs integration of @code{pass} follows the first approach suggested
-by the pass project itself for data organization to find data. This
-means that the filename of the file containing the password for a user
-on a particular host must contain the host name.  The file itself must
-contain the password on the first line, as well as a @code{username}
-field containing the username on a subsequent line. A @code{port}
-field can be used to differentiate the authentication data for several
-services with the same username on the same host.
+Emacs integration of @code{pass} follows the approach suggested by the
+pass project itself for data organization to find data.  In particular,
+to store a password for the user @code{rms} on the host @code{gnu.org}
+on port @code{22}, you should use one of the following filenames.
+
+@itemize
+@item @code{"gnu.org.gpg"} : No username or port in the filename means that any username and port will match.
+@item @code{"gnu.org/rms.gpg"} : The username to match can be expressed as filename inside a directory whose name matches the host.  This is useful if the store has passwords for several users on the same host.
+@item @code{"rms@@gnu.org.gpg"} : The username can also be expressed as a prefix, separated from the host with an at-sign (@code{@@}).
+@item @code{"gnu.org:22.gpg"} : The port (aka. service) to match can only be expressed after the host and separated with a colon (@code{:}).  The separator can be changed through the @code{auth-source-pass-port-separator} variable.
+@item @code{"gnu.org:22/rms.gpg"}
+@item @code{"rms@@gnu.org:22.gpg"}
+@item @code{"a/b/gnu.org.gpg"} : Entries can be stored in arbitrary directories.
+@item @code{"a/b/gnu.org/rms.gpg"}
+@item @code{"a/b/rms@@gnu.org.gpg"}
+@item @code{"a/b/gnu.org:22.gpg"}
+@item @code{"a/b/gnu.org:22/rms.gpg"}
+@item @code{"a/b/rms@@gnu.org:22.gpg"}
+@end itemize
+
+If several entries match, the one matching the most items (where an
+``item'' is one of username, port or host) is preferred.  For example,
+while searching for an entry matching the @code{rms} user on host
+@code{gnu.org} for port @code{22}, then the entry
+@code{"gnu.org:22/rms.gpg"} is preferred over @code{"gnu.org.gpg"}.
 
 Users of @code{pass} may also be interested in functionality provided
-by other Emacs packages dealing with pass:
+by other Emacs packages:
 
 @itemize
 @item
@@ -468,6 +488,16 @@ The Unix password store
 @uref{https://github.com/jabranham/helm-pass,,helm-pass}: helm interface for pass.
 @end itemize
 
+@defvar auth-source-pass-filename
+Set this variable to a string locating the password store on the disk.
+Defaults to @code{"~/.password-store"}.
+@end defvar
+
+@defvar auth-source-pass-port-separator
+Set this variable to a string that should separate an host name from a
+port in an entry.  Defaults to @code{":"}.
+@end defvar
+
 @node Help for developers
 @chapter Help for developers
 
-- 
2.21.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-13 19:59     ` Damien Cassou
  2019-06-13 21:23       ` Noam Postavsky
  2019-06-14  7:10       ` Damien Cassou
@ 2019-06-14  7:47       ` Eli Zaretskii
  2019-06-14 16:16         ` Damien Cassou
  2 siblings, 1 reply; 13+ messages in thread
From: Eli Zaretskii @ 2019-06-14  7:47 UTC (permalink / raw)
  To: Damien Cassou
  Cc: magnus.henoch, nicolas, npostavs, iku.iwasa, camalot, gaby.launay,
	36052, tzz

> From: Damien Cassou <damien@cassou.me>
> Cc: Noam Postavsky <npostavs@gmail.com>, Magnus Henoch <magnus.henoch@gmail.com>, Ted Zlatanov <tzz@lifelogs.com>, Iku Iwasa <iku.iwasa@gmail.com>, Keith Amidon <camalot@picnicpark.org>, galaunay <gaby.launay@tutanota.com>, Nicolas Petton <nicolas@petton.fr>, Eli Zaretskii <eliz@gnu.org>
> Date: Thu, 13 Jun 2019 21:59:32 +0200
> 
> diff --git a/etc/NEWS b/etc/NEWS
> index 95d7e08074..f5781fb7e5 100644
> --- a/etc/NEWS
> +++ b/etc/NEWS
> @@ -1512,6 +1512,21 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
>  value.  Auto Revert mode can use this information to avoid polling the
>  buffer periodically when 'auto-revert-avoid-polling' is non-nil.
>  
> +** auth-source-pass
> +
> +*** New customizable variable 'auth-source-pass-filename'.
> +Allows setting the path to the password-store, defaults to
> +~/.password-store.
> +
> +*** New customizable variable 'auth-source-pass-port-separator'.
> +Specifies separator between host and port, defaults to colon ":".
> +
> +*** Minimize the number of decryptions during password lookup.
> +This makes the package usable with physical tokens requiring touching
> +a sensor for every decryption.
> +
> +*** 'auth-source-pass-get' is now autoloaded.

These are all described in the patch for the manual, right?  If so,
the entries should be marked with "+++", see the beginning of NEWS for
instructions about that.

> +@itemize
> +@item @code{"gnu.org.gpg"} : No username or port in the filename means that any username and port will match.
> +@item @code{"gnu.org/rms.gpg"} : The username to match can be expressed as filename inside a directory whose name matches the host. This is useful if the store has passwords for several users on the same host.
> +@item @code{"rms@@gnu.org.gpg"} : The username can also be expressed as a prefix, separated from the host with an at-sign (@code{@@}).
> +@item @code{"gnu.org:22.gpg"} : The port (aka. service) to match can only be expressed after the host and separated with a colon (@code{:}). The separator can be changed through the @code{auth-source-pass-port-separator} variable.
> +@item @code{"gnu.org:22/rms.gpg"}
> +@item @code{"rms@@gnu.org:22.gpg"}
> +@item @code{"a/b/gnu.org.gpg"} : Entries can be stored in arbitrary directories.
> +@item @code{"a/b/gnu.org/rms.gpg"}
> +@item @code{"a/b/rms@@gnu.org.gpg"}
> +@item @code{"a/b/gnu.org:22.gpg"}
> +@item @code{"a/b/gnu.org:22/rms.gpg"}
> +@item @code{"a/b/rms@@gnu.org:22.gpg"}
> +@end itemize

Please make the text describing each @item start on a new line.  Also,
it sounds like you want @table here, not @itemize.  And finally,
quoting in @code is sub-optimal; would @samp (and losing the quotes)
do the job?

> +@defvar auth-source-pass-filename
> +Set this variable to a string locating the password store on the
> +disk. Defaults to @code{"~/.password-store"}.

File names or their parts should have the @file markup, not @code.

Thanks.





^ permalink raw reply	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-14  7:47       ` Eli Zaretskii
@ 2019-06-14 16:16         ` Damien Cassou
  2019-06-22  9:02           ` Eli Zaretskii
  0 siblings, 1 reply; 13+ messages in thread
From: Damien Cassou @ 2019-06-14 16:16 UTC (permalink / raw)
  To: Eli Zaretskii
  Cc: magnus.henoch, nicolas, npostavs, iku.iwasa, camalot, gaby.launay,
	36052, tzz

[-- Attachment #1: Type: text/plain, Size: 1055 bytes --]

Eli Zaretskii <eliz@gnu.org> writes:
> These are all described in the patch for the manual, right?  If so,
> the entries should be marked with "+++", see the beginning of NEWS for
> instructions about that.


I'm not sure which patch you reviewed so I attach it again to this new
email. The first 2 items in NEWS (new user options) are covered by the
manual so I've added '+++' in front of each. The last 2 items (less
decryption and autoload) shouldn't be in the manual so I've added '---'
in front of each.


> Please make the text describing each @item start on a new line.  Also,
> it sounds like you want @table here, not @itemize.  And finally,
> quoting in @code is sub-optimal; would @samp (and losing the quotes)
> do the job?
>
> File names or their parts should have the @file markup, not @code.

I'm not sure I did it right, but you will find a new patch attached to
this email.

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0013-doc-misc-auth.texi-The-Unix-password-store-Complete-.patch --]
[-- Type: text/x-patch, Size: 3782 bytes --]

From 56e544db44c4a98e567e1407f0519483adc4eac7 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Thu, 13 Jun 2019 21:54:21 +0200
Subject: [PATCH 13/13] * doc/misc/auth.texi (The Unix password store):
 Complete rewrite

---
 doc/misc/auth.texi | 71 +++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 61 insertions(+), 10 deletions(-)

diff --git a/doc/misc/auth.texi b/doc/misc/auth.texi
index a46e3d73fc..bbb66ecab5 100644
--- a/doc/misc/auth.texi
+++ b/doc/misc/auth.texi
@@ -445,19 +445,60 @@ The Unix password store
 
 @uref{http://www.passwordstore.org,,The standard unix password
 manager} (or just @code{pass}) stores your passwords in
-@code{gpg}-protected files following the Unix philosophy.
+@code{gpg}-protected files following the Unix philosophy.  The store
+location (any directory) must be specified in the
+@code{auth-source-pass-filename} variable which defaults to
+@file{~/.password-store}.
 
-Emacs integration of @code{pass} follows the first approach suggested
-by the pass project itself for data organization to find data. This
-means that the filename of the file containing the password for a user
-on a particular host must contain the host name.  The file itself must
-contain the password on the first line, as well as a @code{username}
-field containing the username on a subsequent line. A @code{port}
-field can be used to differentiate the authentication data for several
-services with the same username on the same host.
+Emacs integration of @code{pass} follows the approach suggested by the
+pass project itself for data organization to find data.  In
+particular, to store a password for the user @code{rms} on the host
+@code{gnu.org} and port @code{22}, you should use one of the following
+filenames.
+
+@table @file
+@item gnu.org.gpg
+No username or port in the filename means that any username and port
+will match.
+
+@item gnu.org/rms.gpg
+The username to match can be expressed as filename inside a directory
+whose name matches the host.  This is useful if the store has
+passwords for several users on the same host.
+
+@item rms@@gnu.org.gpg
+The username can also be expressed as a prefix, separated from the
+host with an at-sign (@code{@@}).
+
+@item gnu.org:22.gpg
+The port (aka. service) to match can only be expressed after the host and separated with a colon (@code{:}).  The separator can be changed through the @code{auth-source-pass-port-separator} variable.
+
+@item gnu.org:22/rms.gpg
+
+@item rms@@gnu.org:22.gpg
+
+@item a/b/gnu.org.gpg
+Entries can be stored in arbitrary directories.
+
+@item a/b/gnu.org/rms.gpg
+
+@item a/b/rms@@gnu.org.gpg
+
+@item a/b/gnu.org:22.gpg
+
+@item a/b/gnu.org:22/rms.gpg
+
+@item a/b/rms@@gnu.org:22.gpg
+@end table
+
+If several entries match, the one matching the most items (where an
+``item'' is one of username, port or host) is preferred.  For example,
+while searching for an entry matching the @code{rms} user on host
+@code{gnu.org} and port @code{22}, then the entry
+@file{gnu.org:22/rms.gpg} is preferred over @file{gnu.org.gpg}.
 
 Users of @code{pass} may also be interested in functionality provided
-by other Emacs packages dealing with pass:
+by other Emacs packages:
 
 @itemize
 @item
@@ -468,6 +509,16 @@ The Unix password store
 @uref{https://github.com/jabranham/helm-pass,,helm-pass}: helm interface for pass.
 @end itemize
 
+@defvar auth-source-pass-filename
+Set this variable to a string locating the password store on the disk.
+Defaults to @file{~/.password-store}.
+@end defvar
+
+@defvar auth-source-pass-port-separator
+Set this variable to a string that should separate an host name from a
+port in an entry.  Defaults to @samp{:}.
+@end defvar
+
 @node Help for developers
 @chapter Help for developers
 
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0012-etc-NEWS-Describe-changes-to-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 1194 bytes --]

From 4d4b9408daac0d47b9b2e2d07e2aefae471376a6 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sun, 2 Jun 2019 11:08:40 +0200
Subject: [PATCH 12/13] * etc/NEWS: Describe changes to auth-source-pass

---
 etc/NEWS | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index e3023216ac..67dc2e9164 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1517,6 +1517,25 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
 value.  Auto Revert mode can use this information to avoid polling the
 buffer periodically when 'auto-revert-avoid-polling' is non-nil.
 
+** auth-source-pass
+
++++
+*** New customizable variable 'auth-source-pass-filename'.
+Allows setting the path to the password-store, defaults to
+~/.password-store.
+
++++
+*** New customizable variable 'auth-source-pass-port-separator'.
+Specifies separator between host and port, defaults to colon ":".
+
+---
+*** Minimize the number of decryptions during password lookup.
+This makes the package usable with physical tokens requiring touching
+a sensor for every decryption.
+
+---
+*** 'auth-source-pass-get' is now autoloaded.
+
 ** Bookmarks
 
 ---
-- 
2.21.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-14 16:16         ` Damien Cassou
@ 2019-06-22  9:02           ` Eli Zaretskii
  2019-06-24  7:26             ` Damien Cassou
  0 siblings, 1 reply; 13+ messages in thread
From: Eli Zaretskii @ 2019-06-22  9:02 UTC (permalink / raw)
  To: Damien Cassou
  Cc: magnus.henoch, nicolas, npostavs, iku.iwasa, camalot, gaby.launay,
	36052, tzz

> From: Damien Cassou <damien@cassou.me>
> Cc: 36052@debbugs.gnu.org, npostavs@gmail.com, magnus.henoch@gmail.com, tzz@lifelogs.com, iku.iwasa@gmail.com, camalot@picnicpark.org, gaby.launay@tutanota.com, nicolas@petton.fr
> Date: Fri, 14 Jun 2019 18:16:01 +0200
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> > These are all described in the patch for the manual, right?  If so,
> > the entries should be marked with "+++", see the beginning of NEWS for
> > instructions about that.
> 
> 
> I'm not sure which patch you reviewed so I attach it again to this new
> email. The first 2 items in NEWS (new user options) are covered by the
> manual so I've added '+++' in front of each. The last 2 items (less
> decryption and autoload) shouldn't be in the manual so I've added '---'
> in front of each.
> 
> 
> > Please make the text describing each @item start on a new line.  Also,
> > it sounds like you want @table here, not @itemize.  And finally,
> > quoting in @code is sub-optimal; would @samp (and losing the quotes)
> > do the job?
> >
> > File names or their parts should have the @file markup, not @code.
> 
> I'm not sure I did it right, but you will find a new patch attached to
> this email.

Thanks, these LGTM.





^ permalink raw reply	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-22  9:02           ` Eli Zaretskii
@ 2019-06-24  7:26             ` Damien Cassou
  2019-06-24 14:33               ` Eli Zaretskii
  0 siblings, 1 reply; 13+ messages in thread
From: Damien Cassou @ 2019-06-24  7:26 UTC (permalink / raw)
  To: Eli Zaretskii
  Cc: magnus.henoch, nicolas, npostavs, iku.iwasa, camalot, gaby.launay,
	36052, tzz

Eli Zaretskii <eliz@gnu.org> writes:
> Thanks, these LGTM.

I've just pushed to master. Because it's my first time, can you please
check I did it right?

I guess this bug report can be closed now.

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill





^ permalink raw reply	[flat|nested] 13+ messages in thread

* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
  2019-06-24  7:26             ` Damien Cassou
@ 2019-06-24 14:33               ` Eli Zaretskii
  0 siblings, 0 replies; 13+ messages in thread
From: Eli Zaretskii @ 2019-06-24 14:33 UTC (permalink / raw)
  To: Damien Cassou
  Cc: magnus.henoch, nicolas, npostavs, iku.iwasa, camalot, gaby.launay,
	tzz, 36052-done

> From: Damien Cassou <damien@cassou.me>
> Cc: 36052@debbugs.gnu.org, npostavs@gmail.com, magnus.henoch@gmail.com, tzz@lifelogs.com, iku.iwasa@gmail.com, camalot@picnicpark.org, gaby.launay@tutanota.com, nicolas@petton.fr
> Date: Mon, 24 Jun 2019 09:26:04 +0200
> 
> Eli Zaretskii <eliz@gnu.org> writes:
> > Thanks, these LGTM.
> 
> I've just pushed to master. Because it's my first time, can you please
> check I did it right?

You forgot to mention the bug number, AFAICT (happens to everyone from
time to time).  Other than that, looks OK to me.

> I guess this bug report can be closed now.

Done.

Thanks.





^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2019-06-24 14:33 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-06-02  9:11 bug#36052: 26.2.50; [PATCH] Improve auth-source-pass Damien Cassou
2019-06-07  0:43 ` Noam Postavsky
2019-06-08 15:47   ` Damien Cassou
2019-06-08 16:02     ` Eli Zaretskii
2019-06-08 22:38     ` Noam Postavsky
2019-06-13 19:59     ` Damien Cassou
2019-06-13 21:23       ` Noam Postavsky
2019-06-14  7:10       ` Damien Cassou
2019-06-14  7:47       ` Eli Zaretskii
2019-06-14 16:16         ` Damien Cassou
2019-06-22  9:02           ` Eli Zaretskii
2019-06-24  7:26             ` Damien Cassou
2019-06-24 14:33               ` Eli Zaretskii

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).