1use std::mem::size_of;
31
32use crate::bounds::{
33 checked_add, checked_capacity, checked_mul, checked_range, decoded_len, encode_input_len,
34 u32_to_usize,
35};
36use crate::error::CodecError;
37
38use crate::CODEC_SAMPLE_SIZE;
39
40pub fn encode(values: &[f64]) -> Result<Vec<u8>, CodecError> {
46 let decoded_bytes = checked_mul(values.len(), size_of::<f64>(), "ALP-RD input bytes")?;
47 decoded_len(decoded_bytes, "ALP-RD input")?;
48 let count = encode_input_len(values.len(), "ALP-RD value count")?;
49
50 if values.is_empty() {
51 let mut out = Vec::with_capacity(7);
52 out.extend_from_slice(&0u32.to_le_bytes());
53 out.push(0); out.extend_from_slice(&0u16.to_le_bytes());
55 return Ok(out);
56 }
57
58 let cut = find_best_cut(values);
60 let bits: Vec<u64> = values.iter().map(|v| v.to_bits()).collect();
61
62 let front_mask: u64 = if cut == 64 { 0 } else { u64::MAX << cut };
64 let tail_mask: u64 = if cut == 0 { 0 } else { (1u64 << cut) - 1 };
65 let tail_bytes_per_value = (cut as usize).div_ceil(8);
66
67 let fronts: Vec<u64> = bits.iter().map(|&b| (b & front_mask) >> cut).collect();
68
69 let mut dict: Vec<u64> = fronts.clone();
71 dict.sort_unstable();
72 dict.dedup();
73 let dict_size = u16::try_from(dict.len()).map_err(|_| CodecError::ResourceLimit {
74 resource: "ALP-RD dictionary entries".into(),
75 requested: dict.len(),
76 limit: u16::MAX as usize,
77 })?;
78
79 let indices: Vec<u16> = fronts
83 .iter()
84 .map(|f| {
85 dict.binary_search(f)
86 .map(|idx| idx as u16)
87 .map_err(|_| CodecError::Corrupt {
88 detail: "ALP-RD front value missing from dictionary".into(),
89 })
90 })
91 .collect::<Result<_, _>>()?;
92
93 let use_u8_indices = dict.len() <= 256;
94
95 let dict_bytes = checked_mul(dict.len(), 8, "ALP-RD dictionary")?;
97 let index_bytes = checked_mul(
98 values.len(),
99 if use_u8_indices { 1 } else { 2 },
100 "ALP-RD indices",
101 )?;
102 let tail_bytes = checked_mul(values.len(), tail_bytes_per_value, "ALP-RD tails")?;
103 let capacity = checked_add(
104 checked_add(
105 checked_add(7, dict_bytes, "ALP-RD output")?,
106 index_bytes,
107 "ALP-RD output",
108 )?,
109 tail_bytes,
110 "ALP-RD output",
111 )?;
112 let mut out = Vec::with_capacity(capacity);
113
114 out.extend_from_slice(&count.to_le_bytes());
116 out.push(cut);
117 out.extend_from_slice(&dict_size.to_le_bytes());
118
119 for &entry in &dict {
121 out.extend_from_slice(&entry.to_le_bytes());
122 }
123
124 if use_u8_indices {
126 for &idx in &indices {
127 out.push(idx as u8);
128 }
129 } else {
130 for &idx in &indices {
131 out.extend_from_slice(&idx.to_le_bytes());
132 }
133 }
134
135 for &b in &bits {
137 let tail = b & tail_mask;
138 for byte_idx in 0..tail_bytes_per_value {
139 out.push((tail >> (byte_idx * 8)) as u8);
140 }
141 }
142
143 Ok(out)
144}
145
146pub fn decode(data: &[u8]) -> Result<Vec<f64>, CodecError> {
148 if data.len() < 7 {
149 return Err(CodecError::Truncated {
150 expected: 7,
151 actual: data.len(),
152 });
153 }
154
155 let count = u32_to_usize(
156 u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
157 "ALP-RD value count",
158 )?;
159 decoded_len(
160 checked_mul(count, size_of::<f64>(), "ALP-RD decoded values")?,
161 "ALP-RD",
162 )?;
163 let cut = data[4];
164 let dict_size = usize::from(u16::from_le_bytes([data[5], data[6]]));
165
166 if count == 0 {
167 if dict_size != 0 || data.len() != 7 {
168 return Err(CodecError::Corrupt {
169 detail: "non-canonical empty ALP-RD frame".into(),
170 });
171 }
172 return Ok(Vec::new());
173 }
174
175 if cut > 63 {
176 return Err(CodecError::Corrupt {
177 detail: format!("invalid ALP-RD cut position: {cut}"),
178 });
179 }
180
181 let tail_bytes_per_value = (cut as usize).div_ceil(8);
182 let tail_mask: u64 = if cut == 0 { 0 } else { (1u64 << cut) - 1 };
183 if dict_size == 0 || dict_size > count {
184 return Err(CodecError::Corrupt {
185 detail: "ALP-RD dictionary count is invalid".into(),
186 });
187 }
188 let use_u8_indices = dict_size <= 256;
189
190 let mut pos = 7;
192 let dict_bytes = checked_mul(dict_size, 8, "ALP-RD dictionary")?;
193 checked_range(data, pos, dict_bytes, "ALP-RD dictionary")?;
194 let dict_capacity = checked_capacity(dict_size, size_of::<u64>(), "ALP-RD dictionary")?;
195 let mut dict = Vec::with_capacity(dict_capacity);
196 let mut previous = None;
197 for _ in 0..dict_size {
198 let entry = u64::from_le_bytes([
199 data[pos],
200 data[pos + 1],
201 data[pos + 2],
202 data[pos + 3],
203 data[pos + 4],
204 data[pos + 5],
205 data[pos + 6],
206 data[pos + 7],
207 ]);
208 if entry > (u64::MAX >> cut) || previous.is_some_and(|value| entry <= value) {
209 return Err(CodecError::Corrupt {
210 detail: "ALP-RD dictionary is non-canonical".into(),
211 });
212 }
213 previous = Some(entry);
214 dict.push(entry);
215 pos = checked_add(pos, 8, "ALP-RD dictionary cursor")?;
216 }
217
218 let index_bytes = checked_mul(count, if use_u8_indices { 1 } else { 2 }, "ALP-RD indices")?;
220 checked_range(data, pos, index_bytes, "ALP-RD indices")?;
221 let index_capacity = checked_capacity(count, size_of::<usize>(), "ALP-RD indices")?;
222 let mut indices = Vec::with_capacity(index_capacity);
223 if use_u8_indices {
224 for i in 0..count {
225 indices.push(data[pos + i] as usize);
226 }
227 pos = checked_add(pos, count, "ALP-RD index cursor")?;
228 } else {
229 for i in 0..count {
230 let idx_pos = checked_add(
231 pos,
232 checked_mul(i, 2, "ALP-RD index offset")?,
233 "ALP-RD index position",
234 )?;
235 indices.push(u16::from_le_bytes([data[idx_pos], data[idx_pos + 1]]) as usize);
236 }
237 pos = checked_add(pos, index_bytes, "ALP-RD index cursor")?;
238 }
239
240 let tail_total = checked_mul(count, tail_bytes_per_value, "ALP-RD tails")?;
242 let tails = checked_range(data, pos, tail_total, "ALP-RD tails")?;
243 if checked_add(pos, tail_total, "ALP-RD frame end")? != data.len() {
244 return Err(CodecError::Corrupt {
245 detail: "trailing bytes after ALP-RD frame".into(),
246 });
247 }
248
249 let value_capacity = checked_capacity(count, size_of::<f64>(), "ALP-RD values")?;
250 let mut values = Vec::with_capacity(value_capacity);
251 for (i, &idx) in indices.iter().enumerate() {
252 if idx >= dict.len() {
253 return Err(CodecError::Corrupt {
254 detail: format!("ALP-RD dict index {idx} out of range (max {})", dict.len()),
255 });
256 }
257
258 let front = dict[idx] << cut;
259 let mut tail = 0u64;
260 let tail_pos = checked_mul(i, tail_bytes_per_value, "ALP-RD tail offset")?;
261 for byte_idx in 0..tail_bytes_per_value {
262 tail |= (tails[tail_pos + byte_idx] as u64) << (byte_idx * 8);
263 }
264 let final_bits = usize::from(cut) % 8;
265 if final_bits != 0 {
266 let last = tails[tail_pos + tail_bytes_per_value - 1];
267 let padding_mask = !((1u8 << final_bits) - 1);
268 if last & padding_mask != 0 {
269 return Err(CodecError::Corrupt {
270 detail: "non-zero ALP-RD tail padding".into(),
271 });
272 }
273 }
274 tail &= tail_mask;
275
276 values.push(f64::from_bits(front | tail));
277 }
278
279 Ok(values)
280}
281
282fn find_best_cut(values: &[f64]) -> u8 {
291 let sample_end = values.len().min(CODEC_SAMPLE_SIZE);
292 let sample = &values[..sample_end];
293 let bits: Vec<u64> = sample.iter().map(|v| v.to_bits()).collect();
294
295 let mut best_cut = 48u8;
296 let mut best_unique = usize::MAX;
297
298 for cut in 40..=56 {
300 let mut fronts: Vec<u64> = bits.iter().map(|&b| b >> cut).collect();
301 fronts.sort_unstable();
302 fronts.dedup();
303
304 if fronts.len() < best_unique {
305 best_unique = fronts.len();
306 best_cut = cut;
307 }
308 }
309
310 best_cut
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn empty_roundtrip() {
319 let encoded = encode(&[]).unwrap();
320 let decoded = decode(&encoded).unwrap();
321 assert!(decoded.is_empty());
322 }
323
324 #[test]
325 fn pi_multiples() {
326 let values: Vec<f64> = (1..1000).map(|i| std::f64::consts::PI * i as f64).collect();
327 let encoded = encode(&values).unwrap();
328 let decoded = decode(&encoded).unwrap();
329 for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
330 assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
331 }
332 }
333
334 #[test]
335 fn scientific_data() {
336 let values: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.001).exp()).collect();
337 let encoded = encode(&values).unwrap();
338 let decoded = decode(&encoded).unwrap();
339 for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
340 assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
341 }
342 }
343
344 #[test]
345 fn compression_ratio() {
346 let values: Vec<f64> = (1..10_000)
347 .map(|i| std::f64::consts::E * i as f64 + (i as f64).sqrt())
348 .collect();
349 let encoded = encode(&values).unwrap();
350 let raw_size = values.len() * 8;
351 let ratio = raw_size as f64 / encoded.len() as f64;
352 assert!(
356 ratio >= 0.95,
357 "ALP-RD should not expand >5%, got {ratio:.2}x"
358 );
359
360 let decoded = decode(&encoded).unwrap();
361 for (a, b) in values.iter().zip(decoded.iter()) {
362 assert_eq!(a.to_bits(), b.to_bits());
363 }
364 }
365
366 #[test]
367 fn special_values() {
368 let values = vec![
369 0.0,
370 -0.0,
371 f64::INFINITY,
372 f64::NEG_INFINITY,
373 f64::NAN,
374 f64::MIN,
375 f64::MAX,
376 f64::MIN_POSITIVE,
377 ];
378 let encoded = encode(&values).unwrap();
379 let decoded = decode(&encoded).unwrap();
380 for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
381 assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
382 }
383 }
384
385 #[test]
386 fn identical_values() {
387 let values = vec![42.0f64; 1000];
388 let encoded = encode(&values).unwrap();
389 let decoded = decode(&encoded).unwrap();
390 for (a, b) in values.iter().zip(decoded.iter()) {
391 assert_eq!(a.to_bits(), b.to_bits());
392 }
393 assert!(encoded.len() < values.len() * 8);
395 }
396
397 #[test]
398 fn rejects_invalid_layout_and_trailing_bytes() {
399 assert!(decode(&[]).is_err());
400 assert!(decode(&[1, 0, 0, 0, 48, 0]).is_err());
401 let mut encoded = encode(&[1.25]).unwrap();
402 encoded.push(0);
403 assert!(matches!(decode(&encoded), Err(CodecError::Corrupt { .. })));
404 let mut invalid_index = encode(&[1.25]).unwrap();
405 let dict_size = usize::from(u16::from_le_bytes([invalid_index[5], invalid_index[6]]));
406 let index_start = 7 + dict_size * 8;
407 invalid_index[index_start] = u8::MAX;
408 assert!(matches!(
409 decode(&invalid_index),
410 Err(CodecError::Corrupt { .. })
411 ));
412
413 let mut padded_tail = Vec::new();
414 padded_tail.extend_from_slice(&1u32.to_le_bytes());
415 padded_tail.push(1);
416 padded_tail.extend_from_slice(&1u16.to_le_bytes());
417 padded_tail.extend_from_slice(&0u64.to_le_bytes());
418 padded_tail.push(0);
419 padded_tail.push(0x80);
420 assert!(matches!(
421 decode(&padded_tail),
422 Err(CodecError::Corrupt { .. })
423 ));
424
425 let mut wide_front = padded_tail;
426 wide_front[7..15].copy_from_slice(&u64::MAX.to_le_bytes());
427 wide_front[16] = 0;
428 assert!(matches!(
429 decode(&wide_front),
430 Err(CodecError::Corrupt { .. })
431 ));
432 }
433}