#[macro_use] extern crate failure;
extern crate regex;
extern crate toml;
use std::process::{Command,Child};
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::fs::{File,read_dir};
use std::io::prelude::*;
use std::cmp::Ordering;
use std::path::{PathBuf, Path};
use failure::Error;
use regex::Regex;
use toml::Value;
#[derive(Eq,Clone,Default)]
pub struct PackageManager {
pub name: String,
pub version: String,
pub config_dir: PathBuf,
pub install: Option<String>,
pub install_local: Option<String>,
pub remove: Option<String>,
pub remove_local: Option<String>,
pub search: Option<String>,
}
impl PackageManager {
fn fix_relative_path(config_dir: &PathBuf, command: &str) -> String {
if command.starts_with("./") {
let mut tmp = config_dir.as_os_str().to_str().unwrap().to_owned();
tmp.push_str(command);
tmp
} else {
command.to_owned()
}
}
pub fn exists(&self) -> bool {
let mut version_command = self.make_command("version").unwrap();
let status = version_command.status().expect("Failed to run version command");
status.success()
}
pub fn has_command(&self, name: &str) -> bool {
match name {
"version" => true,
"install" => self.install.is_some(),
"install_local" => self.install_local.is_some(),
"remove" => self.remove.is_some(),
"remove_local" => self.remove_local.is_some(),
&_ => false,
}
}
pub fn run_command(&self, name: &str, args: &str) -> Result<Child,Error> {
let mut command = self.make_command(name).unwrap();
command.args(args.split_whitespace());
match command.spawn() {
Ok(child) => Ok(child),
Err(_) => bail!("Couldn't execute command")
}
}
fn make_command(&self, name: &str) -> Option<Command> {
let tmp: Option<&String> = match name {
"version" => Some(&self.version),
"install" => self.install.as_ref(),
"install_local" => self.install_local.as_ref(),
"remove" => self.remove.as_ref(),
"remove_local" => self.remove_local.as_ref(),
_ => panic!("No such command"),
};
match tmp {
Some(s) => {
let s = PackageManager::fix_relative_path(&self.config_dir, s);
let mut s = s.split_whitespace();
let mut result = Command::new(s.nth(0).unwrap());
let args: Vec<&str> = s.collect();
result.args(args);
Some(result)
},
None => None,
}
}
pub fn install(&self, args: &str) -> Result<Child,Error> {
self.run_command("install", args)
}
pub fn uninstall(&self, args: &str) -> Result<Child,Error> {
self.run_command("uninstall", args)
}
pub fn search(&self, args: &str) -> Result<Child,Error> {
self.run_command("search", args)
}
pub fn get_name(&self) -> String {
self.name.to_owned()
}
pub fn get_config_dir(self) -> PathBuf {
self.config_dir
}
pub fn version(self) -> Result<Child,Error> {
self.run_command("version", "")
}
pub fn get_version(self) -> Result<Version,Error> {
let mut command = self.make_command("version").unwrap();
let output = command.output()?;
let version_string = String::from_utf8(output.stdout)?;
Ok(Version::from_str(&version_string))
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<PackageManager,Error> {
let mut file = File::open(&path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let resource = content.as_str().parse::<Value>()?;
let name: String = String::from(path.as_ref().file_stem().unwrap().to_str().unwrap());
let version: String = match resource.get("version") {
Some(s) => s.as_str().unwrap().to_owned(),
None => bail!("Package manager version command not provided in config")
};
let install: Option<String> = match resource.get("install") {
Some(s) => Some(String::from(s.as_str().unwrap())),
None => None
};
let install_local: Option<String> = match resource.get("install_local") {
Some(s) => Some(String::from(s.as_str().unwrap())),
None => None
};
let remove: Option<String> = match resource.get("remove") {
Some(s) => Some(String::from(s.as_str().unwrap())),
None => None
};
let remove_local: Option<String> = match resource.get("remove_local") {
Some(s) => Some(String::from(s.as_str().unwrap())),
None => None
};
let search: Option<String> = match resource.get("search") {
Some(s) => Some(String::from(s.as_str().unwrap())),
None => None
};
let config_dir: PathBuf = match path.as_ref().parent() {
Some(dir) => dir.to_path_buf(),
None => PathBuf::new()
};
Ok(PackageManager {
name,
version,
config_dir,
install,
install_local,
remove,
remove_local,
search,
})
}
}
impl PartialEq for PackageManager {
fn eq(&self, other: &PackageManager) -> bool {
self.name == other.name
}
}
impl Ord for PackageManager {
fn cmp(&self, other: &PackageManager) -> Ordering {
self.name.cmp(&other.name)
}
}
impl PartialOrd for PackageManager {
fn partial_cmp(&self, other: &PackageManager) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for PackageManager {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
#[derive(Default)]
pub struct Package {
pub name: String,
pub owner: PackageManager,
pub version: Version,
pub description: String,
}
impl Package {
pub fn is_called(&self, name: &str) -> bool {
self.name == name
}
pub fn install(self) -> Result<Child,Error> {
self.owner.install(&self.name)
}
pub fn uninstall(self) -> Result<Child,Error> {
self.owner.uninstall(&self.name)
}
pub fn get_name(&self) -> String {
(&self.name).to_owned()
}
pub fn get_version(self) -> Version {
self.version
}
pub fn get_description(self) -> String {
self.description
}
pub fn get_manager(self) -> PackageManager {
self.owner
}
}
#[derive(Debug,Default)]
pub struct Version {
representation: String,
semantic: bool
}
impl Version {
fn from_str(representation: &str) -> Version {
let semantic = Version::is_semantic(representation);
Version {
representation: String::from(representation),
semantic,
}
}
pub fn get_representation(self) -> String {
self.representation
}
pub fn set_representation(&mut self, val: String) {
self.representation = val;
self.semantic = Version::is_semantic(&self.representation);
}
pub fn is_semantic(representation: &str) -> bool {
let re = Version::get_semantic_regex();
re.is_match(representation)
}
fn get_semantic_regex() -> Regex {
Regex::new(r"^(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?(?:\+([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?$").unwrap()
}
pub fn set_semantic(&mut self, val: bool) -> Result<(),Error> {
if val && !Version::is_semantic(&self.representation) {
bail!("Version does not match semantic structure");
}
self.semantic = val;
Ok(())
}
pub fn get_semantic(self) -> bool {
self.semantic
}
}
impl PartialEq for Version {
fn eq(&self, other: &Version) -> bool {
if self.semantic != other.semantic {
false
}
else if self.semantic && other.semantic {
let re = Version::get_semantic_regex();
let self_groups = re.captures(&self.representation).unwrap();
let other_groups = re.captures(&other.representation).unwrap();
self_groups.get(1)==other_groups.get(1) && self_groups.get(2)==
other_groups.get(2) && self_groups.get(3) == other_groups.get(3)
} else {
self.representation == other.representation
}
}
}
pub fn get_managers<P: AsRef<Path>>(directory: P, names: &ManagerSpecifier) -> Result<Vec<PackageManager>, Error> {
let mut result = Vec::new();
if let Ok(entries) = read_dir(directory) {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
let name = entry.file_name();
if name.to_str().unwrap().ends_with(".toml") {
if let Some(stem) = path.file_stem() {
match *names {
ManagerSpecifier::Excludes(ref set) => {
if set.contains(stem.to_str().unwrap()) {
continue;
}
},
ManagerSpecifier::Includes(ref set) => {
if !set.contains(stem.to_str().unwrap()) {
continue;
}
},
_ => {}
};
let manager = PackageManager::from_file(&path);
match manager {
Ok(man) => result.push(man),
Err(_e) => {}
}
}
}
}
}
}
Ok(result)
}
pub enum ManagerSpecifier {
Excludes(HashSet<&'static str>),
Includes(HashSet<&'static str>),
Empty,
}
pub fn read_config_dirs<P: AsRef<Path>>(directories: Vec<P>, exceptions: &ManagerSpecifier) -> Vec<PackageManager> {
let mut result: HashSet<PackageManager> = HashSet::new();
for dir in directories {
let tmp = get_managers(dir, exceptions);
let tmp = match tmp {
Ok(s) => s,
Err(_e) => panic!("Couldn't get managers from directory"),
};
for manager in tmp {
if !result.contains(&manager) {
result.insert(manager);
}
}
}
let return_value: Vec<PackageManager> = result.into_iter().collect();
return_value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantic_matching() {
let mut semantics: Vec<&str> = Vec::new();
semantics.push("0.1.1");
semantics.push("0.1.1-prerelease");
semantics.push("0.1.1-prerelease.x.3");
semantics.push("0.1.1-pre-pre-release");
semantics.push("0.1.1+builddata");
semantics.push("0.1.1+build-data");
semantics.push("0.1.1+builddata.3");
semantics.push("0.1.1-prerelease+builddata");
let mut jejune: Vec<&str> = Vec::new();
jejune.push("a.b.c");
jejune.push("1-1-1");
jejune.push("0.1.1-b@d");
jejune.push("0.1.1+b@d");
for string in &semantics {
assert!(Version::is_semantic(string), "{} was detected as not semantic", string);
}
for string in &jejune {
assert!(!Version::is_semantic(string), "{} was detected as semantic", string);
}
}
#[test]
fn creation_test() {
let blank_version = Version::new();
assert_eq!(blank_version.representation, String::new());
assert!(!blank_version.semantic);
let semantic_string = "0.1.2";
let non_semantic_string = "1.4rc2";
let semantic_version = Version::from_str(semantic_string);
assert!(semantic_version.get_semantic());
let non_semantic_version = Version::from_str(non_semantic_string);
assert!(!non_semantic_version.get_semantic());
}
#[test]
fn equality_test() {
let version1 = Version::from_str("0.1.2");
let version2 = Version::from_str("1.4rc2");
let mut version3 = Version::from_str("0.1.2");
assert_eq!(version1,version3);
assert_ne!(version1,version2);
let res = version3.set_semantic(false);
assert!(!res.is_err());
assert_ne!(version1,version3);
}
#[test]
fn read_toml() {
let path = PathBuf::from("./test-files");
let path_vec = vec!(&path);
let managers = read_config_dirs(path_vec, ManagerSpecifier::Empty);
let mut expected_managers = HashSet::new();
expected_managers.insert(PackageManager {
name: String::from("pacman"),
version: String::from("./pacman/version.sh"),
config_dir: PathBuf::from("./test-files"),
install: Some(String::from("pacman -S")),
install_local: None,
remove: Some(String::from("pacman -Rs")),
remove_local: None,
search: Some(String::from("pacman -Ss")),
});
for man in managers {
assert!(expected_managers.contains(&man));
}
}
#[test]
fn cargo_exists() {
let cargo = PackageManager {
name: String::from("cargo"),
version: String::from("./cargo/version.sh"),
config_dir: PathBuf::from("./test-files/"),
install: None,
install_local: Some(String::from("cargo install")),
remove: None,
remove_local: Some(String::from("cargo uninstall")),
search: Some(String::from("cargo search")),
};
assert!(cargo.exists(), "cargo apparently isn't installed here?");
}
#[test]
fn commands_fail_gracefully() {
let fake_manager = PackageManager {
name: String::from("fake"),
version: String::from("./fake/version.sh"), config_dir: PathBuf::from("./test-files/"),
install: Some(String::from("./fake/beelzebub")), install_local: Some(String::from("./fake/baphomet")), remove: None,
remove_local: None,
search: None,
};
assert!(&fake_manager.run_command("version", "").is_err());
assert!(&fake_manager.run_command("install", "").is_err());
assert!(&fake_manager.run_command("install_local", "").is_err());
}
}