1use constriction::stream::{
34 model::ContiguousCategoricalEntropyModel, queue::DefaultRangeDecoder,
35 queue::DefaultRangeEncoder, Decode, Encode,
36};
37
38const SCALE: u64 = 1 << 24;
40pub const MIX_FAST_CAP: u64 = 4096;
42pub const MIX_SLOW_CAP: u64 = 65536;
44const EWA_LR: f64 = 0.15;
46const COUNT_INC: u64 = 32;
48pub const MIX5_FAST_CAP: u64 = 32768;
50pub const MIX5_SLOW_CAP: u64 = 1048576;
52pub const MIX5_NCTX: usize = 16;
54
55#[derive(Clone, Copy)]
57pub struct MixParams {
58 pub nctx: usize,
60 pub cap_fast: u64,
62 pub cap_slow: u64,
64}
65
66impl MixParams {
67 pub const BACKEND4: Self = Self {
69 nctx: 8,
70 cap_fast: MIX_FAST_CAP,
71 cap_slow: MIX_SLOW_CAP,
72 };
73 pub const BACKEND5: Self = Self {
75 nctx: MIX5_NCTX,
76 cap_fast: MIX5_FAST_CAP,
77 cap_slow: MIX5_SLOW_CAP,
78 };
79}
80
81#[inline]
84pub fn mix_ctx_bucket(prev: u8) -> u8 {
85 match prev {
86 0 => 0,
87 1..=2 => 1,
88 3..=5 => 2,
89 6..=11 => 3,
90 12..=24 => 4,
91 25..=48 => 5,
92 49..=96 => 6,
93 _ => 7,
94 }
95}
96
97#[derive(Clone)]
99struct MixCtx {
100 fast: Vec<u64>,
101 slow: Vec<u64>,
102 log_w: f64,
103 cap_fast: u64,
104 cap_slow: u64,
105}
106
107impl MixCtx {
108 fn new(cap_fast: u64, cap_slow: u64) -> Self {
109 Self {
110 fast: vec![1; 256],
111 slow: vec![1; 256],
112 log_w: 0.0,
113 cap_fast,
114 cap_slow,
115 }
116 }
117
118 fn probs(&self) -> Vec<u32> {
121 let tf = self.fast.iter().sum::<u64>() as f64;
122 let ts = self.slow.iter().sum::<u64>() as f64;
123 let wf = 1.0 / (1.0 + (-self.log_w).exp());
124 let mut p: Vec<u64> = self
125 .fast
126 .iter()
127 .zip(&self.slow)
128 .map(|(&cf, &cs)| {
129 ((wf * (cf as f64 / tf) + (1.0 - wf) * (cs as f64 / ts)) * SCALE as f64) as u64
130 })
131 .collect();
132 let sum: u64 = p.iter().sum();
135 p[0] += SCALE - sum;
136 let mut dense = 0usize;
137 for (i, &v) in p.iter().enumerate() {
138 if v > p[dense] {
139 dense = i;
140 }
141 }
142 let zeros = p.iter().filter(|&&v| v == 0).count();
143 p[dense] = p[dense].max(1 + zeros as u64);
144 for v in p.iter_mut() {
145 if *v == 0 {
146 *v = 1;
147 }
148 }
149 p.into_iter().map(|v| v as u32).collect()
150 }
151
152 fn model(&self) -> Option<ContiguousCategoricalEntropyModel<u32, Vec<u32>, 24>> {
153 ContiguousCategoricalEntropyModel::<u32, Vec<u32>, 24>::from_nonzero_fixed_point_probabilities(
154 self.probs(),
155 false,
156 )
157 .ok()
158 }
159
160 fn step(&mut self, s: u8) {
161 let si = usize::from(s);
162 let tf = self.fast.iter().sum::<u64>() as f64;
163 let ts = self.slow.iter().sum::<u64>() as f64;
164 let pf = (self.fast[si] as f64 / tf).max(1e-9);
165 let ps = (self.slow[si] as f64 / ts).max(1e-9);
166 self.log_w = (self.log_w + EWA_LR * (pf.ln() - ps.ln())).clamp(-20.0, 20.0);
167 self.fast[si] += COUNT_INC;
168 self.slow[si] += COUNT_INC;
169 if self.fast.iter().sum::<u64>() > self.cap_fast {
170 for c in &mut self.fast {
171 *c = (*c + 1) >> 1;
172 }
173 }
174 if self.slow.iter().sum::<u64>() > self.cap_slow {
175 for c in &mut self.slow {
176 *c = (*c + 1) >> 1;
177 }
178 }
179 }
180}
181
182#[inline]
187fn mix_ctx_key(params: &MixParams, prev: u8, prev2: u8) -> usize {
188 let pb = usize::from(mix_ctx_bucket(prev));
189 if params.nctx == 8 {
190 pb
191 } else {
192 let band = usize::from(mix_ctx_bucket(prev2) >= 3);
193 pb | (band << 3)
194 }
195}
196
197pub fn mix_encode_with(params: &MixParams, data: &[u8]) -> Vec<u8> {
201 let mut ctxs: Vec<MixCtx> = (0..params.nctx)
202 .map(|_| MixCtx::new(params.cap_fast, params.cap_slow))
203 .collect();
204 let mut enc = DefaultRangeEncoder::new();
205 let mut prev = 0u8;
206 let mut prev2 = 0u8;
207 for &b in data {
208 let c = mix_ctx_key(params, prev, prev2);
209 let m = ctxs[c]
210 .model()
211 .expect("mix model with all-nonzero probabilities is always buildable");
212 enc.encode_symbols(std::iter::once((usize::from(b), &m)))
213 .expect("mix encode symbol");
214 ctxs[c].step(b);
215 prev2 = prev;
216 prev = b;
217 }
218 let compressed = enc.into_compressed().expect("mix seal");
219 let mut out = Vec::with_capacity(compressed.len() * 4 + 4);
223 out.extend_from_slice(
224 &u32::try_from(data.len())
225 .expect("stream len fits u32")
226 .to_le_bytes(),
227 );
228 for w in compressed {
229 out.extend_from_slice(&w.to_le_bytes());
230 }
231 out
232}
233
234pub fn mix_encode(data: &[u8]) -> Vec<u8> {
237 mix_encode_with(&MixParams::BACKEND4, data)
238}
239
240pub fn mix_decode_with(
243 params: &MixParams,
244 data: &[u8],
245 limit: usize,
246) -> Result<Vec<u8>, crate::error::Error> {
247 if data.len() < 4 || !(data.len() - 4).is_multiple_of(4) {
249 return Err(crate::error::Error::fmt_msg("mix payload not word-aligned"));
250 }
251 let stream_len = usize::try_from(u32::from_le_bytes(data[..4].try_into().expect("4 bytes")))
252 .map_err(|_| crate::error::Error::fmt_msg("mix stream length"))?;
253 if stream_len > limit {
254 return Err(crate::error::Error::fmt_msg("bomb"));
255 }
256 let mut words = Vec::with_capacity((data.len() - 4) / 4);
257 for chunk in data[4..].chunks_exact(4) {
258 words.push(u32::from_le_bytes(chunk.try_into().expect("4 bytes")));
259 }
260 let mut dec = DefaultRangeDecoder::from_compressed(words)
261 .map_err(|e| crate::error::Error::fmt_msg(format!("mix decoder open: {e:?}")))?;
262 let mut ctxs: Vec<MixCtx> = (0..params.nctx)
263 .map(|_| MixCtx::new(params.cap_fast, params.cap_slow))
264 .collect();
265 let mut out = Vec::with_capacity(stream_len);
266 let mut prev = 0u8;
267 let mut prev2 = 0u8;
268 for _i in 0..stream_len {
269 let c = mix_ctx_key(params, prev, prev2);
270 let m = ctxs[c]
271 .model()
272 .ok_or_else(|| crate::error::Error::fmt_msg("mix model build failed"))?;
273 let s = dec
274 .decode_symbols(std::iter::once(&m))
275 .next()
276 .ok_or_else(|| crate::error::Error::fmt_msg("mix stream exhausted"))?
277 .map_err(|e| crate::error::Error::fmt_msg(format!("mix decode symbol: {e:?}")))?;
278 let s = u8::try_from(s)
279 .map_err(|_| crate::error::Error::fmt_msg("mix symbol out of byte range"))?;
280 ctxs[c].step(s);
281 out.push(s);
282 prev2 = prev;
283 prev = s;
284 }
285 Ok(out)
286}
287
288pub fn mix_decode(data: &[u8], limit: usize) -> Result<Vec<u8>, crate::error::Error> {
290 mix_decode_with(&MixParams::BACKEND4, data, limit)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
299 fn mix_backend4_roundtrip_runs() {
300 let data: Vec<u8> = std::iter::repeat_n(0u8, 4096)
301 .chain(std::iter::repeat_n(7u8, 512))
302 .collect();
303 let wire = mix_encode(&data);
304 assert_eq!(mix_decode(&wire, data.len()).unwrap(), data);
305 }
306
307 #[test]
310 fn mix_backend5_roundtrip_and_distinct() {
311 let data: Vec<u8> = (0..8192u32).map(|i| (i * 7 + (i >> 5)) as u8).collect();
312 let w4 = mix_encode(&data);
313 let w5 = mix_encode_with(&MixParams::BACKEND5, &data);
314 assert_eq!(mix_decode(&w4, data.len()).unwrap(), data);
315 assert_eq!(
316 mix_decode_with(&MixParams::BACKEND5, &w5, data.len()).unwrap(),
317 data
318 );
319 assert_ne!(w4, w5, "backend 5's evolution must differ from backend 4's");
321 }
322
323 #[test]
327 fn mix_backend5_rejects_malformed() {
328 let data: Vec<u8> = (0..4096u32).map(|i| ((i * 31) >> 3) as u8).collect();
329 let w5 = mix_encode_with(&MixParams::BACKEND5, &data);
330 assert!(mix_decode_with(&MixParams::BACKEND5, &w5[..17], 4096).is_err());
332 let mut bomb = w5.clone();
333 bomb[0..4].copy_from_slice(&u32::MAX.to_le_bytes());
334 assert!(mix_decode_with(&MixParams::BACKEND5, &bomb, 4096).is_err());
335 let r = mix_decode_with(&MixParams::BACKEND5, &w5[..12], 4096);
341 assert!(r.is_ok() || r.is_err(), "no panic reachable"); let _ = r.map(|v| assert!(v.len() <= 4096));
343 assert_eq!(
345 mix_decode_with(&MixParams::BACKEND5, &w5, data.len()).unwrap(),
346 data
347 );
348 }
349}