bitwarden_exporters/cxf/
editable_field.rs

1use bitwarden_vault::FieldType;
2use credential_exchange_format::{
3    EditableField, EditableFieldBoolean, EditableFieldConcealedString, EditableFieldCountryCode,
4    EditableFieldDate, EditableFieldString, EditableFieldSubdivisionCode, EditableFieldValue,
5    EditableFieldWifiNetworkSecurityType, EditableFieldYearMonth,
6};
7
8use crate::Field;
9
10/// Helper function to create a Field from any EditableField type
11pub(super) fn create_field<T>(field: &T, overridden_name: Option<impl Into<String>>) -> Field
12where
13    T: EditableFieldToField,
14{
15    let field_name = overridden_name
16        .map(Into::into)
17        .or_else(|| field.label().clone());
18
19    Field {
20        name: field_name,
21        value: Some(field.field_value()),
22        r#type: T::FIELD_TYPE as u8,
23        linked_id: None,
24    }
25}
26
27/// Helper function to create an EditableField with common properties
28pub(super) fn create_editable_field<T>(name: String, value: T) -> EditableField<T> {
29    EditableField {
30        id: None,
31        label: Some(name),
32        value,
33        extensions: None,
34    }
35}
36
37/// Convert Bitwarden Field to CXF EditableFieldValue with proper type mapping
38pub(super) fn field_to_editable_field_value(field: Field) -> Option<EditableFieldValue> {
39    let name = field.name?;
40
41    match field.r#type {
42        x if x == FieldType::Text as u8 => field.value.map(|value| {
43            EditableFieldValue::String(create_editable_field(name, EditableFieldString(value)))
44        }),
45
46        x if x == FieldType::Hidden as u8 => field.value.map(|value| {
47            EditableFieldValue::ConcealedString(create_editable_field(
48                name,
49                EditableFieldConcealedString(value),
50            ))
51        }),
52
53        x if x == FieldType::Boolean as u8 => field.value?.parse::<bool>().ok().map(|bool_value| {
54            EditableFieldValue::Boolean(create_editable_field(
55                name,
56                EditableFieldBoolean(bool_value),
57            ))
58        }),
59
60        x if x == FieldType::Linked as u8 => {
61            let value = field
62                .value
63                .or_else(|| field.linked_id.map(|id| id.to_string()))?;
64            Some(EditableFieldValue::String(create_editable_field(
65                name,
66                EditableFieldString(value),
67            )))
68        }
69
70        _ => field.value.map(|value| {
71            EditableFieldValue::String(create_editable_field(name, EditableFieldString(value)))
72        }),
73    }
74}
75
76/// Trait to define field type and value conversion for inner field types
77pub(super) trait InnerFieldType {
78    const FIELD_TYPE: FieldType;
79
80    fn to_field_value(&self) -> String;
81}
82
83impl InnerFieldType for EditableFieldString {
84    const FIELD_TYPE: FieldType = FieldType::Text;
85
86    fn to_field_value(&self) -> String {
87        self.0.clone()
88    }
89}
90
91impl InnerFieldType for EditableFieldConcealedString {
92    const FIELD_TYPE: FieldType = FieldType::Hidden;
93
94    fn to_field_value(&self) -> String {
95        self.0.clone()
96    }
97}
98
99impl InnerFieldType for EditableFieldBoolean {
100    const FIELD_TYPE: FieldType = FieldType::Boolean;
101
102    fn to_field_value(&self) -> String {
103        self.0.to_string()
104    }
105}
106
107impl InnerFieldType for EditableFieldWifiNetworkSecurityType {
108    const FIELD_TYPE: FieldType = FieldType::Text;
109
110    fn to_field_value(&self) -> String {
111        security_type_to_string(self).to_string()
112    }
113}
114
115impl InnerFieldType for EditableFieldCountryCode {
116    const FIELD_TYPE: FieldType = FieldType::Text;
117
118    fn to_field_value(&self) -> String {
119        self.0.clone()
120    }
121}
122
123impl InnerFieldType for EditableFieldDate {
124    const FIELD_TYPE: FieldType = FieldType::Text;
125
126    fn to_field_value(&self) -> String {
127        self.0.to_string()
128    }
129}
130
131impl InnerFieldType for EditableFieldYearMonth {
132    const FIELD_TYPE: FieldType = FieldType::Text;
133
134    fn to_field_value(&self) -> String {
135        format!("{:04}-{:02}", self.year, self.month.number_from_month())
136    }
137}
138
139impl InnerFieldType for EditableFieldSubdivisionCode {
140    const FIELD_TYPE: FieldType = FieldType::Text;
141
142    fn to_field_value(&self) -> String {
143        self.0.clone()
144    }
145}
146
147/// Trait to convert CXP EditableField types to Bitwarden Field values and types
148pub(super) trait EditableFieldToField {
149    const FIELD_TYPE: FieldType;
150
151    fn field_value(&self) -> String;
152    fn label(&self) -> &Option<String>;
153}
154
155impl<T> EditableFieldToField for EditableField<T>
156where
157    T: InnerFieldType,
158{
159    const FIELD_TYPE: FieldType = T::FIELD_TYPE;
160
161    fn field_value(&self) -> String {
162        self.value.to_field_value()
163    }
164
165    fn label(&self) -> &Option<String> {
166        &self.label
167    }
168}
169
170/// Convert WiFi security type enum to human-readable string
171fn security_type_to_string(security_type: &EditableFieldWifiNetworkSecurityType) -> &str {
172    use EditableFieldWifiNetworkSecurityType::*;
173    match security_type {
174        Unsecured => "Unsecured",
175        WpaPersonal => "WPA Personal",
176        Wpa2Personal => "WPA2 Personal",
177        Wpa3Personal => "WPA3 Personal",
178        Wep => "WEP",
179        Other(s) => s,
180        _ => "Unknown",
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_create_field_string() {
190        let editable_field = EditableField {
191            id: None,
192            label: None,
193            value: EditableFieldString("Test Value".to_string()),
194            extensions: None,
195        };
196
197        let field = create_field(&editable_field, Some("Test Name"));
198
199        assert_eq!(
200            field,
201            Field {
202                name: Some("Test Name".to_string()),
203                value: Some("Test Value".to_string()),
204                r#type: FieldType::Text as u8,
205                linked_id: None,
206            }
207        );
208    }
209
210    #[test]
211    fn test_create_field_concealed_string() {
212        let editable_field = EditableField {
213            id: None,
214            label: None,
215            value: EditableFieldConcealedString("Secret123".to_string()),
216            extensions: None,
217        };
218
219        let field = create_field(&editable_field, Some("Password"));
220
221        assert_eq!(
222            field,
223            Field {
224                name: Some("Password".to_string()),
225                value: Some("Secret123".to_string()),
226                r#type: FieldType::Hidden as u8,
227                linked_id: None,
228            }
229        );
230    }
231
232    #[test]
233    fn test_create_field_boolean_true() {
234        let editable_field = EditableField {
235            id: None,
236            label: None,
237            value: EditableFieldBoolean(true),
238            extensions: None,
239        };
240
241        let field = create_field(&editable_field, Some("Is Enabled"));
242
243        assert_eq!(
244            field,
245            Field {
246                name: Some("Is Enabled".to_string()),
247                value: Some("true".to_string()),
248                r#type: FieldType::Boolean as u8,
249                linked_id: None,
250            }
251        );
252    }
253
254    #[test]
255    fn test_create_field_boolean_false() {
256        let editable_field = EditableField {
257            id: None,
258            label: None,
259            value: EditableFieldBoolean(false),
260            extensions: None,
261        };
262
263        let field = create_field(&editable_field, Some("Is Hidden"));
264
265        assert_eq!(
266            field,
267            Field {
268                name: Some("Is Hidden".to_string()),
269                value: Some("false".to_string()),
270                r#type: FieldType::Boolean as u8,
271                linked_id: None,
272            }
273        );
274    }
275
276    #[test]
277    fn test_create_field_wifi_security() {
278        let editable_field = EditableField {
279            id: None,
280            label: None,
281            value: EditableFieldWifiNetworkSecurityType::Wpa3Personal,
282            extensions: None,
283        };
284
285        let field = create_field(&editable_field, Some("WiFi Security"));
286
287        assert_eq!(
288            field,
289            Field {
290                name: Some("WiFi Security".to_string()),
291                value: Some("WPA3 Personal".to_string()),
292                r#type: FieldType::Text as u8,
293                linked_id: None,
294            }
295        );
296    }
297
298    #[test]
299    fn test_security_type_to_string() {
300        assert_eq!(
301            security_type_to_string(&EditableFieldWifiNetworkSecurityType::Unsecured),
302            "Unsecured"
303        );
304        assert_eq!(
305            security_type_to_string(&EditableFieldWifiNetworkSecurityType::WpaPersonal),
306            "WPA Personal"
307        );
308        assert_eq!(
309            security_type_to_string(&EditableFieldWifiNetworkSecurityType::Wpa2Personal),
310            "WPA2 Personal"
311        );
312        assert_eq!(
313            security_type_to_string(&EditableFieldWifiNetworkSecurityType::Wpa3Personal),
314            "WPA3 Personal"
315        );
316        assert_eq!(
317            security_type_to_string(&EditableFieldWifiNetworkSecurityType::Wep),
318            "WEP"
319        );
320
321        let custom_security = "WPA2 Enterprise";
322        assert_eq!(
323            security_type_to_string(&EditableFieldWifiNetworkSecurityType::Other(
324                custom_security.to_string()
325            )),
326            custom_security
327        );
328    }
329
330    #[test]
331    fn test_create_field_date() {
332        use chrono::NaiveDate;
333
334        let editable_field = EditableField {
335            id: None,
336            label: None,
337            value: EditableFieldDate(NaiveDate::from_ymd_opt(2025, 1, 15).unwrap()),
338            extensions: None,
339        };
340
341        let field = create_field(&editable_field, Some("Expiry Date".to_string()));
342
343        assert_eq!(
344            field,
345            Field {
346                name: Some("Expiry Date".to_string()),
347                value: Some("2025-01-15".to_string()),
348                r#type: FieldType::Text as u8,
349                linked_id: None,
350            }
351        );
352    }
353
354    #[test]
355    fn test_create_field_year_month() {
356        use chrono::Month;
357
358        let editable_field = EditableField {
359            id: None,
360            label: None,
361            value: EditableFieldYearMonth {
362                year: 2025,
363                month: Month::December,
364            },
365            extensions: None,
366        };
367
368        let field = create_field(&editable_field, Some("Card Expiry"));
369
370        assert_eq!(
371            field,
372            Field {
373                name: Some("Card Expiry".to_string()),
374                value: Some("2025-12".to_string()),
375                r#type: FieldType::Text as u8,
376                linked_id: None,
377            }
378        );
379    }
380
381    #[test]
382    fn test_create_field_with_none_name_uses_label() {
383        let editable_field = EditableField {
384            id: None,
385            label: Some("Label From Field".to_string()),
386            value: EditableFieldString("Test Value".to_string()),
387            extensions: None,
388        };
389
390        let field = create_field(&editable_field, None::<String>);
391
392        assert_eq!(
393            field,
394            Field {
395                name: Some("Label From Field".to_string()),
396                value: Some("Test Value".to_string()),
397                r#type: FieldType::Text as u8,
398                linked_id: None,
399            }
400        );
401    }
402
403    #[test]
404    fn test_create_field_with_none_name_and_none_label() {
405        let editable_field = EditableField {
406            id: None,
407            label: None,
408            value: EditableFieldString("Test Value".to_string()),
409            extensions: None,
410        };
411
412        let field = create_field(&editable_field, None::<String>);
413
414        assert_eq!(
415            field,
416            Field {
417                name: None,
418                value: Some("Test Value".to_string()),
419                r#type: FieldType::Text as u8,
420                linked_id: None,
421            }
422        );
423    }
424}