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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
22pub struct EntryId(Uuid);
23
24impl EntryId {
25 pub fn new() -> Self {
27 Self(Uuid::new_v4())
28 }
29
30 pub const fn from_uuid(uuid: Uuid) -> Self {
36 Self(uuid)
37 }
38
39 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#[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 pub fields: HashMap<String, Value<String>>,
62
63 pub autotype: Option<AutoType>,
65
66 pub tags: Vec<String>,
68
69 pub times: Times,
71
72 pub custom_data: HashMap<String, CustomDataItem>,
74
75 pub(crate) icon: Option<Icon>,
76
77 pub foreground_color: Option<Color>,
79
80 pub background_color: Option<Color>,
82
83 pub override_url: Option<String>,
85
86 pub quality_check: bool,
88
89 pub(crate) attachments: HashMap<String, AttachmentId>,
91
92 pub(crate) previous_parent_group: Option<GroupId>,
94
95 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 pub fn id(&self) -> EntryId {
126 self.id
127 }
128
129 pub fn icon(&self) -> Option<&Icon> {
131 self.icon.as_ref()
132 }
133
134 pub fn get(&self, key: &str) -> Option<&str> {
136 self.fields.get(key).map(|v| v.as_str())
137 }
138
139 pub fn set(&mut self, key: impl Into<String>, value: Value<String>) {
141 self.fields.insert(key.into(), value);
142 }
143
144 pub fn set_unprotected(&mut self, key: impl Into<String>, value: impl Into<String>) {
146 self.set(key, Value::unprotected(value));
147 }
148
149 pub fn set_protected(&mut self, key: impl Into<String>, value: impl Into<String>) {
151 self.set(key, Value::protected(value));
152 }
153
154 pub fn get_raw_otp_value(&self) -> Option<&str> {
156 self.get(fields::OTP)
157 }
158
159 pub fn get_title(&self) -> Option<&str> {
161 self.get(fields::TITLE)
162 }
163
164 pub fn get_username(&self) -> Option<&str> {
166 self.get(fields::USERNAME)
167 }
168
169 pub fn get_password(&self) -> Option<&str> {
171 self.get(fields::PASSWORD)
172 }
173
174 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
186pub 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 pub fn parent(&self) -> GroupRef<'_> {
216 #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] self.database.group(self.parent).unwrap()
218 }
219
220 pub fn previous_parent(&self) -> Option<GroupRef<'_>> {
222 self.previous_parent_group
223 .and_then(|id| self.database().group(id))
224 }
225
226 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 pub fn database(&self) -> &Database {
245 self.database
246 }
247
248 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 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 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 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 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)] fn deref(&self) -> &Self::Target {
299 let entry = self.database.entries.get(&self.id).expect("Entry not found");
301
302 if let Some(n) = self.history_index {
303 #[allow(clippy::unwrap_used, clippy::indexing_slicing)]
305 &entry.history.as_ref().unwrap().entries[n]
306 } else {
307 entry
308 }
309 }
310}
311
312pub 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 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 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 pub fn edit(&mut self, f: impl FnOnce(&mut EntryMut<'_>)) -> &mut Self {
364 f(self);
365 self
366 }
367
368 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 pub fn track_changes(&mut self) -> EntryTrack<'_> {
382 let mut historical: Entry = self.deref().deref().clone();
383
384 historical.history = None;
386
387 EntryTrack {
388 database: self.database,
389 id: self.id,
390 historical,
391 }
392 }
393
394 pub fn parent_mut(&mut self) -> GroupMut<'_> {
396 #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] self.database.group_mut(self.parent).unwrap()
398 }
399
400 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 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 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 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 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 self.remove_attachment_by_id(old_id);
447 }
448
449 AttachmentMut::new(self.database, id)
450 }
451
452 pub fn remove_attachment_by_name(&mut self, name: &str) {
456 let id = self.id;
457
458 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 attachment.entries.is_empty() {
465 attachment.remove();
466 }
467 }
468 }
469 }
470
471 pub fn remove_attachment_by_id(&mut self, attachment_id: AttachmentId) {
475 let id = self.id;
476
477 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 attachment.entries.is_empty() {
494 attachment.remove();
495 }
496 }
497 }
498
499 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 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 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 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 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 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 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)] 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 pub fn database_mut(&mut self) -> &mut Database {
603 self.database
604 }
605
606 #[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn remove(mut self) {
609 let id = self.id;
610
611 self.set_icon_none();
613
614 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 self.foreach_attachment_mut(|mut attachment| {
624 attachment.entries.retain(|&(entry_id, _)| entry_id != id);
625
626 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 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 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#[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)] fn deref(&self) -> &Self::Target {
664 let entry = self.database.entries.get(&self.id).expect("Entry not found");
666
667 if let Some(n) = self.history_index {
668 #[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)] fn deref_mut(&mut self) -> &mut Self::Target {
680 let entry = self.database.entries.get_mut(&self.id).expect("Entry not found");
682
683 if let Some(n) = self.history_index {
684 #[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#[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 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 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 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 this.remove();
728 }
729
730 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 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 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 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 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 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 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 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 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)] 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)] 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 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 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}