stuffr_core/source/
limit.rs1use std::io::{Read, Write};
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use super::{SeekRead, Source, SourceCaps};
13use crate::error::{Error, Result};
14
15pub const RATIO_FLOOR: u64 = 1024 * 1024;
21
22pub const DEFAULT_MAX_RATIO: u64 = 10_000;
31
32pub struct Counting {
39 inner: Box<dyn Source>,
40 count: Arc<AtomicU64>,
41}
42
43impl Counting {
44 pub fn new(inner: Box<dyn Source>) -> (Self, Arc<AtomicU64>) {
46 let count = Arc::new(AtomicU64::new(0));
47 (
48 Self {
49 inner,
50 count: Arc::clone(&count),
51 },
52 count,
53 )
54 }
55}
56
57impl Read for Counting {
58 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
59 let n = self.inner.read(buf)?;
60 self.count.fetch_add(n as u64, Ordering::Relaxed);
61 Ok(n)
62 }
63}
64
65impl Source for Counting {
66 fn caps(&self) -> SourceCaps {
67 self.inner.caps()
68 }
69
70 fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
71 self.inner.as_seek()
72 }
73}
74
75pub struct CountingWriter {
82 inner: Box<dyn Write + Send>,
83 count: Arc<AtomicU64>,
84}
85
86impl CountingWriter {
87 pub fn new(inner: Box<dyn Write + Send>) -> (Self, Arc<AtomicU64>) {
89 let count = Arc::new(AtomicU64::new(0));
90 (
91 Self {
92 inner,
93 count: Arc::clone(&count),
94 },
95 count,
96 )
97 }
98}
99
100impl Write for CountingWriter {
101 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
102 let n = self.inner.write(buf)?;
103 self.count.fetch_add(n as u64, Ordering::Relaxed);
104 Ok(n)
105 }
106
107 fn flush(&mut self) -> std::io::Result<()> {
108 self.inner.flush()
109 }
110}
111
112pub struct RatioGuard {
120 input: Arc<AtomicU64>,
121 produced: u64,
122 max_ratio: u64,
123}
124
125impl RatioGuard {
126 pub fn new(input: Arc<AtomicU64>, max_ratio: u64) -> Self {
128 Self {
129 input,
130 produced: 0,
131 max_ratio,
132 }
133 }
134
135 pub fn record(&mut self, n: usize) -> Result<()> {
137 self.produced = self.produced.saturating_add(n as u64);
138 if self.produced <= RATIO_FLOOR {
139 return Ok(());
140 }
141 let consumed = self.input.load(Ordering::Relaxed).max(1);
144 let ratio = self.produced / consumed;
145 if ratio > self.max_ratio {
146 return Err(Error::ResourceLimit(format!(
147 "expansion ratio {ratio}:1 exceeds the {}:1 limit \
148 ({consumed} bytes read, {} produced); raise --max-ratio if this is genuine",
149 self.max_ratio, self.produced
150 )));
151 }
152 Ok(())
153 }
154
155 pub fn produced(&self) -> u64 {
157 self.produced
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::source::ReaderSource;
165
166 fn src(bytes: Vec<u8>) -> Box<dyn Source> {
167 Box::new(ReaderSource::new(std::io::Cursor::new(bytes)))
168 }
169
170 #[test]
171 fn counting_tallies_what_it_serves_and_passes_bytes_through() {
172 let (mut c, count) = Counting::new(src(b"0123456789".to_vec()));
173 let mut out = Vec::new();
174 c.read_to_end(&mut out).unwrap();
175 assert_eq!(out, b"0123456789");
176 assert_eq!(count.load(Ordering::Relaxed), 10);
177 }
178
179 #[test]
180 fn counting_forwards_the_inner_capabilities() {
181 let (c, _) = Counting::new(src(b"x".to_vec()));
183 assert!(!c.caps().seekable);
184 }
185
186 #[test]
187 fn the_guard_ignores_everything_below_the_floor() {
188 let input = Arc::new(AtomicU64::new(1));
192 let mut g = RatioGuard::new(Arc::clone(&input), 10);
193 assert!(g.record(RATIO_FLOOR as usize).is_ok());
195 }
196
197 #[test]
198 fn the_guard_refuses_a_pathological_expansion_past_the_floor() {
199 let input = Arc::new(AtomicU64::new(1));
200 let mut g = RatioGuard::new(Arc::clone(&input), 1000);
201 assert!(g.record(RATIO_FLOOR as usize).is_ok());
202 let err = g.record(1024).unwrap_err();
203 assert!(matches!(err, Error::ResourceLimit(_)));
204 assert_eq!(err.exit_code(), 6, "a bomb must exit 6, not 1");
205 let msg = err.to_string();
206 assert!(
207 msg.contains("1000"),
208 "the message must name the limit: {msg}"
209 );
210 }
211
212 #[test]
213 fn a_legitimate_ratio_passes_indefinitely() {
214 let input = Arc::new(AtomicU64::new(0));
216 let mut g = RatioGuard::new(Arc::clone(&input), 1000);
217 for _ in 0..64 {
218 input.fetch_add(64 * 1024, Ordering::Relaxed);
219 g.record(256 * 1024).unwrap();
220 }
221 }
222
223 #[test]
224 fn a_zero_input_tally_does_not_divide_by_zero() {
225 let input = Arc::new(AtomicU64::new(0));
226 let mut g = RatioGuard::new(input, 1000);
227 assert!(g.record(RATIO_FLOOR as usize + 1).is_err());
229 }
230
231 #[test]
232 fn counting_writer_tallies_every_byte_including_a_short_write() {
233 struct Short;
237 impl std::io::Write for Short {
238 fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
239 Ok(b.len().min(3))
240 }
241 fn flush(&mut self) -> std::io::Result<()> {
242 Ok(())
243 }
244 }
245
246 let (mut w, count) = CountingWriter::new(Box::new(Short));
247 w.write_all(b"0123456789").unwrap();
248 assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 10);
249 }
250}