sos_migrate/import/csv/
macos.rs

1//! Parser for the MacOS passwords CSV export.
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use sos_core::crypto::AccessKey;
6use sos_vault::Vault;
7use sos_vfs as vfs;
8use std::path::{Path, PathBuf};
9use tokio::io::AsyncRead;
10use url::Url;
11
12use super::{
13    GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED,
14};
15use crate::{import::read_csv_records, Convert, Result};
16
17/// Record for an entry in a MacOS passwords CSV export.
18#[derive(Deserialize)]
19pub struct MacPasswordRecord {
20    /// The title of the entry.
21    #[serde(rename = "Title")]
22    pub title: String,
23    /// The URL of the entry.
24    #[serde(rename = "Url")]
25    pub url: Option<Url>,
26    /// The username for the entry.
27    #[serde(rename = "Username")]
28    pub username: String,
29    /// The password for the entry.
30    #[serde(rename = "Password")]
31    pub password: String,
32    /// Notes for the entry.
33    #[serde(rename = "Notes")]
34    pub notes: Option<String>,
35    /// OTP auth information for the entry.
36    #[serde(rename = "OTPAuth")]
37    pub otp_auth: Option<String>,
38}
39
40impl From<MacPasswordRecord> for GenericPasswordRecord {
41    fn from(value: MacPasswordRecord) -> Self {
42        let label = if value.title.is_empty() {
43            UNTITLED.to_owned()
44        } else {
45            value.title
46        };
47
48        let url = if let Some(url) = value.url {
49            vec![url]
50        } else {
51            vec![]
52        };
53
54        Self {
55            label,
56            url,
57            username: value.username,
58            password: value.password,
59            otp_auth: value.otp_auth,
60            tags: None,
61            note: value.notes,
62        }
63    }
64}
65
66impl From<MacPasswordRecord> for GenericCsvEntry {
67    fn from(value: MacPasswordRecord) -> Self {
68        Self::Password(value.into())
69    }
70}
71
72/// Parse records from a reader.
73pub async fn parse_reader<R: AsyncRead + Unpin + Send>(
74    reader: R,
75) -> Result<Vec<MacPasswordRecord>> {
76    read_csv_records::<MacPasswordRecord, _>(reader).await
77}
78
79/// Parse records from a path.
80pub async fn parse_path<P: AsRef<Path>>(
81    path: P,
82) -> Result<Vec<MacPasswordRecord>> {
83    parse_reader(vfs::File::open(path).await?).await
84}
85
86/// Import a MacOS passwords CSV export into a vault.
87pub struct MacPasswordCsv;
88
89#[async_trait]
90impl Convert for MacPasswordCsv {
91    type Input = PathBuf;
92
93    async fn convert(
94        &self,
95        source: Self::Input,
96        vault: Vault,
97        key: &AccessKey,
98    ) -> crate::Result<Vault> {
99        let records: Vec<GenericCsvEntry> = parse_path(source)
100            .await?
101            .into_iter()
102            .map(|r| r.into())
103            .collect();
104        GenericCsvConvert.convert(records, vault, key).await
105    }
106}