pkg_utils/
lib.rs

1//! This crate implements a _subset_ of libalpm functionality,
2//! basically what is required to read databases and parse things.
3//!
4//! The goal is not to replace libalpm for doing actual system
5//! package management, but to be able to access metadata about an
6//! Arch or Arch-based system without needing to use the cumbersome
7//! C API. Generally this is an attempt to write a more flexible API
8//! for a lot of the stuff that libalpm does, in order to facilitate
9//! writing things like AUR helpers.
10
11#[macro_use]
12extern crate display_derive;
13extern crate failure;
14extern crate flate2;
15extern crate itertools;
16#[macro_use]
17extern crate log;
18extern crate rayon;
19extern crate tar;
20extern crate version_compare;
21extern crate xz2;
22
23pub mod db;
24mod error;
25pub mod package;
26
27use std::fmt::{self, Formatter, Display};
28use std::path::Path;
29
30use version_compare::Version;
31
32use db::Db;
33use error::Result;
34use package::MetaPackage;
35
36/// Little helper struct that logically groups a system's
37/// databases together, and makes construction and passing around
38/// a reference easier.
39//TODO: System configuration associated with this
40pub struct Alpm {
41    pub local_db: Db,
42    pub sync_dbs: Vec<Db>
43}
44
45impl Alpm {
46    /// Create an Alpm instance using multiple threads (WIP: Threading is hard)
47    pub fn new(location: impl AsRef<Path> + Sync) -> Result<Alpm> {
48        let (local, sync) = rayon::join(
49            || Db::local_db(&location),
50            || Db::sync_dbs(&location)
51        );
52        Ok(Alpm {
53            local_db: local?,
54            sync_dbs: sync?
55        })
56    }
57    
58    /// Easy iterator over all packages on the system that are installed and are
59    /// not found in the local database.
60    pub fn foreign_pkgs<'a>(&'a self) -> impl Iterator<Item = &'a MetaPackage> {
61        debug!("computing foreign packages");
62        self.local_db.packages.iter()
63            // We want all the pkgs that are foreign, so filter the ones that aren't
64            .filter(move |package| !is_pkg_foreign(&self.sync_dbs, &package.name) )
65    }
66}
67
68fn is_pkg_foreign(sync_dbs: &[Db], pkg_name: &str) -> bool {
69    sync_dbs.iter()
70        .any(|db| db.pkg(&pkg_name).is_some() )
71}
72
73/// This is some package info that can be gained just from parsing file names, not
74/// tucking into the tar entries that contain more extensive metadata.
75#[derive(Debug)]
76pub struct PkgSpec {
77    pub name: String,
78    version: String,
79    release: String,
80    pub arch: Option<String>
81}
82
83impl PkgSpec {
84    /// Parse a package specifier. `None` is returned if the provided
85    /// string is not a valid specifier. Any of the following are valid:
86    /// `pacman-5.1.1-2`, `pacman-5.1.1-2/`, or `pacman-5.1.1-2/desc`
87    /// (trailing paths are removed).
88    //TODO: Make this more legible
89    //TODO: Merge with split_pkgname
90    pub fn split_specifier(specifier: &str) -> Option<PkgSpec> {
91        let specifier = if let Some(indx) = specifier.find('/') {
92            &specifier[..indx]
93        } else {
94            specifier
95        };
96        let mut parts = specifier.rsplit('-');
97        let rel = parts.next();
98        let version = parts.next();
99        
100        let name: Vec<&str> = parts.rev().collect();
101        let name = name.join("-");
102        
103        if let None = rel {
104            None
105        } else if let None = version {
106            None
107        } else {
108            Some(PkgSpec {
109                name: name,
110                version: version.unwrap().to_string(),
111                release: rel.unwrap().to_string(),
112                arch: None
113            })
114        }
115    }
116    
117    /// Parse a full package name, such as `pacman-5.1.1-2-x86_64`
118    /// This is similar to [split_specifier](struct.PkgSpec.html#method.split_specifier),
119    /// except it does not accept trailing paths, and includes an
120    /// architecture specifier on the backend.
121    pub fn split_pkgname(name: &str) -> Option<PkgSpec> {
122        if let Some(indx) = name.rfind('-') {
123            let pkgspec = &name[..indx];
124            if let Some(mut pkgspec) = PkgSpec::split_specifier(pkgspec) {
125                pkgspec.arch = Some(name[indx + 1..].to_string());
126                Some(pkgspec)
127            } else {
128                None
129            }
130        } else {
131            None
132        }
133    }
134    
135    // Ugly as hell for API
136    pub fn version_str(&self) -> String {
137        format!("{}-{}", self.version, self.release)
138    }
139}
140
141#[derive(Clone, Debug, Hash, Eq, PartialEq)]
142enum Comp {
143    Eq,
144    Lt,
145    Le,
146    Ge,
147    Gt,
148}
149
150impl Comp {
151    fn make_inclusive(self) -> Comp {
152        match self {
153            Comp::Lt => Comp::Le,
154            Comp::Gt => Comp::Ge,
155            _ => self
156        }
157    }
158}
159
160impl Display for Comp {
161    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
162        write!(f, "{}", match self {
163            Comp::Eq => "=",
164            Comp::Lt => "<",
165            Comp::Le => "<=",
166            Comp::Ge => ">=",
167            Comp::Gt => ">"
168        })
169    }
170}
171
172/// This either represents a virtual package, or an actual
173/// package in a databse. It can be used to query a db.
174#[derive(Clone, Debug, Hash, Eq, PartialEq)]
175pub struct Provide {
176    pub name: String,
177    version: Option<String>,
178    comp: Comp,
179}
180
181impl Provide {
182    /// This will only return None if `data == ""`
183    /// Valid Provides are a package name, or a package name
184    /// and version, delimited by an equals sign (`pacman=5.1.1`),
185    ///  or one of the comparison operators (as detailed on the
186    /// [archwiki](https://wiki.archlinux.org/index.php/PKGBUILD#Dependencies)).
187    // Currently clones the string it's given, API suggestions welcome
188    // TODO: Parse other equality operators too
189    pub fn parse(data: impl AsRef<str>) -> Option<Provide> {
190        let data = data.as_ref();
191        let mut comp = Comp::Eq;
192        
193        let mut data = data.splitn(2, |c| {
194            if c == '<' {
195                comp = Comp::Lt;
196                true
197            } else if c == '>' {
198                comp = Comp::Gt;
199                true
200            } else if c == '=' {
201                comp = Comp::Eq;
202                true
203            } else {
204                false
205            }
206        });
207        
208        let name = data.next()?.to_string();
209        
210        let version = if let Some(version) = data.next() {
211            if version.chars().nth(0) == Some('=') {
212                comp = comp.make_inclusive();
213                Some(version[1..].to_string())
214            } else {
215                Some(version.to_string())
216            }
217        } else { None };
218        
219        Some(Provide { name, version, comp })
220    }
221    
222    /// If the two Provides have the same name, and `self.version()` is
223    /// acceptable by the terms of the `comp` field of both provides.
224    // It really feels like I'm just doing stuff here and
225    pub fn satisfies(&self, other: &Provide) -> bool {
226        self.name == other.name && if let Some(ref other_ver) = other.version() {
227            if let Some(ref ver) = self.version() {
228                // ver >= other_ver
229                match other.comp {
230                    Comp::Eq => ver >= other_ver,
231                    Comp::Lt => ver < other_ver,
232                    Comp::Le => ver <= other_ver,
233                    Comp::Ge => ver >= other_ver,
234                    Comp::Gt => ver > other_ver,
235                }
236            } else {
237                // This maybe should fail instead of just giving false
238                //   At least give a clue what is going on
239                false
240            }
241        } else {
242            //`None` accepts any version
243            true
244        }
245    }
246    
247    pub fn version(&self) -> Option<Version> {
248        self.version.as_ref().and_then(|ver| {
249            Version::from(ver)
250        })
251    }
252}
253
254impl Display for Provide {
255    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
256        match &self.version {
257            Some(version) => write!(f, "{}{}{}", self.name, self.comp, version),
258            None => write!(f, "{}", self.name)
259        }
260    }
261}
262
263// Tests require curl
264#[cfg(test)]
265mod tests {
266    use std::fs::create_dir_all;
267    use std::path::Path;
268    use std::process::Command;
269    
270    use {Db, package::Package, PkgSpec, Provide};
271    
272    #[test]
273    fn test_pkgload() {
274        let test_dir = Path::join(Path::new(env!("CARGO_MANIFEST_DIR")), "tests");
275        create_dir_all(&test_dir).unwrap();  // Took me a year to realize this wasn't being created...
276        let wget = Command::new("curl")
277            .arg("-O")
278            // Should come up with a more robust way of doing this
279            .arg("https://sgp.mirror.pkgbuild.com/community/os/x86_64/ascii-3.18-1-x86_64.pkg.tar.xz")
280            .arg("--output")
281            .arg("ascii-3.18-1-x86_64.pkg.tar.xz")
282            .current_dir(&test_dir)
283            .spawn().unwrap()
284            .wait().unwrap();
285        
286        if wget.success() {
287            let filename = Path::join(&test_dir, "ascii-3.18-1-x86_64.pkg.tar.xz");
288            let pkg = Package::load(filename).unwrap();
289            
290            assert_eq!(pkg.meta.name, "ascii".to_string());
291            assert_eq!(pkg.meta.version().unwrap().as_str(), "3.18-1");
292        }
293    }
294    
295    #[test]
296    fn test_split_specifier() {
297        let pkgspec = PkgSpec::split_specifier("pacman-5.1.1-2").unwrap();
298        assert_eq!(&pkgspec.name, "pacman");
299        assert_eq!(&pkgspec.version, "5.1.1");
300        assert_eq!(pkgspec.release, 2.to_string());
301        assert!(pkgspec.arch.is_none());
302        
303        let pkgspec = PkgSpec::split_specifier("pithos-git-1.4.1-1/").unwrap();
304        assert_eq!(&pkgspec.name, "pithos-git");
305        assert_eq!(&pkgspec.version, "1.4.1");
306        assert_eq!(pkgspec.release, 1.to_string());
307        assert!(pkgspec.arch.is_none());
308        
309        let pkgspec = PkgSpec::split_specifier("acorn-5.7.2-1/desc").unwrap();
310        assert_eq!(&pkgspec.name, "acorn");
311        assert_eq!(&pkgspec.version, "5.7.2");
312        assert_eq!(pkgspec.release, 1.to_string());
313        assert!(pkgspec.arch.is_none());
314        
315        // split_specifier is _supposed_ to return None if it gets an arch too,
316        //   this is related to the todos above
317        /*
318        let pkgspec = PkgSpec::split_specifier("pacman-5.1.1-2-x86_64");
319        println!("{:?}", pkgspec);
320        assert!(pkgspec.is_none());
321        */
322    }
323    
324    #[test]
325    fn test_split_pkgname() {
326        let pkgspec = PkgSpec::split_pkgname("pacman-5.1.1-2-x86_64").unwrap();
327        assert_eq!(&pkgspec.name, "pacman");
328        assert_eq!(&pkgspec.version, "5.1.1");
329        assert_eq!(pkgspec.release, 2.to_string());
330        assert_eq!(&pkgspec.arch.unwrap(), "x86_64");
331        
332        let pkgspec = PkgSpec::split_pkgname("pithos-git-1.4.1-1-any").unwrap();
333        assert_eq!(&pkgspec.name, "pithos-git");
334        assert_eq!(&pkgspec.version, "1.4.1");
335        assert_eq!(pkgspec.release, 1.to_string());
336        assert_eq!(&pkgspec.arch.unwrap(), "any");
337    }
338    
339    #[test]
340    fn test_localdb_parse() {
341        let _db = Db::local_db("/var/lib/pacman").unwrap();
342    }
343    
344    #[test]
345    fn test_syncdb_parse() {
346        let _dbs = Db::sync_dbs("/var/lib/pacman").unwrap();
347    }
348    
349    // Requires that java-environment=8 is provided
350    // This works on my system at the time of writing
351    #[test]
352    #[ignore]
353    fn test_is_package_provided() {
354        let db = Db::local_db("/var/lib/pacman").unwrap();
355        assert!(db.provides(&Provide { name: "java-environment".to_string(), version: Some("8".to_string()) }));
356    }
357}