Merge pull request #44 from deps-rs/modernize

Modernize
This commit is contained in:
Felix Suchert 2020-09-29 20:11:59 +02:00 committed by GitHub
commit 59410cc7fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2304 additions and 1578 deletions

1817
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,35 +2,40 @@
name = "shiny-robots" name = "shiny-robots"
version = "0.1.0" version = "0.1.0"
authors = ["Sam Rijs <srijs@airpost.net>"] authors = ["Sam Rijs <srijs@airpost.net>"]
edition = "2018"
[workspace]
members = [
".",
"./libs/badge",
]
[dependencies] [dependencies]
cadence = "0.13.1" badge = { path = "./libs/badge" }
failure = "0.1.1"
futures = "0.1.18" anyhow = "1"
hyper = "0.11.15" cadence = "0.13"
hyper-tls = "0.1.2" derive_more = "0.99"
indexmap = { version = "1.0.0", features = ["serde-1"] } futures = "0.1"
lazy_static = "1.0.0" hyper = "0.11"
lru-cache = "0.1.1" hyper-tls = "0.1"
maud = "0.17.2" indexmap = { version = "1", features = ["serde-1"] }
relative-path = { version = "0.3.7", features = ["serde"] } lru-cache = "0.1"
route-recognizer = "0.1.12" maud = "0.22"
rustsec = "0.6.0" once_cell = "1.4"
semver = { version = "0.9.0", features = ["serde"] } relative-path = { version = "0.3.7", features = ["serde"] }
serde = "1.0.27" route-recognizer = "0.1"
serde_derive = "1.0.27" rustsec = "0.21"
serde_json = "1.0.9" semver = { version = "0.11", features = ["serde"] }
shared_failure = "0.1.0" serde = { version = "1", features = ["derive"] }
slog = "2.1.1" serde_json = "1"
slog-json = "2.2.0" slog = "2"
tokio-core = "0.1.12" slog-json = "2"
tokio-service = "0.1.0" tokio-core = "0.1"
toml = "0.4.5" tokio-service = "0.1"
try_future = "0.1.1" toml = "0.5"
try_future = "0.1"
[dependencies.badge]
version = "0.2.0"
path = "libs/badge"
[build-dependencies] [build-dependencies]
sass-rs = "0.2.1" sass-rs = "0.2"

View file

@ -11,8 +11,7 @@ fn build_style() -> String {
..Default::default() ..Default::default()
}; };
sass::compile_file("./assets/styles/main.sass", options) sass::compile_file("./assets/styles/main.sass", options).expect("failed to compile style sheet")
.expect("failed to compile style sheet")
} }
fn main() { fn main() {

1
libs/badge/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/test.svg

View file

@ -6,11 +6,12 @@ authors = ["Onur Aslan <onur@onur.im>"]
license-file = "LICENSE" license-file = "LICENSE"
repository = "https://github.com/onur/docs.rs" repository = "https://github.com/onur/docs.rs"
documentation = "https://docs.rs/badge" documentation = "https://docs.rs/badge"
edition = "2018"
[lib] [lib]
path = "badge.rs" path = "badge.rs"
[dependencies] [dependencies]
base64 = "0.9.0" base64 = "0.12"
lazy_static = "1.0.0" once_cell = "1"
rusttype = "0.5.0" rusttype = "0.9"

View file

@ -1,18 +1,16 @@
//! Simple badge generator //! Simple badge generator
extern crate base64;
#[macro_use] extern crate lazy_static;
extern crate rusttype;
use base64::display::Base64Display; use base64::display::Base64Display;
use rusttype::{Font, FontCollection, Scale, point, Point, PositionedGlyph}; use once_cell::sync::Lazy;
use rusttype::{point, Font, Point, PositionedGlyph, Scale};
const FONT_DATA: &'static [u8] =
const FONT_DATA: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/DejaVuSans.ttf"));
"/DejaVuSans.ttf"));
const FONT_SIZE: f32 = 11.; const FONT_SIZE: f32 = 11.;
const SCALE: Scale = Scale {
x: FONT_SIZE,
y: FONT_SIZE,
};
pub struct BadgeOptions { pub struct BadgeOptions {
/// Subject will be displayed on the left side of badge /// Subject will be displayed on the left side of badge
@ -23,7 +21,6 @@ pub struct BadgeOptions {
pub color: String, pub color: String,
} }
impl Default for BadgeOptions { impl Default for BadgeOptions {
fn default() -> BadgeOptions { fn default() -> BadgeOptions {
BadgeOptions { BadgeOptions {
@ -34,54 +31,47 @@ impl Default for BadgeOptions {
} }
} }
struct BadgeStaticData { struct BadgeStaticData {
font: Font<'static>, font: Font<'static>,
scale: Scale, scale: Scale,
offset: Point<f32> offset: Point<f32>,
} }
static DATA: Lazy<BadgeStaticData> = Lazy::new(|| {
let font = Font::try_from_bytes(FONT_DATA).expect("failed to parse font collection");
lazy_static! { let v_metrics = font.v_metrics(SCALE);
static ref DATA: BadgeStaticData = {
let collection = FontCollection::from_bytes(FONT_DATA)
.expect("failed to parse font collection");
let font = collection.into_font()
.expect("failed to load font data");
let scale = Scale {
x: FONT_SIZE,
y: FONT_SIZE,
};
let v_metrics = font.v_metrics(scale);
let offset = point(0.0, v_metrics.ascent); let offset = point(0.0, v_metrics.ascent);
BadgeStaticData { font, scale, offset } BadgeStaticData {
}; font,
} scale: SCALE.clone(),
offset,
}
});
pub struct Badge { pub struct Badge {
options: BadgeOptions options: BadgeOptions,
} }
impl Badge { impl Badge {
pub fn new(options: BadgeOptions) -> Badge { pub fn new(options: BadgeOptions) -> Badge {
Badge { options } Badge { options }
} }
pub fn to_svg_data_uri(&self) -> String { pub fn to_svg_data_uri(&self) -> String {
format!("data:image/svg+xml;base64,{}", format!(
Base64Display::standard(self.to_svg().as_bytes())) "data:image/svg+xml;base64,{}",
Base64Display::with_config(self.to_svg().as_bytes(), base64::STANDARD)
)
} }
pub fn to_svg(&self) -> String { pub fn to_svg(&self) -> String {
let left_width = self.calculate_width(&self.options.subject) + 6; let left_width = self.calculate_width(&self.options.subject) + 6;
let right_width = self.calculate_width(&self.options.status) + 6; let right_width = self.calculate_width(&self.options.status) + 6;
let svg = format!(r###"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{}" height="20"> let svg = format!(
r###"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{}" height="20">
<linearGradient id="smooth" x2="0" y2="100%"> <linearGradient id="smooth" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/> <stop offset="1" stop-opacity=".1"/>
@ -118,16 +108,17 @@ impl Badge {
left_width + (right_width / 2), left_width + (right_width / 2),
self.options.status, self.options.status,
left_width + (right_width / 2), left_width + (right_width / 2),
self.options.status); self.options.status
);
svg svg
} }
fn calculate_width(&self, text: &str) -> u32 { fn calculate_width(&self, text: &str) -> u32 {
let glyphs: Vec<PositionedGlyph> = let glyphs: Vec<PositionedGlyph> =
DATA.font.layout(text, DATA.scale, DATA.offset).collect(); DATA.font.layout(text, DATA.scale, DATA.offset).collect();
let width = glyphs.iter() let width = glyphs
.iter()
.rev() .rev()
.filter_map(|g| { .filter_map(|g| {
g.pixel_bounding_box() g.pixel_bounding_box()
@ -139,8 +130,6 @@ impl Badge {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

66
src/engine/fut/analyze.rs Normal file
View file

@ -0,0 +1,66 @@
use anyhow::{anyhow, ensure, Error};
use futures::stream::futures_unordered;
use futures::{Future, Poll, Stream};
use crate::models::crates::{AnalyzedDependencies, CrateDeps};
use super::super::machines::analyzer::DependencyAnalyzer;
use super::super::Engine;
pub struct AnalyzeDependenciesFuture {
inner: Box<dyn Future<Item = AnalyzedDependencies, Error = Error>>,
}
impl AnalyzeDependenciesFuture {
pub fn new(engine: Engine, deps: CrateDeps) -> Self {
let future =
engine.fetch_advisory_db().and_then(move |advisory_db| {
let analyzer = DependencyAnalyzer::new(&deps, Some(advisory_db));
let main_deps = deps.main.into_iter().filter_map(|(name, dep)| {
if dep.is_external() {
Some(name)
} else {
None
}
});
let dev_deps = deps.dev.into_iter().filter_map(|(name, dep)| {
if dep.is_external() {
Some(name)
} else {
None
}
});
let build_deps = deps.build.into_iter().filter_map(|(name, dep)| {
if dep.is_external() {
Some(name)
} else {
None
}
});
let release_futures =
engine.fetch_releases(main_deps.chain(dev_deps).chain(build_deps));
futures_unordered(release_futures)
.fold(analyzer, |mut analyzer, releases| {
analyzer.process(releases);
Ok(analyzer) as Result<_, Error>
})
.map(|analyzer| analyzer.finalize())
});
AnalyzeDependenciesFuture {
inner: Box::new(future),
}
}
}
impl Future for AnalyzeDependenciesFuture {
type Item = AnalyzedDependencies;
type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
self.inner.poll()
}
}

View file

@ -1,34 +1,40 @@
use std::mem; use std::mem;
use failure::Error; use anyhow::{anyhow, ensure, Error};
use futures::{Async, Future, Poll, Stream};
use futures::stream::FuturesOrdered; use futures::stream::FuturesOrdered;
use futures::{try_ready, Async, Future, Poll, Stream};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use ::models::repo::RepoPath; use crate::models::repo::RepoPath;
use super::super::Engine;
use super::super::machines::crawler::ManifestCrawler; use super::super::machines::crawler::ManifestCrawler;
pub use super::super::machines::crawler::ManifestCrawlerOutput; pub use super::super::machines::crawler::ManifestCrawlerOutput;
use super::super::Engine;
pub struct CrawlManifestFuture { pub struct CrawlManifestFuture {
repo_path: RepoPath, repo_path: RepoPath,
engine: Engine, engine: Engine,
crawler: ManifestCrawler, crawler: ManifestCrawler,
futures: FuturesOrdered<Box<Future<Item=(RelativePathBuf, String), Error=Error>>> futures: FuturesOrdered<Box<dyn Future<Item = (RelativePathBuf, String), Error = Error>>>,
} }
impl CrawlManifestFuture { impl CrawlManifestFuture {
pub fn new(engine: &Engine, repo_path: RepoPath, entry_point: RelativePathBuf) -> Self { pub fn new(engine: &Engine, repo_path: RepoPath, entry_point: RelativePathBuf) -> Self {
let future: Box<Future<Item=_, Error=_>> = Box::new(engine.retrieve_manifest_at_path(&repo_path, &entry_point) let future: Box<dyn Future<Item = _, Error = _>> = Box::new(
.map(move |contents| (entry_point, contents))); engine
.retrieve_manifest_at_path(&repo_path, &entry_point)
.map(move |contents| (entry_point, contents)),
);
let engine = engine.clone(); let engine = engine.clone();
let crawler = ManifestCrawler::new(); let crawler = ManifestCrawler::new();
let mut futures = FuturesOrdered::new(); let mut futures = FuturesOrdered::new();
futures.push(future); futures.push(future);
CrawlManifestFuture { CrawlManifestFuture {
repo_path, engine, crawler, futures repo_path,
engine,
crawler,
futures,
} }
} }
} }
@ -42,12 +48,15 @@ impl Future for CrawlManifestFuture {
None => { None => {
let crawler = mem::replace(&mut self.crawler, ManifestCrawler::new()); let crawler = mem::replace(&mut self.crawler, ManifestCrawler::new());
Ok(Async::Ready(crawler.finalize())) Ok(Async::Ready(crawler.finalize()))
}, }
Some((path, raw_manifest)) => { Some((path, raw_manifest)) => {
let output = self.crawler.step(path, raw_manifest)?; let output = self.crawler.step(path, raw_manifest)?;
for path in output.paths_of_interest.into_iter() { for path in output.paths_of_interest.into_iter() {
let future: Box<Future<Item=_, Error=_>> = Box::new(self.engine.retrieve_manifest_at_path(&self.repo_path, &path) let future: Box<dyn Future<Item = _, Error = _>> = Box::new(
.map(move |contents| (path, contents))); self.engine
.retrieve_manifest_at_path(&self.repo_path, &path)
.map(move |contents| (path, contents)),
);
self.futures.push(future); self.futures.push(future);
} }
self.poll() self.poll()

View file

@ -1,5 +1,5 @@
mod crawl;
mod analyze; mod analyze;
mod crawl;
pub use self::crawl::CrawlManifestFuture;
pub use self::analyze::AnalyzeDependenciesFuture; pub use self::analyze::AnalyzeDependenciesFuture;
pub use self::crawl::CrawlManifestFuture;

View file

@ -1,49 +0,0 @@
use failure::Error;
use futures::{Future, Poll, Stream};
use futures::stream::futures_unordered;
use ::models::crates::{AnalyzedDependencies, CrateDeps};
use super::super::Engine;
use super::super::machines::analyzer::DependencyAnalyzer;
pub struct AnalyzeDependenciesFuture {
inner: Box<Future<Item=AnalyzedDependencies, Error=Error>>
}
impl AnalyzeDependenciesFuture {
pub fn new(engine: Engine, deps: CrateDeps) -> Self {
let future = engine.fetch_advisory_db().and_then(move |advisory_db| {
let analyzer = DependencyAnalyzer::new(&deps, Some(advisory_db));
let main_deps = deps.main.into_iter().filter_map(|(name, dep)| {
if dep.is_external() { Some(name) } else { None }
});
let dev_deps = deps.dev.into_iter().filter_map(|(name, dep)| {
if dep.is_external() { Some(name) } else { None }
});
let build_deps = deps.build.into_iter().filter_map(|(name, dep)| {
if dep.is_external() { Some(name) } else { None }
});
let release_futures = engine.fetch_releases(main_deps.chain(dev_deps).chain(build_deps));
futures_unordered(release_futures)
.fold(analyzer, |mut analyzer, releases| { analyzer.process(releases); Ok(analyzer) as Result<_, Error> })
.map(|analyzer| analyzer.finalize())
});
AnalyzeDependenciesFuture {
inner: Box::new(future)
}
}
}
impl Future for AnalyzeDependenciesFuture {
type Item = AnalyzedDependencies;
type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
self.inner.poll()
}
}

View file

@ -1,24 +1,31 @@
use std::sync::Arc; use std::sync::Arc;
use rustsec::db::AdvisoryDatabase; use rustsec::database::{Database, Query};
use semver::Version; use semver::Version;
use ::models::crates::{CrateDeps, CrateRelease, CrateName, AnalyzedDependency, AnalyzedDependencies}; use crate::models::crates::{
AnalyzedDependencies, AnalyzedDependency, CrateDeps, CrateName, CrateRelease,
};
pub struct DependencyAnalyzer { pub struct DependencyAnalyzer {
deps: AnalyzedDependencies, deps: AnalyzedDependencies,
advisory_db: Option<Arc<AdvisoryDatabase>> advisory_db: Option<Arc<Database>>,
} }
impl DependencyAnalyzer { impl DependencyAnalyzer {
pub fn new(deps: &CrateDeps, advisory_db: Option<Arc<AdvisoryDatabase>>) -> DependencyAnalyzer { pub fn new(deps: &CrateDeps, advisory_db: Option<Arc<Database>>) -> DependencyAnalyzer {
DependencyAnalyzer { DependencyAnalyzer {
deps: AnalyzedDependencies::new(deps), deps: AnalyzedDependencies::new(deps),
advisory_db advisory_db,
} }
} }
fn process_single(name: &CrateName, dep: &mut AnalyzedDependency, ver: &Version, advisory_db: Option<&AdvisoryDatabase>) { fn process_single(
name: &CrateName,
dep: &mut AnalyzedDependency,
ver: &Version,
advisory_db: Option<&Database>,
) {
if dep.required.matches(&ver) { if dep.required.matches(&ver) {
if let Some(ref mut current_latest_that_matches) = dep.latest_that_matches { if let Some(ref mut current_latest_that_matches) = dep.latest_that_matches {
if *current_latest_that_matches < *ver { if *current_latest_that_matches < *ver {
@ -28,7 +35,14 @@ impl DependencyAnalyzer {
dep.latest_that_matches = Some(ver.clone()); dep.latest_that_matches = Some(ver.clone());
} }
if !advisory_db.map(|db| db.find_vulns_for_crate(name.as_ref(), ver).is_empty()).unwrap_or(true) { let name: rustsec::cargo_lock::Name = name.as_ref().parse().unwrap();
let version: rustsec::cargo_lock::Version = ver.to_string().parse().unwrap();
let query = Query::new().package_version(name, version);
if !advisory_db
.map(|db| db.query(&query).is_empty())
.unwrap_or(true)
{
dep.insecure = true; dep.insecure = true;
} }
} }
@ -43,17 +57,32 @@ impl DependencyAnalyzer {
} }
} }
pub fn process<I: IntoIterator<Item=CrateRelease>>(&mut self, releases: I) { pub fn process<I: IntoIterator<Item = CrateRelease>>(&mut self, releases: I) {
let advisory_db = self.advisory_db.as_ref().map(|r| r.as_ref()); let advisory_db = self.advisory_db.as_ref().map(|r| r.as_ref());
for release in releases.into_iter().filter(|r| !r.yanked) { for release in releases.into_iter().filter(|r| !r.yanked) {
if let Some(main_dep) = self.deps.main.get_mut(&release.name) { if let Some(main_dep) = self.deps.main.get_mut(&release.name) {
DependencyAnalyzer::process_single(&release.name, main_dep, &release.version, advisory_db) DependencyAnalyzer::process_single(
&release.name,
main_dep,
&release.version,
advisory_db,
)
} }
if let Some(dev_dep) = self.deps.dev.get_mut(&release.name) { if let Some(dev_dep) = self.deps.dev.get_mut(&release.name) {
DependencyAnalyzer::process_single(&release.name, dev_dep, &release.version, advisory_db) DependencyAnalyzer::process_single(
&release.name,
dev_dep,
&release.version,
advisory_db,
)
} }
if let Some(build_dep) = self.deps.build.get_mut(&release.name) { if let Some(build_dep) = self.deps.build.get_mut(&release.name) {
DependencyAnalyzer::process_single(&release.name, build_dep, &release.version, advisory_db) DependencyAnalyzer::process_single(
&release.name,
build_dep,
&release.version,
advisory_db,
)
} }
} }
} }
@ -65,75 +94,157 @@ impl DependencyAnalyzer {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use models::crates::{CrateDep, CrateDeps, CrateRelease}; use crate::models::crates::{CrateDep, CrateDeps, CrateRelease};
use super::DependencyAnalyzer;
use super::*;
#[test] #[test]
fn tracks_latest_without_matching() { fn tracks_latest_without_matching() {
let mut deps = CrateDeps::default(); let mut deps = CrateDeps::default();
deps.main.insert("hyper".parse().unwrap(), CrateDep::External("^0.11.0".parse().unwrap())); deps.main.insert(
"hyper".parse().unwrap(),
CrateDep::External("^0.11.0".parse().unwrap()),
);
let mut analyzer = DependencyAnalyzer::new(&deps, None); let mut analyzer = DependencyAnalyzer::new(&deps, None);
analyzer.process(vec![ analyzer.process(vec![
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.0".parse().unwrap(), deps: Default::default(), yanked: false }, CrateRelease {
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.1".parse().unwrap(), deps: Default::default(), yanked: false } name: "hyper".parse().unwrap(),
version: "0.10.0".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
CrateRelease {
name: "hyper".parse().unwrap(),
version: "0.10.1".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
]); ]);
let analyzed = analyzer.finalize(); let analyzed = analyzer.finalize();
assert_eq!(analyzed.main.get("hyper").unwrap().latest_that_matches, None); assert_eq!(
assert_eq!(analyzed.main.get("hyper").unwrap().latest, Some("0.10.1".parse().unwrap())); analyzed.main.get("hyper").unwrap().latest_that_matches,
None
);
assert_eq!(
analyzed.main.get("hyper").unwrap().latest,
Some("0.10.1".parse().unwrap())
);
} }
#[test] #[test]
fn tracks_latest_that_matches() { fn tracks_latest_that_matches() {
let mut deps = CrateDeps::default(); let mut deps = CrateDeps::default();
deps.main.insert("hyper".parse().unwrap(), CrateDep::External("^0.10.0".parse().unwrap())); deps.main.insert(
"hyper".parse().unwrap(),
CrateDep::External("^0.10.0".parse().unwrap()),
);
let mut analyzer = DependencyAnalyzer::new(&deps, None); let mut analyzer = DependencyAnalyzer::new(&deps, None);
analyzer.process(vec![ analyzer.process(vec![
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.0".parse().unwrap(), deps: Default::default(), yanked: false }, CrateRelease {
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.1".parse().unwrap(), deps: Default::default(), yanked: false }, name: "hyper".parse().unwrap(),
CrateRelease { name: "hyper".parse().unwrap(), version: "0.11.0".parse().unwrap(), deps: Default::default(), yanked: false } version: "0.10.0".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
CrateRelease {
name: "hyper".parse().unwrap(),
version: "0.10.1".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
CrateRelease {
name: "hyper".parse().unwrap(),
version: "0.11.0".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
]); ]);
let analyzed = analyzer.finalize(); let analyzed = analyzer.finalize();
assert_eq!(analyzed.main.get("hyper").unwrap().latest_that_matches, Some("0.10.1".parse().unwrap())); assert_eq!(
assert_eq!(analyzed.main.get("hyper").unwrap().latest, Some("0.11.0".parse().unwrap())); analyzed.main.get("hyper").unwrap().latest_that_matches,
Some("0.10.1".parse().unwrap())
);
assert_eq!(
analyzed.main.get("hyper").unwrap().latest,
Some("0.11.0".parse().unwrap())
);
} }
#[test] #[test]
fn skips_yanked_releases() { fn skips_yanked_releases() {
let mut deps = CrateDeps::default(); let mut deps = CrateDeps::default();
deps.main.insert("hyper".parse().unwrap(), CrateDep::External("^0.10.0".parse().unwrap())); deps.main.insert(
"hyper".parse().unwrap(),
CrateDep::External("^0.10.0".parse().unwrap()),
);
let mut analyzer = DependencyAnalyzer::new(&deps, None); let mut analyzer = DependencyAnalyzer::new(&deps, None);
analyzer.process(vec![ analyzer.process(vec![
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.0".parse().unwrap(), deps: Default::default(), yanked: false }, CrateRelease {
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.1".parse().unwrap(), deps: Default::default(), yanked: true }, name: "hyper".parse().unwrap(),
version: "0.10.0".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
CrateRelease {
name: "hyper".parse().unwrap(),
version: "0.10.1".parse().unwrap(),
deps: Default::default(),
yanked: true,
},
]); ]);
let analyzed = analyzer.finalize(); let analyzed = analyzer.finalize();
assert_eq!(analyzed.main.get("hyper").unwrap().latest_that_matches, Some("0.10.0".parse().unwrap())); assert_eq!(
assert_eq!(analyzed.main.get("hyper").unwrap().latest, Some("0.10.0".parse().unwrap())); analyzed.main.get("hyper").unwrap().latest_that_matches,
Some("0.10.0".parse().unwrap())
);
assert_eq!(
analyzed.main.get("hyper").unwrap().latest,
Some("0.10.0".parse().unwrap())
);
} }
#[test] #[test]
fn skips_prereleases() { fn skips_prereleases() {
let mut deps = CrateDeps::default(); let mut deps = CrateDeps::default();
deps.main.insert("hyper".parse().unwrap(), CrateDep::External("^0.10.0".parse().unwrap())); deps.main.insert(
"hyper".parse().unwrap(),
CrateDep::External("^0.10.0".parse().unwrap()),
);
let mut analyzer = DependencyAnalyzer::new(&deps, None); let mut analyzer = DependencyAnalyzer::new(&deps, None);
analyzer.process(vec![ analyzer.process(vec![
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.0".parse().unwrap(), deps: Default::default(), yanked: false }, CrateRelease {
CrateRelease { name: "hyper".parse().unwrap(), version: "0.10.1-alpha".parse().unwrap(), deps: Default::default(), yanked: false }, name: "hyper".parse().unwrap(),
version: "0.10.0".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
CrateRelease {
name: "hyper".parse().unwrap(),
version: "0.10.1-alpha".parse().unwrap(),
deps: Default::default(),
yanked: false,
},
]); ]);
let analyzed = analyzer.finalize(); let analyzed = analyzer.finalize();
assert_eq!(analyzed.main.get("hyper").unwrap().latest_that_matches, Some("0.10.0".parse().unwrap())); assert_eq!(
assert_eq!(analyzed.main.get("hyper").unwrap().latest, Some("0.10.0".parse().unwrap())); analyzed.main.get("hyper").unwrap().latest_that_matches,
Some("0.10.0".parse().unwrap())
);
assert_eq!(
analyzed.main.get("hyper").unwrap().latest,
Some("0.10.0".parse().unwrap())
);
} }
} }

View file

@ -1,49 +1,57 @@
use std::collections::HashMap; use std::collections::HashMap;
use failure::Error; use anyhow::{anyhow, ensure, Error};
use relative_path::RelativePathBuf;
use indexmap::IndexMap; use indexmap::IndexMap;
use relative_path::RelativePathBuf;
use ::parsers::manifest::parse_manifest_toml; use crate::models::crates::{CrateDep, CrateDeps, CrateManifest, CrateName};
use ::models::crates::{CrateDep, CrateDeps, CrateName, CrateManifest}; use crate::parsers::manifest::parse_manifest_toml;
pub struct ManifestCrawlerOutput { pub struct ManifestCrawlerOutput {
pub crates: IndexMap<CrateName, CrateDeps> pub crates: IndexMap<CrateName, CrateDeps>,
} }
pub struct ManifestCrawlerStepOutput { pub struct ManifestCrawlerStepOutput {
pub paths_of_interest: Vec<RelativePathBuf> pub paths_of_interest: Vec<RelativePathBuf>,
} }
pub struct ManifestCrawler { pub struct ManifestCrawler {
manifests: HashMap<RelativePathBuf, CrateManifest>, manifests: HashMap<RelativePathBuf, CrateManifest>,
leaf_crates: IndexMap<CrateName, CrateDeps> leaf_crates: IndexMap<CrateName, CrateDeps>,
} }
impl ManifestCrawler { impl ManifestCrawler {
pub fn new() -> ManifestCrawler { pub fn new() -> ManifestCrawler {
ManifestCrawler { ManifestCrawler {
manifests: HashMap::new(), manifests: HashMap::new(),
leaf_crates: IndexMap::new() leaf_crates: IndexMap::new(),
} }
} }
pub fn step(&mut self, path: RelativePathBuf, raw_manifest: String) -> Result<ManifestCrawlerStepOutput, Error> { pub fn step(
&mut self,
path: RelativePathBuf,
raw_manifest: String,
) -> Result<ManifestCrawlerStepOutput, Error> {
let manifest = parse_manifest_toml(&raw_manifest)?; let manifest = parse_manifest_toml(&raw_manifest)?;
self.manifests.insert(path.clone(), manifest.clone()); self.manifests.insert(path.clone(), manifest.clone());
let mut output = ManifestCrawlerStepOutput { let mut output = ManifestCrawlerStepOutput {
paths_of_interest: vec![] paths_of_interest: vec![],
}; };
match manifest { match manifest {
CrateManifest::Package(name, deps) => { CrateManifest::Package(name, deps) => {
self.process_package(&path, name, deps, &mut output); self.process_package(&path, name, deps, &mut output);
}, }
CrateManifest::Workspace { members } => { CrateManifest::Workspace { members } => {
self.process_workspace(&path, &members, &mut output); self.process_workspace(&path, &members, &mut output);
}, }
CrateManifest::Mixed { name, deps, members } => { CrateManifest::Mixed {
name,
deps,
members,
} => {
self.process_package(&path, name, deps, &mut output); self.process_package(&path, name, deps, &mut output);
self.process_workspace(&path, &members, &mut output); self.process_workspace(&path, &members, &mut output);
} }
@ -52,15 +60,31 @@ impl ManifestCrawler {
Ok(output) Ok(output)
} }
fn register_interest(&mut self, base_path: &RelativePathBuf, path: &RelativePathBuf, output: &mut ManifestCrawlerStepOutput) { fn register_interest(
&mut self,
base_path: &RelativePathBuf,
path: &RelativePathBuf,
output: &mut ManifestCrawlerStepOutput,
) {
let full_path = base_path.join_normalized(path); let full_path = base_path.join_normalized(path);
if !self.manifests.contains_key(&full_path) { if !self.manifests.contains_key(&full_path) {
output.paths_of_interest.push(full_path); output.paths_of_interest.push(full_path);
} }
} }
fn process_package(&mut self, base_path: &RelativePathBuf, name: CrateName, deps: CrateDeps, output: &mut ManifestCrawlerStepOutput) { fn process_package(
for (_, dep) in deps.main.iter().chain(deps.dev.iter()).chain(deps.build.iter()) { &mut self,
base_path: &RelativePathBuf,
name: CrateName,
deps: CrateDeps,
output: &mut ManifestCrawlerStepOutput,
) {
for (_, dep) in deps
.main
.iter()
.chain(deps.dev.iter())
.chain(deps.build.iter())
{
if let &CrateDep::Internal(ref path) = dep { if let &CrateDep::Internal(ref path) = dep {
self.register_interest(base_path, path, output); self.register_interest(base_path, path, output);
} }
@ -69,8 +93,13 @@ impl ManifestCrawler {
self.leaf_crates.insert(name, deps); self.leaf_crates.insert(name, deps);
} }
fn process_workspace(&mut self, base_path: &RelativePathBuf, members: &[RelativePathBuf], output: &mut ManifestCrawlerStepOutput) { fn process_workspace(
for mut path in members { &mut self,
base_path: &RelativePathBuf,
members: &[RelativePathBuf],
output: &mut ManifestCrawlerStepOutput,
) {
for path in members {
if !path.ends_with("*") { if !path.ends_with("*") {
self.register_interest(base_path, path, output); self.register_interest(base_path, path, output);
} }
@ -79,7 +108,7 @@ impl ManifestCrawler {
pub fn finalize(self) -> ManifestCrawlerOutput { pub fn finalize(self) -> ManifestCrawlerOutput {
ManifestCrawlerOutput { ManifestCrawlerOutput {
crates: self.leaf_crates crates: self.leaf_crates,
} }
} }
} }
@ -89,8 +118,9 @@ mod tests {
use relative_path::RelativePath; use relative_path::RelativePath;
use semver::VersionReq; use semver::VersionReq;
use models::crates::CrateDep; use crate::models::crates::CrateDep;
use super::ManifestCrawler;
use super::*;
#[test] #[test]
fn simple_package_manifest() { fn simple_package_manifest() {
@ -99,7 +129,9 @@ mod tests {
name = "simpleton" name = "simpleton"
"#; "#;
let mut crawler = ManifestCrawler::new(); let mut crawler = ManifestCrawler::new();
let step_output = crawler.step("Cargo.toml".into(), manifest.to_string()).unwrap(); let step_output = crawler
.step("Cargo.toml".into(), manifest.to_string())
.unwrap();
assert_eq!(step_output.paths_of_interest.len(), 0); assert_eq!(step_output.paths_of_interest.len(), 0);
let output = crawler.finalize(); let output = crawler.finalize();
assert_eq!(output.crates.len(), 1); assert_eq!(output.crates.len(), 1);
@ -127,16 +159,24 @@ codegen = "0.0.1"
let output = crawler.finalize(); let output = crawler.finalize();
assert_eq!(output.crates.len(), 1); assert_eq!(output.crates.len(), 1);
assert_eq!(output.crates["more-complex"].main.len(), 2); assert_eq!(output.crates["more-complex"].main.len(), 2);
assert_eq!(output.crates["more-complex"].main.get("foo").unwrap(), assert_eq!(
&CrateDep::External(VersionReq::parse("0.30.0").unwrap())); output.crates["more-complex"].main.get("foo").unwrap(),
assert_eq!(output.crates["more-complex"].main.get("bar").unwrap(), &CrateDep::External(VersionReq::parse("0.30.0").unwrap())
&CrateDep::External(VersionReq::parse("1.2.0").unwrap())); );
assert_eq!(
output.crates["more-complex"].main.get("bar").unwrap(),
&CrateDep::External(VersionReq::parse("1.2.0").unwrap())
);
assert_eq!(output.crates["more-complex"].dev.len(), 1); assert_eq!(output.crates["more-complex"].dev.len(), 1);
assert_eq!(output.crates["more-complex"].dev.get("quickcheck").unwrap(), assert_eq!(
&CrateDep::External(VersionReq::parse("0.5").unwrap())); output.crates["more-complex"].dev.get("quickcheck").unwrap(),
&CrateDep::External(VersionReq::parse("0.5").unwrap())
);
assert_eq!(output.crates["more-complex"].build.len(), 1); assert_eq!(output.crates["more-complex"].build.len(), 1);
assert_eq!(output.crates["more-complex"].build.get("codegen").unwrap(), assert_eq!(
&CrateDep::External(VersionReq::parse("0.0.1").unwrap())); output.crates["more-complex"].build.get("codegen").unwrap(),
&CrateDep::External(VersionReq::parse("0.0.1").unwrap())
);
} }
#[test] #[test]
@ -226,10 +266,17 @@ features = ["use_std"]
"#; "#;
let mut crawler = ManifestCrawler::new(); let mut crawler = ManifestCrawler::new();
let step_output = crawler.step("".into(), futures_manifest.to_string()).unwrap(); let step_output = crawler
.step("".into(), futures_manifest.to_string())
.unwrap();
assert_eq!(step_output.paths_of_interest.len(), 1); assert_eq!(step_output.paths_of_interest.len(), 1);
assert_eq!(step_output.paths_of_interest[0].as_str(), "futures-cpupool"); assert_eq!(step_output.paths_of_interest[0].as_str(), "futures-cpupool");
let step_output = crawler.step("futures-cpupool".into(), futures_cpupool_manifest.to_string()).unwrap(); let step_output = crawler
.step(
"futures-cpupool".into(),
futures_cpupool_manifest.to_string(),
)
.unwrap();
assert_eq!(step_output.paths_of_interest.len(), 0); assert_eq!(step_output.paths_of_interest.len(), 0);
let output = crawler.finalize(); let output = crawler.finalize();
assert_eq!(output.crates.len(), 2); assert_eq!(output.crates.len(), 2);
@ -237,10 +284,20 @@ features = ["use_std"]
assert_eq!(output.crates["futures"].dev.len(), 0); assert_eq!(output.crates["futures"].dev.len(), 0);
assert_eq!(output.crates["futures"].build.len(), 0); assert_eq!(output.crates["futures"].build.len(), 0);
assert_eq!(output.crates["futures-cpupool"].main.len(), 2); assert_eq!(output.crates["futures-cpupool"].main.len(), 2);
assert_eq!(output.crates["futures-cpupool"].main.get("num_cpus").unwrap(), assert_eq!(
&CrateDep::External(VersionReq::parse("1.0").unwrap())); output.crates["futures-cpupool"]
assert_eq!(output.crates["futures-cpupool"].main.get("futures").unwrap(), .main
&CrateDep::Internal(RelativePath::new("..").to_relative_path_buf())); .get("num_cpus")
.unwrap(),
&CrateDep::External(VersionReq::parse("1.0").unwrap())
);
assert_eq!(
output.crates["futures-cpupool"]
.main
.get("futures")
.unwrap(),
&CrateDep::Internal(RelativePath::new("..").to_relative_path_buf())
);
assert_eq!(output.crates["futures-cpupool"].dev.len(), 0); assert_eq!(output.crates["futures-cpupool"].dev.len(), 0);
assert_eq!(output.crates["futures-cpupool"].build.len(), 0); assert_eq!(output.crates["futures-cpupool"].build.len(), 0);
} }

View file

@ -1,2 +1,2 @@
pub mod crawler;
pub mod analyzer; pub mod analyzer;
pub mod crawler;

View file

@ -4,33 +4,34 @@ use std::time::{Duration, Instant};
use cadence::prelude::*; use cadence::prelude::*;
use cadence::{MetricSink, NopMetricSink, StatsdClient}; use cadence::{MetricSink, NopMetricSink, StatsdClient};
use failure::Error; use anyhow::{anyhow, ensure, Error};
use futures::{Future, future};
use futures::future::join_all; use futures::future::join_all;
use hyper::Client; use futures::{future, Future};
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::Client;
use hyper_tls::HttpsConnector; use hyper_tls::HttpsConnector;
use once_cell::sync::Lazy;
use relative_path::{RelativePath, RelativePathBuf}; use relative_path::{RelativePath, RelativePathBuf};
use rustsec::db::AdvisoryDatabase; use rustsec::database::Database;
use semver::VersionReq; use semver::VersionReq;
use slog::Logger; use slog::Logger;
use tokio_service::Service; use tokio_service::Service;
use crate::utils::cache::Cache;
use crate::models::crates::{AnalyzedDependencies, CrateName, CratePath, CrateRelease};
use crate::models::repo::{RepoPath, Repository};
use crate::interactors::crates::{GetPopularCrates, QueryCrate};
use crate::interactors::github::GetPopularRepos;
use crate::interactors::rustsec::FetchAdvisoryDatabase;
use crate::interactors::RetrieveFileAtPath;
mod fut;
mod machines; mod machines;
mod futures;
use ::utils::cache::Cache; use self::fut::AnalyzeDependenciesFuture;
use self::fut::CrawlManifestFuture;
use ::models::repo::{Repository, RepoPath};
use ::models::crates::{CrateName, CratePath, CrateRelease, AnalyzedDependencies};
use ::interactors::crates::{QueryCrate, GetPopularCrates};
use ::interactors::RetrieveFileAtPath;
use ::interactors::github::GetPopularRepos;
use ::interactors::rustsec::FetchAdvisoryDatabase;
use self::futures::AnalyzeDependenciesFuture;
use self::futures::CrawlManifestFuture;
type HttpClient = Client<HttpsConnector<HttpConnector>>; type HttpClient = Client<HttpsConnector<HttpConnector>>;
@ -44,7 +45,7 @@ pub struct Engine {
get_popular_crates: Arc<Cache<GetPopularCrates<HttpClient>>>, get_popular_crates: Arc<Cache<GetPopularCrates<HttpClient>>>,
get_popular_repos: Arc<Cache<GetPopularRepos<HttpClient>>>, get_popular_repos: Arc<Cache<GetPopularRepos<HttpClient>>>,
retrieve_file_at_path: Arc<RetrieveFileAtPath<HttpClient>>, retrieve_file_at_path: Arc<RetrieveFileAtPath<HttpClient>>,
fetch_advisory_db: Arc<Cache<FetchAdvisoryDatabase<HttpClient>>> fetch_advisory_db: Arc<Cache<FetchAdvisoryDatabase<HttpClient>>>,
} }
impl Engine { impl Engine {
@ -52,18 +53,26 @@ impl Engine {
let metrics = StatsdClient::from_sink("engine", NopMetricSink); let metrics = StatsdClient::from_sink("engine", NopMetricSink);
let query_crate = Cache::new(QueryCrate(client.clone()), Duration::from_secs(300), 500); let query_crate = Cache::new(QueryCrate(client.clone()), Duration::from_secs(300), 500);
let get_popular_crates = Cache::new(GetPopularCrates(client.clone()), Duration::from_secs(10), 1); let get_popular_crates =
let get_popular_repos = Cache::new(GetPopularRepos(client.clone()), Duration::from_secs(10), 1); Cache::new(GetPopularCrates(client.clone()), Duration::from_secs(10), 1);
let fetch_advisory_db = Cache::new(FetchAdvisoryDatabase(client.clone()), Duration::from_secs(300), 1); let get_popular_repos =
Cache::new(GetPopularRepos(client.clone()), Duration::from_secs(10), 1);
let fetch_advisory_db = Cache::new(
FetchAdvisoryDatabase(client.clone()),
Duration::from_secs(300),
1,
);
Engine { Engine {
client: client.clone(), logger, metrics, client: client.clone(),
logger,
metrics,
query_crate: Arc::new(query_crate), query_crate: Arc::new(query_crate),
get_popular_crates: Arc::new(get_popular_crates), get_popular_crates: Arc::new(get_popular_crates),
get_popular_repos: Arc::new(get_popular_repos), get_popular_repos: Arc::new(get_popular_repos),
retrieve_file_at_path: Arc::new(RetrieveFileAtPath(client)), retrieve_file_at_path: Arc::new(RetrieveFileAtPath(client)),
fetch_advisory_db: Arc::new(fetch_advisory_db) fetch_advisory_db: Arc::new(fetch_advisory_db),
} }
} }
@ -74,7 +83,7 @@ impl Engine {
pub struct AnalyzeDependenciesOutcome { pub struct AnalyzeDependenciesOutcome {
pub crates: Vec<(CrateName, AnalyzedDependencies)>, pub crates: Vec<(CrateName, AnalyzedDependencies)>,
pub duration: Duration pub duration: Duration,
} }
impl AnalyzeDependenciesOutcome { impl AnalyzeDependenciesOutcome {
@ -83,38 +92,42 @@ impl AnalyzeDependenciesOutcome {
} }
pub fn any_insecure(&self) -> bool { pub fn any_insecure(&self) -> bool {
self.crates.iter().any(|&(_, ref deps)| deps.count_insecure() > 0) self.crates
.iter()
.any(|&(_, ref deps)| deps.count_insecure() > 0)
} }
pub fn outdated_ratio(&self) -> (usize, usize) { pub fn outdated_ratio(&self) -> (usize, usize) {
self.crates.iter().fold((0, 0), |(outdated, total), &(_, ref deps)| { self.crates
.iter()
.fold((0, 0), |(outdated, total), &(_, ref deps)| {
(outdated + deps.count_outdated(), total + deps.count_total()) (outdated + deps.count_outdated(), total + deps.count_total())
}) })
} }
} }
impl Engine { impl Engine {
pub fn get_popular_repos(&self) -> pub fn get_popular_repos(&self) -> impl Future<Item = Vec<Repository>, Error = Error> {
impl Future<Item=Vec<Repository>, Error=Error> self.get_popular_repos.call(()).from_err().map(|repos| {
{ repos
self.get_popular_repos.call(()) .iter()
.from_err().map(|repos| { .filter(|repo| !POPULAR_REPO_BLOCK_LIST.contains(&repo.path))
repos.iter() .cloned()
.filter(|repo| !POPULAR_REPOS_BLACKLIST.contains(&repo.path)) .collect()
.cloned().collect()
}) })
} }
pub fn get_popular_crates(&self) -> pub fn get_popular_crates(&self) -> impl Future<Item = Vec<CratePath>, Error = Error> {
impl Future<Item=Vec<CratePath>, Error=Error> self.get_popular_crates
{ .call(())
self.get_popular_crates.call(()) .from_err()
.from_err().map(|crates| crates.clone()) .map(|crates| crates.clone())
} }
pub fn analyze_repo_dependencies(&self, repo_path: RepoPath) -> pub fn analyze_repo_dependencies(
impl Future<Item=AnalyzeDependenciesOutcome, Error=Error> &self,
{ repo_path: RepoPath,
) -> impl Future<Item = AnalyzeDependenciesOutcome, Error = Error> {
let start = Instant::now(); let start = Instant::now();
let entry_point = RelativePath::new("/").to_relative_path_buf(); let entry_point = RelativePath::new("/").to_relative_path_buf();
@ -123,99 +136,125 @@ impl Engine {
let engine = self.clone(); let engine = self.clone();
manifest_future.and_then(move |manifest_output| { manifest_future.and_then(move |manifest_output| {
let engine_for_analyze = engine.clone(); let engine_for_analyze = engine.clone();
let futures = manifest_output.crates.into_iter().map(move |(crate_name, deps)| { let futures = manifest_output
let analyzed_deps_future = AnalyzeDependenciesFuture::new(engine_for_analyze.clone(), deps); .crates
.into_iter()
.map(move |(crate_name, deps)| {
let analyzed_deps_future =
AnalyzeDependenciesFuture::new(engine_for_analyze.clone(), deps);
analyzed_deps_future.map(move |analyzed_deps| (crate_name, analyzed_deps)) analyzed_deps_future.map(move |analyzed_deps| (crate_name, analyzed_deps))
}); });
join_all(futures).and_then(move |crates| { join_all(futures).and_then(move |crates| {
let duration = start.elapsed(); let duration = start.elapsed();
engine.metrics.time_duration_with_tags("analyze_duration", duration) engine
.metrics
.time_duration_with_tags("analyze_duration", duration)
.with_tag("repo_site", repo_path.site.as_ref()) .with_tag("repo_site", repo_path.site.as_ref())
.with_tag("repo_qual", repo_path.qual.as_ref()) .with_tag("repo_qual", repo_path.qual.as_ref())
.with_tag("repo_name", repo_path.name.as_ref()) .with_tag("repo_name", repo_path.name.as_ref())
.send()?; .send()?;
Ok(AnalyzeDependenciesOutcome { Ok(AnalyzeDependenciesOutcome { crates, duration })
crates, duration
})
}) })
}) })
} }
pub fn analyze_crate_dependencies(&self, crate_path: CratePath) -> pub fn analyze_crate_dependencies(
impl Future<Item=AnalyzeDependenciesOutcome, Error=Error> &self,
{ crate_path: CratePath,
) -> impl Future<Item = AnalyzeDependenciesOutcome, Error = Error> {
let start = Instant::now(); let start = Instant::now();
let query_future = self.query_crate.call(crate_path.name.clone()).from_err(); let query_future = self.query_crate.call(crate_path.name.clone()).from_err();
let engine = self.clone(); let engine = self.clone();
query_future.and_then(move |query_response| { query_future.and_then(move |query_response| {
match query_response.releases.iter().find(|release| release.version == crate_path.version) { match query_response
None => future::Either::A(future::err(format_err!("could not find crate release with version {}", crate_path.version))), .releases
.iter()
.find(|release| release.version == crate_path.version)
{
None => future::Either::A(future::err(anyhow!(
"could not find crate release with version {}",
crate_path.version
))),
Some(release) => { Some(release) => {
let analyzed_deps_future = AnalyzeDependenciesFuture::new(engine.clone(), release.deps.clone()); let analyzed_deps_future =
AnalyzeDependenciesFuture::new(engine.clone(), release.deps.clone());
future::Either::B(analyzed_deps_future.map(move |analyzed_deps| { future::Either::B(analyzed_deps_future.map(move |analyzed_deps| {
let crates = vec![(crate_path.name, analyzed_deps)].into_iter().collect(); let crates = vec![(crate_path.name, analyzed_deps)].into_iter().collect();
let duration = start.elapsed(); let duration = start.elapsed();
AnalyzeDependenciesOutcome { AnalyzeDependenciesOutcome { crates, duration }
crates, duration
}
})) }))
} }
} }
}) })
} }
pub fn find_latest_crate_release(&self, name: CrateName, req: VersionReq) -> pub fn find_latest_crate_release(
impl Future<Item=Option<CrateRelease>, Error=Error> &self,
{ name: CrateName,
self.query_crate.call(name).from_err().map(move |query_response| { req: VersionReq,
query_response.releases.iter() ) -> impl Future<Item = Option<CrateRelease>, Error = Error> {
self.query_crate
.call(name)
.from_err()
.map(move |query_response| {
query_response
.releases
.iter()
.filter(|release| req.matches(&release.version)) .filter(|release| req.matches(&release.version))
.max_by(|r1, r2| r1.version.cmp(&r2.version)) .max_by(|r1, r2| r1.version.cmp(&r2.version))
.cloned() .cloned()
}) })
} }
fn fetch_releases<I: IntoIterator<Item=CrateName>>(&self, names: I) -> fn fetch_releases<I: IntoIterator<Item = CrateName>>(
impl Iterator<Item=impl Future<Item=Vec<CrateRelease>, Error=Error>> &self,
{ names: I,
) -> impl Iterator<Item = impl Future<Item = Vec<CrateRelease>, Error = Error>> {
let engine = self.clone(); let engine = self.clone();
names.into_iter().map(move |name| { names.into_iter().map(move |name| {
engine.query_crate.call(name) engine
.query_crate
.call(name)
.from_err() .from_err()
.map(|resp| resp.releases.clone()) .map(|resp| resp.releases.clone())
}) })
} }
fn retrieve_manifest_at_path(&self, repo_path: &RepoPath, path: &RelativePathBuf) -> fn retrieve_manifest_at_path(
impl Future<Item=String, Error=Error> &self,
{ repo_path: &RepoPath,
path: &RelativePathBuf,
) -> impl Future<Item = String, Error = Error> {
let manifest_path = path.join(RelativePath::new("Cargo.toml")); let manifest_path = path.join(RelativePath::new("Cargo.toml"));
self.retrieve_file_at_path.call((repo_path.clone(), manifest_path)) self.retrieve_file_at_path
.call((repo_path.clone(), manifest_path))
} }
fn fetch_advisory_db(&self) -> fn fetch_advisory_db(&self) -> impl Future<Item = Arc<Database>, Error = Error> {
impl Future<Item=Arc<AdvisoryDatabase>, Error=Error> self.fetch_advisory_db
{ .call(())
self.fetch_advisory_db.call(()).from_err().map(|db| db.clone()) .from_err()
.map(|db| db.clone())
} }
} }
lazy_static! { static POPULAR_REPO_BLOCK_LIST: Lazy<HashSet<RepoPath>> = Lazy::new(|| {
static ref POPULAR_REPOS_BLACKLIST: HashSet<RepoPath> = {
vec![ vec![
RepoPath::from_parts("github", "rust-lang", "rust"), RepoPath::from_parts("github", "rust-lang", "rust"),
RepoPath::from_parts("github", "google", "xi-editor"), RepoPath::from_parts("github", "google", "xi-editor"),
RepoPath::from_parts("github", "lk-geimfari", "awesomo"), RepoPath::from_parts("github", "lk-geimfari", "awesomo"),
RepoPath::from_parts("github", "redox-os", "tfs"), RepoPath::from_parts("github", "redox-os", "tfs"),
RepoPath::from_parts("github", "carols10cents", "rustlings"), RepoPath::from_parts("github", "carols10cents", "rustlings"),
RepoPath::from_parts("github", "rust-unofficial", "awesome-rust") RepoPath::from_parts("github", "rust-unofficial", "awesome-rust"),
].into_iter().collect::<Result<HashSet<_>, _>>().unwrap() ]
}; .into_iter()
} .collect::<Result<HashSet<_>, _>>()
.unwrap()
});

View file

@ -1,18 +1,19 @@
use failure::Error; use anyhow::{anyhow, ensure, Error};
use hyper::Uri; use hyper::Uri;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use ::models::repo::RepoPath; use crate::models::repo::RepoPath;
const BITBUCKET_USER_CONTENT_BASE_URI: &'static str = "https://bitbucket.org"; const BITBUCKET_USER_CONTENT_BASE_URI: &'static str = "https://bitbucket.org";
pub fn get_manifest_uri(repo_path: &RepoPath, path: &RelativePathBuf) -> Result<Uri, Error> { pub fn get_manifest_uri(repo_path: &RepoPath, path: &RelativePathBuf) -> Result<Uri, Error> {
let path_str: &str = path.as_ref(); let path_str: &str = path.as_ref();
Ok(format!("{}/{}/{}/raw/HEAD/{}", Ok(format!(
"{}/{}/{}/raw/HEAD/{}",
BITBUCKET_USER_CONTENT_BASE_URI, BITBUCKET_USER_CONTENT_BASE_URI,
repo_path.qual.as_ref(), repo_path.qual.as_ref(),
repo_path.name.as_ref(), repo_path.name.as_ref(),
path_str path_str
).parse::<Uri>()?) )
.parse::<Uri>()?)
} }

View file

@ -1,13 +1,13 @@
use std::str; use std::str;
use failure::Error; use anyhow::{anyhow, ensure, Error};
use futures::{Future, Stream, IntoFuture, future}; use futures::{future, Future, IntoFuture, Stream};
use hyper::{Error as HyperError, Method, Request, Response, Uri}; use hyper::{Error as HyperError, Method, Request, Response, Uri};
use tokio_service::Service;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use serde_json; use serde::Deserialize;
use tokio_service::Service;
use ::models::crates::{CrateName, CrateRelease, CrateDeps, CrateDep, CratePath}; use crate::models::crates::{CrateDep, CrateDeps, CrateName, CratePath, CrateRelease};
const CRATES_INDEX_BASE_URI: &str = "https://raw.githubusercontent.com/rust-lang/crates.io-index"; const CRATES_INDEX_BASE_URI: &str = "https://raw.githubusercontent.com/rust-lang/crates.io-index";
const CRATES_API_BASE_URI: &str = "https://crates.io/api/v1"; const CRATES_API_BASE_URI: &str = "https://crates.io/api/v1";
@ -17,7 +17,7 @@ struct RegistryPackageDep {
name: String, name: String,
req: VersionReq, req: VersionReq,
#[serde(default)] #[serde(default)]
kind: Option<String> kind: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -26,49 +26,61 @@ struct RegistryPackage {
#[serde(default)] #[serde(default)]
deps: Vec<RegistryPackageDep>, deps: Vec<RegistryPackageDep>,
#[serde(default)] #[serde(default)]
yanked: bool yanked: bool,
} }
fn convert_pkgs(name: &CrateName, packages: Vec<RegistryPackage>) -> Result<QueryCrateResponse, Error> { fn convert_pkgs(
let releases = packages.into_iter().map(|package| { name: &CrateName,
packages: Vec<RegistryPackage>,
) -> Result<QueryCrateResponse, Error> {
let releases = packages
.into_iter()
.map(|package| {
let mut deps = CrateDeps::default(); let mut deps = CrateDeps::default();
for dep in package.deps { for dep in package.deps {
match dep.kind.map(|k| k.clone()).unwrap_or_else(|| "normal".into()).as_ref() { match dep
"normal" => .kind
deps.main.insert(dep.name.parse()?, CrateDep::External(dep.req)), .map(|k| k.clone())
"dev" => .unwrap_or_else(|| "normal".into())
deps.dev.insert(dep.name.parse()?, CrateDep::External(dep.req)), .as_ref()
_ => None {
"normal" => deps
.main
.insert(dep.name.parse()?, CrateDep::External(dep.req)),
"dev" => deps
.dev
.insert(dep.name.parse()?, CrateDep::External(dep.req)),
_ => None,
}; };
} }
Ok(CrateRelease { Ok(CrateRelease {
name: name.clone(), name: name.clone(),
version: package.vers, version: package.vers,
deps: deps, deps: deps,
yanked: package.yanked yanked: package.yanked,
}) })
}).collect::<Result<_, Error>>()?; })
.collect::<Result<_, Error>>()?;
Ok(QueryCrateResponse { Ok(QueryCrateResponse { releases: releases })
releases: releases
})
} }
pub struct QueryCrateResponse { pub struct QueryCrateResponse {
pub releases: Vec<CrateRelease> pub releases: Vec<CrateRelease>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct QueryCrate<S>(pub S); pub struct QueryCrate<S>(pub S);
impl<S> Service for QueryCrate<S> impl<S> Service for QueryCrate<S>
where S: Service<Request=Request, Response=Response, Error=HyperError> + Clone + 'static, where
S::Future: 'static S: Service<Request = Request, Response = Response, Error = HyperError> + Clone + 'static,
S::Future: 'static,
{ {
type Request = CrateName; type Request = CrateName;
type Response = QueryCrateResponse; type Response = QueryCrateResponse;
type Error = Error; type Error = Error;
type Future = Box<Future<Item=Self::Response, Error=Self::Error>>; type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
fn call(&self, crate_name: CrateName) -> Self::Future { fn call(&self, crate_name: CrateName) -> Self::Future {
let lower_name = crate_name.as_ref().to_lowercase(); let lower_name = crate_name.as_ref().to_lowercase();
@ -80,21 +92,22 @@ impl<S> Service for QueryCrate<S>
_ => format!("{}/{}/{}", &lower_name[0..2], &lower_name[2..4], lower_name), _ => format!("{}/{}/{}", &lower_name[0..2], &lower_name[2..4], lower_name),
}; };
let uri = try_future_box!(format!("{}/master/{}", CRATES_INDEX_BASE_URI, path) let uri =
.parse::<Uri>()); try_future_box!(format!("{}/master/{}", CRATES_INDEX_BASE_URI, path).parse::<Uri>());
let request = Request::new(Method::Get, uri.clone()); let request = Request::new(Method::Get, uri.clone());
Box::new(self.0.call(request).from_err().and_then(move |response| { Box::new(self.0.call(request).from_err().and_then(move |response| {
let status = response.status(); let status = response.status();
if !status.is_success() { if !status.is_success() {
try_future!(Err(format_err!("Status code {} for URI {}", status, uri))); try_future!(Err(anyhow!("Status code {} for URI {}", status, uri)));
} }
let body_future = response.body().concat2().from_err(); let body_future = response.body().concat2().from_err();
let decode_future = body_future.and_then(move |body| { let decode_future = body_future.and_then(move |body| {
let string_body = str::from_utf8(body.as_ref())?; let string_body = str::from_utf8(body.as_ref())?;
let packages = string_body.lines() let packages = string_body
.lines()
.map(|s| s.trim()) .map(|s| s.trim())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.map(|s| serde_json::from_str::<RegistryPackage>(s)) .map(|s| serde_json::from_str::<RegistryPackage>(s))
@ -112,38 +125,48 @@ impl<S> Service for QueryCrate<S>
#[derive(Deserialize)] #[derive(Deserialize)]
struct SummaryResponseDetail { struct SummaryResponseDetail {
name: String, name: String,
max_version: Version max_version: Version,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct SummaryResponse { struct SummaryResponse {
most_downloaded: Vec<SummaryResponseDetail> most_downloaded: Vec<SummaryResponseDetail>,
} }
fn convert_summary(response: SummaryResponse) -> Result<Vec<CratePath>, Error> { fn convert_summary(response: SummaryResponse) -> Result<Vec<CratePath>, Error> {
response.most_downloaded.into_iter().map(|detail| { response
.most_downloaded
.into_iter()
.map(|detail| {
let name = detail.name.parse()?; let name = detail.name.parse()?;
Ok(CratePath { name, version: detail.max_version }) Ok(CratePath {
}).collect() name,
version: detail.max_version,
})
})
.collect()
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GetPopularCrates<S>(pub S); pub struct GetPopularCrates<S>(pub S);
impl<S> Service for GetPopularCrates<S> impl<S> Service for GetPopularCrates<S>
where S: Service<Request=Request, Response=Response, Error=HyperError> + Clone + 'static, where
S::Future: 'static S: Service<Request = Request, Response = Response, Error = HyperError> + Clone + 'static,
S::Future: 'static,
{ {
type Request = (); type Request = ();
type Response = Vec<CratePath>; type Response = Vec<CratePath>;
type Error = Error; type Error = Error;
type Future = Box<Future<Item=Self::Response, Error=Self::Error>>; type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
fn call(&self, _req: ()) -> Self::Future { fn call(&self, _req: ()) -> Self::Future {
let service = self.0.clone(); let service = self.0.clone();
let uri_future = format!("{}/summary", CRATES_API_BASE_URI) let uri_future = format!("{}/summary", CRATES_API_BASE_URI)
.parse::<Uri>().into_future().from_err(); .parse::<Uri>()
.into_future()
.from_err();
Box::new(uri_future.and_then(move |uri| { Box::new(uri_future.and_then(move |uri| {
let request = Request::new(Method::Get, uri.clone()); let request = Request::new(Method::Get, uri.clone());
@ -151,7 +174,11 @@ impl<S> Service for GetPopularCrates<S>
service.call(request).from_err().and_then(move |response| { service.call(request).from_err().and_then(move |response| {
let status = response.status(); let status = response.status();
if !status.is_success() { if !status.is_success() {
future::Either::A(future::err(format_err!("Status code {} for URI {}", status, uri))) future::Either::A(future::err(anyhow!(
"Status code {} for URI {}",
status,
uri
)))
} else { } else {
let body_future = response.body().concat2().from_err(); let body_future = response.body().concat2().from_err();
let decode_future = body_future.and_then(|body| { let decode_future = body_future.and_then(|body| {

View file

@ -1,57 +1,63 @@
use failure::Error; use anyhow::{anyhow, ensure, Error};
use futures::{Future, Stream}; use futures::{Future, Stream};
use hyper::{Error as HyperError, Method, Request, Response, Uri};
use hyper::header::UserAgent; use hyper::header::UserAgent;
use hyper::{Error as HyperError, Method, Request, Response, Uri};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use serde::Deserialize;
use tokio_service::Service; use tokio_service::Service;
use serde_json;
use ::models::repo::{Repository, RepoPath}; use crate::models::repo::{RepoPath, Repository};
const GITHUB_API_BASE_URI: &'static str = "https://api.github.com"; const GITHUB_API_BASE_URI: &'static str = "https://api.github.com";
const GITHUB_USER_CONTENT_BASE_URI: &'static str = "https://raw.githubusercontent.com"; const GITHUB_USER_CONTENT_BASE_URI: &'static str = "https://raw.githubusercontent.com";
pub fn get_manifest_uri(repo_path: &RepoPath, path: &RelativePathBuf) -> Result<Uri, Error> { pub fn get_manifest_uri(repo_path: &RepoPath, path: &RelativePathBuf) -> Result<Uri, Error> {
let path_str: &str = path.as_ref(); let path_str: &str = path.as_ref();
Ok(format!("{}/{}/{}/HEAD/{}", Ok(format!(
"{}/{}/{}/HEAD/{}",
GITHUB_USER_CONTENT_BASE_URI, GITHUB_USER_CONTENT_BASE_URI,
repo_path.qual.as_ref(), repo_path.qual.as_ref(),
repo_path.name.as_ref(), repo_path.name.as_ref(),
path_str path_str
).parse::<Uri>()?) )
.parse::<Uri>()?)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct GithubSearchResponse { struct GithubSearchResponse {
items: Vec<GithubRepo> items: Vec<GithubRepo>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct GithubRepo { struct GithubRepo {
name: String, name: String,
owner: GithubOwner, owner: GithubOwner,
description: String description: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct GithubOwner { struct GithubOwner {
login: String login: String,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GetPopularRepos<S>(pub S); pub struct GetPopularRepos<S>(pub S);
impl<S> Service for GetPopularRepos<S> impl<S> Service for GetPopularRepos<S>
where S: Service<Request=Request, Response=Response, Error=HyperError> + Clone + 'static, where
S::Future: 'static S: Service<Request = Request, Response = Response, Error = HyperError> + Clone + 'static,
S::Future: 'static,
{ {
type Request = (); type Request = ();
type Response = Vec<Repository>; type Response = Vec<Repository>;
type Error = Error; type Error = Error;
type Future = Box<Future<Item=Self::Response, Error=Self::Error>>; type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
fn call(&self, _req: ()) -> Self::Future { fn call(&self, _req: ()) -> Self::Future {
let uri = try_future_box!(format!("{}/search/repositories?q=language:rust&sort=stars", GITHUB_API_BASE_URI) let uri = try_future_box!(format!(
"{}/search/repositories?q=language:rust&sort=stars",
GITHUB_API_BASE_URI
)
.parse::<Uri>()); .parse::<Uri>());
let mut request = Request::new(Method::Get, uri); let mut request = Request::new(Method::Get, uri);
@ -60,18 +66,31 @@ impl<S> Service for GetPopularRepos<S>
Box::new(self.0.call(request).from_err().and_then(|response| { Box::new(self.0.call(request).from_err().and_then(|response| {
let status = response.status(); let status = response.status();
if !status.is_success() { if !status.is_success() {
try_future!(Err(format_err!("Status code {} for popular repo search", status))); try_future!(Err(anyhow!(
"Status code {} for popular repo search",
status
)));
} }
let body_future = response.body().concat2().from_err(); let body_future = response.body().concat2().from_err();
let decode_future = body_future let decode_future = body_future
.and_then(|body| serde_json::from_slice(body.as_ref()).map_err(|err| err.into())); .and_then(|body| serde_json::from_slice(body.as_ref()).map_err(|err| err.into()));
decode_future.and_then(|search_response: GithubSearchResponse| { decode_future
search_response.items.into_iter().map(|item| { .and_then(|search_response: GithubSearchResponse| {
let path = RepoPath::from_parts("github", &item.owner.login, &item.name)?; search_response
Ok(Repository { path, description: item.description }) .items
}).collect::<Result<Vec<_>, _>>() .into_iter()
}).into() .map(|item| {
let path =
RepoPath::from_parts("github", &item.owner.login, &item.name)?;
Ok(Repository {
path,
description: item.description,
})
})
.collect::<Result<Vec<_>, _>>()
})
.into()
})) }))
} }
} }

View file

@ -1,8 +1,8 @@
use anyhow::{anyhow, ensure, Error};
use hyper::Uri; use hyper::Uri;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use failure::Error;
use ::models::repo::RepoPath; use crate::models::repo::RepoPath;
const GITLAB_USER_CONTENT_BASE_URI: &'static str = "https://gitlab.com"; const GITLAB_USER_CONTENT_BASE_URI: &'static str = "https://gitlab.com";
@ -15,10 +15,12 @@ pub fn get_manifest_uri(repo_path: &RepoPath, path: &RelativePathBuf) -> Result<
} else { } else {
path_str path_str
}; };
Ok(format!("{}/{}/{}/raw/HEAD/{}", Ok(format!(
"{}/{}/{}/raw/HEAD/{}",
GITLAB_USER_CONTENT_BASE_URI, GITLAB_USER_CONTENT_BASE_URI,
repo_path.qual.as_ref(), repo_path.qual.as_ref(),
repo_path.name.as_ref(), repo_path.name.as_ref(),
slash_path slash_path
).parse::<Uri>()?) )
.parse::<Uri>()?)
} }

View file

@ -1,10 +1,10 @@
use failure::Error; use anyhow::{anyhow, ensure, Error};
use futures::{Future, Stream}; use futures::{Future, Stream};
use hyper::{Error as HyperError, Method, Request, Response}; use hyper::{Error as HyperError, Method, Request, Response};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use tokio_service::Service; use tokio_service::Service;
use ::models::repo::{RepoSite, RepoPath}; use crate::models::repo::{RepoPath, RepoSite};
pub mod bitbucket; pub mod bitbucket;
pub mod crates; pub mod crates;
@ -16,26 +16,21 @@ pub mod rustsec;
pub struct RetrieveFileAtPath<S>(pub S); pub struct RetrieveFileAtPath<S>(pub S);
impl<S> Service for RetrieveFileAtPath<S> impl<S> Service for RetrieveFileAtPath<S>
where S: Service<Request=Request, Response=Response, Error=HyperError> + Clone + 'static, where
S::Future: 'static S: Service<Request = Request, Response = Response, Error = HyperError> + Clone + 'static,
S::Future: 'static,
{ {
type Request = (RepoPath, RelativePathBuf); type Request = (RepoPath, RelativePathBuf);
type Response = String; type Response = String;
type Error = Error; type Error = Error;
type Future = Box<Future<Item=Self::Response, Error=Self::Error>>; type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
fn call(&self, req: Self::Request) -> Self::Future { fn call(&self, req: Self::Request) -> Self::Future {
let (repo_path, path) = req; let (repo_path, path) = req;
let uri = match &repo_path.site { let uri = match &repo_path.site {
&RepoSite::Github => { &RepoSite::Github => try_future_box!(github::get_manifest_uri(&repo_path, &path)),
try_future_box!(github::get_manifest_uri(&repo_path, &path)) &RepoSite::Gitlab => try_future_box!(gitlab::get_manifest_uri(&repo_path, &path)),
}, &RepoSite::Bitbucket => try_future_box!(bitbucket::get_manifest_uri(&repo_path, &path)),
&RepoSite::Gitlab => {
try_future_box!(gitlab::get_manifest_uri(&repo_path, &path))
},
&RepoSite::Bitbucket => {
try_future_box!(bitbucket::get_manifest_uri(&repo_path, &path))
}
}; };
let request = Request::new(Method::Get, uri.clone()); let request = Request::new(Method::Get, uri.clone());
@ -43,7 +38,7 @@ impl<S> Service for RetrieveFileAtPath<S>
Box::new(self.0.call(request).from_err().and_then(move |response| { Box::new(self.0.call(request).from_err().and_then(move |response| {
let status = response.status(); let status = response.status();
if !status.is_success() { if !status.is_success() {
try_future!(Err(format_err!("Status code {} for URI {}", status, uri))); try_future!(Err(anyhow!("Status code {} for URI {}", status, uri)));
} }
let body_future = response.body().concat2().from_err(); let body_future = response.body().concat2().from_err();
@ -54,5 +49,3 @@ impl<S> Service for RetrieveFileAtPath<S>
})) }))
} }
} }

View file

@ -1,44 +1,75 @@
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
use failure::Error; use anyhow::{anyhow, ensure, Error};
use futures::{Future, IntoFuture, Stream, future}; use futures::{future, future::done, Future, IntoFuture, Stream};
use hyper::{Error as HyperError, Method, Request, Response}; use hyper::{Error as HyperError, Method, Request, Response};
use rustsec::ADVISORY_DB_URL; use rustsec::database::Database;
use rustsec::db::AdvisoryDatabase; use rustsec::repository::DEFAULT_URL;
use tokio_service::Service; use tokio_service::Service;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FetchAdvisoryDatabase<S>(pub S); pub struct FetchAdvisoryDatabase<S>(pub S);
impl<S> Service for FetchAdvisoryDatabase<S> impl<S> Service for FetchAdvisoryDatabase<S>
where S: Service<Request=Request, Response=Response, Error=HyperError> + Clone + 'static, where
S::Future: 'static S: Service<Request = Request, Response = Response, Error = HyperError> + Clone + 'static,
S::Future: 'static,
{ {
type Request = (); type Request = ();
type Response = Arc<AdvisoryDatabase>; type Response = Arc<Database>;
type Error = Error; type Error = Error;
type Future = Box<Future<Item=Self::Response, Error=Self::Error>>; type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
fn call(&self, _req: ()) -> Self::Future { fn call(&self, _req: ()) -> Self::Future {
let service = self.0.clone(); let service = self.0.clone();
let uri_future = ADVISORY_DB_URL.parse().into_future().from_err(); Box::new(done(
rustsec::Database::fetch()
Box::new(uri_future.and_then(move |uri| { .map(|db| Arc::new(db))
let request = Request::new(Method::Get, uri); .map_err(|err| anyhow!("err fetching rustsec DB")),
))
service.call(request).from_err().and_then(|response| {
let status = response.status();
if !status.is_success() {
future::Either::A(future::err(format_err!("Status code {} when fetching advisory db", status)))
} else {
let body_future = response.body().concat2().from_err();
let decode_future = body_future
.and_then(|body| Ok(Arc::new(AdvisoryDatabase::from_toml(str::from_utf8(&body)?)?)));
future::Either::B(decode_future)
}
})
}))
} }
} }
// #[derive(Debug, Clone)]
// pub struct FetchAdvisoryDatabase<S>(pub S);
// impl<S> Service for FetchAdvisoryDatabase<S>
// where
// S: Service<Request = Request, Response = Response, Error = HyperError> + Clone + 'static,
// S::Future: 'static,
// {
// type Request = ();
// type Response = Arc<Database>;
// type Error = Error;
// type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
// fn call(&self, _req: ()) -> Self::Future {
// let service = self.0.clone();
// let uri_future = DEFAULT_URL.parse().into_future().from_err();
// Box::new(uri_future.and_then(move |uri| {
// let request = Request::new(Method::Get, uri);
// service.call(request).from_err().and_then(|response| {
// let status = response.status();
// if !status.is_success() {
// future::Either::A(future::err(anyhow!(
// "Status code {} when fetching advisory db",
// status
// )))
// } else {
// let body_future = response.body().concat2().from_err();
// let decode_future = body_future.and_then(|body| {
// Ok(Arc::new(Database::from_toml(str::from_utf8(
// &body,
// )?)?))
// });
// future::Either::B(decode_future)
// }
// })
// }))
// }
// }

View file

@ -1,54 +1,32 @@
#![feature(ascii_ctype)] #![deny(rust_2018_idioms)]
#![feature(conservative_impl_trait)] #![allow(unused)]
#![feature(ip_constructors)]
#![feature(proc_macro)]
extern crate badge;
extern crate cadence;
#[macro_use] extern crate failure;
#[macro_use] extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate indexmap;
#[macro_use] extern crate lazy_static;
extern crate lru_cache;
extern crate maud;
extern crate relative_path;
extern crate route_recognizer;
extern crate rustsec;
extern crate semver;
#[macro_use] extern crate serde_derive;
extern crate serde;
extern crate serde_json;
extern crate shared_failure;
#[macro_use] extern crate slog;
extern crate slog_json;
extern crate tokio_core;
extern crate tokio_service;
extern crate toml;
#[macro_use] extern crate try_future;
mod utils; #[macro_use]
mod models; extern crate try_future;
mod parsers;
mod interactors;
mod engine;
mod server;
use std::env; use std::env;
use std::net::{IpAddr, Ipv4Addr, UdpSocket, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::sync::Mutex; use std::sync::Mutex;
use cadence::{QueuingMetricSink, UdpMetricSink}; use cadence::{QueuingMetricSink, UdpMetricSink};
use futures::{Future, Stream}; use futures::{Future, Stream};
use hyper::Client;
use hyper::server::Http; use hyper::server::Http;
use hyper::Client;
use hyper_tls::HttpsConnector; use hyper_tls::HttpsConnector;
use slog::Drain; use slog::Drain;
use slog::{info, o};
use tokio_core::reactor::Core; use tokio_core::reactor::Core;
use self::server::Server; mod engine;
mod interactors;
mod models;
mod parsers;
mod server;
mod utils;
use self::engine::Engine; use self::engine::Engine;
use self::server::Server;
fn init_metrics() -> QueuingMetricSink { fn init_metrics() -> QueuingMetricSink {
let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
@ -61,27 +39,27 @@ fn init_metrics() -> QueuingMetricSink {
fn main() { fn main() {
let logger = slog::Logger::root( let logger = slog::Logger::root(
Mutex::new(slog_json::Json::default(std::io::stderr())).map(slog::Fuse), Mutex::new(slog_json::Json::default(std::io::stderr())).map(slog::Fuse),
o!("version" => env!("CARGO_PKG_VERSION")) o!("version" => env!("CARGO_PKG_VERSION")),
); );
let metrics = init_metrics(); let metrics = init_metrics();
let mut core = Core::new() let mut core = Core::new().expect("failed to create event loop");
.expect("failed to create event loop");
let handle = core.handle(); let handle = core.handle();
let connector = HttpsConnector::new(4, &handle) let connector = HttpsConnector::new(4, &handle).expect("failed to create https connector");
.expect("failed to create https connector");
let client = Client::configure() let client = Client::configure()
.connector(connector) .connector(connector)
.build(&core.handle()); .build(&core.handle());
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string()).parse() let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse()
.expect("could not read port"); .expect("could not read port");
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::unspecified()), port); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port);
let http = Http::new(); let http = Http::new();
@ -90,7 +68,8 @@ fn main() {
let server = Server::new(logger.clone(), engine); let server = Server::new(logger.clone(), engine);
let serve = http.serve_addr_handle(&addr, &handle, move || Ok(server.clone())) let serve = http
.serve_addr_handle(&addr, &handle, move || Ok(server.clone()))
.expect("failed to bind server"); .expect("failed to bind server");
let serving = serve.for_each(move |conn| { let serving = serve.for_each(move |conn| {

View file

@ -1,7 +1,7 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use std::str::FromStr; use std::str::FromStr;
use failure::Error; use anyhow::{anyhow, ensure, Error};
use indexmap::IndexMap; use indexmap::IndexMap;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
@ -9,14 +9,14 @@ use semver::{Version, VersionReq};
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct CratePath { pub struct CratePath {
pub name: CrateName, pub name: CrateName,
pub version: Version pub version: Version,
} }
impl CratePath { impl CratePath {
pub fn from_parts(name: &str, version: &str) -> Result<CratePath, Error> { pub fn from_parts(name: &str, version: &str) -> Result<CratePath, Error> {
Ok(CratePath { Ok(CratePath {
name: name.parse()?, name: name.parse()?,
version: version.parse()? version: version.parse()?,
}) })
} }
} }
@ -46,12 +46,12 @@ impl FromStr for CrateName {
type Err = Error; type Err = Error;
fn from_str(input: &str) -> Result<CrateName, Error> { fn from_str(input: &str) -> Result<CrateName, Error> {
let is_valid = input.chars().all(|c| { let is_valid = input
c.is_ascii_alphanumeric() || c == '_' || c == '-' .chars()
}); .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
if !is_valid { if !is_valid {
Err(format_err!("failed to validate crate name: {}", input)) Err(anyhow!("failed to validate crate name: {}", input))
} else { } else {
Ok(CrateName(input.to_string())) Ok(CrateName(input.to_string()))
} }
@ -63,13 +63,13 @@ pub struct CrateRelease {
pub name: CrateName, pub name: CrateName,
pub version: Version, pub version: Version,
pub deps: CrateDeps, pub deps: CrateDeps,
pub yanked: bool pub yanked: bool,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum CrateDep { pub enum CrateDep {
External(VersionReq), External(VersionReq),
Internal(RelativePathBuf) Internal(RelativePathBuf),
} }
impl CrateDep { impl CrateDep {
@ -86,7 +86,7 @@ impl CrateDep {
pub struct CrateDeps { pub struct CrateDeps {
pub main: IndexMap<CrateName, CrateDep>, pub main: IndexMap<CrateName, CrateDep>,
pub dev: IndexMap<CrateName, CrateDep>, pub dev: IndexMap<CrateName, CrateDep>,
pub build: IndexMap<CrateName, CrateDep> pub build: IndexMap<CrateName, CrateDep>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -94,7 +94,7 @@ pub struct AnalyzedDependency {
pub required: VersionReq, pub required: VersionReq,
pub latest_that_matches: Option<Version>, pub latest_that_matches: Option<Version>,
pub latest: Option<Version>, pub latest: Option<Version>,
pub insecure: bool pub insecure: bool,
} }
impl AnalyzedDependency { impl AnalyzedDependency {
@ -103,7 +103,7 @@ impl AnalyzedDependency {
required, required,
latest_that_matches: None, latest_that_matches: None,
latest: None, latest: None,
insecure: false insecure: false,
} }
} }
@ -116,32 +116,44 @@ impl AnalyzedDependency {
pub struct AnalyzedDependencies { pub struct AnalyzedDependencies {
pub main: IndexMap<CrateName, AnalyzedDependency>, pub main: IndexMap<CrateName, AnalyzedDependency>,
pub dev: IndexMap<CrateName, AnalyzedDependency>, pub dev: IndexMap<CrateName, AnalyzedDependency>,
pub build: IndexMap<CrateName, AnalyzedDependency> pub build: IndexMap<CrateName, AnalyzedDependency>,
} }
impl AnalyzedDependencies { impl AnalyzedDependencies {
pub fn new(deps: &CrateDeps) -> AnalyzedDependencies { pub fn new(deps: &CrateDeps) -> AnalyzedDependencies {
let main = deps.main.iter().filter_map(|(name, dep)| { let main = deps
.main
.iter()
.filter_map(|(name, dep)| {
if let &CrateDep::External(ref req) = dep { if let &CrateDep::External(ref req) = dep {
Some((name.clone(), AnalyzedDependency::new(req.clone()))) Some((name.clone(), AnalyzedDependency::new(req.clone())))
} else { } else {
None None
} }
}).collect(); })
let dev = deps.dev.iter().filter_map(|(name, dep)| { .collect();
let dev = deps
.dev
.iter()
.filter_map(|(name, dep)| {
if let &CrateDep::External(ref req) = dep { if let &CrateDep::External(ref req) = dep {
Some((name.clone(), AnalyzedDependency::new(req.clone()))) Some((name.clone(), AnalyzedDependency::new(req.clone())))
} else { } else {
None None
} }
}).collect(); })
let build = deps.build.iter().filter_map(|(name, dep)| { .collect();
let build = deps
.build
.iter()
.filter_map(|(name, dep)| {
if let &CrateDep::External(ref req) = dep { if let &CrateDep::External(ref req) = dep {
Some((name.clone(), AnalyzedDependency::new(req.clone()))) Some((name.clone(), AnalyzedDependency::new(req.clone())))
} else { } else {
None None
} }
}).collect(); })
.collect();
AnalyzedDependencies { main, dev, build } AnalyzedDependencies { main, dev, build }
} }
@ -150,38 +162,35 @@ impl AnalyzedDependencies {
} }
pub fn count_outdated(&self) -> usize { pub fn count_outdated(&self) -> usize {
let main_outdated = self.main.iter() let main_outdated = self
.main
.iter()
.filter(|&(_, dep)| dep.is_outdated()) .filter(|&(_, dep)| dep.is_outdated())
.count(); .count();
let dev_outdated = self.dev.iter() let dev_outdated = self
.dev
.iter()
.filter(|&(_, dep)| dep.is_outdated()) .filter(|&(_, dep)| dep.is_outdated())
.count(); .count();
let build_outdated = self.build.iter() let build_outdated = self
.build
.iter()
.filter(|&(_, dep)| dep.is_outdated()) .filter(|&(_, dep)| dep.is_outdated())
.count(); .count();
main_outdated + dev_outdated + build_outdated main_outdated + dev_outdated + build_outdated
} }
pub fn count_insecure(&self) -> usize { pub fn count_insecure(&self) -> usize {
let main_insecure = self.main.iter() let main_insecure = self.main.iter().filter(|&(_, dep)| dep.insecure).count();
.filter(|&(_, dep)| dep.insecure) let dev_insecure = self.dev.iter().filter(|&(_, dep)| dep.insecure).count();
.count(); let build_insecure = self.build.iter().filter(|&(_, dep)| dep.insecure).count();
let dev_insecure = self.dev.iter()
.filter(|&(_, dep)| dep.insecure)
.count();
let build_insecure = self.build.iter()
.filter(|&(_, dep)| dep.insecure)
.count();
main_insecure + dev_insecure + build_insecure main_insecure + dev_insecure + build_insecure
} }
pub fn any_outdated(&self) -> bool { pub fn any_outdated(&self) -> bool {
let main_any_outdated = self.main.iter() let main_any_outdated = self.main.iter().any(|(_, dep)| dep.is_outdated());
.any(|(_, dep)| dep.is_outdated()); let dev_any_outdated = self.dev.iter().any(|(_, dep)| dep.is_outdated());
let dev_any_outdated = self.dev.iter() let build_any_outdated = self.build.iter().any(|(_, dep)| dep.is_outdated());
.any(|(_, dep)| dep.is_outdated());
let build_any_outdated = self.build.iter()
.any(|(_, dep)| dep.is_outdated());
main_any_outdated || dev_any_outdated || build_any_outdated main_any_outdated || dev_any_outdated || build_any_outdated
} }
} }
@ -189,6 +198,12 @@ impl AnalyzedDependencies {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum CrateManifest { pub enum CrateManifest {
Package(CrateName, CrateDeps), Package(CrateName, CrateDeps),
Workspace { members: Vec<RelativePathBuf> }, Workspace {
Mixed { name: CrateName, deps: CrateDeps, members: Vec<RelativePathBuf> } members: Vec<RelativePathBuf>,
},
Mixed {
name: CrateName,
deps: CrateDeps,
members: Vec<RelativePathBuf>,
},
} }

View file

@ -3,5 +3,5 @@ pub mod repo;
pub enum SubjectPath { pub enum SubjectPath {
Repo(self::repo::RepoPath), Repo(self::repo::RepoPath),
Crate(self::crates::CratePath) Crate(self::crates::CratePath),
} }

View file

@ -1,18 +1,18 @@
use std::str::FromStr; use std::str::FromStr;
use failure::Error; use anyhow::{anyhow, ensure, Error};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Repository { pub struct Repository {
pub path: RepoPath, pub path: RepoPath,
pub description: String pub description: String,
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct RepoPath { pub struct RepoPath {
pub site: RepoSite, pub site: RepoSite,
pub qual: RepoQualifier, pub qual: RepoQualifier,
pub name: RepoName pub name: RepoName,
} }
impl RepoPath { impl RepoPath {
@ -20,7 +20,7 @@ impl RepoPath {
Ok(RepoPath { Ok(RepoPath {
site: site.parse()?, site: site.parse()?,
qual: qual.parse()?, qual: qual.parse()?,
name: name.parse()? name: name.parse()?,
}) })
} }
} }
@ -50,7 +50,7 @@ impl FromStr for RepoSite {
"github" => Ok(RepoSite::Github), "github" => Ok(RepoSite::Github),
"gitlab" => Ok(RepoSite::Gitlab), "gitlab" => Ok(RepoSite::Gitlab),
"bitbucket" => Ok(RepoSite::Bitbucket), "bitbucket" => Ok(RepoSite::Bitbucket),
_ => Err(format_err!("unknown repo site identifier")) _ => Err(anyhow!("unknown repo site identifier")),
} }
} }
} }
@ -72,9 +72,9 @@ impl FromStr for RepoQualifier {
type Err = Error; type Err = Error;
fn from_str(input: &str) -> Result<RepoQualifier, Error> { fn from_str(input: &str) -> Result<RepoQualifier, Error> {
let is_valid = input.chars().all(|c| { let is_valid = input
c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' .chars()
}); .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_');
ensure!(is_valid, "invalid repo qualifier"); ensure!(is_valid, "invalid repo qualifier");
Ok(RepoQualifier(input.to_string())) Ok(RepoQualifier(input.to_string()))
@ -94,9 +94,9 @@ impl FromStr for RepoName {
type Err = Error; type Err = Error;
fn from_str(input: &str) -> Result<RepoName, Error> { fn from_str(input: &str) -> Result<RepoName, Error> {
let is_valid = input.chars().all(|c| { let is_valid = input
c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' .chars()
}); .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_');
ensure!(is_valid, "invalid repo name"); ensure!(is_valid, "invalid repo name");
Ok(RepoName(input.to_string())) Ok(RepoName(input.to_string()))

View file

@ -1,34 +1,34 @@
use failure::Error; use anyhow::{anyhow, ensure, Error};
use indexmap::IndexMap; use indexmap::IndexMap;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use semver::VersionReq; use semver::VersionReq;
use toml; use serde::{Deserialize, Serialize};
use ::models::crates::{CrateName, CrateDep, CrateDeps, CrateManifest}; use crate::models::crates::{CrateDep, CrateDeps, CrateManifest, CrateName};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
struct CargoTomlComplexDependency { struct CargoTomlComplexDependency {
git: Option<String>, git: Option<String>,
path: Option<RelativePathBuf>, path: Option<RelativePathBuf>,
version: Option<String> version: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)] #[serde(untagged)]
enum CargoTomlDependency { enum CargoTomlDependency {
Simple(String), Simple(String),
Complex(CargoTomlComplexDependency) Complex(CargoTomlComplexDependency),
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
struct CargoTomlPackage { struct CargoTomlPackage {
name: String name: String,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
struct CargoTomlWorkspace { struct CargoTomlWorkspace {
#[serde(default)] #[serde(default)]
members: Vec<RelativePathBuf> members: Vec<RelativePathBuf>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -44,30 +44,40 @@ struct CargoToml {
dev_dependencies: IndexMap<String, CargoTomlDependency>, dev_dependencies: IndexMap<String, CargoTomlDependency>,
#[serde(rename = "build-dependencies")] #[serde(rename = "build-dependencies")]
#[serde(default)] #[serde(default)]
build_dependencies: IndexMap<String, CargoTomlDependency> build_dependencies: IndexMap<String, CargoTomlDependency>,
} }
fn convert_dependency(cargo_dep: (String, CargoTomlDependency)) -> Option<Result<(CrateName, CrateDep), Error>> { fn convert_dependency(
cargo_dep: (String, CargoTomlDependency),
) -> Option<Result<(CrateName, CrateDep), Error>> {
match cargo_dep { match cargo_dep {
(name, CargoTomlDependency::Simple(string)) => { (name, CargoTomlDependency::Simple(string)) => Some(
Some(name.parse::<CrateName>().map_err(|err| err.into()).and_then(|parsed_name| { name.parse::<CrateName>()
string.parse::<VersionReq>().map_err(|err| err.into()) .map_err(|err| err.into())
.and_then(|parsed_name| {
string
.parse::<VersionReq>()
.map_err(|err| err.into())
.map(|version| (parsed_name, CrateDep::External(version))) .map(|version| (parsed_name, CrateDep::External(version)))
})) }),
} ),
(name, CargoTomlDependency::Complex(cplx)) => { (name, CargoTomlDependency::Complex(cplx)) => {
if cplx.git.is_some() { if cplx.git.is_some() {
None None
} else if cplx.path.is_some() { } else if cplx.path.is_some() {
cplx.path.map(|path| { cplx.path.map(|path| {
name.parse::<CrateName>().map_err(|err| err.into()).map(|parsed_name| { name.parse::<CrateName>()
(parsed_name, CrateDep::Internal(path)) .map_err(|err| err.into())
}) .map(|parsed_name| (parsed_name, CrateDep::Internal(path)))
}) })
} else { } else {
cplx.version.map(|string| { cplx.version.map(|string| {
name.parse::<CrateName>().map_err(|err| err.into()).and_then(|parsed_name| { name.parse::<CrateName>()
string.parse::<VersionReq>().map_err(|err| err.into()) .map_err(|err| err.into())
.and_then(|parsed_name| {
string
.parse::<VersionReq>()
.map_err(|err| err.into())
.map(|version| (parsed_name, CrateDep::External(version))) .map(|version| (parsed_name, CrateDep::External(version)))
}) })
}) })
@ -85,17 +95,26 @@ pub fn parse_manifest_toml(input: &str) -> Result<CrateManifest, Error> {
if let Some(package) = cargo_toml.package { if let Some(package) = cargo_toml.package {
let crate_name = package.name.parse::<CrateName>()?; let crate_name = package.name.parse::<CrateName>()?;
let dependencies = cargo_toml.dependencies let dependencies = cargo_toml
.into_iter().filter_map(convert_dependency).collect::<Result<IndexMap<_, _>, _>>()?; .dependencies
let dev_dependencies = cargo_toml.dev_dependencies .into_iter()
.into_iter().filter_map(convert_dependency).collect::<Result<IndexMap<_, _>, _>>()?; .filter_map(convert_dependency)
let build_dependencies = cargo_toml.build_dependencies .collect::<Result<IndexMap<_, _>, _>>()?;
.into_iter().filter_map(convert_dependency).collect::<Result<IndexMap<_, _>, _>>()?; let dev_dependencies = cargo_toml
.dev_dependencies
.into_iter()
.filter_map(convert_dependency)
.collect::<Result<IndexMap<_, _>, _>>()?;
let build_dependencies = cargo_toml
.build_dependencies
.into_iter()
.filter_map(convert_dependency)
.collect::<Result<IndexMap<_, _>, _>>()?;
let deps = CrateDeps { let deps = CrateDeps {
main: dependencies, main: dependencies,
dev: dev_dependencies, dev: dev_dependencies,
build: build_dependencies build: build_dependencies,
}; };
package_part = Some((crate_name, deps)); package_part = Some((crate_name, deps));
@ -106,21 +125,22 @@ pub fn parse_manifest_toml(input: &str) -> Result<CrateManifest, Error> {
} }
match (package_part, workspace_part) { match (package_part, workspace_part) {
(Some((name, deps)), None) => (Some((name, deps)), None) => Ok(CrateManifest::Package(name, deps)),
Ok(CrateManifest::Package(name, deps)), (None, Some(members)) => Ok(CrateManifest::Workspace { members }),
(None, Some(members)) => (Some((name, deps)), Some(members)) => Ok(CrateManifest::Mixed {
Ok(CrateManifest::Workspace { members }), name,
(Some((name, deps)), Some(members)) => deps,
Ok(CrateManifest::Mixed { name, deps, members }), members,
(None, None) => }),
Err(format_err!("neither workspace nor package found in manifest")) (None, None) => Err(anyhow!("neither workspace nor package found in manifest")),
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use models::crates::CrateManifest; use crate::models::crates::CrateManifest;
use super::parse_manifest_toml;
use super::*;
#[test] #[test]
fn parse_workspace_without_members_declaration() { fn parse_workspace_without_members_declaration() {
@ -136,14 +156,18 @@ symbolic-common = { version = "2.0.6", path = "common" }
let manifest = parse_manifest_toml(toml).unwrap(); let manifest = parse_manifest_toml(toml).unwrap();
match manifest { match manifest {
CrateManifest::Mixed { name, deps, members } => { CrateManifest::Mixed {
name,
deps,
members,
} => {
assert_eq!(name.as_ref(), "symbolic"); assert_eq!(name.as_ref(), "symbolic");
assert_eq!(deps.main.len(), 1); assert_eq!(deps.main.len(), 1);
assert_eq!(deps.dev.len(), 0); assert_eq!(deps.dev.len(), 0);
assert_eq!(deps.build.len(), 0); assert_eq!(deps.build.len(), 0);
assert_eq!(members.len(), 0); assert_eq!(members.len(), 0);
}, }
_ => panic!("expected mixed manifest") _ => panic!("expected mixed manifest"),
} }
} }
} }

View file

@ -1,4 +1,2 @@
pub static STATIC_STYLE_CSS: &'static str = pub static STATIC_STYLE_CSS: &'static str = include_str!(concat!(env!("OUT_DIR"), "/style.css"));
include_str!(concat!(env!("OUT_DIR"), "/style.css")); pub static STATIC_FAVICON_PNG: &'static [u8; 1338] = include_bytes!("../../assets/favicon.png");
pub static STATIC_FAVICON_PNG: &'static [u8; 1338] =
include_bytes!("../../assets/favicon.png");

View file

@ -1,32 +1,34 @@
use std::env; use std::env;
use std::sync::Arc; use std::sync::Arc;
use futures::{Future, IntoFuture, future}; use futures::{future, Future, IntoFuture};
use hyper::{Error as HyperError, Method, Request, Response, StatusCode};
use hyper::header::{ContentType, Location}; use hyper::header::{ContentType, Location};
use hyper::{Error as HyperError, Method, Request, Response, StatusCode};
use once_cell::sync::Lazy;
use route_recognizer::{Params, Router}; use route_recognizer::{Params, Router};
use semver::VersionReq; use semver::VersionReq;
use slog::Logger; use slog::Logger;
use slog::{error, o};
use tokio_service::Service; use tokio_service::Service;
mod assets; mod assets;
mod views; mod views;
use ::engine::{Engine, AnalyzeDependenciesOutcome}; use crate::engine::{AnalyzeDependenciesOutcome, Engine};
use ::models::crates::{CrateName, CratePath}; use crate::models::crates::{CrateName, CratePath};
use ::models::repo::RepoPath; use crate::models::repo::RepoPath;
use ::models::SubjectPath; use crate::models::SubjectPath;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
enum StatusFormat { enum StatusFormat {
Html, Html,
Svg Svg,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum StaticFile { enum StaticFile {
StyleCss, StyleCss,
FaviconPng FaviconPng,
} }
enum Route { enum Route {
@ -34,14 +36,14 @@ enum Route {
Static(StaticFile), Static(StaticFile),
RepoStatus(StatusFormat), RepoStatus(StatusFormat),
CrateRedirect, CrateRedirect,
CrateStatus(StatusFormat) CrateStatus(StatusFormat),
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Server { pub struct Server {
logger: Logger, logger: Logger,
engine: Engine, engine: Engine,
router: Arc<Router<Route>> router: Arc<Router<Route>>,
} }
impl Server { impl Server {
@ -53,14 +55,30 @@ impl Server {
router.add("/static/style.css", Route::Static(StaticFile::StyleCss)); router.add("/static/style.css", Route::Static(StaticFile::StyleCss));
router.add("/static/favicon.png", Route::Static(StaticFile::FaviconPng)); router.add("/static/favicon.png", Route::Static(StaticFile::FaviconPng));
router.add("/repo/:site/:qual/:name", Route::RepoStatus(StatusFormat::Html)); router.add(
router.add("/repo/:site/:qual/:name/status.svg", Route::RepoStatus(StatusFormat::Svg)); "/repo/:site/:qual/:name",
Route::RepoStatus(StatusFormat::Html),
);
router.add(
"/repo/:site/:qual/:name/status.svg",
Route::RepoStatus(StatusFormat::Svg),
);
router.add("/crate/:name", Route::CrateRedirect); router.add("/crate/:name", Route::CrateRedirect);
router.add("/crate/:name/:version", Route::CrateStatus(StatusFormat::Html)); router.add(
router.add("/crate/:name/:version/status.svg", Route::CrateStatus(StatusFormat::Svg)); "/crate/:name/:version",
Route::CrateStatus(StatusFormat::Html),
);
router.add(
"/crate/:name/:version/status.svg",
Route::CrateStatus(StatusFormat::Svg),
);
Server { logger, engine, router: Arc::new(router) } Server {
logger,
engine,
router: Arc::new(router),
}
} }
} }
@ -68,10 +86,12 @@ impl Service for Server {
type Request = Request; type Request = Request;
type Response = Response; type Response = Response;
type Error = HyperError; type Error = HyperError;
type Future = Box<Future<Item=Response, Error=HyperError>>; type Future = Box<dyn Future<Item = Response, Error = HyperError>>;
fn call(&self, req: Request) -> Self::Future { fn call(&self, req: Request) -> Self::Future {
let logger = self.logger.new(o!("http_path" => req.uri().path().to_owned())); let logger = self
.logger
.new(o!("http_path" => req.uri().path().to_owned()));
if let Ok(route_match) = self.router.recognize(req.uri().path()) { if let Ok(route_match) = self.router.recognize(req.uri().path()) {
match route_match.handler { match route_match.handler {
@ -79,28 +99,32 @@ impl Service for Server {
if *req.method() == Method::Get { if *req.method() == Method::Get {
return Box::new(self.index(req, route_match.params, logger)); return Box::new(self.index(req, route_match.params, logger));
} }
}, }
&Route::RepoStatus(format) => { &Route::RepoStatus(format) => {
if *req.method() == Method::Get { if *req.method() == Method::Get {
return Box::new(self.repo_status(req, route_match.params, logger, format)); return Box::new(self.repo_status(req, route_match.params, logger, format));
} }
}, }
&Route::CrateStatus(format) => { &Route::CrateStatus(format) => {
if *req.method() == Method::Get { if *req.method() == Method::Get {
return Box::new(self.crate_status(req, route_match.params, logger, format)); return Box::new(self.crate_status(
req,
route_match.params,
logger,
format,
));
}
} }
},
&Route::CrateRedirect => { &Route::CrateRedirect => {
if *req.method() == Method::Get { if *req.method() == Method::Get {
return Box::new(self.crate_redirect(req, route_match.params, logger)); return Box::new(self.crate_redirect(req, route_match.params, logger));
} }
}, }
&Route::Static(file) => { &Route::Static(file) => {
if *req.method() == Method::Get { if *req.method() == Method::Get {
return Box::new(future::ok(Server::static_file(file))); return Box::new(future::ok(Server::static_file(file)));
} }
} }
} }
} }
@ -111,175 +135,216 @@ impl Service for Server {
} }
impl Server { impl Server {
fn index(&self, _req: Request, _params: Params, logger: Logger) -> fn index(
impl Future<Item=Response, Error=HyperError> &self,
{ _req: Request,
self.engine.get_popular_repos() _params: Params,
logger: Logger,
) -> impl Future<Item = Response, Error = HyperError> {
self.engine
.get_popular_repos()
.join(self.engine.get_popular_crates()) .join(self.engine.get_popular_crates())
.then(move |popular_result| { .then(move |popular_result| match popular_result {
match popular_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", err); error!(logger, "error: {}", err);
let mut response = views::html::error::render("Could not retrieve popular items", ""); let mut response =
views::html::error::render("Could not retrieve popular items", "");
response.set_status(StatusCode::InternalServerError); response.set_status(StatusCode::InternalServerError);
future::ok(response) future::ok(response)
}, }
Ok((popular_repos, popular_crates)) => Ok((popular_repos, popular_crates)) => {
future::ok(views::html::index::render(popular_repos, popular_crates)) future::ok(views::html::index::render(popular_repos, popular_crates))
} }
}) })
} }
fn repo_status(&self, _req: Request, params: Params, logger: Logger, format: StatusFormat) -> fn repo_status(
impl Future<Item=Response, Error=HyperError> &self,
{ _req: Request,
params: Params,
logger: Logger,
format: StatusFormat,
) -> impl Future<Item = Response, Error = HyperError> {
let server = self.clone(); let server = self.clone();
let site = params.find("site").expect("route param 'site' not found"); let site = params.find("site").expect("route param 'site' not found");
let qual = params.find("qual").expect("route param 'qual' not found"); let qual = params.find("qual").expect("route param 'qual' not found");
let name = params.find("name").expect("route param 'name' not found"); let name = params.find("name").expect("route param 'name' not found");
RepoPath::from_parts(site, qual, name).into_future().then(move |repo_path_result| { RepoPath::from_parts(site, qual, name)
match repo_path_result { .into_future()
.then(move |repo_path_result| match repo_path_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", err); error!(logger, "error: {}", err);
let mut response = views::html::error::render("Could not parse repository path", let mut response = views::html::error::render(
"Please make sure to provide a valid repository path."); "Could not parse repository path",
"Please make sure to provide a valid repository path.",
);
response.set_status(StatusCode::BadRequest); response.set_status(StatusCode::BadRequest);
future::Either::A(future::ok(response)) future::Either::A(future::ok(response))
}, }
Ok(repo_path) => { Ok(repo_path) => future::Either::B(
future::Either::B(server.engine.analyze_repo_dependencies(repo_path.clone()).then(move |analyze_result| { server
match analyze_result { .engine
.analyze_repo_dependencies(repo_path.clone())
.then(move |analyze_result| match analyze_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", err); error!(logger, "error: {}", err);
let response = Server::status_format_analysis(None, format, SubjectPath::Repo(repo_path)); let response = Server::status_format_analysis(
None,
format,
SubjectPath::Repo(repo_path),
);
future::ok(response) future::ok(response)
}, }
Ok(analysis_outcome) => { Ok(analysis_outcome) => {
let response = Server::status_format_analysis(Some(analysis_outcome), format, SubjectPath::Repo(repo_path)); let response = Server::status_format_analysis(
Some(analysis_outcome),
format,
SubjectPath::Repo(repo_path),
);
future::ok(response) future::ok(response)
} }
} }),
})) ),
}
}
}) })
} }
fn crate_redirect(&self, _req: Request, params: Params, logger: Logger) -> fn crate_redirect(
impl Future<Item=Response, Error=HyperError> &self,
{ _req: Request,
params: Params,
logger: Logger,
) -> impl Future<Item = Response, Error = HyperError> {
let engine = self.engine.clone(); let engine = self.engine.clone();
let name = params.find("name").expect("route param 'name' not found"); let name = params.find("name").expect("route param 'name' not found");
name.parse::<CrateName>().into_future().then(move |crate_name_result| { name.parse::<CrateName>()
match crate_name_result { .into_future()
.then(move |crate_name_result| match crate_name_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", err); error!(logger, "error: {}", err);
let mut response = views::html::error::render("Could not parse crate name", let mut response = views::html::error::render(
"Please make sure to provide a valid crate name."); "Could not parse crate name",
"Please make sure to provide a valid crate name.",
);
response.set_status(StatusCode::BadRequest); response.set_status(StatusCode::BadRequest);
future::Either::A(future::ok(response)) future::Either::A(future::ok(response))
}, }
Ok(crate_name) => { Ok(crate_name) => future::Either::B(
future::Either::B(engine.find_latest_crate_release(crate_name, VersionReq::any()).then(move |release_result| { engine
match release_result { .find_latest_crate_release(crate_name, VersionReq::any())
.then(move |release_result| match release_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", err); error!(logger, "error: {}", err);
let mut response = views::html::error::render("Could not fetch crate information", let mut response = views::html::error::render(
"Please make sure to provide a valid crate name."); "Could not fetch crate information",
"Please make sure to provide a valid crate name.",
);
response.set_status(StatusCode::NotFound); response.set_status(StatusCode::NotFound);
future::ok(response) future::ok(response)
}, }
Ok(None) => { Ok(None) => {
let mut response = views::html::error::render("Could not fetch crate information", let mut response = views::html::error::render(
"Please make sure to provide a valid crate name."); "Could not fetch crate information",
"Please make sure to provide a valid crate name.",
);
response.set_status(StatusCode::NotFound); response.set_status(StatusCode::NotFound);
future::ok(response) future::ok(response)
}, }
Ok(Some(release)) => { Ok(Some(release)) => {
let mut response = Response::new(); let mut response = Response::new();
response.set_status(StatusCode::TemporaryRedirect); response.set_status(StatusCode::TemporaryRedirect);
let url = format!("{}/crate/{}/{}", let url = format!(
"{}/crate/{}/{}",
&SELF_BASE_URL as &str, &SELF_BASE_URL as &str,
release.name.as_ref(), release.name.as_ref(),
release.version); release.version
);
response.headers_mut().set(Location::new(url)); response.headers_mut().set(Location::new(url));
future::ok(response) future::ok(response)
} }
} }),
})) ),
}
}
}) })
} }
fn crate_status(&self, _req: Request, params: Params, logger: Logger, format: StatusFormat) -> fn crate_status(
impl Future<Item=Response, Error=HyperError> &self,
{ _req: Request,
params: Params,
logger: Logger,
format: StatusFormat,
) -> impl Future<Item = Response, Error = HyperError> {
let server = self.clone(); let server = self.clone();
let name = params.find("name").expect("route param 'name' not found"); let name = params.find("name").expect("route param 'name' not found");
let version = params.find("version").expect("route param 'version' not found"); let version = params
.find("version")
.expect("route param 'version' not found");
CratePath::from_parts(name, version).into_future().then(move |crate_path_result| { CratePath::from_parts(name, version)
match crate_path_result { .into_future()
.then(move |crate_path_result| match crate_path_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", err); error!(logger, "error: {}", err);
let mut response = views::html::error::render("Could not parse crate path", let mut response = views::html::error::render(
"Please make sure to provide a valid crate name and version."); "Could not parse crate path",
"Please make sure to provide a valid crate name and version.",
);
response.set_status(StatusCode::BadRequest); response.set_status(StatusCode::BadRequest);
future::Either::A(future::ok(response)) future::Either::A(future::ok(response))
}, }
Ok(crate_path) => { Ok(crate_path) => future::Either::B(
future::Either::B(server.engine.analyze_crate_dependencies(crate_path.clone()).then(move |analyze_result| { server
match analyze_result { .engine
.analyze_crate_dependencies(crate_path.clone())
.then(move |analyze_result| match analyze_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", err); error!(logger, "error: {}", err);
let response = Server::status_format_analysis(None, format, SubjectPath::Crate(crate_path)); let response = Server::status_format_analysis(
None,
format,
SubjectPath::Crate(crate_path),
);
future::ok(response) future::ok(response)
}, }
Ok(analysis_outcome) => { Ok(analysis_outcome) => {
let response = Server::status_format_analysis(Some(analysis_outcome), format, SubjectPath::Crate(crate_path)); let response = Server::status_format_analysis(
Some(analysis_outcome),
format,
SubjectPath::Crate(crate_path),
);
future::ok(response) future::ok(response)
} }
} }),
})) ),
}
}
}) })
} }
fn status_format_analysis(analysis_outcome: Option<AnalyzeDependenciesOutcome>, format: StatusFormat, subject_path: SubjectPath) -> Response { fn status_format_analysis(
analysis_outcome: Option<AnalyzeDependenciesOutcome>,
format: StatusFormat,
subject_path: SubjectPath,
) -> Response {
match format { match format {
StatusFormat::Svg => StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref()),
views::badge::response(analysis_outcome.as_ref()), StatusFormat::Html => views::html::status::render(analysis_outcome, subject_path),
StatusFormat::Html =>
views::html::status::render(analysis_outcome, subject_path)
} }
} }
fn static_file(file: StaticFile) -> Response { fn static_file(file: StaticFile) -> Response {
match file { match file {
StaticFile::StyleCss => { StaticFile::StyleCss => Response::new()
Response::new()
.with_header(ContentType("text/css".parse().unwrap())) .with_header(ContentType("text/css".parse().unwrap()))
.with_body(assets::STATIC_STYLE_CSS) .with_body(assets::STATIC_STYLE_CSS),
}, StaticFile::FaviconPng => Response::new()
StaticFile::FaviconPng => {
Response::new()
.with_header(ContentType("image/png".parse().unwrap())) .with_header(ContentType("image/png".parse().unwrap()))
.with_body(assets::STATIC_FAVICON_PNG.to_vec()) .with_body(assets::STATIC_FAVICON_PNG.to_vec()),
}
} }
} }
} }
lazy_static! { static SELF_BASE_URL: Lazy<String> =
static ref SELF_BASE_URL: String = { Lazy::new(|| env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()));
env::var("BASE_URL")
.unwrap_or_else(|_| "http://localhost:8080".to_string())
};
}

View file

@ -1,8 +1,8 @@
use badge::{Badge, BadgeOptions}; use badge::{Badge, BadgeOptions};
use hyper::Response;
use hyper::header::ContentType; use hyper::header::ContentType;
use hyper::Response;
use ::engine::AnalyzeDependenciesOutcome; use crate::engine::AnalyzeDependenciesOutcome;
pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge { pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge {
let opts = match analysis_outcome { let opts = match analysis_outcome {
@ -11,7 +11,7 @@ pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge {
BadgeOptions { BadgeOptions {
subject: "dependencies".into(), subject: "dependencies".into(),
status: "insecure".into(), status: "insecure".into(),
color: "#e05d44".into() color: "#e05d44".into(),
} }
} else { } else {
let (outdated, total) = outcome.outdated_ratio(); let (outdated, total) = outcome.outdated_ratio();
@ -20,30 +20,28 @@ pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge {
BadgeOptions { BadgeOptions {
subject: "dependencies".into(), subject: "dependencies".into(),
status: format!("{} of {} outdated", outdated, total), status: format!("{} of {} outdated", outdated, total),
color: "#dfb317".into() color: "#dfb317".into(),
} }
} else if total > 0 { } else if total > 0 {
BadgeOptions { BadgeOptions {
subject: "dependencies".into(), subject: "dependencies".into(),
status: "up to date".into(), status: "up to date".into(),
color: "#4c1".into() color: "#4c1".into(),
} }
} else { } else {
BadgeOptions { BadgeOptions {
subject: "dependencies".into(), subject: "dependencies".into(),
status: "none".into(), status: "none".into(),
color: "#4c1".into() color: "#4c1".into(),
} }
} }
} }
}, }
None => { None => BadgeOptions {
BadgeOptions {
subject: "dependencies".into(), subject: "dependencies".into(),
status: "unknown".into(), status: "unknown".into(),
color: "#9f9f9f".into() color: "#9f9f9f".into(),
} },
}
}; };
Badge::new(opts) Badge::new(opts)

View file

@ -2,18 +2,21 @@ use hyper::Response;
use maud::html; use maud::html;
pub fn render(title: &str, descr: &str) -> Response { pub fn render(title: &str, descr: &str) -> Response {
super::render_html(title, html! { super::render_html(
title,
html! {
section class="hero is-light" { section class="hero is-light" {
div class="hero-head" (super::render_navbar()) div class="hero-head" { (super::render_navbar()) }
} }
section class="section" { section class="section" {
div class="container" { div class="container" {
div class="notification is-danger" { div class="notification is-danger" {
p class="title is-3" (title) p class="title is-3" { (title) }
p (descr) p { (descr) }
} }
} }
} }
(super::render_footer(None)) (super::render_footer(None))
}) },
)
} }

View file

@ -1,20 +1,20 @@
use hyper::Response; use hyper::Response;
use maud::{Markup, html}; use maud::{html, Markup};
use ::models::repo::Repository; use crate::models::crates::CratePath;
use ::models::crates::CratePath; use crate::models::repo::Repository;
fn popular_table(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>) -> Markup { fn popular_table(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>) -> Markup {
html! { html! {
div class="columns" { div class="columns" {
div class="column" { div class="column" {
h2 class="title is-3" "Popular Repositories" h2 class="title is-3" { "Popular Repositories" }
table class="table is-fullwidth is-striped is-hoverable" { table class="table is-fullwidth is-striped is-hoverable" {
thead { thead {
tr { tr {
th "Repository" th { "Repository" }
th class="has-text-right" "Status" th class="has-text-right" { "Status" }
} }
} }
tbody { tbody {
@ -34,13 +34,13 @@ fn popular_table(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>)
} }
} }
div class="column" { div class="column" {
h2 class="title is-3" "Popular Crates" h2 class="title is-3" { "Popular Crates" }
table class="table is-fullwidth is-striped is-hoverable" { table class="table is-fullwidth is-striped is-hoverable" {
thead { thead {
tr { tr {
th "Crate" th { "Crate" }
th class="has-text-right" "Status" th class="has-text-right" { "Status" }
} }
} }
tbody { tbody {
@ -64,23 +64,26 @@ fn popular_table(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>)
} }
pub fn render(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>) -> Response { pub fn render(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>) -> Response {
super::render_html("Keep your dependencies up-to-date", html! { super::render_html(
"Keep your dependencies up-to-date",
html! {
section class="hero is-light" { section class="hero is-light" {
div class="hero-head" (super::render_navbar()) div class="hero-head" { (super::render_navbar()) }
div class="hero-body" { div class="hero-body" {
div class="container" { div class="container" {
p class="title is-1" "Keep your dependencies up-to-date" p class="title is-1" { "Keep your dependencies up-to-date" }
p { p {
"Deps.rs uses semantic versioning to detect outdated or insecure dependencies in your project's" "Deps.rs uses semantic versioning to detect outdated or insecure dependencies in your project's"
code "Cargo.toml" code { "Cargo.toml" }
"." "."
} }
} }
} }
} }
section class="section" { section class="section" {
div class="container" (popular_table(popular_repos, popular_crates)) div class="container" { (popular_table(popular_repos, popular_crates)) }
} }
(super::render_footer(None)) (super::render_footer(None))
}) },
)
} }

View file

@ -1,11 +1,11 @@
use std::time::Duration; use std::time::Duration;
use hyper::Response;
use hyper::header::ContentType; use hyper::header::ContentType;
use maud::{Markup, Render, html}; use hyper::Response;
use maud::{html, Markup, Render};
pub mod index;
pub mod error; pub mod error;
pub mod index;
pub mod status; pub mod status;
use super::super::SELF_BASE_URL; use super::super::SELF_BASE_URL;
@ -16,14 +16,14 @@ fn render_html<B: Render>(title: &str, body: B) -> Response {
head { head {
meta charset="utf-8"; meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1"; meta name="viewport" content="width=device-width, initial-scale=1";
title (format!("{} - Deps.rs", title)) title { (format!("{} - Deps.rs", title)) }
link rel="icon" type="image/png" href="/static/favicon.png"; link rel="icon" type="image/png" href="/static/favicon.png";
link rel="stylesheet" type="text/css" href="/static/style.css"; link rel="stylesheet" type="text/css" href="/static/style.css";
link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Fira+Sans:400,500,600"; link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Fira+Sans:400,500,600";
link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Source+Code+Pro"; link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Source+Code+Pro";
link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"; link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css";
} }
body (body) body { (body) }
} }
}; };
@ -38,7 +38,7 @@ fn render_navbar() -> Markup {
div class="container" { div class="container" {
div class="navbar-brand" { div class="navbar-brand" {
a class="navbar-item is-dark" href=(SELF_BASE_URL) { a class="navbar-item is-dark" href=(SELF_BASE_URL) {
h1 class="title is-3" "Deps.rs" h1 class="title is-3" { "Deps.rs" }
} }
} }
} }
@ -47,25 +47,26 @@ fn render_navbar() -> Markup {
} }
fn render_footer(duration: Option<Duration>) -> Markup { fn render_footer(duration: Option<Duration>) -> Markup {
let duration_millis = duration.map(|d| d.as_secs() * 1000 + (d.subsec_nanos() / 1000 / 1000) as u64); let duration_millis =
duration.map(|d| d.as_secs() * 1000 + (d.subsec_nanos() / 1000 / 1000) as u64);
html! { html! {
footer class="footer" { footer class="footer" {
div class="container" { div class="container" {
div class="content has-text-centered" { div class="content has-text-centered" {
p { p {
strong "Deps.rs" strong { "Deps.rs" }
" is a service for the Rust community. It is open source on " " is a service for the Rust community. It is open source on "
a href="https://github.com/srijs/deps.rs" "GitHub" a href="https://github.com/srijs/deps.rs" { "GitHub" }
"." "."
} }
p { p {
"Please report any issues on the " "Please report any issues on the "
a href="https://github.com/srijs/deps.rs/issues" "issue tracker" a href="https://github.com/srijs/deps.rs/issues" { "issue tracker" }
"." "."
} }
@if let Some(millis) = duration_millis { @if let Some(millis) = duration_millis {
p class="has-text-grey is-size-7" (format!("(rendered in {} ms)", millis)) p class="has-text-grey is-size-7" { (format!("(rendered in {} ms)", millis)) }
} }
} }
} }

View file

@ -1,11 +1,11 @@
use hyper::Response; use hyper::Response;
use maud::{Markup, html};
use indexmap::IndexMap; use indexmap::IndexMap;
use maud::{html, Markup};
use ::engine::AnalyzeDependenciesOutcome; use crate::engine::AnalyzeDependenciesOutcome;
use ::models::crates::{CrateName, AnalyzedDependency, AnalyzedDependencies}; use crate::models::crates::{AnalyzedDependencies, AnalyzedDependency, CrateName};
use ::models::SubjectPath; use crate::models::repo::RepoSite;
use ::models::repo::RepoSite; use crate::models::SubjectPath;
use super::super::badge; use super::super::badge;
@ -13,11 +13,11 @@ fn dependency_tables(crate_name: CrateName, deps: AnalyzedDependencies) -> Marku
html! { html! {
h2 class="title is-3" { h2 class="title is-3" {
"Crate " "Crate "
code (crate_name.as_ref()) code { (crate_name.as_ref()) }
} }
@if deps.main.is_empty() && deps.dev.is_empty() && deps.build.is_empty() { @if deps.main.is_empty() && deps.dev.is_empty() && deps.build.is_empty() {
p class="notification has-text-centered" "No external dependencies! 🙌" p class="notification has-text-centered" { "No external dependencies! 🙌" }
} }
@if !deps.main.is_empty() { @if !deps.main.is_empty() {
@ -40,7 +40,7 @@ fn dependency_table(title: &str, deps: IndexMap<CrateName, AnalyzedDependency>)
let count_outdated = deps.iter().filter(|&(_, dep)| dep.is_outdated()).count(); let count_outdated = deps.iter().filter(|&(_, dep)| dep.is_outdated()).count();
html! { html! {
h3 class="title is-4" (title) h3 class="title is-4" { (title) }
p class="subtitle is-5" { p class="subtitle is-5" {
@if count_insecure > 0 { @if count_insecure > 0 {
(format!(" ({} total, {} insecure)", count_total, count_insecure)) (format!(" ({} total, {} insecure)", count_total, count_insecure))
@ -54,33 +54,33 @@ fn dependency_table(title: &str, deps: IndexMap<CrateName, AnalyzedDependency>)
table class="table is-fullwidth is-striped is-hoverable" { table class="table is-fullwidth is-striped is-hoverable" {
thead { thead {
tr { tr {
th "Crate" th { "Crate" }
th class="has-text-right" "Required" th class="has-text-right" { "Required" }
th class="has-text-right" "Latest" th class="has-text-right" { "Latest" }
th class="has-text-right" "Status" th class="has-text-right" { "Status" }
} }
} }
tbody { tbody {
@for (name, dep) in deps { @for (name, dep) in deps {
tr { tr {
td { td {
a href=(format!("https://crates.io/crates/{}", name.as_ref())) (name.as_ref()) a href=(format!("https://crates.io/crates/{}", name.as_ref())) { (name.as_ref()) }
} }
td class="has-text-right" code (dep.required.to_string()) td class="has-text-right" { code { (dep.required.to_string()) } }
td class="has-text-right" { td class="has-text-right" {
@if let Some(ref latest) = dep.latest { @if let Some(ref latest) = dep.latest {
code (latest.to_string()) code { (latest.to_string()) }
} @else { } @else {
"N/A" "N/A"
} }
} }
td class="has-text-right" { td class="has-text-right" {
@if dep.insecure { @if dep.insecure {
span class="tag is-danger" "insecure" span class="tag is-danger" { "insecure" }
} @else if dep.is_outdated() { } @else if dep.is_outdated() {
span class="tag is-warning" "out of date" span class="tag is-warning" { "out of date" }
} @else { } @else {
span class="tag is-success" "up to date" span class="tag is-success" { "up to date" }
} }
} }
} }
@ -94,7 +94,7 @@ fn get_site_icon(site: &RepoSite) -> &'static str {
match *site { match *site {
RepoSite::Github => "fa-github", RepoSite::Github => "fa-github",
RepoSite::Gitlab => "fa-gitlab", RepoSite::Gitlab => "fa-gitlab",
RepoSite::Bitbucket => "fa-bitbucket" RepoSite::Bitbucket => "fa-bitbucket",
} }
} }
@ -104,15 +104,15 @@ fn render_title(subject_path: &SubjectPath) -> Markup {
let site_icon = get_site_icon(&repo_path.site); let site_icon = get_site_icon(&repo_path.site);
html! { html! {
a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) { a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) {
i class=(format!("fa {}", site_icon)) "" i class=(format!("fa {}", site_icon)) { "" }
(format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref())) (format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()))
} }
} }
}, }
SubjectPath::Crate(ref crate_path) => { SubjectPath::Crate(ref crate_path) => {
html! { html! {
a href=(format!("https://crates.io/crates/{}/{}", crate_path.name.as_ref(), crate_path.version)) { a href=(format!("https://crates.io/crates/{}/{}", crate_path.name.as_ref(), crate_path.version)) {
i class="fa fa-cube" "" i class="fa fa-cube" { "" }
(format!(" {} {}", crate_path.name.as_ref(), crate_path.version)) (format!(" {} {}", crate_path.name.as_ref(), crate_path.version))
} }
} }
@ -123,7 +123,7 @@ fn render_title(subject_path: &SubjectPath) -> Markup {
fn render_failure(subject_path: SubjectPath) -> Markup { fn render_failure(subject_path: SubjectPath) -> Markup {
html! { html! {
section class="hero is-light" { section class="hero is-light" {
div class="hero-head" (super::render_navbar()) div class="hero-head" { (super::render_navbar()) }
div class="hero-body" { div class="hero-body" {
div class="container" { div class="container" {
h1 class="title is-1" { h1 class="title is-1" {
@ -135,8 +135,8 @@ fn render_failure(subject_path: SubjectPath) -> Markup {
section class="section" { section class="section" {
div class="container" { div class="container" {
div class="notification is-danger" { div class="notification is-danger" {
h2 class="title is-3" "Failed to analyze repository" h2 class="title is-3" { "Failed to analyze repository" }
p "The repository you requested might be structured in an uncommon way that is not yet supported." p { "The repository you requested might be structured in an uncommon way that is not yet supported." }
} }
} }
} }
@ -144,12 +144,20 @@ fn render_failure(subject_path: SubjectPath) -> Markup {
} }
} }
fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, subject_path: SubjectPath) -> Markup { fn render_success(
analysis_outcome: AnalyzeDependenciesOutcome,
subject_path: SubjectPath,
) -> Markup {
let self_path = match subject_path { let self_path = match subject_path {
SubjectPath::Repo(ref repo_path) => SubjectPath::Repo(ref repo_path) => format!(
format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref()), "repo/{}/{}/{}",
SubjectPath::Crate(ref crate_path) => repo_path.site.as_ref(),
repo_path.qual.as_ref(),
repo_path.name.as_ref()
),
SubjectPath::Crate(ref crate_path) => {
format!("crate/{}/{}", crate_path.name.as_ref(), crate_path.version) format!("crate/{}/{}", crate_path.name.as_ref(), crate_path.version)
}
}; };
let status_base_url = format!("{}/{}", &super::SELF_BASE_URL as &str, self_path); let status_base_url = format!("{}/{}", &super::SELF_BASE_URL as &str, self_path);
@ -165,7 +173,7 @@ fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, subject_path: Su
html! { html! {
section class=(format!("hero {}", hero_class)) { section class=(format!("hero {}", hero_class)) {
div class="hero-head" (super::render_navbar()) div class="hero-head" { (super::render_navbar()) }
div class="hero-body" { div class="hero-body" {
div class="container" { div class="container" {
h1 class="title is-1" { h1 class="title is-1" {
@ -194,12 +202,17 @@ fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, subject_path: Su
} }
} }
pub fn render(analysis_outcome: Option<AnalyzeDependenciesOutcome>, subject_path: SubjectPath) -> Response { pub fn render(
analysis_outcome: Option<AnalyzeDependenciesOutcome>,
subject_path: SubjectPath,
) -> Response {
let title = match subject_path { let title = match subject_path {
SubjectPath::Repo(ref repo_path) => SubjectPath::Repo(ref repo_path) => {
format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()), format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref())
SubjectPath::Crate(ref crate_path) => }
SubjectPath::Crate(ref crate_path) => {
format!("{} {}", crate_path.name.as_ref(), crate_path.version) format!("{} {}", crate_path.name.as_ref(), crate_path.version)
}
}; };
if let Some(outcome) = analysis_outcome { if let Some(outcome) = analysis_outcome {

View file

@ -1,2 +1,2 @@
pub mod html;
pub mod badge; pub mod badge;
pub mod html;

View file

@ -1,30 +1,31 @@
use std::fmt::{Debug, Formatter, Result as FmtResult}; use std::fmt::{Debug, Formatter, Result as FmtResult};
use std::hash::Hash; use std::hash::Hash;
use std::time::{Duration, Instant};
use std::ops::Deref; use std::ops::Deref;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::{Duration, Instant};
use failure::Error; use anyhow::{anyhow, ensure, Error};
use futures::{Future, Poll};
use futures::future::{FromErr, Shared, SharedItem}; use futures::future::{FromErr, Shared, SharedItem};
use futures::{Future, Poll};
use lru_cache::LruCache; use lru_cache::LruCache;
use shared_failure::SharedFailure;
use tokio_service::Service; use tokio_service::Service;
pub struct Cache<S> pub struct Cache<S>
where S: Service<Error=Error>, where
S::Request: Hash + Eq S: Service<Error = Error>,
S::Request: Hash + Eq,
{ {
inner: S, inner: S,
duration: Duration, duration: Duration,
cache: Mutex<LruCache<S::Request, (Instant, Shared<FromErr<S::Future, SharedFailure>>)>> cache: Mutex<LruCache<S::Request, (Instant, Shared<FromErr<S::Future, Error>>)>>,
} }
impl<S> Debug for Cache<S> impl<S> Debug for Cache<S>
where S: Service<Error=Error> + Debug, where
S::Request: Hash + Eq S: Service<Error = Error> + Debug,
S::Request: Hash + Eq,
{ {
fn fmt(&self, fmt: &mut Formatter) -> FmtResult { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
fmt.debug_struct("Cache") fmt.debug_struct("Cache")
.field("inner", &self.inner) .field("inner", &self.inner)
.field("duration", &self.duration) .field("duration", &self.duration)
@ -33,25 +34,27 @@ impl<S> Debug for Cache<S>
} }
impl<S> Cache<S> impl<S> Cache<S>
where S: Service<Error=Error>, where
S::Request: Hash + Eq S: Service<Error = Error>,
S::Request: Hash + Eq,
{ {
pub fn new(service: S, duration: Duration, capacity: usize) -> Cache<S> { pub fn new(service: S, duration: Duration, capacity: usize) -> Cache<S> {
Cache { Cache {
inner: service, inner: service,
duration: duration, duration: duration,
cache: Mutex::new(LruCache::new(capacity)) cache: Mutex::new(LruCache::new(capacity)),
} }
} }
} }
impl<S> Service for Cache<S> impl<S> Service for Cache<S>
where S: Service<Error=Error>, where
S::Request: Clone + Hash + Eq S: Service<Error = Error>,
S::Request: Clone + Hash + Eq,
{ {
type Request = S::Request; type Request = S::Request;
type Response = CachedItem<S::Response>; type Response = CachedItem<S::Response>;
type Error = SharedFailure; type Error = Error;
type Future = Cached<S::Future>; type Future = Cached<S::Future>;
fn call(&self, req: Self::Request) -> Self::Future { fn call(&self, req: Self::Request) -> Self::Future {
@ -70,25 +73,27 @@ impl<S> Service for Cache<S>
} }
} }
pub struct Cached<F: Future<Error=Error>>(Shared<FromErr<F, SharedFailure>>); pub struct Cached<F: Future<Error = Error>>(Shared<FromErr<F, Error>>);
impl<F> Debug for Cached<F> impl<F> Debug for Cached<F>
where F: Future<Error=Error> + Debug, where
F::Item: Debug F: Future<Error = Error> + Debug,
F::Item: Debug,
{ {
fn fmt(&self, fmt: &mut Formatter) -> FmtResult { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
self.0.fmt(fmt) self.0.fmt(fmt)
} }
} }
impl<F: Future<Error=Error>> Future for Cached<F> { impl<F: Future<Error = Error>> Future for Cached<F> {
type Item = CachedItem<F::Item>; type Item = CachedItem<F::Item>;
type Error = SharedFailure; type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
self.0.poll() self.0
.map_err(|err| (*err).clone()) .poll()
.map(|async| async.map(CachedItem)) .map_err(|_err| anyhow!("TODO: shared error not clone-able"))
.map(|item| item.map(CachedItem))
} }
} }