Skip to main content

keepass/db/types/
entry.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ops::{Deref, DerefMut},
4};
5
6use thiserror::Error;
7use uuid::Uuid;
8
9use crate::{
10    db::{
11        attachment::{AttachmentMut, AttachmentRef},
12        fields, Attachment, AttachmentId, AutoType, Color, CustomDataItem, CustomIcon, CustomIconId,
13        CustomIconMut, CustomIconNotFoundError, CustomIconRef, GroupId, GroupMut, GroupRef, History, Icon,
14        Times, Value,
15    },
16    Database,
17};
18
19/// Unique identifier for an [Entry]
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
22pub struct EntryId(Uuid);
23
24impl EntryId {
25    /// Generate a new random `EntryId`.
26    pub fn new() -> Self {
27        Self(Uuid::new_v4())
28    }
29
30    /// Build an `EntryId` from an existing [Uuid].
31    ///
32    /// Useful when an entry's identifier needs to be pinned (e.g. test fixtures or migrations).
33    /// Pair with [Group::add_entry_with_id][crate::db::GroupMut::add_entry_with_id] to insert
34    /// an entry under a chosen identifier.
35    pub const fn from_uuid(uuid: Uuid) -> Self {
36        Self(uuid)
37    }
38
39    /// Get the Uuid contained inside
40    pub fn uuid(&self) -> Uuid {
41        self.0
42    }
43}
44
45impl From<Uuid> for EntryId {
46    fn from(uuid: Uuid) -> Self {
47        Self::from_uuid(uuid)
48    }
49}
50
51/// A database entry containing several key-value fields.
52#[derive(Debug, Eq, PartialEq, Clone)]
53#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
54pub struct Entry {
55    pub(crate) id: EntryId,
56    pub(crate) parent: GroupId,
57
58    /// the key-value fields of this entry, such as username and password.
59    ///
60    /// Common field names are available in [crate::db::fields].
61    pub fields: HashMap<String, Value<String>>,
62
63    /// AutoType settings for this entry
64    pub autotype: Option<AutoType>,
65
66    /// tags associated with this entry
67    pub tags: Vec<String>,
68
69    /// timestamps for this entry
70    pub times: Times,
71
72    /// custom data items associated with this entry
73    pub custom_data: HashMap<String, CustomDataItem>,
74
75    pub(crate) icon: Option<Icon>,
76
77    /// foreground color for this entry
78    pub foreground_color: Option<Color>,
79
80    /// background color for this entry
81    pub background_color: Option<Color>,
82
83    /// URL override for this entry
84    pub override_url: Option<String>,
85
86    /// whether to enable password quality check for this entry
87    pub quality_check: bool,
88
89    /// attachments associated with this entry, mapped by attachment name to attachment ID
90    pub(crate) attachments: HashMap<String, AttachmentId>,
91
92    /// Identifier of the group that the Entry was previously contained in
93    pub(crate) previous_parent_group: Option<GroupId>,
94
95    /// history of this entry
96    pub history: Option<History>,
97}
98
99impl Entry {
100    pub(crate) fn new(parent: GroupId) -> Self {
101        Entry::with_id(EntryId::new(), parent)
102    }
103
104    pub(crate) fn with_id(id: EntryId, parent: GroupId) -> Self {
105        Entry {
106            id,
107            parent,
108            fields: HashMap::new(),
109            autotype: None,
110            tags: Vec::new(),
111            times: Times::new(),
112            custom_data: HashMap::new(),
113            icon: None,
114            foreground_color: None,
115            background_color: None,
116            override_url: None,
117            quality_check: true,
118            attachments: HashMap::new(),
119            history: Some(History::default()),
120            previous_parent_group: None,
121        }
122    }
123
124    /// Get the unique identifier for the [Entry]
125    pub fn id(&self) -> EntryId {
126        self.id
127    }
128
129    /// Get the icon of this entry, if it exists
130    pub fn icon(&self) -> Option<&Icon> {
131        self.icon.as_ref()
132    }
133
134    /// Get a field by name, taking care of unprotecting Protected values automatically
135    pub fn get(&self, key: &str) -> Option<&str> {
136        self.fields.get(key).map(|v| v.as_str())
137    }
138
139    /// Set a field's value by name
140    pub fn set(&mut self, key: impl Into<String>, value: Value<String>) {
141        self.fields.insert(key.into(), value);
142    }
143
144    /// Set a field's unprotected value by name
145    pub fn set_unprotected(&mut self, key: impl Into<String>, value: impl Into<String>) {
146        self.set(key, Value::unprotected(value));
147    }
148
149    /// Set a field's protected value by name
150    pub fn set_protected(&mut self, key: impl Into<String>, value: impl Into<String>) {
151        self.set(key, Value::protected(value));
152    }
153
154    /// Convenience method for getting the raw value of the 'otp' field
155    pub fn get_raw_otp_value(&self) -> Option<&str> {
156        self.get(fields::OTP)
157    }
158
159    /// Convenience method for getting the value of the 'Title' field
160    pub fn get_title(&self) -> Option<&str> {
161        self.get(fields::TITLE)
162    }
163
164    /// Convenience method for getting the value of the 'UserName' field
165    pub fn get_username(&self) -> Option<&str> {
166        self.get(fields::USERNAME)
167    }
168
169    /// Convenience method for getting the value of the 'Password' field
170    pub fn get_password(&self) -> Option<&str> {
171        self.get(fields::PASSWORD)
172    }
173
174    /// Convenience method for getting the value of the 'URL' field
175    pub fn get_url(&self) -> Option<&str> {
176        self.get(fields::URL)
177    }
178}
179
180impl std::fmt::Display for EntryId {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        write!(f, "{}", self.0)
183    }
184}
185
186/// An immutable reference to an [Entry]. Implements [Deref] to [&Entry][Entry].
187pub struct EntryRef<'a> {
188    database: &'a Database,
189    id: EntryId,
190    history_index: Option<usize>,
191}
192
193impl EntryRef<'_> {
194    pub(crate) fn new(database: &Database, id: EntryId) -> EntryRef<'_> {
195        EntryRef {
196            database,
197            id,
198            history_index: None,
199        }
200    }
201
202    pub(crate) fn new_historical(
203        database: &Database,
204        id: EntryId,
205        history_index: Option<usize>,
206    ) -> EntryRef<'_> {
207        EntryRef {
208            database,
209            id,
210            history_index,
211        }
212    }
213
214    /// Get a reference to the parent group of this entry.
215    pub fn parent(&self) -> GroupRef<'_> {
216        #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] // parent always exists
217        self.database.group(self.parent).unwrap()
218    }
219
220    /// Get a reference to the previous parent group, if any
221    pub fn previous_parent(&self) -> Option<GroupRef<'_>> {
222        self.previous_parent_group
223            .and_then(|id| self.database().group(id))
224    }
225
226    /// Gets an [EntryRef] to a historical version of the [Entry], if it exists
227    pub fn historical(&self, index: usize) -> Option<EntryRef<'_>> {
228        if let Some(h) = &self.history {
229            if index < h.entries.len() {
230                Some(EntryRef {
231                    database: self.database,
232                    id: self.id,
233                    history_index: Some(index),
234                })
235            } else {
236                None
237            }
238        } else {
239            None
240        }
241    }
242
243    /// Get a reference to the underlying database
244    pub fn database(&self) -> &Database {
245        self.database
246    }
247
248    /// Get a reference to an attachment by id, if it exists.
249    pub fn attachment(&self, id: AttachmentId) -> Option<AttachmentRef<'_>> {
250        self.attachments
251            .values()
252            .find(|&attachment_id| *attachment_id == id)
253            .cloned()
254            .map(move |attachment_id| AttachmentRef::new(self.database, attachment_id))
255    }
256
257    /// Get a reference to an attachment by name, if it exists.
258    pub fn attachment_by_name(&self, name: &str) -> Option<AttachmentRef<'_>> {
259        self.attachments
260            .get(name)
261            .cloned()
262            .map(move |attachment_id| AttachmentRef::new(self.database, attachment_id))
263    }
264
265    /// Get an iterator over the attachments of this entry.
266    pub fn attachments(&self) -> impl Iterator<Item = AttachmentRef<'_>> {
267        self.attachments
268            .values()
269            .cloned()
270            .map(move |attachment_id| AttachmentRef::new(self.database, attachment_id))
271    }
272
273    /// Get an iterator over the (name, attachment) pairs of this entry.
274    ///
275    /// Useful when callers need both the attachment's filename (the key under
276    /// which it is stored on the entry) and its data, since [`AttachmentRef`]
277    /// itself does not expose the per-entry name.
278    pub fn attachments_named(&self) -> impl Iterator<Item = (&str, AttachmentRef<'_>)> {
279        self.attachments.iter().map(move |(name, &attachment_id)| {
280            (name.as_str(), AttachmentRef::new(self.database, attachment_id))
281        })
282    }
283
284    /// Get the custom icon of this entry, if it exists and is a custom icon.
285    pub fn custom_icon(&self) -> Option<CustomIconRef<'_>> {
286        if let Some(Icon::Custom(custom_icon_id)) = self.icon {
287            Some(CustomIconRef::new(self.database, custom_icon_id))
288        } else {
289            None
290        }
291    }
292}
293
294impl Deref for EntryRef<'_> {
295    type Target = Entry;
296
297    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // entry existence is guaranteed
298    fn deref(&self) -> &Self::Target {
299        // UNWRAP safety: EntryRef can only be constructed with a valid EntryId
300        let entry = self.database.entries.get(&self.id).expect("Entry not found");
301
302        if let Some(n) = self.history_index {
303            // UNWRAP safety: history existance checked on EntryRef creation
304            #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
305            &entry.history.as_ref().unwrap().entries[n]
306        } else {
307            entry
308        }
309    }
310}
311
312/// A mutable reference to an [Entry]. Implements [DerefMut] to [&mut Entry][Entry].
313pub struct EntryMut<'a> {
314    database: &'a mut Database,
315    id: EntryId,
316    history_index: Option<usize>,
317}
318
319impl EntryMut<'_> {
320    pub(crate) fn new(database: &mut Database, id: EntryId) -> EntryMut<'_> {
321        EntryMut {
322            database,
323            id,
324            history_index: None,
325        }
326    }
327
328    pub(crate) fn new_historical(
329        database: &mut Database,
330        id: EntryId,
331        history_index: Option<usize>,
332    ) -> EntryMut<'_> {
333        EntryMut {
334            database,
335            id,
336            history_index,
337        }
338    }
339
340    /// Gets an [EntryMut] to a historical version of the [Entry], if it exists
341    pub(crate) fn historical(&mut self, index: usize) -> Option<EntryMut<'_>> {
342        if index < self.history.as_ref()?.entries.len() {
343            Some(EntryMut {
344                database: self.database,
345                id: self.id,
346                history_index: Some(index),
347            })
348        } else {
349            None
350        }
351    }
352
353    /// Get an immutable reference to the entry.
354    pub fn as_ref(&self) -> EntryRef<'_> {
355        EntryRef {
356            database: self.database,
357            id: self.id,
358            history_index: self.history_index,
359        }
360    }
361
362    /// Convenience method to edit the entry in a closure.
363    pub fn edit(&mut self, f: impl FnOnce(&mut EntryMut<'_>)) -> &mut Self {
364        f(self);
365        self
366    }
367
368    /// Convenience method to edit the entry in a closure, tracking changes.
369    pub fn edit_tracking(&mut self, f: impl FnOnce(&mut EntryTrack<'_>)) -> &mut Self {
370        {
371            let mut tracked = self.track_changes();
372            f(&mut tracked);
373        }
374        self
375    }
376
377    /// Convert this mutable reference into a history-tracking variant that will persist the
378    /// current state of the entry into its history when dropped.
379    ///
380    /// NOTE: will always operate on the main Entry, not a historical version of it.
381    pub fn track_changes(&mut self) -> EntryTrack<'_> {
382        let mut historical: Entry = self.deref().deref().clone();
383
384        // Remove history from the historical entry to avoid exponential growth
385        historical.history = None;
386
387        EntryTrack {
388            database: self.database,
389            id: self.id,
390            historical,
391        }
392    }
393
394    /// Get a mutable reference to the parent group of this entry.
395    pub fn parent_mut(&mut self) -> GroupMut<'_> {
396        #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] // parent always exists
397        self.database.group_mut(self.parent).unwrap()
398    }
399
400    /// Get a mutable reference to the previous parent group, if any
401    pub fn previous_parent_mut(&mut self) -> Option<GroupMut<'_>> {
402        self.previous_parent_group
403            .and_then(move |id| self.database_mut().group_mut(id))
404    }
405
406    /// Get a mutable reference to an attachment by id, if it exists.
407    pub fn attachment_mut(&mut self, id: AttachmentId) -> Option<AttachmentMut<'_>> {
408        self.attachments
409            .values()
410            .find(|&attachment_id| *attachment_id == id)
411            .cloned()
412            .map(move |attachment_id| AttachmentMut::new(self.database, attachment_id))
413    }
414
415    /// Get a mutable reference to an attachment by name, if it exists.
416    pub fn attachment_by_name_mut(&mut self, name: &str) -> Option<AttachmentMut<'_>> {
417        self.attachments
418            .get(name)
419            .cloned()
420            .map(move |attachment_id| AttachmentMut::new(self.database, attachment_id))
421    }
422
423    /// Apply a closure to each attachment of this entry, with mutable access.
424    pub fn foreach_attachment_mut<F>(&mut self, mut f: F)
425    where
426        F: FnMut(AttachmentMut<'_>),
427    {
428        let attachments: Vec<AttachmentId> = self.attachments.values().copied().collect();
429        for attachment_id in attachments {
430            f(AttachmentMut::new(self.database, attachment_id));
431        }
432    }
433
434    /// Add an attachment to this entry with the given name and data.
435    pub fn add_attachment(&mut self, name: impl Into<String>, data: Value<Vec<u8>>) -> AttachmentMut<'_> {
436        let id = AttachmentId::next_free(self.database);
437
438        let entries: HashSet<(EntryId, Option<usize>)> = vec![(self.id, None)].into_iter().collect();
439
440        self.database
441            .attachments
442            .insert(id, Attachment { id, entries, data });
443
444        if let Some(old_id) = self.attachments.insert(name.into(), id) {
445            // if there was an old attachment with this name, remove it
446            self.remove_attachment_by_id(old_id);
447        }
448
449        AttachmentMut::new(self.database, id)
450    }
451
452    /// Remove an attachment by name from this entry.
453    ///
454    /// If it was the last reference to the attachment, remove it from the database.
455    pub fn remove_attachment_by_name(&mut self, name: &str) {
456        let id = self.id;
457
458        // remove the attachment reference from this entry
459        if let Some(attachment_id) = self.attachments.remove(name) {
460            if let Some(mut attachment) = self.database.attachment_mut(attachment_id) {
461                attachment.entries.retain(|&(entry_id, _)| entry_id != id);
462
463                // if this was the last entry referencing the attachment, remove it from the database
464                if attachment.entries.is_empty() {
465                    attachment.remove();
466                }
467            }
468        }
469    }
470
471    /// Remove an attachment by id from this entry.
472    ///
473    /// If it was the last reference to the attachment, remove it from the database.
474    pub fn remove_attachment_by_id(&mut self, attachment_id: AttachmentId) {
475        let id = self.id;
476
477        // remove the attachment reference from this entry
478        let mut names_to_remove = Vec::new();
479        for (name, &att_id) in &self.attachments {
480            if att_id == attachment_id {
481                names_to_remove.push(name.clone());
482            }
483        }
484
485        for name in names_to_remove {
486            self.attachments.remove(&name);
487        }
488
489        if let Some(mut attachment) = self.database.attachment_mut(attachment_id) {
490            attachment.entries.retain(|&(entry_id, _)| entry_id != id);
491
492            // if this was the last entry referencing the attachment, remove it from the database
493            if attachment.entries.is_empty() {
494                attachment.remove();
495            }
496        }
497    }
498
499    /// Remove the icon from this entry, if it exists.
500    pub fn set_icon_none(&mut self) {
501        let id = self.id;
502        let history_index = self.history_index;
503
504        if let Some(Icon::Custom(custom_icon_id)) = self.icon {
505            // if this entry had a custom icon, remove this entry from the icon's reference list
506            if let Some(mut custom_icon) = self.database.custom_icon_mut(custom_icon_id) {
507                custom_icon.entries.retain(|&(entry_id, entry_history_index)| {
508                    !(entry_id == id && entry_history_index == history_index)
509                });
510            }
511        }
512
513        self.icon = None;
514    }
515
516    /// Set a built-in icon for this entry by its ID, removing any existing icon.
517    pub fn set_icon_builtin(&mut self, icon_id: usize) {
518        self.set_icon_none();
519        self.icon = Some(Icon::BuiltIn(icon_id));
520    }
521
522    /// Set a custom icon for this entry by its ID, removing any existing icon.
523    pub fn set_icon_custom(&mut self, custom_icon_id: CustomIconId) -> Result<(), CustomIconNotFoundError> {
524        self.set_icon_none();
525
526        let id = self.id;
527        let history_index = self.history_index;
528
529        let mut custom_icon = self
530            .database
531            .custom_icon_mut(custom_icon_id)
532            .ok_or(CustomIconNotFoundError(custom_icon_id))?;
533
534        custom_icon.entries.insert((id, history_index));
535
536        self.icon = Some(Icon::Custom(custom_icon_id));
537
538        Ok(())
539    }
540
541    /// Set a custom icon for this entry by providing the raw data, removing any existing icon.
542    /// Returns a mutable reference to the newly created custom icon.
543    pub fn set_icon_custom_new(&mut self, data: Vec<u8>) -> CustomIconMut<'_> {
544        self.set_icon_none();
545
546        let custom_icon_id = CustomIconId::new();
547
548        let id = self.id;
549        let history_index = self.history_index;
550
551        self.database.custom_icons.insert(
552            custom_icon_id,
553            CustomIcon {
554                id: custom_icon_id,
555                entries: vec![(id, history_index)].into_iter().collect(),
556                groups: HashSet::new(),
557                name: None,
558                last_modification_time: Some(Times::now()),
559                data,
560            },
561        );
562
563        self.icon = Some(Icon::Custom(custom_icon_id));
564
565        CustomIconMut::new(self.database, custom_icon_id)
566    }
567
568    /// Get a mutable reference to the custom icon of this entry, if it exists and is a custom
569    /// icon.
570    pub fn custom_icon_mut(&mut self) -> Option<CustomIconMut<'_>> {
571        if let Some(Icon::Custom(custom_icon_id)) = self.icon {
572            Some(CustomIconMut::new(self.database, custom_icon_id))
573        } else {
574            None
575        }
576    }
577
578    /// Move this entry to another group.
579    ///
580    /// NOTE: will always operate on the main Entry, not a historical version of it.
581    pub fn move_to(&mut self, group_id: GroupId) -> Result<(), DestinationGroupNotFoundError> {
582        if !self.database.groups.contains_key(&group_id) {
583            return Err(DestinationGroupNotFoundError(group_id));
584        }
585
586        let my_id = self.id;
587        let previous_parent = self.parent;
588
589        let mut parent = self.parent_mut();
590        parent.entries.shift_remove(&my_id);
591
592        #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] // group existence is checked
593        let mut new_parent = self.database.group_mut(group_id).unwrap();
594        new_parent.entries.insert(my_id);
595        self.parent = group_id;
596        self.previous_parent_group = Some(previous_parent);
597
598        Ok(())
599    }
600
601    /// Get a mutable reference to the underlying database
602    pub fn database_mut(&mut self) -> &mut Database {
603        self.database
604    }
605
606    /// Remove this entry from the database, including all its attachments.
607    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // the entry and parent should always be found
608    pub fn remove(mut self) {
609        let id = self.id;
610
611        // remove this entry's back-reference from its custom icon (if any)
612        self.set_icon_none();
613
614        // also remove back-references for any historical versions that have a custom icon
615        let history_len = self.history.as_ref().map_or(0, |h| h.entries.len());
616        for i in 0..history_len {
617            if let Some(mut hist_entry) = self.historical(i) {
618                hist_entry.set_icon_none();
619            }
620        }
621
622        // remove references to this entry from attachments
623        self.foreach_attachment_mut(|mut attachment| {
624            attachment.entries.retain(|&(entry_id, _)| entry_id != id);
625
626            // if this was the last entry referencing the attachment, remove it from the database
627            if attachment.entries.is_empty() {
628                attachment.remove();
629            }
630        });
631
632        let entry = self.database.entries.remove(&self.id).expect("Entry not found");
633
634        // Remove from parent group
635        let mut parent = self
636            .database
637            .group_mut(entry.parent)
638            .expect("Parent group not found");
639        parent.entries.shift_remove(&self.id);
640
641        // Clear any group's last_top_visible_entry that pointed to this entry.
642        // This field is a UI hint and should not hold a dangling EntryId.
643        let group_ids: Vec<GroupId> = self.database.groups.keys().copied().collect();
644        for group_id in group_ids {
645            if let Some(group) = self.database.groups.get_mut(&group_id) {
646                if group.last_top_visible_entry == Some(id) {
647                    group.last_top_visible_entry = None;
648                }
649            }
650        }
651    }
652}
653
654/// Error type for when a destination [GroupId] is provided that does not exist in the database
655#[derive(Error, Debug)]
656#[error("Destination group {0} not found")]
657pub struct DestinationGroupNotFoundError(pub(crate) GroupId);
658
659impl Deref for EntryMut<'_> {
660    type Target = Entry;
661
662    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // entry existence is guaranteed
663    fn deref(&self) -> &Self::Target {
664        // UNWRAP safety: EntryMut can only be constructed with a valid EntryId
665        let entry = self.database.entries.get(&self.id).expect("Entry not found");
666
667        if let Some(n) = self.history_index {
668            // UNWRAP safety: history existence checked on EntryMut creation
669            #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
670            &entry.history.as_ref().unwrap().entries[n]
671        } else {
672            entry
673        }
674    }
675}
676
677impl DerefMut for EntryMut<'_> {
678    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // entry existence is guaranteed
679    fn deref_mut(&mut self) -> &mut Self::Target {
680        // UNWRAP safety: EntryMut can only be constructed with a valid EntryId
681        let entry = self.database.entries.get_mut(&self.id).expect("Entry not found");
682
683        if let Some(n) = self.history_index {
684            // UNWRAP safety: history existence checked on EntryMut creation
685            #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
686            &mut entry.history.as_mut().unwrap().entries[n]
687        } else {
688            entry
689        }
690    }
691}
692
693/// A variant of [EntryMut] that will persist the history of the entry when dropped.
694#[clippy::has_significant_drop]
695pub struct EntryTrack<'a> {
696    database: &'a mut Database,
697    id: EntryId,
698
699    historical: Entry,
700}
701
702impl EntryTrack<'_> {
703    /// Turn this tracked entry into a normal mutable reference to the entry
704    pub fn as_mut(&mut self) -> EntryMut<'_> {
705        EntryMut {
706            database: self.database,
707            id: self.id,
708            history_index: None,
709        }
710    }
711
712    /// Move this entry to another group, tracking the change in history.
713    pub fn move_to(&mut self, group_id: GroupId) -> Result<(), DestinationGroupNotFoundError> {
714        self.as_mut().move_to(group_id)?;
715        self.times.location_changed = Some(Times::now());
716        Ok(())
717    }
718
719    /// Remove this entry from the database, tracking the change in history.
720    pub fn remove(mut self) {
721        let this = self.as_mut();
722        this.database
723            .deleted_objects
724            .insert(this.id.uuid(), Some(Times::now()));
725
726        // use EntryMut::remove to handle actual removal
727        this.remove();
728    }
729
730    /// Convenience method to edit the entry in a closure, tracking changes.
731    pub fn edit(&mut self, f: impl FnOnce(&mut EntryTrack<'_>)) -> &mut Self {
732        f(self);
733        self.times.last_modification = Some(Times::now());
734        self
735    }
736
737    /// Set a field value, tracking changes. See [crate::db::fields] for common field names.
738    pub fn set(&mut self, key: impl Into<String>, value: Value<String>) {
739        let mut this = self.as_mut();
740        this.set(key, value);
741        this.times.last_modification = Some(Times::now());
742    }
743
744    /// Set a protected field value, tracking changes. See [crate::db::fields] for common field names.
745    pub fn set_protected(&mut self, key: impl Into<String>, value: impl Into<String>) {
746        let mut this = self.as_mut();
747        this.set_protected(key, value);
748        this.times.last_modification = Some(Times::now());
749    }
750
751    /// Set an unprotected field value, tracking changes. See [crate::db::fields] for common field names.
752    pub fn set_unprotected(&mut self, key: impl Into<String>, value: impl Into<String>) {
753        let mut this = self.as_mut();
754        this.set_unprotected(key, value);
755        this.times.last_modification = Some(Times::now());
756    }
757
758    /// Add an attachment, tracking changes.
759    pub fn add_attachment(&mut self, name: impl Into<String>, data: Value<Vec<u8>>) -> AttachmentMut<'_> {
760        self.times.last_modification = Some(Times::now());
761        let mut this = self.as_mut();
762        let id = this.add_attachment(name, data).id;
763
764        AttachmentMut::new(self.database, id)
765    }
766
767    /// Remove the entry's icon, tracking changes.
768    pub fn set_icon_none(&mut self) {
769        let mut this = self.as_mut();
770        this.set_icon_none();
771        this.times.last_modification = Some(Times::now());
772    }
773
774    /// Set a built-in icon for this entry by its ID, tracking changes.
775    pub fn set_icon_builtin(&mut self, icon_id: usize) {
776        let mut this = self.as_mut();
777        this.set_icon_builtin(icon_id);
778        this.times.last_modification = Some(Times::now());
779    }
780
781    /// Set a custom icon for this entry by its ID, tracking changes.
782    pub fn set_icon_custom(&mut self, custom_icon_id: CustomIconId) -> Result<(), CustomIconNotFoundError> {
783        let mut this = self.as_mut();
784        this.set_icon_custom(custom_icon_id)?;
785        this.times.last_modification = Some(Times::now());
786        Ok(())
787    }
788
789    /// Set a custom icon for this entry by providing the raw data, tracking changes. Returns a mutable reference to the newly created custom icon.
790    pub fn set_icon_custom_new(&mut self, data: Vec<u8>) -> CustomIconMut<'_> {
791        self.set_icon_none();
792
793        let custom_icon_id = CustomIconId::new();
794
795        let id = self.id;
796        let history_index = self.as_mut().history_index;
797
798        self.database.custom_icons.insert(
799            custom_icon_id,
800            CustomIcon {
801                id: custom_icon_id,
802                entries: vec![(id, history_index)].into_iter().collect(),
803                groups: HashSet::new(),
804                name: None,
805                last_modification_time: Some(Times::now()),
806                data,
807            },
808        );
809
810        self.icon = Some(Icon::Custom(custom_icon_id));
811
812        CustomIconMut::new(self.database, custom_icon_id)
813    }
814}
815
816impl Deref for EntryTrack<'_> {
817    type Target = Entry;
818
819    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // entry existence is guaranteed
820    fn deref(&self) -> &Self::Target {
821        self.database.entries.get(&self.id).expect("Entry not found")
822    }
823}
824
825impl DerefMut for EntryTrack<'_> {
826    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // entry existence is guaranteed
827    fn deref_mut(&mut self) -> &mut Self::Target {
828        self.database.entries.get_mut(&self.id).expect("Entry not found")
829    }
830}
831
832impl Drop for EntryTrack<'_> {
833    fn drop(&mut self) {
834        // see if the entry is still there (it might have been removed)
835        if let Some(entry) = self.database.entries.get_mut(&self.id) {
836            let parent_id = entry.parent;
837            let historical = std::mem::replace(&mut self.historical, Entry::new(parent_id));
838
839            entry.history.get_or_insert_default().add_entry(historical);
840        }
841    }
842}
843
844#[cfg(test)]
845#[allow(clippy::unwrap_used)]
846mod tests {
847
848    use super::EntryId;
849    use crate::{
850        db::{fields, Value},
851        Database,
852    };
853
854    #[test]
855    fn entry_id_new_generates_distinct_ids() {
856        assert_ne!(EntryId::new(), EntryId::new());
857    }
858
859    #[test]
860    fn test_entry() {
861        let mut db = Database::new();
862
863        let entry_id = db
864            .root_mut()
865            .add_entry()
866            .edit(|e| {
867                e.set_unprotected(fields::TITLE, "Entry 1");
868                e.set(
869                    fields::USERNAME,
870                    crate::db::Value::unprotected("user".to_string()),
871                );
872                e.set_protected(fields::PASSWORD, "asdf");
873
874                e.set_icon_custom_new(vec![1, 2, 3]);
875            })
876            .id();
877
878        assert_eq!(db.num_attachments(), 0);
879        assert_eq!(db.num_entries(), 1);
880
881        assert_eq!(
882            db.entry(entry_id).unwrap().history.clone().unwrap().entries.len(),
883            0
884        );
885
886        assert_eq!(db.entry(entry_id).unwrap().get(fields::TITLE).unwrap(), "Entry 1");
887
888        db.entry_mut(entry_id).unwrap().edit_tracking(|e| {
889            e.set_unprotected(fields::TITLE, "Modified Entry 1");
890            e.set(
891                fields::USERNAME,
892                crate::db::Value::unprotected(format!("modified_{}", e.get(fields::USERNAME).unwrap())),
893            );
894
895            e.add_attachment("Attachment 1", Value::protected(b"Attachment data".to_vec()));
896        });
897
898        assert_eq!(db.num_attachments(), 1);
899        assert_eq!(db.num_entries(), 1);
900        assert_eq!(
901            db.entry(entry_id).unwrap().history.clone().unwrap().entries.len(),
902            1
903        );
904
905        assert!(db
906            .entry(entry_id)
907            .unwrap()
908            .attachments
909            .contains_key("Attachment 1"));
910
911        assert_eq!(
912            db.entry(entry_id).unwrap().get(fields::TITLE).unwrap(),
913            "Modified Entry 1"
914        );
915
916        // test moving to a non-existent group returns an error and does not modify the entry
917        assert!(db
918            .entry_mut(entry_id)
919            .unwrap()
920            .move_to(crate::db::GroupId::new())
921            .is_err());
922
923        db.entry_mut(entry_id).unwrap().edit(|e| {
924            let mut att = e.attachment_by_name_mut("Attachment 1").unwrap();
925
926            att.data = Value::unprotected(b"Modified attachment data".to_vec());
927        });
928
929        db.entry_mut(entry_id).unwrap().remove();
930
931        assert_eq!(db.num_entries(), 0);
932        assert_eq!(db.num_attachments(), 0);
933    }
934}