use clap::{Arg, Command};
use std::path::Path;
use std::process;
mod readmdict;
use readmdict::{Mdx, Mdd, Passcode, Result};
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..];
if let Ok(regcode) = hex::decode(regcode_hex) {
return Some(Passcode {
regcode,
userid: userid.to_string(),
});
}
}
if let Ok(regcode) = hex::decode(passcode_str) {
Some(Passcode {
regcode,
userid: String::new(),
})
} else {
None
}
}
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);
}
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; } }
}
Err(e) => {
eprintln!("Error reading MDX entries: {}", e);
}
}
Ok(())
}
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);
}
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; } }
}
Err(e) => {
eprintln!("Error reading MDD entries: {}", e);
}
}
Ok(())
}
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(())
}
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(())
}
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));
if !Path::new(filename).exists() {
eprintln!("Error: File '{}' not found", filename);
process::exit(1);
}
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);
}
}