diff --git a/Cargo.lock b/Cargo.lock index c716236..70a2f94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,8 @@ dependencies = [ "base64", "once_cell", "rusttype", + "serde", + "serde_urlencoded", ] [[package]] @@ -63,9 +65,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "bytes" @@ -173,9 +175,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" dependencies = [ "cfg-if", "crossbeam-utils", @@ -183,9 +185,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" dependencies = [ "cfg-if", "lazy_static", @@ -263,6 +265,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "fastrand" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +dependencies = [ + "instant", +] + [[package]] name = "fnv" version = "1.0.7" @@ -348,9 +359,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -367,9 +378,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if", "libc", @@ -399,9 +410,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "h2" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd" +checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" dependencies = [ "bytes", "fnv", @@ -451,13 +462,13 @@ dependencies = [ [[package]] name = "http" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa 0.4.8", + "itoa 1.0.1", ] [[package]] @@ -549,15 +560,24 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", "hashbrown", "serde", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.3.1" @@ -800,9 +820,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" @@ -834,9 +854,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -859,12 +879,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ppv-lite86" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -919,46 +933,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" version = "0.2.10" @@ -998,15 +972,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c4e0a76dc12a116108933f6301b95e83634e0c47b0afbed6abbaa0601e99258" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "hyper", @@ -1156,18 +1131,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -1176,9 +1151,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.73" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" +checksum = "c059c05b48c5c0067d4b4b2b4f0732dd65feb52daf7e0ea09cd87e7dadc1af79" dependencies = [ "itoa 1.0.1", "ryu", @@ -1233,6 +1208,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_urlencoded", "sha-1", "slog", "slog-async", @@ -1306,9 +1282,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" dependencies = [ "proc-macro2", "quote", @@ -1323,13 +1299,13 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", + "fastrand", "libc", - "rand", "redox_syscall", "remove_dir_all", "winapi", @@ -1561,9 +1537,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" diff --git a/Cargo.toml b/Cargo.toml index d6f0a60..15816dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ rustsec = "0.25" semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_urlencoded = "0.7" slog = "2" slog-async = "2" slog-term = "2" diff --git a/libs/badge/Cargo.toml b/libs/badge/Cargo.toml index f187595..4b652c6 100644 --- a/libs/badge/Cargo.toml +++ b/libs/badge/Cargo.toml @@ -15,3 +15,7 @@ path = "badge.rs" base64 = "0.13" once_cell = "1" rusttype = "0.9" +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +serde_urlencoded = "0.7" diff --git a/libs/badge/badge.rs b/libs/badge/badge.rs index 923aed7..8e5d19d 100644 --- a/libs/badge/badge.rs +++ b/libs/badge/badge.rs @@ -3,6 +3,7 @@ use base64::display::Base64Display; use once_cell::sync::Lazy; use rusttype::{point, Font, Point, PositionedGlyph, Scale}; +use serde::Deserialize; const FONT_DATA: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/DejaVuSans.ttf")); const FONT_SIZE: f32 = 11.; @@ -11,13 +12,37 @@ const SCALE: Scale = Scale { y: FONT_SIZE, }; +/// Badge style name. +/// +/// Default style is "flat". +/// +/// Matches style names from shields.io. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum BadgeStyle { + Flat, + FlatSquare, +} + +impl Default for BadgeStyle { + fn default() -> Self { + Self::Flat + } +} + +#[derive(Debug, Clone)] pub struct BadgeOptions { /// Subject will be displayed on the left side of badge pub subject: String, + /// Status will be displayed on the right side of badge pub status: String, + /// HTML color of badge pub color: String, + + /// Style of badge. + pub style: BadgeStyle, } impl Default for BadgeOptions { @@ -26,6 +51,7 @@ impl Default for BadgeOptions { subject: "build".to_owned(), status: "passing".to_owned(), color: "#4c1".to_owned(), + style: BadgeStyle::Flat, } } } @@ -66,48 +92,78 @@ impl Badge { } pub fn to_svg(&self) -> String { + match self.options.style { + BadgeStyle::Flat => self.to_flat_svg(), + BadgeStyle::FlatSquare => self.to_flat_square_svg(), + } + } + + pub fn to_flat_svg(&self) -> String { let left_width = self.calculate_width(&self.options.subject) + 6; let right_width = self.calculate_width(&self.options.status) + 6; + let total_width = left_width + right_width; + + let left_center = left_width / 2; + let right_center = left_width + (right_width / 2); + + let color = &self.options.color; + let subject = &self.options.subject; + let status = &self.options.status; let svg = format!( - r###" + r###" - + - - - + + + - {} - {} - {} - {} + {subject} + {subject} + {status} + {status} -"###, - left_width + right_width, - left_width + right_width, - left_width, - left_width, - right_width, - self.options.color, - left_width + right_width, - (left_width) / 2, - self.options.subject, - (left_width) / 2, - self.options.subject, - left_width + (right_width / 2), - self.options.status, - left_width + (right_width / 2), - self.options.status +"### + ); + + svg + } + + pub fn to_flat_square_svg(&self) -> String { + let left_width = self.calculate_width(&self.options.subject) + 6; + let right_width = self.calculate_width(&self.options.status) + 6; + let total_width = left_width + right_width; + + let left_center = left_width / 2; + let right_center = left_width + (right_width / 2); + + let color = &self.options.color; + let subject = &self.options.subject; + let status = &self.options.status; + + let svg = format!( + r###" + + + + + + + {subject} + {status} + + +"###, ); svg @@ -158,4 +214,18 @@ mod tests { let badge = Badge::new(options); file.write_all(badge.to_svg().as_bytes()).unwrap(); } + + #[test] + fn deserialize_badge_style() { + #[derive(Debug, Deserialize)] + struct Foo { + style: BadgeStyle, + } + + let style = serde_urlencoded::from_str::("style=flat").unwrap(); + assert_eq!(style.style, BadgeStyle::Flat); + + let style = serde_urlencoded::from_str::("style=flat-square").unwrap(); + assert_eq!(style.style, BadgeStyle::FlatSquare); + } } diff --git a/src/main.rs b/src/main.rs index 0ba3d4e..3ac048d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,10 +72,10 @@ async fn main() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port); let mut managed_index = ManagedIndex::new(Duration::from_secs(20), logger.clone()); - if let Err(e) = managed_index.initial_clone().await { + if let Err(err) = managed_index.initial_clone().await { error!( logger, - "failed running initial clone of the crates.io-index: {}", e + "failed running initial clone of the crates.io-index: {err}", ); } diff --git a/src/server/mod.rs b/src/server/mod.rs index 8230ba1..7bbfca0 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,6 @@ use std::{env, sync::Arc, time::Instant}; +use badge::BadgeStyle; use futures_util::future; use hyper::{ header::{CACHE_CONTROL, CONTENT_TYPE, ETAG, LOCATION}, @@ -8,6 +9,7 @@ use hyper::{ use once_cell::sync::Lazy; use route_recognizer::{Params, Router}; use semver::VersionReq; +use serde::Deserialize; use slog::{error, info, o, Logger}; mod assets; @@ -162,7 +164,7 @@ impl App { async fn repo_status( &self, - _req: Request, + req: Request, params: Params, logger: Logger, format: StatusFormat, @@ -173,6 +175,8 @@ impl App { let qual = params.find("qual").expect("route param 'qual' not found"); let name = params.find("name").expect("route param 'name' not found"); + let badge_knobs = BadgeKnobs::from_query_string(req.uri().query()); + let repo_path_result = RepoPath::from_parts(site, qual, name); match repo_path_result { @@ -195,8 +199,12 @@ impl App { match analyze_result { Err(err) => { error!(logger, "error: {}", err); - let response = - App::status_format_analysis(None, format, SubjectPath::Repo(repo_path)); + let response = App::status_format_analysis( + None, + format, + SubjectPath::Repo(repo_path), + badge_knobs, + ); Ok(response) } Ok(analysis_outcome) => { @@ -204,6 +212,7 @@ impl App { Some(analysis_outcome), format, SubjectPath::Repo(repo_path), + badge_knobs, ); Ok(response) } @@ -280,7 +289,7 @@ impl App { async fn crate_status( &self, - _req: Request, + req: Request, params: Params, logger: Logger, format: StatusFormat, @@ -292,6 +301,8 @@ impl App { .find("version") .expect("route param 'version' not found"); + let badge_knobs = BadgeKnobs::from_query_string(req.uri().query()); + let crate_path_result = CratePath::from_parts(name, version); match crate_path_result { @@ -317,6 +328,7 @@ impl App { None, format, SubjectPath::Crate(crate_path), + badge_knobs, ); Ok(response) } @@ -325,6 +337,7 @@ impl App { Some(analysis_outcome), format, SubjectPath::Crate(crate_path), + badge_knobs, ); Ok(response) @@ -338,9 +351,10 @@ impl App { analysis_outcome: Option, format: StatusFormat, subject_path: SubjectPath, + badge_knobs: BadgeKnobs, ) -> Response { match format { - StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref()), + StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref(), badge_knobs), StatusFormat::Html => views::html::status::render(analysis_outcome, subject_path), } } @@ -367,3 +381,28 @@ fn not_found() -> Response { static SELF_BASE_URL: Lazy = Lazy::new(|| env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string())); + +#[derive(Debug, Clone, Default)] +pub struct BadgeKnobs { + style: BadgeStyle, + compact: bool, +} + +impl BadgeKnobs { + fn from_query_string(qs: Option<&str>) -> Self { + #[derive(Debug, Clone, Default, Deserialize)] + struct BadgeKnobsPartial { + style: Option, + compact: Option, + } + + let badge_knobs = qs + .and_then(|qs| serde_urlencoded::from_str::(qs).ok()) + .unwrap_or_default(); + + Self { + style: badge_knobs.style.unwrap_or_default(), + compact: badge_knobs.compact.unwrap_or_default(), + } + } +} diff --git a/src/server/views/badge.rs b/src/server/views/badge.rs index 4282f1b..7dc4bbe 100644 --- a/src/server/views/badge.rs +++ b/src/server/views/badge.rs @@ -3,60 +3,80 @@ use hyper::header::CONTENT_TYPE; use hyper::{Body, Response}; use crate::engine::AnalyzeDependenciesOutcome; +use crate::server::BadgeKnobs; + +pub fn badge( + analysis_outcome: Option<&AnalyzeDependenciesOutcome>, + badge_knobs: BadgeKnobs, +) -> Badge { + let subject = if badge_knobs.compact { + "deps" + } else { + "dependencies" + } + .to_owned(); -pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge { let opts = match analysis_outcome { Some(outcome) => { if outcome.any_always_insecure() { BadgeOptions { - subject: "dependencies".into(), + subject, status: "insecure".into(), color: "#e05d44".into(), + style: badge_knobs.style, } } else { let (outdated, total) = outcome.outdated_ratio(); if outdated > 0 { BadgeOptions { - subject: "dependencies".into(), + subject, status: format!("{} of {} outdated", outdated, total), color: "#dfb317".into(), + style: badge_knobs.style, } } else if total > 0 { if outcome.any_insecure() { BadgeOptions { - subject: "dependencies".into(), + subject, status: "maybe insecure".into(), color: "#8b1".into(), + style: badge_knobs.style, } } else { BadgeOptions { - subject: "dependencies".into(), + subject, status: "up to date".into(), color: "#4c1".into(), + style: badge_knobs.style, } } } else { BadgeOptions { - subject: "dependencies".into(), + subject, status: "none".into(), color: "#4c1".into(), + style: badge_knobs.style, } } } } None => BadgeOptions { - subject: "dependencies".into(), + subject, status: "unknown".into(), color: "#9f9f9f".into(), + style: badge_knobs.style, }, }; Badge::new(opts) } -pub fn response(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Response { - let badge = badge(analysis_outcome).to_svg(); +pub fn response( + analysis_outcome: Option<&AnalyzeDependenciesOutcome>, + badge_knobs: BadgeKnobs, +) -> Response { + let badge = badge(analysis_outcome, badge_knobs).to_svg(); Response::builder() .header(CONTENT_TYPE, "image/svg+xml; charset=utf-8") diff --git a/src/server/views/html/status.rs b/src/server/views/html/status.rs index 475959b..4596794 100644 --- a/src/server/views/html/status.rs +++ b/src/server/views/html/status.rs @@ -11,6 +11,7 @@ use crate::models::crates::{AnalyzedDependencies, AnalyzedDependency, CrateName} use crate::models::repo::RepoSite; use crate::models::SubjectPath; use crate::server::views::badge; +use crate::server::BadgeKnobs; fn get_crates_url(name: impl AsRef) -> String { format!("https://crates.io/crates/{}", name.as_ref()) @@ -343,7 +344,8 @@ fn render_success( }; let status_base_url = format!("{}/{}", &super::SELF_BASE_URL as &str, self_path); - let status_data_uri = badge::badge(Some(&analysis_outcome)).to_svg_data_uri(); + let status_data_uri = + badge::badge(Some(&analysis_outcome), BadgeKnobs::default()).to_svg_data_uri(); let hero_class = if analysis_outcome.any_always_insecure() { "is-danger"