diff --git a/Cargo.lock b/Cargo.lock index d7e01c79..1da74e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata 0.4.8", @@ -649,9 +649,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] @@ -1414,9 +1414,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "691142b1a34d18e8ed6e6114bc1a2736516c5ad60ef3aa9bd1b694886e3ca92d" +checksum = "c57c477b645ee248b173bb1176b52dd528872f12c50375801a58aaf5ae91113f" dependencies = [ "bstr", "itoa", @@ -1505,9 +1505,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34740384d8d763975858fa2c176b68652a6fcc09f616e24e3ce967b0d370e4d8" +checksum = "3b3d4fac505a621f97e5ce2c69fdc425742af00c0920363ca4074f0eb48b1db9" dependencies = [ "fastrand", "gix-features", @@ -1601,9 +1601,9 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.46.0" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d93e2bbfa83a307e47f45e45de7b6c04d7375a8bd5907b215f4bf45237d879" +checksum = "e42d58010183ef033f31088479b4eb92b44fe341b35b62d39eb8b185573d77ea" dependencies = [ "bstr", "gix-actor", @@ -1611,6 +1611,7 @@ dependencies = [ "gix-features", "gix-hash", "gix-hashtable", + "gix-path", "gix-utils", "gix-validate", "itoa", @@ -1712,9 +1713,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.49.0" +version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eae462723686272a58f49501015ef7c0d67c3e042c20049d8dd9c7eff92efde" +checksum = "a91b61776c839d0f1b7114901179afb0947aa7f4d30793ca1c56d335dfef485f" dependencies = [ "gix-actor", "gix-features", @@ -1747,9 +1748,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44488e0380847967bc3e3cacd8b22652e02ea1eb58afb60edd91847695cd2d8d" +checksum = "61e1ddc474405a68d2ce8485705dd72fe6ce959f2f5fe718601ead5da2c8f9e7" dependencies = [ "bitflags 2.6.0", "bstr", @@ -1827,9 +1828,9 @@ checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" [[package]] name = "gix-traverse" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff2ec9f779680f795363db1c563168b32b8d6728ec58564c628e85c92d29faf" +checksum = "6ed47d648619e23e93f971d2bba0d10c1100e54ef95d2981d609907a8cabac89" dependencies = [ "bitflags 2.6.0", "gix-commitgraph", @@ -1844,13 +1845,14 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e09f97db3618fb8e473d7d97e77296b50aaee0ddcd6a867f07443e3e87391099" +checksum = "d096fb733ba6bd3f5403dba8bd72bdd8809fe2b347b57844040b8f49c93492d9" dependencies = [ "bstr", "gix-features", "gix-path", + "percent-encoding", "thiserror 2.0.6", "url", ] @@ -2188,11 +2190,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/hipcheck/src/cache/plugin.rs b/hipcheck/src/cache/plugin.rs index f60e3ec2..7d24047a 100644 --- a/hipcheck/src/cache/plugin.rs +++ b/hipcheck/src/cache/plugin.rs @@ -1,21 +1,154 @@ // SPDX-License-Identifier: Apache-2.0 +#![allow(unused)] +use std::path::{Path, PathBuf, Component}; +use std::{borrow::Borrow, + time::{SystemTime,Duration}, + env +}; +use pathbuf::pathbuf; +use tabled::{Table, Tabled}; +use walkdir::{DirEntry, WalkDir}; +use crate::plugin::PluginId; +use crate::StdResult; +//use super::super::cli::CliConfig; +use crate::error::Result; +use regex::Regex; //used for regex +use semver::{Version, VersionReq}; +use crate::cache::repo; -use std::path::{Path, PathBuf}; +///enums used for sorting are the same as the ones used for listing repo cache entries except for +/// does not include Largest +#[derive(Debug, Clone)] +pub enum PluginCacheSort { + Oldest, + Alpha, +} + +#[derive(Debug, Clone)] +pub enum PluginCacheDeleteScope { + All, + Group { + sort: PluginCacheSort, + invert: bool, + n: usize, + }, +} + +#[derive(Debug, Clone)] +pub struct PluginCacheListScope { + pub sort: PluginCacheSort, + pub invert: bool, + pub n: Option, +} + + +#[derive(Debug, Clone, Tabled)] +pub struct PluginCacheEntry { + pub publisher: String, + pub name: String, + pub version: String, + #[tabled(display_with("Self::display_modified", self), rename = "last_modified")] + pub modified: SystemTime +} + +impl PluginCacheEntry { //copied the function from repo for simplicity + pub fn display_modified(&self) -> String { + let Ok(dur) = self.modified.duration_since(SystemTime::UNIX_EPOCH) else { + return "".to_owned(); + }; + let Some(dt) = chrono::DateTime::::from_timestamp( + dur.as_secs() as i64, + dur.subsec_nanos(), + ) else { + return "".to_owned(); + }; + let chars = dt.to_rfc2822().chars().collect::>(); + // Remove unnecessary " +0000" from end of rfc datetime str + chars[..chars.len() - 6].iter().collect() + } + +} + +struct HcPluginCacheIterator { + wd: Box>> +} + +impl HcPluginCacheIterator { + fn new(path: &Path) -> Self { + HcPluginCacheIterator{ + wd: Box::new( + WalkDir::new(path) //builds a new iterator using WalkDir + .min_depth(3) //makes sure to get the publisher, name, and version folders + .into_iter() + .filter_entry(|e| e.path().is_dir()), //makes sure the path is a directory + ) + } + } + fn path_to_plugin_entry(&self, path: &Path) -> Result { //does function need to work when you clone a repo? + let components: Vec = path + .components() + .filter_map(|component| { + if let Component::Normal(name) = component { //extracts components seperated by "/" from the path + name.to_str().map(|s| s.to_string()) //converts the string slices into Strings + } else { + None + } + + }) + .collect(); //collects filepath into vector +//Todo: add error handling for when the path length is less than 3 + let relevant_components: &[String] = &components[components.len() - 3..]; + let plugin_publisher = relevant_components[0].to_owned(); + let plugin_name = relevant_components[1].to_owned(); + let plugin_version = relevant_components[2].to_owned(); + let last_modified: SystemTime = repo::get_last_modified_or_now(path); + Ok(PluginCacheEntry{ + publisher:plugin_publisher, + name:plugin_name, + version: plugin_version, + modified: last_modified + }) + + } + +} + + + +///Function will take the full file path and put the last 3 directories (publisher, plugin, and review) +///into the Plugin Cache entry +impl Iterator for HcPluginCacheIterator { + type Item = PluginCacheEntry; + fn next(&mut self) -> Option { + if let Some(Ok(e)) = self.wd.next() { + if let Ok(ce) = self.path_to_plugin_entry(e.path()) { + Some(ce) + } + else { + None + } + } + else { + None + } + } +} -use pathbuf::pathbuf; -use crate::plugin::PluginId; /// Plugins are stored with the following format `///` pub struct HcPluginCache { - /// path to the root of the plugin cache - path: PathBuf, + pub path: PathBuf, //path to the root of the plugin cache + pub entries: Vec, //needs to store a vector of PluginEntries + } impl HcPluginCache { pub fn new(path: &Path) -> Self { let plugins_path = pathbuf![path, "plugins"]; - Self { path: plugins_path } + let entries: Vec = + HcPluginCacheIterator::new(plugins_path.as_path()).collect(); + Self { path: plugins_path , entries} } /// The folder in which a specific PluginID will be stored @@ -34,4 +167,379 @@ impl HcPluginCache { pub fn plugin_kdl(&self, plugin_id: &PluginId) -> PathBuf { self.plugin_download_dir(plugin_id).join("plugin.kdl") } + ///Sort function is the same as in repo.cache but has been modified to get rid of the enum variant Largest + fn sort>(entries: &mut [A], sort: PluginCacheSort, invert: bool) { + // Generic allows sort to handle both owned and borrowed lists + let sort_func: fn(&PluginCacheEntry, &PluginCacheEntry) -> std::cmp::Ordering = + match (sort, invert) { + (PluginCacheSort::Alpha, false) => |a, b| a.name.partial_cmp(&b.name).unwrap(), + (PluginCacheSort::Alpha, true) => |a, b| b.name.partial_cmp(&a.name).unwrap(), + (PluginCacheSort::Oldest, false) => { + |a, b| a.modified.partial_cmp(&b.modified).unwrap() + } + (PluginCacheSort::Oldest, true) => { + |a, b| b.modified.partial_cmp(&a.modified).unwrap() + } + }; + entries.sort_by(|a1: &A, a2: &A| sort_func(a1.borrow(), a2.borrow())); + } + + ///lists all the plugin entries. Works the same as the function in repo.rs except for + ///user can filter plugins by version and publisher + /// TODO: split into inner function that you can call, outer function can call inner and display on the result + pub fn list(mut self, scope: PluginCacheListScope, name: Option, publisher:Option, version: Option) -> Result<()> { + //using borrowed data in list_inner in order to avoid transferring ownership + let filtered_entries = HcPluginCache::list_inner(&self.entries, scope, name, publisher, version); + match filtered_entries { + Ok(v) => self.display(v), + Err(e) => return Err(e) + } + Ok(()) + } + fn list_inner(entries: &[PluginCacheEntry], scope: PluginCacheListScope, name: Option, publisher:Option, version:Option)->Result> { + let opt_pat: Option = match name { //converts the string to a regex expression + Some(raw_p) => Some(Regex::new(format!("^{raw_p}$").as_str())?), + None => None, + }; + //filters based on the regex pattern + let mut filt_entries = entries + .iter() + .filter(|e| match &opt_pat { + Some(pat) => pat.is_match(e.name.as_str()), + None => true, //if there is no regex pattern passed in, the default is true + }) + .filter(|e| match &publisher { + Some(a) => e.publisher == *a, //must dereference the borrowed value of a to compare it to the plugin's publisher + None => true, + }) + .filter(|e| match &version { + Some(a) => { + let version_to_compare = VersionReq::parse(a.as_str()).unwrap(); + let plugin_version = Version::parse(e.version.as_str()).unwrap(); + version_to_compare.matches(&plugin_version) + } + None => true, + }) + .collect::>(); //creates borrowed data to pass to display + + // Sort filtered entries + HcPluginCache::sort(&mut filt_entries, scope.sort, scope.invert); + + // Limit to first N if specified + if let Some(n) = scope.n { + filt_entries.truncate(n); + } + Ok(filt_entries) + } + + /// Internal helper for list functions + fn display(&self, to_show: Vec<&PluginCacheEntry>) { + println!("{}", Table::new(to_show)); + } + } + + + +#[cfg(test)] +mod tests { + use super::*; + fn test_data() -> Vec { + let cache_entry_1 = PluginCacheEntry{publisher: String::from("randomhouse"), + name: String::from("bugs bunny"), + version: String::from("0.2.0"), + modified: SystemTime::UNIX_EPOCH + Duration::from_secs(3) + }; + let cache_entry_2 = PluginCacheEntry{publisher: String::from("mitre"), + name: String::from("affiliation"), + version: String::from("0.1.0"), + modified: SystemTime::UNIX_EPOCH + Duration::from_secs(1) + }; + let cache_entry_3 = PluginCacheEntry{publisher: String::from("mitre2"), + name: String::from("activity"), + version: String::from("0.1.0"), + modified: SystemTime::UNIX_EPOCH + Duration::from_secs(2) + }; + let cache_entry_4 = PluginCacheEntry{publisher: String::from("mitre"), + name: String::from("difference"), + version: String::from("0.0.5"), + modified: SystemTime::now() + }; + let mut entries: Vec = Vec::new(); + entries.push(cache_entry_1); + entries.push(cache_entry_2); + entries.push(cache_entry_3); + entries.push(cache_entry_4); + entries + } + #[test] + fn test_scope_works_as_expected() { + let time_sort = PluginCacheListScope { + sort:PluginCacheSort::Oldest, + invert:false, + n:None, + }; + let reverse_time_sort = PluginCacheListScope { + sort:PluginCacheSort::Oldest, + invert:true, + n:None, + }; + let reverse_time_sort_with_two = PluginCacheListScope { + sort:PluginCacheSort::Oldest, + invert:true, + n:Some(2), + }; + let alpha_sort = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + let reverse_alpha_sort = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:true, + n:None, + }; + + let entries = &test_data(); + let results = HcPluginCache::list_inner(entries, time_sort, None, None, None); + let expected_output = vec!["affiliation", "activity", "bugs bunny", "difference"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + + }, + Err(e) => panic!("Error in creating list") + + } + + let results = HcPluginCache::list_inner(entries, reverse_time_sort, None, None, None); + let expected_output = vec!["difference", "bugs bunny", "activity", "affiliation"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + + }, + Err(e) => panic!("Error in creating list") + + } + let results = HcPluginCache::list_inner(entries, alpha_sort, None, None, None); + let expected_output = vec!["activity", "affiliation", "bugs bunny", "difference"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + }, + Err(e) => panic!("Error in creating list") + + } + + let results = HcPluginCache::list_inner(entries, reverse_alpha_sort, None, None, None); + let expected_output = vec!["difference", "bugs bunny", "affiliation", "activity"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + }, + Err(e) => panic!("Error in creating list") + + } + + let results = HcPluginCache::list_inner(entries, reverse_time_sort_with_two, None, None, None); + let expected_output = vec!["difference", "bugs bunny"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + }, + Err(e) => panic!("Error in creating list") + + } + } + #[test] + fn test_version_filtering() { + let alpha_sort = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + + let entries = &test_data(); + let results = HcPluginCache::list_inner(entries, alpha_sort, None, None, Some(String::from(">0.0.5"))); + let expected_output = vec!["activity", "affiliation", "bugs bunny"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + + }, + Err(e) => panic!("Error in creating list") + + } + + let alpha_sort_2 = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + let results = HcPluginCache::list_inner(entries, alpha_sort_2, None, None, Some(String::from("<=0.1.0"))); + let expected_output = vec!["activity", "affiliation", "difference"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + + }, + Err(e) => panic!("Error in creating list") + + } + + let alpha_sort_3 = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + let results = HcPluginCache::list_inner(entries, alpha_sort_3, None, None, Some(String::from("<=0.2.0"))); + let expected_output = vec!["activity", "affiliation", "bugs bunny", "difference"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + + }, + Err(e) => panic!("Error in creating list") + + } + + } + #[test] + fn test_name_filtering() { + let alpha_sort = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + let entries = &test_data(); + let results = HcPluginCache::list_inner(entries, alpha_sort, Some(String::from("bugs bunny")), None, None); + let expected_output = vec!["bugs bunny"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + + }, + Err(e) => panic!("Error in creating list") + + } + let alpha_sort = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + let results = HcPluginCache::list_inner(entries, alpha_sort, Some(String::from("a.*")), None, None); + let expected_output = vec!["activity", "affiliation"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output); + + + }, + Err(e) => panic!("Error in creating list") + + } + } + #[test] + fn test_publisher_filtering() { + let alpha_sort = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + let entries = &test_data(); + let results = HcPluginCache::list_inner(entries, alpha_sort, None, Some(String::from("mitre")), None); + let expected_output = vec!["affiliation", "difference"]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output) + + + + }, + Err(e) => panic!("Error in creating list") + + } + let alpha_sort = PluginCacheListScope { + sort:PluginCacheSort::Alpha, + invert:false, + n:None, + }; + let entries = &test_data(); + let results = HcPluginCache::list_inner(entries, alpha_sort, None, Some(String::from("")), None); + let expected_output:Vec<&String> = vec![]; + let mut actual_output: Vec<&String> = Vec::new(); + match results { + Ok(e) => { + for entry in &e { + actual_output.push(&entry.name); + } + assert_eq!(actual_output, expected_output) + + + + }, + Err(e) => panic!("Error in creating list") + + } + + } } + + + + + diff --git a/hipcheck/src/cache/repo.rs b/hipcheck/src/cache/repo.rs index 5ef5fb06..d16263fd 100644 --- a/hipcheck/src/cache/repo.rs +++ b/hipcheck/src/cache/repo.rs @@ -64,7 +64,7 @@ impl RepoCacheEntry { .unwrap_or("".to_owned()) .to_string() } - fn display_modified(&self) -> String { + pub fn display_modified(&self) -> String { let Ok(dur) = self.modified.duration_since(SystemTime::UNIX_EPOCH) else { return "".to_owned(); }; @@ -129,7 +129,7 @@ fn load_or_get_empty(path: &Path) -> HcRepoCacheDisk { fn try_get_last_modified(path: &Path) -> Result { Ok(fs::metadata(path)?.modified()?) } -fn get_last_modified_or_now(path: &Path) -> SystemTime { +pub fn get_last_modified_or_now(path: &Path) -> SystemTime { try_get_last_modified(path).unwrap_or(SystemTime::now()) }