use crate::JoplinReaderError;
use regex::{Captures, Regex};
use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader};
use std::iter::DoubleEndedIterator;
use std::path::{Path, PathBuf};
use std::str::Chars;
use std::time::SystemTime;
use chrono::NaiveDateTime;
use percent_encoding::percent_decode_str;
use sjcl::decrypt_raw;
use serde;
use serde::ser::{Serialize, Serializer, SerializeStruct};
const REFRESH_INTERVAL: u64 = 60 * 60 * 12;
const HEADER_SIZE: u32 = 45;
#[derive(Debug, PartialEq, serde::Serialize)]
pub enum JoplinItemType {
Undefined = 0,
Note = 1,
Folder = 2,
Setting = 3,
Resource = 4,
Tag = 5,
NoteTag = 6,
Search = 7,
Alarm = 8,
MasterKey = 9,
ItemChange = 10,
NoteResource = 11,
ResourceLocalState = 12,
Revision = 13,
Migration = 14,
SmartFilter = 15,
Command = 16,
}
impl From<i32> for JoplinItemType {
fn from(v: i32) -> Self {
match v {
1 => JoplinItemType::Note,
2 => JoplinItemType::Folder,
3 => JoplinItemType::Setting,
4 => JoplinItemType::Resource,
5 => JoplinItemType::Tag,
6 => JoplinItemType::NoteTag,
7 => JoplinItemType::Search,
8 => JoplinItemType::Alarm,
9 => JoplinItemType::MasterKey,
10 => JoplinItemType::ItemChange,
11 => JoplinItemType::NoteResource,
12 => JoplinItemType::ResourceLocalState,
13 => JoplinItemType::Revision,
14 => JoplinItemType::Migration,
15 => JoplinItemType::SmartFilter,
16 => JoplinItemType::Command,
_ => JoplinItemType::Undefined,
}
}
}
#[derive(Debug)]
pub struct NoteInfo {
path: PathBuf,
id: String,
type_: JoplinItemType,
encryption_applied: bool,
parent_id: Option<String>,
encryption_key_id: Option<String>,
updated_time: Option<NaiveDateTime>,
read_time: Option<SystemTime>,
content: NoteProperties,
}
impl Serialize for NoteInfo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("NoteInfo", 9)?;
state.serialize_field("created_time", &self.path)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("type_", &self.type_)?;
state.serialize_field("encryption_applied", &self.encryption_applied)?;
state.serialize_field("parent_id", &self.parent_id)?;
state.serialize_field("encryption_key_id", &self.encryption_key_id)?;
state.serialize_field("updated_time", &self.updated_time.map_or(0, |ut| ut.timestamp()))?;
state.serialize_field("read_time", &self.read_time)?;
state.serialize_field("content", &self.content)?;
state.end()
}
}
#[derive(Debug, Clone)]
pub struct NoteProperties {
title: Option<String>,
body: Option<String>,
created_time: Option<NaiveDateTime>,
altitude: Option<f32>,
latitude: Option<f64>,
longitude: Option<f64>,
author: Option<String>,
source_url: Option<String>,
is_todo: Option<bool>,
todo_due: Option<bool>,
todo_completed: Option<bool>,
source: Option<String>,
source_application: Option<String>,
application_data: Option<String>,
order: Option<i32>,
user_created_time: Option<NaiveDateTime>,
user_updated_time: Option<NaiveDateTime>,
markup_language: Option<String>,
is_shared: Option<bool>,
}
impl Default for NoteProperties {
fn default() -> Self {
Self {
title: None,
body: None,
created_time: None,
altitude: None,
latitude: None,
longitude: None,
author: None,
source_url: None,
is_todo: None,
todo_due: None,
todo_completed: None,
source: None,
source_application: None,
application_data: None,
order: None,
user_created_time: None,
user_updated_time: None,
markup_language: None,
is_shared: None,
}
}
}
impl From<HashMap<String, String>> for NoteProperties {
fn from(mut kv_store: HashMap<String, String>) -> Self {
let mut title: Option<String> = None;
let mut body: Option<String> = None;
let mut created_time: Option<NaiveDateTime> = None;
let mut altitude: Option<f32> = None;
let mut latitude: Option<f64> = None;
let mut longitude: Option<f64> = None;
let mut author: Option<String> = None;
let mut source_url: Option<String> = None;
let mut is_todo: Option<bool> = None;
let mut todo_due: Option<bool> = None;
let mut todo_completed: Option<bool> = None;
let mut source: Option<String> = None;
let mut source_application: Option<String> = None;
let mut application_data: Option<String> = None;
let mut order: Option<i32> = None;
let mut user_created_time: Option<NaiveDateTime> = None;
let mut user_updated_time: Option<NaiveDateTime> = None;
let mut markup_language: Option<String> = None;
let mut is_shared: Option<bool> = None;
for (k, v) in kv_store.drain() {
match k.as_str() {
"title" => title = Some(v),
"body" => body = Some(v),
"created_time" => {
created_time = match NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.fZ")
{
Ok(ut) => Some(ut),
Err(_) => None,
}
}
"altitude" => {
altitude = match v.trim().parse::<f32>() {
Ok(l) => Some(l),
_ => None,
}
}
"latitude" => {
latitude = match v.trim().parse::<f64>() {
Ok(l) => Some(l),
_ => None,
}
}
"longitude" => {
longitude = match v.trim().parse::<f64>() {
Ok(l) => Some(l),
_ => None,
}
}
"author" => author = Some(v),
"source_url" => source_url = Some(v),
"is_todo" => {
is_todo = match v.trim().parse::<i8>() {
Ok(b) => Some(b == 1),
_ => None,
}
}
"todo_due" => {
todo_due = match v.trim().parse::<i8>() {
Ok(b) => Some(b == 1),
_ => None,
}
}
"todo_completed" => {
todo_completed = match v.trim().parse::<i8>() {
Ok(b) => Some(b == 1),
_ => None,
}
}
"source" => source = Some(v),
"source_application" => source_application = Some(v),
"application_data" => application_data = Some(v),
"order" => {
order = match v.trim().parse::<i32>() {
Ok(o) => Some(o),
_ => None,
}
}
"user_created_time" => {
user_created_time =
match NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.fZ") {
Ok(ut) => Some(ut),
Err(_) => None,
}
}
"user_updated_time" => {
user_updated_time =
match NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.fZ") {
Ok(ut) => Some(ut),
Err(_) => None,
}
}
"markup_language" => markup_language = Some(v),
"is_shared" => {
is_shared = match v.trim().parse::<i8>() {
Ok(b) => Some(b == 1),
_ => None,
}
}
_ => { }
}
}
Self {
title,
body,
created_time,
altitude,
latitude,
longitude,
author,
source_url,
is_todo,
todo_due,
todo_completed,
source,
source_application,
application_data,
order,
user_created_time,
user_updated_time,
markup_language,
is_shared,
}
}
}
impl Serialize for NoteProperties {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("NoteProperties", 19)?;
state.serialize_field("title", &self.title.as_ref().unwrap())?;
state.serialize_field("body", &self.body.as_ref().unwrap())?;
state.serialize_field("created_time", &self.created_time.as_ref().unwrap().timestamp())?;
state.serialize_field("altitude", &self.altitude.as_ref().unwrap())?;
state.serialize_field("latitude", &self.latitude.as_ref().unwrap())?;
state.serialize_field("longitude", &self.longitude.as_ref().unwrap())?;
state.serialize_field("author", &self.author.as_ref().unwrap())?;
state.serialize_field("source_url", &self.source_url.as_ref().unwrap())?;
state.serialize_field("is_todo", &self.is_todo.as_ref().unwrap())?;
state.serialize_field("todo_due", &self.todo_due.as_ref().unwrap())?;
state.serialize_field("todo_completed", &self.todo_completed.as_ref().unwrap())?;
state.serialize_field("source", &self.source.as_ref().unwrap())?;
state.serialize_field("source_application", &self.source_application.as_ref().unwrap())?;
state.serialize_field("application_data", &self.application_data.as_ref().unwrap())?;
state.serialize_field("order", &self.order.as_ref().unwrap())?;
state.serialize_field("user_created_time", &self.user_created_time.as_ref().unwrap().timestamp())?;
state.serialize_field("user_updated_time", &self.user_updated_time.as_ref().unwrap().timestamp())?;
state.serialize_field("markup_language", &self.markup_language.as_ref().unwrap())?;
state.serialize_field("is_shared", &self.is_shared.as_ref().unwrap())?;
state.end()
}
}
#[derive(Debug)]
struct JoplinEncryptionHeader {
version: u8,
length: u32,
encryption_method: JoplinEncryptionMethod,
master_key_id: String,
}
#[derive(Debug, PartialEq)]
pub enum JoplinEncryptionMethod {
MethodUndefined = 0x0,
MethodSjcl = 0x1,
MethodSjcl2 = 0x2,
MethodSjcl3 = 0x3,
MethodSjcl4 = 0x4,
MethodSjcl1a = 0x5,
}
impl From<u8> for JoplinEncryptionMethod {
fn from(v: u8) -> Self {
match v {
0x1 => JoplinEncryptionMethod::MethodSjcl,
0x2 => JoplinEncryptionMethod::MethodSjcl2,
0x3 => JoplinEncryptionMethod::MethodSjcl3,
0x4 => JoplinEncryptionMethod::MethodSjcl4,
0x5 => JoplinEncryptionMethod::MethodSjcl1a,
_ => JoplinEncryptionMethod::MethodUndefined,
}
}
}
impl NoteInfo {
fn parse_encrypted_file<R: BufRead>(
reader: &mut R,
) -> Result<HashMap<String, String>, JoplinReaderError> {
let mut kv_store: HashMap<String, String> = HashMap::new();
for line in reader.lines() {
let line = match line {
Ok(line) => line,
Err(_) => {
return Err(JoplinReaderError::FileReadError {
message: "Failed to read file".to_string(),
})
}
};
let mut iter = line.splitn(2, ":");
let key = iter.next();
let value = iter.next();
if let (Some(key), Some(value)) = (key, value) {
kv_store.insert(
key.to_string().trim().to_string(),
value.to_string().trim().to_string(),
);
}
}
Ok(kv_store)
}
fn deserialize(
text: impl DoubleEndedIterator<Item = impl AsRef<str>>,
) -> Result<HashMap<String, String>, JoplinReaderError> {
let mut kv_store: HashMap<String, String> = HashMap::new();
let mut body: Vec<String> = Vec::new();
enum ReadingState {
Props,
Body,
}
let mut state: ReadingState = ReadingState::Props;
for line in text.rev() {
let line = line.as_ref().trim().to_string();
match state {
ReadingState::Props => {
if line.is_empty() {
state = ReadingState::Body;
continue;
}
let mut iter = line.splitn(2, ":");
let key = iter.next();
let value = iter.next();
if let (Some(key), Some(value)) = (key, value) {
kv_store.insert(
key.to_string().trim().to_string(),
value.to_string().trim().to_string(),
);
} else {
return Err(JoplinReaderError::InvalidFormat {
message: "Invalid property format".to_string(),
});
}
}
ReadingState::Body => {
body.insert(0, line);
}
}
}
let type_ = match kv_store.get(&"type_".to_string()) {
Some(t) => match t.parse::<i32>() {
Ok(t) => JoplinItemType::from(t),
Err(_) => {
return Err(JoplinReaderError::InvalidFormat {
message: "Missing required property: `type_`".to_string(),
});
}
},
None => {
return Err(JoplinReaderError::InvalidFormat {
message: "Missing required property: `type_`".to_string(),
});
}
};
if !body.is_empty() && body.len() >= 2 {
kv_store.insert("title".to_string(), body.remove(0));
body.remove(0);
}
if type_ == JoplinItemType::Note {
kv_store.insert("body".to_string(), body.join("\n"));
}
Ok(kv_store)
}
pub fn new(note_path: &Path) -> Result<NoteInfo, JoplinReaderError> {
let file = match fs::File::open(note_path) {
Ok(file) => file,
Err(_) => {
return Err(JoplinReaderError::FileReadError {
message: "Failed to open file".to_string(),
})
}
};
let reader = BufReader::new(file);
let mut id: Option<String> = None;
let mut parent_id: Option<String> = None;
let mut type_: Option<JoplinItemType> = None;
let mut encryption_cipher_text: Option<String> = None;
let mut encryption_applied: Option<i8> = None;
let mut updated_time: Option<NaiveDateTime> = None;
for line in reader.lines() {
let line = match line {
Ok(line) => line,
Err(_) => {
return Err(JoplinReaderError::FileReadError {
message: "Failed to read file".to_string(),
})
}
};
let mut iter = line.splitn(2, ":");
let key = iter.next();
let value = iter.next();
if let (Some(key), Some(value)) = (key, value) {
match key {
"id" => id = Some(value.to_string().trim().to_string()),
"parent_id" => parent_id = Some(value.to_string().trim().to_string()),
"type_" => {
if let Ok(t) = value.to_string().trim().parse::<i32>() {
type_ = Some(JoplinItemType::from(t))
} else {
return Err(JoplinReaderError::FileReadError {
message: "Invalid value specified for `type_`".to_string(),
});
}
}
"encryption_applied" => {
if let Ok(ea) = value.to_string().trim().parse::<i8>() {
encryption_applied = Some(ea)
} else {
return Err(JoplinReaderError::FileReadError {
message: "Invalid value specified for `encryption_applied`"
.to_string(),
});
}
}
"encryption_cipher_text" => {
encryption_cipher_text = Some(value.to_string().trim().to_string())
}
"updated_time" => {
let ut = value.to_string().trim().to_string();
updated_time =
match NaiveDateTime::parse_from_str(&ut, "%Y-%m-%dT%H:%M:%S%.fZ") {
Ok(ut) => Some(ut),
Err(_) => None,
}
}
_ => { }
};
}
}
if let None = id {
return Err(JoplinReaderError::FileReadError {
message: "No `id` specified in note".to_string(),
});
}
if let None = encryption_applied {
return Err(JoplinReaderError::FileReadError {
message: "No `encryption_applied` attribute specified in note".to_string(),
});
}
let encryption_applied = encryption_applied.unwrap();
let encryption_applied = match encryption_applied {
1 => true,
_ => false,
};
let encryption_key_id = match encryption_applied {
true => match NoteInfo::parse_encrypted_header(
encryption_cipher_text.clone().unwrap().chars(),
) {
Ok(header) => Some(header.master_key_id),
Err(_) => {
return Err(JoplinReaderError::FileReadError {
message: "Failed to read the encryption header".to_string(),
});
}
},
_ => None,
};
Ok(NoteInfo {
path: note_path.to_path_buf(),
id: id.unwrap(),
type_: type_.unwrap(),
encryption_applied,
parent_id,
encryption_key_id,
updated_time,
read_time: None,
content: NoteProperties::default(),
})
}
pub fn get_id(&self) -> &str {
&self.id
}
pub fn is_encrypted(&self) -> bool {
self.encryption_applied
}
pub fn get_type_(&self) -> &JoplinItemType {
&self.type_
}
pub fn get_parent_id(&self) -> Option<&str> {
match &self.parent_id {
Some(parent_id) => Some(&parent_id),
None => None,
}
}
pub fn get_encryption_key_id(&self) -> Option<&str> {
match &self.encryption_key_id {
Some(encryption_key_id) => Some(&encryption_key_id),
None => None,
}
}
fn parse_encrypted_header(
mut chars: Chars<'_>,
) -> Result<JoplinEncryptionHeader, JoplinReaderError> {
let mut identifier = String::from("");
for _ in 0..3 {
if let Some(v) = chars.next() {
identifier.push(v);
}
}
if identifier.is_empty() || identifier.len() != 3 {
return Err(JoplinReaderError::DecryptionError {
message: "Header has invalid size".to_string(),
});
}
if identifier != "JED" {
return Err(JoplinReaderError::DecryptionError {
message: "Identifier is not 'JED'".to_string(),
});
}
let mut version = String::from("");
for _ in 0..2 {
if let Some(v) = chars.next() {
version.push(v);
}
}
if version.is_empty() || version.len() != 2 {
return Err(JoplinReaderError::DecryptionError {
message: "Header has invalid size".to_string(),
});
}
let version = match u8::from_str_radix(&version, 16) {
Ok(v) => v,
Err(_) => {
return Err(JoplinReaderError::DecryptionError {
message: "Version is not a number".to_string(),
});
}
};
if version != 1 {
return Err(JoplinReaderError::DecryptionError {
message: "Invalid version. Needs to be '01'".to_string(),
});
}
let mut length = String::from("");
for _ in 0..6 {
if let Some(v) = chars.next() {
length.push(v);
}
}
if length.is_empty() || length.len() != 6 {
return Err(JoplinReaderError::DecryptionError {
message: "Header has invalid size".to_string(),
});
}
let length = match u32::from_str_radix(&length, 16) {
Ok(v) => v,
Err(_) => {
return Err(JoplinReaderError::DecryptionError {
message: "Length is not a number".to_string(),
});
}
};
if length != 34 {
return Err(JoplinReaderError::DecryptionError {
message: "Expected length 34: Method + master key id".to_string(),
});
}
let mut encryption_method = String::from("");
for _ in 0..2 {
if let Some(v) = chars.next() {
encryption_method.push(v);
}
}
if encryption_method.is_empty() || encryption_method.len() != 2 {
return Err(JoplinReaderError::DecryptionError {
message: "Header has invalid size".to_string(),
});
}
let encryption_method = match u8::from_str_radix(&encryption_method, 16) {
Ok(v) => JoplinEncryptionMethod::from(v),
Err(_) => {
return Err(JoplinReaderError::DecryptionError {
message: "Encryption Method is not a number".to_string(),
});
}
};
if encryption_method == JoplinEncryptionMethod::MethodUndefined {
return Err(JoplinReaderError::DecryptionError {
message: "Unknown decryption method".to_string(),
});
}
let mut master_key_id = String::from("");
for _ in 0..32 {
if let Some(v) = chars.next() {
master_key_id.push(v);
}
}
if master_key_id.is_empty() || master_key_id.len() != 32 {
return Err(JoplinReaderError::DecryptionError {
message: "Header has invalid size".to_string(),
});
}
Ok(JoplinEncryptionHeader {
version,
length,
encryption_method,
master_key_id,
})
}
fn clean_encoded_ascii(text: String) -> String {
let re = Regex::new(r"%([0-9a-fA-F]{2})").unwrap();
let text = re.replace_all(&text, |caps: &Captures| {
let value = caps[0].strip_prefix("%").unwrap();
let value = u8::from_str_radix(value, 16).unwrap();
let value = value as char;
value.to_string()
});
text.to_string()
}
fn clean_encoded_unicode(text: String) -> String {
let re = Regex::new(r"%u([0-9a-fA-F]{4})").unwrap();
let text = re.replace_all(&text, |_caps: &Captures| {
"".to_string()
});
text.to_string()
}
fn decrypt(mut chars: Chars<'_>, encryption_key: &str) -> Result<String, JoplinReaderError> {
let mut _chunks_read: u32 = 0;
let mut _bytes_read: u32 = 0;
let mut body = String::from("");
loop {
let mut length = String::from("");
for _ in 0..6 {
if let Some(v) = chars.next() {
length.push(v);
}
}
if length.is_empty() || length.len() != 6 {
break;
}
let length = match u32::from_str_radix(&length, 16) {
Ok(v) => v,
Err(_) => {
return Err(JoplinReaderError::DecryptionError {
message: "Length is not a number".to_string(),
});
}
};
let mut data = String::from("");
for _ in 0..length {
if let Some(v) = chars.next() {
data.push(v);
}
}
if data.is_empty() || data.len() != length as usize {
return Err(JoplinReaderError::UnexpectedEndOfNote);
}
match decrypt_raw(data, encryption_key.to_string()) {
Ok(data) => {
let data = match String::from_utf8(data) {
Ok(data) => data,
Err(_) => {
return Err(JoplinReaderError::DecryptionError {
message: "Message did not contain valid ascii".to_string(),
})
}
};
let data = NoteInfo::clean_encoded_ascii(data);
let data = NoteInfo::clean_encoded_unicode(data);
body.push_str(&data)
}
Err(_) => {
return Err(JoplinReaderError::DecryptionError {
message: "Error decrypting".to_string(),
})
}
};
_bytes_read += length;
_chunks_read += 1;
}
let body = percent_decode_str(&body).decode_utf8_lossy();
Ok(body.to_string())
}
fn read_content(&mut self, encryption_key: Option<&str>) -> Result<(), JoplinReaderError> {
let content = match self.is_encrypted() {
true => self.read_decrypted(encryption_key),
false => self.read_unencrypted(),
};
match content {
Ok(content) => {
self.content = NoteProperties::from(content);
Ok(())
}
Err(e) => Err(e),
}
}
fn read_unencrypted(&self) -> Result<HashMap<String, String>, JoplinReaderError> {
let file = match fs::File::open(self.path.clone()) {
Ok(file) => file,
Err(_) => {
return Err(JoplinReaderError::FileReadError {
message: "Failed to open file".to_string(),
})
}
};
let reader = BufReader::new(file);
let mut text: Vec<String> = Vec::new();
for line in reader.lines() {
let line = line.unwrap();
text.insert(0, line);
}
NoteInfo::deserialize(text.iter())
}
fn read_decrypted(
&self,
encryption_key: Option<&str>,
) -> Result<HashMap<String, String>, JoplinReaderError> {
let encryption_key = match encryption_key {
Some(ek) => ek,
_ => {
return Err(JoplinReaderError::NoEncryptionKey { key: format!("{:?}", encryption_key)});
}
};
let file = match fs::File::open(&self.path) {
Ok(file) => file,
Err(_) => {
return Err(JoplinReaderError::FileReadError {
message: "Failed to open file".to_string(),
})
}
};
let mut reader = BufReader::new(file);
let content = match NoteInfo::parse_encrypted_file(&mut reader) {
Ok(content) => content,
Err(e) => return Err(e),
};
if let Some(text) = content.get(&"encryption_cipher_text".to_string()) {
if !text.is_ascii() {
return Err(JoplinReaderError::DecryptionError {
message: "Encrypted text is not ascii".to_string(),
});
}
let mut chars = text.chars();
for _ in 0..HEADER_SIZE {
chars.next();
}
let plaintext = match NoteInfo::decrypt(chars, encryption_key) {
Ok(plaintext) => plaintext,
Err(_e) => {
println!("{:?}", _e);
return Err(JoplinReaderError::DecryptionError {
message: "Failed to decrypt SJCL chunks".to_string(),
});
}
};
NoteInfo::deserialize(plaintext.lines())
} else {
Err(JoplinReaderError::NoEncryptionText)
}
}
pub fn read(&mut self, encryption_key: Option<&str>) -> Result<&str, JoplinReaderError> {
let reading = match self.read_time {
None => self.read_content(encryption_key),
Some(t) => {
let since_last_refresh = SystemTime::now()
.duration_since(t)
.expect("Time went backwards!")
.as_secs();
if since_last_refresh >= REFRESH_INTERVAL {
self.read_content(encryption_key)
} else {
Ok(())
}
}
};
match reading {
Ok(_) => match &self.content.body {
Some(body) => Ok(body),
None => Err(JoplinReaderError::NoText),
},
Err(e) => Err(e),
}
}
}