bitwarden_exporters/cxf/
card.rs1use bitwarden_vault::CardBrand;
6use chrono::Month;
7use credential_exchange_format::{Credential, CreditCardCredential, EditableFieldYearMonth};
8use num_traits::FromPrimitive;
9
10use crate::Card;
11
12impl From<Card> for Vec<Credential> {
13 fn from(value: Card) -> Self {
14 let expiry_date = match (value.exp_year, value.exp_month) {
15 (Some(year), Some(month)) => {
16 let year_parsed = year.parse().ok();
17 let numeric_month: Option<u32> = month.parse().ok();
18 let month_parsed = numeric_month.and_then(Month::from_u32);
19 match (year_parsed, month_parsed) {
20 (Some(year), Some(month)) => {
21 Some(EditableFieldYearMonth { year, month }.into())
22 }
23 _ => None,
24 }
25 }
26 _ => None,
27 };
28
29 vec![Credential::CreditCard(Box::new(CreditCardCredential {
30 number: value.number.map(|v| v.into()),
31 full_name: value.cardholder_name.map(|v| v.into()),
32 card_type: value.brand.map(|v| v.into()),
33 verification_number: value.code.map(|v| v.into()),
34 pin: None,
35 expiry_date,
36 valid_from: None,
37 }))]
38 }
39}
40
41impl From<&CreditCardCredential> for Card {
42 fn from(value: &CreditCardCredential) -> Self {
43 Card {
44 cardholder_name: value.full_name.clone().map(|v| v.into()),
45 exp_month: value
46 .expiry_date
47 .as_ref()
48 .map(|v| v.value.month.number_from_month().to_string()),
49 exp_year: value.expiry_date.as_ref().map(|v| v.value.year.to_string()),
50 code: value.verification_number.clone().map(|v| v.into()),
51 brand: value
52 .card_type
53 .as_ref()
54 .and_then(|brand| sanitize_brand(&brand.value.0)),
55 number: value.number.clone().map(|v| v.into()),
56 }
57 }
58}
59
60fn sanitize_brand(value: &str) -> Option<String> {
68 match value.to_lowercase().replace(" ", "").as_str() {
69 "visa" => Some(CardBrand::Visa),
70 "mastercard" => Some(CardBrand::Mastercard),
71 "amex" | "americanexpress" => Some(CardBrand::Amex),
72 "discover" => Some(CardBrand::Discover),
73 "dinersclub" => Some(CardBrand::DinersClub),
74 "jcb" => Some(CardBrand::Jcb),
75 "maestro" => Some(CardBrand::Maestro),
76 "unionpay" => Some(CardBrand::UnionPay),
77 "rupay" => Some(CardBrand::RuPay),
78 _ => None,
79 }
80 .and_then(|brand| serde_json::to_value(&brand).ok())
81 .and_then(|v| v.as_str().map(|s| s.to_string()))
82}
83
84#[cfg(test)]
85mod tests {
86 use chrono::Month;
87 use credential_exchange_format::EditableFieldYearMonth;
88
89 use super::*;
90
91 #[test]
92 fn test_sanitize_brand() {
93 assert_eq!(sanitize_brand("Visa"), Some("Visa".to_string()));
94 assert_eq!(sanitize_brand(" visa "), Some("Visa".to_string()));
95 assert_eq!(sanitize_brand("MasterCard"), Some("Mastercard".to_string()));
96 assert_eq!(sanitize_brand("amex"), Some("Amex".to_string()));
97 assert_eq!(sanitize_brand("American Express"), Some("Amex".to_string()));
98 assert_eq!(
99 sanitize_brand("DinersClub"),
100 Some("Diners Club".to_string())
101 );
102 assert_eq!(sanitize_brand("j c b"), Some("JCB".to_string()));
103 assert_eq!(sanitize_brand("Some unknown"), None);
104 }
105
106 #[test]
107 fn test_card_to_credentials() {
108 let card = Card {
109 cardholder_name: Some("John Doe".to_string()),
110 exp_month: Some("12".to_string()),
111 exp_year: Some("2025".to_string()),
112 code: Some("123".to_string()),
113 brand: Some("Visa".to_string()),
114 number: Some("4111111111111111".to_string()),
115 };
116
117 let credentials: Vec<Credential> = card.into();
118 assert_eq!(credentials.len(), 1);
119
120 if let Credential::CreditCard(credit_card) = &credentials[0] {
121 assert_eq!(credit_card.full_name.as_ref().unwrap().value.0, "John Doe");
122 assert_eq!(
123 credit_card.expiry_date.as_ref().unwrap().value,
124 EditableFieldYearMonth {
125 year: 2025,
126 month: Month::December
127 }
128 );
129 assert_eq!(
130 credit_card.verification_number.as_ref().unwrap().value.0,
131 "123".to_string()
132 );
133 assert_eq!(
134 credit_card.card_type.as_ref().unwrap().value.0,
135 "Visa".to_string()
136 );
137 assert_eq!(
138 credit_card.number.as_ref().unwrap().value.0,
139 "4111111111111111"
140 );
141 } else {
142 panic!("Expected CreditCardCredential");
143 }
144 }
145
146 #[test]
147 fn test_credit_card_credential_to_card() {
148 let credit_card = CreditCardCredential {
149 number: Some("4111111111111111".to_string().into()),
150 full_name: Some("John Doe".to_string().into()),
151 card_type: Some("Visa".to_string().into()),
152 verification_number: Some("123".to_string().into()),
153 pin: None,
154 expiry_date: Some(
155 EditableFieldYearMonth {
156 year: 2025,
157 month: Month::December,
158 }
159 .into(),
160 ),
161 valid_from: None,
162 };
163
164 let card: Card = (&credit_card).into();
165 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
166 assert_eq!(card.exp_month, Some("12".to_string()));
167 assert_eq!(card.exp_year, Some("2025".to_string()));
168 assert_eq!(card.code, Some("123".to_string()));
169 assert_eq!(card.brand, Some("Visa".to_string()));
170 assert_eq!(card.number, Some("4111111111111111".to_string()));
171 }
172}