use std::collections::HashMap;
use chrono::NaiveDateTime;
use secstr::SecStr;
use uuid::Uuid;
use crate::db::{Color, CustomData, Times};
#[cfg(feature = "totp")]
use crate::db::otp::{TOTPError, TOTP};
#[derive(Debug, Default, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct Entry {
pub uuid: Uuid,
pub fields: HashMap<String, Value>,
pub autotype: Option<AutoType>,
pub tags: Vec<String>,
pub times: Times,
pub custom_data: CustomData,
pub icon_id: Option<usize>,
pub custom_icon_uuid: Option<Uuid>,
pub foreground_color: Option<Color>,
pub background_color: Option<Color>,
pub override_url: Option<String>,
pub quality_check: Option<bool>,
pub history: Option<History>,
}
impl Entry {
pub fn new() -> Entry {
Entry {
uuid: Uuid::new_v4(),
times: Times::new(),
..Default::default()
}
}
}
impl<'a> Entry {
pub fn get(&'a self, key: &str) -> Option<&'a str> {
match self.fields.get(key) {
Some(&Value::Bytes(_)) => None,
Some(&Value::Protected(ref pv)) => std::str::from_utf8(pv.unsecure()).ok(),
Some(&Value::Unprotected(ref uv)) => Some(&uv),
None => None,
}
}
pub fn get_bytes(&'a self, key: &str) -> Option<&'a [u8]> {
match self.fields.get(key) {
Some(&Value::Bytes(ref b)) => Some(&b),
_ => None,
}
}
pub fn get_uuid(&'a self) -> &'a Uuid {
&self.uuid
}
pub fn get_time(&self, key: &str) -> Option<&chrono::NaiveDateTime> {
self.times.get(key)
}
pub fn get_expiry_time(&self) -> Option<&chrono::NaiveDateTime> {
self.times.get_expiry()
}
#[cfg(feature = "totp")]
pub fn get_otp(&'a self) -> Result<TOTP, TOTPError> {
self.get_raw_otp_value().ok_or(TOTPError::NoRecord)?.parse()
}
pub fn get_raw_otp_value(&'a self) -> Option<&'a str> {
self.get("otp")
}
pub fn get_title(&'a self) -> Option<&'a str> {
self.get("Title")
}
pub fn get_username(&'a self) -> Option<&'a str> {
self.get("UserName")
}
pub fn get_password(&'a self) -> Option<&'a str> {
self.get("Password")
}
pub fn get_url(&'a self) -> Option<&'a str> {
self.get("URL")
}
pub fn update_history(&mut self) -> bool {
if self.history.is_none() {
self.history = Some(History::default());
}
if !self.has_uncommitted_changes() {
return false;
}
self.times.set_last_modification(Times::now());
let mut new_history_entry = self.clone();
new_history_entry.history.take().unwrap();
self.history.as_mut().unwrap().add_entry(new_history_entry);
true
}
fn has_uncommitted_changes(&self) -> bool {
if let Some(history) = self.history.as_ref() {
if history.entries.len() == 0 {
return true;
}
let mut sanitized_entry = self.clone();
sanitized_entry
.times
.set_last_modification(NaiveDateTime::default());
sanitized_entry.history.take();
let mut last_history_entry = history.entries.get(0).unwrap().clone();
last_history_entry
.times
.set_last_modification(NaiveDateTime::default());
last_history_entry.history.take();
if sanitized_entry.eq(&last_history_entry) {
return false;
}
}
true
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum Value {
Bytes(Vec<u8>),
Unprotected(String),
Protected(SecStr),
}
impl Value {
pub fn is_empty(&self) -> bool {
match self {
Value::Bytes(b) => b.is_empty(),
Value::Unprotected(u) => u.is_empty(),
Value::Protected(p) => p.unsecure().is_empty(),
}
}
}
#[cfg(feature = "serialization")]
impl serde::Serialize for Value {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Value::Bytes(b) => serializer.serialize_bytes(b),
Value::Unprotected(u) => serializer.serialize_str(u),
Value::Protected(p) => serializer.serialize_str(String::from_utf8_lossy(p.unsecure()).as_ref()),
}
}
}
#[derive(Debug, Default, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct AutoType {
pub enabled: bool,
pub sequence: Option<String>,
pub associations: Vec<AutoTypeAssociation>,
}
#[derive(Debug, Default, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct AutoTypeAssociation {
pub window: Option<String>,
pub sequence: Option<String>,
}
#[derive(Debug, Default, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct History {
pub(crate) entries: Vec<Entry>,
}
impl History {
pub fn add_entry(&mut self, mut entry: Entry) {
if entry.history.is_some() {
entry.history.take().unwrap();
}
self.entries.insert(0, entry);
}
pub fn get_entries(&self) -> &Vec<Entry> {
&self.entries
}
}
#[cfg(test)]
mod entry_tests {
use std::{thread, time};
use secstr::SecStr;
use super::{Entry, Value};
#[test]
fn byte_values() {
let mut entry = Entry::new();
entry
.fields
.insert("a-bytes".to_string(), Value::Bytes(vec![1, 2, 3]));
entry.fields.insert(
"a-unprotected".to_string(),
Value::Unprotected("asdf".to_string()),
);
entry.fields.insert(
"a-protected".to_string(),
Value::Protected(SecStr::new("asdf".as_bytes().to_vec())),
);
assert_eq!(entry.get_bytes("a-bytes"), Some(&[1, 2, 3][..]));
assert_eq!(entry.get_bytes("a-unprotected"), None);
assert_eq!(entry.get_bytes("a-protected"), None);
assert_eq!(entry.get("a-bytes"), None);
assert_eq!(entry.fields["a-bytes"].is_empty(), false);
}
#[test]
fn update_history() {
let mut entry = Entry::new();
let mut last_modification_time = entry.times.get_last_modification().unwrap().clone();
entry
.fields
.insert("Username".to_string(), Value::Unprotected("user".to_string()));
thread::sleep(time::Duration::from_secs(1));
assert!(entry.update_history());
assert!(entry.history.is_some());
assert_eq!(entry.history.as_ref().unwrap().entries.len(), 1);
assert_ne!(
entry.times.get_last_modification().unwrap(),
&last_modification_time
);
last_modification_time = entry.times.get_last_modification().unwrap().clone();
thread::sleep(time::Duration::from_secs(1));
assert!(!entry.update_history());
assert!(entry.history.is_some());
assert_eq!(entry.history.as_ref().unwrap().entries.len(), 1);
assert_eq!(
entry.times.get_last_modification().unwrap(),
&last_modification_time
);
entry
.fields
.insert("Title".to_string(), Value::Unprotected("first title".to_string()));
assert!(entry.update_history());
assert!(entry.history.is_some());
assert_eq!(entry.history.as_ref().unwrap().entries.len(), 2);
assert_ne!(
entry.times.get_last_modification().unwrap(),
&last_modification_time
);
last_modification_time = entry.times.get_last_modification().unwrap().clone();
thread::sleep(time::Duration::from_secs(1));
assert!(!entry.update_history());
assert!(entry.history.is_some());
assert_eq!(entry.history.as_ref().unwrap().entries.len(), 2);
assert_eq!(
entry.times.get_last_modification().unwrap(),
&last_modification_time
);
entry.fields.insert(
"Title".to_string(),
Value::Unprotected("second title".to_string()),
);
assert!(entry.update_history());
assert!(entry.history.is_some());
assert_eq!(entry.history.as_ref().unwrap().entries.len(), 3);
assert_ne!(
entry.times.get_last_modification().unwrap(),
&last_modification_time
);
last_modification_time = entry.times.get_last_modification().unwrap().clone();
thread::sleep(time::Duration::from_secs(1));
assert!(!entry.update_history());
assert!(entry.history.is_some());
assert_eq!(entry.history.as_ref().unwrap().entries.len(), 3);
assert_eq!(
entry.times.get_last_modification().unwrap(),
&last_modification_time
);
let last_history_entry = entry.history.as_ref().unwrap().entries.get(0).unwrap();
assert_eq!(last_history_entry.get_title().unwrap(), "second title");
for history_entry in &entry.history.unwrap().entries {
assert!(history_entry.history.is_none());
}
}
#[cfg(feature = "totp")]
#[test]
fn totp() {
let mut entry = Entry::new();
entry.fields.insert("otp".to_string(), Value::Unprotected("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30".to_string()));
assert!(entry.get_otp().is_ok());
}
#[cfg(feature = "serialization")]
#[test]
fn serialization() {
assert_eq!(
serde_json::to_string(&Value::Bytes(vec![65, 66, 67])).unwrap(),
"[65,66,67]".to_string()
);
assert_eq!(
serde_json::to_string(&Value::Unprotected("ABC".to_string())).unwrap(),
"\"ABC\"".to_string()
);
assert_eq!(
serde_json::to_string(&Value::Protected(SecStr::new("ABC".as_bytes().to_vec()))).unwrap(),
"\"ABC\"".to_string()
);
}
}