use serde_json::{json, Value};
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use chrono::{DateTime, Utc};
const LDAP_NT_EPOCH: i64 = 11644473600;
pub fn convert_to_nt_timestamp(unix_timestamp: i64) -> i64 {
let seconds_since_nt_epoch = unix_timestamp + LDAP_NT_EPOCH;
seconds_since_nt_epoch * 1_000_000 * 10 }
pub fn parse_html_bookmarks(html_path: &Path) -> io::Result<Value> {
let file = fs::File::open(html_path)?;
let reader = io::BufReader::new(file);
let mut roots = json!({
"name": "Bookmarks Bar",
"type": "folder",
"children": []
});
let mut stack: Vec<Value> = Vec::new();
for line in reader.lines() {
let line = line?;
if line.contains("<DT><H3") {
let name = line.split('>').nth(2).and_then(|s| s.split('<').next()).unwrap_or("");
let add_date = line.split("ADD_DATE=\"").nth(1).and_then(|s| s.split('"').next()).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
let last_modified = line.split("LAST_MODIFIED=\"").nth(1).and_then(|s| s.split('"').next()).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
let folder = json!({
"name": name,
"type": "folder",
"date_added": convert_to_nt_timestamp(add_date).to_string(),
"date_modified": convert_to_nt_timestamp(last_modified).to_string(),
"children": []
});
stack.push(roots.clone());
roots = folder;
} else if line.contains("<DT><A") {
let name = line.split('>').nth(2).and_then(|s| s.split('<').next()).unwrap_or("");
let url = line.split("HREF=\"").nth(1).and_then(|s| s.split('"').next()).unwrap_or("");
let add_date = line.split("ADD_DATE=\"").nth(1).and_then(|s| s.split('"').next()).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
let bookmark = json!({
"name": name,
"type": "url",
"url": url,
"date_added": convert_to_nt_timestamp(add_date).to_string()
});
if let Some(children) = roots.get_mut("children") {
if let Some(arr) = children.as_array_mut() {
arr.push(bookmark);
}
}
} else if line.contains("</DL>") {
if let Some(mut parent) = stack.pop() {
if let Some(parent_children) = parent.get_mut("children") {
if let Some(mut arr) = parent_children.as_array_mut() {
arr.push(roots);
}
}
roots = parent;
}
}
}
Ok(json!({
"roots": {
"bookmark_bar": roots,
"other": {
"name": "Other Bookmarks",
"type": "folder",
"children": []
},
"synced": {
"name": "Synced Bookmarks",
"type": "folder",
"children": []
}
},
"version": 1
}))
}
pub fn run(input: &str, output: &str) -> io::Result<()> {
let html_file_path = Path::new(input);
let json_file_dir = Path::new(output);
let exported_time = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let json_file_path = json_file_dir.join(format!("Bookmarks_{}.json", exported_time));
if !html_file_path.exists() {
return Err(io::Error::new(io::ErrorKind::NotFound,
format!("Source file path {:?} does not exist!", html_file_path)));
}
if !json_file_dir.exists() {
return Err(io::Error::new(io::ErrorKind::NotFound,
format!("Destination directory path {:?} does not exist!", json_file_dir)));
}
let json_data = parse_html_bookmarks(&html_file_path)?;
fs::write(json_file_path, json_data.to_string())?;
Ok(())
}