pkg-utils 0.1.0

A reimplementation of some pacman functionality in pure rust. WIP
Documentation
//! Provides structs with both metadata about a package
//! and a representation of a package on the disk.

use std::io::Read;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;

use failure::err_msg;
use tar::Archive;
use version_compare::Version;
use xz2::read::XzDecoder;

use Provide;
use error::{ParseError, Result};

/// This is a handle to an actual package on the filesystem.
// In the loosest of terms.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Package {
    pub filename: PathBuf,
    pub meta: MetaPackage
}

impl Package {
    /// Load package info from a `.pkg.tar.xz` on the filesystem
    pub fn load(filename: impl AsRef<Path>) -> Result<Package> {
        let tarball_file = File::open(&filename)?;
        let xz_decoder = XzDecoder::new(tarball_file);
        let mut tar_decoder = Archive::new(xz_decoder);
        for entry in tar_decoder.entries()? {
            let mut entry = entry?;
            if entry.path()? == Path::new(".PKGINFO") {
                let mut data = String::new();
                entry.read_to_string(&mut data)?;
                let meta_package = MetaPackage::parse_pkginfo(data)?;
                let package = Package {
                    filename: filename.as_ref().to_path_buf(),
                    meta: meta_package
                };
                return Ok(package);
            }
        }
        Err(err_msg("tarball missing a .PKGINFO entry"))
    }
}

/// This represents metadata about a package. Construction should
/// only be done via [`Package`](struct.Package.html) or [`Db`](struct.Db.html).
/// For the most part, fields that were not found by my primitive
/// parsers will just be empty. This isn't really ideal in the case of
/// the string members.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct MetaPackage {
    pub name: String,
    version: String,
    pub description: String,
    //TODO: Use a crate url type
    pub url: String,
    // pub build_date: ?,
    pub packager: String,
    // Installed size
    //pub size: ?,
    pub architecture: String,
    pub groups: Vec<String>,
    pub licenses: Vec<String>,
    pub depends: Vec<String>,
    pub optdepends: Vec<String>,
    pub makedepends: Vec<String>,
    pub conflicts: Vec<String>,
    pub replaces: Vec<String>,
    pub provides: Vec<Provide>,
    pub backups: Vec<String>
}
/* Just an idea to try and make things more efficient
macro_rules! declare_pkg_vars {
    () => {
        let mut name: String;
        let mut version: String;
        let mut description: String;
        let mut url: String;
        let mut packager: String;
        let mut architecture: String;
        
        let mut groups: Vec<String>;
        let mut licenses: Vec<String>;
        let mut depends: Vec<String>;
        let mut optdepends: Vec<String>;
        let mut makedepends: Vec<String>;
        let mut conflicts: Vec<String>;
        let mut replaces: Vec<String>;
        let mut provides: Vec<String>;
        let mut backups: Vec<String>;
    }
}

macro_rules! zip_pkg {
    () => {
        MetaPackage {
            name, version,
            description, url,
            packager, architecture,
            groups, licenses,
            depends, optdepends,
            makedepends, conflicts,
            replaces, provides,
            backups
        }
    }
}*/

// There are probably more efficient ways of doing this
macro_rules! empty_meta_pkg {
    () => {
        MetaPackage {
            name: String::new(),
            version: String::new(),
            description: String::new(),
            url: String::new(),
            packager: String::new(),
            architecture: String::new(),
            
            groups: Vec::new(),
            licenses: Vec::new(),
            depends: Vec::new(),
            optdepends: Vec::new(),
            makedepends: Vec::new(),
            conflicts: Vec::new(),
            replaces: Vec::new(),
            provides: Vec::new(),
            backups: Vec::new()
        }
    }
}

impl MetaPackage {
    /// This function parses a `.PKGINFO` file (usually found in the root of a `.pkg.tar.xz`)
    pub(crate) fn parse_pkginfo(pkginfo_data: String) -> StdResult<MetaPackage, ParseError> {
        let mut pkg = empty_meta_pkg!();
        
        let mut line_num = 0;
        
        for line in pkginfo_data.lines() {
            line_num += 1;
            
            let line = line.trim();
            if line.starts_with("#") || line.len() == 0 {
                continue
            }
            let mut pair = line.splitn(2, '=');
            let key = pair.next()
                .ok_or(ParseError::MissingKey(line_num))?
                .trim().to_string();
            let value = pair.next()
                .ok_or(ParseError::MissingValue(line_num, key.to_string()))?
                .trim().to_string();
            
            macro_rules! load_value {
                ($var:expr) => (if $var.len() == 0 {
                        $var = value;
                    } else {
                        return Err(ParseError::MultipleKeys(line_num, key.to_string()));
                    })
            }
            
            match key.as_str() {
                "pkgname" => load_value!(pkg.name),
                "pkgbase" => {}, // Unused
                "pkgver" => load_value!(pkg.version),
                "pkgdesc" => load_value!(pkg.description),
                "group" => pkg.groups.push(value),
                "url" => load_value!(pkg.url),
                "builddate" => {}, //TODO: Parse this
                "packager" => load_value!(pkg.packager),
                "size" => {}, //TODO: Parse this
                "arch" => load_value!(pkg.architecture),
                "license" => pkg.licenses.push(value),
                "depend" => pkg.depends.push(value),
                "optdepend" => pkg.optdepends.push(value),
                "makedepend" => pkg.makedepends.push(value),
                "conflict" => pkg.conflicts.push(value),
                "replaces" => pkg.replaces.push(value),
                //TODO: Error instead of unwrap
                "provides" => pkg.provides.push(Provide::parse(&value)
                    .expect(&format!("Invalid Provide: {}", value))),
                "backup" => pkg.backups.push(value),
                "force" => {}, // Deprecated
                "makepkgopt" => {}, // Unused
                // Really finickey parser, this...
                &_ => return Err(ParseError::UnknownKey(line_num, key.to_string()))
            }
        }
        Ok(pkg)
    }
    
    /// Parse a package description (`pkgspec/desc` in a db) from a database
    //    Maybe make a distinction somehow between a sync and local db?
    //    Their impls are a little different.
    pub(crate) fn parse_pkgdesc(desc_data: String) -> StdResult<MetaPackage, ParseError> {
        let mut pkg = empty_meta_pkg!();
        // Really not sure if this is going to be accurate.
        //   Probably doesn't matter
        let mut line_num: u32 = 0;
        
        for entry in desc_data.split("\n\n") {
            line_num += 1;
            let mut lines = entry.lines();
            // ignore empty entries
            if let Some(key) = lines.next() {
                line_num += 1;
                
                if key == "" {
                    continue;
                }
                
                macro_rules! load_value {
                    ($field:ident, $required:expr) => {
                        if let Some(val) = lines.next() {
                            line_num += 1;
                            pkg.$field = val.to_string();
                        } else if !$required {
                            line_num += 1;
                        } else {
                            return Err(ParseError::MissingValue(line_num, key.to_string()))
                        }
                    }
                }
                
                macro_rules! load_value_all {
                    ($field:ident) => {
                        pkg.$field = lines.map(|line| line.to_string() ).collect()
                    }
                }
                
                //BUG: This might not work right if it gets two instances of a key
                match key {
                    "%NAME%" => load_value!(name, true),
                    "%VERSION%" => load_value!(version, true),
                    "%FILENAME%" => {} //not sure
                    "%BASE%" => {}, // unused
                    "%DESC%" => load_value!(description, false),
                    "%GROUPS%" => load_value_all!(groups),
                    "%URL%" => load_value!(url, false),
                    "%LICENSE%" => load_value_all!(licenses),
                    "%ARCH%" => load_value!(architecture, true),
                    "%BUILDDATE%" => {}, //TODO
                    "%INSTALLDATE%" => {}, //TODO: local only
                    "%REASON%" => {}, //TODO: local only
                    "%PACKAGER%" => load_value!(packager, true),
                    "%SIZE%" => {}, //TODO
                    "%CSIZE%" => {}, // what is this
                    "%ISIZE%" => {}, // what is this
                    "%MD5SUM%" => {}, //TODO
                    "%SHA256SUM%" => {}, //TODO
                    "%PGPSIG%" => {}, // TODO
                    "%VALIDATION%" => {}, //TODO: local only
                    "%REPLACES%" => load_value_all!(replaces),
                    "%DEPENDS%" => load_value_all!(depends),
                    "%OPTDEPENDS%" => load_value_all!(optdepends),
                    "%MAKEDEPENDS%" => load_value_all!(makedepends),
                    "%CHECKDEPENDS%" => {}, // TODO
                    "%CONFLICTS%" => load_value_all!(conflicts),
                    //TODO: Err instead of unwrap
                    "%PROVIDES%" => pkg.provides = lines
                        .map(|line| Provide::parse(line)
                            .expect(&format!("Invalid Provide: {}", line)))
                        .collect(),
                    "%DELTAS%" => {}, //TODO
                    "%FILES%" => {}, //TODO
                    // Really finickey parser, this...
                    &_ => return Err(ParseError::UnknownKey(line_num, key.to_string()))
                }
            }
        }
        //let pkg = zip_pkg!();
        Ok(pkg)
    }
    
    /// Getter over a private field
    pub fn version(&self) -> Option<Version> {
        Version::from(&self.version)
    }
    
    /// `other.name` can either be this package name or be a
    /// package name provided by this package. Repects versions.
    pub fn satisfies(&self, other: &Provide) -> bool {
        // Legit impossible for this to panic
        let self_provide = Provide::parse(format!("{}={}", self.name, self.version))
            .expect(&format!("{:?} does not have a name", self));
        
        self_provide.satisfies(other) || self.provides.iter()
            .any(|provide| provide.satisfies(other) )
    }
}