1use std::collections::BTreeSet;
17
18use serde::{Deserialize, Serialize};
19
20use crate::event::{RunId, Seq};
21
22pub const IDEM_DERIVE_V: u8 = 1;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct IdemKey {
29 v: u8,
30 digest: [u8; 16],
31}
32
33impl IdemKey {
34 pub fn derive(run: &RunId, seq: Seq, action_canonical: &[u8]) -> IdemKey {
38 let mut hasher = blake3::Hasher::new();
39 hasher.update(run.as_bytes());
40 hasher.update(&seq.value().to_le_bytes());
41 hasher.update(action_canonical);
42 let mut digest = [0u8; 16];
43 hasher.finalize_xof().fill(&mut digest);
44 IdemKey {
45 v: IDEM_DERIVE_V,
46 digest,
47 }
48 }
49
50 fn parse(raw: &str) -> Option<IdemKey> {
51 let rest = raw.strip_prefix("idem")?;
52 let (v_raw, hex) = rest.split_once('-')?;
53 if v_raw != "1" {
54 return None;
55 }
56 let digest = crate::locator::decode_hex_fixed::<16>(hex)?;
57 Some(IdemKey {
58 v: IDEM_DERIVE_V,
59 digest,
60 })
61 }
62}
63
64#[derive(Debug, PartialEq, Eq)]
74pub struct IdemGuard {
75 key: IdemKey,
76}
77
78impl IdemGuard {
79 #[must_use]
82 pub fn key(&self) -> &IdemKey {
83 &self.key
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct Duplicate {
91 key: IdemKey,
92}
93
94impl Duplicate {
95 #[must_use]
96 pub fn key(&self) -> &IdemKey {
97 &self.key
98 }
99}
100
101pub fn claim(seen: &mut BTreeSet<IdemKey>, key: IdemKey) -> Result<IdemGuard, Duplicate> {
112 if seen.contains(&key) {
113 return Err(Duplicate { key });
114 }
115 seen.insert(key);
116 Ok(IdemGuard { key })
117}
118
119impl std::fmt::Display for IdemKey {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(f, "idem{}-", self.v)?;
122 crate::locator::write_hex(f, &self.digest)
123 }
124}
125
126impl Serialize for IdemKey {
127 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
128 serializer.collect_str(self)
129 }
130}
131
132impl<'de> Deserialize<'de> for IdemKey {
133 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
134 let raw = String::deserialize(deserializer)?;
135 IdemKey::parse(&raw).ok_or_else(|| {
136 serde::de::Error::custom("expected `idem1-` plus 32 lowercase hex digits")
137 })
138 }
139}
140
141#[cfg(test)]
142#[allow(
143 clippy::unwrap_used,
144 clippy::expect_used,
145 clippy::panic,
146 clippy::indexing_slicing,
147 reason = "test code"
148)]
149mod tests {
150 use super::*;
151 use crate::event::{RunId, Seq};
152
153 fn run() -> RunId {
154 RunId::from_bytes([7; 16])
155 }
156
157 #[test]
158 fn same_inputs_same_key_always() {
159 let a = IdemKey::derive(&run(), Seq::new(42), b"tool:exec{cmd}");
160 let b = IdemKey::derive(&run(), Seq::new(42), b"tool:exec{cmd}");
161 assert_eq!(a, b);
162 }
163
164 #[test]
165 fn any_input_change_changes_the_key() {
166 let base = IdemKey::derive(&run(), Seq::new(42), b"action");
167 assert_ne!(base, IdemKey::derive(&run(), Seq::new(43), b"action"));
168 assert_ne!(
169 base,
170 IdemKey::derive(&RunId::from_bytes([8; 16]), Seq::new(42), b"action")
171 );
172 assert_ne!(base, IdemKey::derive(&run(), Seq::new(42), b"actioN"));
173 }
174
175 #[test]
176 fn display_carries_the_derivation_version() {
177 let key = IdemKey::derive(&run(), Seq::FIRST, b"x");
178 let text = key.to_string();
179 assert!(text.starts_with("idem1-"), "{text}");
180 assert_eq!(text.len(), "idem1-".len() + 32, "16 bytes as 32 hex digits");
181 }
182
183 #[test]
184 fn serde_roundtrips_the_string_form() {
185 let key = IdemKey::derive(&run(), Seq::new(5), b"payload");
186 let json = serde_json::to_string(&key).unwrap();
187 let back: IdemKey = serde_json::from_str(&json).unwrap();
188 assert_eq!(back, key);
189 assert!(serde_json::from_str::<IdemKey>("\"idem1-zz\"").is_err());
190 assert!(
191 serde_json::from_str::<IdemKey>("\"idem9-00000000000000000000000000000000\"").is_err()
192 );
193 }
194
195 #[test]
196 fn a_key_is_claimable_once_and_the_second_claim_names_it() {
197 let key = IdemKey::derive(&run(), Seq::FIRST, b"send mail");
198 let other = IdemKey::derive(&run(), Seq::FIRST, b"send other mail");
199 let mut seen = BTreeSet::new();
200 let guard = claim(&mut seen, key).expect("the first claim on a fresh key");
201 assert_eq!(guard.key(), &key);
202 let again = claim(&mut seen, key).expect_err("the second claim is a duplicate");
203 assert_eq!(again.key(), &key);
204 assert!(
205 claim(&mut seen, other).is_ok(),
206 "a different action is a different key"
207 );
208 }
209
210 proptest::proptest! {
211 #[test]
212 fn rederivation_is_identity(
213 run_bytes in proptest::array::uniform16(0u8..=255),
214 seq in 0u64..,
215 action in proptest::collection::vec(0u8..=255, 0..64),
216 ) {
217 let r = RunId::from_bytes(run_bytes);
218 let first = IdemKey::derive(&r, Seq::new(seq), &action);
219 let second = IdemKey::derive(&r, Seq::new(seq), &action);
220 proptest::prop_assert_eq!(first, second);
221 }
222 }
223}