joplin_reader/
note.rs

1use crate::JoplinReaderError;
2
3use regex::{Captures, Regex};
4use std::collections::HashMap;
5use std::fs;
6use std::io::{BufRead, BufReader};
7use std::iter::DoubleEndedIterator;
8use std::path::{Path, PathBuf};
9use std::str::Chars;
10use std::time::SystemTime;
11
12use chrono::NaiveDateTime;
13use percent_encoding::percent_decode_str;
14use sjcl::decrypt_raw;
15use serde;
16use serde::ser::{Serialize, Serializer, SerializeStruct};
17
18/// How often encrypted notes should be refreshed in seconds
19const REFRESH_INTERVAL: u64 = 60 * 60 * 12;
20/// Size of the full encryption header
21const HEADER_SIZE: u32 = 45;
22
23/// Various types of items a joplin file can be.
24/// See: https://joplinapp.org/api/references/rest_api/#item-type-ids
25#[derive(Debug, PartialEq, serde::Serialize)]
26pub enum JoplinItemType {
27    Undefined = 0,
28    Note = 1,
29    Folder = 2,
30    Setting = 3,
31    Resource = 4,
32    Tag = 5,
33    NoteTag = 6,
34    Search = 7,
35    Alarm = 8,
36    MasterKey = 9,
37    ItemChange = 10,
38    NoteResource = 11,
39    ResourceLocalState = 12,
40    Revision = 13,
41    Migration = 14,
42    SmartFilter = 15,
43    Command = 16,
44}
45
46impl From<i32> for JoplinItemType {
47    fn from(v: i32) -> Self {
48        match v {
49            1 => JoplinItemType::Note,
50            2 => JoplinItemType::Folder,
51            3 => JoplinItemType::Setting,
52            4 => JoplinItemType::Resource,
53            5 => JoplinItemType::Tag,
54            6 => JoplinItemType::NoteTag,
55            7 => JoplinItemType::Search,
56            8 => JoplinItemType::Alarm,
57            9 => JoplinItemType::MasterKey,
58            10 => JoplinItemType::ItemChange,
59            11 => JoplinItemType::NoteResource,
60            12 => JoplinItemType::ResourceLocalState,
61            13 => JoplinItemType::Revision,
62            14 => JoplinItemType::Migration,
63            15 => JoplinItemType::SmartFilter,
64            16 => JoplinItemType::Command,
65            _ => JoplinItemType::Undefined,
66        }
67    }
68}
69
70/// Contains general information about a note, and reads a part of the header
71/// when created to check if the note needs to be decrypted (and with which
72/// key).
73#[derive(Debug)]
74pub struct NoteInfo {
75    path: PathBuf,
76    id: String,
77    type_: JoplinItemType,
78    encryption_applied: bool,
79    parent_id: Option<String>,
80    encryption_key_id: Option<String>,
81    updated_time: Option<NaiveDateTime>,
82    // `read_time` is when it was read into by **us**
83    read_time: Option<SystemTime>,
84    content: NoteProperties,
85}
86
87impl Serialize for NoteInfo {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        let mut state = serializer.serialize_struct("NoteInfo", 9)?;
93        state.serialize_field("created_time", &self.path)?;
94        state.serialize_field("id", &self.id)?;
95        state.serialize_field("type_", &self.type_)?;
96        state.serialize_field("encryption_applied", &self.encryption_applied)?;
97        state.serialize_field("parent_id", &self.parent_id)?;
98        state.serialize_field("encryption_key_id", &self.encryption_key_id)?;
99        state.serialize_field("updated_time", &self.updated_time.map_or(0, |ut| ut.timestamp()))?;
100        state.serialize_field("read_time", &self.read_time)?;
101        state.serialize_field("content", &self.content)?;
102        state.end()
103    }
104}
105
106/// Contains the actual properties and content of a note. This follows the
107/// general structure of the note properties from Joplin minus the ones already
108/// read into [`NoteInfo`].
109/// See: https://joplinapp.org/api/references/rest_api/#properties
110#[derive(Debug, Clone)]
111pub struct NoteProperties {
112    title: Option<String>,
113    body: Option<String>,
114    created_time: Option<NaiveDateTime>,
115    altitude: Option<f32>,
116    latitude: Option<f64>,
117    longitude: Option<f64>,
118    author: Option<String>,
119    source_url: Option<String>,
120    is_todo: Option<bool>,
121    todo_due: Option<bool>,
122    todo_completed: Option<bool>,
123    source: Option<String>,
124    source_application: Option<String>,
125    application_data: Option<String>,
126    order: Option<i32>,
127    user_created_time: Option<NaiveDateTime>,
128    user_updated_time: Option<NaiveDateTime>,
129    markup_language: Option<String>,
130    is_shared: Option<bool>,
131}
132impl Default for NoteProperties {
133    fn default() -> Self {
134        Self {
135            title: None,
136            body: None,
137            created_time: None,
138            altitude: None,
139            latitude: None,
140            longitude: None,
141            author: None,
142            source_url: None,
143            is_todo: None,
144            todo_due: None,
145            todo_completed: None,
146            source: None,
147            source_application: None,
148            application_data: None,
149            order: None,
150            user_created_time: None,
151            user_updated_time: None,
152            markup_language: None,
153            is_shared: None,
154        }
155    }
156}
157impl From<HashMap<String, String>> for NoteProperties {
158    fn from(mut kv_store: HashMap<String, String>) -> Self {
159        let mut title: Option<String> = None;
160        let mut body: Option<String> = None;
161        let mut created_time: Option<NaiveDateTime> = None;
162        let mut altitude: Option<f32> = None;
163        let mut latitude: Option<f64> = None;
164        let mut longitude: Option<f64> = None;
165        let mut author: Option<String> = None;
166        let mut source_url: Option<String> = None;
167        let mut is_todo: Option<bool> = None;
168        let mut todo_due: Option<bool> = None;
169        let mut todo_completed: Option<bool> = None;
170        let mut source: Option<String> = None;
171        let mut source_application: Option<String> = None;
172        let mut application_data: Option<String> = None;
173        let mut order: Option<i32> = None;
174        let mut user_created_time: Option<NaiveDateTime> = None;
175        let mut user_updated_time: Option<NaiveDateTime> = None;
176        let mut markup_language: Option<String> = None;
177        let mut is_shared: Option<bool> = None;
178
179        for (k, v) in kv_store.drain() {
180            match k.as_str() {
181                "title" => title = Some(v),
182                "body" => body = Some(v),
183                "created_time" => {
184                    created_time = match NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.fZ")
185                    {
186                        Ok(ut) => Some(ut),
187                        Err(_) => None,
188                    }
189                }
190                "altitude" => {
191                    altitude = match v.trim().parse::<f32>() {
192                        Ok(l) => Some(l),
193                        _ => None,
194                    }
195                }
196                "latitude" => {
197                    latitude = match v.trim().parse::<f64>() {
198                        Ok(l) => Some(l),
199                        _ => None,
200                    }
201                }
202                "longitude" => {
203                    longitude = match v.trim().parse::<f64>() {
204                        Ok(l) => Some(l),
205                        _ => None,
206                    }
207                }
208                "author" => author = Some(v),
209                "source_url" => source_url = Some(v),
210                "is_todo" => {
211                    is_todo = match v.trim().parse::<i8>() {
212                        Ok(b) => Some(b == 1),
213                        _ => None,
214                    }
215                }
216                "todo_due" => {
217                    todo_due = match v.trim().parse::<i8>() {
218                        Ok(b) => Some(b == 1),
219                        _ => None,
220                    }
221                }
222                "todo_completed" => {
223                    todo_completed = match v.trim().parse::<i8>() {
224                        Ok(b) => Some(b == 1),
225                        _ => None,
226                    }
227                }
228                "source" => source = Some(v),
229                "source_application" => source_application = Some(v),
230                "application_data" => application_data = Some(v),
231                "order" => {
232                    order = match v.trim().parse::<i32>() {
233                        Ok(o) => Some(o),
234                        _ => None,
235                    }
236                }
237                "user_created_time" => {
238                    user_created_time =
239                        match NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.fZ") {
240                            Ok(ut) => Some(ut),
241                            Err(_) => None,
242                        }
243                }
244                "user_updated_time" => {
245                    user_updated_time =
246                        match NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.fZ") {
247                            Ok(ut) => Some(ut),
248                            Err(_) => None,
249                        }
250                }
251                "markup_language" => markup_language = Some(v),
252                "is_shared" => {
253                    is_shared = match v.trim().parse::<i8>() {
254                        Ok(b) => Some(b == 1),
255                        _ => None,
256                    }
257                }
258                _ => { /* unknown key */ }
259            }
260        }
261
262        Self {
263            title,
264            body,
265            created_time,
266            altitude,
267            latitude,
268            longitude,
269            author,
270            source_url,
271            is_todo,
272            todo_due,
273            todo_completed,
274            source,
275            source_application,
276            application_data,
277            order,
278            user_created_time,
279            user_updated_time,
280            markup_language,
281            is_shared,
282        }
283    }
284}
285
286impl Serialize for NoteProperties {
287    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
288    where
289        S: Serializer,
290    {
291        let mut state = serializer.serialize_struct("NoteProperties", 19)?;
292        state.serialize_field("title", &self.title.as_ref().unwrap())?;
293        state.serialize_field("body", &self.body.as_ref().unwrap())?;
294        state.serialize_field("created_time", &self.created_time.as_ref().unwrap().timestamp())?;
295        state.serialize_field("altitude", &self.altitude.as_ref().unwrap())?;
296        state.serialize_field("latitude", &self.latitude.as_ref().unwrap())?;
297        state.serialize_field("longitude", &self.longitude.as_ref().unwrap())?;
298        state.serialize_field("author", &self.author.as_ref().unwrap())?;
299        state.serialize_field("source_url", &self.source_url.as_ref().unwrap())?;
300        state.serialize_field("is_todo", &self.is_todo.as_ref().unwrap())?;
301        state.serialize_field("todo_due", &self.todo_due.as_ref().unwrap())?;
302        state.serialize_field("todo_completed", &self.todo_completed.as_ref().unwrap())?;
303        state.serialize_field("source", &self.source.as_ref().unwrap())?;
304        state.serialize_field("source_application", &self.source_application.as_ref().unwrap())?;
305        state.serialize_field("application_data", &self.application_data.as_ref().unwrap())?;
306        state.serialize_field("order", &self.order.as_ref().unwrap())?;
307        state.serialize_field("user_created_time", &self.user_created_time.as_ref().unwrap().timestamp())?;
308        state.serialize_field("user_updated_time", &self.user_updated_time.as_ref().unwrap().timestamp())?;
309        state.serialize_field("markup_language", &self.markup_language.as_ref().unwrap())?;
310        state.serialize_field("is_shared", &self.is_shared.as_ref().unwrap())?;
311        state.end()
312    }
313}
314
315/// Leading header of the `encryption_cipher_text` in an item
316#[derive(Debug)]
317struct JoplinEncryptionHeader {
318    version: u8,
319    length: u32,
320    encryption_method: JoplinEncryptionMethod,
321    master_key_id: String,
322}
323
324/// Joplin defines the various cipher suits and key lengths SJCL provides as
325/// methods in an enumerated fashion.
326/// Method 4 is used for key encryption, and method 1a for notes.
327/// Everything else is deprecated (and also considered unsecure).
328#[derive(Debug, PartialEq)]
329pub enum JoplinEncryptionMethod {
330    MethodUndefined = 0x0,
331    MethodSjcl = 0x1,
332    MethodSjcl2 = 0x2,
333    MethodSjcl3 = 0x3,
334    MethodSjcl4 = 0x4,
335    MethodSjcl1a = 0x5,
336}
337
338impl From<u8> for JoplinEncryptionMethod {
339    fn from(v: u8) -> Self {
340        match v {
341            0x1 => JoplinEncryptionMethod::MethodSjcl,
342            0x2 => JoplinEncryptionMethod::MethodSjcl2,
343            0x3 => JoplinEncryptionMethod::MethodSjcl3,
344            0x4 => JoplinEncryptionMethod::MethodSjcl4,
345            0x5 => JoplinEncryptionMethod::MethodSjcl1a,
346            _ => JoplinEncryptionMethod::MethodUndefined,
347        }
348    }
349}
350
351impl NoteInfo {
352    /// Reads an encrypted file, which has some unencrypted keys as well as the
353    /// ciphertext. List of all keys which are stored unencrypted:
354    /// https://github.com/laurent22/joplin/blob/bfacf71397e21fda5c7c1675365c4199d29de9e7/packages/lib/models/BaseItem.ts#L418
355    fn parse_encrypted_file<R: BufRead>(
356        reader: &mut R,
357    ) -> Result<HashMap<String, String>, JoplinReaderError> {
358        let mut kv_store: HashMap<String, String> = HashMap::new();
359        for line in reader.lines() {
360            let line = match line {
361                Ok(line) => line,
362                Err(_) => {
363                    return Err(JoplinReaderError::FileReadError {
364                        message: "Failed to read file".to_string(),
365                    })
366                }
367            };
368
369            let mut iter = line.splitn(2, ":");
370            let key = iter.next();
371            let value = iter.next();
372            if let (Some(key), Some(value)) = (key, value) {
373                // This will update&succeed in case of duplicate keys:
374                kv_store.insert(
375                    key.to_string().trim().to_string(),
376                    value.to_string().trim().to_string(),
377                );
378            }
379        }
380
381        Ok(kv_store)
382    }
383
384    /// So the general format for notes is:
385    /// Title\n\nBody\n\n[Prop: PropValue\n,...]
386    /// But if they are encrypted, instead some unencrypted properties may be
387    /// stored at first, and then this general format is encrypted entirely.
388    /// See [`NoteInfo::parse_encrypted_file`].
389    /// Serialization:
390    /// https://github.com/laurent22/joplin/blob/bfacf71397e21fda5c7c1675365c4199d29de9e7/packages/lib/models/BaseItem.ts#L330
391    fn deserialize(
392        text: impl DoubleEndedIterator<Item = impl AsRef<str>>,
393    ) -> Result<HashMap<String, String>, JoplinReaderError> {
394        let mut kv_store: HashMap<String, String> = HashMap::new();
395        let mut body: Vec<String> = Vec::new();
396
397        enum ReadingState {
398            Props,
399            Body,
400        }
401        let mut state: ReadingState = ReadingState::Props;
402        // Because \n\n is used for splitting, the content has to be read backwards
403        // See: https://github.com/laurent22/joplin/blob/bfacf71397e21fda5c7c1675365c4199d29de9e7/packages/lib/models/BaseItem.ts#L446
404        for line in text.rev() {
405            let line = line.as_ref().trim().to_string();
406            match state {
407                ReadingState::Props => {
408                    if line.is_empty() {
409                        state = ReadingState::Body;
410                        continue;
411                    }
412
413                    let mut iter = line.splitn(2, ":");
414                    let key = iter.next();
415                    let value = iter.next();
416                    if let (Some(key), Some(value)) = (key, value) {
417                        // This will update&succeed in case of duplicate keys:
418                        kv_store.insert(
419                            key.to_string().trim().to_string(),
420                            value.to_string().trim().to_string(),
421                        );
422                    } else {
423                        return Err(JoplinReaderError::InvalidFormat {
424                            message: "Invalid property format".to_string(),
425                        });
426                    }
427                }
428                ReadingState::Body => {
429                    // Since we read backwards, we insert the lines into the beginning
430                    body.insert(0, line);
431                }
432            }
433        }
434
435        let type_ = match kv_store.get(&"type_".to_string()) {
436            Some(t) => match t.parse::<i32>() {
437                Ok(t) => JoplinItemType::from(t),
438                Err(_) => {
439                    return Err(JoplinReaderError::InvalidFormat {
440                        message: "Missing required property: `type_`".to_string(),
441                    });
442                }
443            },
444            None => {
445                return Err(JoplinReaderError::InvalidFormat {
446                    message: "Missing required property: `type_`".to_string(),
447                });
448            }
449        };
450
451        if !body.is_empty() && body.len() >= 2 {
452            kv_store.insert("title".to_string(), body.remove(0));
453            body.remove(0); // Because it is title\n\n
454        }
455        if type_ == JoplinItemType::Note {
456            kv_store.insert("body".to_string(), body.join("\n"));
457        }
458
459        Ok(kv_store)
460    }
461
462    /// Reads in a new from a `Path`.
463    pub fn new(note_path: &Path) -> Result<NoteInfo, JoplinReaderError> {
464        let file = match fs::File::open(note_path) {
465            Ok(file) => file,
466            Err(_) => {
467                return Err(JoplinReaderError::FileReadError {
468                    message: "Failed to open file".to_string(),
469                })
470            }
471        };
472        let reader = BufReader::new(file);
473
474        let mut id: Option<String> = None;
475        let mut parent_id: Option<String> = None;
476        let mut type_: Option<JoplinItemType> = None;
477        let mut encryption_cipher_text: Option<String> = None;
478        let mut encryption_applied: Option<i8> = None;
479        let mut updated_time: Option<NaiveDateTime> = None;
480
481        for line in reader.lines() {
482            let line = match line {
483                Ok(line) => line,
484                Err(_) => {
485                    return Err(JoplinReaderError::FileReadError {
486                        message: "Failed to read file".to_string(),
487                    })
488                }
489            };
490            let mut iter = line.splitn(2, ":");
491            let key = iter.next();
492            let value = iter.next();
493            if let (Some(key), Some(value)) = (key, value) {
494                match key {
495                    "id" => id = Some(value.to_string().trim().to_string()),
496                    "parent_id" => parent_id = Some(value.to_string().trim().to_string()),
497                    "type_" => {
498                        if let Ok(t) = value.to_string().trim().parse::<i32>() {
499                            type_ = Some(JoplinItemType::from(t))
500                        } else {
501                            return Err(JoplinReaderError::FileReadError {
502                                message: "Invalid value specified for `type_`".to_string(),
503                            });
504                        }
505                    }
506                    "encryption_applied" => {
507                        if let Ok(ea) = value.to_string().trim().parse::<i8>() {
508                            encryption_applied = Some(ea)
509                        } else {
510                            return Err(JoplinReaderError::FileReadError {
511                                message: "Invalid value specified for `encryption_applied`"
512                                    .to_string(),
513                            });
514                        }
515                    }
516                    "encryption_cipher_text" => {
517                        encryption_cipher_text = Some(value.to_string().trim().to_string())
518                    }
519                    "updated_time" => {
520                        let ut = value.to_string().trim().to_string();
521                        updated_time =
522                            match NaiveDateTime::parse_from_str(&ut, "%Y-%m-%dT%H:%M:%S%.fZ") {
523                                Ok(ut) => Some(ut),
524                                Err(_) => None,
525                            }
526                    }
527                    _ => { /*println!("Unsupported key: {}", key);*/ }
528                };
529            }
530        }
531
532        // Mandatory attributes:
533        if let None = id {
534            return Err(JoplinReaderError::FileReadError {
535                message: "No `id` specified in note".to_string(),
536            });
537        }
538        if let None = encryption_applied {
539            return Err(JoplinReaderError::FileReadError {
540                message: "No `encryption_applied` attribute specified in note".to_string(),
541            });
542        }
543        let encryption_applied = encryption_applied.unwrap();
544        let encryption_applied = match encryption_applied {
545            1 => true,
546            _ => false,
547        };
548        let encryption_key_id = match encryption_applied {
549            true => match NoteInfo::parse_encrypted_header(
550                encryption_cipher_text.clone().unwrap().chars(),
551            ) {
552                Ok(header) => Some(header.master_key_id),
553                Err(_) => {
554                    return Err(JoplinReaderError::FileReadError {
555                        message: "Failed to read the encryption header".to_string(),
556                    });
557                }
558            },
559            _ => None,
560        };
561
562        Ok(NoteInfo {
563            path: note_path.to_path_buf(),
564            id: id.unwrap(),
565            type_: type_.unwrap(),
566            encryption_applied,
567            parent_id,
568            encryption_key_id,
569            updated_time,
570            read_time: None,
571            content: NoteProperties::default(),
572        })
573    }
574
575    pub fn get_id(&self) -> &str {
576        &self.id
577    }
578
579    pub fn is_encrypted(&self) -> bool {
580        self.encryption_applied
581    }
582
583    pub fn get_type_(&self) -> &JoplinItemType {
584        &self.type_
585    }
586
587    pub fn get_parent_id(&self) -> Option<&str> {
588        match &self.parent_id {
589            Some(parent_id) => Some(&parent_id),
590            None => None,
591        }
592    }
593
594    pub fn get_encryption_key_id(&self) -> Option<&str> {
595        match &self.encryption_key_id {
596            Some(encryption_key_id) => Some(&encryption_key_id),
597            None => None,
598        }
599    }
600
601    /// Parses the [`JoplinEncryptionHeader`].
602    /// Spec: https://joplinapp.org/spec/e2ee/
603    fn parse_encrypted_header(
604        mut chars: Chars<'_>,
605    ) -> Result<JoplinEncryptionHeader, JoplinReaderError> {
606        // Header (3 chars): Always 'JED'
607        let mut identifier = String::from("");
608        for _ in 0..3 {
609            if let Some(v) = chars.next() {
610                identifier.push(v);
611            }
612        }
613        if identifier.is_empty() || identifier.len() != 3 {
614            return Err(JoplinReaderError::DecryptionError {
615                message: "Header has invalid size".to_string(),
616            });
617        }
618        if identifier != "JED" {
619            return Err(JoplinReaderError::DecryptionError {
620                message: "Identifier is not 'JED'".to_string(),
621            });
622        }
623        // Version number (2 chars)
624        let mut version = String::from("");
625        for _ in 0..2 {
626            if let Some(v) = chars.next() {
627                version.push(v);
628            }
629        }
630        if version.is_empty() || version.len() != 2 {
631            return Err(JoplinReaderError::DecryptionError {
632                message: "Header has invalid size".to_string(),
633            });
634        }
635        let version = match u8::from_str_radix(&version, 16) {
636            Ok(v) => v,
637            Err(_) => {
638                return Err(JoplinReaderError::DecryptionError {
639                    message: "Version is not a number".to_string(),
640                });
641            }
642        };
643        if version != 1 {
644            return Err(JoplinReaderError::DecryptionError {
645                message: "Invalid version. Needs to be '01'".to_string(),
646            });
647        }
648        // Length (6 chars)
649        let mut length = String::from("");
650        for _ in 0..6 {
651            if let Some(v) = chars.next() {
652                length.push(v);
653            }
654        }
655        if length.is_empty() || length.len() != 6 {
656            return Err(JoplinReaderError::DecryptionError {
657                message: "Header has invalid size".to_string(),
658            });
659        }
660        let length = match u32::from_str_radix(&length, 16) {
661            Ok(v) => v,
662            Err(_) => {
663                return Err(JoplinReaderError::DecryptionError {
664                    message: "Length is not a number".to_string(),
665                });
666            }
667        };
668        if length != 34 {
669            return Err(JoplinReaderError::DecryptionError {
670                message: "Expected length 34: Method + master key id".to_string(),
671            });
672        }
673        // Encryption Method (2 chars)
674        let mut encryption_method = String::from("");
675        for _ in 0..2 {
676            if let Some(v) = chars.next() {
677                encryption_method.push(v);
678            }
679        }
680        if encryption_method.is_empty() || encryption_method.len() != 2 {
681            return Err(JoplinReaderError::DecryptionError {
682                message: "Header has invalid size".to_string(),
683            });
684        }
685        let encryption_method = match u8::from_str_radix(&encryption_method, 16) {
686            Ok(v) => JoplinEncryptionMethod::from(v),
687            Err(_) => {
688                return Err(JoplinReaderError::DecryptionError {
689                    message: "Encryption Method is not a number".to_string(),
690                });
691            }
692        };
693        if encryption_method == JoplinEncryptionMethod::MethodUndefined {
694            return Err(JoplinReaderError::DecryptionError {
695                message: "Unknown decryption method".to_string(),
696            });
697        }
698        // Master key ID (32 chars)
699        let mut master_key_id = String::from("");
700        for _ in 0..32 {
701            if let Some(v) = chars.next() {
702                master_key_id.push(v);
703            }
704        }
705        if master_key_id.is_empty() || master_key_id.len() != 32 {
706            return Err(JoplinReaderError::DecryptionError {
707                message: "Header has invalid size".to_string(),
708            });
709        }
710
711        Ok(JoplinEncryptionHeader {
712            version,
713            length,
714            encryption_method,
715            master_key_id,
716        })
717    }
718
719    fn clean_encoded_ascii(text: String) -> String {
720        let re = Regex::new(r"%([0-9a-fA-F]{2})").unwrap();
721
722        let text = re.replace_all(&text, |caps: &Captures| {
723            let value = caps[0].strip_prefix("%").unwrap();
724            let value = u8::from_str_radix(value, 16).unwrap();
725            let value = value as char;
726            value.to_string()
727        });
728
729        text.to_string()
730    }
731
732    fn clean_encoded_unicode(text: String) -> String {
733        let re = Regex::new(r"%u([0-9a-fA-F]{4})").unwrap();
734
735        let text = re.replace_all(&text, |_caps: &Captures| {
736            // We should do this properly, but it's UTF-16 which gets inserted
737            // by my kindle and I do not really need these values.
738            // The text is more important
739            // let value = caps[0].strip_prefix("%u").unwrap();
740            // let value = u32::from_str_radix(value, 16).unwrap();
741            // let value = char::try_from(value).unwrap();
742            "".to_string()
743        });
744
745        text.to_string()
746    }
747
748    /// Decrypts all chunks one after another and returns the whole `String`
749    /// or breaks on an error.
750    fn decrypt(mut chars: Chars<'_>, encryption_key: &str) -> Result<String, JoplinReaderError> {
751        let mut _chunks_read: u32 = 0;
752        let mut _bytes_read: u32 = 0;
753        let mut body = String::from("");
754        loop {
755            let mut length = String::from("");
756            for _ in 0..6 {
757                if let Some(v) = chars.next() {
758                    length.push(v);
759                }
760            }
761            if length.is_empty() || length.len() != 6 {
762                break;
763            }
764            let length = match u32::from_str_radix(&length, 16) {
765                Ok(v) => v,
766                Err(_) => {
767                    return Err(JoplinReaderError::DecryptionError {
768                        message: "Length is not a number".to_string(),
769                    });
770                }
771            };
772
773            let mut data = String::from("");
774            for _ in 0..length {
775                if let Some(v) = chars.next() {
776                    data.push(v);
777                }
778            }
779            if data.is_empty() || data.len() != length as usize {
780                return Err(JoplinReaderError::UnexpectedEndOfNote);
781            }
782            match decrypt_raw(data, encryption_key.to_string()) {
783                Ok(data) => {
784                    let data = match String::from_utf8(data) {
785                        Ok(data) => data,
786                        Err(_) => {
787                            return Err(JoplinReaderError::DecryptionError {
788                                message: "Message did not contain valid ascii".to_string(),
789                            })
790                        }
791                    };
792                    let data = NoteInfo::clean_encoded_ascii(data);
793                    let data = NoteInfo::clean_encoded_unicode(data);
794                    body.push_str(&data)
795                }
796                Err(_) => {
797                    return Err(JoplinReaderError::DecryptionError {
798                        message: "Error decrypting".to_string(),
799                    })
800                }
801            };
802
803            _bytes_read += length;
804            _chunks_read += 1;
805        }
806        let body = percent_decode_str(&body).decode_utf8_lossy();
807        Ok(body.to_string())
808    }
809
810    /// Reads the content into the `content` attribute of `self`
811    fn read_content(&mut self, encryption_key: Option<&str>) -> Result<(), JoplinReaderError> {
812        let content = match self.is_encrypted() {
813            true => self.read_decrypted(encryption_key),
814            false => self.read_unencrypted(),
815        };
816
817        match content {
818            Ok(content) => {
819                self.content = NoteProperties::from(content);
820                Ok(())
821            }
822            Err(e) => Err(e),
823        }
824    }
825
826    /// Read an unencrypted item and return a [`std::collection::HashMap`]
827    /// with the key value pairs
828    fn read_unencrypted(&self) -> Result<HashMap<String, String>, JoplinReaderError> {
829        let file = match fs::File::open(self.path.clone()) {
830            Ok(file) => file,
831            Err(_) => {
832                return Err(JoplinReaderError::FileReadError {
833                    message: "Failed to open file".to_string(),
834                })
835            }
836        };
837        let reader = BufReader::new(file);
838        let mut text: Vec<String> = Vec::new();
839        // Reverse the order of the lines
840        for line in reader.lines() {
841            let line = line.unwrap();
842            text.insert(0, line);
843        }
844
845        NoteInfo::deserialize(text.iter())
846    }
847
848    /// Read and decrypt an encrypted item and return a
849    /// [`std::collection::HashMap`] with the key value pairs
850    fn read_decrypted(
851        &self,
852        encryption_key: Option<&str>,
853    ) -> Result<HashMap<String, String>, JoplinReaderError> {
854        let encryption_key = match encryption_key {
855            Some(ek) => ek,
856            _ => {
857                return Err(JoplinReaderError::NoEncryptionKey { key: format!("{:?}", encryption_key)});
858            }
859        };
860
861        let file = match fs::File::open(&self.path) {
862            Ok(file) => file,
863            Err(_) => {
864                return Err(JoplinReaderError::FileReadError {
865                    message: "Failed to open file".to_string(),
866                })
867            }
868        };
869        let mut reader = BufReader::new(file);
870        let content = match NoteInfo::parse_encrypted_file(&mut reader) {
871            Ok(content) => content,
872            Err(e) => return Err(e),
873        };
874
875        if let Some(text) = content.get(&"encryption_cipher_text".to_string()) {
876            if !text.is_ascii() {
877                return Err(JoplinReaderError::DecryptionError {
878                    message: "Encrypted text is not ascii".to_string(),
879                });
880            }
881            let mut chars = text.chars();
882            // Skip header
883            for _ in 0..HEADER_SIZE {
884                chars.next();
885            }
886            let plaintext = match NoteInfo::decrypt(chars, encryption_key) {
887                Ok(plaintext) => plaintext,
888                Err(_e) => {
889                    println!("{:?}", _e);
890                    return Err(JoplinReaderError::DecryptionError {
891                        message: "Failed to decrypt SJCL chunks".to_string(),
892                    });
893                }
894            };
895
896            NoteInfo::deserialize(plaintext.lines())
897        } else {
898            Err(JoplinReaderError::NoEncryptionText)
899        }
900    }
901
902    /// The content is only read when not existant or after a certain amount of
903    /// time has passed. That is written into the attributes of `self` and
904    /// returned directly from the body.
905    pub fn read(&mut self, encryption_key: Option<&str>) -> Result<&str, JoplinReaderError> {
906        let reading = match self.read_time {
907            None => self.read_content(encryption_key),
908            Some(t) => {
909                let since_last_refresh = SystemTime::now()
910                    .duration_since(t)
911                    .expect("Time went backwards!")
912                    .as_secs();
913                if since_last_refresh >= REFRESH_INTERVAL {
914                    self.read_content(encryption_key)
915                } else {
916                    Ok(())
917                }
918            }
919        };
920
921        match reading {
922            Ok(_) => match &self.content.body {
923                Some(body) => Ok(body),
924                None => Err(JoplinReaderError::NoText),
925            },
926            Err(e) => Err(e),
927        }
928    }
929}