1use std::fmt;
4
5use zeroize::Zeroize;
6
7#[derive(Clone, PartialEq, Eq)]
17pub struct SmallKey {
18 bytes: [u8; 32],
19 len: u8,
20}
21
22impl SmallKey {
23 pub const MAX_LEN: usize = 32;
25
26 #[must_use]
29 pub(crate) fn from_prefix(source: &[u8], len: usize) -> Self {
30 let len = len.min(Self::MAX_LEN);
31 let mut bytes = [0u8; Self::MAX_LEN];
32 let copied = len.min(source.len());
33 if let (Some(head), Some(from)) = (bytes.get_mut(..copied), source.get(..copied)) {
34 head.copy_from_slice(from);
35 }
36 #[expect(
37 clippy::cast_possible_truncation,
38 reason = "len <= 32 by the line above"
39 )]
40 Self {
41 bytes,
42 len: len as u8,
43 }
44 }
45
46 #[must_use]
48 pub(crate) const fn from_full(bytes: [u8; 32]) -> Self {
49 #[expect(clippy::cast_possible_truncation, reason = "32 fits in a u8")]
50 Self {
51 bytes,
52 len: Self::MAX_LEN as u8,
53 }
54 }
55
56 #[must_use]
58 pub fn bytes(&self) -> &[u8] {
59 self.bytes.get(..self.len()).unwrap_or_default()
60 }
61
62 #[must_use]
64 pub fn len(&self) -> usize {
65 usize::from(self.len)
66 }
67
68 #[must_use]
70 pub fn is_empty(&self) -> bool {
71 self.len == 0
72 }
73}
74
75impl Drop for SmallKey {
76 fn drop(&mut self) {
77 self.bytes.zeroize();
78 self.len = 0;
79 }
80}
81
82impl fmt::Debug for SmallKey {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 write!(f, "SmallKey(<{} bytes redacted>)", self.len())
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::SmallKey;
91
92 #[test]
93 fn a_prefix_shorter_than_requested_is_zero_filled() {
94 let key = SmallKey::from_prefix(b"abc", 5);
95 assert_eq!(key.bytes(), b"abc\0\0");
96 assert_eq!(key.len(), 5);
97 }
98
99 #[test]
100 fn a_key_never_exceeds_thirty_two_bytes() {
101 let key = SmallKey::from_prefix(&[0xFFu8; 64], 64);
102 assert_eq!(key.len(), SmallKey::MAX_LEN);
103 assert_eq!(key.bytes().len(), SmallKey::MAX_LEN);
104 }
105
106 #[test]
107 fn debug_redacts_the_material() {
108 let key = SmallKey::from_full([0xAB; 32]);
109 let dump = format!("{key:?}");
110 assert_eq!(dump, "SmallKey(<32 bytes redacted>)");
111 assert!(!dump.contains("ab"), "{dump}");
112 assert!(!dump.contains("171"), "{dump}");
113 }
114
115 #[test]
116 fn an_empty_key_is_representable_and_says_so() {
117 let key = SmallKey::from_prefix(b"", 0);
118 assert!(key.is_empty());
119 assert_eq!(key.bytes(), b"");
120 }
121}