1use core::sync::atomic::Ordering;
6
7#[cfg(not(target_has_atomic = "64"))]
8use core::sync::atomic::AtomicU32 as Counter;
9#[cfg(target_has_atomic = "64")]
10use core::sync::atomic::AtomicU64 as Counter;
11
12use crate::error::{FormError, SLOT_CODES, SLOTS};
13
14#[derive(Debug)]
43pub struct RefusalDigest<const N: usize = 0> {
44 form: [Counter; SLOTS],
45 codes: [&'static str; N],
46 named: [Counter; N],
47 unnamed: Counter,
48}
49
50impl RefusalDigest<0> {
51 #[must_use]
54 pub const fn new() -> RefusalDigest<0> {
55 RefusalDigest::with_codes([])
56 }
57}
58
59impl Default for RefusalDigest<0> {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl<const N: usize> RefusalDigest<N> {
66 #[must_use]
69 pub const fn with_codes(codes: [&'static str; N]) -> RefusalDigest<N> {
70 RefusalDigest {
71 form: [const { Counter::new(0) }; SLOTS],
72 codes,
73 named: [const { Counter::new(0) }; N],
74 unnamed: Counter::new(0),
75 }
76 }
77
78 #[inline]
83 pub fn record(&self, error: &FormError) {
84 let counter = match error.slot() {
85 Some(slot) => self.form.get(slot),
86 None => {
87 let code = error.code();
88 self.codes
89 .iter()
90 .position(|c| *c == code)
91 .and_then(|i| self.named.get(i))
92 }
93 };
94 counter
95 .unwrap_or(&self.unnamed)
96 .fetch_add(1, Ordering::Relaxed);
97 }
98
99 #[must_use]
103 pub fn count(&self, code: &str) -> u64 {
104 self.iter_all()
105 .filter(|(c, _)| *c == code)
106 .fold(0u64, |sum, (_, n)| sum.wrapping_add(n))
107 }
108
109 #[must_use]
111 pub fn unnamed(&self) -> u64 {
112 load(&self.unnamed)
113 }
114
115 #[must_use]
117 pub fn total(&self) -> u64 {
118 self.iter_all()
119 .fold(self.unnamed(), |sum, (_, n)| sum.wrapping_add(n))
120 }
121
122 pub fn iter(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
125 self.iter_all().filter(|(_, n)| *n > 0)
126 }
127
128 fn iter_all(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
129 let form = SLOT_CODES.iter().copied().zip(self.form.iter().map(load));
130 let named = self.codes.iter().copied().zip(self.named.iter().map(load));
131 form.chain(named)
132 }
133}
134
135#[inline]
137fn load(c: &Counter) -> u64 {
138 #[cfg(target_has_atomic = "64")]
139 let n = c.load(Ordering::Relaxed);
140 #[cfg(not(target_has_atomic = "64"))]
141 let n = u64::from(c.load(Ordering::Relaxed));
142 n
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::error::Location;
149 use crate::hook::Refusal;
150
151 fn refused(code: &'static str) -> FormError {
152 FormError::Refused {
153 refusal: Refusal::new(422, code),
154 at: Location::NONE,
155 }
156 }
157
158 #[test]
159 fn test_every_form_net_code_counts_in_its_own_counter() {
160 let digest = RefusalDigest::new();
161 let all = crate::error::tests::every_error();
162 for (i, e) in all.iter().enumerate() {
163 for _ in 0..=i {
164 digest.record(e);
165 }
166 }
167 for (i, e) in all.iter().enumerate() {
168 assert_eq!(digest.count(e.code()), i as u64 + 1, "{e:?}");
169 }
170 assert_eq!(digest.iter().count(), all.len());
171 assert_eq!(digest.unnamed(), 0);
172 let n = all.len() as u64;
173 assert_eq!(digest.total(), n * (n + 1) / 2);
174 }
175
176 #[test]
177 fn test_inspector_codes_count_by_name_and_the_rest_together() {
178 let digest = RefusalDigest::with_codes(["customs.a", "customs.b"]);
179 digest.record(&refused("customs.b"));
180 digest.record(&refused("customs.b"));
181 digest.record(&refused("customs.a"));
182 digest.record(&refused("customs.other"));
183 digest.record(&refused("csrf.rejected"));
184 assert_eq!(digest.count("customs.a"), 1);
185 assert_eq!(digest.count("customs.b"), 2);
186 assert_eq!(digest.count("customs.other"), 0);
187 assert_eq!(digest.unnamed(), 2);
188 assert_eq!(digest.total(), 5);
189 let seen: alloc::vec::Vec<_> = digest.iter().collect();
190 assert_eq!(seen, [("customs.a", 1), ("customs.b", 2)]);
191 }
192
193 #[test]
194 fn test_a_code_named_twice_or_shadowing_form_net_sums() {
195 let digest = RefusalDigest::with_codes(["x", "x", "limit.parts"]);
196 digest.record(&refused("x"));
197 digest.record(&refused("limit.parts"));
198 digest.record(&FormError::limit(crate::Limit::Parts, Location::NONE));
199 assert_eq!(digest.count("x"), 1);
200 assert_eq!(digest.count("limit.parts"), 2);
201 assert_eq!(digest.total(), 3);
202 }
203
204 #[test]
205 fn test_counts_from_many_threads_are_all_kept() {
206 static DIGEST: RefusalDigest = RefusalDigest::new();
207 let e = FormError::UnsupportedMediaType;
208 std::thread::scope(|s| {
209 for _ in 0..8 {
210 s.spawn(|| {
211 for _ in 0..10_000 {
212 DIGEST.record(&e);
213 }
214 });
215 }
216 });
217 assert_eq!(DIGEST.count("unsupported_media_type"), 80_000);
218 }
219}