Coverage for pass_import/auto.py: 95%
80 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 12:11 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 12:11 +0000
1# -*- encoding: utf-8 -*-
2# pass import - Passwords importer swiss army knife
3# Copyright (C) 2017-2024 Alexandre PUJOL <alexandre@pujol.io>.
4#
6import io
7import os
8from contextlib import contextmanager
9from typing import Callable, List, Tuple, Union
11import pass_import
12from pass_import.core import Cap
13from pass_import.detecter import Formatter
14from pass_import.errors import PMError
17class DummyDetecter(Formatter):
18 """Dummy detecter class.
20 In the detector context manager, if the :func:`~detecter_open` method of a
21 Detecter object fails, it means the format tested is not the format
22 considered. Then, we fall back to this dummy password manager class to fail
23 silently and continue the search of the file password manager and format.
25 """
27 def detecter_open(self):
28 """Do nothing."""
30 def detecter_close(self):
31 """Do nothing."""
33 def is_format(self) -> bool:
34 """Return ``False``."""
35 return False
37 def checkheader(self, header, only=False) -> bool:
38 """No header check."""
39 return False # pragma: no cover
41 @classmethod
42 def header(cls) -> str:
43 """No header."""
44 return '' # pragma: no cover
47@contextmanager
48def detector(cls, prefix, settings=None):
49 """Context manager for password format/encryption detection."""
50 manager = cls(prefix, settings)
51 try:
52 manager.detecter_open()
53 except (PMError, IsADirectoryError):
54 dummy = DummyDetecter(prefix)
55 dummy.detecter_open()
56 yield dummy
57 dummy.detecter_close()
58 else:
59 yield manager
60 manager.detecter_close()
63class AutoDetect():
64 """Give a file, detect the format, and the password manager.
66 Considering a manager's name and optional version number, tell if a given
67 path is supported by the password manager and, if yes, tell what format is
68 supported.
70 :param str name: (optional) Name of the password manager. Only the
71 ``manager`` method can be used without the manager name.
72 :param str version: (optional) Version number of the password manager.
74 """
76 def __init__(self, name='', settings=None):
77 self.settings = {} if settings is None else settings
78 self.managers = pass_import.Managers()
79 self.formats = pass_import.Detecters(Cap.FORMAT)
80 self.decrypters = pass_import.Detecters(Cap.DECRYPT)
81 self.classes = self.managers.matrix().get(name, [])
82 self.stream = self.settings.get('decrypted', False)
84 def default(self, name='') -> Callable:
85 """Retrieve the class of the default importer."""
86 classes = self.classes
87 if name != '':
88 classes = self.managers.matrix().get(name, [])
89 for pm in classes:
90 if pm.default:
91 return pm
92 raise pass_import.ManagerError('No default manager found.')
94 def format(self, path: str) -> Callable:
95 """Full format detection of a file for a given password manager.
97 - If only one format supported, use it.
98 - If path is a file, try to open it with all supported format.
99 - Then if the format is not supported by :func:`~tryopen`,
100 open it if this is the last remaining.
101 - Get the default format otherwise
103 :param str path: Path, directory, or plain data of the manager.
104 :returns PasswordManager: The detected password manager class.
105 ``None`` if not found.
107 """
108 if len(self.classes) == 1:
109 return self.classes[0]
111 if not (self.stream or os.path.isfile(path) or os.path.isdir(path)):
112 return self.default()
114 pm, unknowns = self._tryopen(path)
115 if pm:
116 return pm
117 if len(unknowns) == 1:
118 return unknowns[0]
119 return self.default() # pragma: no cover
121 def manager(self, path: str) -> Union[Callable, None]:
122 """Full format detection of a file without knowing the manager's name.
124 :algorithm:
126 .. code-block:: console
128 For all format classes in Formats:
129 Open the path,
130 Check if it is in the considered format,
131 If yes:
132 For all managers that support the format:
133 Compare manager header for the file header.
135 :param str path: Path, directory, or plain data of the manager.
136 :returns PasswordManager: The detected password manager class.
137 ``None`` if not found.
139 """
140 if not (self.stream or os.path.isfile(path) or os.path.isdir(path)):
141 return None
143 prefix = path
144 for frmt in self.formats:
145 if self.stream:
146 prefix = io.StringIO(path)
147 with detector(self.formats[frmt], prefix, self.settings) as file:
148 if file.is_format():
149 for pm in self.managers.classes(frmt=frmt):
150 if file.checkheader(pm.header(), pm.only):
151 return pm
152 return None
154 def _tryopen(self, path: str
155 ) -> Tuple[Union[Callable, None], List[Callable]]:
156 """Knowing the manager's name, try to open the path in all formats.
158 :algorithm:
160 .. code-block:: console
162 For all classes that support the password manager 'name':
163 If the format is supported by pass-import:
164 Open the path
165 Check if it is in the considered format
166 Compare manager header against the file header
167 Else:
168 The could format could be this one, update unknowns list.
170 :param str path: Path, directory, or plain data of the manager.
171 :returns PasswordManager: The detected password manager class.
172 ``None`` if not found.
173 :returns list: List of untested :term:`pm`, format could be any of
174 them.
176 """
177 unknowns = []
178 prefix = path
179 for pm in self.classes:
180 if pm.format in self.formats:
181 if self.stream:
182 prefix = io.StringIO(path)
183 with detector(pm, prefix, self.settings) as file:
184 if file.is_format():
185 if file.checkheader(file.header(), file.only):
186 return pm, []
187 else:
188 unknowns.append(pm)
189 return None, unknowns