Skip to content

Commit bba47ac

Browse files
authored
Merge pull request #9 from RustSec/lockfile
Lockfile support
2 parents a7d1157 + c8ef192 commit bba47ac

File tree

5 files changed

+155
-55
lines changed

5 files changed

+155
-55
lines changed

src/advisory.rs

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Advisory type and related parsing code
22
3-
use error::{Error, Result};
3+
use error::Result;
44
use semver::VersionReq;
55
use toml;
6+
use util;
67

78
/// An individual security advisory pertaining to a single vulnerability
89
#[derive(Debug, PartialEq)]
@@ -33,42 +34,13 @@ impl Advisory {
3334
/// Parse an Advisory from a TOML table object
3435
pub fn from_toml_table(value: &toml::value::Table) -> Result<Advisory> {
3536
Ok(Advisory {
36-
id: try!(parse_mandatory_string(value, "id")),
37-
package: try!(parse_mandatory_string(value, "package")),
38-
patched_versions: try!(parse_versions(&value["patched_versions"])),
39-
date: try!(parse_optional_string(value, "date")),
40-
url: try!(parse_optional_string(value, "url")),
41-
title: try!(parse_mandatory_string(value, "title")),
42-
description: try!(parse_mandatory_string(value, "description")),
37+
id: util::parse_mandatory_string(value, "id")?,
38+
package: util::parse_mandatory_string(value, "package")?,
39+
patched_versions: util::parse_versions(value, "patched_versions")?,
40+
date: util::parse_optional_string(value, "date")?,
41+
url: util::parse_optional_string(value, "url")?,
42+
title: util::parse_mandatory_string(value, "title")?,
43+
description: util::parse_mandatory_string(value, "description")?,
4344
})
4445
}
4546
}
46-
47-
fn parse_optional_string(table: &toml::value::Table, attribute: &str) -> Result<Option<String>> {
48-
match table.get(attribute) {
49-
Some(v) => Ok(Some(String::from(try!(v.as_str().ok_or(Error::InvalidAttribute))))),
50-
None => Ok(None),
51-
}
52-
}
53-
54-
fn parse_mandatory_string(table: &toml::value::Table, attribute: &str) -> Result<String> {
55-
let str = try!(parse_optional_string(table, attribute));
56-
str.ok_or(Error::MissingAttribute)
57-
}
58-
59-
fn parse_versions(value: &toml::Value) -> Result<Vec<VersionReq>> {
60-
match *value {
61-
toml::Value::Array(ref arr) => {
62-
let mut result = Vec::new();
63-
for version in arr {
64-
let version_str = try!(version.as_str().ok_or(Error::MissingAttribute));
65-
let version_req = try!(VersionReq::parse(version_str)
66-
.map_err(|_| Error::MalformedVersion));
67-
68-
result.push(version_req)
69-
}
70-
Ok(result)
71-
}
72-
_ => Err(Error::MissingAttribute),
73-
}
74-
}

src/error.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ use std::error::Error as StdError;
66
/// Custom error type for this library
77
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
88
pub enum Error {
9-
/// An error occurred while making a request to the advisory database
10-
Request,
9+
/// An error occurred performing an I/O operation (e.g. network, file)
10+
IO,
1111

1212
/// Advisory database server responded with an error
13-
Response,
13+
ServerResponse,
1414

1515
/// Couldn't parse response data
1616
Parse,
@@ -34,8 +34,8 @@ impl fmt::Display for Error {
3434
impl StdError for Error {
3535
fn description(&self) -> &str {
3636
match *self {
37-
Error::Request => "network request failed",
38-
Error::Response => "invalid response",
37+
Error::IO => "I/O operation failed",
38+
Error::ServerResponse => "invalid response",
3939
Error::Parse => "couldn't parse data",
4040
Error::MissingAttribute => "expected attribute missing",
4141
Error::InvalidAttribute => "attribute is not the expected type/format",

src/lib.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
#![deny(trivial_casts, trivial_numeric_casts)]
88
#![deny(unsafe_code, unstable_features, unused_import_braces, unused_qualifications)]
99

10+
pub mod advisory;
11+
pub mod error;
12+
pub mod lockfile;
13+
mod util;
14+
1015
extern crate reqwest;
1116
extern crate semver;
1217
extern crate toml;
1318

14-
pub mod advisory;
15-
pub mod error;
16-
1719
use advisory::Advisory;
1820
use error::{Error, Result};
1921
use semver::Version;
@@ -42,26 +44,26 @@ impl AdvisoryDatabase {
4244

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

4749
if !response.status().is_success() {
48-
return Err(Error::Response);
50+
return Err(Error::ServerResponse);
4951
}
5052

5153
let mut body = Vec::new();
52-
try!(response.read_to_end(&mut body).map_err(|_| Error::Response));
53-
let response_str = try!(str::from_utf8(&body).map_err(|_| Error::Parse));
54+
response.read_to_end(&mut body).or(Err(Error::ServerResponse))?;
55+
let response_str = str::from_utf8(&body).or(Err(Error::Parse))?;
5456

5557
Self::from_toml(response_str)
5658
}
5759

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

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

7577
for advisory_toml in advisories_toml.iter() {
7678
let advisory = match *advisory_toml {
77-
toml::Value::Table(ref table) => try!(Advisory::from_toml_table(table)),
79+
toml::Value::Table(ref table) => Advisory::from_toml_table(table)?,
7880
_ => return Err(Error::InvalidAttribute),
7981
};
8082

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

123125
for advisory in self.find_by_crate(crate_name) {
@@ -138,6 +140,7 @@ impl AdvisoryDatabase {
138140
#[cfg(test)]
139141
mod tests {
140142
use AdvisoryDatabase;
143+
use lockfile::Lockfile;
141144
use semver::VersionReq;
142145

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

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

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

192195
let ref crate_advisories = db.find_by_crate("sodiumoxide");
193-
assert_eq!(*example_advisory, crate_advisories[0])
196+
assert_eq!(*example_advisory, crate_advisories[0]);
197+
198+
let lockfile = Lockfile::load("Cargo.toml").unwrap();
199+
lockfile.vulnerabilities(&db).unwrap();
194200
}
195201
}

src/lockfile.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Types for representing Cargo.lock files
2+
3+
use AdvisoryDatabase;
4+
use advisory::Advisory;
5+
use error::{Error, Result};
6+
use std::fs::File;
7+
use std::io::Read;
8+
use std::path::Path;
9+
use toml;
10+
use util;
11+
12+
/// Entry from Cargo.lock's `[[package]]` array
13+
/// TODO: serde macros or switch to cargo's builtin types
14+
#[derive(Debug, PartialEq)]
15+
pub struct Package {
16+
/// Name of a dependent crate
17+
pub name: String,
18+
19+
/// Version of dependent crate
20+
pub version: String,
21+
}
22+
23+
/// Parsed Cargo.lock file containing dependencies
24+
#[derive(Debug, PartialEq)]
25+
pub struct Lockfile {
26+
/// Dependencies enumerated in the lockfile
27+
pub packages: Vec<Package>,
28+
}
29+
30+
impl Lockfile {
31+
/// Load lockfile from disk
32+
pub fn load(filename: &str) -> Result<Self> {
33+
let path = Path::new(filename);
34+
let mut file = File::open(&path).or(Err(Error::IO))?;
35+
let mut toml = String::new();
36+
37+
file.read_to_string(&mut toml).or(Err(Error::IO))?;
38+
Self::from_toml(&toml)
39+
}
40+
41+
/// Load lockfile from a TOML string
42+
pub fn from_toml(string: &str) -> Result<Self> {
43+
let toml = string.parse::<toml::Value>().or(Err(Error::Parse))?;
44+
45+
let packages_toml = match toml.get("package") {
46+
Some(&toml::Value::Array(ref arr)) => arr,
47+
_ => return Ok(Lockfile { packages: Vec::new() }),
48+
};
49+
50+
let mut packages = Vec::new();
51+
52+
for package in packages_toml {
53+
match *package {
54+
toml::Value::Table(ref table) => {
55+
packages.push(Package {
56+
name: util::parse_mandatory_string(table, "name")?,
57+
version: util::parse_mandatory_string(table, "version")?,
58+
})
59+
}
60+
_ => return Err(Error::InvalidAttribute),
61+
}
62+
}
63+
64+
Ok(Lockfile { packages: packages })
65+
}
66+
67+
/// Find all relevant vulnerabilities for this lockfile using the given database
68+
pub fn vulnerabilities<'a>(&self, db: &'a AdvisoryDatabase) -> Result<Vec<&'a Advisory>> {
69+
let mut result = Vec::new();
70+
71+
for package in &self.packages {
72+
result.extend(&db.find_vulns_for_crate(&package.name, &package.version)?)
73+
}
74+
75+
Ok(result)
76+
}
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use lockfile::Lockfile;
82+
83+
#[test]
84+
fn load_cargo_lockfile() {
85+
Lockfile::load("Cargo.lock").unwrap();
86+
}
87+
}

src/util.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use error::{Error, Result};
2+
use semver::VersionReq;
3+
use toml::value::Table;
4+
use toml::Value;
5+
6+
pub fn parse_optional_string(table: &Table, attribute: &str) -> Result<Option<String>> {
7+
match table.get(attribute) {
8+
Some(v) => Ok(Some(String::from(v.as_str().ok_or(Error::InvalidAttribute)?))),
9+
None => Ok(None),
10+
}
11+
}
12+
13+
pub fn parse_mandatory_string(table: &Table, attribute: &str) -> Result<String> {
14+
let str = parse_optional_string(table, attribute)?;
15+
str.ok_or(Error::MissingAttribute)
16+
}
17+
18+
pub fn parse_versions(table: &Table, attribute: &str) -> Result<Vec<VersionReq>> {
19+
match table.get(attribute) {
20+
Some(&Value::Array(ref arr)) => {
21+
let mut result = Vec::new();
22+
23+
for version in arr {
24+
let version_str = version.as_str().ok_or(Error::MissingAttribute)?;
25+
let version_req = VersionReq::parse(version_str).or(Err(Error::MalformedVersion))?;
26+
27+
result.push(version_req)
28+
}
29+
30+
Ok(result)
31+
}
32+
Some(_) => Err(Error::InvalidAttribute),
33+
None => Err(Error::MissingAttribute),
34+
}
35+
}

0 commit comments

Comments
 (0)