1use bitwarden_vault::FieldType;
2use credential_exchange_format::{
3 EditableField, EditableFieldBoolean, EditableFieldConcealedString, EditableFieldCountryCode,
4 EditableFieldDate, EditableFieldEmail, EditableFieldNumber, EditableFieldString,
5 EditableFieldSubdivisionCode, EditableFieldType as CxfEditableFieldType, EditableFieldValue,
6 EditableFieldWifiNetworkSecurityType, EditableFieldYearMonth, FieldType as CxfFieldType,
7 UnexpectedField,
8};
9
10use crate::Field;
11
12pub(super) fn create_field<T>(
18 field: &EditableField<T>,
19 overridden_name: Option<impl Into<String>>,
20) -> Field
21where
22 T: InnerFieldType + CxfEditableFieldType,
23{
24 let value = match field.value.as_expected() {
25 Ok(t) => t.to_field_value(),
26 Err(unexpected) => unexpected.to_field_value(),
27 };
28
29 Field {
30 name: overridden_name
31 .map(Into::into)
32 .or_else(|| field.label.clone()),
33 value: Some(value),
34 r#type: cxf_to_bitwarden_field_type(&field.value.field_type()) as u8,
35 linked_id: None,
36 }
37}
38
39fn cxf_to_bitwarden_field_type(cxf_type: &CxfFieldType) -> FieldType {
44 match cxf_type {
45 CxfFieldType::ConcealedString => FieldType::Hidden,
46 CxfFieldType::Boolean => FieldType::Boolean,
47 _ => FieldType::Text,
48 }
49}
50
51pub(super) fn create_editable_field<T>(name: String, value: T) -> EditableField<T> {
53 EditableField {
54 id: None,
55 label: Some(name),
56 value: value.into(),
57 extensions: None,
58 }
59}
60
61pub(super) fn field_to_editable_field_value(field: Field) -> Option<EditableFieldValue> {
63 let name = field.name?;
64
65 match field.r#type {
66 x if x == FieldType::Text as u8 => field.value.map(|value| {
67 EditableFieldValue::String(create_editable_field(name, EditableFieldString(value)))
68 }),
69
70 x if x == FieldType::Hidden as u8 => field.value.map(|value| {
71 EditableFieldValue::ConcealedString(create_editable_field(
72 name,
73 EditableFieldConcealedString(value),
74 ))
75 }),
76
77 x if x == FieldType::Boolean as u8 => field.value?.parse::<bool>().ok().map(|bool_value| {
78 EditableFieldValue::Boolean(create_editable_field(
79 name,
80 EditableFieldBoolean(bool_value),
81 ))
82 }),
83
84 x if x == FieldType::Linked as u8 => {
85 let value = field
86 .value
87 .or_else(|| field.linked_id.map(|id| id.to_string()))?;
88 Some(EditableFieldValue::String(create_editable_field(
89 name,
90 EditableFieldString(value),
91 )))
92 }
93
94 _ => field.value.map(|value| {
95 EditableFieldValue::String(create_editable_field(name, EditableFieldString(value)))
96 }),
97 }
98}
99
100pub(super) trait InnerFieldType: Clone + Into<String> {
106 fn to_field_value(&self) -> String {
107 self.clone().into()
108 }
109}
110
111impl InnerFieldType for EditableFieldString {}
112impl InnerFieldType for EditableFieldConcealedString {}
113impl InnerFieldType for EditableFieldBoolean {}
114impl InnerFieldType for EditableFieldDate {}
115impl InnerFieldType for EditableFieldYearMonth {}
116impl InnerFieldType for EditableFieldCountryCode {}
117impl InnerFieldType for EditableFieldSubdivisionCode {}
118impl InnerFieldType for EditableFieldEmail {}
119impl InnerFieldType for EditableFieldNumber {}
120
121impl InnerFieldType for EditableFieldWifiNetworkSecurityType {
122 fn to_field_value(&self) -> String {
123 use EditableFieldWifiNetworkSecurityType::*;
124 match self {
125 Unsecured => "Unsecured",
126 WpaPersonal => "WPA Personal",
127 Wpa2Personal => "WPA2 Personal",
128 Wpa3Personal => "WPA3 Personal",
129 Wep => "WEP",
130 Other(s) => s,
131 _ => "Unknown",
132 }
133 .to_string()
134 }
135}
136
137impl InnerFieldType for UnexpectedField {
141 fn to_field_value(&self) -> String {
142 match self {
143 UnexpectedField::String(v) => v.to_field_value(),
144 UnexpectedField::ConcealedString(v) => v.to_field_value(),
145 UnexpectedField::Boolean(v) => v.to_field_value(),
146 UnexpectedField::Date(v) => v.to_field_value(),
147 UnexpectedField::YearMonth(v) => v.to_field_value(),
148 UnexpectedField::SubdivisionCode(v) => v.to_field_value(),
149 UnexpectedField::CountryCode(v) => v.to_field_value(),
150 UnexpectedField::WifiNetworkSecurityType(v) => v.to_field_value(),
151 UnexpectedField::Email(v) => v.to_field_value(),
152 UnexpectedField::Number(v) => v.to_field_value(),
153 UnexpectedField::Unknown { value, .. } => value.clone(),
154 _ => self.clone().into(),
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_create_field_string() {
165 let editable_field = EditableField {
166 id: None,
167 label: None,
168 value: EditableFieldString("Test Value".to_string()).into(),
169 extensions: None,
170 };
171
172 let field = create_field(&editable_field, Some("Test Name"));
173
174 assert_eq!(
175 field,
176 Field {
177 name: Some("Test Name".to_string()),
178 value: Some("Test Value".to_string()),
179 r#type: FieldType::Text as u8,
180 linked_id: None,
181 }
182 );
183 }
184
185 #[test]
186 fn test_create_field_concealed_string() {
187 let editable_field = EditableField {
188 id: None,
189 label: None,
190 value: EditableFieldConcealedString("Secret123".to_string()).into(),
191 extensions: None,
192 };
193
194 let field = create_field(&editable_field, Some("Password"));
195
196 assert_eq!(
197 field,
198 Field {
199 name: Some("Password".to_string()),
200 value: Some("Secret123".to_string()),
201 r#type: FieldType::Hidden as u8,
202 linked_id: None,
203 }
204 );
205 }
206
207 #[test]
208 fn test_create_field_boolean_true() {
209 let editable_field = EditableField {
210 id: None,
211 label: None,
212 value: EditableFieldBoolean(true).into(),
213 extensions: None,
214 };
215
216 let field = create_field(&editable_field, Some("Is Enabled"));
217
218 assert_eq!(
219 field,
220 Field {
221 name: Some("Is Enabled".to_string()),
222 value: Some("true".to_string()),
223 r#type: FieldType::Boolean as u8,
224 linked_id: None,
225 }
226 );
227 }
228
229 #[test]
230 fn test_create_field_boolean_false() {
231 let editable_field = EditableField {
232 id: None,
233 label: None,
234 value: EditableFieldBoolean(false).into(),
235 extensions: None,
236 };
237
238 let field = create_field(&editable_field, Some("Is Hidden"));
239
240 assert_eq!(
241 field,
242 Field {
243 name: Some("Is Hidden".to_string()),
244 value: Some("false".to_string()),
245 r#type: FieldType::Boolean as u8,
246 linked_id: None,
247 }
248 );
249 }
250
251 #[test]
252 fn test_create_field_wifi_security() {
253 let editable_field = EditableField {
254 id: None,
255 label: None,
256 value: EditableFieldWifiNetworkSecurityType::Wpa3Personal.into(),
257 extensions: None,
258 };
259
260 let field = create_field(&editable_field, Some("WiFi Security"));
261
262 assert_eq!(
263 field,
264 Field {
265 name: Some("WiFi Security".to_string()),
266 value: Some("WPA3 Personal".to_string()),
267 r#type: FieldType::Text as u8,
268 linked_id: None,
269 }
270 );
271 }
272
273 #[test]
274 fn test_create_field_email() {
275 let editable_field = EditableField {
276 id: None,
277 label: None,
278 value: EditableFieldEmail("[email protected]".to_string()).into(),
279 extensions: None,
280 };
281
282 let field = create_field(&editable_field, Some("Email"));
283
284 assert_eq!(
285 field,
286 Field {
287 name: Some("Email".to_string()),
288 value: Some("[email protected]".to_string()),
289 r#type: FieldType::Text as u8,
290 linked_id: None,
291 }
292 );
293 }
294
295 #[test]
296 fn test_wifi_security_to_field_value() {
297 assert_eq!(
298 EditableFieldWifiNetworkSecurityType::Unsecured.to_field_value(),
299 "Unsecured"
300 );
301 assert_eq!(
302 EditableFieldWifiNetworkSecurityType::WpaPersonal.to_field_value(),
303 "WPA Personal"
304 );
305 assert_eq!(
306 EditableFieldWifiNetworkSecurityType::Wpa2Personal.to_field_value(),
307 "WPA2 Personal"
308 );
309 assert_eq!(
310 EditableFieldWifiNetworkSecurityType::Wpa3Personal.to_field_value(),
311 "WPA3 Personal"
312 );
313 assert_eq!(
314 EditableFieldWifiNetworkSecurityType::Wep.to_field_value(),
315 "WEP"
316 );
317 assert_eq!(
318 EditableFieldWifiNetworkSecurityType::Other("WPA2 Enterprise".to_string())
319 .to_field_value(),
320 "WPA2 Enterprise"
321 );
322 }
323
324 #[test]
325 fn test_create_field_date() {
326 use chrono::NaiveDate;
327
328 let editable_field = EditableField {
329 id: None,
330 label: None,
331 value: EditableFieldDate(NaiveDate::from_ymd_opt(2025, 1, 15).unwrap()).into(),
332 extensions: None,
333 };
334
335 let field = create_field(&editable_field, Some("Expiry Date".to_string()));
336
337 assert_eq!(
338 field,
339 Field {
340 name: Some("Expiry Date".to_string()),
341 value: Some("2025-01-15".to_string()),
342 r#type: FieldType::Text as u8,
343 linked_id: None,
344 }
345 );
346 }
347
348 #[test]
349 fn test_create_field_year_month() {
350 use chrono::Month;
351
352 let editable_field = EditableField {
353 id: None,
354 label: None,
355 value: EditableFieldYearMonth {
356 year: 2025,
357 month: Month::December,
358 }
359 .into(),
360 extensions: None,
361 };
362
363 let field = create_field(&editable_field, Some("Card Expiry"));
364
365 assert_eq!(
366 field,
367 Field {
368 name: Some("Card Expiry".to_string()),
369 value: Some("2025-12".to_string()),
370 r#type: FieldType::Text as u8,
371 linked_id: None,
372 }
373 );
374 }
375
376 #[test]
377 fn test_create_field_with_none_name_uses_label() {
378 let editable_field = EditableField {
379 id: None,
380 label: Some("Label From Field".to_string()),
381 value: EditableFieldString("Test Value".to_string()).into(),
382 extensions: None,
383 };
384
385 let field = create_field(&editable_field, None::<String>);
386
387 assert_eq!(
388 field,
389 Field {
390 name: Some("Label From Field".to_string()),
391 value: Some("Test Value".to_string()),
392 r#type: FieldType::Text as u8,
393 linked_id: None,
394 }
395 );
396 }
397
398 #[test]
402 fn test_create_field_unexpected_type_reports_actual_type() {
403 let editable_field: EditableField<EditableFieldString> =
404 serde_json::from_value(serde_json::json!({
405 "fieldType": "boolean",
406 "value": "true",
407 }))
408 .unwrap();
409
410 let field = create_field(&editable_field, Some("Mismatched"));
411
412 assert_eq!(
413 field,
414 Field {
415 name: Some("Mismatched".to_string()),
416 value: Some("true".to_string()),
417 r#type: FieldType::Boolean as u8,
418 linked_id: None,
419 }
420 );
421 }
422
423 #[test]
425 fn test_create_field_unexpected_concealed_string_in_string_slot() {
426 let editable_field: EditableField<EditableFieldString> =
427 serde_json::from_value(serde_json::json!({
428 "fieldType": "concealed-string",
429 "value": "secret",
430 }))
431 .unwrap();
432
433 let field = create_field(&editable_field, Some("Hidden Surprise"));
434
435 assert_eq!(
436 field,
437 Field {
438 name: Some("Hidden Surprise".to_string()),
439 value: Some("secret".to_string()),
440 r#type: FieldType::Hidden as u8,
441 linked_id: None,
442 }
443 );
444 }
445
446 #[test]
450 fn test_create_field_unexpected_wifi_security_renders_in_vault_form() {
451 let editable_field: EditableField<EditableFieldString> =
452 serde_json::from_value(serde_json::json!({
453 "fieldType": "wifi-network-security-type",
454 "value": "wpa3-personal",
455 }))
456 .unwrap();
457
458 let field = create_field(&editable_field, Some("WiFi"));
459
460 assert_eq!(field.value, Some("WPA3 Personal".to_string()));
461 }
462
463 #[test]
465 fn test_create_field_unknown_cxf_type_falls_back_to_text() {
466 let editable_field: EditableField<EditableFieldString> =
467 serde_json::from_value(serde_json::json!({
468 "fieldType": "some-future-type",
469 "value": "future-value",
470 }))
471 .unwrap();
472
473 let field = create_field(&editable_field, Some("Future"));
474
475 assert_eq!(
476 field,
477 Field {
478 name: Some("Future".to_string()),
479 value: Some("future-value".to_string()),
480 r#type: FieldType::Text as u8,
481 linked_id: None,
482 }
483 );
484 }
485
486 #[test]
487 fn test_create_field_with_none_name_and_none_label() {
488 let editable_field = EditableField {
489 id: None,
490 label: None,
491 value: EditableFieldString("Test Value".to_string()).into(),
492 extensions: None,
493 };
494
495 let field = create_field(&editable_field, None::<String>);
496
497 assert_eq!(
498 field,
499 Field {
500 name: None,
501 value: Some("Test Value".to_string()),
502 r#type: FieldType::Text as u8,
503 linked_id: None,
504 }
505 );
506 }
507}