use std::{
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
};
use thiserror::Error;
use uuid::Uuid;
use crate::{
db::{
attachment::{AttachmentMut, AttachmentRef},
fields, Attachment, AttachmentId, AutoType, Color, CustomDataItem, CustomIcon, CustomIconId,
CustomIconMut, CustomIconNotFoundError, CustomIconRef, GroupId, GroupMut, GroupRef, History, Icon,
Times, Value,
},
Database,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct EntryId(Uuid);
impl EntryId {
pub(crate) fn new() -> Self {
Self(Uuid::new_v4())
}
pub(crate) const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn uuid(&self) -> Uuid {
self.0
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct Entry {
pub(crate) id: EntryId,
pub(crate) parent: GroupId,
pub fields: HashMap<String, Value<String>>,
pub autotype: Option<AutoType>,
pub tags: Vec<String>,
pub times: Times,
pub custom_data: HashMap<String, CustomDataItem>,
pub(crate) icon: Option<Icon>,
pub foreground_color: Option<Color>,
pub background_color: Option<Color>,
pub override_url: Option<String>,
pub quality_check: Option<bool>,
pub(crate) attachments: HashMap<String, AttachmentId>,
pub history: Option<History>,
}
impl Entry {
pub(crate) fn new(parent: GroupId) -> Self {
Entry::with_id(EntryId::new(), parent)
}
pub(crate) fn with_id(id: EntryId, parent: GroupId) -> Self {
Entry {
id,
parent,
fields: HashMap::new(),
autotype: None,
tags: Vec::new(),
times: Times::new(),
custom_data: HashMap::new(),
icon: None,
foreground_color: None,
background_color: None,
override_url: None,
quality_check: None,
attachments: HashMap::new(),
history: Some(History::default()),
}
}
pub fn id(&self) -> EntryId {
self.id
}
pub fn icon(&self) -> Option<&Icon> {
self.icon.as_ref()
}
pub fn get(&self, key: &str) -> Option<&str> {
self.fields.get(key).map(|v| v.as_str())
}
pub fn set(&mut self, key: impl Into<String>, value: Value<String>) {
self.fields.insert(key.into(), value);
}
pub fn set_unprotected(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.set(key, Value::unprotected(value));
}
pub fn set_protected(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.set(key, Value::protected(value));
}
pub fn get_raw_otp_value(&self) -> Option<&str> {
self.get(fields::OTP)
}
pub fn get_title(&self) -> Option<&str> {
self.get(fields::TITLE)
}
pub fn get_username(&self) -> Option<&str> {
self.get(fields::USERNAME)
}
pub fn get_password(&self) -> Option<&str> {
self.get(fields::PASSWORD)
}
pub fn get_url(&self) -> Option<&str> {
self.get(fields::URL)
}
}
impl std::fmt::Display for EntryId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub struct EntryRef<'a> {
database: &'a Database,
id: EntryId,
history_index: Option<usize>,
}
impl EntryRef<'_> {
pub(crate) fn new(database: &Database, id: EntryId) -> EntryRef<'_> {
EntryRef {
database,
id,
history_index: None,
}
}
pub(crate) fn new_historical(
database: &Database,
id: EntryId,
history_index: Option<usize>,
) -> EntryRef<'_> {
EntryRef {
database,
id,
history_index,
}
}
pub fn parent(&self) -> GroupRef<'_> {
#[allow(clippy::unwrap_used, clippy::missing_panics_doc)] self.database.group(self.parent).unwrap()
}
pub fn historical(&self, index: usize) -> Option<EntryRef<'_>> {
if let Some(h) = &self.history {
if index < h.entries.len() {
Some(EntryRef {
database: self.database,
id: self.id,
history_index: Some(index),
})
} else {
None
}
} else {
None
}
}
pub fn database(&self) -> &Database {
self.database
}
pub fn attachment(&self, id: AttachmentId) -> Option<AttachmentRef<'_>> {
self.attachments
.values()
.find(|&attachment_id| *attachment_id == id)
.cloned()
.map(move |attachment_id| AttachmentRef::new(self.database, attachment_id))
}
pub fn attachment_by_name(&self, name: &str) -> Option<AttachmentRef<'_>> {
self.attachments
.get(name)
.cloned()
.map(move |attachment_id| AttachmentRef::new(self.database, attachment_id))
}
pub fn attachments(&self) -> impl Iterator<Item = AttachmentRef<'_>> {
self.attachments
.values()
.cloned()
.map(move |attachment_id| AttachmentRef::new(self.database, attachment_id))
}
pub fn custom_icon(&self) -> Option<CustomIconRef<'_>> {
if let Some(Icon::Custom(custom_icon_id)) = self.icon {
Some(CustomIconRef::new(self.database, custom_icon_id))
} else {
None
}
}
}
impl Deref for EntryRef<'_> {
type Target = Entry;
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
let entry = self.database.entries.get(&self.id).expect("Entry not found");
if let Some(n) = self.history_index {
&entry.history.as_ref().unwrap().entries[n]
} else {
entry
}
}
}
pub struct EntryMut<'a> {
database: &'a mut Database,
id: EntryId,
history_index: Option<usize>,
}
impl EntryMut<'_> {
pub(crate) fn new(database: &mut Database, id: EntryId) -> EntryMut<'_> {
EntryMut {
database,
id,
history_index: None,
}
}
pub(crate) fn new_historical(
database: &mut Database,
id: EntryId,
history_index: Option<usize>,
) -> EntryMut<'_> {
EntryMut {
database,
id,
history_index,
}
}
pub(crate) fn historical(&mut self, index: usize) -> Option<EntryMut<'_>> {
if let Some(h) = &self.history {
if index < h.entries.len() {
Some(EntryMut {
database: self.database,
id: self.id,
history_index: Some(index),
})
} else {
None
}
} else {
None
}
}
pub fn as_ref(&self) -> EntryRef<'_> {
EntryRef {
database: self.database,
id: self.id,
history_index: self.history_index,
}
}
pub fn edit(&mut self, f: impl FnOnce(&mut EntryMut<'_>)) -> &mut Self {
f(self);
self
}
pub fn edit_tracking(&mut self, f: impl FnOnce(&mut EntryTrack<'_>)) -> &mut Self {
{
let mut tracked = self.track_changes();
f(&mut tracked);
}
self
}
pub fn track_changes(&mut self) -> EntryTrack<'_> {
let mut historical: Entry = self.deref().deref().clone();
historical.history = None;
EntryTrack {
database: self.database,
id: self.id,
historical,
}
}
pub fn parent_mut(&mut self) -> GroupMut<'_> {
#[allow(clippy::unwrap_used, clippy::missing_panics_doc)] self.database.group_mut(self.parent).unwrap()
}
pub fn attachment_mut(&mut self, id: AttachmentId) -> Option<AttachmentMut<'_>> {
self.attachments
.values()
.find(|&attachment_id| *attachment_id == id)
.cloned()
.map(move |attachment_id| AttachmentMut::new(self.database, attachment_id))
}
pub fn attachment_by_name_mut(&mut self, name: &str) -> Option<AttachmentMut<'_>> {
self.attachments
.get(name)
.cloned()
.map(move |attachment_id| AttachmentMut::new(self.database, attachment_id))
}
pub fn foreach_attachment_mut<F>(&mut self, mut f: F)
where
F: FnMut(AttachmentMut<'_>),
{
let attachments: Vec<AttachmentId> = self.attachments.values().copied().collect();
for attachment_id in attachments {
f(AttachmentMut::new(self.database, attachment_id));
}
}
pub fn add_attachment(&mut self, name: impl Into<String>, data: Value<Vec<u8>>) -> AttachmentMut<'_> {
let id = AttachmentId::next_free(self.database);
let entries: HashSet<(EntryId, Option<usize>)> = vec![(self.id, None)].into_iter().collect();
self.database
.attachments
.insert(id, Attachment { id, entries, data });
if let Some(old_id) = self.attachments.insert(name.into(), id) {
self.remove_attachment_by_id(old_id);
}
AttachmentMut::new(self.database, id)
}
pub fn remove_attachment_by_name(&mut self, name: &str) {
let id = self.id;
if let Some(attachment_id) = self.attachments.remove(name) {
if let Some(mut attachment) = self.database.attachment_mut(attachment_id) {
attachment.entries.retain(|&(entry_id, _)| entry_id != id);
if attachment.entries.is_empty() {
attachment.remove();
}
}
}
}
pub fn remove_attachment_by_id(&mut self, attachment_id: AttachmentId) {
let id = self.id;
let mut names_to_remove = Vec::new();
for (name, &att_id) in &self.attachments {
if att_id == attachment_id {
names_to_remove.push(name.clone());
}
}
for name in names_to_remove {
self.attachments.remove(&name);
}
if let Some(mut attachment) = self.database.attachment_mut(attachment_id) {
attachment.entries.retain(|&(entry_id, _)| entry_id != id);
if attachment.entries.is_empty() {
attachment.remove();
}
}
}
pub fn set_icon_none(&mut self) {
let id = self.id;
let history_index = self.history_index;
if let Some(Icon::Custom(custom_icon_id)) = self.icon {
if let Some(mut custom_icon) = self.database.custom_icon_mut(custom_icon_id) {
custom_icon.entries.retain(|&(entry_id, entry_history_index)| {
!(entry_id == id && entry_history_index == history_index)
});
}
}
self.icon = None;
}
pub fn set_icon_builtin(&mut self, icon_id: usize) {
self.set_icon_none();
self.icon = Some(Icon::BuiltIn(icon_id));
}
pub fn set_icon_custom(&mut self, custom_icon_id: CustomIconId) -> Result<(), CustomIconNotFoundError> {
self.set_icon_none();
let id = self.id;
let history_index = self.history_index;
let mut custom_icon = self
.database
.custom_icon_mut(custom_icon_id)
.ok_or(CustomIconNotFoundError(custom_icon_id))?;
custom_icon.entries.insert((id, history_index));
self.icon = Some(Icon::Custom(custom_icon_id));
Ok(())
}
pub fn set_icon_custom_new(&mut self, data: Vec<u8>) -> CustomIconMut<'_> {
self.set_icon_none();
let custom_icon_id = CustomIconId::new();
let id = self.id;
let history_index = self.history_index;
self.database.custom_icons.insert(
custom_icon_id,
CustomIcon {
id: custom_icon_id,
entries: vec![(id, history_index)].into_iter().collect(),
groups: HashSet::new(),
data,
},
);
self.icon = Some(Icon::Custom(custom_icon_id));
CustomIconMut::new(self.database, custom_icon_id)
}
pub fn custom_icon_mut(&mut self) -> Option<CustomIconMut<'_>> {
if let Some(Icon::Custom(custom_icon_id)) = self.icon {
Some(CustomIconMut::new(self.database, custom_icon_id))
} else {
None
}
}
pub fn move_to(&mut self, group_id: GroupId) -> Result<(), DestinationGroupNotFoundError> {
if !self.database.groups.contains_key(&group_id) {
return Err(DestinationGroupNotFoundError(group_id));
}
let my_id = self.id;
let mut parent = self.parent_mut();
parent.entries.remove(&my_id);
#[allow(clippy::unwrap_used, clippy::missing_panics_doc)] let mut new_parent = self.database.group_mut(group_id).unwrap();
new_parent.entries.insert(my_id);
self.parent = group_id;
Ok(())
}
pub fn database_mut(&mut self) -> &mut Database {
self.database
}
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn remove(mut self) {
let id = self.id;
self.foreach_attachment_mut(|mut attachment| {
attachment.entries.retain(|&(entry_id, _)| entry_id != id);
if attachment.entries.is_empty() {
attachment.remove();
}
});
let entry = self.database.entries.remove(&self.id).expect("Entry not found");
let mut parent = self
.database
.group_mut(entry.parent)
.expect("Parent group not found");
parent.entries.remove(&self.id);
}
}
#[derive(Error, Debug)]
#[error("Destination group {0} not found")]
pub struct DestinationGroupNotFoundError(pub(crate) GroupId);
impl Deref for EntryMut<'_> {
type Target = Entry;
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
let entry = self.database.entries.get(&self.id).expect("Entry not found");
if let Some(n) = self.history_index {
&entry.history.as_ref().unwrap().entries[n]
} else {
entry
}
}
}
impl DerefMut for EntryMut<'_> {
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref_mut(&mut self) -> &mut Self::Target {
let entry = self.database.entries.get_mut(&self.id).expect("Entry not found");
if let Some(n) = self.history_index {
&mut entry.history.as_mut().unwrap().entries[n]
} else {
entry
}
}
}
#[clippy::has_significant_drop]
pub struct EntryTrack<'a> {
database: &'a mut Database,
id: EntryId,
historical: Entry,
}
impl EntryTrack<'_> {
pub fn as_mut(&mut self) -> EntryMut<'_> {
EntryMut {
database: self.database,
id: self.id,
history_index: None,
}
}
pub fn move_to(&mut self, group_id: GroupId) -> Result<(), DestinationGroupNotFoundError> {
self.as_mut().move_to(group_id)?;
self.times.location_changed = Some(Times::now());
Ok(())
}
pub fn remove(mut self) {
let this = self.as_mut();
this.database
.deleted_objects
.insert(this.id.uuid(), Some(Times::now()));
this.remove();
}
pub fn edit(&mut self, f: impl FnOnce(&mut EntryTrack<'_>)) -> &mut Self {
f(self);
self.times.last_modification = Some(Times::now());
self
}
pub fn set(&mut self, key: impl Into<String>, value: Value<String>) {
let mut this = self.as_mut();
this.set(key, value);
this.times.last_modification = Some(Times::now());
}
pub fn set_protected(&mut self, key: impl Into<String>, value: impl Into<String>) {
let mut this = self.as_mut();
this.set_protected(key, value);
this.times.last_modification = Some(Times::now());
}
pub fn set_unprotected(&mut self, key: impl Into<String>, value: impl Into<String>) {
let mut this = self.as_mut();
this.set_unprotected(key, value);
this.times.last_modification = Some(Times::now());
}
pub fn add_attachment(&mut self, name: impl Into<String>, data: Value<Vec<u8>>) -> AttachmentMut<'_> {
self.times.last_modification = Some(Times::now());
let mut this = self.as_mut();
let id = this.add_attachment(name, data).id;
AttachmentMut::new(self.database, id)
}
}
impl Deref for EntryTrack<'_> {
type Target = Entry;
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
self.database.entries.get(&self.id).expect("Entry not found")
}
}
impl DerefMut for EntryTrack<'_> {
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref_mut(&mut self) -> &mut Self::Target {
self.database.entries.get_mut(&self.id).expect("Entry not found")
}
}
impl Drop for EntryTrack<'_> {
fn drop(&mut self) {
if let Some(entry) = self.database.entries.get_mut(&self.id) {
let parent_id = entry.parent;
let historical = std::mem::replace(&mut self.historical, Entry::new(parent_id));
entry.history.get_or_insert_default().add_entry(historical);
}
}
}
#[cfg(test)]
mod tests {
use crate::{
db::{fields, Value},
Database,
};
#[test]
fn test_entry() {
let mut db = Database::new();
let entry_id = db
.root_mut()
.add_entry()
.edit(|e| {
e.set_unprotected(fields::TITLE, "Entry 1");
e.set(
fields::USERNAME,
crate::db::Value::unprotected("user".to_string()),
);
e.set_protected(fields::PASSWORD, "asdf");
e.set_icon_custom_new(vec![1, 2, 3]);
})
.id();
assert_eq!(db.num_attachments(), 0);
assert_eq!(db.num_entries(), 1);
assert_eq!(
db.entry(entry_id).unwrap().history.clone().unwrap().entries.len(),
0
);
assert_eq!(db.entry(entry_id).unwrap().get(fields::TITLE).unwrap(), "Entry 1");
db.entry_mut(entry_id).unwrap().edit_tracking(|e| {
e.set_unprotected(fields::TITLE, "Modified Entry 1");
e.set(
fields::USERNAME,
crate::db::Value::unprotected(format!("modified_{}", e.get(fields::USERNAME).unwrap())),
);
e.add_attachment("Attachment 1", Value::protected(b"Attachment data".to_vec()));
});
assert_eq!(db.num_attachments(), 1);
assert_eq!(db.num_entries(), 1);
assert_eq!(
db.entry(entry_id).unwrap().history.clone().unwrap().entries.len(),
1
);
assert!(db
.entry(entry_id)
.unwrap()
.attachments
.get("Attachment 1")
.is_some());
assert_eq!(
db.entry(entry_id).unwrap().get(fields::TITLE).unwrap(),
"Modified Entry 1"
);
assert!(db
.entry_mut(entry_id)
.unwrap()
.move_to(crate::db::GroupId::new())
.is_err());
db.entry_mut(entry_id).unwrap().edit(|e| {
let mut att = e.attachment_by_name_mut("Attachment 1").unwrap();
att.data = Value::unprotected(b"Modified attachment data".to_vec());
});
db.entry_mut(entry_id).unwrap().remove();
assert_eq!(db.num_entries(), 0);
assert_eq!(db.num_attachments(), 0);
}
}