@@ -2,6 +2,7 @@ use crate::otp::otp_element::OTPDatabase;
22use crate :: { clipboard, otp:: otp_element:: OTPElement } ;
33use clap:: Args ;
44use color_eyre:: eyre:: eyre;
5+ use globset:: { Glob , GlobMatcher } ;
56
67use super :: SubcommandExecutor ;
78
@@ -24,19 +25,48 @@ pub struct ExtractArgs {
2425 pub copy_to_clipboard : bool ,
2526}
2627
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+
2759impl SubcommandExecutor for ExtractArgs {
2860 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) ;
3565
3666 if let Some ( otp) = first_with_filters {
3767 let code = otp. get_otp_code ( ) ?;
3868 println ! ( "{code}" ) ;
39- if self . copy_to_clipboard {
69+ if copy_to_clipboard {
4070 let _ = clipboard:: copy_string_to_clipboard ( code. as_str ( ) ) ?;
4171 println ! ( "Copied to clipboard" ) ;
4272 }
@@ -47,18 +77,211 @@ impl SubcommandExecutor for ExtractArgs {
4777 }
4878}
4979
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 {
5190 let match_by_index = args. index . is_none_or ( |i| i == index) ;
5291
5392 let match_by_issuer = args
54- . issuer
93+ . issuer_glob
5594 . as_ref ( )
56- . is_none_or ( |issuer| code . issuer . to_lowercase ( ) == issuer . to_lowercase ( ) ) ;
95+ . is_none_or ( |issuer| issuer. is_match ( & candidate . issuer ) ) ;
5796
5897 let match_by_label = args
59- . label
98+ . label_glob
6099 . as_ref ( )
61- . is_none_or ( |label| code . label . to_lowercase ( ) == label . to_lowercase ( ) ) ;
100+ . is_none_or ( |label| label. is_match ( & candidate . label ) ) ;
62101
63102 match_by_index && match_by_issuer && match_by_label
64103}
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