@@ -2,6 +2,7 @@ use crate::otp::otp_element::OTPDatabase;
2
2
use crate :: { clipboard, otp:: otp_element:: OTPElement } ;
3
3
use clap:: Args ;
4
4
use color_eyre:: eyre:: eyre;
5
+ use globset:: { Glob , GlobMatcher } ;
5
6
6
7
use super :: SubcommandExecutor ;
7
8
@@ -24,19 +25,48 @@ pub struct ExtractArgs {
24
25
pub copy_to_clipboard : bool ,
25
26
}
26
27
28
+ // Contains glob filters for each field we can filter on
29
+ struct ExtractFilterGlob {
30
+ issuer_glob : Option < GlobMatcher > ,
31
+ label_glob : Option < GlobMatcher > ,
32
+ index : Option < usize > ,
33
+ }
34
+
35
+ impl TryFrom < ExtractArgs > for ExtractFilterGlob {
36
+ type Error = color_eyre:: eyre:: ErrReport ;
37
+
38
+ fn try_from ( value : ExtractArgs ) -> Result < Self , Self :: Error > {
39
+ let issuer_glob = if value. issuer . is_some ( ) {
40
+ Some ( Glob :: new ( & value. issuer . unwrap ( ) ) ?. compile_matcher ( ) )
41
+ } else {
42
+ None
43
+ } ;
44
+
45
+ let label_glob = if value. label . is_some ( ) {
46
+ Some ( Glob :: new ( & value. label . unwrap ( ) ) ?. compile_matcher ( ) )
47
+ } else {
48
+ None
49
+ } ;
50
+
51
+ Ok ( Self {
52
+ issuer_glob,
53
+ label_glob,
54
+ index : value. index ,
55
+ } )
56
+ }
57
+ }
58
+
27
59
impl SubcommandExecutor for ExtractArgs {
28
60
fn run_command ( self , otp_database : OTPDatabase ) -> color_eyre:: Result < OTPDatabase > {
29
- let first_with_filters = otp_database
30
- . elements
31
- . iter ( )
32
- . enumerate ( )
33
- . find ( |( index, code) | filter_extract ( & self , * index, code) )
34
- . map ( |( _, code) | code) ;
61
+ let copy_to_clipboard = self . copy_to_clipboard ;
62
+ let globbed: ExtractFilterGlob = self . try_into ( ) ?;
63
+
64
+ let first_with_filters = find_match ( & otp_database, globbed) ;
35
65
36
66
if let Some ( otp) = first_with_filters {
37
67
let code = otp. get_otp_code ( ) ?;
38
68
println ! ( "{code}" ) ;
39
- if self . copy_to_clipboard {
69
+ if copy_to_clipboard {
40
70
let _ = clipboard:: copy_string_to_clipboard ( code. as_str ( ) ) ?;
41
71
println ! ( "Copied to clipboard" ) ;
42
72
}
@@ -47,18 +77,211 @@ impl SubcommandExecutor for ExtractArgs {
47
77
}
48
78
}
49
79
50
- fn filter_extract ( args : & ExtractArgs , index : usize , code : & OTPElement ) -> bool {
80
+ fn find_match ( otp_database : & OTPDatabase , globbed : ExtractFilterGlob ) -> Option < & OTPElement > {
81
+ otp_database
82
+ . elements
83
+ . iter ( )
84
+ . enumerate ( )
85
+ . find ( |( index, code) | filter_extract ( & globbed, * index, code) )
86
+ . map ( |( _, code) | code)
87
+ }
88
+
89
+ fn filter_extract ( args : & ExtractFilterGlob , index : usize , candidate : & OTPElement ) -> bool {
51
90
let match_by_index = args. index . is_none_or ( |i| i == index) ;
52
91
53
92
let match_by_issuer = args
54
- . issuer
93
+ . issuer_glob
55
94
. as_ref ( )
56
- . is_none_or ( |issuer| code . issuer . to_lowercase ( ) == issuer . to_lowercase ( ) ) ;
95
+ . is_none_or ( |issuer| issuer. is_match ( & candidate . issuer ) ) ;
57
96
58
97
let match_by_label = args
59
- . label
98
+ . label_glob
60
99
. as_ref ( )
61
- . is_none_or ( |label| code . label . to_lowercase ( ) == label . to_lowercase ( ) ) ;
100
+ . is_none_or ( |label| label. is_match ( & candidate . label ) ) ;
62
101
63
102
match_by_index && match_by_issuer && match_by_label
64
103
}
104
+
105
+ #[ cfg( test) ]
106
+ mod tests {
107
+ use globset:: Glob ;
108
+
109
+ use crate :: otp:: otp_element:: { OTPDatabase , OTPElementBuilder } ;
110
+
111
+ use super :: { find_match, ExtractFilterGlob } ;
112
+
113
+ #[ test]
114
+ fn test_glob_filtering_good_issuer ( ) {
115
+ // Arrange
116
+ let mut otp_database = OTPDatabase :: default ( ) ;
117
+ otp_database. add_element (
118
+ OTPElementBuilder :: default ( )
119
+ . issuer ( "test-issuer" )
120
+ . label ( "test-label" )
121
+ . secret ( "AA" )
122
+ . build ( )
123
+ . unwrap ( ) ,
124
+ ) ;
125
+
126
+ otp_database. add_element (
127
+ OTPElementBuilder :: default ( )
128
+ . issuer ( "test-issuer2" )
129
+ . label ( "test-label2" )
130
+ . secret ( "AA" )
131
+ . build ( )
132
+ . unwrap ( ) ,
133
+ ) ;
134
+
135
+ let filter = ExtractFilterGlob {
136
+ issuer_glob : Some ( Glob :: new ( "test-iss*" ) . unwrap ( ) . compile_matcher ( ) ) ,
137
+ label_glob : None ,
138
+ index : None ,
139
+ } ;
140
+
141
+ // Act
142
+ let found_match = find_match ( & otp_database, filter) ;
143
+
144
+ // Assert
145
+ assert ! ( found_match. is_some( ) ) ;
146
+ }
147
+
148
+ #[ test]
149
+ fn test_glob_filtering_good_label ( ) {
150
+ // Arrange
151
+ let mut otp_database = OTPDatabase :: default ( ) ;
152
+ otp_database. add_element (
153
+ OTPElementBuilder :: default ( )
154
+ . issuer ( "test-issuer" )
155
+ . label ( "test-label" )
156
+ . secret ( "AA" )
157
+ . build ( )
158
+ . unwrap ( ) ,
159
+ ) ;
160
+
161
+ otp_database. add_element (
162
+ OTPElementBuilder :: default ( )
163
+ . issuer ( "test-issuer2" )
164
+ . label ( "test-label2" )
165
+ . secret ( "AA" )
166
+ . build ( )
167
+ . unwrap ( ) ,
168
+ ) ;
169
+
170
+ let filter = ExtractFilterGlob {
171
+ issuer_glob : None ,
172
+ label_glob : Some ( Glob :: new ( "test-la*" ) . unwrap ( ) . compile_matcher ( ) ) ,
173
+ index : None ,
174
+ } ;
175
+
176
+ // Act
177
+ let found_match = find_match ( & otp_database, filter) ;
178
+
179
+ // Assert
180
+ assert ! ( found_match. is_some( ) ) ;
181
+ }
182
+
183
+ #[ test]
184
+ fn test_glob_filtering_no_match ( ) {
185
+ // Arrange
186
+ let mut otp_database = OTPDatabase :: default ( ) ;
187
+ otp_database. add_element (
188
+ OTPElementBuilder :: default ( )
189
+ . issuer ( "test-issuer" )
190
+ . label ( "test-label" )
191
+ . secret ( "AA" )
192
+ . build ( )
193
+ . unwrap ( ) ,
194
+ ) ;
195
+
196
+ otp_database. add_element (
197
+ OTPElementBuilder :: default ( )
198
+ . issuer ( "test-issuer2" )
199
+ . label ( "test-label2" )
200
+ . secret ( "AA" )
201
+ . build ( )
202
+ . unwrap ( ) ,
203
+ ) ;
204
+
205
+ let filter = ExtractFilterGlob {
206
+ issuer_glob : None ,
207
+ label_glob : Some ( Glob :: new ( "test-lala*" ) . unwrap ( ) . compile_matcher ( ) ) ,
208
+ index : None ,
209
+ } ;
210
+
211
+ // Act
212
+ let found_match = find_match ( & otp_database, filter) ;
213
+
214
+ // Assert
215
+ assert ! ( found_match. is_none( ) ) ;
216
+ }
217
+
218
+ #[ test]
219
+ fn test_glob_filtering_multiple_filters_match ( ) {
220
+ // Arrange
221
+ let mut otp_database = OTPDatabase :: default ( ) ;
222
+ otp_database. add_element (
223
+ OTPElementBuilder :: default ( )
224
+ . issuer ( "test-issuer" )
225
+ . label ( "test-label" )
226
+ . secret ( "AA" )
227
+ . build ( )
228
+ . unwrap ( ) ,
229
+ ) ;
230
+
231
+ otp_database. add_element (
232
+ OTPElementBuilder :: default ( )
233
+ . issuer ( "test-issuer2" )
234
+ . label ( "test-label2" )
235
+ . secret ( "AA" )
236
+ . build ( )
237
+ . unwrap ( ) ,
238
+ ) ;
239
+
240
+ let filter = ExtractFilterGlob {
241
+ issuer_glob : Some ( Glob :: new ( "test*" ) . unwrap ( ) . compile_matcher ( ) ) ,
242
+ label_glob : Some ( Glob :: new ( "test-la*" ) . unwrap ( ) . compile_matcher ( ) ) ,
243
+ index : None ,
244
+ } ;
245
+
246
+ // Act
247
+ let found_match = find_match ( & otp_database, filter) ;
248
+
249
+ // Assert
250
+ assert ! ( found_match. is_some( ) ) ;
251
+ }
252
+
253
+ #[ test]
254
+ fn test_glob_filtering_multiple_filters_no_match ( ) {
255
+ // Arrange
256
+ let mut otp_database = OTPDatabase :: default ( ) ;
257
+ otp_database. add_element (
258
+ OTPElementBuilder :: default ( )
259
+ . issuer ( "test-issuer" )
260
+ . label ( "test-label" )
261
+ . secret ( "AA" )
262
+ . build ( )
263
+ . unwrap ( ) ,
264
+ ) ;
265
+
266
+ otp_database. add_element (
267
+ OTPElementBuilder :: default ( )
268
+ . issuer ( "test-issuer2" )
269
+ . label ( "test-label2" )
270
+ . secret ( "AA" )
271
+ . build ( )
272
+ . unwrap ( ) ,
273
+ ) ;
274
+
275
+ let filter = ExtractFilterGlob {
276
+ issuer_glob : Some ( Glob :: new ( "test-no*" ) . unwrap ( ) . compile_matcher ( ) ) ,
277
+ label_glob : Some ( Glob :: new ( "test-la*" ) . unwrap ( ) . compile_matcher ( ) ) ,
278
+ index : None ,
279
+ } ;
280
+
281
+ // Act
282
+ let found_match = find_match ( & otp_database, filter) ;
283
+
284
+ // Assert
285
+ assert ! ( found_match. is_none( ) ) ;
286
+ }
287
+ }
0 commit comments