sos_migrate/import/csv/
one_password.rs1use async_trait::async_trait;
4use serde::{
5 de::{self, Deserializer, Unexpected, Visitor},
6 Deserialize,
7};
8use sos_core::crypto::AccessKey;
9use sos_vault::Vault;
10use sos_vfs as vfs;
11use std::{
12 collections::HashSet,
13 fmt,
14 path::{Path, PathBuf},
15};
16use tokio::io::AsyncRead;
17use url::Url;
18
19use super::{
20 GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED,
21};
22use crate::{import::read_csv_records, Convert, Result};
23
24#[derive(Deserialize)]
26pub struct OnePasswordRecord {
27 #[serde(rename = "Title")]
29 pub title: String,
30 #[serde(rename = "Url")]
32 pub url: Option<Url>,
33 #[serde(rename = "Username")]
35 pub username: String,
36 #[serde(rename = "Password")]
38 pub password: String,
39 #[serde(rename = "OTPAuth")]
41 pub otp_auth: Option<String>,
42 #[serde(rename = "Favorite", deserialize_with = "deserialize_bool")]
44 pub favorite: bool,
45 #[serde(rename = "Archived", deserialize_with = "deserialize_bool")]
47 pub archived: bool,
48 #[serde(rename = "Tags")]
50 pub tags: String,
51 #[serde(rename = "Notes")]
53 pub notes: String,
54}
55
56impl From<OnePasswordRecord> for GenericPasswordRecord {
57 fn from(value: OnePasswordRecord) -> Self {
58 let tags: Option<HashSet<String>> = if !value.tags.is_empty() {
59 Some(value.tags.split(';').map(|s| s.trim().to_owned()).collect())
60 } else {
61 None
62 };
63
64 let label = if value.title.is_empty() {
65 UNTITLED.to_owned()
66 } else {
67 value.title
68 };
69
70 let note = if !value.notes.is_empty() {
71 Some(value.notes)
72 } else {
73 None
74 };
75
76 let url = if let Some(url) = value.url {
77 vec![url]
78 } else {
79 vec![]
80 };
81
82 Self {
83 label,
84 url,
85 username: value.username,
86 password: value.password,
87 otp_auth: value.otp_auth,
88 tags,
89 note,
90 }
91 }
92}
93
94impl From<OnePasswordRecord> for GenericCsvEntry {
95 fn from(value: OnePasswordRecord) -> Self {
96 Self::Password(value.into())
97 }
98}
99
100pub async fn parse_reader<R: AsyncRead + Unpin + Send>(
102 reader: R,
103) -> Result<Vec<OnePasswordRecord>> {
104 read_csv_records::<OnePasswordRecord, _>(reader).await
105}
106
107pub async fn parse_path<P: AsRef<Path>>(
109 path: P,
110) -> Result<Vec<OnePasswordRecord>> {
111 parse_reader(vfs::File::open(path).await?).await
112}
113
114pub struct OnePasswordCsv;
116
117#[async_trait]
118impl Convert for OnePasswordCsv {
119 type Input = PathBuf;
120
121 async fn convert(
122 &self,
123 source: Self::Input,
124 vault: Vault,
125 key: &AccessKey,
126 ) -> crate::Result<Vault> {
127 let records: Vec<GenericCsvEntry> = parse_path(source)
128 .await?
129 .into_iter()
130 .map(|r| r.into())
131 .collect();
132 GenericCsvConvert.convert(records, vault, key).await
133 }
134}
135
136struct BoolString;
137
138impl<'de> Visitor<'de> for BoolString {
139 type Value = String;
140
141 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
142 write!(formatter, "a string representing a boolean flag")
143 }
144
145 fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
146 where
147 E: de::Error,
148 {
149 let b = s.to_lowercase();
150 if b == "true" || b == "false" {
151 Ok(s.to_owned())
152 } else {
153 Err(de::Error::invalid_value(Unexpected::Str(s), &self))
154 }
155 }
156}
157
158fn deserialize_bool<'de, D>(
159 deserializer: D,
160) -> std::result::Result<bool, D::Error>
161where
162 D: Deserializer<'de>,
163{
164 let value = deserializer.deserialize_str(BoolString)?;
165 Ok(value.to_lowercase() == "true")
166}