Skip to content

Commit

Permalink
Lockfile support
Browse files Browse the repository at this point in the history
Adds a lockfile::Lockfile type for modeling Cargo.lock files

This could potentially be replaced with built-in types from Cargo, but suffices
to meet RustSec's needs for now.
  • Loading branch information
tarcieri committed Mar 6, 2017
1 parent a7d1157 commit c8ef192
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 55 deletions.
46 changes: 9 additions & 37 deletions src/advisory.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Advisory type and related parsing code
use error::{Error, Result};
use error::Result;
use semver::VersionReq;
use toml;
use util;

/// An individual security advisory pertaining to a single vulnerability
#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -33,42 +34,13 @@ impl Advisory {
/// Parse an Advisory from a TOML table object
pub fn from_toml_table(value: &toml::value::Table) -> Result<Advisory> {
Ok(Advisory {
id: try!(parse_mandatory_string(value, "id")),
package: try!(parse_mandatory_string(value, "package")),
patched_versions: try!(parse_versions(&value["patched_versions"])),
date: try!(parse_optional_string(value, "date")),
url: try!(parse_optional_string(value, "url")),
title: try!(parse_mandatory_string(value, "title")),
description: try!(parse_mandatory_string(value, "description")),
id: util::parse_mandatory_string(value, "id")?,
package: util::parse_mandatory_string(value, "package")?,
patched_versions: util::parse_versions(value, "patched_versions")?,
date: util::parse_optional_string(value, "date")?,
url: util::parse_optional_string(value, "url")?,
title: util::parse_mandatory_string(value, "title")?,
description: util::parse_mandatory_string(value, "description")?,
})
}
}

fn parse_optional_string(table: &toml::value::Table, attribute: &str) -> Result<Option<String>> {
match table.get(attribute) {
Some(v) => Ok(Some(String::from(try!(v.as_str().ok_or(Error::InvalidAttribute))))),
None => Ok(None),
}
}

fn parse_mandatory_string(table: &toml::value::Table, attribute: &str) -> Result<String> {
let str = try!(parse_optional_string(table, attribute));
str.ok_or(Error::MissingAttribute)
}

fn parse_versions(value: &toml::Value) -> Result<Vec<VersionReq>> {
match *value {
toml::Value::Array(ref arr) => {
let mut result = Vec::new();
for version in arr {
let version_str = try!(version.as_str().ok_or(Error::MissingAttribute));
let version_req = try!(VersionReq::parse(version_str)
.map_err(|_| Error::MalformedVersion));

result.push(version_req)
}
Ok(result)
}
_ => Err(Error::MissingAttribute),
}
}
10 changes: 5 additions & 5 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ use std::error::Error as StdError;
/// Custom error type for this library
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Error {
/// An error occurred while making a request to the advisory database
Request,
/// An error occurred performing an I/O operation (e.g. network, file)
IO,

/// Advisory database server responded with an error
Response,
ServerResponse,

/// Couldn't parse response data
Parse,
Expand All @@ -34,8 +34,8 @@ impl fmt::Display for Error {
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Error::Request => "network request failed",
Error::Response => "invalid response",
Error::IO => "I/O operation failed",
Error::ServerResponse => "invalid response",
Error::Parse => "couldn't parse data",
Error::MissingAttribute => "expected attribute missing",
Error::InvalidAttribute => "attribute is not the expected type/format",
Expand Down
32 changes: 19 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
#![deny(trivial_casts, trivial_numeric_casts)]
#![deny(unsafe_code, unstable_features, unused_import_braces, unused_qualifications)]

pub mod advisory;
pub mod error;
pub mod lockfile;
mod util;

extern crate reqwest;
extern crate semver;
extern crate toml;

pub mod advisory;
pub mod error;

use advisory::Advisory;
use error::{Error, Result};
use semver::Version;
Expand Down Expand Up @@ -42,26 +44,26 @@ impl AdvisoryDatabase {

/// Fetch advisory database from a custom URL
pub fn fetch_from_url(url: &str) -> Result<Self> {
let mut response = try!(reqwest::get(url).map_err(|_| Error::Request));
let mut response = reqwest::get(url).or(Err(Error::IO))?;

if !response.status().is_success() {
return Err(Error::Response);
return Err(Error::ServerResponse);
}

let mut body = Vec::new();
try!(response.read_to_end(&mut body).map_err(|_| Error::Response));
let response_str = try!(str::from_utf8(&body).map_err(|_| Error::Parse));
response.read_to_end(&mut body).or(Err(Error::ServerResponse))?;
let response_str = str::from_utf8(&body).or(Err(Error::Parse))?;

Self::from_toml(response_str)
}

/// Parse the advisory database from a TOML serialization of it
pub fn from_toml(data: &str) -> Result<Self> {
let db_toml = try!(data.parse::<toml::Value>().map_err(|_| Error::Parse));
let db_toml = data.parse::<toml::Value>().or(Err(Error::Parse))?;

let advisories_toml = match db_toml {
toml::Value::Table(ref table) => {
match *try!(table.get("advisory").ok_or(Error::MissingAttribute)) {
match *table.get("advisory").ok_or(Error::MissingAttribute)? {
toml::Value::Array(ref arr) => arr,
_ => return Err(Error::InvalidAttribute),
}
Expand All @@ -74,7 +76,7 @@ impl AdvisoryDatabase {

for advisory_toml in advisories_toml.iter() {
let advisory = match *advisory_toml {
toml::Value::Table(ref table) => try!(Advisory::from_toml_table(table)),
toml::Value::Table(ref table) => Advisory::from_toml_table(table)?,
_ => return Err(Error::InvalidAttribute),
};

Expand Down Expand Up @@ -117,7 +119,7 @@ impl AdvisoryDatabase {
crate_name: &str,
version_str: &str)
-> Result<Vec<&Advisory>> {
let version = try!(Version::parse(version_str).map_err(|_| Error::MalformedVersion));
let version = Version::parse(version_str).or(Err(Error::MalformedVersion))?;
let mut result = Vec::new();

for advisory in self.find_by_crate(crate_name) {
Expand All @@ -138,6 +140,7 @@ impl AdvisoryDatabase {
#[cfg(test)]
mod tests {
use AdvisoryDatabase;
use lockfile::Lockfile;
use semver::VersionReq;

pub const EXAMPLE_PACKAGE: &'static str = "heffalump";
Expand Down Expand Up @@ -173,7 +176,7 @@ mod tests {

// End-to-end integration test (has online dependency on GitHub)
#[test]
fn test_fetch() {
fn test_integration() {
let db = AdvisoryDatabase::fetch().unwrap();
let ref example_advisory = db.find("RUSTSEC-2017-0001").unwrap();

Expand All @@ -190,6 +193,9 @@ mod tests {
"The `scalarmult()` function in");

let ref crate_advisories = db.find_by_crate("sodiumoxide");
assert_eq!(*example_advisory, crate_advisories[0])
assert_eq!(*example_advisory, crate_advisories[0]);

let lockfile = Lockfile::load("Cargo.toml").unwrap();
lockfile.vulnerabilities(&db).unwrap();
}
}
87 changes: 87 additions & 0 deletions src/lockfile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Types for representing Cargo.lock files
use AdvisoryDatabase;
use advisory::Advisory;
use error::{Error, Result};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use toml;
use util;

/// Entry from Cargo.lock's `[[package]]` array
/// TODO: serde macros or switch to cargo's builtin types
#[derive(Debug, PartialEq)]
pub struct Package {
/// Name of a dependent crate
pub name: String,

/// Version of dependent crate
pub version: String,
}

/// Parsed Cargo.lock file containing dependencies
#[derive(Debug, PartialEq)]
pub struct Lockfile {
/// Dependencies enumerated in the lockfile
pub packages: Vec<Package>,
}

impl Lockfile {
/// Load lockfile from disk
pub fn load(filename: &str) -> Result<Self> {
let path = Path::new(filename);
let mut file = File::open(&path).or(Err(Error::IO))?;
let mut toml = String::new();

file.read_to_string(&mut toml).or(Err(Error::IO))?;
Self::from_toml(&toml)
}

/// Load lockfile from a TOML string
pub fn from_toml(string: &str) -> Result<Self> {
let toml = string.parse::<toml::Value>().or(Err(Error::Parse))?;

let packages_toml = match toml.get("package") {
Some(&toml::Value::Array(ref arr)) => arr,
_ => return Ok(Lockfile { packages: Vec::new() }),
};

let mut packages = Vec::new();

for package in packages_toml {
match *package {
toml::Value::Table(ref table) => {
packages.push(Package {
name: util::parse_mandatory_string(table, "name")?,
version: util::parse_mandatory_string(table, "version")?,
})
}
_ => return Err(Error::InvalidAttribute),
}
}

Ok(Lockfile { packages: packages })
}

/// Find all relevant vulnerabilities for this lockfile using the given database
pub fn vulnerabilities<'a>(&self, db: &'a AdvisoryDatabase) -> Result<Vec<&'a Advisory>> {
let mut result = Vec::new();

for package in &self.packages {
result.extend(&db.find_vulns_for_crate(&package.name, &package.version)?)
}

Ok(result)
}
}

#[cfg(test)]
mod tests {
use lockfile::Lockfile;

#[test]
fn load_cargo_lockfile() {
Lockfile::load("Cargo.lock").unwrap();
}
}
35 changes: 35 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use error::{Error, Result};
use semver::VersionReq;
use toml::value::Table;
use toml::Value;

pub fn parse_optional_string(table: &Table, attribute: &str) -> Result<Option<String>> {
match table.get(attribute) {
Some(v) => Ok(Some(String::from(v.as_str().ok_or(Error::InvalidAttribute)?))),
None => Ok(None),
}
}

pub fn parse_mandatory_string(table: &Table, attribute: &str) -> Result<String> {
let str = parse_optional_string(table, attribute)?;
str.ok_or(Error::MissingAttribute)
}

pub fn parse_versions(table: &Table, attribute: &str) -> Result<Vec<VersionReq>> {
match table.get(attribute) {
Some(&Value::Array(ref arr)) => {
let mut result = Vec::new();

for version in arr {
let version_str = version.as_str().ok_or(Error::MissingAttribute)?;
let version_req = VersionReq::parse(version_str).or(Err(Error::MalformedVersion))?;

result.push(version_req)
}

Ok(result)
}
Some(_) => Err(Error::InvalidAttribute),
None => Err(Error::MissingAttribute),
}
}

0 comments on commit c8ef192

Please sign in to comment.