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