From 0abce6a2d5e4dff92201edf3db64dc0dcd46a973 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 00:54:31 -0400 Subject: [PATCH 01/19] 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 --- libherokubuildpack/src/inventory/artifact.rs | 160 +++++++++++++++++ libherokubuildpack/src/inventory/checksum.rs | 158 +++++++++++++++++ libherokubuildpack/src/inventory/inventory.rs | 167 ++++++++++++++++++ libherokubuildpack/src/inventory/lib.rs | 17 ++ libherokubuildpack/src/inventory/semver.rs | 7 + libherokubuildpack/src/inventory/sha2.rs | 23 +++ libherokubuildpack/src/inventory/unit.rs | 11 ++ libherokubuildpack/src/inventory/version.rs | 21 +++ 8 files changed, 564 insertions(+) create mode 100644 libherokubuildpack/src/inventory/artifact.rs create mode 100644 libherokubuildpack/src/inventory/checksum.rs create mode 100644 libherokubuildpack/src/inventory/inventory.rs create mode 100644 libherokubuildpack/src/inventory/lib.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/libherokubuildpack/src/inventory/artifact.rs b/libherokubuildpack/src/inventory/artifact.rs new file mode 100644 index 00000000..64d41061 --- /dev/null +++ b/libherokubuildpack/src/inventory/artifact.rs @@ -0,0 +1,160 @@ +use crate::checksum::{Checksum, Digest}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::FromStr; + +#[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::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..83893b25 --- /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: Default::default(), + } + } + } + + 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/inventory.rs b/libherokubuildpack/src/inventory/inventory.rs new file mode 100644 index 00000000..34a0fb1c --- /dev/null +++ b/libherokubuildpack/src/inventory/inventory.rs @@ -0,0 +1,167 @@ +use crate::artifact::{Arch, Artifact, Os}; +use crate::checksum::Digest; +use crate::version::ArtifactRequirement; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::cmp; +use std::cmp::Ordering; +use std::fmt::Formatter; +use std::str::FromStr; + +/// Represents an inventory of artifacts. +#[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 { + pub fn new() -> Self { + Self::default() + } + + pub fn push(&mut self, artifact: Artifact) { + self.artifacts.push(artifact); + } + + 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) + } + + 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, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&toml::to_string(self).unwrap()) + } +} + +#[cfg(test)] +mod test { + use crate::artifact::{Arch, Artifact, Os}; + use crate::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/lib.rs b/libherokubuildpack/src/inventory/lib.rs new file mode 100644 index 00000000..5d164463 --- /dev/null +++ b/libherokubuildpack/src/inventory/lib.rs @@ -0,0 +1,17 @@ +pub mod artifact; +pub mod checksum; +pub mod inventory; +pub mod version; + +#[cfg(feature = "semver")] +mod semver; +#[cfg(feature = "sha2")] +mod sha2; +mod unit; + +#[allow(unused_imports)] +#[cfg(feature = "semver")] +pub use semver::*; +#[allow(unused_imports)] +#[cfg(feature = "sha2")] +pub use sha2::*; diff --git a/libherokubuildpack/src/inventory/semver.rs b/libherokubuildpack/src/inventory/semver.rs new file mode 100644 index 00000000..c5921b0e --- /dev/null +++ b/libherokubuildpack/src/inventory/semver.rs @@ -0,0 +1,7 @@ +use crate::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..774e61d7 --- /dev/null +++ b/libherokubuildpack/src/inventory/sha2.rs @@ -0,0 +1,23 @@ +use crate::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..a6f94a62 --- /dev/null +++ b/libherokubuildpack/src/inventory/unit.rs @@ -0,0 +1,11 @@ +use crate::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..42bda75a --- /dev/null +++ b/libherokubuildpack/src/inventory/version.rs @@ -0,0 +1,21 @@ +pub trait ArtifactRequirement { + fn satisfies_metadata(&self, metadata: &M) -> bool; + fn satisfies_version(&self, version: &V) -> bool; +} + +pub trait VersionRequirement { + 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) + } +} From b6aa3e8b21db2586d1d73032cfc29b18facabeff Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 01:34:29 -0400 Subject: [PATCH 02/19] Merge lib.rs and inventory.rs --- .../src/{inventory => }/inventory.rs | 21 +++++++++++++++++-- libherokubuildpack/src/inventory/lib.rs | 17 --------------- 2 files changed, 19 insertions(+), 19 deletions(-) rename libherokubuildpack/src/{inventory => }/inventory.rs (92%) delete mode 100644 libherokubuildpack/src/inventory/lib.rs diff --git a/libherokubuildpack/src/inventory/inventory.rs b/libherokubuildpack/src/inventory.rs similarity index 92% rename from libherokubuildpack/src/inventory/inventory.rs rename to libherokubuildpack/src/inventory.rs index 34a0fb1c..9e1e135e 100644 --- a/libherokubuildpack/src/inventory/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -1,3 +1,20 @@ +pub mod artifact; +pub mod checksum; +pub mod version; + +#[cfg(feature = "semver")] +mod semver; +#[cfg(feature = "sha2")] +mod sha2; +mod unit; + +#[allow(unused_imports)] +#[cfg(feature = "semver")] +pub use semver::*; +#[allow(unused_imports)] +#[cfg(feature = "sha2")] +pub use sha2::*; + use crate::artifact::{Arch, Artifact, Os}; use crate::checksum::Digest; use crate::version::ArtifactRequirement; @@ -116,8 +133,8 @@ where #[cfg(test)] mod test { - use crate::artifact::{Arch, Artifact, Os}; - use crate::checksum::tests::BogusDigest; + use crate::inventory::artifact::{Arch, Artifact, Os}; + use crate::inventory::checksum::tests::BogusDigest; use crate::inventory::Inventory; #[test] diff --git a/libherokubuildpack/src/inventory/lib.rs b/libherokubuildpack/src/inventory/lib.rs deleted file mode 100644 index 5d164463..00000000 --- a/libherokubuildpack/src/inventory/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod artifact; -pub mod checksum; -pub mod inventory; -pub mod version; - -#[cfg(feature = "semver")] -mod semver; -#[cfg(feature = "sha2")] -mod sha2; -mod unit; - -#[allow(unused_imports)] -#[cfg(feature = "semver")] -pub use semver::*; -#[allow(unused_imports)] -#[cfg(feature = "sha2")] -pub use sha2::*; From e345f99912b3764fef2fa95bdaf8e9ebed629da6 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 01:41:26 -0400 Subject: [PATCH 03/19] Declare inventory module and feature --- libherokubuildpack/Cargo.toml | 6 +++++- libherokubuildpack/src/inventory.rs | 6 +++--- libherokubuildpack/src/inventory/artifact.rs | 4 ++-- libherokubuildpack/src/inventory/unit.rs | 2 +- libherokubuildpack/src/lib.rs | 2 ++ 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index abdea519..7f9d2673 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -18,10 +18,11 @@ 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", "tar", "toml", "fs", "write", "buildpack_output"] download = ["dep:ureq", "dep:thiserror"] digest = ["dep:sha2"] error = ["log", "dep:libcnb"] +inventory = ["dep:hex", "dep:serde"] log = ["dep:termcolor"] tar = ["dep:tar", "dep:flate2"] toml = ["dep:toml"] @@ -38,8 +39,10 @@ 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", optional = true } libcnb = { workspace = true, optional = true } pathdiff = { version = "0.2.1", optional = true } +serde = { version = "1", 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 +53,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" tempfile = "3.12.0" diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index 9e1e135e..6683bf68 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -15,9 +15,9 @@ pub use semver::*; #[cfg(feature = "sha2")] pub use sha2::*; -use crate::artifact::{Arch, Artifact, Os}; -use crate::checksum::Digest; -use crate::version::ArtifactRequirement; +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; diff --git a/libherokubuildpack/src/inventory/artifact.rs b/libherokubuildpack/src/inventory/artifact.rs index 64d41061..2b99cbd4 100644 --- a/libherokubuildpack/src/inventory/artifact.rs +++ b/libherokubuildpack/src/inventory/artifact.rs @@ -1,4 +1,4 @@ -use crate::checksum::{Checksum, Digest}; +use crate::inventory::checksum::{Checksum, Digest}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -106,7 +106,7 @@ impl FromStr for Arch { #[cfg(test)] mod tests { use super::*; - use crate::version::VersionRequirement; + use crate::inventory::version::VersionRequirement; #[test] fn test_arch_display_format() { diff --git a/libherokubuildpack/src/inventory/unit.rs b/libherokubuildpack/src/inventory/unit.rs index a6f94a62..d04422cc 100644 --- a/libherokubuildpack/src/inventory/unit.rs +++ b/libherokubuildpack/src/inventory/unit.rs @@ -1,4 +1,4 @@ -use crate::checksum::Digest; +use crate::inventory::checksum::Digest; impl Digest for () { fn name_compatible(_: &str) -> bool { 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")] From 988bbe62c3dc6c58062212eadf2f9324f6e00d33 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 01:46:18 -0400 Subject: [PATCH 04/19] Prefer PhantomData --- libherokubuildpack/src/inventory/checksum.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libherokubuildpack/src/inventory/checksum.rs b/libherokubuildpack/src/inventory/checksum.rs index 83893b25..f2eb6daf 100644 --- a/libherokubuildpack/src/inventory/checksum.rs +++ b/libherokubuildpack/src/inventory/checksum.rs @@ -101,7 +101,7 @@ pub(crate) mod tests { Checksum { name: String::from("bogus"), value: hex::decode(hex_string).unwrap(), - digest: Default::default(), + digest: PhantomData, } } } From 27dc5a286d5afc55dd8ef340978a46190672c230 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 01:46:32 -0400 Subject: [PATCH 05/19] Add must_use attribute --- libherokubuildpack/src/inventory.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index 6683bf68..10d9c47f 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -39,6 +39,7 @@ impl Default for Inventory { } impl Inventory { + #[must_use] pub fn new() -> Self { Self::default() } From eb8fa87b8c3e1ad563119d65a883e8cc42a1176a Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 01:47:07 -0400 Subject: [PATCH 06/19] Remove unused import --- libherokubuildpack/src/inventory.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index 10d9c47f..5f7d7d7a 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -20,7 +20,6 @@ use crate::inventory::checksum::Digest; use crate::inventory::version::ArtifactRequirement; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use std::cmp; use std::cmp::Ordering; use std::fmt::Formatter; use std::str::FromStr; From a2c28f9853614544e137da8c03bbc438deb8c54c Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 01:49:47 -0400 Subject: [PATCH 07/19] Allow unwrap for now --- libherokubuildpack/src/inventory.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index 5f7d7d7a..d992fe06 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -126,6 +126,7 @@ where 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()) } From 7cb5e0f63685921d478085696a8253cc4b03f050 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 02:16:39 -0400 Subject: [PATCH 08/19] 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. --- libherokubuildpack/Cargo.toml | 3 ++- libherokubuildpack/src/inventory/sha2.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index 7f9d2673..0d156d3f 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -18,12 +18,13 @@ all-features = true workspace = true [features] -default = ["command", "download", "digest", "error", "inventory", "log", "tar", "toml", "fs", "write", "buildpack_output"] +default = ["command", "download", "digest", "error", "inventory", "log", "sha2", "tar", "toml", "fs", "write", "buildpack_output"] download = ["dep:ureq", "dep:thiserror"] digest = ["dep:sha2"] error = ["log", "dep:libcnb"] inventory = ["dep:hex", "dep:serde"] log = ["dep:termcolor"] +sha2 = ["dep:sha2"] tar = ["dep:tar", "dep:flate2"] toml = ["dep:toml"] fs = ["dep:pathdiff"] diff --git a/libherokubuildpack/src/inventory/sha2.rs b/libherokubuildpack/src/inventory/sha2.rs index 774e61d7..d6dbbccb 100644 --- a/libherokubuildpack/src/inventory/sha2.rs +++ b/libherokubuildpack/src/inventory/sha2.rs @@ -1,4 +1,4 @@ -use crate::checksum::Digest; +use crate::inventory::checksum::Digest; use sha2::digest::crypto_common::OutputSizeUser; use sha2::{Sha256, Sha512}; From c590c0a19742b4f705e026bea568515cc27a60d3 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 02:25:32 -0400 Subject: [PATCH 09/19] Add semver feature --- libherokubuildpack/Cargo.toml | 4 +++- libherokubuildpack/src/inventory/semver.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index 0d156d3f..96c50c2a 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -18,12 +18,13 @@ all-features = true workspace = true [features] -default = ["command", "download", "digest", "error", "inventory", "log", "sha2", "tar", "toml", "fs", "write", "buildpack_output"] +default = ["command", "download", "digest", "error", "inventory", "log", "semver", "sha2", "tar", "toml", "fs", "write", "buildpack_output"] download = ["dep:ureq", "dep:thiserror"] digest = ["dep:sha2"] error = ["log", "dep:libcnb"] inventory = ["dep:hex", "dep:serde"] log = ["dep:termcolor"] +semver = ["dep:semver"] sha2 = ["dep:sha2"] tar = ["dep:tar", "dep:flate2"] toml = ["dep:toml"] @@ -43,6 +44,7 @@ flate2 = { version = "1.0.33", default-features = false, features = ["zlib"], op hex = { version = "0.4", optional = true } libcnb = { workspace = true, optional = true } pathdiff = { version = "0.2.1", optional = true } +semver = { version = "1", features = ["serde"], optional = true } serde = { version = "1", features = ["derive"], optional = true } sha2 = { version = "0.10.8", optional = true } tar = { version = "0.4.41", default-features = false, optional = true } diff --git a/libherokubuildpack/src/inventory/semver.rs b/libherokubuildpack/src/inventory/semver.rs index c5921b0e..0b2be6e5 100644 --- a/libherokubuildpack/src/inventory/semver.rs +++ b/libherokubuildpack/src/inventory/semver.rs @@ -1,4 +1,4 @@ -use crate::version::VersionRequirement; +use crate::inventory::version::VersionRequirement; impl VersionRequirement for semver::VersionReq { fn satisfies(&self, version: &semver::Version) -> bool { From 45d6cb4107f69ac5c5de71960752235e26f52fa8 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 02:31:30 -0400 Subject: [PATCH 10/19] Include patch version in dependency requirements --- libherokubuildpack/Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index 96c50c2a..f360fe0e 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -41,11 +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", optional = true } +hex = { version = "0.4.3", optional = true } libcnb = { workspace = true, optional = true } pathdiff = { version = "0.2.1", optional = true } -semver = { version = "1", features = ["serde"], optional = true } -serde = { version = "1", features = ["derive"], 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 } @@ -56,5 +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" +serde_test = "1.0.177" tempfile = "3.12.0" From 77806dc86f710811a93c772519d5f58d7be38293 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 03:19:46 -0400 Subject: [PATCH 11/19] Add inventory toml dependency --- libherokubuildpack/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index f360fe0e..cfb26747 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -22,7 +22,7 @@ default = ["command", "download", "digest", "error", "inventory", "log", "semver download = ["dep:ureq", "dep:thiserror"] digest = ["dep:sha2"] error = ["log", "dep:libcnb"] -inventory = ["dep:hex", "dep:serde"] +inventory = ["dep:hex", "dep:serde", "dep:toml"] log = ["dep:termcolor"] semver = ["dep:semver"] sha2 = ["dep:sha2"] From 50ace6fce6a43445ab09182872eebe8e1c6b502d Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 03:25:03 -0400 Subject: [PATCH 12/19] Add inventory thiserror dependency --- libherokubuildpack/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index cfb26747..29b2684a 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -22,7 +22,7 @@ default = ["command", "download", "digest", "error", "inventory", "log", "semver download = ["dep:ureq", "dep:thiserror"] digest = ["dep:sha2"] error = ["log", "dep:libcnb"] -inventory = ["dep:hex", "dep:serde", "dep:toml"] +inventory = ["dep:hex", "dep:serde", "dep:thiserror", "dep:toml"] log = ["dep:termcolor"] semver = ["dep:semver"] sha2 = ["dep:sha2"] From 9deec38a24f55be2484eb4af83b8bcbe41358d14 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 03:33:42 -0400 Subject: [PATCH 13/19] Add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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 From 99d7d3a172da21a83448c3bb49d71eba8a5e30bc Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 03:46:14 -0400 Subject: [PATCH 14/19] Allow unreachable pub for re-exports Also see https://github.com/Malax/inventory/pull/2#issuecomment-2109873726 --- libherokubuildpack/src/inventory.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index d992fe06..e23cd620 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -8,10 +8,10 @@ mod semver; mod sha2; mod unit; -#[allow(unused_imports)] +#[allow(unused_imports, unreachable_pub)] #[cfg(feature = "semver")] pub use semver::*; -#[allow(unused_imports)] +#[allow(unused_imports, unreachable_pub)] #[cfg(feature = "sha2")] pub use sha2::*; From 369a70f8a1e801e365863dffabe48289e53ff93c Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Tue, 24 Sep 2024 10:31:38 -0400 Subject: [PATCH 15/19] 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. --- libherokubuildpack/src/inventory.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index e23cd620..723b3d7d 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -8,13 +8,6 @@ mod semver; mod sha2; mod unit; -#[allow(unused_imports, unreachable_pub)] -#[cfg(feature = "semver")] -pub use semver::*; -#[allow(unused_imports, unreachable_pub)] -#[cfg(feature = "sha2")] -pub use sha2::*; - use crate::inventory::artifact::{Arch, Artifact, Os}; use crate::inventory::checksum::Digest; use crate::inventory::version::ArtifactRequirement; From 2906e0bf27885a7fc8a2036c0343848a692a2c4c Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Wed, 2 Oct 2024 14:24:07 -0400 Subject: [PATCH 16/19] Rename `semver` feature to `inventory-semver` --- libherokubuildpack/Cargo.toml | 4 ++-- libherokubuildpack/src/inventory.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index 29b2684a..bec17721 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -18,13 +18,13 @@ all-features = true workspace = true [features] -default = ["command", "download", "digest", "error", "inventory", "log", "semver", "sha2", "tar", "toml", "fs", "write", "buildpack_output"] +default = ["command", "download", "digest", "error", "inventory", "log", "inventory-semver", "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"] log = ["dep:termcolor"] -semver = ["dep:semver"] +inventory-semver = ["dep:semver"] sha2 = ["dep:sha2"] tar = ["dep:tar", "dep:flate2"] toml = ["dep:toml"] diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index 723b3d7d..aa067743 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -2,7 +2,7 @@ pub mod artifact; pub mod checksum; pub mod version; -#[cfg(feature = "semver")] +#[cfg(feature = "inventory-semver")] mod semver; #[cfg(feature = "sha2")] mod sha2; From 453861c66c715dd3cf5517b298c8ac75e81dcfc3 Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Wed, 2 Oct 2024 14:25:20 -0400 Subject: [PATCH 17/19] Rename `sha2` feature to `inventory-sha2` --- libherokubuildpack/Cargo.toml | 4 ++-- libherokubuildpack/src/inventory.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index bec17721..7a11b413 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -18,14 +18,14 @@ all-features = true workspace = true [features] -default = ["command", "download", "digest", "error", "inventory", "log", "inventory-semver", "sha2", "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"] log = ["dep:termcolor"] inventory-semver = ["dep:semver"] -sha2 = ["dep:sha2"] +inventory-sha2 = ["dep:sha2"] tar = ["dep:tar", "dep:flate2"] toml = ["dep:toml"] fs = ["dep:pathdiff"] diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index aa067743..c31f2435 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -4,7 +4,7 @@ pub mod version; #[cfg(feature = "inventory-semver")] mod semver; -#[cfg(feature = "sha2")] +#[cfg(feature = "inventory-sha2")] mod sha2; mod unit; From 1ac0955988801fc43563ab183a3f0be7b965ab15 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Thu, 3 Oct 2024 16:43:16 -0500 Subject: [PATCH 18/19] [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 --- libherokubuildpack/src/inventory.rs | 106 +++++++++++++++++++ libherokubuildpack/src/inventory/artifact.rs | 9 ++ libherokubuildpack/src/inventory/version.rs | 8 ++ 3 files changed, 123 insertions(+) diff --git a/libherokubuildpack/src/inventory.rs b/libherokubuildpack/src/inventory.rs index c31f2435..5faa1ba3 100644 --- a/libherokubuildpack/src/inventory.rs +++ b/libherokubuildpack/src/inventory.rs @@ -1,3 +1,94 @@ +//! # 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; @@ -18,6 +109,12 @@ 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")] @@ -31,15 +128,20 @@ impl Default for Inventory { } 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, @@ -56,6 +158,10 @@ impl Inventory { .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, diff --git a/libherokubuildpack/src/inventory/artifact.rs b/libherokubuildpack/src/inventory/artifact.rs index 2b99cbd4..e13ae023 100644 --- a/libherokubuildpack/src/inventory/artifact.rs +++ b/libherokubuildpack/src/inventory/artifact.rs @@ -4,6 +4,15 @@ 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")] diff --git a/libherokubuildpack/src/inventory/version.rs b/libherokubuildpack/src/inventory/version.rs index 42bda75a..775ac974 100644 --- a/libherokubuildpack/src/inventory/version.rs +++ b/libherokubuildpack/src/inventory/version.rs @@ -1,9 +1,17 @@ +/// 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; } From 8f66c88a387078bc4ba13021df8bcfdc2967387d Mon Sep 17 00:00:00 2001 From: Rune Soerensen Date: Thu, 3 Oct 2024 17:48:49 -0400 Subject: [PATCH 19/19] Group inventory features alphabetically --- libherokubuildpack/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index 7a11b413..965c339a 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -23,9 +23,9 @@ download = ["dep:ureq", "dep:thiserror"] digest = ["dep:sha2"] error = ["log", "dep:libcnb"] inventory = ["dep:hex", "dep:serde", "dep:thiserror", "dep:toml"] -log = ["dep:termcolor"] inventory-semver = ["dep:semver"] inventory-sha2 = ["dep:sha2"] +log = ["dep:termcolor"] tar = ["dep:tar", "dep:flate2"] toml = ["dep:toml"] fs = ["dep:pathdiff"]