use async_trait::async_trait;
use serde::{
de::{self, Deserializer, Unexpected, Visitor},
Deserialize,
};
use sos_core::crypto::AccessKey;
use sos_vault::Vault;
use sos_vfs as vfs;
use std::{
collections::HashSet,
fmt,
path::{Path, PathBuf},
};
use tokio::io::AsyncRead;
use url::Url;
use super::{
GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED,
};
use crate::{import::read_csv_records, Convert, Result};
#[derive(Deserialize)]
pub struct OnePasswordRecord {
#[serde(rename = "Title")]
pub title: String,
#[serde(rename = "Url")]
pub url: Option<Url>,
#[serde(rename = "Username")]
pub username: String,
#[serde(rename = "Password")]
pub password: String,
#[serde(rename = "OTPAuth")]
pub otp_auth: Option<String>,
#[serde(rename = "Favorite", deserialize_with = "deserialize_bool")]
pub favorite: bool,
#[serde(rename = "Archived", deserialize_with = "deserialize_bool")]
pub archived: bool,
#[serde(rename = "Tags")]
pub tags: String,
#[serde(rename = "Notes")]
pub notes: String,
}
impl From<OnePasswordRecord> for GenericPasswordRecord {
fn from(value: OnePasswordRecord) -> Self {
let tags: Option<HashSet<String>> = if !value.tags.is_empty() {
Some(value.tags.split(';').map(|s| s.trim().to_owned()).collect())
} else {
None
};
let label = if value.title.is_empty() {
UNTITLED.to_owned()
} else {
value.title
};
let note = if !value.notes.is_empty() {
Some(value.notes)
} else {
None
};
let url = if let Some(url) = value.url {
vec![url]
} else {
vec![]
};
Self {
label,
url,
username: value.username,
password: value.password,
otp_auth: value.otp_auth,
tags,
note,
}
}
}
impl From<OnePasswordRecord> for GenericCsvEntry {
fn from(value: OnePasswordRecord) -> Self {
Self::Password(value.into())
}
}
pub async fn parse_reader<R: AsyncRead + Unpin + Send>(
reader: R,
) -> Result<Vec<OnePasswordRecord>> {
read_csv_records::<OnePasswordRecord, _>(reader).await
}
pub async fn parse_path<P: AsRef<Path>>(
path: P,
) -> Result<Vec<OnePasswordRecord>> {
parse_reader(vfs::File::open(path).await?).await
}
pub struct OnePasswordCsv;
#[async_trait]
impl Convert for OnePasswordCsv {
type Input = PathBuf;
async fn convert(
&self,
source: Self::Input,
vault: Vault,
key: &AccessKey,
) -> crate::Result<Vault> {
let records: Vec<GenericCsvEntry> = parse_path(source)
.await?
.into_iter()
.map(|r| r.into())
.collect();
GenericCsvConvert.convert(records, vault, key).await
}
}
struct BoolString;
impl<'de> Visitor<'de> for BoolString {
type Value = String;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a string representing a boolean flag")
}
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
let b = s.to_lowercase();
if b == "true" || b == "false" {
Ok(s.to_owned())
} else {
Err(de::Error::invalid_value(Unexpected::Str(s), &self))
}
}
}
fn deserialize_bool<'de, D>(
deserializer: D,
) -> std::result::Result<bool, D::Error>
where
D: Deserializer<'de>,
{
let value = deserializer.deserialize_str(BoolString)?;
Ok(value.to_lowercase() == "true")
}