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