use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Component {
Config,
Analyzer,
ToolVersion,
Variant,
}
impl Component {
pub const fn tag(self) -> &'static str {
match self {
Self::Config => "config",
Self::Analyzer => "analyzer",
Self::ToolVersion => "tool",
Self::Variant => "variant",
}
}
}
impl fmt::Display for Component {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.tag())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CacheKey {
components: Vec<(Component, String)>,
}
impl CacheKey {
pub const fn new() -> Self {
Self {
components: Vec::new(),
}
}
pub fn from_config_hash(hash: Option<String>) -> Self {
let mut key = Self::new();
if let Some(hash) = hash {
key.push(Component::Config, hash);
}
key
}
pub fn push(&mut self, component: Component, value: impl Into<String>) {
self.components.push((component, value.into()));
}
pub const fn is_empty(&self) -> bool {
self.components.is_empty()
}
pub fn components(&self) -> &[(Component, String)] {
&self.components
}
pub fn digest(&self) -> Option<String> {
if self.components.is_empty() {
return None;
}
let parts: Vec<&str> = self
.components
.iter()
.flat_map(|(c, v)| [c.tag(), v.as_str()])
.collect();
Some(crate::checksum::hash_parts(&parts))
}
pub fn descriptor_key(&self, processor: &str, input_checksum: &str) -> String {
let version = crate::registries::processor_version(processor)
.unwrap_or(0)
.to_string();
let digest = self.digest().unwrap_or_default();
crate::checksum::hash_parts(&[processor, &version, &digest, input_checksum])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_key_has_no_digest() {
assert_eq!(CacheKey::new().digest(), None);
assert!(CacheKey::new().is_empty());
}
#[test]
fn from_config_hash_none_is_empty() {
assert!(CacheKey::from_config_hash(None).is_empty());
assert!(!CacheKey::from_config_hash(Some("x".into())).is_empty());
}
#[test]
fn component_kind_is_part_of_the_digest() {
let mut a = CacheKey::new();
a.push(Component::Config, "abc");
let mut b = CacheKey::new();
b.push(Component::ToolVersion, "abc");
assert_ne!(a.digest(), b.digest());
}
#[test]
fn order_is_significant() {
let mut a = CacheKey::new();
a.push(Component::Config, "one");
a.push(Component::Analyzer, "two");
let mut b = CacheKey::new();
b.push(Component::Analyzer, "two");
b.push(Component::Config, "one");
assert_ne!(a.digest(), b.digest());
}
#[test]
fn digest_is_stable_across_calls() {
let mut key = CacheKey::new();
key.push(Component::Config, "abc");
key.push(Component::ToolVersion, "def");
assert_eq!(key.digest(), key.digest());
}
#[test]
fn adding_a_component_changes_the_digest() {
let mut key = CacheKey::new();
key.push(Component::Config, "abc");
let before = key.digest();
key.push(Component::ToolVersion, "def");
assert_ne!(before, key.digest());
}
#[test]
fn descriptor_key_is_injection_proof() {
assert_ne!(
CacheKey::new().descriptor_key("p:v0:q", "r"),
CacheKey::new().descriptor_key("p", "q:v0:r"),
"instance names must not be able to realign key boundaries"
);
}
#[test]
fn digest_is_injection_proof() {
let mut spliced = CacheKey::new();
spliced.push(Component::Analyzer, "a|analyzer=b");
let mut two = CacheKey::new();
two.push(Component::Analyzer, "a");
two.push(Component::Analyzer, "b");
assert_ne!(
spliced.digest(),
two.digest(),
"analyzer values must not be able to fake component boundaries"
);
}
#[test]
fn descriptor_key_varies_with_every_input() {
let mut key = CacheKey::new();
key.push(Component::Config, "abc");
let base = key.descriptor_key("proc", "chk");
assert_ne!(base, key.descriptor_key("other", "chk"));
assert_ne!(base, key.descriptor_key("proc", "other"));
assert_ne!(base, CacheKey::new().descriptor_key("proc", "chk"));
}
#[test]
fn components_are_reported_in_contribution_order() {
let mut key = CacheKey::new();
key.push(Component::Config, "c");
key.push(Component::Analyzer, "a");
key.push(Component::ToolVersion, "t");
let tags: Vec<_> = key.components().iter().map(|(c, _)| c.tag()).collect();
assert_eq!(tags, vec!["config", "analyzer", "tool"]);
}
}