Skip to main content

keepass/db/types/
group.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ops::{Deref, DerefMut},
4};
5
6use indexmap::IndexSet;
7use thiserror::Error;
8use uuid::Uuid;
9
10use crate::{
11    db::{
12        CustomDataItem, CustomIcon, CustomIconId, CustomIconMut, CustomIconNotFoundError, CustomIconRef, Entry,
13        EntryId, EntryMut, EntryRef, Icon, Times,
14    },
15    Database,
16};
17
18/// Unique identifier for a [Group]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
21pub struct GroupId(Uuid);
22
23impl GroupId {
24    /// Generate a new random `GroupId`.
25    pub fn new() -> Self {
26        Self(Uuid::new_v4())
27    }
28
29    /// Build a `GroupId` from an existing [Uuid].
30    ///
31    /// Useful when a group's identifier needs to be pinned (e.g. test fixtures or migrations).
32    /// Pair with [Group::add_group_with_id][GroupMut::add_group_with_id] to insert
33    /// a group under a chosen identifier.
34    pub const fn from_uuid(uuid: Uuid) -> Self {
35        Self(uuid)
36    }
37
38    /// Get the Uuid contained inside
39    pub fn uuid(&self) -> Uuid {
40        self.0
41    }
42}
43
44impl From<Uuid> for GroupId {
45    fn from(uuid: Uuid) -> Self {
46        Self::from_uuid(uuid)
47    }
48}
49
50impl std::fmt::Display for GroupId {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{}", self.0)
53    }
54}
55
56/// A database group with child groups and entries
57#[derive(Debug, Eq, PartialEq, Clone)]
58#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
59pub struct Group {
60    /// The unique identifier of the group
61    pub(crate) id: GroupId,
62
63    /// The unique identifier for the parent group
64    pub(crate) parent: Option<GroupId>,
65
66    /// The name of the group
67    pub name: String,
68
69    /// Notes for the group
70    pub notes: Option<String>,
71
72    /// The list of tags for this group
73    pub tags: Vec<String>,
74
75    /// Icon for the group
76    pub(crate) icon: Option<Icon>,
77
78    /// The list of child group identifiers
79    pub(crate) groups: IndexSet<GroupId>,
80
81    /// The list of entry identifiers directly under this group
82    pub(crate) entries: IndexSet<EntryId>,
83
84    /// The list of time fields for this group
85    pub times: Times,
86
87    /// Custom Data
88    pub custom_data: HashMap<String, CustomDataItem>,
89
90    /// Whether the group is expanded in the user interface
91    pub is_expanded: bool,
92
93    /// Default autotype sequence
94    pub default_autotype_sequence: Option<String>,
95
96    /// Whether autotype is enabled
97    pub enable_autotype: Option<bool>,
98
99    /// Whether searching is enabled
100    pub enable_searching: Option<bool>,
101
102    /// UUID for the last top visible entry
103    pub(crate) last_top_visible_entry: Option<EntryId>,
104
105    pub(crate) previous_parent_group: Option<GroupId>,
106}
107
108impl Group {
109    /// Get the unique identifier for this group
110    pub fn id(&self) -> GroupId {
111        self.id
112    }
113
114    pub(crate) fn new(parent: Option<GroupId>) -> Group {
115        Group {
116            id: GroupId::new(),
117            parent,
118            name: String::new(),
119            notes: None,
120            tags: Vec::new(),
121            icon: None,
122            groups: IndexSet::new(),
123            entries: IndexSet::new(),
124            times: Times::new(),
125            custom_data: HashMap::new(),
126            is_expanded: true,
127            default_autotype_sequence: None,
128            enable_autotype: None,
129            enable_searching: None,
130            last_top_visible_entry: None,
131            previous_parent_group: None,
132        }
133    }
134
135    pub(crate) fn with_id(id: GroupId, parent: Option<GroupId>) -> Group {
136        Group {
137            id,
138            parent,
139            name: String::new(),
140            notes: None,
141            tags: Vec::new(),
142            icon: None,
143            groups: IndexSet::new(),
144            entries: IndexSet::new(),
145            times: Times::new(),
146            custom_data: HashMap::new(),
147            is_expanded: true,
148            default_autotype_sequence: None,
149            enable_autotype: None,
150            enable_searching: None,
151            last_top_visible_entry: None,
152            previous_parent_group: None,
153        }
154    }
155
156    /// Get an iterator over the IDs of all contained groups
157    pub fn group_ids(&self) -> impl Iterator<Item = GroupId> + '_ {
158        self.groups.iter().cloned()
159    }
160
161    /// Get an iterator over the IDs of all contained entries
162    pub fn entry_ids(&self) -> impl Iterator<Item = EntryId> + '_ {
163        self.entries.iter().cloned()
164    }
165
166    /// Get a reference to the icon of this group, if any
167    pub fn icon(&self) -> Option<&Icon> {
168        self.icon.as_ref()
169    }
170}
171
172/// Immutable reference to a [Group]. Implements [Deref] to [&Group][Group].
173#[derive(Clone)]
174pub struct GroupRef<'a> {
175    database: &'a crate::db::Database,
176    id: GroupId,
177}
178
179impl GroupRef<'_> {
180    pub(crate) fn new(database: &Database, id: GroupId) -> GroupRef<'_> {
181        GroupRef { database, id }
182    }
183
184    /// Get a contained group by ID
185    pub fn group(&self, id: GroupId) -> Option<GroupRef<'_>> {
186        self.groups
187            .contains(&id)
188            .then(move || GroupRef::new(self.database, id))
189    }
190
191    /// Get a contained entry by ID
192    pub fn entry(&self, id: EntryId) -> Option<EntryRef<'_>> {
193        self.entries
194            .contains(&id)
195            .then(move || EntryRef::new(self.database, id))
196    }
197
198    /// Get an iterator over all contained groups
199    pub fn groups(&self) -> impl Iterator<Item = GroupRef<'_>> + '_ {
200        self.groups
201            .iter()
202            .map(move |id| GroupRef::new(self.database, *id))
203    }
204
205    /// Get an iterator over all contained entries
206    pub fn entries(&self) -> impl Iterator<Item = EntryRef<'_>> + '_ {
207        self.entries
208            .iter()
209            .map(move |id| EntryRef::new(self.database, *id))
210    }
211
212    /// Find a contained group by name, case-insensitively.
213    pub fn group_by_name(&self, name: &str) -> Option<GroupRef<'_>> {
214        self.groups().find(|g| g.name.eq_ignore_ascii_case(name))
215    }
216
217    /// Find a contained entry by title, case-insensitively.
218    pub fn entry_by_name(&self, title: &str) -> Option<EntryRef<'_>> {
219        self.entries().find(|e| {
220            e.get(crate::db::fields::TITLE)
221                .is_some_and(|t| t.eq_ignore_ascii_case(title))
222        })
223    }
224
225    /// Find a contained group by a path of names, case-insensitively.
226    pub fn group_by_path(&self, path: &[&str]) -> Option<GroupRef<'_>> {
227        let mut current = self.id;
228
229        for part in path {
230            current = self
231                .database
232                .groups
233                .get(&current)?
234                .groups
235                .iter()
236                .filter_map(|id| self.database.groups.get(id))
237                .find(|g| g.name.eq_ignore_ascii_case(part))?
238                .id;
239        }
240
241        Some(GroupRef::new(self.database, current))
242    }
243
244    /// Get the database this group belongs to
245    pub fn database(&self) -> &Database {
246        self.database
247    }
248
249    /// Get a reference to the parent group, if any
250    pub fn parent(&self) -> Option<GroupRef<'_>> {
251        self.parent.map(|id| GroupRef::new(self.database, id))
252    }
253
254    /// Get a reference to the previous parent group, if any
255    pub fn previous_parent(&self) -> Option<GroupRef<'_>> {
256        self.previous_parent_group
257            .and_then(|id| self.database().group(id))
258    }
259
260    /// Get a reference to the custom icon of this group, if it has one and it is a custom icon
261    pub fn custom_icon(&self) -> Option<CustomIconRef<'_>> {
262        if let Some(Icon::Custom(cid)) = self.icon {
263            Some(CustomIconRef::new(self.database, cid))
264        } else {
265            None
266        }
267    }
268}
269
270impl Deref for GroupRef<'_> {
271    type Target = Group;
272
273    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // group existence is guaranteed
274    fn deref(&self) -> &Self::Target {
275        self.database
276            .groups
277            .get(&self.id)
278            .expect("GroupRef points to a non-existing group")
279    }
280}
281
282/// Mutable reference to a [Group]. Implements [DerefMut] to [&mut Group][Group]
283pub struct GroupMut<'a> {
284    database: &'a mut crate::db::Database,
285    id: GroupId,
286}
287
288impl GroupMut<'_> {
289    pub(crate) fn new(database: &mut Database, id: GroupId) -> GroupMut<'_> {
290        GroupMut { database, id }
291    }
292
293    /// Get an immutable reference to this group
294    pub fn as_ref(&self) -> GroupRef<'_> {
295        GroupRef::new(self.database, self.id)
296    }
297
298    /// Get a mutable reference to a contained group by ID
299    pub fn group_mut(&mut self, id: GroupId) -> Option<GroupMut<'_>> {
300        self.groups
301            .contains(&id)
302            .then(move || GroupMut::new(self.database, id))
303    }
304
305    /// Get a mutable reference to a contained entry by ID
306    pub fn entry_mut(&mut self, id: EntryId) -> Option<EntryMut<'_>> {
307        self.entries
308            .contains(&id)
309            .then(move || EntryMut::new(self.database, id))
310    }
311
312    /// Convenience method to edit the group in a closure.
313    pub fn edit(&mut self, f: impl FnOnce(&mut GroupMut<'_>)) -> &mut Self {
314        f(self);
315        self
316    }
317
318    /// Convenience method to edit the group in a closure that tracks changes.
319    pub fn edit_tracking(&mut self, f: impl FnOnce(&mut GroupTrack<'_>)) -> &mut Self {
320        let mut tracker = self.track_changes();
321        f(&mut tracker);
322        tracker.as_mut().times.last_modification = Some(Times::now());
323        self
324    }
325
326    /// Adds a new subgroup to this group and returns a mutable reference to it.
327    #[allow(clippy::missing_panics_doc)]
328    pub fn add_group(&mut self) -> GroupMut<'_> {
329        let new_group = Group::new(Some(self.id));
330        let id = new_group.id;
331
332        // A freshly-generated v4 UUID does not collide with an existing identifier in any
333        // realistic scenario, so the duplicate check inside `add_group_with_id` cannot trip
334        // here.
335        #[allow(clippy::expect_used)] // fresh v4 UUID cannot collide
336        self.add_group_with_id(id)
337            .expect("fresh v4 UUID cannot collide with an existing group identifier")
338    }
339
340    /// Adds a new entry to this group and returns a mutable reference to it.
341    #[allow(clippy::missing_panics_doc)]
342    pub fn add_entry(&mut self) -> EntryMut<'_> {
343        let new_entry = Entry::new(self.id);
344        let id = new_entry.id();
345
346        // A freshly-generated v4 UUID does not collide with an existing identifier in any
347        // realistic scenario, so the duplicate check inside `add_entry_with_id` cannot trip
348        // here.
349        #[allow(clippy::expect_used)] // fresh v4 UUID cannot collide
350        self.add_entry_with_id(id)
351            .expect("fresh v4 UUID cannot collide with an existing entry identifier")
352    }
353
354    /// Adds a new entry under a caller-supplied [EntryId] and returns a mutable reference to it.
355    ///
356    /// Returns [DuplicateEntryIdError] if an entry with the same identifier already
357    /// exists anywhere in the database.
358    ///
359    /// # Example
360    ///
361    /// ```
362    /// use keepass::db::{Database, EntryId, fields};
363    /// use uuid::uuid;
364    ///
365    /// let mut db = Database::new();
366    /// let entry_id: EntryId = uuid!("00000000-0000-0000-0000-000000000001").into();
367    /// db.root_mut()
368    ///     .add_entry_with_id(entry_id)
369    ///     .unwrap()
370    ///     .edit(|e| {
371    ///         e.set_unprotected(fields::TITLE, "My entry with defined UUID");
372    ///     });
373    /// ```
374    pub fn add_entry_with_id(&mut self, id: EntryId) -> Result<EntryMut<'_>, DuplicateEntryIdError> {
375        if self.database.entries.contains_key(&id) {
376            return Err(DuplicateEntryIdError(id));
377        }
378
379        let new_entry = Entry::with_id(id, self.id);
380        self.entries.insert(id);
381        self.database.entries.insert(id, new_entry);
382
383        Ok(EntryMut::new(self.database, id))
384    }
385
386    /// Adds a new subgroup under a caller-supplied [GroupId] and returns a mutable reference to
387    /// it.
388    ///
389    /// Returns [DuplicateGroupIdError] if a group with the same identifier already
390    /// exists anywhere in the database.
391    ///
392    /// # Example
393    ///
394    /// ```
395    /// use keepass::db::{Database, GroupId};
396    /// use uuid::uuid;
397    ///
398    /// let mut db = Database::new();
399    /// let group_id: GroupId = uuid!("00000000-0000-0000-0000-000000000002").into();
400    /// db.root_mut()
401    ///     .add_group_with_id(group_id)
402    ///     .unwrap()
403    ///     .edit(|g| g.name = "Pinned group".to_string());
404    /// ```
405    pub fn add_group_with_id(&mut self, id: GroupId) -> Result<GroupMut<'_>, DuplicateGroupIdError> {
406        if self.database.groups.contains_key(&id) {
407            return Err(DuplicateGroupIdError(id));
408        }
409
410        let new_group = Group::with_id(id, Some(self.id));
411        self.groups.insert(id);
412        self.database.groups.insert(id, new_group);
413
414        Ok(GroupMut::new(self.database, id))
415    }
416
417    /// Get a mutable reference to the database this group belongs to
418    pub fn database_mut(&mut self) -> &mut Database {
419        self.database
420    }
421
422    /// Get a mutable reference to the parent group, if any
423    pub fn parent_mut(&mut self) -> Option<GroupMut<'_>> {
424        self.parent.map(move |id| GroupMut::new(self.database, id))
425    }
426
427    /// Get a mutable reference to the previous parent group, if any
428    pub fn previous_parent_mut(&mut self) -> Option<GroupMut<'_>> {
429        self.previous_parent_group
430            .and_then(move |id| self.database_mut().group_mut(id))
431    }
432
433    /// Find a contained group by name, case-insensitively, and return a mutable reference to it.
434    pub fn group_by_name_mut(&mut self, name: &str) -> Option<GroupMut<'_>> {
435        let gid = self.as_ref().groups().find_map(|g| {
436            if g.name.eq_ignore_ascii_case(name) {
437                Some(g.id)
438            } else {
439                None
440            }
441        });
442
443        gid.map(move |id| GroupMut::new(self.database, id))
444    }
445
446    /// Find a contained entry by title, case-insensitively, and return a mutable reference to it.
447    pub fn entry_by_name_mut(&mut self, title: &str) -> Option<EntryMut<'_>> {
448        let eid = self.as_ref().entries().find_map(|e| {
449            e.get(crate::db::fields::TITLE)
450                .is_some_and(|t| t.eq_ignore_ascii_case(title))
451                .then(|| e.id())
452        });
453
454        eid.map(move |id| EntryMut::new(self.database, id))
455    }
456
457    /// Find a contained group by a path of names, case-insensitively, and return a mutable
458    /// reference to it.
459    pub fn group_by_path_mut(&mut self, path: &[&str]) -> Option<GroupMut<'_>> {
460        let mut current = self.id;
461
462        for part in path {
463            current = self
464                .database
465                .groups
466                .get(&current)?
467                .groups
468                .iter()
469                .filter_map(|id| self.database.groups.get(id))
470                .find(|g| g.name.eq_ignore_ascii_case(part))?
471                .id;
472        }
473
474        Some(GroupMut::new(self.database, current))
475    }
476
477    /// Remove the icon from this group, if it exists.
478    pub fn set_icon_none(&mut self) {
479        let id = self.id;
480
481        if let Some(Icon::Custom(custom_icon_id)) = self.icon {
482            // if this group had a custom icon, remove this group from the icon's reference list
483            if let Some(mut custom_icon) = self.database.custom_icon_mut(custom_icon_id) {
484                custom_icon.groups.retain(|&group_id| group_id != id);
485            }
486        }
487
488        self.icon = None;
489    }
490
491    /// Set a built-in icon for this group by its ID, removing any existing icon.
492    pub fn set_icon_builtin(&mut self, icon_id: usize) {
493        self.set_icon_none();
494        self.icon = Some(Icon::BuiltIn(icon_id));
495    }
496
497    /// Set a custom icon for this group by its ID, removing any existing icon.
498    pub fn set_icon_custom(&mut self, custom_icon_id: CustomIconId) -> Result<(), CustomIconNotFoundError> {
499        self.set_icon_none();
500
501        let id = self.id;
502
503        let mut custom_icon = self
504            .database
505            .custom_icon_mut(custom_icon_id)
506            .ok_or(CustomIconNotFoundError(custom_icon_id))?;
507
508        custom_icon.groups.insert(id);
509
510        self.icon = Some(Icon::Custom(custom_icon_id));
511
512        Ok(())
513    }
514
515    /// Set a custom icon for this group by providing the raw data, removing any existing icon.
516    /// Returns a mutable reference to the newly created custom icon.
517    pub fn set_icon_custom_new(&mut self, data: Vec<u8>) -> CustomIconMut<'_> {
518        self.set_icon_none();
519
520        let custom_icon_id = CustomIconId::new();
521
522        let id = self.id;
523
524        self.database.custom_icons.insert(
525            custom_icon_id,
526            CustomIcon {
527                id: custom_icon_id,
528                entries: HashSet::new(),
529                groups: vec![id].into_iter().collect(),
530                name: None,
531                last_modification_time: Some(Times::now()),
532                data,
533            },
534        );
535
536        self.icon = Some(Icon::Custom(custom_icon_id));
537
538        CustomIconMut::new(self.database, custom_icon_id)
539    }
540
541    /// Get a mutable reference to the custom icon of this group, if it exists and is a custom
542    /// icon.
543    pub fn custom_icon_mut(&mut self) -> Option<CustomIconMut<'_>> {
544        if let Some(Icon::Custom(custom_icon_id)) = self.icon {
545            Some(CustomIconMut::new(self.database, custom_icon_id))
546        } else {
547            None
548        }
549    }
550
551    /// Move this group to a new parent group.
552    ///
553    /// Performs sanity checking and will return an error if the destination does not exist,
554    /// belongs to a different database, or if the move would create a cycle in the group
555    /// hierarchy.
556    pub fn move_to(&mut self, new_parent_id: GroupId) -> Result<(), MoveGroupError> {
557        let old_parent_id = self.parent.ok_or(MoveGroupError::CannotMoveRoot)?;
558
559        if !self.database.groups.contains_key(&new_parent_id) {
560            return Err(MoveGroupError::NotFound(new_parent_id));
561        }
562
563        // Check for cycles
564        let mut current = Some(new_parent_id);
565        while let Some(curr_id) = current {
566            if curr_id == self.id {
567                return Err(MoveGroupError::WouldCreateCycle);
568            }
569            current = self.database.groups.get(&curr_id).and_then(|g| g.parent);
570        }
571
572        // Remove from old parent
573        #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] // we checked that old_parent_id exists
574        let mut old_parent = self.database.group_mut(old_parent_id).unwrap();
575        old_parent.groups.shift_remove(&self.id);
576
577        // Insert into new parent
578        #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] // we checked that new_parent_id exists
579        let mut new_parent = self.database.group_mut(new_parent_id).unwrap();
580        new_parent.groups.insert(self.id);
581
582        // Update parent reference
583        self.parent = Some(new_parent_id);
584        self.previous_parent_group = Some(old_parent_id);
585
586        Ok(())
587    }
588
589    /// Deletes this group and all its child groups and entries from the database.
590    pub fn remove(mut self) {
591        // Remove this group's back-reference from its custom icon (if any) before
592        // the group is erased from db.groups, so the back-ref set stays consistent.
593        self.set_icon_none();
594
595        // Remove from parent
596        if let Some(parent_id) = self.parent {
597            if let Some(mut parent) = self.database.group_mut(parent_id) {
598                parent.groups.shift_remove(&self.id);
599            }
600        }
601
602        // Delete entries
603        let entry_ids: Vec<EntryId> = self.entries.iter().cloned().collect();
604        for entry_id in entry_ids {
605            if let Some(entry) = self.database.entry_mut(entry_id) {
606                entry.remove();
607            }
608        }
609
610        // Recursively delete child groups
611        let child_group_ids: Vec<GroupId> = self.groups.iter().cloned().collect();
612        for child_id in child_group_ids {
613            if let Some(child_group) = self.database.group_mut(child_id) {
614                child_group.remove();
615            }
616        }
617
618        // Finally, remove this group from the database
619        self.database.groups.remove(&self.id);
620
621        // Clear any Meta UUID fields that referenced this group, so they don't
622        // silently hold stale UUIDs that would be round-tripped back to disk.
623        let uuid = self.id.0;
624        let meta = &mut self.database.meta;
625        if meta.recyclebin_uuid == Some(uuid) {
626            meta.recyclebin_uuid = None;
627        }
628        if meta.entry_templates_group == Some(uuid) {
629            meta.entry_templates_group = None;
630        }
631        if meta.last_selected_group == Some(uuid) {
632            meta.last_selected_group = None;
633        }
634        if meta.last_top_visible_group == Some(uuid) {
635            meta.last_top_visible_group = None;
636        }
637    }
638
639    /// Convert this mutable group reference into a history-tracking variant that will record
640    /// changes such as deletions and moves.
641    pub fn track_changes(&mut self) -> GroupTrack<'_> {
642        GroupTrack {
643            database: self.database,
644            id: self.id,
645        }
646    }
647}
648
649/// Attempted to add a group with an ID that already exists in the database.
650#[derive(Debug, Error)]
651#[error("a group with ID {0} already exists in the database")]
652pub struct DuplicateGroupIdError(pub GroupId);
653
654/// Attempted to add an entry with an ID that already exists in the database.
655#[derive(Debug, Error)]
656#[error("an entry with ID {0} already exists in the database")]
657pub struct DuplicateEntryIdError(pub EntryId);
658
659/// Errors that can occur when moving a group to a new parent.
660#[derive(Debug, Error)]
661#[non_exhaustive]
662pub enum MoveGroupError {
663    /// The root group cannot be moved
664    #[error("Cannot move the root group")]
665    CannotMoveRoot,
666
667    /// The destination group was not found in the database.
668    ///
669    /// This error can also occur if the destination group belongs to a different database.
670    #[error("Destination group with ID {0} not found")]
671    NotFound(GroupId),
672
673    /// Moving the group would create a cycle in the group hierarchy, which is not allowed.
674    #[error("Cannot move a group into itself or one of its descendants")]
675    WouldCreateCycle,
676}
677
678impl Deref for GroupMut<'_> {
679    type Target = Group;
680
681    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // group existence is guaranteed
682    fn deref(&self) -> &Self::Target {
683        self.database
684            .groups
685            .get(&self.id)
686            .expect("GroupMut points to a non-existing group")
687    }
688}
689
690impl DerefMut for GroupMut<'_> {
691    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // group existence is guaranteed
692    fn deref_mut(&mut self) -> &mut Self::Target {
693        self.database
694            .groups
695            .get_mut(&self.id)
696            .expect("GroupMut points to a non-existing group")
697    }
698}
699
700/// A variant of [GroupMut] that tracks changes to the group, such as deletions and moves, and
701/// updates the location changed time when the group is moved.
702pub struct GroupTrack<'a> {
703    database: &'a mut crate::db::Database,
704    id: GroupId,
705}
706
707impl GroupTrack<'_> {
708    /// Turn the GroupTrack back into a regular GroupMut
709    pub fn as_mut(&mut self) -> GroupMut<'_> {
710        GroupMut::new(self.database, self.id)
711    }
712
713    /// Move this group to a new parent group, updating the location changed time.
714    pub fn move_to(&mut self, new_parent_id: GroupId) -> Result<(), MoveGroupError> {
715        self.as_mut().move_to(new_parent_id)?;
716        self.times.location_changed = Some(Times::now());
717        Ok(())
718    }
719
720    /// Deletes this group and all its child groups and entries from the database,
721    /// adding them to the deleted entries and groups sets.
722    pub fn remove(self) -> Result<(), CannotDeleteRootError> {
723        if self.id() == self.database.root().id() {
724            return Err(CannotDeleteRootError);
725        }
726
727        // Remove from parent
728        if let Some(parent_id) = self.parent {
729            if let Some(mut parent) = self.database.group_mut(parent_id) {
730                parent.groups.shift_remove(&self.id);
731            }
732        }
733
734        // Delete entries
735        let entry_ids: Vec<EntryId> = self.entries.iter().cloned().collect();
736
737        for entry_id in entry_ids {
738            if let Some(mut entry) = self.database.entry_mut(entry_id) {
739                entry.track_changes().remove();
740            }
741        }
742
743        // Recursively delete child groups
744        let child_group_ids: Vec<GroupId> = self.groups.iter().cloned().collect();
745        for child_id in child_group_ids {
746            if let Some(mut child_group) = self.database.group_mut(child_id) {
747                child_group.track_changes().remove()?;
748            }
749        }
750
751        // Finally, remove this group from the database and add to deleted groups
752        self.database.groups.remove(&self.id);
753        self.database
754            .deleted_objects
755            .insert(self.id.uuid(), Some(Times::now()));
756
757        Ok(())
758    }
759
760    /// Convenience method to edit the group in a closure, updating the last modification time.
761    pub fn edit(&mut self, f: impl FnOnce(&mut GroupTrack<'_>)) -> &mut Self {
762        f(self);
763        self.as_mut().times.last_modification = Some(Times::now());
764        self
765    }
766}
767
768impl Deref for GroupTrack<'_> {
769    type Target = Group;
770
771    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // group existence is guaranteed
772    fn deref(&self) -> &Self::Target {
773        self.database.groups.get(&self.id).expect("Group not found")
774    }
775}
776
777impl DerefMut for GroupTrack<'_> {
778    #[allow(clippy::expect_used, clippy::missing_panics_doc)] // group existence is guaranteed
779    fn deref_mut(&mut self) -> &mut Self::Target {
780        self.database.groups.get_mut(&self.id).expect("Group not found")
781    }
782}
783
784/// Error returned when attempting to delete the root group, which is not allowed.
785#[derive(Debug, Error)]
786#[error("Cannot delete the root group")]
787pub struct CannotDeleteRootError;
788
789#[cfg(test)]
790#[allow(clippy::unwrap_used)]
791mod group_tests {
792    use crate::db::fields;
793    use crate::Database;
794
795    #[test]
796    fn get() {
797        let mut db = Database::new();
798
799        let general_group_id = db.root_mut().add_group().edit(|g| g.name = "General".into()).id();
800
801        let sample_entry_id = db
802            .group_mut(general_group_id)
803            .unwrap()
804            .add_entry()
805            .edit(|e| {
806                e.set_unprotected(fields::TITLE, "Sample Entry #2");
807            })
808            .id();
809
810        assert_eq!(
811            db.entry(sample_entry_id).unwrap().get(fields::TITLE),
812            Some("Sample Entry #2")
813        );
814
815        let root = db.root();
816
817        assert!(root.group(general_group_id).is_some());
818        assert!(db
819            .group(general_group_id)
820            .unwrap()
821            .entry(sample_entry_id)
822            .is_some());
823
824        let grp = root.group_by_path(&["General"]).unwrap();
825
826        assert!(grp.entry_by_name("Sample Entry #2").is_some());
827
828        assert!(root.group_by_name("General").is_some());
829
830        assert!(root.group_by_name("Invalid Group").is_none());
831
832        assert!(root.group_by_path(&[]).is_some());
833    }
834
835    #[test]
836    fn get_mut() {
837        let mut db = Database::new();
838
839        let general_group_id = db.root_mut().add_group().edit(|g| g.name = "General".into()).id();
840
841        let sample_entry_id = db
842            .group_mut(general_group_id)
843            .unwrap()
844            .add_entry()
845            .edit(|e| {
846                e.set_unprotected(fields::TITLE, "Sample Entry #2");
847            })
848            .id();
849
850        assert_eq!(
851            db.entry_mut(sample_entry_id).unwrap().get(fields::TITLE),
852            Some("Sample Entry #2")
853        );
854
855        let mut root = db.root_mut();
856        assert!(root.group_mut(general_group_id).is_some());
857
858        let mut grp = root.group_by_path_mut(&["General"]).unwrap();
859
860        assert!(grp.entry_by_name_mut("Sample Entry #2").is_some());
861
862        assert!(root.group_by_name_mut("General").is_some());
863        assert!(root.group_by_name_mut("Invalid Group").is_none());
864        assert!(root.group_by_path_mut(&[]).is_some());
865
866        assert!(db
867            .group_mut(general_group_id)
868            .unwrap()
869            .entry_mut(sample_entry_id)
870            .is_some());
871    }
872
873    #[test]
874    fn add_entry_with_id_uses_supplied_uuid() {
875        use crate::db::EntryId;
876        use uuid::uuid;
877
878        let mut db = Database::new();
879        let pinned_uuid = uuid!("00000000-0000-0000-0000-0000000000aa");
880        let pinned: EntryId = pinned_uuid.into();
881
882        let inserted_id = db
883            .root_mut()
884            .add_entry_with_id(pinned)
885            .unwrap()
886            .edit(|e| {
887                e.set_unprotected(fields::TITLE, "pinned");
888            })
889            .id();
890
891        assert_eq!(inserted_id, pinned);
892        assert_eq!(inserted_id.uuid(), pinned_uuid);
893        assert_eq!(db.entry(pinned).unwrap().get(fields::TITLE), Some("pinned"),);
894    }
895
896    #[test]
897    fn add_entry_with_id_duplicate_returns_error() {
898        use crate::db::{DuplicateEntryIdError, EntryId};
899        use uuid::uuid;
900
901        let mut db = Database::new();
902        let pinned: EntryId = uuid!("00000000-0000-0000-0000-0000000000ab").into();
903
904        db.root_mut().add_entry_with_id(pinned).unwrap();
905
906        assert!(matches!(
907            db.root_mut().add_entry_with_id(pinned),
908            Err(DuplicateEntryIdError(eid)) if eid == pinned
909        ));
910    }
911
912    #[test]
913    fn add_group_with_id_uses_supplied_uuid() {
914        use crate::db::GroupId;
915        use uuid::uuid;
916
917        let mut db = Database::new();
918        let pinned_uuid = uuid!("00000000-0000-0000-0000-0000000000ba");
919        let pinned: GroupId = pinned_uuid.into();
920
921        let inserted_id = db
922            .root_mut()
923            .add_group_with_id(pinned)
924            .unwrap()
925            .edit(|g| g.name = "pinned".into())
926            .id();
927
928        assert_eq!(inserted_id, pinned);
929        assert_eq!(inserted_id.uuid(), pinned_uuid);
930        assert_eq!(db.group(pinned).unwrap().name, "pinned");
931    }
932
933    #[test]
934    fn add_group_with_id_duplicate_returns_error() {
935        use crate::db::{DuplicateGroupIdError, GroupId};
936        use uuid::uuid;
937
938        let mut db = Database::new();
939        let pinned: GroupId = uuid!("00000000-0000-0000-0000-0000000000bb").into();
940
941        db.root_mut().add_group_with_id(pinned).unwrap();
942
943        assert!(matches!(
944            db.root_mut().add_group_with_id(pinned),
945            Err(DuplicateGroupIdError(gid)) if gid == pinned
946        ));
947    }
948
949    #[test]
950    fn group_id_new_generates_distinct_ids() {
951        use crate::db::GroupId;
952
953        assert_ne!(GroupId::new(), GroupId::new());
954    }
955
956    #[test]
957    fn from_uuid_impls_match_constructors() {
958        use crate::db::{EntryId, GroupId};
959        use uuid::uuid;
960
961        let raw = uuid!("00000000-0000-0000-0000-0000000000cc");
962        let from_entry: EntryId = raw.into();
963        let from_entry_ctor = EntryId::from_uuid(raw);
964        assert_eq!(from_entry, from_entry_ctor);
965        assert_eq!(from_entry.uuid(), raw);
966
967        let from_group: GroupId = raw.into();
968        let from_group_ctor = GroupId::from_uuid(raw);
969        assert_eq!(from_group, from_group_ctor);
970        assert_eq!(from_group.uuid(), raw);
971    }
972}