Skip to main content

bitwarden_importers/importers/onepassword/convert/
value.rs

1//! Renders a 1Password field value as the text of one Bitwarden custom field.
2
3use chrono::{DateTime, TimeDelta};
4
5/// Renders a field value as the text of one custom field. 1Password stores dates as unix seconds
6/// and month/year as an integer such as `202112`; anything else it sends stays in its JSON form.
7pub(super) fn render_value(kind: Option<&str>, value: &serde_json::Value) -> Option<String> {
8    match (kind, value) {
9        (_, serde_json::Value::Null) => None,
10        (_, serde_json::Value::String(text)) => non_blank(text).map(str::to_string),
11        (Some("date"), serde_json::Value::Number(seconds)) => seconds
12            .as_i64()
13            .map(render_date)
14            .or(Some(value.to_string())),
15        (Some("monthYear"), serde_json::Value::Number(number)) => Some(
16            number
17                .as_i64()
18                .and_then(month_year)
19                .map(|(year, month)| format!("{year:04}-{month:02}"))
20                .unwrap_or_else(|| value.to_string()),
21        ),
22        (_, other) => Some(other.to_string()),
23    }
24}
25
26/// 1Password writes a date as midnight in the writer's own time zone, which it does not record.
27/// Adding 12 hours before formatting in UTC recovers the intended day for every zone from UTC-11
28/// to UTC+12; further east it lands a day early. No single shift covers the full 26 hours of real
29/// offsets.
30fn render_date(seconds: i64) -> String {
31    DateTime::from_timestamp(seconds, 0)
32        .and_then(|date| date.checked_add_signed(TimeDelta::hours(12)))
33        .map(|date| date.format("%Y-%m-%d").to_string())
34        .unwrap_or_else(|| seconds.to_string())
35}
36
37/// Splits a month/year into year and month: `202112` is December 2021.
38pub(super) fn month_year(value: i64) -> Option<(i64, i64)> {
39    let (year, month) = (value / 100, value % 100);
40    (1..=12).contains(&month).then_some((year, month))
41}
42
43pub(super) fn non_blank(value: &str) -> Option<&str> {
44    (!value.trim().is_empty()).then_some(value)
45}