use std::fs::File;
use std::io;
use std::io::IsTerminal;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use anyhow::anyhow;
use anyhow::Result;
use clap::{Args, Parser};
use log::*;
use serde_json::{to_string, to_writer_pretty};
use shared_mime::FileQuery;
use shared_mime::MimeDB;
use stderrlog::StdErrLog;
use shared_mime::load_mime_db as load_xdg_mime_db;
use shared_mime::runtime::mimeinfo::load_xdg_mime_info;
use shared_mime::runtime::parse_mime_package;
use shared_mime::runtime::xdg_mime_search_dirs;
#[cfg(feature = "embedded")]
use shared_mime_embedded::{embedded_mime_db, load_mime_db as load_joint_mime_db};
#[derive(Parser)]
#[command()]
pub struct CLI {
#[command(flatten)]
action: MIMEActions,
#[arg(short = 'p', long = "package")]
pkg_files: Vec<PathBuf>,
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short = 'q', long = "quiet")]
quiet: bool,
#[arg(short = 'o', long = "output")]
output: Option<PathBuf>,
#[arg(long = "json")]
json: bool,
#[arg(long = "no-runtime")]
no_runtime: bool,
#[arg(long = "no-embedded")]
no_embedded: bool,
}
#[derive(Args)]
#[group(multiple = false, required = true)]
pub struct MIMEActions {
#[arg(long = "list-dirs")]
list_dirs: bool,
#[arg(long = "compile")]
compile: bool,
#[arg(long = "dump")]
dump: bool,
#[arg(short = 'I', long = "type-info")]
type_info: Option<String>,
#[arg(short = 'T', long = "type-of")]
type_of: Option<PathBuf>,
}
fn main() -> Result<()> {
let cli = CLI::parse();
StdErrLog::new()
.verbosity(if cli.quiet {
1
} else {
cli.verbose as usize + 2
})
.init()
.expect("log setup error");
info!("CLI launching");
if cli.action.list_dirs {
cli.list_dirs()
} else if cli.action.compile {
cli.compile()
} else if cli.action.dump {
cli.dump()
} else if let Some(path) = &cli.action.type_of {
cli.type_of(path)
} else if let Some(typ) = &cli.action.type_info {
cli.type_info(typ)
} else {
error!("no specified action");
exit(2)
}
}
impl CLI {
fn load_db(&self) -> Result<MimeDB> {
if self.no_embedded && self.no_runtime {
warn!("no XDG source specified");
return Ok(MimeDB::new());
}
#[cfg(feature = "embedded")]
if self.no_runtime {
info!("loading embedded MIME database");
return Ok(embedded_mime_db());
} else if !self.no_embedded {
info!("loading joint MIME database");
return Ok(load_joint_mime_db()?);
}
info!("loading runtime MIME database");
return Ok(load_xdg_mime_db()?);
}
fn list_dirs(&self) -> Result<()> {
for dir in xdg_mime_search_dirs() {
println!("{}", dir.display());
}
Ok(())
}
fn compile(&self) -> Result<()> {
if self.pkg_files.len() != 1 {
error!("--compile must have exactly one package");
exit(2)
}
let file = &self.pkg_files[0];
let pkg = parse_mime_package(file)?;
let records = pkg.into_records();
if self.json {
info!("compiling to JSON");
let mut out = self.open_text_output()?;
for rec in records {
writeln!(out, "{}", to_string(&rec)?)?;
}
} else {
info!("compiling to compressed binary");
let mut out = self.open_bin_output()?;
postcard::to_io(&records, &mut out)?;
}
Ok(())
}
fn dump(&self) -> Result<()> {
info!("loading XDG mime info");
let db = load_xdg_mime_info()?;
let out = self.open_text_output()?;
if self.json {
to_writer_pretty(out, &db.directories)?;
} else {
for dir in db.directories {
println!(
"directory {} ({} packages):",
dir.path.display(),
dir.packages.len()
);
for pkg in dir.packages {
println!(" package {} ({} types):", pkg.filename, pkg.types.len());
for t in pkg.types {
println!(" - {:?}", t)
}
}
}
}
Ok(())
}
fn open_text_output(&self) -> Result<Box<dyn Write>> {
let out: Box<dyn Write> = if let Some(op) = &self.output {
Box::new(
File::options()
.write(true)
.create(true)
.truncate(true)
.open(op)?,
)
} else {
Box::new(std::io::stdout())
};
Ok(out)
}
fn open_bin_output(&self) -> Result<Box<dyn Write>> {
let out: Box<dyn Write> = if let Some(op) = &self.output {
Box::new(
File::options()
.write(true)
.create(true)
.truncate(true)
.open(op)?,
)
} else if io::stdout().is_terminal() {
error!("standard output is a terminal, refusing to write binary");
return Err(anyhow!("terminals do not get binary output"));
} else {
Box::new(std::io::stdout())
};
Ok(out)
}
fn type_info(&self, name: &str) -> Result<()> {
let db = self.load_db()?;
info!("looking up type information for {}", name);
if let Some(desc) = db.description(name) {
println!("description: {}", desc);
}
let aliases = db.aliases(name);
if !aliases.is_empty() {
println!("aliases: {}", aliases.join(", "))
}
let parents = db.parents(name);
if !parents.is_empty() {
println!("parents: {}", parents.join(", "))
}
println!("supertypes:");
for typ in db.supertypes(name) {
println!("- {}", typ);
}
Ok(())
}
fn type_of(&self, path: &Path) -> Result<()> {
let db = self.load_db()?;
info!("looking up type for {}", path.display());
let query = FileQuery::for_path(path)?;
let ans = db.query(&query)?;
let all = ans.all_types();
if let Some(mt) = ans.best() {
println!("{}: {}", path.display(), mt);
if all.len() > 1 {
info!("file has {} other types", all.len() - 1);
}
} else if ans.is_unknown() {
error!("{}: unknown type", path.display());
} else if ans.is_ambiguous() {
warn!("{}: ambiguous type", path.display());
for mt in all {
println!("{}: {}", path.display(), mt);
}
}
Ok(())
}
}