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

1# -*- encoding: utf-8 -*- 

2# pass import - Passwords importer swiss army knife 

3# Copyright (C) 2017-2024 Alexandre PUJOL <alexandre@pujol.io>. 

4# 

5 

6import io 

7import os 

8from contextlib import contextmanager 

9from typing import Callable, List, Tuple, Union 

10 

11import pass_import 

12from pass_import.core import Cap 

13from pass_import.detecter import Formatter 

14from pass_import.errors import PMError 

15 

16 

17class DummyDetecter(Formatter): 

18 """Dummy detecter class. 

19 

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. 

24 

25 """ 

26 

27 def detecter_open(self): 

28 """Do nothing.""" 

29 

30 def detecter_close(self): 

31 """Do nothing.""" 

32 

33 def is_format(self) -> bool: 

34 """Return ``False``.""" 

35 return False 

36 

37 def checkheader(self, header, only=False) -> bool: 

38 """No header check.""" 

39 return False # pragma: no cover 

40 

41 @classmethod 

42 def header(cls) -> str: 

43 """No header.""" 

44 return '' # pragma: no cover 

45 

46 

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() 

61 

62 

63class AutoDetect(): 

64 """Give a file, detect the format, and the password manager. 

65 

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. 

69 

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. 

73 

74 """ 

75 

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) 

83 

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.') 

93 

94 def format(self, path: str) -> Callable: 

95 """Full format detection of a file for a given password manager. 

96 

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 

102 

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. 

106 

107 """ 

108 if len(self.classes) == 1: 

109 return self.classes[0] 

110 

111 if not (self.stream or os.path.isfile(path) or os.path.isdir(path)): 

112 return self.default() 

113 

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 

120 

121 def manager(self, path: str) -> Union[Callable, None]: 

122 """Full format detection of a file without knowing the manager's name. 

123 

124 :algorithm: 

125 

126 .. code-block:: console 

127 

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. 

134 

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. 

138 

139 """ 

140 if not (self.stream or os.path.isfile(path) or os.path.isdir(path)): 

141 return None 

142 

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 

153 

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. 

157 

158 :algorithm: 

159 

160 .. code-block:: console 

161 

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. 

169 

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. 

175 

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