use serde::Deserialize;
use std::sync::OnceLock;
use thiserror::Error;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct DependencyBinary {
package: String,
binary: String,
version: String,
license: String,
repository: String,
}
impl DependencyBinary {
#[must_use]
pub fn package(&self) -> &str {
&self.package
}
#[must_use]
pub fn binary(&self) -> &str {
&self.binary
}
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
#[must_use]
pub fn license(&self) -> &str {
&self.license
}
#[must_use]
pub fn repository(&self) -> &str {
&self.repository
}
}
#[derive(Debug, Deserialize)]
struct DependencyBinaryManifest {
dependency_binaries: Vec<DependencyBinary>,
}
#[derive(Debug, Clone, Error)]
pub enum ManifestError {
#[error("manifest parse error: {0}")]
ParseError(String),
#[error("duplicate package in manifest: {0}")]
DuplicatePackage(String),
}
impl From<toml::de::Error> for ManifestError {
fn from(error: toml::de::Error) -> Self {
ManifestError::ParseError(error.to_string())
}
}
#[must_use]
pub fn manifest_contents() -> &'static str {
include_str!("../../dependency-binaries.toml")
}
pub fn parse_manifest(contents: &str) -> Result<Vec<DependencyBinary>, ManifestError> {
let manifest: DependencyBinaryManifest = toml::from_str(contents)?;
let mut seen_packages = std::collections::HashSet::new();
for dependency in &manifest.dependency_binaries {
let package = dependency.package();
if !seen_packages.insert(package.to_string()) {
return Err(ManifestError::DuplicatePackage(package.to_string()));
}
}
Ok(manifest.dependency_binaries)
}
pub fn required_dependency_binaries() -> Result<&'static [DependencyBinary], ManifestError> {
static MANIFEST: OnceLock<Result<Vec<DependencyBinary>, ManifestError>> = OnceLock::new();
match MANIFEST.get_or_init(|| parse_manifest(manifest_contents())) {
Ok(dependencies) => Ok(dependencies.as_slice()),
Err(error) => Err(error.clone()),
}
}
#[must_use = "callers should handle missing packages and manifest parse failures"]
pub fn find_dependency_binary(
package: &str,
) -> Result<Option<&'static DependencyBinary>, ManifestError> {
Ok(required_dependency_binaries()?
.iter()
.find(|dependency| dependency.package() == package))
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::{fixture, rstest};
#[fixture]
fn missing_field_manifest() -> &'static str {
r#"
[[dependency_binaries]]
package = "cargo-dylint"
binary = "cargo-dylint"
version = "4.1.0"
license = "MIT OR Apache-2.0"
"#
}
#[fixture]
fn duplicate_packages_manifest() -> &'static str {
r#"
[[dependency_binaries]]
package = "cargo-dylint"
binary = "cargo-dylint"
version = "4.1.0"
license = "MIT OR Apache-2.0"
repository = "https://github.com/trailofbits/dylint"
[[dependency_binaries]]
package = "cargo-dylint"
binary = "cargo-dylint-alt"
version = "4.2.0"
license = "MIT OR Apache-2.0"
repository = "https://github.com/trailofbits/dylint"
"#
}
#[test]
fn manifest_contains_expected_tools() {
let dependencies =
required_dependency_binaries().expect("embedded manifest should stay parseable");
assert_eq!(dependencies.len(), 2);
assert!(
dependencies
.iter()
.any(|tool| tool.package() == "cargo-dylint")
);
assert!(
dependencies
.iter()
.any(|tool| tool.package() == "dylint-link")
);
}
#[rstest]
#[case::missing_field(missing_field_manifest(), &["repository"])]
#[case::duplicate_packages(duplicate_packages_manifest(), &["cargo-dylint", "duplicate"])]
fn parse_manifest_rejects_invalid_manifests(
#[case] manifest_fixture: &str,
#[case] expected_substrings: &[&str],
) {
let error = parse_manifest(manifest_fixture).expect_err("should reject invalid manifest");
let error_string = error.to_string();
for substring in expected_substrings {
assert!(
error_string.contains(substring),
"expected error to contain '{substring}'"
);
}
}
#[test]
fn find_dependency_binary_returns_matching_package() {
let tool = find_dependency_binary("cargo-dylint")
.expect("embedded manifest should stay parseable")
.expect("tool should exist");
assert_eq!(tool.binary(), "cargo-dylint");
assert_eq!(tool.version(), "4.1.0");
}
}