1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
//! This crate implements a _subset_ of libalpm functionality,
//! basically what is required to read databases and parse things.
//!
//! The goal is not to replace libalpm for doing actual system
//! package management, but to be able to access metadata about an
//! Arch or Arch-based system without needing to use the cumbersome
//! C API. Generally this is an attempt to write a more flexible API
//! for a lot of the stuff that libalpm does, in order to facilitate
//! writing things like AUR helpers.

#[macro_use]
extern crate display_derive;
extern crate failure;
extern crate flate2;
extern crate itertools;
#[macro_use]
extern crate log;
extern crate rayon;
extern crate tar;
extern crate version_compare;
extern crate xz2;

pub mod db;
mod error;
pub mod package;

use std::fmt::{self, Formatter, Display};
use std::path::Path;

use version_compare::Version;

use db::Db;
use error::Result;
use package::MetaPackage;

/// Little helper struct that logically groups a system's
/// databases together, and makes construction and passing around
/// a reference easier.
//TODO: System configuration associated with this
pub struct Alpm {
    pub local_db: Db,
    pub sync_dbs: Vec<Db>
}

impl Alpm {
    /// Create an Alpm instance using multiple threads (WIP: Threading is hard)
    pub fn new(location: impl AsRef<Path> + Sync) -> Result<Alpm> {
        let (local, sync) = rayon::join(
            || Db::local_db(&location),
            || Db::sync_dbs(&location)
        );
        Ok(Alpm {
            local_db: local?,
            sync_dbs: sync?
        })
    }
    
    /// Easy iterator over all packages on the system that are installed and are
    /// not found in the local database.
    pub fn foreign_pkgs<'a>(&'a self) -> impl Iterator<Item = &'a MetaPackage> {
        debug!("computing foreign packages");
        self.local_db.packages.iter()
            // We want all the pkgs that are foreign, so filter the ones that aren't
            .filter(move |package| !is_pkg_foreign(&self.sync_dbs, &package.name) )
    }
}

fn is_pkg_foreign(sync_dbs: &[Db], pkg_name: &str) -> bool {
    sync_dbs.iter()
        .any(|db| db.pkg(&pkg_name).is_some() )
}

/// This is some package info that can be gained just from parsing file names, not
/// tucking into the tar entries that contain more extensive metadata.
#[derive(Debug)]
pub struct PkgSpec {
    pub name: String,
    version: String,
    release: String,
    pub arch: Option<String>
}

impl PkgSpec {
    /// Parse a package specifier. `None` is returned if the provided
    /// string is not a valid specifier. Any of the following are valid:
    /// `pacman-5.1.1-2`, `pacman-5.1.1-2/`, or `pacman-5.1.1-2/desc`
    /// (trailing paths are removed).
    //TODO: Make this more legible
    //TODO: Merge with split_pkgname
    pub fn split_specifier(specifier: &str) -> Option<PkgSpec> {
        let specifier = if let Some(indx) = specifier.find('/') {
            &specifier[..indx]
        } else {
            specifier
        };
        let mut parts = specifier.rsplit('-');
        let rel = parts.next();
        let version = parts.next();
        
        let name: Vec<&str> = parts.rev().collect();
        let name = name.join("-");
        
        if let None = rel {
            None
        } else if let None = version {
            None
        } else {
            Some(PkgSpec {
                name: name,
                version: version.unwrap().to_string(),
                release: rel.unwrap().to_string(),
                arch: None
            })
        }
    }
    
    /// Parse a full package name, such as `pacman-5.1.1-2-x86_64`
    /// This is similar to [split_specifier](struct.PkgSpec.html#method.split_specifier),
    /// except it does not accept trailing paths, and includes an
    /// architecture specifier on the backend.
    pub fn split_pkgname(name: &str) -> Option<PkgSpec> {
        if let Some(indx) = name.rfind('-') {
            let pkgspec = &name[..indx];
            if let Some(mut pkgspec) = PkgSpec::split_specifier(pkgspec) {
                pkgspec.arch = Some(name[indx + 1..].to_string());
                Some(pkgspec)
            } else {
                None
            }
        } else {
            None
        }
    }
    
    // Ugly as hell for API
    pub fn version_str(&self) -> String {
        format!("{}-{}", self.version, self.release)
    }
}

#[derive(Clone, Debug, Hash, Eq, PartialEq)]
enum Comp {
    Eq,
    Lt,
    Le,
    Ge,
    Gt,
}

impl Comp {
    fn make_inclusive(self) -> Comp {
        match self {
            Comp::Lt => Comp::Le,
            Comp::Gt => Comp::Ge,
            _ => self
        }
    }
}

impl Display for Comp {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}", match self {
            Comp::Eq => "=",
            Comp::Lt => "<",
            Comp::Le => "<=",
            Comp::Ge => ">=",
            Comp::Gt => ">"
        })
    }
}

/// This either represents a virtual package, or an actual
/// package in a databse. It can be used to query a db.
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Provide {
    pub name: String,
    version: Option<String>,
    comp: Comp,
}

impl Provide {
    /// This will only return None if `data == ""`
    /// Valid Provides are a package name, or a package name
    /// and version, delimited by an equals sign (`pacman=5.1.1`),
    ///  or one of the comparison operators (as detailed on the
    /// [archwiki](https://wiki.archlinux.org/index.php/PKGBUILD#Dependencies)).
    // Currently clones the string it's given, API suggestions welcome
    // TODO: Parse other equality operators too
    pub fn parse(data: impl AsRef<str>) -> Option<Provide> {
        let data = data.as_ref();
        let mut comp = Comp::Eq;
        
        let mut data = data.splitn(2, |c| {
            if c == '<' {
                comp = Comp::Lt;
                true
            } else if c == '>' {
                comp = Comp::Gt;
                true
            } else if c == '=' {
                comp = Comp::Eq;
                true
            } else {
                false
            }
        });
        
        let name = data.next()?.to_string();
        
        let version = if let Some(version) = data.next() {
            if version.chars().nth(0) == Some('=') {
                comp = comp.make_inclusive();
                Some(version[1..].to_string())
            } else {
                Some(version.to_string())
            }
        } else { None };
        
        Some(Provide { name, version, comp })
    }
    
    /// If the two Provides have the same name, and `self.version()` is
    /// acceptable by the terms of the `comp` field of both provides.
    // It really feels like I'm just doing stuff here and
    pub fn satisfies(&self, other: &Provide) -> bool {
        self.name == other.name && if let Some(ref other_ver) = other.version() {
            if let Some(ref ver) = self.version() {
                // ver >= other_ver
                match other.comp {
                    Comp::Eq => ver >= other_ver,
                    Comp::Lt => ver < other_ver,
                    Comp::Le => ver <= other_ver,
                    Comp::Ge => ver >= other_ver,
                    Comp::Gt => ver > other_ver,
                }
            } else {
                // This maybe should fail instead of just giving false
                //   At least give a clue what is going on
                false
            }
        } else {
            //`None` accepts any version
            true
        }
    }
    
    pub fn version(&self) -> Option<Version> {
        self.version.as_ref().and_then(|ver| {
            Version::from(ver)
        })
    }
}

impl Display for Provide {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        match &self.version {
            Some(version) => write!(f, "{}{}{}", self.name, self.comp, version),
            None => write!(f, "{}", self.name)
        }
    }
}

// Tests require curl
#[cfg(test)]
mod tests {
    use std::fs::create_dir_all;
    use std::path::Path;
    use std::process::Command;
    
    use {Db, package::Package, PkgSpec, Provide};
    
    #[test]
    fn test_pkgload() {
        let test_dir = Path::join(Path::new(env!("CARGO_MANIFEST_DIR")), "tests");
        create_dir_all(&test_dir).unwrap();  // Took me a year to realize this wasn't being created...
        let wget = Command::new("curl")
            .arg("-O")
            // Should come up with a more robust way of doing this
            .arg("https://sgp.mirror.pkgbuild.com/community/os/x86_64/ascii-3.18-1-x86_64.pkg.tar.xz")
            .arg("--output")
            .arg("ascii-3.18-1-x86_64.pkg.tar.xz")
            .current_dir(&test_dir)
            .spawn().unwrap()
            .wait().unwrap();
        
        if wget.success() {
            let filename = Path::join(&test_dir, "ascii-3.18-1-x86_64.pkg.tar.xz");
            let pkg = Package::load(filename).unwrap();
            
            assert_eq!(pkg.meta.name, "ascii".to_string());
            assert_eq!(pkg.meta.version().unwrap().as_str(), "3.18-1");
        }
    }
    
    #[test]
    fn test_split_specifier() {
        let pkgspec = PkgSpec::split_specifier("pacman-5.1.1-2").unwrap();
        assert_eq!(&pkgspec.name, "pacman");
        assert_eq!(&pkgspec.version, "5.1.1");
        assert_eq!(pkgspec.release, 2.to_string());
        assert!(pkgspec.arch.is_none());
        
        let pkgspec = PkgSpec::split_specifier("pithos-git-1.4.1-1/").unwrap();
        assert_eq!(&pkgspec.name, "pithos-git");
        assert_eq!(&pkgspec.version, "1.4.1");
        assert_eq!(pkgspec.release, 1.to_string());
        assert!(pkgspec.arch.is_none());
        
        let pkgspec = PkgSpec::split_specifier("acorn-5.7.2-1/desc").unwrap();
        assert_eq!(&pkgspec.name, "acorn");
        assert_eq!(&pkgspec.version, "5.7.2");
        assert_eq!(pkgspec.release, 1.to_string());
        assert!(pkgspec.arch.is_none());
        
        // split_specifier is _supposed_ to return None if it gets an arch too,
        //   this is related to the todos above
        /*
        let pkgspec = PkgSpec::split_specifier("pacman-5.1.1-2-x86_64");
        println!("{:?}", pkgspec);
        assert!(pkgspec.is_none());
        */
    }
    
    #[test]
    fn test_split_pkgname() {
        let pkgspec = PkgSpec::split_pkgname("pacman-5.1.1-2-x86_64").unwrap();
        assert_eq!(&pkgspec.name, "pacman");
        assert_eq!(&pkgspec.version, "5.1.1");
        assert_eq!(pkgspec.release, 2.to_string());
        assert_eq!(&pkgspec.arch.unwrap(), "x86_64");
        
        let pkgspec = PkgSpec::split_pkgname("pithos-git-1.4.1-1-any").unwrap();
        assert_eq!(&pkgspec.name, "pithos-git");
        assert_eq!(&pkgspec.version, "1.4.1");
        assert_eq!(pkgspec.release, 1.to_string());
        assert_eq!(&pkgspec.arch.unwrap(), "any");
    }
    
    #[test]
    fn test_localdb_parse() {
        let _db = Db::local_db("/var/lib/pacman").unwrap();
    }
    
    #[test]
    fn test_syncdb_parse() {
        let _dbs = Db::sync_dbs("/var/lib/pacman").unwrap();
    }
    
    // Requires that java-environment=8 is provided
    // This works on my system at the time of writing
    #[test]
    #[ignore]
    fn test_is_package_provided() {
        let db = Db::local_db("/var/lib/pacman").unwrap();
        assert!(db.provides(&Provide { name: "java-environment".to_string(), version: Some("8".to_string()) }));
    }
}