1use std::borrow::Cow;
10
11#[cfg(feature = "blosc2")]
12use crate::compression::Blosc2Compressor;
13#[cfg(feature = "lz4")]
14use crate::compression::Lz4Compressor;
15#[cfg(feature = "sz3")]
16use crate::compression::Sz3Compressor;
17#[cfg(feature = "szip")]
18use crate::compression::SzipCompressor;
19#[cfg(feature = "szip-pure")]
20use crate::compression::SzipPureCompressor;
21#[cfg(feature = "zfp")]
22use crate::compression::ZfpCompressor;
23#[cfg(feature = "zstd")]
24use crate::compression::ZstdCompressor;
25#[cfg(feature = "zstd-pure")]
26use crate::compression::ZstdPureCompressor;
27use crate::compression::{CompressResult, CompressionError, Compressor};
28use crate::shuffle;
29use crate::simple_packing::{self, PackingError, SimplePackingParams};
30use serde::{Deserialize, Serialize};
31use std::sync::OnceLock;
32use thiserror::Error;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum ByteOrder {
37 Big,
38 Little,
39}
40
41impl ByteOrder {
42 #[inline]
44 pub fn native() -> Self {
45 #[cfg(target_endian = "little")]
46 {
47 ByteOrder::Little
48 }
49 #[cfg(target_endian = "big")]
50 {
51 ByteOrder::Big
52 }
53 }
54}
55
56pub fn byteswap(data: &mut [u8], unit_size: usize) -> Result<(), PipelineError> {
64 if unit_size <= 1 {
65 return Ok(());
66 }
67 if !data.len().is_multiple_of(unit_size) {
68 return Err(PipelineError::Range(format!(
69 "byteswap: data length {} is not a multiple of unit_size {}",
70 data.len(),
71 unit_size,
72 )));
73 }
74 for chunk in data.chunks_exact_mut(unit_size) {
75 chunk.reverse();
76 }
77 Ok(())
78}
79
80#[derive(Debug, Error)]
81#[non_exhaustive]
82pub enum PipelineError {
83 #[error("encoding error: {0}")]
84 Encoding(#[from] PackingError),
85 #[error("compression error: {0}")]
86 Compression(#[from] CompressionError),
87 #[error("shuffle error: {0}")]
88 Shuffle(String),
89 #[error("range error: {0}")]
90 Range(String),
91 #[error("unknown encoding: {0}")]
92 UnknownEncoding(String),
93 #[error("unknown filter: {0}")]
94 UnknownFilter(String),
95 #[error("unknown compression: {0}")]
96 UnknownCompression(String),
97 #[error("invalid configuration: {0}")]
102 InvalidConfig(String),
103 #[error(
125 "descriptor claims {claimed_bytes} decoded byte(s) but frame payload is \
126 {payload_bytes} byte(s) (encoding {encoding}, dtype width {dtype_byte_width})"
127 )]
128 DescriptorSizeMismatch {
129 claimed_bytes: u128,
130 payload_bytes: usize,
131 encoding: String,
132 dtype_byte_width: usize,
133 },
134}
135
136#[derive(Debug, Clone)]
137pub enum EncodingType {
138 None,
139 SimplePacking(SimplePackingParams),
140}
141
142impl std::fmt::Display for EncodingType {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 EncodingType::None => write!(f, "none"),
146 EncodingType::SimplePacking(_) => write!(f, "simple_packing"),
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
152pub enum FilterType {
153 None,
154 Shuffle { element_size: usize },
155}
156
157#[cfg(feature = "blosc2")]
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(rename_all = "lowercase")]
160pub enum Blosc2Codec {
161 Blosclz,
162 Lz4,
163 Lz4hc,
164 Zlib,
165 Zstd,
166}
167
168#[cfg(feature = "zfp")]
169#[derive(Debug, Clone)]
170pub enum ZfpMode {
171 FixedRate { rate: f64 },
172 FixedPrecision { precision: u32 },
173 FixedAccuracy { tolerance: f64 },
174}
175
176#[cfg(feature = "sz3")]
177#[derive(Debug, Clone)]
178pub enum Sz3ErrorBound {
179 Absolute(f64),
180 Relative(f64),
181 Psnr(f64),
182}
183
184#[derive(Debug, Clone)]
185pub enum CompressionType {
186 None,
187 #[cfg(any(feature = "szip", feature = "szip-pure"))]
188 Szip {
189 rsi: u32,
190 block_size: u32,
191 flags: u32,
192 bits_per_sample: u32,
193 },
194 #[cfg(any(feature = "zstd", feature = "zstd-pure"))]
195 Zstd {
196 level: i32,
197 },
198 #[cfg(feature = "lz4")]
199 Lz4,
200 #[cfg(feature = "blosc2")]
201 Blosc2 {
202 codec: Blosc2Codec,
203 clevel: i32,
204 typesize: usize,
205 },
206 #[cfg(feature = "zfp")]
207 Zfp {
208 mode: ZfpMode,
209 },
210 #[cfg(feature = "sz3")]
211 Sz3 {
212 error_bound: Sz3ErrorBound,
213 },
214 Rle,
218 Roaring,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
235pub enum CompressionBackend {
236 #[default]
241 Auto,
242 Ffi,
245 Pure,
248}
249
250pub const ENV_COMPRESSION_BACKEND: &str = "TENSOGRAM_COMPRESSION_BACKEND";
255
256pub fn resolve_compression_backend(
272 requested: CompressionBackend,
273) -> Result<CompressionBackend, PipelineError> {
274 match requested {
275 CompressionBackend::Ffi | CompressionBackend::Pure => Ok(requested),
276 CompressionBackend::Auto => resolve_auto_backend(),
277 }
278}
279
280fn resolve_auto_backend() -> Result<CompressionBackend, PipelineError> {
281 static CACHED: OnceLock<Result<CompressionBackend, String>> = OnceLock::new();
287 CACHED
288 .get_or_init(|| {
289 if cfg!(target_arch = "wasm32") {
290 return Ok(CompressionBackend::Pure);
291 }
292 match std::env::var(ENV_COMPRESSION_BACKEND) {
293 Ok(val) => parse_backend(&val),
294 Err(_) => Ok(CompressionBackend::Ffi),
296 }
297 })
298 .clone()
299 .map_err(PipelineError::InvalidConfig)
300}
301
302fn parse_backend(val: &str) -> Result<CompressionBackend, String> {
303 match val.trim().to_ascii_lowercase().as_str() {
304 "pure" | "rust" => Ok(CompressionBackend::Pure),
305 "ffi" | "c" => Ok(CompressionBackend::Ffi),
306 other => Err(format!(
307 "invalid {ENV_COMPRESSION_BACKEND} value {other:?}; \
308 expected 'ffi' or 'pure' (case-insensitive)"
309 )),
310 }
311}
312
313#[derive(Debug, Clone)]
314pub struct PipelineConfig {
315 pub encoding: EncodingType,
316 pub filter: FilterType,
317 pub compression: CompressionType,
318 pub num_values: usize,
319 pub byte_order: ByteOrder,
320 pub dtype_byte_width: usize,
321 pub swap_unit_size: usize,
326 pub compression_backend: CompressionBackend,
328 pub intra_codec_threads: u32,
342 pub compute_hash: bool,
360}
361
362pub struct PipelineResult {
363 pub encoded_bytes: Vec<u8>,
364 pub block_offsets: Option<Vec<u64>>,
366 pub hash: Option<u64>,
372}
373
374#[cfg(any(feature = "szip", feature = "szip-pure"))]
376fn build_szip_compressor(
377 #[allow(unused_variables)] backend: CompressionBackend,
378 rsi: u32,
379 block_size: u32,
380 flags: u32,
381 bits_per_sample: u32,
382) -> Box<dyn Compressor> {
383 #[cfg(all(feature = "szip", feature = "szip-pure"))]
385 if matches!(backend, CompressionBackend::Pure) {
386 return Box::new(SzipPureCompressor {
387 rsi,
388 block_size,
389 flags,
390 bits_per_sample,
391 });
392 }
393
394 #[cfg(feature = "szip")]
398 {
399 Box::new(SzipCompressor {
400 rsi,
401 block_size,
402 flags,
403 bits_per_sample,
404 })
405 }
406
407 #[cfg(all(feature = "szip-pure", not(feature = "szip")))]
409 {
410 Box::new(SzipPureCompressor {
411 rsi,
412 block_size,
413 flags,
414 bits_per_sample,
415 })
416 }
417}
418
419#[cfg(any(feature = "zstd", feature = "zstd-pure"))]
425fn build_zstd_compressor(
426 #[allow(unused_variables)] backend: CompressionBackend,
427 level: i32,
428 #[allow(unused_variables)] nb_workers: u32,
429) -> Box<dyn Compressor> {
430 #[cfg(all(feature = "zstd", feature = "zstd-pure"))]
431 if matches!(backend, CompressionBackend::Pure) {
432 return Box::new(ZstdPureCompressor { level });
433 }
434
435 #[cfg(feature = "zstd")]
436 {
437 Box::new(ZstdCompressor { level, nb_workers })
438 }
439
440 #[cfg(all(feature = "zstd-pure", not(feature = "zstd")))]
441 {
442 Box::new(ZstdPureCompressor { level })
443 }
444}
445
446fn build_compressor(
458 compression: &CompressionType,
459 #[allow(unused_variables)] config: &PipelineConfig,
460) -> Result<Option<Box<dyn Compressor>>, PipelineError> {
461 match compression {
462 CompressionType::None => Ok(None),
463 #[cfg(any(feature = "szip", feature = "szip-pure"))]
464 CompressionType::Szip {
465 rsi,
466 block_size,
467 flags,
468 bits_per_sample,
469 } => {
470 let backend = resolve_compression_backend(config.compression_backend)?;
471 let mut szip_flags = *flags;
472 if matches!(config.encoding, EncodingType::SimplePacking(_)) {
476 szip_flags |= 4; }
478
479 let compressor: Box<dyn Compressor> =
483 build_szip_compressor(backend, *rsi, *block_size, szip_flags, *bits_per_sample);
484 Ok(Some(compressor))
485 }
486 #[cfg(any(feature = "zstd", feature = "zstd-pure"))]
487 CompressionType::Zstd { level } => {
488 let backend = resolve_compression_backend(config.compression_backend)?;
489 let compressor: Box<dyn Compressor> =
490 build_zstd_compressor(backend, *level, config.intra_codec_threads);
491 Ok(Some(compressor))
492 }
493 #[cfg(feature = "lz4")]
494 CompressionType::Lz4 => Ok(Some(Box::new(Lz4Compressor))),
495 #[cfg(feature = "blosc2")]
496 CompressionType::Blosc2 {
497 codec,
498 clevel,
499 typesize,
500 } => Ok(Some(Box::new(Blosc2Compressor {
501 codec: *codec,
502 clevel: *clevel,
503 typesize: *typesize,
504 nthreads: config.intra_codec_threads,
505 }))),
506 #[cfg(feature = "zfp")]
507 CompressionType::Zfp { mode } => Ok(Some(Box::new(ZfpCompressor {
508 mode: mode.clone(),
509 num_values: config.num_values,
510 byte_order: config.byte_order,
511 }))),
512 #[cfg(feature = "sz3")]
513 CompressionType::Sz3 { error_bound } => Ok(Some(Box::new(Sz3Compressor {
514 error_bound: error_bound.clone(),
515 num_values: config.num_values,
516 byte_order: config.byte_order,
517 }))),
518 CompressionType::Rle => Ok(Some(Box::new(crate::compression::RleCompressor))),
519 CompressionType::Roaring => Ok(Some(Box::new(crate::compression::RoaringCompressor))),
520 }
521}
522
523#[inline]
537fn copy_and_hash(src: &[u8], hasher: Option<&mut xxhash_rust::xxh3::Xxh3Default>) -> Vec<u8> {
538 match hasher {
539 None => src.to_vec(),
540 Some(h) => {
541 const CHUNK: usize = 64 * 1024;
542 let mut dst = Vec::with_capacity(src.len());
543 let mut offset = 0;
544 while offset < src.len() {
545 let end = (offset + CHUNK).min(src.len());
546 let chunk = &src[offset..end];
547 h.update(chunk);
548 dst.extend_from_slice(chunk);
549 offset = end;
550 }
551 dst
552 }
553 }
554}
555
556#[inline]
560fn update_hasher(bytes: &[u8], hasher: Option<&mut xxhash_rust::xxh3::Xxh3Default>) {
561 if let Some(h) = hasher {
562 h.update(bytes);
563 }
564}
565
566#[tracing::instrument(skip(data, config), fields(data_len = data.len(), encoding = %config.encoding))]
577pub fn encode_pipeline(
578 data: &[u8],
579 config: &PipelineConfig,
580) -> Result<PipelineResult, PipelineError> {
581 let mut hasher = config
582 .compute_hash
583 .then(xxhash_rust::xxh3::Xxh3Default::new);
584
585 let encoded: Cow<'_, [u8]> = match &config.encoding {
587 EncodingType::None => Cow::Borrowed(data),
588 EncodingType::SimplePacking(params) => {
589 let values = bytes_to_f64(data, config.byte_order)?;
590 Cow::Owned(simple_packing::encode_with_threads(
591 &values,
592 params,
593 config.intra_codec_threads,
594 )?)
595 }
596 };
597
598 let filtered: Cow<'_, [u8]> = match &config.filter {
600 FilterType::None => encoded,
601 FilterType::Shuffle { element_size } => Cow::Owned(
602 shuffle::shuffle_with_threads(&encoded, *element_size, config.intra_codec_threads)
603 .map_err(|e| PipelineError::Shuffle(e.to_string()))?,
604 ),
605 };
606
607 let compressor = build_compressor(&config.compression, config)?;
609 let (encoded_bytes, block_offsets) = match compressor {
610 None => {
611 let owned = match filtered {
616 Cow::Borrowed(src) => copy_and_hash(src, hasher.as_mut()),
617 Cow::Owned(buf) => {
618 update_hasher(&buf, hasher.as_mut());
619 buf
620 }
621 };
622 (owned, None)
623 }
624 Some(compressor) => {
625 let CompressResult {
626 data: compressed,
627 block_offsets,
628 } = compressor.compress(&filtered)?;
629 update_hasher(&compressed, hasher.as_mut());
630 (compressed, block_offsets)
631 }
632 };
633
634 Ok(PipelineResult {
635 encoded_bytes,
636 block_offsets,
637 hash: hasher.map(|h| h.digest()),
638 })
639}
640
641#[tracing::instrument(skip(values, config), fields(num_values = values.len(), encoding = %config.encoding))]
647pub fn encode_pipeline_f64(
648 values: &[f64],
649 config: &PipelineConfig,
650) -> Result<PipelineResult, PipelineError> {
651 let mut hasher = config
652 .compute_hash
653 .then(xxhash_rust::xxh3::Xxh3Default::new);
654
655 let encoded: Cow<'_, [u8]> = match &config.encoding {
656 EncodingType::None => Cow::Owned(f64_to_bytes(values, config.byte_order)?),
657 EncodingType::SimplePacking(params) => Cow::Owned(simple_packing::encode_with_threads(
658 values,
659 params,
660 config.intra_codec_threads,
661 )?),
662 };
663
664 let filtered: Cow<'_, [u8]> = match &config.filter {
665 FilterType::None => encoded,
666 FilterType::Shuffle { element_size } => Cow::Owned(
667 shuffle::shuffle_with_threads(&encoded, *element_size, config.intra_codec_threads)
668 .map_err(|e| PipelineError::Shuffle(e.to_string()))?,
669 ),
670 };
671
672 let compressor = build_compressor(&config.compression, config)?;
673 let (encoded_bytes, block_offsets) = match compressor {
674 None => {
675 let owned = filtered.into_owned();
679 update_hasher(&owned, hasher.as_mut());
680 (owned, None)
681 }
682 Some(compressor) => {
683 let CompressResult {
684 data: compressed,
685 block_offsets,
686 } = compressor.compress(&filtered)?;
687 update_hasher(&compressed, hasher.as_mut());
688 (compressed, block_offsets)
689 }
690 };
691
692 Ok(PipelineResult {
693 encoded_bytes,
694 block_offsets,
695 hash: hasher.map(|h| h.digest()),
696 })
697}
698
699#[tracing::instrument(skip(encoded, config), fields(encoded_len = encoded.len()))]
706pub fn decode_pipeline(
707 encoded: &[u8],
708 config: &PipelineConfig,
709 native_byte_order: bool,
710) -> Result<Vec<u8>, PipelineError> {
711 validate_descriptor_size(encoded.len(), config)?;
716
717 let decompressed: Cow<'_, [u8]> = match build_compressor(&config.compression, config)? {
719 None => Cow::Borrowed(encoded),
720 Some(compressor) => {
721 let expected_size = estimate_decompressed_size(config);
722 Cow::Owned(compressor.decompress(encoded, expected_size)?)
723 }
724 };
725
726 let unfiltered: Cow<'_, [u8]> = match &config.filter {
728 FilterType::None => decompressed,
729 FilterType::Shuffle { element_size } => Cow::Owned(
730 shuffle::unshuffle_with_threads(
731 &decompressed,
732 *element_size,
733 config.intra_codec_threads,
734 )
735 .map_err(|e| PipelineError::Shuffle(e.to_string()))?,
736 ),
737 };
738
739 let target_byte_order = if native_byte_order {
743 ByteOrder::native()
744 } else {
745 config.byte_order
746 };
747
748 let mut decoded = match &config.encoding {
750 EncodingType::None => unfiltered.into_owned(),
751 EncodingType::SimplePacking(params) => {
752 let values = simple_packing::decode_with_threads(
755 &unfiltered,
756 config.num_values,
757 params,
758 config.intra_codec_threads,
759 )?;
760 f64_to_bytes(&values, target_byte_order)?
761 }
762 };
763
764 if native_byte_order
767 && matches!(config.encoding, EncodingType::None)
768 && config.byte_order != ByteOrder::native()
769 {
770 byteswap(&mut decoded, config.swap_unit_size)?;
771 }
772
773 Ok(decoded)
774}
775
776pub fn decode_range_pipeline(
787 encoded: &[u8],
788 config: &PipelineConfig,
789 block_offsets: &[u64],
790 sample_offset: u64,
791 sample_count: u64,
792 native_byte_order: bool,
793) -> Result<Vec<u8>, PipelineError> {
794 if matches!(config.filter, FilterType::Shuffle { .. }) {
795 return Err(PipelineError::Shuffle(
796 "partial range decode is not supported with shuffle filter".to_string(),
797 ));
798 }
799
800 validate_descriptor_size(encoded.len(), config)?;
807
808 let (byte_start, byte_size, bit_offset_in_chunk) = match &config.encoding {
810 EncodingType::SimplePacking(params) => {
811 let bpv = params.bits_per_value as u128;
814 let bit_start_u128 = (sample_offset as u128)
815 .checked_mul(bpv)
816 .ok_or_else(|| PipelineError::Range("bit start overflow".to_string()))?;
817 let bit_count_u128 = (sample_count as u128)
818 .checked_mul(bpv)
819 .ok_or_else(|| PipelineError::Range("bit count overflow".to_string()))?;
820 let bit_end_u128 = bit_start_u128
821 .checked_add(bit_count_u128)
822 .ok_or_else(|| PipelineError::Range("bit end overflow".to_string()))?;
823 let bs = usize::try_from(bit_start_u128 / 8)
824 .map_err(|_| PipelineError::Range("byte start exceeds usize".to_string()))?;
825 let be = usize::try_from(bit_end_u128.div_ceil(8))
826 .map_err(|_| PipelineError::Range("byte end exceeds usize".to_string()))?;
827 let size = be
828 .checked_sub(bs)
829 .ok_or_else(|| PipelineError::Range("byte range invariant violated".to_string()))?;
830 (bs, size, Some((bit_start_u128 % 8) as usize))
831 }
832 EncodingType::None => {
833 let elem_size = config.dtype_byte_width;
834 let offset_usize = usize::try_from(sample_offset).map_err(|_| {
838 PipelineError::Range(format!(
839 "sample_offset {sample_offset} exceeds usize on this target"
840 ))
841 })?;
842 let count_usize = usize::try_from(sample_count).map_err(|_| {
843 PipelineError::Range(format!(
844 "sample_count {sample_count} exceeds usize on this target"
845 ))
846 })?;
847 let bs = offset_usize
848 .checked_mul(elem_size)
849 .ok_or_else(|| PipelineError::Range("byte offset overflow".to_string()))?;
850 let sz = count_usize
851 .checked_mul(elem_size)
852 .ok_or_else(|| PipelineError::Range("byte count overflow".to_string()))?;
853 (bs, sz, None)
854 }
855 };
856
857 let decompressed = match build_compressor(&config.compression, config)? {
859 None => {
860 let byte_end = byte_start
862 .checked_add(byte_size)
863 .ok_or_else(|| PipelineError::Range("byte end overflow".to_string()))?;
864 if byte_end > encoded.len() {
865 return Err(PipelineError::Range(format!(
866 "range ({sample_offset}, {sample_count}) exceeds payload size"
867 )));
868 }
869 try_clone_bytes(&encoded[byte_start..byte_end])?
870 }
871 Some(compressor) => {
872 compressor.decompress_range(encoded, block_offsets, byte_start, byte_size)?
873 }
874 };
875
876 let target_byte_order = if native_byte_order {
877 ByteOrder::native()
878 } else {
879 config.byte_order
880 };
881
882 match &config.encoding {
884 EncodingType::None => {
885 let mut result = decompressed;
886 if native_byte_order && config.byte_order != ByteOrder::native() {
887 byteswap(&mut result, config.swap_unit_size)?;
888 }
889 Ok(result)
890 }
891 EncodingType::SimplePacking(params) => {
892 let count_usize = usize::try_from(sample_count).map_err(|_| {
893 PipelineError::Range(format!(
894 "sample_count {sample_count} exceeds usize on this target"
895 ))
896 })?;
897 let values = simple_packing::decode_range(
898 &decompressed,
899 bit_offset_in_chunk.unwrap_or(0),
900 count_usize,
901 params,
902 )?;
903 f64_to_bytes(&values, target_byte_order)
904 }
905 }
906}
907
908fn exact_decoded_bytes(config: &PipelineConfig) -> u128 {
923 match &config.encoding {
924 EncodingType::None => {
925 if config.dtype_byte_width == 0 {
929 (config.num_values as u128).div_ceil(8)
930 } else {
931 (config.num_values as u128) * (config.dtype_byte_width as u128)
932 }
933 }
934 EncodingType::SimplePacking(params) => {
935 let total_bits = (config.num_values as u128) * (params.bits_per_value as u128);
936 total_bits.div_ceil(8)
937 }
938 }
939}
940
941fn estimate_decompressed_size(config: &PipelineConfig) -> usize {
942 exact_decoded_bytes(config).min(usize::MAX as u128) as usize
947}
948
949fn validate_descriptor_size(
967 payload_len: usize,
968 config: &PipelineConfig,
969) -> Result<(), PipelineError> {
970 if !matches!(config.compression, CompressionType::None)
973 || !matches!(config.filter, FilterType::None)
974 {
975 return Ok(());
976 }
977
978 let claimed = exact_decoded_bytes(config);
979 if claimed != payload_len as u128 {
980 return Err(PipelineError::DescriptorSizeMismatch {
981 claimed_bytes: claimed,
982 payload_bytes: payload_len,
983 encoding: config.encoding.to_string(),
984 dtype_byte_width: config.dtype_byte_width,
985 });
986 }
987 Ok(())
988}
989
990pub(crate) fn try_clone_bytes(src: &[u8]) -> Result<Vec<u8>, PipelineError> {
991 let mut out: Vec<u8> = Vec::new();
992 out.try_reserve_exact(src.len()).map_err(|e| {
993 PipelineError::Range(format!(
994 "failed to reserve {} bytes for range output clone: {e}",
995 src.len()
996 ))
997 })?;
998 out.extend_from_slice(src);
999 Ok(out)
1000}
1001
1002fn bytes_to_f64(data: &[u8], byte_order: ByteOrder) -> Result<Vec<f64>, PipelineError> {
1003 if !data.len().is_multiple_of(8) {
1008 return Err(PipelineError::Range(format!(
1009 "byte-to-f64 input length {} is not a multiple of 8",
1010 data.len()
1011 )));
1012 }
1013 let n = data.len() / 8;
1014 let mut out: Vec<f64> = Vec::new();
1015 out.try_reserve_exact(n).map_err(|e| {
1016 PipelineError::Range(format!(
1017 "failed to reserve {} bytes for byte-to-f64 conversion: {e}",
1018 n.saturating_mul(std::mem::size_of::<f64>()),
1019 ))
1020 })?;
1021 for chunk in data.chunks_exact(8) {
1022 let mut arr = [0u8; 8];
1023 arr.copy_from_slice(chunk);
1024 out.push(match byte_order {
1025 ByteOrder::Big => f64::from_be_bytes(arr),
1026 ByteOrder::Little => f64::from_le_bytes(arr),
1027 });
1028 }
1029 Ok(out)
1030}
1031
1032fn f64_to_bytes(values: &[f64], byte_order: ByteOrder) -> Result<Vec<u8>, PipelineError> {
1033 let bytes_len = values.len().checked_mul(8).ok_or_else(|| {
1034 PipelineError::Range(format!(
1035 "f64-to-byte output length overflows usize: {} values x 8 bytes",
1036 values.len()
1037 ))
1038 })?;
1039 let mut out: Vec<u8> = Vec::new();
1040 out.try_reserve_exact(bytes_len).map_err(|e| {
1041 PipelineError::Range(format!(
1042 "failed to reserve {bytes_len} bytes for f64-to-byte conversion: {e}"
1043 ))
1044 })?;
1045 for v in values {
1046 out.extend_from_slice(&match byte_order {
1047 ByteOrder::Big => v.to_be_bytes(),
1048 ByteOrder::Little => v.to_le_bytes(),
1049 });
1050 }
1051 Ok(out)
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056 use super::*;
1057
1058 #[test]
1061 fn parse_backend_accepts_ffi_pure_aliases() {
1062 for (raw, expected) in [
1063 ("ffi", CompressionBackend::Ffi),
1064 ("FFI", CompressionBackend::Ffi),
1065 ("c", CompressionBackend::Ffi),
1066 ("pure", CompressionBackend::Pure),
1067 ("Pure", CompressionBackend::Pure),
1068 ("rust", CompressionBackend::Pure),
1069 (" ffi ", CompressionBackend::Ffi),
1070 ] {
1071 assert_eq!(
1072 parse_backend(raw).unwrap(),
1073 expected,
1074 "backend {raw:?} should parse as {expected:?}"
1075 );
1076 }
1077 }
1078
1079 #[test]
1080 fn parse_backend_rejects_unknown_value() {
1081 let err = parse_backend("ffix").unwrap_err();
1087 assert!(err.contains("TENSOGRAM_COMPRESSION_BACKEND"));
1088 assert!(err.contains("ffix"));
1089 assert!(err.contains("'ffi' or 'pure'"));
1090 }
1091
1092 #[test]
1093 fn parse_backend_rejects_empty_string() {
1094 let err = parse_backend("").unwrap_err();
1095 assert!(err.contains("TENSOGRAM_COMPRESSION_BACKEND"));
1096 }
1097
1098 #[test]
1099 fn parse_backend_rejects_whitespace_only() {
1100 let err = parse_backend(" ").unwrap_err();
1101 assert!(err.contains("TENSOGRAM_COMPRESSION_BACKEND"));
1102 }
1103
1104 #[test]
1105 fn resolve_compression_backend_passes_through_explicit() {
1106 assert_eq!(
1108 resolve_compression_backend(CompressionBackend::Ffi).unwrap(),
1109 CompressionBackend::Ffi
1110 );
1111 assert_eq!(
1112 resolve_compression_backend(CompressionBackend::Pure).unwrap(),
1113 CompressionBackend::Pure
1114 );
1115 }
1116
1117 #[test]
1118 fn compression_backend_default_is_auto() {
1119 assert_eq!(CompressionBackend::default(), CompressionBackend::Auto);
1123 }
1124
1125 #[test]
1126 fn test_passthrough_pipeline() {
1127 let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
1128 let config = PipelineConfig {
1129 encoding: EncodingType::None,
1130 filter: FilterType::None,
1131 compression: CompressionType::None,
1132 num_values: 1,
1133 byte_order: ByteOrder::Little,
1134 dtype_byte_width: 8,
1135 swap_unit_size: 8,
1136 compression_backend: CompressionBackend::default(),
1137 intra_codec_threads: 0,
1138 compute_hash: false,
1139 };
1140 let result = encode_pipeline(&data, &config).unwrap();
1141 assert_eq!(result.encoded_bytes, data);
1142 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
1143 assert_eq!(decoded, data);
1144 }
1145
1146 #[test]
1147 fn test_simple_packing_pipeline() {
1148 let values: Vec<f64> = (0..50).map(|i| 200.0 + i as f64 * 0.1).collect();
1149 let data: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1150 let params = simple_packing::compute_params(&values, 16, 0).unwrap();
1151
1152 let config = PipelineConfig {
1153 encoding: EncodingType::SimplePacking(params),
1154 filter: FilterType::None,
1155 compression: CompressionType::None,
1156 num_values: values.len(),
1157 byte_order: ByteOrder::Little,
1158 dtype_byte_width: 8,
1159 swap_unit_size: 8,
1160 compression_backend: CompressionBackend::default(),
1161 intra_codec_threads: 0,
1162 compute_hash: false,
1163 };
1164
1165 let result = encode_pipeline(&data, &config).unwrap();
1166 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
1167 let decoded_values = bytes_to_f64(&decoded, ByteOrder::Little).unwrap();
1168
1169 for (orig, dec) in values.iter().zip(decoded_values.iter()) {
1170 assert!((orig - dec).abs() < 0.01, "orig={orig}, dec={dec}");
1171 }
1172 }
1173
1174 #[test]
1175 fn test_shuffle_pipeline() {
1176 let data: Vec<u8> = (0..16).collect();
1177 let config = PipelineConfig {
1178 encoding: EncodingType::None,
1179 filter: FilterType::Shuffle { element_size: 4 },
1180 compression: CompressionType::None,
1181 num_values: 4,
1182 byte_order: ByteOrder::Little,
1183 dtype_byte_width: 4,
1184 swap_unit_size: 4,
1185 compression_backend: CompressionBackend::default(),
1186 intra_codec_threads: 0,
1187 compute_hash: false,
1188 };
1189
1190 let result = encode_pipeline(&data, &config).unwrap();
1191 assert_ne!(result.encoded_bytes, data); let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
1193 assert_eq!(decoded, data);
1194 }
1195
1196 #[cfg(any(feature = "szip", feature = "szip-pure"))]
1197 #[test]
1198 fn test_szip_round_trip_pipeline() {
1199 let data: Vec<u8> = (0..2048).map(|i| (i % 256) as u8).collect();
1200
1201 let preprocess_flag = 8u32;
1203
1204 let config = PipelineConfig {
1205 encoding: EncodingType::None,
1206 filter: FilterType::None,
1207 compression: CompressionType::Szip {
1208 rsi: 128,
1209 block_size: 16,
1210 flags: preprocess_flag,
1211 bits_per_sample: 8,
1212 },
1213 num_values: 2048,
1214 byte_order: ByteOrder::Little,
1215 dtype_byte_width: 1,
1216 swap_unit_size: 1,
1217 compression_backend: CompressionBackend::default(),
1218 intra_codec_threads: 0,
1219 compute_hash: false,
1220 };
1221
1222 let result = encode_pipeline(&data, &config).unwrap();
1223 assert!(result.block_offsets.is_some());
1224
1225 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
1226 assert_eq!(decoded, data);
1227 }
1228
1229 #[test]
1234 fn test_byteswap_noop_for_single_byte() {
1235 let mut data = vec![1, 2, 3, 4];
1236 let original = data.clone();
1237 byteswap(&mut data, 1).unwrap();
1238 assert_eq!(data, original);
1239 byteswap(&mut data, 0).unwrap();
1240 assert_eq!(data, original);
1241 }
1242
1243 #[test]
1244 fn test_byteswap_2_bytes() {
1245 let mut data = vec![0xAA, 0xBB, 0xCC, 0xDD];
1246 byteswap(&mut data, 2).unwrap();
1247 assert_eq!(data, vec![0xBB, 0xAA, 0xDD, 0xCC]);
1248 }
1249
1250 #[test]
1251 fn test_byteswap_4_bytes() {
1252 let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8];
1253 byteswap(&mut data, 4).unwrap();
1254 assert_eq!(data, vec![4, 3, 2, 1, 8, 7, 6, 5]);
1255 }
1256
1257 #[test]
1258 fn test_byteswap_8_bytes() {
1259 let mut data: Vec<u8> = (1..=16).collect();
1260 byteswap(&mut data, 8).unwrap();
1261 assert_eq!(
1262 data,
1263 vec![8, 7, 6, 5, 4, 3, 2, 1, 16, 15, 14, 13, 12, 11, 10, 9]
1264 );
1265 }
1266
1267 #[test]
1268 fn test_byteswap_round_trip() {
1269 let original = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
1270 let mut data = original.clone();
1271 byteswap(&mut data, 4).unwrap();
1272 assert_ne!(data, original);
1273 byteswap(&mut data, 4).unwrap();
1274 assert_eq!(data, original);
1275 }
1276
1277 #[test]
1278 fn test_byteswap_misaligned_returns_error() {
1279 let mut data = vec![1, 2, 3, 4, 5]; let result = byteswap(&mut data, 4);
1281 assert!(result.is_err());
1282 }
1283
1284 #[test]
1289 fn test_decode_native_byte_order_encoding_none() {
1290 let value: f32 = 42.0;
1292 let be_bytes = value.to_be_bytes();
1293 let config = PipelineConfig {
1294 encoding: EncodingType::None,
1295 filter: FilterType::None,
1296 compression: CompressionType::None,
1297 num_values: 1,
1298 byte_order: ByteOrder::Big,
1299 dtype_byte_width: 4,
1300 swap_unit_size: 4,
1301 compression_backend: CompressionBackend::default(),
1302 intra_codec_threads: 0,
1303 compute_hash: false,
1304 };
1305
1306 let result = encode_pipeline(&be_bytes, &config).unwrap();
1307
1308 let native_decoded = decode_pipeline(&result.encoded_bytes, &config, true).unwrap();
1310 let ne_value = f32::from_ne_bytes(native_decoded[..4].try_into().unwrap());
1311 assert_eq!(ne_value, value);
1312
1313 let wire_decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
1315 let be_value = f32::from_be_bytes(wire_decoded[..4].try_into().unwrap());
1316 assert_eq!(be_value, value);
1317 }
1318
1319 #[test]
1320 fn test_decode_native_byte_order_simple_packing() {
1321 let values: Vec<f64> = vec![100.0, 200.0, 300.0, 400.0];
1322 let data: Vec<u8> = values.iter().flat_map(|v| v.to_be_bytes()).collect();
1324 let params = simple_packing::compute_params(&values, 24, 0).unwrap();
1325
1326 let config = PipelineConfig {
1327 encoding: EncodingType::SimplePacking(params),
1328 filter: FilterType::None,
1329 compression: CompressionType::None,
1330 num_values: values.len(),
1331 byte_order: ByteOrder::Big,
1332 dtype_byte_width: 8,
1333 swap_unit_size: 8,
1334 compression_backend: CompressionBackend::default(),
1335 intra_codec_threads: 0,
1336 compute_hash: false,
1337 };
1338
1339 let result = encode_pipeline(&data, &config).unwrap();
1340
1341 let native_decoded = decode_pipeline(&result.encoded_bytes, &config, true).unwrap();
1343 let decoded_values: Vec<f64> = native_decoded
1344 .chunks_exact(8)
1345 .map(|c| f64::from_ne_bytes(c.try_into().unwrap()))
1346 .collect();
1347 for (orig, dec) in values.iter().zip(decoded_values.iter()) {
1348 assert!((orig - dec).abs() < 1.0, "orig={orig}, dec={dec}");
1349 }
1350
1351 let wire_decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
1353 let wire_values: Vec<f64> = wire_decoded
1354 .chunks_exact(8)
1355 .map(|c| f64::from_be_bytes(c.try_into().unwrap()))
1356 .collect();
1357 for (orig, dec) in values.iter().zip(wire_values.iter()) {
1358 assert!((orig - dec).abs() < 1.0, "orig={orig}, dec={dec}");
1359 }
1360 }
1361
1362 #[test]
1363 fn test_native_byte_order_same_as_wire_is_noop() {
1364 let values: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
1367 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
1368
1369 let config = PipelineConfig {
1370 encoding: EncodingType::None,
1371 filter: FilterType::None,
1372 compression: CompressionType::None,
1373 num_values: values.len(),
1374 byte_order: ByteOrder::native(),
1375 dtype_byte_width: 4,
1376 swap_unit_size: 4,
1377 compression_backend: CompressionBackend::default(),
1378 intra_codec_threads: 0,
1379 compute_hash: false,
1380 };
1381
1382 let result = encode_pipeline(&data, &config).unwrap();
1383 let native_decoded = decode_pipeline(&result.encoded_bytes, &config, true).unwrap();
1384 let wire_decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
1385 assert_eq!(native_decoded, wire_decoded);
1386 }
1387
1388 #[test]
1389 fn test_decode_native_byte_order_2byte_dtype() {
1390 let value: u16 = 0x0102;
1392 let be_bytes = value.to_be_bytes();
1393 let config = PipelineConfig {
1394 encoding: EncodingType::None,
1395 filter: FilterType::None,
1396 compression: CompressionType::None,
1397 num_values: 1,
1398 byte_order: ByteOrder::Big,
1399 dtype_byte_width: 2,
1400 swap_unit_size: 2,
1401 compression_backend: CompressionBackend::default(),
1402 intra_codec_threads: 0,
1403 compute_hash: false,
1404 };
1405 let result = encode_pipeline(&be_bytes, &config).unwrap();
1406 let native = decode_pipeline(&result.encoded_bytes, &config, true).unwrap();
1407 assert_eq!(u16::from_ne_bytes(native[..2].try_into().unwrap()), value);
1408 }
1409
1410 #[test]
1411 fn test_decode_native_byte_order_8byte_dtype() {
1412 let value: f64 = std::f64::consts::E;
1414 let be_bytes = value.to_be_bytes();
1415 let config = PipelineConfig {
1416 encoding: EncodingType::None,
1417 filter: FilterType::None,
1418 compression: CompressionType::None,
1419 num_values: 1,
1420 byte_order: ByteOrder::Big,
1421 dtype_byte_width: 8,
1422 swap_unit_size: 8,
1423 compression_backend: CompressionBackend::default(),
1424 intra_codec_threads: 0,
1425 compute_hash: false,
1426 };
1427 let result = encode_pipeline(&be_bytes, &config).unwrap();
1428 let native = decode_pipeline(&result.encoded_bytes, &config, true).unwrap();
1429 assert_eq!(f64::from_ne_bytes(native[..8].try_into().unwrap()), value);
1430 }
1431
1432 #[test]
1433 fn test_decode_native_byte_order_complex64() {
1434 let real: f32 = 1.5;
1437 let imag: f32 = 2.5;
1438 let mut be_bytes = Vec::new();
1439 be_bytes.extend_from_slice(&real.to_be_bytes());
1440 be_bytes.extend_from_slice(&imag.to_be_bytes());
1441 let config = PipelineConfig {
1442 encoding: EncodingType::None,
1443 filter: FilterType::None,
1444 compression: CompressionType::None,
1445 num_values: 1,
1446 byte_order: ByteOrder::Big,
1447 dtype_byte_width: 8,
1448 swap_unit_size: 4, compression_backend: CompressionBackend::default(),
1450 intra_codec_threads: 0,
1451 compute_hash: false,
1452 };
1453 let result = encode_pipeline(&be_bytes, &config).unwrap();
1454 let native = decode_pipeline(&result.encoded_bytes, &config, true).unwrap();
1455 let decoded_real = f32::from_ne_bytes(native[0..4].try_into().unwrap());
1456 let decoded_imag = f32::from_ne_bytes(native[4..8].try_into().unwrap());
1457 assert_eq!(decoded_real, real);
1458 assert_eq!(decoded_imag, imag);
1459 }
1460
1461 #[test]
1462 fn test_decode_native_byte_order_uint8_noop() {
1463 let data = vec![1u8, 2, 3, 4, 5];
1465 let config = PipelineConfig {
1466 encoding: EncodingType::None,
1467 filter: FilterType::None,
1468 compression: CompressionType::None,
1469 num_values: 5,
1470 byte_order: ByteOrder::Big, dtype_byte_width: 1,
1472 swap_unit_size: 1,
1473 compression_backend: CompressionBackend::default(),
1474 intra_codec_threads: 0,
1475 compute_hash: false,
1476 };
1477 let result = encode_pipeline(&data, &config).unwrap();
1478 let native = decode_pipeline(&result.encoded_bytes, &config, true).unwrap();
1479 assert_eq!(native, data); }
1481
1482 fn passthrough_config(num_values: usize, compute_hash: bool) -> PipelineConfig {
1490 PipelineConfig {
1491 encoding: EncodingType::None,
1492 filter: FilterType::None,
1493 compression: CompressionType::None,
1494 num_values,
1495 byte_order: ByteOrder::Little,
1496 dtype_byte_width: 1,
1497 swap_unit_size: 1,
1498 compression_backend: CompressionBackend::default(),
1499 intra_codec_threads: 0,
1500 compute_hash,
1501 }
1502 }
1503
1504 #[test]
1505 fn streaming_and_oneshot_xxh3_agree() {
1506 use xxhash_rust::xxh3::{Xxh3Default, xxh3_64};
1512
1513 for size in [0usize, 1, 239, 240, 1024 * 1024 + 17] {
1514 let data: Vec<u8> = (0..size).map(|i| (i * 31 + 7) as u8).collect();
1515
1516 let one_shot = xxh3_64(&data);
1518
1519 let mut h = Xxh3Default::new();
1521 for chunk in data.chunks(64 * 1024) {
1522 h.update(chunk);
1523 }
1524 assert_eq!(h.digest(), one_shot, "streaming vs one-shot at size {size}");
1525
1526 let mut h = Xxh3Default::new();
1528 for chunk in data.chunks(1) {
1529 h.update(chunk);
1530 }
1531 assert_eq!(
1532 h.digest(),
1533 one_shot,
1534 "streaming 1-byte chunks vs one-shot at size {size}"
1535 );
1536 }
1537 }
1538
1539 #[test]
1540 fn pipeline_hash_none_when_disabled() {
1541 let data: Vec<u8> = (0..64).collect();
1542 let config = passthrough_config(data.len(), false);
1543 let result = encode_pipeline(&data, &config).unwrap();
1544 assert!(
1545 result.hash.is_none(),
1546 "compute_hash = false must leave PipelineResult.hash = None"
1547 );
1548 }
1549
1550 #[test]
1551 fn pipeline_hash_matches_post_hoc_for_passthrough() {
1552 use xxhash_rust::xxh3::xxh3_64;
1553
1554 for size in [0usize, 1, 64 * 1024 - 1, 64 * 1024, 64 * 1024 + 1, 250_000] {
1557 let data: Vec<u8> = (0..size).map(|i| (i as u32 ^ 0xA5A5A5A5) as u8).collect();
1558 let config = passthrough_config(size, true);
1559 let result = encode_pipeline(&data, &config).unwrap();
1560 let expected = xxh3_64(&result.encoded_bytes);
1561 assert_eq!(
1562 result.hash,
1563 Some(expected),
1564 "passthrough hash-while-encoding mismatch at size {size}"
1565 );
1566 assert_eq!(
1567 result.encoded_bytes, data,
1568 "passthrough must still produce identical bytes at size {size}"
1569 );
1570 }
1571 }
1572
1573 #[test]
1574 fn pipeline_hash_matches_post_hoc_for_simple_packing() {
1575 use xxhash_rust::xxh3::xxh3_64;
1576
1577 let values: Vec<f64> = (0..10_000).map(|i| 200.0 + i as f64 * 0.1).collect();
1578 let data: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1579 let params = simple_packing::compute_params(&values, 16, 0).unwrap();
1580
1581 let config = PipelineConfig {
1582 encoding: EncodingType::SimplePacking(params),
1583 filter: FilterType::None,
1584 compression: CompressionType::None,
1585 num_values: values.len(),
1586 byte_order: ByteOrder::Little,
1587 dtype_byte_width: 8,
1588 swap_unit_size: 8,
1589 compression_backend: CompressionBackend::default(),
1590 intra_codec_threads: 0,
1591 compute_hash: true,
1592 };
1593 let result = encode_pipeline(&data, &config).unwrap();
1594 let expected = xxh3_64(&result.encoded_bytes);
1595 assert_eq!(result.hash, Some(expected));
1596 }
1597
1598 #[cfg(feature = "lz4")]
1599 #[test]
1600 fn pipeline_hash_matches_post_hoc_for_lz4() {
1601 use xxhash_rust::xxh3::xxh3_64;
1602
1603 let data: Vec<u8> = (0..16_000).map(|i| (i % 257) as u8).collect();
1604 let config = PipelineConfig {
1605 encoding: EncodingType::None,
1606 filter: FilterType::None,
1607 compression: CompressionType::Lz4,
1608 num_values: data.len(),
1609 byte_order: ByteOrder::Little,
1610 dtype_byte_width: 1,
1611 swap_unit_size: 1,
1612 compression_backend: CompressionBackend::default(),
1613 intra_codec_threads: 0,
1614 compute_hash: true,
1615 };
1616 let result = encode_pipeline(&data, &config).unwrap();
1617 let expected = xxh3_64(&result.encoded_bytes);
1618 assert_eq!(result.hash, Some(expected));
1619 }
1620
1621 #[test]
1622 fn pipeline_f64_hash_matches_post_hoc() {
1623 use xxhash_rust::xxh3::xxh3_64;
1624
1625 let values: Vec<f64> = (0..1_000).map(|i| (i as f64).sqrt()).collect();
1626 let config = PipelineConfig {
1627 encoding: EncodingType::None,
1628 filter: FilterType::None,
1629 compression: CompressionType::None,
1630 num_values: values.len(),
1631 byte_order: ByteOrder::Little,
1632 dtype_byte_width: 8,
1633 swap_unit_size: 8,
1634 compression_backend: CompressionBackend::default(),
1635 intra_codec_threads: 0,
1636 compute_hash: true,
1637 };
1638 let result = encode_pipeline_f64(&values, &config).unwrap();
1639 let expected = xxh3_64(&result.encoded_bytes);
1640 assert_eq!(result.hash, Some(expected));
1641 }
1642
1643 #[test]
1644 fn pipeline_hash_byte_identical_across_threads_transparent() {
1645 let values: Vec<f64> = (0..50_000)
1649 .map(|i| 280.0 + (i as f64 * 0.001).sin())
1650 .collect();
1651 let data: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1652 let params = simple_packing::compute_params(&values, 24, 0).unwrap();
1653
1654 let mut hashes = Vec::new();
1655 for threads in [0u32, 1, 2, 4] {
1656 let config = PipelineConfig {
1657 encoding: EncodingType::SimplePacking(params.clone()),
1658 filter: FilterType::None,
1659 compression: CompressionType::None,
1660 num_values: values.len(),
1661 byte_order: ByteOrder::Little,
1662 dtype_byte_width: 8,
1663 swap_unit_size: 8,
1664 compression_backend: CompressionBackend::default(),
1665 intra_codec_threads: threads,
1666 compute_hash: true,
1667 };
1668 let result = encode_pipeline(&data, &config).unwrap();
1669 hashes.push(result.hash);
1670 }
1671 assert!(
1672 hashes.windows(2).all(|w| w[0] == w[1]),
1673 "transparent simple_packing must produce byte-identical hashes across thread counts: {hashes:?}"
1674 );
1675 }
1676
1677 #[cfg(any(feature = "szip", feature = "szip-pure"))]
1686 fn szip_hostile_config(num_values: usize, backend: CompressionBackend) -> PipelineConfig {
1687 PipelineConfig {
1688 encoding: EncodingType::None,
1689 filter: FilterType::None,
1690 compression: CompressionType::Szip {
1691 bits_per_sample: 8,
1692 block_size: 16,
1693 rsi: 64,
1694 flags: 0,
1695 },
1696 num_values,
1697 byte_order: ByteOrder::Little,
1698 dtype_byte_width: 1,
1699 swap_unit_size: 1,
1700 compression_backend: backend,
1701 intra_codec_threads: 0,
1702 compute_hash: false,
1703 }
1704 }
1705
1706 #[cfg(any(feature = "szip", feature = "szip-pure"))]
1707 fn small_szip_payload(backend: CompressionBackend) -> Vec<u8> {
1708 let data: Vec<u8> = (0..256).map(|i| i as u8).collect();
1712 let honest = PipelineConfig {
1713 num_values: data.len(),
1714 ..szip_hostile_config(data.len(), backend)
1715 };
1716 encode_pipeline(&data, &honest).unwrap().encoded_bytes
1717 }
1718
1719 #[cfg(feature = "szip")]
1720 #[test]
1721 fn pipeline_rejects_malicious_num_values_szip_ffi() {
1722 let payload = small_szip_payload(CompressionBackend::Ffi);
1723 let hostile = szip_hostile_config(usize::MAX, CompressionBackend::Ffi);
1724
1725 let err = decode_pipeline(&payload, &hostile, false)
1726 .expect_err("expected allocation failure, not success nor abort");
1727 let msg = format!("{err}");
1728 assert!(
1729 msg.contains("failed to reserve"),
1730 "error should report allocation failure, got: {msg}"
1731 );
1732 }
1733
1734 #[cfg(feature = "szip-pure")]
1735 #[test]
1736 fn pipeline_rejects_malicious_num_values_szip_pure() {
1737 let payload = small_szip_payload(CompressionBackend::Pure);
1738 let hostile = szip_hostile_config(usize::MAX, CompressionBackend::Pure);
1739
1740 let err = decode_pipeline(&payload, &hostile, false)
1741 .expect_err("expected allocation failure, not success nor abort");
1742 let msg = format!("{err}");
1743 assert!(
1744 msg.contains("failed to reserve"),
1745 "error should report allocation failure, got: {msg}"
1746 );
1747 }
1748
1749 #[test]
1750 fn pipeline_rejects_malicious_num_values_simple_packing() {
1751 let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1755 let data: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1756 let sp_params = simple_packing::compute_params(&values, 16, 0).unwrap();
1757
1758 let honest = PipelineConfig {
1759 encoding: EncodingType::SimplePacking(sp_params.clone()),
1760 filter: FilterType::None,
1761 compression: CompressionType::None,
1762 num_values: values.len(),
1763 byte_order: ByteOrder::Little,
1764 dtype_byte_width: 8,
1765 swap_unit_size: 8,
1766 compression_backend: CompressionBackend::default(),
1767 intra_codec_threads: 0,
1768 compute_hash: false,
1769 };
1770 let payload = encode_pipeline(&data, &honest).unwrap().encoded_bytes;
1771
1772 let hostile = PipelineConfig {
1773 num_values: usize::MAX,
1774 ..honest
1775 };
1776 let err = decode_pipeline(&payload, &hostile, false)
1777 .expect_err("pathological num_values on simple_packing must surface as an error");
1778 assert!(
1783 matches!(err, PipelineError::DescriptorSizeMismatch { .. }),
1784 "expected DescriptorSizeMismatch, got: {err}"
1785 );
1786 }
1787
1788 #[cfg(feature = "zfp")]
1789 #[test]
1790 fn pipeline_rejects_malicious_num_values_zfp() {
1791 let values: Vec<f64> = (0..64).map(|i| (i as f64) * 0.1).collect();
1792 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
1793
1794 let honest = PipelineConfig {
1795 encoding: EncodingType::None,
1796 filter: FilterType::None,
1797 compression: CompressionType::Zfp {
1798 mode: ZfpMode::FixedRate { rate: 16.0 },
1799 },
1800 num_values: values.len(),
1801 byte_order: ByteOrder::native(),
1802 dtype_byte_width: 8,
1803 swap_unit_size: 8,
1804 compression_backend: CompressionBackend::default(),
1805 intra_codec_threads: 0,
1806 compute_hash: false,
1807 };
1808 let payload = encode_pipeline(&data, &honest).unwrap().encoded_bytes;
1809
1810 let hostile = PipelineConfig {
1811 num_values: usize::MAX,
1812 ..honest
1813 };
1814 let err = decode_pipeline(&payload, &hostile, false)
1815 .expect_err("pathological num_values on zfp must surface as an error");
1816 let msg = format!("{err}");
1817 assert!(
1818 msg.contains("failed to reserve"),
1819 "error should report allocation failure, got: {msg}"
1820 );
1821 }
1822
1823 fn sizecheck_none_config(num_values: usize, dtype_byte_width: usize) -> PipelineConfig {
1833 PipelineConfig {
1834 encoding: EncodingType::None,
1835 filter: FilterType::None,
1836 compression: CompressionType::None,
1837 num_values,
1838 byte_order: ByteOrder::Little,
1839 dtype_byte_width,
1840 swap_unit_size: dtype_byte_width,
1841 compression_backend: CompressionBackend::default(),
1842 intra_codec_threads: 0,
1843 compute_hash: false,
1844 }
1845 }
1846
1847 #[test]
1848 fn descriptor_size_none_exact_match_succeeds() {
1849 let payload = vec![0u8; 16];
1851 let cfg = sizecheck_none_config(4, 4);
1852 let out = decode_pipeline(&payload, &cfg, false).expect("exact size must decode");
1853 assert_eq!(out.len(), 16);
1854 }
1855
1856 #[test]
1857 fn descriptor_size_none_too_small_payload_rejected() {
1858 let payload = vec![0u8; 15];
1860 let cfg = sizecheck_none_config(4, 4);
1861 let err =
1862 decode_pipeline(&payload, &cfg, false).expect_err("short payload must be rejected");
1863 match err {
1864 PipelineError::DescriptorSizeMismatch {
1865 claimed_bytes,
1866 payload_bytes,
1867 ..
1868 } => {
1869 assert_eq!(claimed_bytes, 16);
1870 assert_eq!(payload_bytes, 15);
1871 }
1872 other => panic!("expected DescriptorSizeMismatch, got: {other}"),
1873 }
1874 }
1875
1876 #[test]
1877 fn descriptor_size_none_too_large_payload_rejected() {
1878 let payload = vec![0u8; 17];
1881 let cfg = sizecheck_none_config(4, 4);
1882 let err =
1883 decode_pipeline(&payload, &cfg, false).expect_err("over-long payload must be rejected");
1884 assert!(
1885 matches!(
1886 err,
1887 PipelineError::DescriptorSizeMismatch {
1888 claimed_bytes: 16,
1889 payload_bytes: 17,
1890 ..
1891 }
1892 ),
1893 "expected 16-vs-17 mismatch, got: {err}"
1894 );
1895 }
1896
1897 #[test]
1898 fn descriptor_size_bitmask_uses_ceil_div_8() {
1899 let cfg_ok = sizecheck_none_config(17, 0);
1902 assert!(decode_pipeline(&[0u8; 3], &cfg_ok, false).is_ok());
1903 let err = decode_pipeline(&[0u8; 2], &cfg_ok, false)
1905 .expect_err("bitmask short payload must be rejected");
1906 assert!(
1907 matches!(
1908 err,
1909 PipelineError::DescriptorSizeMismatch {
1910 claimed_bytes: 3,
1911 payload_bytes: 2,
1912 ..
1913 }
1914 ),
1915 "expected 3-vs-2 mismatch, got: {err}"
1916 );
1917 }
1918
1919 #[test]
1920 fn descriptor_size_none_hostile_num_values_rejected_before_alloc() {
1921 let payload = vec![0u8; 8];
1925 let cfg = sizecheck_none_config(usize::MAX, 8);
1926 let err = decode_pipeline(&payload, &cfg, false)
1927 .expect_err("hostile num_values must be rejected");
1928 assert!(
1929 matches!(err, PipelineError::DescriptorSizeMismatch { .. }),
1930 "expected DescriptorSizeMismatch, got: {err}"
1931 );
1932 }
1933
1934 #[test]
1935 fn descriptor_size_simple_packing_exact_and_mismatch() {
1936 let values: Vec<f64> = (0..10).map(|i| i as f64).collect();
1938 let data: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1939 let params = simple_packing::compute_params(&values, 16, 0).unwrap();
1940 let cfg = PipelineConfig {
1941 encoding: EncodingType::SimplePacking(params),
1942 filter: FilterType::None,
1943 compression: CompressionType::None,
1944 num_values: values.len(),
1945 byte_order: ByteOrder::Little,
1946 dtype_byte_width: 8,
1947 swap_unit_size: 8,
1948 compression_backend: CompressionBackend::default(),
1949 intra_codec_threads: 0,
1950 compute_hash: false,
1951 };
1952 let payload = encode_pipeline(&data, &cfg).unwrap().encoded_bytes;
1953 assert_eq!(payload.len(), 20);
1955 assert!(decode_pipeline(&payload, &cfg, false).is_ok());
1957
1958 let mut over = payload.clone();
1962 over.push(0);
1963 let err = decode_pipeline(&over, &cfg, false)
1964 .expect_err("trailing-byte payload must be rejected");
1965 assert!(
1966 matches!(
1967 err,
1968 PipelineError::DescriptorSizeMismatch {
1969 claimed_bytes: 20,
1970 payload_bytes: 21,
1971 ..
1972 }
1973 ),
1974 "expected 20-vs-21 mismatch, got: {err}"
1975 );
1976 }
1977
1978 #[test]
1979 fn descriptor_size_gate_is_noop_for_compressed() {
1980 #[cfg(any(feature = "zstd", feature = "zstd-pure"))]
1986 {
1987 let data: Vec<u8> = vec![0u8; 64 * 1024];
1994 let cfg = PipelineConfig {
1995 encoding: EncodingType::None,
1996 filter: FilterType::None,
1997 compression: CompressionType::Zstd { level: 3 },
1998 num_values: data.len(),
1999 byte_order: ByteOrder::Little,
2000 dtype_byte_width: 1,
2001 swap_unit_size: 1,
2002 compression_backend: CompressionBackend::default(),
2003 intra_codec_threads: 0,
2004 compute_hash: false,
2005 };
2006 let payload = encode_pipeline(&data, &cfg).unwrap().encoded_bytes;
2007 assert!(
2012 payload.len() < data.len(),
2013 "64 KiB of zeros must compress smaller; got {} >= {}",
2014 payload.len(),
2015 data.len()
2016 );
2017 let out = decode_pipeline(&payload, &cfg, false).expect("compressed must decode");
2018 assert_eq!(out, data);
2019 }
2020 }
2021
2022 #[test]
2023 fn descriptor_size_gate_is_noop_for_shuffle_filter() {
2024 let values: Vec<f32> = (0..8).map(|i| i as f32).collect();
2028 let data: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
2029 let cfg = PipelineConfig {
2030 encoding: EncodingType::None,
2031 filter: FilterType::Shuffle { element_size: 4 },
2032 compression: CompressionType::None,
2033 num_values: values.len(),
2034 byte_order: ByteOrder::Little,
2035 dtype_byte_width: 4,
2036 swap_unit_size: 4,
2037 compression_backend: CompressionBackend::default(),
2038 intra_codec_threads: 0,
2039 compute_hash: false,
2040 };
2041 let payload = encode_pipeline(&data, &cfg).unwrap().encoded_bytes;
2042 let out = decode_pipeline(&payload, &cfg, false).expect("shuffle must decode");
2043 assert_eq!(out, data);
2044 }
2045
2046 #[test]
2047 fn descriptor_size_range_path_rejects_mismatch() {
2048 let cfg = sizecheck_none_config(100, 4);
2053 let payload = vec![0u8; 16];
2055 let err = decode_range_pipeline(&payload, &cfg, &[], 0, 2, false)
2056 .expect_err("range decode must reject a descriptor/payload size mismatch");
2057 assert!(
2058 matches!(
2059 err,
2060 PipelineError::DescriptorSizeMismatch {
2061 claimed_bytes: 400,
2062 payload_bytes: 16,
2063 ..
2064 }
2065 ),
2066 "expected 400-vs-16 mismatch from the range path, got: {err}"
2067 );
2068 }
2069
2070 #[test]
2071 fn descriptor_size_range_path_accepts_exact_match() {
2072 let cfg = sizecheck_none_config(4, 4);
2075 let payload: Vec<u8> = (0..16).collect();
2076 let out = decode_range_pipeline(&payload, &cfg, &[], 1, 2, false)
2077 .expect("exact-size range decode must succeed");
2078 assert_eq!(out, payload[4..12]);
2080 }
2081
2082 #[test]
2089 fn encode_pipeline_f64_simple_packing_round_trip() {
2090 let values: Vec<f64> = (0..64).map(|i| 250.0 + i as f64 * 0.25).collect();
2094 let params = simple_packing::compute_params(&values, 16, 0).unwrap();
2095 let config = PipelineConfig {
2096 encoding: EncodingType::SimplePacking(params),
2097 filter: FilterType::None,
2098 compression: CompressionType::None,
2099 num_values: values.len(),
2100 byte_order: ByteOrder::Little,
2101 dtype_byte_width: 8,
2102 swap_unit_size: 8,
2103 compression_backend: CompressionBackend::default(),
2104 intra_codec_threads: 0,
2105 compute_hash: false,
2106 };
2107 let result = encode_pipeline_f64(&values, &config).unwrap();
2108 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2109 let decoded_values = bytes_to_f64(&decoded, ByteOrder::Little).unwrap();
2110 for (orig, dec) in values.iter().zip(decoded_values.iter()) {
2111 assert!((orig - dec).abs() < 0.01, "orig={orig}, dec={dec}");
2112 }
2113 }
2114
2115 #[test]
2116 fn encode_pipeline_f64_shuffle_filter_round_trip() {
2117 let values: Vec<f64> = (0..16).map(|i| i as f64 * 1.5).collect();
2119 let config = PipelineConfig {
2120 encoding: EncodingType::None,
2121 filter: FilterType::Shuffle { element_size: 8 },
2122 compression: CompressionType::None,
2123 num_values: values.len(),
2124 byte_order: ByteOrder::Little,
2125 dtype_byte_width: 8,
2126 swap_unit_size: 8,
2127 compression_backend: CompressionBackend::default(),
2128 intra_codec_threads: 0,
2129 compute_hash: false,
2130 };
2131 let result = encode_pipeline_f64(&values, &config).unwrap();
2132 let plain: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
2134 assert_ne!(result.encoded_bytes, plain);
2135 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2136 assert_eq!(decoded, plain);
2137 }
2138
2139 #[cfg(feature = "lz4")]
2140 #[test]
2141 fn encode_pipeline_f64_with_compression() {
2142 let values: Vec<f64> = (0..1000).map(|i| (i as f64).sqrt()).collect();
2147 let config = PipelineConfig {
2148 encoding: EncodingType::None,
2149 filter: FilterType::None,
2150 compression: CompressionType::Lz4,
2151 num_values: values.len(),
2152 byte_order: ByteOrder::Little,
2153 dtype_byte_width: 8,
2154 swap_unit_size: 8,
2155 compression_backend: CompressionBackend::default(),
2156 intra_codec_threads: 0,
2157 compute_hash: false,
2158 };
2159 let result = encode_pipeline_f64(&values, &config).unwrap();
2160 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2161 let plain: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
2162 assert_eq!(decoded, plain);
2163 }
2164
2165 #[test]
2166 fn encode_pipeline_f64_passthrough_none_encoding() {
2167 let values: Vec<f64> = vec![1.0, -2.0, 3.5, 4.25];
2170 let config = PipelineConfig {
2171 encoding: EncodingType::None,
2172 filter: FilterType::None,
2173 compression: CompressionType::None,
2174 num_values: values.len(),
2175 byte_order: ByteOrder::Big,
2176 dtype_byte_width: 8,
2177 swap_unit_size: 8,
2178 compression_backend: CompressionBackend::default(),
2179 intra_codec_threads: 0,
2180 compute_hash: false,
2181 };
2182 let result = encode_pipeline_f64(&values, &config).unwrap();
2183 let expected: Vec<u8> = values.iter().flat_map(|v| v.to_be_bytes()).collect();
2184 assert_eq!(result.encoded_bytes, expected);
2185 }
2186
2187 #[test]
2193 fn decode_range_pipeline_rejects_shuffle_filter() {
2194 let cfg = PipelineConfig {
2198 encoding: EncodingType::None,
2199 filter: FilterType::Shuffle { element_size: 4 },
2200 compression: CompressionType::None,
2201 num_values: 4,
2202 byte_order: ByteOrder::Little,
2203 dtype_byte_width: 4,
2204 swap_unit_size: 4,
2205 compression_backend: CompressionBackend::default(),
2206 intra_codec_threads: 0,
2207 compute_hash: false,
2208 };
2209 let payload = vec![0u8; 16];
2210 let err = decode_range_pipeline(&payload, &cfg, &[], 0, 2, false)
2211 .expect_err("shuffle filter must be rejected by range decode");
2212 match err {
2213 PipelineError::Shuffle(msg) => {
2214 assert!(
2215 msg.contains("not supported"),
2216 "expected unsupported-shuffle message, got: {msg}"
2217 );
2218 }
2219 other => panic!("expected Shuffle error, got: {other}"),
2220 }
2221 }
2222
2223 #[test]
2224 fn decode_range_pipeline_simple_packing_uncompressed() {
2225 let values: Vec<f64> = (0..32).map(|i| i as f64).collect();
2229 let params = simple_packing::compute_params(&values, 16, 0).unwrap();
2230 let cfg = PipelineConfig {
2231 encoding: EncodingType::SimplePacking(params),
2232 filter: FilterType::None,
2233 compression: CompressionType::None,
2234 num_values: values.len(),
2235 byte_order: ByteOrder::Little,
2236 dtype_byte_width: 8,
2237 swap_unit_size: 8,
2238 compression_backend: CompressionBackend::default(),
2239 intra_codec_threads: 0,
2240 compute_hash: false,
2241 };
2242 let payload = encode_pipeline_f64(&values, &cfg).unwrap().encoded_bytes;
2243
2244 let out = decode_range_pipeline(&payload, &cfg, &[], 8, 10, false)
2246 .expect("simple_packing range decode must succeed");
2247 let decoded: Vec<f64> = out
2248 .chunks_exact(8)
2249 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
2250 .collect();
2251 assert_eq!(decoded.len(), 10);
2252 for (i, v) in decoded.iter().enumerate() {
2253 assert!(
2254 (v - (i + 8) as f64).abs() < 0.5,
2255 "range elem {i}: got {v}, want ~{}",
2256 i + 8
2257 );
2258 }
2259 }
2260
2261 #[test]
2262 fn decode_range_pipeline_simple_packing_native_byte_order() {
2263 let values: Vec<f64> = (0..40).map(|i| 500.0 + i as f64).collect();
2266 let params = simple_packing::compute_params(&values, 16, 0).unwrap();
2267 let cfg = PipelineConfig {
2268 encoding: EncodingType::SimplePacking(params),
2269 filter: FilterType::None,
2270 compression: CompressionType::None,
2271 num_values: values.len(),
2272 byte_order: ByteOrder::Big,
2273 dtype_byte_width: 8,
2274 swap_unit_size: 8,
2275 compression_backend: CompressionBackend::default(),
2276 intra_codec_threads: 0,
2277 compute_hash: false,
2278 };
2279 let payload = encode_pipeline_f64(&values, &cfg).unwrap().encoded_bytes;
2280 let out = decode_range_pipeline(&payload, &cfg, &[], 0, 5, true)
2281 .expect("native-order simple_packing range decode must succeed");
2282 let decoded: Vec<f64> = out
2283 .chunks_exact(8)
2284 .map(|c| f64::from_ne_bytes(c.try_into().unwrap()))
2285 .collect();
2286 for (i, v) in decoded.iter().enumerate() {
2287 assert!((v - (500.0 + i as f64)).abs() < 1.0, "elem {i}: {v}");
2288 }
2289 }
2290
2291 #[test]
2292 fn decode_range_pipeline_none_native_byte_swap() {
2293 let values: Vec<u32> = vec![0x0102_0304, 0x0506_0708, 0x090A_0B0C, 0x0D0E_0F10];
2297 let data: Vec<u8> = values.iter().flat_map(|v| v.to_be_bytes()).collect();
2298 let cfg = PipelineConfig {
2299 encoding: EncodingType::None,
2300 filter: FilterType::None,
2301 compression: CompressionType::None,
2302 num_values: values.len(),
2303 byte_order: ByteOrder::Big,
2304 dtype_byte_width: 4,
2305 swap_unit_size: 4,
2306 compression_backend: CompressionBackend::default(),
2307 intra_codec_threads: 0,
2308 compute_hash: false,
2309 };
2310 let out = decode_range_pipeline(&data, &cfg, &[], 1, 2, true)
2312 .expect("native-order none range decode must succeed");
2313 let decoded: Vec<u32> = out
2314 .chunks_exact(4)
2315 .map(|c| u32::from_ne_bytes(c.try_into().unwrap()))
2316 .collect();
2317 assert_eq!(decoded, vec![0x0506_0708, 0x090A_0B0C]);
2318 }
2319
2320 #[cfg(any(feature = "szip", feature = "szip-pure"))]
2321 #[test]
2322 fn decode_range_pipeline_szip_compressed_range() {
2323 let data: Vec<u8> = (0..2048).map(|i| (i % 256) as u8).collect();
2327 let config = PipelineConfig {
2328 encoding: EncodingType::None,
2329 filter: FilterType::None,
2330 compression: CompressionType::Szip {
2331 rsi: 128,
2332 block_size: 16,
2333 flags: 8, bits_per_sample: 8,
2335 },
2336 num_values: data.len(),
2337 byte_order: ByteOrder::Little,
2338 dtype_byte_width: 1,
2339 swap_unit_size: 1,
2340 compression_backend: CompressionBackend::default(),
2341 intra_codec_threads: 0,
2342 compute_hash: false,
2343 };
2344 let result = encode_pipeline(&data, &config).unwrap();
2345 let block_offsets = result.block_offsets.expect("szip yields block offsets");
2346
2347 let out = decode_range_pipeline(
2349 &result.encoded_bytes,
2350 &config,
2351 &block_offsets,
2352 256,
2353 128,
2354 false,
2355 )
2356 .expect("szip compressed range decode must succeed");
2357 assert_eq!(out, data[256..384]);
2358 }
2359
2360 #[test]
2361 fn decode_range_pipeline_none_byte_range_exceeds_payload() {
2362 let cfg = sizecheck_none_config(4, 4);
2368 let payload: Vec<u8> = (0..16).collect();
2369 let err = decode_range_pipeline(&payload, &cfg, &[], 3, 4, false)
2371 .expect_err("out-of-range slice must be rejected");
2372 match err {
2373 PipelineError::Range(msg) => {
2374 assert!(
2375 msg.contains("exceeds payload size"),
2376 "expected payload-size error, got: {msg}"
2377 );
2378 }
2379 other => panic!("expected Range error, got: {other}"),
2380 }
2381 }
2382
2383 #[test]
2388 fn bytes_to_f64_rejects_non_multiple_of_8() {
2389 let data = vec![0u8; 12]; let err = bytes_to_f64(&data, ByteOrder::Little)
2393 .expect_err("non-multiple-of-8 length must be rejected");
2394 match err {
2395 PipelineError::Range(msg) => {
2396 assert!(msg.contains("not a multiple of 8"), "got: {msg}");
2397 }
2398 other => panic!("expected Range error, got: {other}"),
2399 }
2400 }
2401
2402 #[test]
2403 fn bytes_to_f64_big_endian_branch() {
2404 let values = [1.0f64, -2.5, 3.75];
2406 let data: Vec<u8> = values.iter().flat_map(|v| v.to_be_bytes()).collect();
2407 let out = bytes_to_f64(&data, ByteOrder::Big).unwrap();
2408 assert_eq!(out, values);
2409 }
2410
2411 #[test]
2412 fn f64_to_bytes_both_byte_orders() {
2413 let values = [10.0f64, 20.0, 30.0];
2415 let le = f64_to_bytes(&values, ByteOrder::Little).unwrap();
2416 let be = f64_to_bytes(&values, ByteOrder::Big).unwrap();
2417 let expected_le: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
2418 let expected_be: Vec<u8> = values.iter().flat_map(|v| v.to_be_bytes()).collect();
2419 assert_eq!(le, expected_le);
2420 assert_eq!(be, expected_be);
2421 }
2422
2423 #[test]
2424 fn try_clone_bytes_round_trips_payload() {
2425 let src: Vec<u8> = (0..37).collect();
2428 let cloned = try_clone_bytes(&src).unwrap();
2429 assert_eq!(cloned, src);
2430 }
2431
2432 #[cfg(any(feature = "zstd", feature = "zstd-pure"))]
2440 #[test]
2441 fn pipeline_zstd_round_trip() {
2442 let data: Vec<u8> = (0..4096).map(|i| (i * 7 % 251) as u8).collect();
2443 let config = PipelineConfig {
2444 encoding: EncodingType::None,
2445 filter: FilterType::None,
2446 compression: CompressionType::Zstd { level: 5 },
2447 num_values: data.len(),
2448 byte_order: ByteOrder::Little,
2449 dtype_byte_width: 1,
2450 swap_unit_size: 1,
2451 compression_backend: CompressionBackend::default(),
2452 intra_codec_threads: 0,
2453 compute_hash: false,
2454 };
2455 let result = encode_pipeline(&data, &config).unwrap();
2456 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2457 assert_eq!(decoded, data);
2458 }
2459
2460 #[cfg(feature = "blosc2")]
2461 #[test]
2462 fn pipeline_blosc2_round_trip() {
2463 let data: Vec<u8> = (0..8192).map(|i| (i % 64) as u8).collect();
2466 for codec in [
2467 Blosc2Codec::Blosclz,
2468 Blosc2Codec::Lz4,
2469 Blosc2Codec::Lz4hc,
2470 Blosc2Codec::Zlib,
2471 Blosc2Codec::Zstd,
2472 ] {
2473 let config = PipelineConfig {
2474 encoding: EncodingType::None,
2475 filter: FilterType::None,
2476 compression: CompressionType::Blosc2 {
2477 codec,
2478 clevel: 5,
2479 typesize: 1,
2480 },
2481 num_values: data.len(),
2482 byte_order: ByteOrder::Little,
2483 dtype_byte_width: 1,
2484 swap_unit_size: 1,
2485 compression_backend: CompressionBackend::default(),
2486 intra_codec_threads: 0,
2487 compute_hash: false,
2488 };
2489 let result = encode_pipeline(&data, &config).unwrap();
2490 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2491 assert_eq!(decoded, data, "blosc2 codec {codec:?} round-trip mismatch");
2492 }
2493 }
2494
2495 #[cfg(feature = "sz3")]
2496 #[test]
2497 fn pipeline_sz3_round_trip() {
2498 let values: Vec<f64> = (0..1024).map(|i| (i as f64 * 0.01).sin()).collect();
2501 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
2502 for error_bound in [
2503 Sz3ErrorBound::Absolute(1e-3),
2504 Sz3ErrorBound::Relative(1e-3),
2505 Sz3ErrorBound::Psnr(80.0),
2506 ] {
2507 let config = PipelineConfig {
2508 encoding: EncodingType::None,
2509 filter: FilterType::None,
2510 compression: CompressionType::Sz3 {
2511 error_bound: error_bound.clone(),
2512 },
2513 num_values: values.len(),
2514 byte_order: ByteOrder::native(),
2515 dtype_byte_width: 8,
2516 swap_unit_size: 8,
2517 compression_backend: CompressionBackend::default(),
2518 intra_codec_threads: 0,
2519 compute_hash: false,
2520 };
2521 let result = encode_pipeline(&data, &config).unwrap();
2522 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2523 let decoded_values: Vec<f64> = decoded
2524 .chunks_exact(8)
2525 .map(|c| f64::from_ne_bytes(c.try_into().unwrap()))
2526 .collect();
2527 assert_eq!(decoded_values.len(), values.len());
2528 }
2529 }
2530
2531 #[cfg(feature = "zfp")]
2532 #[test]
2533 fn pipeline_zfp_round_trip_all_modes() {
2534 let values: Vec<f64> = (0..512).map(|i| 100.0 + (i as f64 * 0.05).cos()).collect();
2536 let data: Vec<u8> = values.iter().flat_map(|v| v.to_ne_bytes()).collect();
2537 for mode in [
2538 ZfpMode::FixedRate { rate: 16.0 },
2539 ZfpMode::FixedPrecision { precision: 20 },
2540 ZfpMode::FixedAccuracy { tolerance: 1e-4 },
2541 ] {
2542 let config = PipelineConfig {
2543 encoding: EncodingType::None,
2544 filter: FilterType::None,
2545 compression: CompressionType::Zfp { mode: mode.clone() },
2546 num_values: values.len(),
2547 byte_order: ByteOrder::native(),
2548 dtype_byte_width: 8,
2549 swap_unit_size: 8,
2550 compression_backend: CompressionBackend::default(),
2551 intra_codec_threads: 0,
2552 compute_hash: false,
2553 };
2554 let result = encode_pipeline(&data, &config).unwrap();
2555 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2556 assert_eq!(
2557 decoded.len(),
2558 data.len(),
2559 "zfp mode {mode:?} length mismatch"
2560 );
2561 }
2562 }
2563
2564 #[cfg(feature = "lz4")]
2565 #[test]
2566 fn pipeline_lz4_round_trip() {
2567 let data: Vec<u8> = (0..4000).map(|i| (i % 17) as u8).collect();
2570 let config = PipelineConfig {
2571 encoding: EncodingType::None,
2572 filter: FilterType::None,
2573 compression: CompressionType::Lz4,
2574 num_values: data.len(),
2575 byte_order: ByteOrder::Little,
2576 dtype_byte_width: 1,
2577 swap_unit_size: 1,
2578 compression_backend: CompressionBackend::default(),
2579 intra_codec_threads: 0,
2580 compute_hash: false,
2581 };
2582 let result = encode_pipeline(&data, &config).unwrap();
2583 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2584 assert_eq!(decoded, data);
2585 }
2586
2587 #[test]
2588 fn pipeline_rle_round_trip_bitmask() {
2589 let data: Vec<u8> = vec![0xFFu8, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF];
2592 let config = PipelineConfig {
2593 encoding: EncodingType::None,
2594 filter: FilterType::None,
2595 compression: CompressionType::Rle,
2596 num_values: data.len() * 8,
2597 byte_order: ByteOrder::Little,
2598 dtype_byte_width: 0, swap_unit_size: 1,
2600 compression_backend: CompressionBackend::default(),
2601 intra_codec_threads: 0,
2602 compute_hash: false,
2603 };
2604 let result = encode_pipeline(&data, &config).unwrap();
2605 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2606 assert_eq!(decoded, data);
2607 }
2608
2609 #[test]
2610 fn pipeline_roaring_round_trip_bitmask() {
2611 let data: Vec<u8> = vec![0b1010_1010u8, 0x00, 0xFF, 0b0101_0101];
2614 let config = PipelineConfig {
2615 encoding: EncodingType::None,
2616 filter: FilterType::None,
2617 compression: CompressionType::Roaring,
2618 num_values: data.len() * 8,
2619 byte_order: ByteOrder::Little,
2620 dtype_byte_width: 0, swap_unit_size: 1,
2622 compression_backend: CompressionBackend::default(),
2623 intra_codec_threads: 0,
2624 compute_hash: false,
2625 };
2626 let result = encode_pipeline(&data, &config).unwrap();
2627 let decoded = decode_pipeline(&result.encoded_bytes, &config, false).unwrap();
2628 assert_eq!(decoded, data);
2629 }
2630}