1use std::fmt;
2
3use crate::{PolicyFingerprint, SubjectKey};
4
5#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct CounterKey {
12 fingerprint: PolicyFingerprint,
13 subject: SubjectKey,
14}
15
16impl CounterKey {
17 pub const fn new(fingerprint: PolicyFingerprint, subject: SubjectKey) -> Self {
19 Self {
20 fingerprint,
21 subject,
22 }
23 }
24
25 pub const fn fingerprint(self) -> PolicyFingerprint {
27 self.fingerprint
28 }
29
30 pub const fn subject(self) -> SubjectKey {
32 self.subject
33 }
34
35 pub fn to_bytes(self) -> [u8; 64] {
40 let mut bytes = [0_u8; 64];
41 bytes[..32].copy_from_slice(self.fingerprint.as_bytes());
42 bytes[32..].copy_from_slice(self.subject.as_bytes());
43 bytes
44 }
45}
46
47impl fmt::Debug for CounterKey {
48 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49 formatter
50 .debug_struct("CounterKey")
51 .field("fingerprint", &self.fingerprint)
52 .field("subject", &self.subject)
53 .finish()
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use std::time::Duration;
60
61 use crate::{Check, FixedWindowPolicy, PolicyId, ScopeId, SubjectKey};
62
63 #[test]
64 fn fixed_width_encoding_is_fingerprint_then_subject() {
65 let policy = FixedWindowPolicy::new(
66 PolicyId::new("auth.login").unwrap(),
67 ScopeId::new("client").unwrap(),
68 8,
69 Duration::from_secs(60),
70 )
71 .unwrap();
72 let subject = SubjectKey::from_digest([0x5a; 32]);
73 let key = Check::new(&policy, subject).counter_key();
74 let bytes = key.to_bytes();
75
76 assert_eq!(&bytes[..32], policy.fingerprint().as_bytes());
77 assert_eq!(&bytes[32..], subject.as_bytes());
78 assert!(format!("{key:?}").contains("[REDACTED]"));
79 assert!(!format!("{key:?}").contains("5a5a"));
80 }
81}