1#[macro_use] extern crate failure;
23extern crate regex;
24extern crate toml;
25
26use std::process::{Command,Child};
27use std::collections::HashSet;
28use std::hash::{Hash, Hasher};
29use std::fs::{File,read_dir};
30use std::io::prelude::*;
31use std::cmp::Ordering;
32use std::path::{PathBuf, Path};
33use failure::Error;
34use regex::Regex;
35use toml::Value;
36
37#[derive(Eq,Clone,Default)]
41pub struct PackageManager {
42 pub name: String,
43 pub version: String,
44 pub config_dir: PathBuf,
45 pub install: Option<String>,
46 pub install_local: Option<String>,
47 pub remove: Option<String>,
48 pub remove_local: Option<String>,
49 pub search: Option<String>,
50}
51
52impl PackageManager {
53 fn fix_relative_path(config_dir: &PathBuf, command: &str) -> String {
55 if command.starts_with("./") {
56 let mut tmp = config_dir.as_os_str().to_str().unwrap().to_owned();
57 tmp.push_str(command);
58 tmp
59 } else {
60 command.to_owned()
61 }
62 }
63
64 pub fn exists(&self) -> bool {
67 let mut version_command = self.make_command("version").unwrap();
68 let status = version_command.status().expect("Failed to run version command");
69 status.success()
70 }
71
72 pub fn has_command(&self, name: &str) -> bool {
74 match name {
75 "version" => true,
76 "install" => self.install.is_some(),
77 "install_local" => self.install_local.is_some(),
78 "remove" => self.remove.is_some(),
79 "remove_local" => self.remove_local.is_some(),
80 &_ => false,
81 }
82 }
83
84 pub fn run_command(&self, name: &str, args: &str) -> Result<Child,Error> {
87 let mut command = self.make_command(name).unwrap();
88 command.args(args.split_whitespace());
89 match command.spawn() {
90 Ok(child) => Ok(child),
91 Err(_) => bail!("Couldn't execute command")
92 }
93 }
94
95 fn make_command(&self, name: &str) -> Option<Command> {
99 let tmp: Option<&String> = match name {
100 "version" => Some(&self.version),
101 "install" => self.install.as_ref(),
102 "install_local" => self.install_local.as_ref(),
103 "remove" => self.remove.as_ref(),
104 "remove_local" => self.remove_local.as_ref(),
105 _ => panic!("No such command"),
106 };
107 match tmp {
108 Some(s) => {
109 let s = PackageManager::fix_relative_path(&self.config_dir, s);
110 let mut s = s.split_whitespace();
111 let mut result = Command::new(s.nth(0).unwrap());
112 let args: Vec<&str> = s.collect();
113 result.args(args);
114 Some(result)
115 },
116 None => None,
117 }
118 }
119
120 pub fn install(&self, args: &str) -> Result<Child,Error> {
122 self.run_command("install", args)
123 }
124
125 pub fn uninstall(&self, args: &str) -> Result<Child,Error> {
127 self.run_command("uninstall", args)
128 }
129
130 pub fn search(&self, args: &str) -> Result<Child,Error> {
132 self.run_command("search", args)
133 }
134
135 pub fn get_name(&self) -> String {
137 self.name.to_owned()
138 }
139
140 pub fn get_config_dir(self) -> PathBuf {
142 self.config_dir
143 }
144
145 pub fn version(self) -> Result<Child,Error> {
147 self.run_command("version", "")
148 }
149
150 pub fn get_version(self) -> Result<Version,Error> {
152 let mut command = self.make_command("version").unwrap();
153 let output = command.output()?;
154 let version_string = String::from_utf8(output.stdout)?;
155 Ok(Version::from_str(&version_string))
156 }
157
158 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<PackageManager,Error> {
161 let mut file = File::open(&path)?;
162
163 let mut content = String::new();
164
165 file.read_to_string(&mut content)?;
166
167 let resource = content.as_str().parse::<Value>()?;
168
169 let name: String = String::from(path.as_ref().file_stem().unwrap().to_str().unwrap());
170
171 let version: String = match resource.get("version") {
172 Some(s) => s.as_str().unwrap().to_owned(),
173 None => bail!("Package manager version command not provided in config")
174 };
175
176 let install: Option<String> = match resource.get("install") {
177 Some(s) => Some(String::from(s.as_str().unwrap())),
178 None => None
179 };
180 let install_local: Option<String> = match resource.get("install_local") {
181 Some(s) => Some(String::from(s.as_str().unwrap())),
182 None => None
183 };
184 let remove: Option<String> = match resource.get("remove") {
185 Some(s) => Some(String::from(s.as_str().unwrap())),
186 None => None
187 };
188 let remove_local: Option<String> = match resource.get("remove_local") {
189 Some(s) => Some(String::from(s.as_str().unwrap())),
190 None => None
191 };
192 let search: Option<String> = match resource.get("search") {
193 Some(s) => Some(String::from(s.as_str().unwrap())),
194 None => None
195 };
196
197 let config_dir: PathBuf = match path.as_ref().parent() {
198 Some(dir) => dir.to_path_buf(),
199 None => PathBuf::new()
200 };
201
202 Ok(PackageManager {
203 name,
204 version,
205 config_dir,
206 install,
207 install_local,
208 remove,
209 remove_local,
210 search,
211 })
212 }
213}
214
215impl PartialEq for PackageManager {
216 fn eq(&self, other: &PackageManager) -> bool {
217 self.name == other.name
218 }
219}
220
221impl Ord for PackageManager {
222 fn cmp(&self, other: &PackageManager) -> Ordering {
223 self.name.cmp(&other.name)
224 }
225}
226
227impl PartialOrd for PackageManager {
228 fn partial_cmp(&self, other: &PackageManager) -> Option<Ordering> {
229 Some(self.cmp(other))
230 }
231}
232
233impl Hash for PackageManager {
234 fn hash<H: Hasher>(&self, state: &mut H) {
235 self.name.hash(state);
236 }
237}
238
239#[derive(Default)]
241pub struct Package {
242 pub name: String,
243 pub owner: PackageManager,
244 pub version: Version,
245 pub description: String,
246}
247
248impl Package {
249 pub fn is_called(&self, name: &str) -> bool {
251 self.name == name
252 }
253
254 pub fn install(self) -> Result<Child,Error> {
256 self.owner.install(&self.name)
257 }
258
259 pub fn uninstall(self) -> Result<Child,Error> {
261 self.owner.uninstall(&self.name)
262 }
263
264 pub fn get_name(&self) -> String {
266 (&self.name).to_owned()
267 }
268
269 pub fn get_version(self) -> Version {
271 self.version
272 }
273
274 pub fn get_description(self) -> String {
276 self.description
277 }
278
279 pub fn get_manager(self) -> PackageManager {
282 self.owner
283 }
284}
285
286#[derive(Debug,Default)]
289pub struct Version {
290 representation: String,
291 semantic: bool
292}
293
294impl Version {
295 fn from_str(representation: &str) -> Version {
298 let semantic = Version::is_semantic(representation);
299 Version {
300 representation: String::from(representation),
301 semantic,
302 }
303 }
304
305 pub fn get_representation(self) -> String {
307 self.representation
308 }
309
310 pub fn set_representation(&mut self, val: String) {
312 self.representation = val;
313 self.semantic = Version::is_semantic(&self.representation);
314 }
315
316 pub fn is_semantic(representation: &str) -> bool {
318 let re = Version::get_semantic_regex();
319 re.is_match(representation)
320 }
321
322 fn get_semantic_regex() -> Regex {
323 Regex::new(r"^(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?(?:\+([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?$").unwrap()
324 }
325
326 pub fn set_semantic(&mut self, val: bool) -> Result<(),Error> {
329 if val && !Version::is_semantic(&self.representation) {
330 bail!("Version does not match semantic structure");
331 }
332 self.semantic = val;
333 Ok(())
334 }
335
336 pub fn get_semantic(self) -> bool {
338 self.semantic
339 }
340
341}
342
343impl PartialEq for Version {
344 fn eq(&self, other: &Version) -> bool {
345 if self.semantic != other.semantic {
346 false
347 }
348 else if self.semantic && other.semantic {
349 let re = Version::get_semantic_regex();
350 let self_groups = re.captures(&self.representation).unwrap();
351 let other_groups = re.captures(&other.representation).unwrap();
352 self_groups.get(1)==other_groups.get(1) && self_groups.get(2)==
353 other_groups.get(2) && self_groups.get(3) == other_groups.get(3)
354 } else {
355 self.representation == other.representation
356 }
357 }
358}
359pub fn get_managers<P: AsRef<Path>>(directory: P, names: &ManagerSpecifier) -> Result<Vec<PackageManager>, Error> {
364 let mut result = Vec::new();
365 if let Ok(entries) = read_dir(directory) {
366 for entry in entries {
367 if let Ok(entry) = entry {
368 let path = entry.path();
369 let name = entry.file_name();
370 if name.to_str().unwrap().ends_with(".toml") {
371 if let Some(stem) = path.file_stem() {
372 match *names {
374 ManagerSpecifier::Excludes(ref set) => {
375 if set.contains(stem.to_str().unwrap()) {
376 continue;
377 }
378 },
379 ManagerSpecifier::Includes(ref set) => {
380 if !set.contains(stem.to_str().unwrap()) {
381 continue;
382 }
383 },
384 _ => {}
385 };
386 let manager = PackageManager::from_file(&path);
388 match manager {
389 Ok(man) => result.push(man),
390 Err(_e) => {}
391 }
392 }
393 }
394 }
395 }
396 }
397 Ok(result)
398}
399
400pub enum ManagerSpecifier {
402 Excludes(HashSet<&'static str>),
403 Includes(HashSet<&'static str>),
404 Empty,
405}
406
407pub fn read_config_dirs<P: AsRef<Path>>(directories: Vec<P>, exceptions: &ManagerSpecifier) -> Vec<PackageManager> {
417 let mut result: HashSet<PackageManager> = HashSet::new();
418 for dir in directories {
419 let tmp = get_managers(dir, exceptions);
420 let tmp = match tmp {
421 Ok(s) => s,
422 Err(_e) => panic!("Couldn't get managers from directory"),
423 };
424 for manager in tmp {
425 if !result.contains(&manager) {
426 result.insert(manager);
427 }
428 }
429 }
430let return_value: Vec<PackageManager> = result.into_iter().collect();
433 return_value
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 #[test]
440 fn semantic_matching() {
441 let mut semantics: Vec<&str> = Vec::new();
442 semantics.push("0.1.1");
443 semantics.push("0.1.1-prerelease");
444 semantics.push("0.1.1-prerelease.x.3");
445 semantics.push("0.1.1-pre-pre-release");
446 semantics.push("0.1.1+builddata");
447 semantics.push("0.1.1+build-data");
448 semantics.push("0.1.1+builddata.3");
449 semantics.push("0.1.1-prerelease+builddata");
450 let mut jejune: Vec<&str> = Vec::new();
451 jejune.push("a.b.c");
452 jejune.push("1-1-1");
453 jejune.push("0.1.1-b@d");
454 jejune.push("0.1.1+b@d");
455 for string in &semantics {
456 assert!(Version::is_semantic(string), "{} was detected as not semantic", string);
457 }
458 for string in &jejune {
459 assert!(!Version::is_semantic(string), "{} was detected as semantic", string);
460 }
461 }
462
463 #[test]
464 fn creation_test() {
465 let blank_version = Version::new();
466 assert_eq!(blank_version.representation, String::new());
467 assert!(!blank_version.semantic);
468 let semantic_string = "0.1.2";
469 let non_semantic_string = "1.4rc2";
470 let semantic_version = Version::from_str(semantic_string);
471 assert!(semantic_version.get_semantic());
472 let non_semantic_version = Version::from_str(non_semantic_string);
473 assert!(!non_semantic_version.get_semantic());
474 }
475
476 #[test]
477 fn equality_test() {
478 let version1 = Version::from_str("0.1.2");
479 let version2 = Version::from_str("1.4rc2");
480 let mut version3 = Version::from_str("0.1.2");
481 assert_eq!(version1,version3);
482 assert_ne!(version1,version2);
483 let res = version3.set_semantic(false);
484 assert!(!res.is_err());
485 assert_ne!(version1,version3);
486 }
487
488 #[test]
489 fn read_toml() {
490 let path = PathBuf::from("./test-files");
491 let path_vec = vec!(&path);
492 let managers = read_config_dirs(path_vec, ManagerSpecifier::Empty);
493
494 let mut expected_managers = HashSet::new();
495 expected_managers.insert(PackageManager {
496 name: String::from("pacman"),
497 version: String::from("./pacman/version.sh"),
498 config_dir: PathBuf::from("./test-files"),
499 install: Some(String::from("pacman -S")),
500 install_local: None,
501 remove: Some(String::from("pacman -Rs")),
502 remove_local: None,
503 search: Some(String::from("pacman -Ss")),
504 });
505 for man in managers {
506 assert!(expected_managers.contains(&man));
507 }
508 }
509
510 #[test]
511 fn cargo_exists() {
512 let cargo = PackageManager {
513 name: String::from("cargo"),
514 version: String::from("./cargo/version.sh"),
515 config_dir: PathBuf::from("./test-files/"),
516 install: None,
517 install_local: Some(String::from("cargo install")),
518 remove: None,
519 remove_local: Some(String::from("cargo uninstall")),
520 search: Some(String::from("cargo search")),
521 };
522 assert!(cargo.exists(), "cargo apparently isn't installed here?");
523 }
524
525 #[test]
526 fn commands_fail_gracefully() {
527 let fake_manager = PackageManager {
528 name: String::from("fake"),
529 version: String::from("./fake/version.sh"), config_dir: PathBuf::from("./test-files/"),
531 install: Some(String::from("./fake/beelzebub")), install_local: Some(String::from("./fake/baphomet")), remove: None,
534 remove_local: None,
535 search: None,
536 };
537 assert!(&fake_manager.run_command("version", "").is_err());
538 assert!(&fake_manager.run_command("install", "").is_err());
539 assert!(&fake_manager.run_command("install_local", "").is_err());
540 }
541}