From 325a22d19bdf86a055925d0cc057e1af5204faac Mon Sep 17 00:00:00 2001 From: eatradish Date: Sat, 14 Sep 2024 23:38:37 +0800 Subject: [PATCH] feat(oma-ubuntu-cmd-not-found): init for ubuntu command-not-found query --- Cargo.lock | 10 +++ Cargo.toml | 5 +- oma-ubuntu-cmd-not-found/Cargo.toml | 10 +++ oma-ubuntu-cmd-not-found/src/lib.rs | 107 ++++++++++++++++++++++++++++ src/error.rs | 10 +++ src/subcommand/command_not_found.rs | 93 ++++++++++++++++++++++-- 6 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 oma-ubuntu-cmd-not-found/Cargo.toml create mode 100644 oma-ubuntu-cmd-not-found/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 820b29981..c48bf554b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3025,6 +3025,7 @@ dependencies = [ "oma-refresh", "oma-repo-verify", "oma-topics", + "oma-ubuntu-cmd-not-found", "oma-utils 0.8.6", "ratatui", "reqwest", @@ -3248,6 +3249,15 @@ dependencies = [ "url", ] +[[package]] +name = "oma-ubuntu-cmd-not-found" +version = "0.1.0" +dependencies = [ + "rusqlite", + "thiserror", + "tracing", +] + [[package]] name = "oma-utils" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 8e5f3254b..f904424c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ oma-fetch = { path = "./oma-fetch", default-features = false } oma-topics = { path = "./oma-topics", optional = true, default-features = false } oma-history = { path = "./oma-history" } oma-repo-verify = { path = "./oma-repo-verify" } +oma-ubuntu-cmd-not-found = { path = "./oma-ubuntu-cmd-not-found", optional = true } # i18n i18n-embed = { version = "0.15.0", features = ["fluent-system", "desktop-requester"]} @@ -70,13 +71,14 @@ sequoia-openssl-backend = ["oma-refresh/sequoia-openssl-backend"] sequoia-nettle-backend = ["oma-refresh/sequoia-nettle-backend"] egg = ["dep:colored", "dep:image"] tokio-console = ["dep:console-subscriber"] +ubuntu = ["dep:oma-ubuntu-cmd-not-found"] rustls = ["reqwest/rustls-tls", "oma-fetch/rustls", "oma-refresh/rustls", "oma-topics/rustls"] openssl = ["reqwest/native-tls", "oma-fetch/native-tls", "oma-refresh/native-tls", "oma-topics/native-tls"] generic = ["sequoia-nettle-backend", "rustls"] default = ["aosc", "generic"] [workspace] -members = ["oma-contents", "oma-console", "oma-topics", "oma-fetch", "oma-refresh", "oma-utils", "oma-pm", "oma-history", "oma-pm-operation-type", "oma-repo-verify"] +members = ["oma-contents", "oma-console", "oma-topics", "oma-fetch", "oma-refresh", "oma-utils", "oma-pm", "oma-history", "oma-pm-operation-type", "oma-repo-verify", "oma-ubuntu-cmd-not-found"] [package.metadata.deb] copyright = "2024, AOSC Dev " @@ -104,3 +106,4 @@ default-features = false lto = "thin" opt-level = 3 codegen-units = 1 +debug = 2 diff --git a/oma-ubuntu-cmd-not-found/Cargo.toml b/oma-ubuntu-cmd-not-found/Cargo.toml new file mode 100644 index 000000000..a2ad381af --- /dev/null +++ b/oma-ubuntu-cmd-not-found/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "oma-ubuntu-cmd-not-found" +version = "0.1.0" +edition = "2021" +description = "Ubuntu command-not-found database handling library" + +[dependencies] +rusqlite = { version = "0.32", features = ["bundled"] } +tracing = "0.1" +thiserror = "1.0" diff --git a/oma-ubuntu-cmd-not-found/src/lib.rs b/oma-ubuntu-cmd-not-found/src/lib.rs new file mode 100644 index 000000000..9f84a1b64 --- /dev/null +++ b/oma-ubuntu-cmd-not-found/src/lib.rs @@ -0,0 +1,107 @@ +use rusqlite::Connection; +pub use rusqlite::Error; +use std::path::Path; + +pub struct UbuntuCmdNotFound { + db: Connection, +} + +impl UbuntuCmdNotFound { + const DEFAULT_DB_PATH: &str = "/var/lib/command-not-found/commands.db"; + pub fn new(db: impl AsRef) -> Result { + let db = Connection::open(db)?; + + Ok(Self { db }) + } + + pub fn default_new() -> Result { + Self::new(Self::DEFAULT_DB_PATH) + } + + pub fn query_where_command_like(&self, query: &str) -> Result, Error> { + let mut stmt = self + .db + .prepare("SELECT pkgID, command FROM commands WHERE command LIKE ?1")?; + let query_str = format!("{}%", query); + let res_iter = stmt.query_map([query_str], |row| { + let pkg_id: i64 = row.get(0)?; + let cmd: String = row.get(1)?; + + Ok((pkg_id, cmd)) + })?; + let mut res = vec![]; + for i in res_iter { + let (pkg_id, cmd) = i?; + let pkgs = self.get_pkg_from_from_pkg_id(pkg_id)?; + + for pkg in pkgs { + res.push((pkg, cmd.clone())); + } + } + Ok(res) + } + + pub fn query_where_command_count(&self, query: &str) -> Result { + let mut stmt = self + .db + .prepare("SELECT COUNT(command) AS count FROM commands WHERE command = ?1")?; + let mut res_iter = stmt.query_map([query], |row| row.get(0))?; + + if let Some(Ok(n)) = res_iter.next() { + return Ok(n); + } + + Ok(0) + } + + pub fn query_where_command_like_count(&self, query: &str) -> Result { + let mut stmt = self + .db + .prepare("SELECT COUNT(command) AS count FROM commands WHERE command LIKE ?")?; + let query_str = format!("{}%", query); + let mut res_iter = stmt.query_map([query_str], |row| row.get(0))?; + + if let Some(Ok(n)) = res_iter.next() { + return Ok(n); + } + + Ok(0) + } + + pub fn query_where_command(&self, query: &str) -> Result, Error> { + let mut stmt = self + .db + .prepare("SELECT pkgID, command FROM commands WHERE command = ?1")?; + let res_iter = stmt.query_map([query], |row| { + let pkg_id: i64 = row.get(0)?; + let cmd: String = row.get(1)?; + + Ok((pkg_id, cmd)) + })?; + let mut res = vec![]; + for i in res_iter { + let (pkg_id, cmd) = i?; + let pkgs = self.get_pkg_from_from_pkg_id(pkg_id)?; + + for pkg in pkgs { + res.push((pkg, cmd.clone())); + } + } + Ok(res) + } + + fn get_pkg_from_from_pkg_id(&self, pkg_id: i64) -> Result, rusqlite::Error> { + let mut res = vec![]; + let mut stmt = self + .db + .prepare("SELECT name FROM packages where pkgID = ?1")?; + let pkgs = stmt.query_map([pkg_id], |row| row.get(0))?; + + for pkg in pkgs { + let pkg = pkg?; + res.push(pkg); + } + + Ok(res) + } +} diff --git a/src/error.rs b/src/error.rs index 449aab155..3a08a2501 100644 --- a/src/error.rs +++ b/src/error.rs @@ -607,6 +607,16 @@ impl From for OutputError { } } +#[cfg(feature = "ubuntu")] +impl From for OutputError { + fn from(value: oma_ubuntu_cmd_not_found::Error) -> Self { + Self { + description: value.to_string(), + source: Some(Box::new(value)), + } + } +} + pub fn oma_apt_error_to_output(err: OmaAptError) -> OutputError { debug!("{:?}", err); match err { diff --git a/src/subcommand/command_not_found.rs b/src/subcommand/command_not_found.rs index 2fa5ebfc9..7fe75914c 100644 --- a/src/subcommand/command_not_found.rs +++ b/src/subcommand/command_not_found.rs @@ -1,11 +1,7 @@ -use std::error::Error; use std::io::stdout; use ahash::AHashMap; -use oma_console::due_to; use oma_console::print::Action; -use oma_contents::searcher::{pure_search, ripgrep_search, Mode}; -use oma_contents::OmaContentsError; use oma_pm::apt::{AptConfig, OmaApt, OmaAptArgs}; use oma_pm::format_description; use tracing::error; @@ -14,12 +10,19 @@ use crate::error::OutputError; use crate::table::PagerPrinter; use crate::{color_formatter, fl}; -const FILTER_JARO_NUM: u8 = 204; -const APT_LIST_PATH: &str = "/var/lib/apt/lists"; - +#[cfg(not(feature = "ubuntu"))] type IndexSet = indexmap::IndexSet; +#[cfg(not(feature = "ubuntu"))] pub fn execute(query: &str) -> Result { + use oma_console::due_to; + use oma_contents::searcher::{pure_search, ripgrep_search, Mode}; + use oma_contents::OmaContentsError; + use std::error::Error; + + const FILTER_JARO_NUM: u8 = 204; + const APT_LIST_PATH: &str = "/var/lib/apt/lists"; + let mut res = IndexSet::with_hasher(ahash::RandomState::new()); let cb = |line| { @@ -136,6 +139,82 @@ pub fn execute(query: &str) -> Result { Ok(127) } +#[cfg(feature = "ubuntu")] +pub fn execute(query: &str) -> Result { + use oma_ubuntu_cmd_not_found::UbuntuCmdNotFound; + + let cnf = UbuntuCmdNotFound::default_new()?; + let count = cnf.query_where_command_count(query)?; + + let query_res = if count != 0 { + cnf.query_where_command(query)? + } else if cnf.query_where_command_like_count(query)? == 0 { + error!("{}", fl!("command-not-found", kw = query)); + return Ok(127); + } else { + cnf.query_where_command_like(query)? + }; + + let mut too_many = false; + let mut map: AHashMap = AHashMap::new(); + let apt_config = AptConfig::new(); + let oma_apt_args = OmaAptArgs::builder().build(); + let apt = OmaApt::new(vec![], oma_apt_args, false, apt_config)?; + let mut res = vec![]; + + for (i, (pkg, cmd)) in query_res.iter().enumerate() { + if i == 10 { + too_many = true; + break; + } + + let desc = if let Some(desc) = map.get(pkg) { + desc.to_string() + } else if let Some(pkg) = apt.cache.get(&pkg) { + let desc = pkg + .candidate() + .and_then(|x| { + x.description() + .map(|x| format_description(&x).0.to_string()) + }) + .unwrap_or_else(|| "no description.".to_string()); + + map.insert(pkg.name().to_string(), desc.to_string()); + + desc + } else { + continue; + }; + + let entry = ( + color_formatter() + .color_str(pkg, Action::Emphasis) + .bold() + .to_string(), + color_formatter() + .color_str(cmd, Action::Secondary) + .to_string(), + desc, + ); + + res.push(entry); + } + + println!("{}\n", fl!("command-not-found-with-result", kw = query)); + let mut printer = PagerPrinter::new(stdout()); + printer + .print_table(res, vec!["Name", "Path", "Description"]) + .ok(); + + if too_many { + println!("\n{}", fl!("cnf-too-many-query")); + println!("{}", fl!("cnf-too-many-query-2", query = query)); + } + + Ok(127) +} + +#[cfg(not(feature = "ubuntu"))] fn jaro_nums(input: IndexSet<(String, String)>, query: &str) -> Vec<(String, String, u8)> { let mut output = vec![];