chronix_encoding/
patas.rs1use crate::coding::{checked_count, checked_decode_count};
36use crate::error::{EncodingError, Result};
37
38#[derive(Debug, Clone, Copy)]
40pub struct PatasEncoder;
41
42#[derive(Debug, Clone, Copy)]
44pub struct PatasDecoder;
45
46impl PatasEncoder {
47 pub fn encode(values: &[f64]) -> Result<Vec<u8>> {
53 if values.is_empty() {
54 return Err(EncodingError::EmptyInput {
55 context: "patas encoder",
56 });
57 }
58
59 let count = checked_count(values.len())?;
60
61 let mut buf = Vec::with_capacity(12 + values.len() * 9);
63
64 buf.extend_from_slice(&count.to_le_bytes());
66
67 let mut prev_bits = values[0].to_bits();
69 buf.extend_from_slice(&prev_bits.to_le_bytes());
70
71 for &val in &values[1..] {
72 let bits = val.to_bits();
73 let xor = prev_bits ^ bits;
74
75 if xor == 0 {
76 buf.push(0);
78 } else {
79 let tz_bytes = (xor.trailing_zeros() / 8) as u8; let total_bytes = 8 - (xor.leading_zeros() / 8) as u8; let sig_bytes = total_bytes.saturating_sub(tz_bytes); let flag = (tz_bytes << 4) | sig_bytes;
86 buf.push(flag);
87
88 let shifted = xor >> (u32::from(tz_bytes) * 8);
90 let sig_slice = shifted.to_le_bytes();
91 buf.extend_from_slice(&sig_slice[..sig_bytes as usize]);
92 }
93
94 prev_bits = bits;
95 }
96
97 Ok(buf)
98 }
99}
100
101impl PatasDecoder {
102 pub fn decode(payload: &[u8]) -> Result<Vec<f64>> {
108 if payload.len() < 12 {
109 return Err(EncodingError::CorruptData {
110 detail: "patas payload too short for header".to_string(),
111 });
112 }
113
114 let count = checked_decode_count(
115 u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize,
116 "Patas",
117 )?;
118 if count == 0 {
119 return Err(EncodingError::CorruptData {
120 detail: "patas count is zero".to_string(),
121 });
122 }
123
124 let mut values = Vec::with_capacity(count);
125 let mut pos = 4;
126
127 if pos + 8 > payload.len() {
129 return Err(EncodingError::CorruptData {
130 detail: "patas payload truncated at first value".to_string(),
131 });
132 }
133 let mut prev_bits = u64::from_le_bytes(payload[pos..pos + 8].try_into().expect("8 bytes"));
134 values.push(f64::from_bits(prev_bits));
135 pos += 8;
136
137 for _ in 1..count {
138 if pos >= payload.len() {
139 return Err(EncodingError::CorruptData {
140 detail: "patas payload truncated".to_string(),
141 });
142 }
143
144 let flag = payload[pos];
145 pos += 1;
146
147 let tz_bytes = (flag >> 4) & 0x0F;
148 let sig_bytes = flag & 0x0F;
149
150 if tz_bytes + sig_bytes > 8 {
160 return Err(EncodingError::CorruptData {
161 detail: format!(
162 "patas flag byte claims {tz_bytes} trailing-zero and {sig_bytes} significant bytes, which do not fit in 8 — header is corrupt"
163 ),
164 });
165 }
166
167 if sig_bytes == 0 {
168 values.push(f64::from_bits(prev_bits));
170 continue;
171 }
172
173 if pos + sig_bytes as usize > payload.len() {
174 return Err(EncodingError::CorruptData {
175 detail: "patas payload truncated at significant bytes".to_string(),
176 });
177 }
178
179 let mut sig_val = [0u8; 8];
181 sig_val[..sig_bytes as usize].copy_from_slice(&payload[pos..pos + sig_bytes as usize]);
182 pos += sig_bytes as usize;
183
184 let shifted = u64::from_le_bytes(sig_val);
185 let xor = shifted << (u32::from(tz_bytes) * 8);
186 prev_bits ^= xor;
187 values.push(f64::from_bits(prev_bits));
188 }
189
190 if values.len() != count {
191 return Err(EncodingError::CorruptData {
192 detail: format!("patas decoded {} values, expected {}", values.len(), count),
193 });
194 }
195
196 Ok(values)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn roundtrip_basic() {
206 let values = vec![1.0, 1.1, 1.2, 1.3, 1.4, 1.5];
207 let encoded = PatasEncoder::encode(&values).unwrap();
208 let decoded = PatasDecoder::decode(&encoded).unwrap();
209 assert_eq!(values, decoded);
210 }
211
212 #[test]
213 fn roundtrip_identical() {
214 let values = vec![42.0; 100];
215 let encoded = PatasEncoder::encode(&values).unwrap();
216 let decoded = PatasDecoder::decode(&encoded).unwrap();
217 assert_eq!(values, decoded);
218 assert_eq!(encoded.len(), 111);
220 }
221
222 #[test]
223 fn roundtrip_single() {
224 let values = vec![std::f64::consts::PI];
225 let encoded = PatasEncoder::encode(&values).unwrap();
226 let decoded = PatasDecoder::decode(&encoded).unwrap();
227 assert_eq!(values, decoded);
228 }
229
230 #[test]
231 fn roundtrip_special_values() {
232 let values = vec![
233 0.0,
234 f64::INFINITY,
235 f64::NEG_INFINITY,
236 -0.0,
237 f64::MAX,
238 f64::MIN,
239 ];
240 let encoded = PatasEncoder::encode(&values).unwrap();
241 let decoded = PatasDecoder::decode(&encoded).unwrap();
242 for (a, b) in values.iter().zip(decoded.iter()) {
244 assert_eq!(a.to_bits(), b.to_bits());
245 }
246 }
247
248 #[test]
249 fn roundtrip_nan() {
250 let values = vec![1.0, f64::NAN, 2.0];
251 let encoded = PatasEncoder::encode(&values).unwrap();
252 let decoded = PatasDecoder::decode(&encoded).unwrap();
253 assert_eq!(values[0], decoded[0]);
254 assert!(decoded[1].is_nan());
255 assert_eq!(values[2], decoded[2]);
256 }
257
258 #[test]
259 fn empty_input_error() {
260 let result = PatasEncoder::encode(&[]);
261 assert!(result.is_err());
262 }
263
264 #[test]
265 fn truncated_payload_error() {
266 let result = PatasDecoder::decode(&[0; 4]);
267 assert!(result.is_err());
268 }
269
270 #[test]
271 fn compression_ratio_slowly_varying() {
272 let values: Vec<f64> = (0..1000).map(|i| 20.0 + (i as f64) * 0.001).collect();
274 let encoded = PatasEncoder::encode(&values).unwrap();
275 let raw_size = values.len() * 8;
276 assert!(
278 encoded.len() < raw_size,
279 "Expected compression, got {}/{}",
280 encoded.len(),
281 raw_size
282 );
283 let decoded = PatasDecoder::decode(&encoded).unwrap();
284 assert_eq!(values, decoded);
285 }
286}
287
288#[cfg(test)]
289mod proptests {
290 use super::*;
291 use proptest::prelude::*;
292
293 proptest! {
294 #[test]
295 fn roundtrip_arbitrary(values in proptest::collection::vec(any::<f64>(), 1..500)) {
296 let encoded = PatasEncoder::encode(&values).unwrap();
297 let decoded = PatasDecoder::decode(&encoded).unwrap();
298 prop_assert_eq!(values.len(), decoded.len());
299 for (a, b) in values.iter().zip(decoded.iter()) {
300 prop_assert_eq!(a.to_bits(), b.to_bits(), "bitwise mismatch");
301 }
302 }
303 }
304}
305
306#[cfg(test)]
307mod flag_validation_tests {
308 use super::*;
309
310 fn payload_with_flag(flag: u8, tail: &[u8]) -> Vec<u8> {
312 let mut p = Vec::new();
313 p.extend_from_slice(&2u32.to_le_bytes());
314 p.extend_from_slice(&1.0f64.to_bits().to_le_bytes());
315 p.push(flag);
316 p.extend_from_slice(tail);
317 p
318 }
319
320 #[test]
328 fn a_trailing_zero_count_past_the_width_of_a_u64_is_rejected() {
329 for tz in 8u8..=15 {
330 let flag = (tz << 4) | 1; let err = PatasDecoder::decode(&payload_with_flag(flag, &[0xFF]))
332 .expect_err("tz_bytes {tz} must be refused");
333 assert!(
334 err.to_string().contains("do not fit in 8"),
335 "tz={tz}: expected the flag guard, got: {err}"
336 );
337 }
338 }
339
340 #[test]
342 fn a_significant_byte_count_past_eight_is_rejected() {
343 for sig in 9u8..=15 {
344 let err = PatasDecoder::decode(&payload_with_flag(sig, &[0xFF; 16]))
345 .expect_err("sig_bytes {sig} must be refused");
346 assert!(
347 err.to_string().contains("do not fit in 8"),
348 "sig={sig}: expected the flag guard, got: {err}"
349 );
350 }
351 }
352
353 #[test]
355 fn every_flag_the_encoder_can_produce_still_decodes() {
356 let values = vec![
359 1.0,
360 1.0,
361 1.5,
362 1.5000001,
363 2.0,
364 -2.0,
365 0.0,
366 -0.0,
367 f64::MAX,
368 f64::MIN_POSITIVE,
369 1e300,
370 1e-300,
371 ];
372 let encoded = PatasEncoder::encode(&values).expect("encode");
373 let decoded = PatasDecoder::decode(&encoded).expect("decode");
374 assert_eq!(decoded.len(), values.len());
375 for (a, b) in values.iter().zip(&decoded) {
376 assert_eq!(a.to_bits(), b.to_bits(), "round trip must be bitwise exact");
377 }
378 }
379}