1use crate::db::{
2 Attachment, AutoType, Color, CustomDataItem, History, IconId, Times, Value,
3 node::{Node, NodePtr},
4 rc_refcell_node,
5};
6use secrecy::ExposeSecret;
7use std::collections::HashMap;
8use uuid::Uuid;
9
10#[derive(Debug, Clone)]
12#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
13pub struct Entry {
14 pub(crate) uuid: Uuid,
15 pub(crate) fields: HashMap<String, Value<String>>,
16 pub(crate) autotype: Option<AutoType>,
17 pub(crate) tags: Vec<String>,
18
19 pub(crate) times: Times,
20
21 pub(crate) custom_data: HashMap<String, CustomDataItem>,
22
23 pub(crate) icon_id: Option<IconId>,
24 pub(crate) custom_icon: Option<Uuid>,
25
26 pub(crate) foreground_color: Option<Color>,
27 pub(crate) background_color: Option<Color>,
28
29 pub(crate) override_url: Option<String>,
30 pub(crate) quality_check: Option<bool>,
31
32 pub attachments: HashMap<String, Attachment>,
33
34 pub(crate) history: Option<History>,
35
36 pub(crate) parent: Option<Uuid>,
37
38 pub(crate) previous_parent_group: Option<Uuid>,
39}
40
41impl Default for Entry {
42 fn default() -> Self {
43 Self {
44 uuid: Uuid::new_v4(),
45 fields: HashMap::new(),
46 autotype: None,
47 tags: Vec::new(),
48 times: Times::new(),
49 custom_data: Default::default(),
50 icon_id: Some(IconId::KEY),
51 custom_icon: None,
52 foreground_color: None,
53 background_color: None,
54 override_url: None,
55 quality_check: None,
56 attachments: HashMap::new(),
57 history: None,
58 parent: None,
59 previous_parent_group: None,
60 }
61 }
62}
63
64impl PartialEq for Entry {
65 fn eq(&self, other: &Self) -> bool {
66 self.uuid == other.uuid
67 && self.fields == other.fields
68 && self.autotype == other.autotype
69 && self.tags == other.tags
70 && self.times == other.times
71 && self.custom_data == other.custom_data
72 && self.icon_id == other.icon_id
73 && self.custom_icon == other.custom_icon
74 && self.foreground_color == other.foreground_color
75 && self.background_color == other.background_color
76 && self.override_url == other.override_url
77 && self.quality_check == other.quality_check
78 && self.attachments == other.attachments
79 && self.history == other.history
80 }
82}
83
84impl Eq for Entry {}
85
86impl Node for Entry {
87 fn duplicate(&self) -> NodePtr {
88 let mut tmp = self.clone();
89 tmp.parent = None;
90 rc_refcell_node(tmp)
91 }
92
93 fn get_uuid(&self) -> Uuid {
94 self.uuid
95 }
96
97 fn set_uuid(&mut self, uuid: Uuid) {
98 self.uuid = uuid;
99 }
100
101 fn get_title(&self) -> Option<&str> {
102 self.get("Title")
103 }
104
105 fn set_title(&mut self, title: Option<&str>) {
106 self.set_unprotected_field_pair("Title", title);
107 }
108
109 fn get_notes(&self) -> Option<&str> {
110 self.get("Notes")
111 }
112
113 fn set_notes(&mut self, notes: Option<&str>) {
114 self.set_unprotected_field_pair("Notes", notes);
115 }
116
117 fn get_icon_id(&self) -> Option<IconId> {
118 self.icon_id
119 }
120
121 fn set_icon_id(&mut self, icon_id: Option<IconId>) {
122 self.icon_id = icon_id;
123 }
124
125 fn get_custom_icon_uuid(&self) -> Option<Uuid> {
126 self.custom_icon
127 }
128
129 fn get_times(&self) -> &Times {
130 &self.times
131 }
132
133 fn get_times_mut(&mut self) -> &mut Times {
134 &mut self.times
135 }
136
137 fn get_parent(&self) -> Option<Uuid> {
138 self.parent
139 }
140
141 fn set_parent(&mut self, parent: Option<Uuid>) {
142 self.parent = parent;
143 }
144}
145
146impl Entry {
147 pub fn set_custom_icon_uuid(&mut self, uuid: Option<Uuid>) {
148 self.custom_icon = uuid;
149 }
150
151 pub fn quality_check(&self) -> bool {
152 self.quality_check.unwrap_or(true)
153 }
154
155 pub fn previous_parent_group(&self) -> Option<Uuid> {
156 self.previous_parent_group
157 }
158
159 pub fn custom_icon_uuid(&self) -> Option<Uuid> {
160 self.custom_icon
161 }
162
163 pub fn custom_data(&self) -> &HashMap<String, CustomDataItem> {
164 &self.custom_data
165 }
166
167 pub fn custom_data_mut(&mut self) -> &mut HashMap<String, CustomDataItem> {
168 &mut self.custom_data
169 }
170
171 pub fn get_history(&self) -> &Option<History> {
172 &self.history
173 }
174
175 pub fn purge_history(&mut self) {
176 self.history = None;
177 }
178
179 pub(crate) fn set_unprotected_field_pair(&mut self, field_name: &str, field_value: Option<&str>) {
180 if let Some(field_value) = field_value {
181 let v = Value::Unprotected(field_value.to_string());
182 self.fields.insert(field_name.to_string(), v);
183 } else {
184 self.fields.remove(field_name);
185 }
186 }
187
188 pub(crate) fn set_protected_field_pair<T: AsRef<[u8]>>(&mut self, field_name: &str, field_value: Option<T>) {
189 if let Some(field_value) = field_value {
190 let value = String::from_utf8_lossy(field_value.as_ref()).into_owned();
191 let v = Value::protected(value);
192 self.fields.insert(field_name.to_string(), v);
193 } else {
194 self.fields.remove(field_name);
195 }
196 }
197
198 pub(crate) fn set_binary_field_pair<T: AsRef<[u8]>>(&mut self, field_name: &str, field_value: Option<T>) {
199 if let Some(field_value) = field_value {
200 self.attachments.insert(
201 field_name.to_string(),
202 Attachment {
203 data: Value::unprotected(field_value.as_ref().to_vec()),
204 },
205 );
206 } else {
207 self.attachments.remove(field_name);
208 }
209 }
210}
211
212impl<'a> Entry {
213 pub fn get(&'a self, key: &str) -> Option<&'a str> {
215 match self.fields.get(key) {
216 None => None,
217 Some(Value::Protected(pv)) => Some(pv.expose_secret()),
218 Some(Value::Unprotected(uv)) => Some(uv),
219 }
220 }
221
222 pub fn get_bytes(&'a self, key: &str) -> Option<&'a [u8]> {
224 self.attachments.get(key).map(|attachment| attachment.data.get().as_slice())
225 }
226
227 pub fn get_autotype(&self) -> Option<&AutoType> {
228 self.autotype.as_ref()
229 }
230
231 pub fn set_autotype(&mut self, autotype: Option<AutoType>) {
232 self.autotype = autotype;
233 }
234
235 pub fn get_tags(&self) -> &Vec<String> {
238 self.tags.as_ref()
239 }
240
241 pub fn get_tags_mut(&mut self) -> &mut Vec<String> {
242 self.tags.as_mut()
243 }
244
245 #[rustfmt::skip]
246 const EXCLUDED_FIELDS: [&'static str; 9] = ["Password", "BinaryData", "otp", "Title", "URL", "UserName", "Notes", "Additional", "BinaryDesc"];
247
248 pub fn set_additional_attribute(&mut self, key: &str, value: Option<&str>) -> crate::Result<()> {
250 if Self::EXCLUDED_FIELDS.contains(&key) {
251 return Err(format!("Cannot set additional attribute for field {key}").into());
252 }
253 self.set_unprotected_field_pair(key, value);
254 Ok(())
255 }
256
257 pub fn get_additional_attribute(&self, key: &str) -> Option<&str> {
259 if Self::EXCLUDED_FIELDS.contains(&key) {
260 return None;
261 }
262 self.get(key)
263 }
264
265 pub fn additional_attributes(&self) -> Vec<(String, String)> {
267 self.fields
268 .keys()
269 .filter(|key| !Self::EXCLUDED_FIELDS.contains(&key.as_str()))
270 .filter_map(|key| self.get(key).map(|value| (key.clone(), value.to_string())))
271 .collect()
272 }
273
274 pub fn get_username(&'a self) -> Option<&'a str> {
276 self.get("UserName")
277 }
278
279 pub fn set_username(&mut self, username: Option<&str>) {
280 self.set_unprotected_field_pair("UserName", username);
281 }
282
283 pub fn get_password(&self) -> Option<&str> {
285 self.get("Password")
286 }
287
288 pub fn set_password(&mut self, password: Option<&str>) {
289 self.set_protected_field_pair("Password", password.map(|p| p.as_bytes()));
290 }
291
292 pub fn get_url(&self) -> Option<&str> {
294 self.get("URL")
295 }
296
297 pub fn set_url(&mut self, url: Option<&str>) {
298 self.set_unprotected_field_pair("URL", url);
299 }
300
301 pub fn update_history(&mut self) -> bool {
308 if self.history.is_none() {
309 self.history = Some(History::default());
310 }
311
312 if !self.has_uncommited_changes() {
313 return false;
314 }
315
316 self.times.set_last_modification(Some(Times::now()));
317
318 let mut new_history_entry = self.clone();
319 new_history_entry.history = None;
320
321 if let Some(h) = self.history.as_mut() {
324 h.add_entry(new_history_entry);
325 }
326
327 true
328 }
329
330 pub(crate) fn has_uncommited_changes(&self) -> bool {
333 if let Some(history) = self.history.as_ref() {
334 if history.entries.is_empty() {
335 return true;
336 }
337
338 let new_times = Times::default();
339
340 let mut sanitized_entry = self.clone();
341 sanitized_entry.times = new_times.clone();
342 sanitized_entry.history.take();
343
344 let mut last_history_entry = history.entries.first().unwrap().clone();
345 last_history_entry.times = new_times;
346 last_history_entry.history.take();
347
348 if sanitized_entry.eq(&last_history_entry) {
349 return false;
350 }
351 }
352 true
353 }
354}
355
356#[cfg(test)]
357mod entry_tests {
358 use super::{Entry, Node};
359 use std::{thread, time};
360
361 #[test]
362 fn byte_values() {
363 let mut entry = Entry::default();
364 entry.set_binary_field_pair("a-bytes", Some(&[1, 2, 3]));
365
366 entry.set_unprotected_field_pair("a-unprotected", Some("asdf"));
367 entry.set_protected_field_pair("a-protected", Some("asdf".as_bytes()));
368
369 assert_eq!(entry.get_bytes("a-bytes"), Some(&[1, 2, 3][..]));
370 assert_eq!(entry.get_bytes("a-unprotected"), None);
371 assert_eq!(entry.get_bytes("a-protected"), None);
372
373 assert_eq!(entry.get("a-bytes"), None);
374
375 assert!(!entry.attachments["a-bytes"].data.is_empty());
376 entry.set_binary_field_pair::<&[u8]>("a-bytes", None);
377 assert_eq!(entry.get_bytes("a-bytes"), None);
378 }
379
380 #[test]
381 fn update_history() {
382 let mut entry = Entry::default();
383 let mut last_modification_time = entry.times.get_last_modification().unwrap();
384
385 entry.set_username(Some("user"));
386 thread::sleep(time::Duration::from_secs(1));
389
390 assert!(entry.update_history());
391 assert!(entry.history.is_some());
392 assert_eq!(entry.history.as_ref().unwrap().entries.len(), 1);
393 assert_ne!(entry.times.get_last_modification().unwrap(), last_modification_time);
394 last_modification_time = entry.times.get_last_modification().unwrap();
395 thread::sleep(time::Duration::from_secs(1));
396
397 assert!(!entry.update_history());
400 assert!(entry.history.is_some());
401 assert_eq!(entry.history.as_ref().unwrap().entries.len(), 1);
402 assert_eq!(entry.times.get_last_modification().unwrap(), last_modification_time);
403
404 entry.set_title(Some("first title"));
405
406 assert!(entry.update_history());
407 assert!(entry.history.is_some());
408 assert_eq!(entry.history.as_ref().unwrap().entries.len(), 2);
409 assert_ne!(entry.times.get_last_modification().unwrap(), last_modification_time);
410 last_modification_time = entry.times.get_last_modification().unwrap();
411 thread::sleep(time::Duration::from_secs(1));
412
413 assert!(!entry.update_history());
414 assert!(entry.history.is_some());
415 assert_eq!(entry.history.as_ref().unwrap().entries.len(), 2);
416 assert_eq!(entry.times.get_last_modification().unwrap(), last_modification_time);
417
418 entry.set_title(Some("second title"));
419
420 assert!(entry.update_history());
421 assert!(entry.history.is_some());
422 assert_eq!(entry.history.as_ref().unwrap().entries.len(), 3);
423 assert_ne!(entry.times.get_last_modification().unwrap(), last_modification_time);
424 last_modification_time = entry.times.get_last_modification().unwrap();
425 thread::sleep(time::Duration::from_secs(1));
426
427 assert!(!entry.update_history());
428 assert!(entry.history.is_some());
429 assert_eq!(entry.history.as_ref().unwrap().entries.len(), 3);
430 assert_eq!(entry.times.get_last_modification().unwrap(), last_modification_time);
431
432 let last_history_entry = entry.history.as_ref().unwrap().entries.first().unwrap();
433 assert_eq!(last_history_entry.get_title().unwrap(), "second title");
434
435 for history_entry in &entry.history.unwrap().entries {
436 assert!(history_entry.history.is_none());
437 }
438 }
439
440 #[cfg(feature = "totp")]
441 #[test]
442 fn totp() {
443 let mut entry = Entry::default();
444 entry.set_raw_otp_value(
445 Some("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"),
446 );
447
448 assert!(entry.get_otp().is_ok());
449 }
450
451 #[cfg(feature = "serialization")]
452 #[test]
453 fn serialization() {
454 use super::Value;
455 assert_eq!(
456 serde_json::to_string(&Value::Unprotected(vec![65, 66, 67])).unwrap(),
457 "[65,66,67]".to_string()
458 );
459
460 assert_eq!(
461 serde_json::to_string(&Value::Unprotected("ABC".to_string())).unwrap(),
462 "\"ABC\"".to_string()
463 );
464
465 assert_eq!(
466 serde_json::to_string(&Value::<String>::protected("ABC")).unwrap(),
467 "\"ABC\"".to_string()
468 );
469 }
470}