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
« 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 os
7import re
8from typing import Dict, List
9from collections import defaultdict
11# Cleaning variables.
12SEPARATOR = '-'
13NOTITLE = 'notitle'
14PROTOCOLS = ['http://', 'https://']
15INVALIDS = ['<', '>', ':', '"', '/', '\\', '|', '?', '*', '\0', '\t']
16CLEANS = {
17 ' ': '-',
18 '&': 'and',
19 '@': 'At',
20 "'": '',
21 '[': '',
22 ']': '',
23}
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
31 return replaces(cleans, string)
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)
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
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)
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)
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
77 if ptitle == '' and os.path.basename(path) != NOTITLE:
78 path = os.path.join(path, NOTITLE)
79 entry.pop('title', '')
80 return path
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)
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)
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)
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
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)
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
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)
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