Coverage for pass_import/clean.py: 95%

97 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 os 

7import re 

8from typing import Dict, List 

9from collections import defaultdict 

10 

11# Cleaning variables. 

12SEPARATOR = '-' 

13NOTITLE = 'notitle' 

14PROTOCOLS = ['http://', 'https://'] 

15INVALIDS = ['<', '>', ':', '"', '/', '\\', '|', '?', '*', '\0', '\t'] 

16CLEANS = { 

17 ' ': '-', 

18 '&': 'and', 

19 '@': 'At', 

20 "'": '', 

21 '[': '', 

22 ']': '', 

23} 

24 

25 

26def cmdline(string: str, cleans: Dict[str, str] = None) -> str: 

27 """Make the string more command line friendly.""" 

28 if not cleans: 

29 cleans = CLEANS 

30 

31 return replaces(cleans, string) 

32 

33 

34def convert(string: str) -> str: 

35 """Convert invalid characters by the separator in a string.""" 

36 characters = dict(zip(INVALIDS, [SEPARATOR] * len(INVALIDS))) 

37 return replaces(characters, string) 

38 

39 

40def domain(string: str) -> str: 

41 """Return the hostname part of a (potential) URLs.""" 

42 for component in string.split('/'): 

43 if component != '': 

44 return component 

45 return string 

46 

47 

48def group(string: str) -> str: 

49 """Remove invalids characters in a group. Convert sep to os.sep.""" 

50 characters = dict(zip(INVALIDS, [SEPARATOR] * len(INVALIDS))) 

51 characters['/'] = os.sep 

52 characters['\\'] = os.sep 

53 return replaces(characters, string) 

54 

55 

56def cpath(entry: Dict[str, str], path: str, cmdclean: bool, conv: bool) -> str: 

57 """Create path from title and group.""" 

58 ptitle = '' 

59 for key in ['title', 'host', 'url', 'login']: 

60 if key in entry and entry[key]: 

61 ptitle = entry[key] 

62 if key in ['title', 'host', 'url']: 

63 ptitle = protocol(ptitle) 

64 if key in ['host', 'url']: 

65 ptitle = domain(ptitle) 

66 

67 ptitle = title(ptitle) 

68 if cmdclean: 

69 ptitle = cmdline(ptitle) 

70 if conv: 

71 ptitle = convert(ptitle) 

72 if ptitle != '': 

73 if os.path.basename(path) != ptitle: 

74 path = os.path.join(path, ptitle) 

75 break 

76 

77 if ptitle == '' and os.path.basename(path) != NOTITLE: 

78 path = os.path.join(path, NOTITLE) 

79 entry.pop('title', '') 

80 return path 

81 

82 

83def dpaths(data: List[Dict[str, str]], cmdclean: bool, conv: bool): 

84 """Create subfolders for duplicated paths.""" 

85 duplicated = defaultdict(list) 

86 for idx, entry in enumerate(data): 

87 path = entry.get('path', '') 

88 duplicated[path].append(idx) 

89 

90 for path in duplicated: 

91 if len(duplicated[path]) > 1: 

92 for idx in duplicated[path]: 

93 entry = data[idx] 

94 entry['path'] = cpath(entry, path, cmdclean, conv) 

95 

96 

97def protocol(string: str) -> str: 

98 """Remove the protocol prefix in a string.""" 

99 characters = dict(zip(PROTOCOLS, [''] * len(PROTOCOLS))) 

100 return replaces(characters, string) 

101 

102 

103def replaces(characters: Dict[str, str], string: str) -> str: 

104 """General purpose replace function.""" 

105 for key in characters: 

106 string = string.replace(key, characters[key]) 

107 return string 

108 

109 

110def title(string: str) -> str: 

111 """Clean the title from separator before addition to a path.""" 

112 characters = {'/': SEPARATOR, '\\': SEPARATOR} 

113 return replaces(characters, string) 

114 

115 

116def unused(entry: Dict[str, str]) -> Dict[str, str]: 

117 """Remove unused keys and empty values.""" 

118 empty = [k for k, v in entry.items() if not v] 

119 for key in empty: 

120 entry.pop(key) 

121 return entry 

122 

123 

124def duplicate(data: List[Dict[str, str]]): 

125 """Add number to the remaining duplicated path.""" 

126 seen = set() 

127 for entry in data: 

128 idx_added = False 

129 path = entry.get('path', '') 

130 if path in seen: 

131 idx = 1 

132 while path in seen: 

133 if not idx_added: 

134 path += SEPARATOR + str(idx) 

135 idx_added = True 

136 else: 

137 path = re.sub(rf'^(.*){SEPARATOR}{idx}$', 

138 rf'\1{SEPARATOR}{idx + 1}', 

139 path) 

140 idx += 1 

141 seen.add(path) 

142 entry['path'] = path 

143 else: 

144 seen.add(path) 

145 

146 

147def otp(data: List[Dict[str, str]]): 

148 """Format the otpauth url with sane default.""" 

149 for entry in data: 

150 if 'otpauth' in entry: 

151 if not entry['otpauth'].startswith('otpauth://'): 

152 secret = entry['otpauth'] 

153 otp = f"otpauth://totp/{entry.get('title', 'otp-secret')}" 

154 otp += f"?secret={secret}" 

155 entry['otpauth'] = otp