readmdict 0.1.0

A Rust implementation for reading MDict dictionary files (.mdx format)
Documentation
//! Command-line interface for rust-readmdict
//! Direct port of readmdict/__main__.py

use clap::{Arg, Command};
use std::path::Path;
use std::process;

mod readmdict;
use readmdict::{Mdx, Mdd, Passcode, Result};

/// Parse passcode from string format
/// Port of passcode parsing from readmdict.py:607-616
fn parse_passcode(passcode_str: &str) -> Option<Passcode> {
    if passcode_str.is_empty() {
        return None;
    }
    
    if let Some(colon_pos) = passcode_str.find(':') {
        let (regcode_hex, userid) = passcode_str.split_at(colon_pos);
        let userid = &userid[1..]; // Skip the ':'
        
        if let Ok(regcode) = hex::decode(regcode_hex) {
            return Some(Passcode {
                regcode,
                userid: userid.to_string(),
            });
        }
    }
    
    // If no colon, treat entire string as hex regcode
    if let Ok(regcode) = hex::decode(passcode_str) {
        Some(Passcode {
            regcode,
            userid: String::new(),
        })
    } else {
        None
    }
}

/// Extract MDX dictionary
/// Port of extract_mdx from readmdict.py:618-635
fn extract_mdx(fname: &str, encoding: Option<String>, substyle: bool, passcode: Option<Passcode>) -> Result<()> {
    println!("Reading MDX file: {}", fname);
    
    let mdx = Mdx::new(fname, encoding, substyle, passcode)?;
    
    println!("Number of entries: {}", mdx.len());
    println!("Header information:");
    for (key, value) in mdx.header() {
        println!("  {}: {}", key, value);
    }
    
    // Extract entries
    println!("Extracting entries...");
    match mdx.items() {
        Ok(items) => {
            for (i, (key, value)) in items.iter().enumerate() {
                println!("Entry {}: {} -> {} bytes", i + 1, 
                    String::from_utf8_lossy(key), value.len());
                if i >= 4 { break; } // Show first 5 entries
            }
        }
        Err(e) => {
            eprintln!("Error reading MDX entries: {}", e);
        }
    }
    
    Ok(())
}

/// Extract MDD dictionary
/// Port of extract_mdd from readmdict.py:637-654
fn extract_mdd(fname: &str, passcode: Option<Passcode>) -> Result<()> {
    println!("Reading MDD file: {}", fname);
    
    let mdd = Mdd::new(fname, passcode)?;
    
    println!("Number of entries: {}", mdd.len());
    println!("Header information:");
    for (key, value) in mdd.header() {
        println!("  {}: {}", key, value);
    }
    
    // Extract entries
    println!("Extracting entries...");
    match mdd.items() {
        Ok(items) => {
            for (i, (key, value)) in items.iter().enumerate() {
                println!("File {}: {} -> {} bytes", i + 1, 
                    String::from_utf8_lossy(key), value.len());
                if i >= 4 { break; } // Show first 5 files
            }
        }
        Err(e) => {
            eprintln!("Error reading MDD entries: {}", e);
        }
    }
    
    Ok(())
}

/// List all keys from MDX dictionary
/// Port of list_keys functionality from readmdict.py
fn list_keys(fname: &str, encoding: Option<String>, substyle: bool, passcode: Option<Passcode>, limit: usize) -> Result<()> {
    println!("Reading MDX file: {}", fname);
    
    let mdx = Mdx::new(fname, encoding, substyle, passcode)?;
    
    println!("Number of entries: {}", mdx.len());
    println!("Keys (showing first {}):", limit);
    
    let keys: Vec<Vec<u8>> = mdx.keys().map(|k| k.to_vec()).collect();
    for (i, key) in keys.iter().take(limit).enumerate() {
        println!("Key {}: {}", i + 1, String::from_utf8_lossy(key));
    }
    
    if keys.len() > limit {
        println!("... and {} more keys", keys.len() - limit);
    }
    
    Ok(())
}

/// List keys from MDX dictionary starting from a specific key
/// Port of list_keys_since functionality from readmdict.py
fn list_keys_since(fname: &str, encoding: Option<String>, substyle: bool, passcode: Option<Passcode>, since_key: &str, limit: usize) -> Result<()> {
    println!("Reading MDX file: {}", fname);
    println!("Listing keys since: {}", since_key);
    
    let mdx = Mdx::new(fname, encoding, substyle, passcode)?;
    
    let keys: Vec<Vec<u8>> = mdx.keys().map(|k| k.to_vec()).collect();
    let since_key_bytes = since_key.as_bytes();
    let mut found = false;
    let mut count = 0;
    let mut total_shown = 0;
    
    for key in keys.iter() {
        if !found && key == since_key_bytes {
            found = true;
        }
        
        if found {
            if total_shown < limit {
                count += 1;
                total_shown += 1;
                println!("Key {}: {}", count, String::from_utf8_lossy(key));
            } else {
                break;
            }
        }
    }
    
    if !found {
        println!("Warning: Key '{}' not found in dictionary", since_key);
    } else {
        let remaining = keys.iter().skip_while(|k| *k != since_key_bytes).count();
        if remaining > limit {
            println!("Showing {} of {} keys starting from '{}'", limit, remaining, since_key);
        } else {
            println!("Total keys listed: {}", total_shown);
        }
    }
    
    Ok(())
}

/// Main function - direct port of __main__.py
/// Port of main logic from readmdict.py:656-690
fn main() {
    let matches = Command::new("rust-readmdict")
        .version("1.0.0")
        .about("A Rust implementation of MDict reader")
        .arg(
            Arg::new("filename")
                .help("MDX/MDD file to read")
                .required(true)
                .index(1),
        )
        .arg(
            Arg::new("encoding")
                .short('e')
                .long("encoding")
                .value_name("ENCODING")
                .help("Text encoding (default: auto-detect)"),
        )
        .arg(
            Arg::new("substyle")
                .short('s')
                .long("substyle")
                .action(clap::ArgAction::SetTrue)
                .help("Substitute style (MDX only)"),
        )
        .arg(
            Arg::new("passcode")
                .short('p')
                .long("passcode")
                .value_name("PASSCODE")
                .help("Passcode for encrypted files (format: regcode:userid or regcode)"),
        )
        .arg(
            Arg::new("list_keys")
                .long("list-keys")
                .action(clap::ArgAction::SetTrue)
                .help("List all keys from MDX file"),
        )
        .arg(
            Arg::new("list_keys_since")
                .long("list-keys-since")
                .value_name("KEY")
                .help("List keys from MDX file starting from specified key"),
        )
        .arg(
            Arg::new("limit")
                .long("limit")
                .short('l')
                .value_name("NUMBER")
                .help("Maximum number of keys to display (default: 1000)")
                .default_value("1000"),
        )
        .get_matches();

    let filename = matches.get_one::<String>("filename").unwrap();
    let encoding = matches.get_one::<String>("encoding").cloned();
    let substyle = matches.get_flag("substyle");
    let passcode = matches
        .get_one::<String>("passcode")
        .and_then(|p| parse_passcode(p));

    // Check if file exists
    if !Path::new(filename).exists() {
        eprintln!("Error: File '{}' not found", filename);
        process::exit(1);
    }

    // Determine operation mode and process accordingly
    let list_keys_flag = matches.get_flag("list_keys");
    let list_keys_since_key = matches.get_one::<String>("list_keys_since");
    let limit = matches
        .get_one::<String>("limit")
        .unwrap()
        .parse::<usize>()
        .unwrap_or(1000);
    
    let result = if filename.to_lowercase().ends_with(".mdx") {
        if list_keys_since_key.is_some() {
            list_keys_since(filename, encoding, substyle, passcode, list_keys_since_key.unwrap(), limit)
        } else if list_keys_flag {
            list_keys(filename, encoding, substyle, passcode, limit)
        } else {
            extract_mdx(filename, encoding, substyle, passcode)
        }
    } else if filename.to_lowercase().ends_with(".mdd") {
        if substyle {
            eprintln!("Warning: --substyle option is ignored for MDD files");
        }
        if list_keys_flag || list_keys_since_key.is_some() {
            eprintln!("Error: --list-keys and --list-keys-since are only supported for MDX files");
            process::exit(1);
        }
        extract_mdd(filename, passcode)
    } else {
        eprintln!("Error: Unsupported file type. Only .mdx and .mdd files are supported.");
        process::exit(1);
    };

    if let Err(e) = result {
        eprintln!("Error: {}", e);
        process::exit(1);
    }
}