From f8dc601e5b0295a26af49b58f7bfad6b3ccc7313 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Wed, 16 Oct 2024 13:34:04 -0400 Subject: [PATCH] Migrate inventory code (#861) * Add src files from inventory repo Copy all files currently living in https://github.com/Malax/inventory/tree/main/inventory/src without any changes. Co-Authored-By: Manuel Fuchs Co-Authored-By: Josh W Lewis * Merge lib.rs and inventory.rs * Declare inventory module and feature * Prefer PhantomData * Add must_use attribute * Remove unused import * Allow unwrap for now * Add sha2 feature We may want to consider another approach here (e.g. naming the feature `inventory-sha2` and/or pulling in the inventory dependency ["inventory", "dep:sha2"]). While the crate will be pulled in if a user enables the `sha2`, the inventory-specific sha2 code won't be compiled unless the `inventory` feature is also enabled. * Add semver feature * Include patch version in dependency requirements * Add inventory toml dependency * Add inventory thiserror dependency * Add changelog entry * Allow unreachable pub for re-exports Also see https://github.com/Malax/inventory/pull/2#issuecomment-2109873726 * Remove semver and sha2 re-exports These re-exports appear to be unnecessary, and doesn't have any impact on code that rely on the `inventory` module (and have the `semver` and/or `sha2` features enabled). Also see related prior discussion here https://github.com/Malax/inventory/pull/2#issuecomment-2110436978. The `semver` and `sha2` modules only contain implementations of public traits, so I don't think we need to re-export those (unlike bringing for instance a function or struct in to scope). If I understand how trait implementations work correctly, it doesn't matter where an implementation lives (only the visibility of the trait and the type it's implemented for is relevant) - in other words, the implementation will be available to any code that can access both the trait and the type, even across crate binaries. * Rename `semver` feature to `inventory-semver` * Rename `sha2` feature to `inventory-sha2` * [stacked] Add docs to inventory code (#864) * Add module docs and example * Document resolve and partial_resolve * Document more inventory methods * Document Artifact * Document ArtifactRequirement I also suggest we change the name of `inventory/version` to `inventory/artifact_requirement.rs` or `requirement.rs`. * Update example to compare Checksum instead of string * Apply suggestions from code review Co-authored-by: Rune Soerensen * Update feature names * Rewrite example usage * Show how to display checksum in example --------- Co-authored-by: Rune Soerensen * Group inventory features alphabetically --------- Co-authored-by: Manuel Fuchs Co-authored-by: Josh W Lewis Co-authored-by: Richard Schneeman --- CHANGELOG.md | 4 + libherokubuildpack/Cargo.toml | 9 +- libherokubuildpack/src/inventory.rs | 284 +++++++++++++++++++ libherokubuildpack/src/inventory/artifact.rs | 169 +++++++++++ libherokubuildpack/src/inventory/checksum.rs | 158 +++++++++++ libherokubuildpack/src/inventory/semver.rs | 7 + libherokubuildpack/src/inventory/sha2.rs | 23 ++ libherokubuildpack/src/inventory/unit.rs | 11 + libherokubuildpack/src/inventory/version.rs | 29 ++ libherokubuildpack/src/lib.rs | 2 + 10 files changed, 695 insertions(+), 1 deletion(-) create mode 100644 libherokubuildpack/src/inventory.rs create mode 100644 libherokubuildpack/src/inventory/artifact.rs create mode 100644 libherokubuildpack/src/inventory/checksum.rs create mode 100644 libherokubuildpack/src/inventory/semver.rs create mode 100644 libherokubuildpack/src/inventory/sha2.rs create mode 100644 libherokubuildpack/src/inventory/unit.rs create mode 100644 libherokubuildpack/src/inventory/version.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7758ed50..7457aaf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `libherokubuildpack`: + - Added `inventory` module. ([#861](https://github.com/heroku/libcnb.rs/pull/861)) ## [0.23.0] - 2024-08-28 diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index abdea519..965c339a 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -18,10 +18,13 @@ all-features = true workspace = true [features] -default = ["command", "download", "digest", "error", "log", "tar", "toml", "fs", "write", "buildpack_output"] +default = ["command", "download", "digest", "error", "inventory", "log", "inventory-semver", "inventory-sha2", "tar", "toml", "fs", "write", "buildpack_output"] download = ["dep:ureq", "dep:thiserror"] digest = ["dep:sha2"] error = ["log", "dep:libcnb"] +inventory = ["dep:hex", "dep:serde", "dep:thiserror", "dep:toml"] +inventory-semver = ["dep:semver"] +inventory-sha2 = ["dep:sha2"] log = ["dep:termcolor"] tar = ["dep:tar", "dep:flate2"] toml = ["dep:toml"] @@ -38,8 +41,11 @@ crossbeam-utils = { version = "0.8.20", optional = true } # https://github.com/rust-lang/libz-sys/issues/93 # As such we have to use the next best alternate backend, which is `zlib`. flate2 = { version = "1.0.33", default-features = false, features = ["zlib"], optional = true } +hex = { version = "0.4.3", optional = true } libcnb = { workspace = true, optional = true } pathdiff = { version = "0.2.1", optional = true } +semver = { version = "1.0.21", features = ["serde"], optional = true } +serde = { version = "1.0.209", features = ["derive"], optional = true } sha2 = { version = "0.10.8", optional = true } tar = { version = "0.4.41", default-features = false, optional = true } termcolor = { version = "1.4.1", optional = true } @@ -50,4 +56,5 @@ ureq = { version = "2.10.1", default-features = false, features = ["tls"], optio [dev-dependencies] indoc = "2.0.5" libcnb-test = { workspace = true } +serde_test = "1.0.177" tempfile = "3.12.0" diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs new file mode 100644 index 00000000..5faa1ba3 --- /dev/null +++ b/libherokubuildpack/src/inventory.rs @@ -0,0 +1,284 @@ +//! # Inventory +//! +//! Many buildpacks need to provide artifacts from different URLs. A helpful pattern +//! is to provide a list of artifacts in a TOML file, which can be parsed and used by +//! the buildpack to download the correct artifact. For example, a Ruby buildpack +//! might need to download pre-compiled Ruby binaries hosted on S3. +//! +//! This module can be used to produce and consume such an inventory file. +//! +//! ## Features +//! +//! - Version lookup and comparison: To implement the inventory, you'll need to define how +//! versions are compared. This allows the inventory code to find an appropriate artifact +//! based on whatever custom version logic you need. If you don't need custom logic, you can +//! use the included `inventory-semver` feature. +//! - Architecture aware: Beyond version specifiers, buildpack authors may need to provide different +//! artifacts for different computer architectures such as ARM64 or AMD64. The inventory encodes +//! this information which is used to select the correct artifact. +//! - Checksum validation: In addition to knowing the URL of an artifact, buildp authors +//! want to be confident that the artifact they download is the correct one. To accomplish this +//! the inventory contains a checksum of the download and can be used to validate the download +//! has not been modified or tampered with. To use sha256 or sha512 checksums out of the box, +//! enable the `inventory-sha2` feature +//! - Extensible with metadata: The default inventory format covers a lot of common use cases, +//! but if you need more, you can extend it by adding custom metadata to each artifact. +//! +//! ## Example usage +//! +//! This example demonstrates: +//! * Creating an artifact using the `inventory-sha2` and `inventory-semver` features. +//! * Adding the artifact to an inventory. +//! * Serializing and deserializing the inventory [to](Inventory#method.fmt) and [from](Inventory::from_str) TOML. +//! * [Resolving an inventory artifact](Inventory::resolve) specifying relevant OS, architecture, and version requirements. +//! * Using the resolved artifact's checksum value to verify "downloaded" data. +//! +//! ```rust +//! use libherokubuildpack::inventory::{artifact::{Arch, Artifact, Os}, Inventory, checksum::Checksum}; +//! use semver::{Version, VersionReq}; +//! use sha2::{Sha256, Digest}; +//! +//! // Create an artifact with a SHA256 checksum and `semver::Version` +//! let new_artifact = Artifact { +//! version: Version::new(1, 0, 0), +//! os: Os::Linux, +//! arch: Arch::Arm64, +//! url: "https://example.com/foo.txt".to_string(), +//! checksum: "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" +//! .parse::>() +//! .unwrap(), +//! metadata: None, +//! }; +//! +//! // Create an inventory and add the artifact +//! let mut inventory = Inventory::>::new(); +//! inventory.push(new_artifact.clone()); +//! +//! // Serialize the inventory to TOML +//! let inventory_toml = inventory.to_string(); +//! assert_eq!( +//! r#"[[artifacts]] +//! version = "1.0.0" +//! os = "linux" +//! arch = "arm64" +//! url = "https://example.com/foo.txt" +//! checksum = "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" +//! "#, +//! inventory_toml +//! ); +//! +//! // Deserialize the inventory from TOML +//! let parsed_inventory = inventory_toml +//! .parse::>>() +//! .unwrap(); +//! +//! // Resolve the artifact by OS, architecture, and version requirement +//! let version_req = VersionReq::parse("=1.0.0").unwrap(); +//! let resolved_artifact = parsed_inventory.resolve(Os::Linux, Arch::Arm64, &version_req).unwrap(); +//! +//! assert_eq!(&new_artifact, resolved_artifact); +//! +//! // Verify checksum of the resolved artifact +//! let downloaded_data = "foo"; // Example downloaded file content +//! let downloaded_checksum = Sha256::digest(downloaded_data).to_vec(); +//! +//! assert_eq!(downloaded_checksum, resolved_artifact.checksum.value); +//! println!( +//! "Successfully downloaded {} with checksum {}", +//! resolved_artifact.url, +//! hex::encode(&resolved_artifact.checksum.value) +//! ); +//! ``` +pub mod artifact; +pub mod checksum; +pub mod version; + +#[cfg(feature = "inventory-semver")] +mod semver; +#[cfg(feature = "inventory-sha2")] +mod sha2; +mod unit; + +use crate::inventory::artifact::{Arch, Artifact, Os}; +use crate::inventory::checksum::Digest; +use crate::inventory::version::ArtifactRequirement; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::fmt::Formatter; +use std::str::FromStr; + +/// Represents an inventory of artifacts. +/// +/// An inventory can be read directly from a TOML file on disk and used by a buildpack to resolve +/// requirements for a specific artifact to download. +/// +/// The inventory can be manipulated in-memory and then re-serialized to disk to facilitate both +/// reading and writing inventory files. +#[derive(Debug, Serialize, Deserialize)] +pub struct Inventory { + #[serde(bound = "V: Serialize + DeserializeOwned, D: Digest, M: Serialize + DeserializeOwned")] + pub artifacts: Vec>, +} + +impl Default for Inventory { + fn default() -> Self { + Self { artifacts: vec![] } + } +} + +impl Inventory { + /// Creates a new empty inventory + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Add a new artifact to the in-memory inventory + pub fn push(&mut self, artifact: Artifact) { + self.artifacts.push(artifact); + } + + /// Return a single artifact as the best match given the input constraints + /// + /// If multiple artifacts match the constraints, the one with the highest version is returned. + pub fn resolve(&self, os: Os, arch: Arch, requirement: &R) -> Option<&Artifact> + where + V: Ord, + R: ArtifactRequirement, + { + self.artifacts + .iter() + .filter(|artifact| { + artifact.os == os + && artifact.arch == arch + && requirement.satisfies_version(&artifact.version) + && requirement.satisfies_metadata(&artifact.metadata) + }) + .max_by_key(|artifact| &artifact.version) + } + + /// Resolve logic for Artifacts that implement `PartialOrd` rather than `Ord` + /// + /// Some version implementations are only partially ordered. One example could be f32 which is not totally ordered + /// because NaN is not comparable to any other number. + pub fn partial_resolve( + &self, + os: Os, + arch: Arch, + requirement: &R, + ) -> Option<&Artifact> + where + V: PartialOrd, + R: ArtifactRequirement, + { + #[inline] + fn partial_max_by_key(iterator: I, f: F) -> Option + where + I: Iterator, + F: Fn(&I::Item) -> A, + A: PartialOrd, + { + iterator.fold(None, |acc, item| match acc { + None => Some(item), + Some(acc) => match f(&item).partial_cmp(&f(&acc)) { + Some(Ordering::Greater | Ordering::Equal) => Some(item), + None | Some(Ordering::Less) => Some(acc), + }, + }) + } + + partial_max_by_key( + self.artifacts.iter().filter(|artifact| { + artifact.os == os + && artifact.arch == arch + && requirement.satisfies_version(&artifact.version) + && requirement.satisfies_metadata(&artifact.metadata) + }), + |artifact| &artifact.version, + ) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum ParseInventoryError { + #[error("TOML parsing error: {0}")] + TomlError(toml::de::Error), +} + +impl FromStr for Inventory +where + V: Serialize + DeserializeOwned, + D: Digest, + M: Serialize + DeserializeOwned, +{ + type Err = ParseInventoryError; + + fn from_str(s: &str) -> Result { + toml::from_str(s).map_err(ParseInventoryError::TomlError) + } +} + +impl std::fmt::Display for Inventory +where + V: Serialize + DeserializeOwned, + D: Digest, + M: Serialize + DeserializeOwned, +{ + #![allow(clippy::unwrap_used)] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&toml::to_string(self).unwrap()) + } +} + +#[cfg(test)] +mod test { + use crate::inventory::artifact::{Arch, Artifact, Os}; + use crate::inventory::checksum::tests::BogusDigest; + use crate::inventory::Inventory; + + #[test] + fn test_matching_artifact_resolution() { + let mut inventory = Inventory::new(); + inventory.push(create_artifact("foo", Os::Linux, Arch::Arm64)); + + assert_eq!( + "foo", + &inventory + .resolve(Os::Linux, Arch::Arm64, &String::from("foo")) + .expect("should resolve matching artifact") + .version, + ); + } + + #[test] + fn test_dont_resolve_artifact_with_wrong_arch() { + let mut inventory = Inventory::new(); + inventory.push(create_artifact("foo", Os::Linux, Arch::Arm64)); + + assert!(inventory + .resolve(Os::Linux, Arch::Amd64, &String::from("foo")) + .is_none()); + } + + #[test] + fn test_dont_resolve_artifact_with_wrong_version() { + let mut inventory = Inventory::new(); + inventory.push(create_artifact("foo", Os::Linux, Arch::Arm64)); + + assert!(inventory + .resolve(Os::Linux, Arch::Arm64, &String::from("bar")) + .is_none()); + } + + fn create_artifact(version: &str, os: Os, arch: Arch) -> Artifact { + Artifact { + version: String::from(version), + os, + arch, + url: "https://example.com".to_string(), + checksum: BogusDigest::checksum("cafebabe"), + metadata: (), + } + } +} diff --git a/libherokubuildpack/src/inventory/artifact.rs b/libherokubuildpack/src/inventory/artifact.rs new file mode 100644 index 00000000..e13ae023 --- /dev/null +++ b/libherokubuildpack/src/inventory/artifact.rs @@ -0,0 +1,169 @@ +use crate::inventory::checksum::{Checksum, Digest}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::FromStr; + +/// Representation of a downloadable artifact such as a binary tarball. +/// +/// An inventory is made up of multiple artifacts that have a version that +/// can be compared to each other and a URL where the artifact can be downloaded. +/// +/// Artifacts are OS and architectures specific. The checksum value can +/// be used to validate an artifact once it has been downloaded. +/// +/// Metadata can be used to store additional information about the artifact. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Artifact { + #[serde(bound = "V: Serialize + DeserializeOwned")] + pub version: V, + pub os: Os, + pub arch: Arch, + pub url: String, + #[serde(bound = "D: Digest")] + pub checksum: Checksum, + #[serde(bound = "M: Serialize + DeserializeOwned")] + pub metadata: M, +} + +impl PartialEq for Artifact +where + V: Eq, + M: Eq, +{ + fn eq(&self, other: &Self) -> bool { + self.version == other.version + && self.os == other.os + && self.arch == other.arch + && self.url == other.url + && self.checksum == other.checksum + && self.metadata == other.metadata + } +} + +impl Eq for Artifact +where + V: Eq, + M: Eq, +{ +} + +#[derive(Debug, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Os { + Darwin, + Linux, +} + +#[derive(Debug, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Arch { + Amd64, + Arm64, +} + +impl Display for Os { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Os::Darwin => write!(f, "darwin"), + Os::Linux => write!(f, "linux"), + } + } +} + +#[derive(thiserror::Error, Debug)] +#[error("OS is not supported: {0}")] +pub struct UnsupportedOsError(String); + +impl FromStr for Os { + type Err = UnsupportedOsError; + + fn from_str(s: &str) -> Result { + match s { + "linux" => Ok(Os::Linux), + "darwin" | "osx" => Ok(Os::Darwin), + _ => Err(UnsupportedOsError(s.to_string())), + } + } +} + +impl Display for Arch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Arch::Amd64 => write!(f, "amd64"), + Arch::Arm64 => write!(f, "arm64"), + } + } +} + +#[derive(thiserror::Error, Debug)] +#[error("Arch is not supported: {0}")] +pub struct UnsupportedArchError(String); + +impl FromStr for Arch { + type Err = UnsupportedArchError; + + fn from_str(s: &str) -> Result { + match s { + "amd64" | "x86_64" => Ok(Arch::Amd64), + "arm64" | "aarch64" => Ok(Arch::Arm64), + _ => Err(UnsupportedArchError(s.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::inventory::version::VersionRequirement; + + #[test] + fn test_arch_display_format() { + let archs = [(Arch::Amd64, "amd64"), (Arch::Arm64, "arm64")]; + + for (input, expected) in archs { + assert_eq!(expected, input.to_string()); + } + } + + #[test] + fn test_arch_parsing() { + let archs = [ + ("amd64", Arch::Amd64), + ("arm64", Arch::Arm64), + ("x86_64", Arch::Amd64), + ("aarch64", Arch::Arm64), + ]; + for (input, expected) in archs { + assert_eq!(expected, input.parse::().unwrap()); + } + + assert!(matches!( + "foo".parse::().unwrap_err(), + UnsupportedArchError(..) + )); + } + + #[test] + fn test_os_display_format() { + assert_eq!("linux", Os::Linux.to_string()); + } + + #[test] + fn test_os_parsing() { + assert_eq!(Os::Linux, "linux".parse::().unwrap()); + assert_eq!(Os::Darwin, "darwin".parse::().unwrap()); + assert_eq!(Os::Darwin, "osx".parse::().unwrap()); + + assert!(matches!( + "foo".parse::().unwrap_err(), + UnsupportedOsError(..) + )); + } + + impl VersionRequirement for String { + fn satisfies(&self, version: &String) -> bool { + self == version + } + } +} diff --git a/libherokubuildpack/src/inventory/checksum.rs b/libherokubuildpack/src/inventory/checksum.rs new file mode 100644 index 00000000..f2eb6daf --- /dev/null +++ b/libherokubuildpack/src/inventory/checksum.rs @@ -0,0 +1,158 @@ +use hex::FromHexError; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; +use std::str::FromStr; + +#[derive(Debug, Clone, Eq)] +pub struct Checksum { + pub name: String, + pub value: Vec, + digest: PhantomData, +} + +impl PartialEq for Checksum { + fn eq(&self, other: &Self) -> bool { + (self.name == other.name) && (self.value == other.value) + } +} + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum ChecksumParseError { + #[error("Checksum prefix is missing")] + MissingPrefix, + #[error("Checksum prefix \"{0}\" is incompatible")] + IncompatiblePrefix(String), + #[error("Checksum value cannot be parsed as hex string: {0}")] + InvalidValue(FromHexError), + #[error("Checksum value length {0} is invalid")] + InvalidChecksumLength(usize), +} + +impl FromStr for Checksum +where + D: Digest, +{ + type Err = ChecksumParseError; + + fn from_str(value: &str) -> Result { + let (name, value) = value + .split_once(':') + .ok_or(ChecksumParseError::MissingPrefix) + .and_then(|(key, value)| { + hex::decode(value) + .map_err(ChecksumParseError::InvalidValue) + .map(|value| (String::from(key), value)) + })?; + + if !D::name_compatible(&name) { + Err(ChecksumParseError::IncompatiblePrefix(name)) + } else if !D::length_compatible(value.len()) { + Err(ChecksumParseError::InvalidChecksumLength(value.len())) + } else { + Ok(Checksum { + name, + value, + digest: PhantomData, + }) + } + } +} + +pub trait Digest { + fn name_compatible(name: &str) -> bool; + fn length_compatible(len: usize) -> bool; +} + +impl Serialize for Checksum +where + D: Digest, +{ + fn serialize(&self, serializer: T) -> Result + where + T: serde::Serializer, + { + serializer.serialize_str(&format!("{}:{}", self.name, hex::encode(&self.value))) + } +} + +impl<'de, D> Deserialize<'de> for Checksum +where + D: Digest, +{ + fn deserialize(deserializer: T) -> Result + where + T: serde::Deserializer<'de>, + { + String::deserialize(deserializer) + .and_then(|string| string.parse::().map_err(serde::de::Error::custom)) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use serde_test::{assert_de_tokens_error, assert_tokens, Token}; + + #[derive(Debug)] + pub(crate) struct BogusDigest; + + impl BogusDigest { + pub(crate) fn checksum(hex_string: &str) -> Checksum { + Checksum { + name: String::from("bogus"), + value: hex::decode(hex_string).unwrap(), + digest: PhantomData, + } + } + } + + impl Digest for BogusDigest { + fn name_compatible(name: &str) -> bool { + name == "bogus" + } + + fn length_compatible(len: usize) -> bool { + len == 4 + } + } + + #[test] + fn test_checksum_serialization() { + assert_tokens( + &BogusDigest::checksum("cafebabe"), + &[Token::BorrowedStr("bogus:cafebabe")], + ); + } + + #[test] + fn test_invalid_checksum_deserialization() { + assert_de_tokens_error::>( + &[Token::BorrowedStr("baz:cafebabe")], + "Checksum prefix \"baz\" is incompatible", + ); + } + + #[test] + fn test_invalid_checksum_size() { + assert_eq!( + "bogus:123456".parse::>(), + Err(ChecksumParseError::InvalidChecksumLength(3)) + ); + } + + #[test] + fn test_invalid_hex_input() { + assert!(matches!( + "bogus:quux".parse::>(), + Err(ChecksumParseError::InvalidValue( + FromHexError::InvalidHexCharacter { c: 'q', index: 0 } + )) + )); + } + + #[test] + fn test_checksum_parse_and_serialize() { + let checksum = "bogus:cafebabe".parse::>().unwrap(); + assert_tokens(&checksum, &[Token::BorrowedStr("bogus:cafebabe")]); + } +} diff --git a/libherokubuildpack/src/inventory/semver.rs b/libherokubuildpack/src/inventory/semver.rs new file mode 100644 index 00000000..0b2be6e5 --- /dev/null +++ b/libherokubuildpack/src/inventory/semver.rs @@ -0,0 +1,7 @@ +use crate::inventory::version::VersionRequirement; + +impl VersionRequirement for semver::VersionReq { + fn satisfies(&self, version: &semver::Version) -> bool { + self.matches(version) + } +} diff --git a/libherokubuildpack/src/inventory/sha2.rs b/libherokubuildpack/src/inventory/sha2.rs new file mode 100644 index 00000000..d6dbbccb --- /dev/null +++ b/libherokubuildpack/src/inventory/sha2.rs @@ -0,0 +1,23 @@ +use crate::inventory::checksum::Digest; +use sha2::digest::crypto_common::OutputSizeUser; +use sha2::{Sha256, Sha512}; + +impl Digest for Sha256 { + fn name_compatible(name: &str) -> bool { + name == "sha256" + } + + fn length_compatible(len: usize) -> bool { + len == Self::output_size() + } +} + +impl Digest for Sha512 { + fn name_compatible(name: &str) -> bool { + name == "sha512" + } + + fn length_compatible(len: usize) -> bool { + len == Self::output_size() + } +} diff --git a/libherokubuildpack/src/inventory/unit.rs b/libherokubuildpack/src/inventory/unit.rs new file mode 100644 index 00000000..d04422cc --- /dev/null +++ b/libherokubuildpack/src/inventory/unit.rs @@ -0,0 +1,11 @@ +use crate::inventory::checksum::Digest; + +impl Digest for () { + fn name_compatible(_: &str) -> bool { + true + } + + fn length_compatible(_: usize) -> bool { + true + } +} diff --git a/libherokubuildpack/src/inventory/version.rs b/libherokubuildpack/src/inventory/version.rs new file mode 100644 index 00000000..775ac974 --- /dev/null +++ b/libherokubuildpack/src/inventory/version.rs @@ -0,0 +1,29 @@ +/// Represents the requirements for a valid artifact +/// +/// Checks the version and metadata of an artifact are valid or not +pub trait ArtifactRequirement { + /// Return true if the given metadata satisfies the requirement + fn satisfies_metadata(&self, metadata: &M) -> bool; + + /// Return true if the given version satisfies the requirement + fn satisfies_version(&self, version: &V) -> bool; +} + +/// Check if the version satisfies the requirement (ignores Metadata) +pub trait VersionRequirement { + /// Return true if the given version satisfies the requirement + fn satisfies(&self, version: &V) -> bool; +} + +impl ArtifactRequirement for VR +where + VR: VersionRequirement, +{ + fn satisfies_metadata(&self, _: &M) -> bool { + true + } + + fn satisfies_version(&self, version: &V) -> bool { + self.satisfies(version) + } +} diff --git a/libherokubuildpack/src/lib.rs b/libherokubuildpack/src/lib.rs index c742a325..0596a3c6 100644 --- a/libherokubuildpack/src/lib.rs +++ b/libherokubuildpack/src/lib.rs @@ -12,6 +12,8 @@ pub mod download; pub mod error; #[cfg(feature = "fs")] pub mod fs; +#[cfg(feature = "inventory")] +pub mod inventory; #[cfg(feature = "log")] pub mod log; #[cfg(feature = "tar")]