Skip to content

Commit 2422c9d

Browse files
committed
fix: implement first glob matching logic for extract subcommand
1 parent 96530a4 commit 2422c9d

File tree

1 file changed

+235
-12
lines changed

1 file changed

+235
-12
lines changed

src/arguments/extract.rs

Lines changed: 235 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::otp::otp_element::OTPDatabase;
22
use crate::{clipboard, otp::otp_element::OTPElement};
33
use clap::Args;
44
use color_eyre::eyre::eyre;
5+
use globset::{Glob, GlobMatcher};
56

67
use 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+
2759
impl 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

Comments
 (0)