Skip to main content

tensogram_szip/
lib.rs

1// (C) Copyright 2026- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9//! Pure-Rust CCSDS 121.0-B-3 Adaptive Entropy Coding (AEC/SZIP).
10//!
11//! This crate provides encode, decode, and range-decode functions with
12//! the same `AecParams` interface as the C libaec library. It can be
13//! used as a drop-in replacement for `libaec-sys` in environments where
14//! C FFI is unavailable (e.g. WebAssembly).
15//!
16//! # Example
17//!
18//! ```
19//! use tensogram_szip::{aec_compress, aec_decompress, AecParams, AEC_DATA_PREPROCESS};
20//!
21//! let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
22//! let params = AecParams {
23//!     bits_per_sample: 8,
24//!     block_size: 16,
25//!     rsi: 128,
26//!     flags: AEC_DATA_PREPROCESS,
27//! };
28//!
29//! let (compressed, offsets) = aec_compress(&data, &params).unwrap();
30//! let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
31//! assert_eq!(decompressed, data);
32//! ```
33
34mod bitstream;
35mod decoder;
36mod encoder;
37mod error;
38pub mod params;
39mod preprocessor;
40
41pub use error::AecError;
42pub use params::{
43    AEC_ALLOW_K13, AEC_DATA_3BYTE, AEC_DATA_MSB, AEC_DATA_PREPROCESS, AEC_DATA_SIGNED,
44    AEC_NOT_ENFORCE, AEC_PAD_RSI, AEC_RESTRICTED, AecParams,
45};
46
47/// Compress data using CCSDS 121.0-B-3 adaptive entropy coding.
48///
49/// Returns `(compressed_bytes, rsi_block_bit_offsets)` where the
50/// offsets track the bit position of each RSI boundary in the
51/// compressed stream — needed for [`aec_decompress_range`].
52pub fn aec_compress(data: &[u8], params: &AecParams) -> Result<(Vec<u8>, Vec<u64>), AecError> {
53    params::validate(params)?;
54    encoder::encode(data, params, true)
55}
56
57/// Compress data without tracking RSI block offsets (slightly faster).
58pub fn aec_compress_no_offsets(data: &[u8], params: &AecParams) -> Result<Vec<u8>, AecError> {
59    params::validate(params)?;
60    let (bytes, _) = encoder::encode(data, params, false)?;
61    Ok(bytes)
62}
63
64/// Decompress an entire AEC-compressed stream.
65///
66/// `expected_size` is the expected decompressed size in bytes.
67pub fn aec_decompress(
68    data: &[u8],
69    expected_size: usize,
70    params: &AecParams,
71) -> Result<Vec<u8>, AecError> {
72    params::validate(params)?;
73    decoder::decode(data, expected_size, params)
74}
75
76/// Decompress a partial byte range from AEC-compressed data using
77/// pre-computed RSI block bit offsets.
78///
79/// `block_offsets` are the bit offsets returned by [`aec_compress`].
80/// `byte_pos` and `byte_size` specify the byte range within the
81/// decompressed output to extract.
82pub fn aec_decompress_range(
83    data: &[u8],
84    block_offsets: &[u64],
85    byte_pos: usize,
86    byte_size: usize,
87    params: &AecParams,
88) -> Result<Vec<u8>, AecError> {
89    params::validate(params)?;
90    decoder::decode_range(data, block_offsets, byte_pos, byte_size, params)
91}
92
93// ── Tests ────────────────────────────────────────────────────────────────────
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn default_params(bits_per_sample: u32) -> AecParams {
100        AecParams {
101            bits_per_sample,
102            block_size: 16,
103            rsi: 128,
104            flags: AEC_DATA_PREPROCESS,
105        }
106    }
107
108    #[test]
109    fn round_trip_u8() {
110        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
111        let params = default_params(8);
112
113        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
114        assert!(!compressed.is_empty());
115        assert!(!offsets.is_empty());
116
117        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
118        assert_eq!(decompressed, data);
119    }
120
121    #[test]
122    fn round_trip_u16() {
123        let values: Vec<u16> = (0..2048).map(|i| (i * 7 % 65536) as u16).collect();
124        let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
125        let params = default_params(16);
126
127        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
128        assert!(!compressed.is_empty());
129        assert!(!offsets.is_empty());
130
131        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
132        assert_eq!(decompressed, data);
133    }
134
135    #[test]
136    fn round_trip_u24() {
137        let n = 4096;
138        let data: Vec<u8> = (0..n * 3).map(|i| (i % 256) as u8).collect();
139        let params = default_params(24);
140
141        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
142        assert!(!compressed.is_empty());
143        assert!(!offsets.is_empty());
144
145        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
146        assert_eq!(decompressed, data);
147    }
148
149    #[test]
150    fn round_trip_u32() {
151        let values: Vec<u32> = (0..4096).map(|i| i * 13).collect();
152        let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
153        let params = default_params(32);
154
155        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
156        assert!(!compressed.is_empty());
157        assert!(!offsets.is_empty());
158
159        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
160        assert_eq!(decompressed, data);
161    }
162
163    #[test]
164    fn empty_input_returns_empty() {
165        let params = default_params(8);
166        let (compressed, offsets) = aec_compress(&[], &params).unwrap();
167        assert!(compressed.is_empty());
168        assert!(offsets.is_empty());
169
170        let decompressed = aec_decompress(&[], 0, &params).unwrap();
171        assert!(decompressed.is_empty());
172    }
173
174    #[test]
175    fn misaligned_data_returns_error() {
176        let data = vec![1u8, 2, 3]; // 3 bytes, not multiple of 2 (u16)
177        let params = default_params(16);
178        assert!(aec_compress(&data, &params).is_err());
179    }
180
181    #[test]
182    fn offsets_match_rsi_count() {
183        // 4096 8-bit samples / (128 RSI * 16 block_size) = 2 RSIs
184        let data: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
185        let params = default_params(8);
186        let (_, offsets) = aec_compress(&data, &params).unwrap();
187        let expected_rsis = 4096usize.div_ceil(128 * 16);
188        assert_eq!(offsets.len(), expected_rsis);
189    }
190
191    #[test]
192    fn round_trip_constant_data() {
193        let data = vec![42u8; 2048];
194        let params = default_params(8);
195
196        let (compressed, _) = aec_compress(&data, &params).unwrap();
197        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
198        assert_eq!(decompressed, data);
199    }
200
201    #[test]
202    fn round_trip_no_preprocess() {
203        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
204        let params = AecParams {
205            bits_per_sample: 8,
206            block_size: 16,
207            rsi: 128,
208            flags: 0, // no preprocessing
209        };
210
211        let (compressed, _) = aec_compress(&data, &params).unwrap();
212        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
213        assert_eq!(decompressed, data);
214    }
215
216    #[test]
217    fn range_decode_matches_full() {
218        let data: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
219        let params = default_params(8);
220
221        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
222        let full = aec_decompress(&compressed, data.len(), &params).unwrap();
223
224        // Decode a range from the middle
225        let pos = 100;
226        let size = 200;
227        let partial = aec_decompress_range(&compressed, &offsets, pos, size, &params).unwrap();
228
229        assert_eq!(partial.len(), size);
230        assert_eq!(&partial[..], &full[pos..pos + size]);
231    }
232
233    #[test]
234    fn range_decode_first_block() {
235        let data: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
236        let params = default_params(8);
237
238        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
239        let full = aec_decompress(&compressed, data.len(), &params).unwrap();
240
241        let partial = aec_decompress_range(&compressed, &offsets, 0, 50, &params).unwrap();
242        assert_eq!(&partial[..], &full[..50]);
243    }
244
245    #[test]
246    fn range_decode_zero_size() {
247        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
248        let params = default_params(8);
249
250        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
251
252        let partial = aec_decompress_range(&compressed, &offsets, 0, 0, &params).unwrap();
253        assert!(partial.is_empty());
254    }
255
256    #[test]
257    fn round_trip_msb_data() {
258        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
259        let params = AecParams {
260            bits_per_sample: 8,
261            block_size: 16,
262            rsi: 128,
263            flags: AEC_DATA_PREPROCESS | AEC_DATA_MSB,
264        };
265
266        let (compressed, _) = aec_compress(&data, &params).unwrap();
267        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
268        assert_eq!(decompressed, data);
269    }
270
271    #[test]
272    fn round_trip_small_block_size() {
273        let data: Vec<u8> = (0..512).map(|i| (i % 256) as u8).collect();
274        let params = AecParams {
275            bits_per_sample: 8,
276            block_size: 8,
277            rsi: 64,
278            flags: AEC_DATA_PREPROCESS,
279        };
280
281        let (compressed, _) = aec_compress(&data, &params).unwrap();
282        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
283        assert_eq!(decompressed, data);
284    }
285
286    // ── Coverage: aec_compress_no_offsets ────────────────────────────────
287
288    #[test]
289    fn compress_no_offsets_round_trip() {
290        let data: Vec<u8> = (0..2048).map(|i| (i % 256) as u8).collect();
291        let params = default_params(8);
292
293        let compressed = aec_compress_no_offsets(&data, &params).unwrap();
294        assert!(!compressed.is_empty());
295
296        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
297        assert_eq!(decompressed, data);
298    }
299
300    #[test]
301    fn compress_no_offsets_empty() {
302        let params = default_params(8);
303        let compressed = aec_compress_no_offsets(&[], &params).unwrap();
304        assert!(compressed.is_empty());
305    }
306
307    // ── Coverage: signed data round-trip ─────────────────────────────────
308
309    #[test]
310    fn round_trip_signed_8bit() {
311        // 8-bit signed: values encoded as unsigned representation
312        let data: Vec<u8> = (-128..=127i8).map(|v| v as u8).collect();
313        let params = AecParams {
314            bits_per_sample: 8,
315            block_size: 16,
316            rsi: 128,
317            flags: AEC_DATA_PREPROCESS | AEC_DATA_SIGNED,
318        };
319
320        let (compressed, _) = aec_compress(&data, &params).unwrap();
321        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
322        assert_eq!(decompressed, data);
323    }
324
325    // ── Coverage: range decode out-of-bounds ─────────────────────────────
326
327    #[test]
328    fn range_decode_oob_rsi_returns_error() {
329        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
330        let params = default_params(8);
331        let (compressed, offsets) = aec_compress(&data, &params).unwrap();
332
333        // Request beyond available RSIs
334        let result = aec_decompress_range(&compressed, &offsets, 999999, 100, &params);
335        assert!(result.is_err());
336    }
337
338    #[test]
339    fn range_decode_empty_data_returns_error() {
340        let params = default_params(8);
341        let result = aec_decompress_range(&[], &[0], 0, 100, &params);
342        assert!(result.is_err());
343    }
344
345    // ── Coverage: validation error paths ─────────────────────────────────
346
347    #[test]
348    fn compress_bad_rsi_returns_error() {
349        let params = AecParams {
350            bits_per_sample: 8,
351            block_size: 16,
352            rsi: 0, // invalid
353            flags: 0,
354        };
355        assert!(aec_compress(&[0u8; 256], &params).is_err());
356    }
357
358    #[test]
359    fn decompress_bad_block_size_returns_error() {
360        let params = AecParams {
361            bits_per_sample: 8,
362            block_size: 12, // invalid without NOT_ENFORCE
363            rsi: 128,
364            flags: 0,
365        };
366        assert!(aec_decompress(&[0u8; 256], 256, &params).is_err());
367    }
368
369    #[test]
370    fn compress_restricted_high_bps_returns_error() {
371        let params = AecParams {
372            bits_per_sample: 8,
373            block_size: 16,
374            rsi: 128,
375            flags: AEC_RESTRICTED, // requires bps ≤ 4
376        };
377        assert!(aec_compress(&[0u8; 256], &params).is_err());
378    }
379
380    // ── Coverage: 32-bit round trip ──────────────────────────────────────
381
382    #[test]
383    fn round_trip_u32_max_values() {
384        // Extreme u32 values
385        let values: Vec<u32> = vec![0, u32::MAX, u32::MAX / 2, 1, u32::MAX - 1];
386        let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
387        let params = default_params(32);
388
389        let (compressed, _) = aec_compress(&data, &params).unwrap();
390        let decompressed = aec_decompress(&compressed, data.len(), &params).unwrap();
391        assert_eq!(decompressed, data);
392    }
393}