netscape_bookmark_parser 0.1.4

A Netspace bookmark parser for Rust
Documentation
use serde_json::{json, Value};
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use chrono::{DateTime, Utc};

// Constants and helper functions for timestamp conversion
const LDAP_NT_EPOCH: i64 = 11644473600; // Seconds from 1601-01-01 to 1970-01-01

/// Converts a Unix timestamp to a Windows NT timestamp.
///
/// # Arguments
///
/// * `unix_timestamp` - An i64 representing the Unix timestamp.
///
/// # Returns
///
/// * Returns an i64 representing the Windows NT timestamp.
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 // Convert to 100-nanosecond intervals
}

/// Parses HTML bookmarks file and converts it to a JSON value.
///
/// # Arguments
///
/// * `html_path` - A reference to a Path representing the file path of the HTML bookmarks.
///
/// # Returns
///
/// * Returns an io::Result containing a serde_json::Value representing the parsed bookmarks.
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") {
            // Parse folder
            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": []
            });
            
            // Push current folder to stack and make new folder current
            stack.push(roots.clone());
            roots = folder;
        } else if line.contains("<DT><A") {
            // Parse bookmark
            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>") {
            // End of current folder - pop from stack
            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
    }))
}

/// Main function to run the HTML bookmarks to JSON conversion.
///
/// # Arguments
///
/// * `input` - A string slice representing the input file path of the HTML bookmarks.
/// * `output` - A string slice representing the output directory path for the JSON file.
///
/// # Returns
///
/// * Returns an io::Result indicating success or failure.
pub fn run(input: &str, output: &str) -> io::Result<()> {
    let html_file_path = Path::new(input);
    let json_file_dir = Path::new(output);
    // let html_file_path = Path::new(&std::env::var("USERPROFILE").unwrap())
    //     .join("Documents")
    //     .join("EdgeChromium-Bookmarks.backup.html");
        
    // let json_file_dir = Path::new(&std::env::var("LOCALAPPDATA").unwrap())
    //     .join("Microsoft")
    //     .join("Edge")
    //     .join("User Data")
    //     .join("Default");
        
    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(())
}