#[cfg(any(feature = "nvcomp-gpu", test))]
pub use crate::{MAX_DECOMPRESSED_BYTES, validate_decompress_manifest};
#[cfg(feature = "nvcomp-gpu")]
mod imp {
use std::sync::Arc;
use crate::ferro_compress::{Algo, BitcompDataType, Codec as FerroCodec, NvcompCodec};
use bytes::Bytes;
use super::validate_decompress_manifest;
use crate::{ChunkManifest, Codec, CodecError, CodecKind};
pub struct NvcompZstdCodec {
inner: Arc<NvcompCodec>,
}
impl NvcompZstdCodec {
pub fn new() -> Result<Self, CodecError> {
let inner = NvcompCodec::new(Algo::Zstd)
.map_err(|e| CodecError::Backend(anyhow::anyhow!("nvcomp zstd init: {e}")))?;
Ok(Self {
inner: Arc::new(inner),
})
}
}
#[async_trait::async_trait]
impl Codec for NvcompZstdCodec {
fn kind(&self) -> CodecKind {
CodecKind::NvcompZstd
}
async fn compress(&self, input: Bytes) -> Result<(Bytes, ChunkManifest), CodecError> {
let original_size = input.len() as u64;
let original_crc = crc32c::crc32c(&input);
let codec = Arc::clone(&self.inner);
let compressed = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, CodecError> {
let mut out = Vec::with_capacity(codec.max_compressed_len(input.len()));
codec.compress(input.as_ref(), &mut out).map_err(|e| {
CodecError::Backend(anyhow::anyhow!("nvcomp zstd compress: {e}"))
})?;
Ok(out)
})
.await??;
let manifest = ChunkManifest {
codec: CodecKind::NvcompZstd,
original_size,
compressed_size: compressed.len() as u64,
crc32c: original_crc,
};
Ok((Bytes::from(compressed), manifest))
}
async fn decompress(
&self,
input: Bytes,
manifest: &ChunkManifest,
) -> Result<Bytes, CodecError> {
if manifest.codec != CodecKind::NvcompZstd {
return Err(CodecError::CodecMismatch {
expected: CodecKind::NvcompZstd,
got: manifest.codec,
});
}
let expected_crc = manifest.crc32c;
let expected_orig_size = validate_decompress_manifest(manifest, input.len())?;
let codec = Arc::clone(&self.inner);
let decompressed =
tokio::task::spawn_blocking(move || -> Result<Vec<u8>, CodecError> {
let mut out = Vec::with_capacity(expected_orig_size);
codec.decompress(input.as_ref(), &mut out).map_err(|e| {
CodecError::Backend(anyhow::anyhow!("nvcomp zstd decompress: {e}"))
})?;
Ok(out)
})
.await??;
if decompressed.len() != expected_orig_size {
return Err(CodecError::SizeMismatch {
expected: manifest.original_size,
got: decompressed.len() as u64,
});
}
let actual_crc = crc32c::crc32c(&decompressed);
if actual_crc != expected_crc {
return Err(CodecError::CrcMismatch {
expected: expected_crc,
got: actual_crc,
});
}
Ok(Bytes::from(decompressed))
}
}
pub struct NvcompBitcompCodec {
inner: Arc<NvcompCodec>,
}
impl NvcompBitcompCodec {
pub fn new(data_type: BitcompDataType) -> Result<Self, CodecError> {
let inner = NvcompCodec::new(Algo::Bitcomp { data_type })
.map_err(|e| CodecError::Backend(anyhow::anyhow!("nvcomp bitcomp init: {e}")))?;
Ok(Self {
inner: Arc::new(inner),
})
}
pub fn default_general() -> Result<Self, CodecError> {
Self::new(BitcompDataType::Char)
}
}
#[async_trait::async_trait]
impl Codec for NvcompBitcompCodec {
fn kind(&self) -> CodecKind {
CodecKind::NvcompBitcomp
}
async fn compress(&self, input: Bytes) -> Result<(Bytes, ChunkManifest), CodecError> {
let original_size = input.len() as u64;
let original_crc = crc32c::crc32c(&input);
let codec = Arc::clone(&self.inner);
let compressed = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, CodecError> {
let mut out = Vec::with_capacity(codec.max_compressed_len(input.len()));
codec.compress(input.as_ref(), &mut out).map_err(|e| {
CodecError::Backend(anyhow::anyhow!("nvcomp bitcomp compress: {e}"))
})?;
Ok(out)
})
.await??;
let manifest = ChunkManifest {
codec: CodecKind::NvcompBitcomp,
original_size,
compressed_size: compressed.len() as u64,
crc32c: original_crc,
};
Ok((Bytes::from(compressed), manifest))
}
async fn decompress(
&self,
input: Bytes,
manifest: &ChunkManifest,
) -> Result<Bytes, CodecError> {
if manifest.codec != CodecKind::NvcompBitcomp {
return Err(CodecError::CodecMismatch {
expected: CodecKind::NvcompBitcomp,
got: manifest.codec,
});
}
let expected_crc = manifest.crc32c;
let expected_orig_size = validate_decompress_manifest(manifest, input.len())?;
let codec = Arc::clone(&self.inner);
let decompressed =
tokio::task::spawn_blocking(move || -> Result<Vec<u8>, CodecError> {
let mut out = Vec::with_capacity(expected_orig_size);
codec.decompress(input.as_ref(), &mut out).map_err(|e| {
CodecError::Backend(anyhow::anyhow!("nvcomp bitcomp decompress: {e}"))
})?;
Ok(out)
})
.await??;
if decompressed.len() != expected_orig_size {
return Err(CodecError::SizeMismatch {
expected: manifest.original_size,
got: decompressed.len() as u64,
});
}
let actual_crc = crc32c::crc32c(&decompressed);
if actual_crc != expected_crc {
return Err(CodecError::CrcMismatch {
expected: expected_crc,
got: actual_crc,
});
}
Ok(Bytes::from(decompressed))
}
}
pub struct NvcompGDeflateCodec {
inner: Arc<NvcompCodec>,
}
impl NvcompGDeflateCodec {
pub fn new() -> Result<Self, CodecError> {
let inner = NvcompCodec::new(Algo::GDeflate)
.map_err(|e| CodecError::Backend(anyhow::anyhow!("nvcomp gdeflate init: {e}")))?;
Ok(Self {
inner: Arc::new(inner),
})
}
}
#[async_trait::async_trait]
impl Codec for NvcompGDeflateCodec {
fn kind(&self) -> CodecKind {
CodecKind::NvcompGDeflate
}
async fn compress(&self, input: Bytes) -> Result<(Bytes, ChunkManifest), CodecError> {
let original_size = input.len() as u64;
let original_crc = crc32c::crc32c(&input);
let codec = Arc::clone(&self.inner);
let compressed = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, CodecError> {
let mut out = Vec::with_capacity(codec.max_compressed_len(input.len()));
codec.compress(input.as_ref(), &mut out).map_err(|e| {
CodecError::Backend(anyhow::anyhow!("nvcomp gdeflate compress: {e}"))
})?;
Ok(out)
})
.await??;
let manifest = ChunkManifest {
codec: CodecKind::NvcompGDeflate,
original_size,
compressed_size: compressed.len() as u64,
crc32c: original_crc,
};
Ok((Bytes::from(compressed), manifest))
}
async fn decompress(
&self,
input: Bytes,
manifest: &ChunkManifest,
) -> Result<Bytes, CodecError> {
if manifest.codec != CodecKind::NvcompGDeflate {
return Err(CodecError::CodecMismatch {
expected: CodecKind::NvcompGDeflate,
got: manifest.codec,
});
}
let expected_crc = manifest.crc32c;
let expected_orig_size = validate_decompress_manifest(manifest, input.len())?;
let codec = Arc::clone(&self.inner);
let decompressed =
tokio::task::spawn_blocking(move || -> Result<Vec<u8>, CodecError> {
let mut out = Vec::with_capacity(expected_orig_size);
codec.decompress(input.as_ref(), &mut out).map_err(|e| {
CodecError::Backend(anyhow::anyhow!("nvcomp gdeflate decompress: {e}"))
})?;
Ok(out)
})
.await??;
if decompressed.len() != expected_orig_size {
return Err(CodecError::SizeMismatch {
expected: manifest.original_size,
got: decompressed.len() as u64,
});
}
let actual_crc = crc32c::crc32c(&decompressed);
if actual_crc != expected_crc {
return Err(CodecError::CrcMismatch {
expected: expected_crc,
got: actual_crc,
});
}
Ok(Bytes::from(decompressed))
}
}
pub fn is_gpu_available() -> bool {
NvcompCodec::is_available()
}
}
#[cfg(feature = "nvcomp-gpu")]
pub use imp::{NvcompBitcompCodec, NvcompGDeflateCodec, NvcompZstdCodec, is_gpu_available};
#[cfg(feature = "nvcomp-gpu")]
pub use crate::ferro_compress::BitcompDataType;
#[cfg(not(feature = "nvcomp-gpu"))]
pub fn is_gpu_available() -> bool {
false
}
#[cfg(all(test, feature = "nvcomp-gpu"))]
mod tests {
use super::*;
use crate::Codec;
use bytes::Bytes;
#[tokio::test]
#[ignore = "requires CUDA-capable GPU + NVCOMP_HOME at build time"]
async fn nvcomp_zstd_roundtrip() {
if !is_gpu_available() {
eprintln!("skipping: no CUDA GPU detected at runtime");
return;
}
let codec = NvcompZstdCodec::new().expect("init");
let input = Bytes::from(vec![b'a'; 100_000]);
let (compressed, manifest) = codec.compress(input.clone()).await.expect("compress");
assert!(compressed.len() < input.len() / 10);
let decompressed = codec
.decompress(compressed, &manifest)
.await
.expect("decompress");
assert_eq!(decompressed, input);
}
#[tokio::test]
#[ignore = "requires CUDA-capable GPU + NVCOMP_HOME at build time"]
async fn nvcomp_bitcomp_roundtrip_on_integer_column() {
if !is_gpu_available() {
eprintln!("skipping: no CUDA GPU detected at runtime");
return;
}
let codec = NvcompBitcompCodec::default_general().expect("init");
let mut payload: Vec<u8> = Vec::with_capacity(8192);
for i in 0i64..1024 {
payload.extend_from_slice(&i.to_le_bytes());
}
let input = Bytes::from(payload);
let (compressed, manifest) = codec.compress(input.clone()).await.expect("compress");
assert!(
compressed.len() < input.len() / 2,
"bitcomp on monotone i64 should compress >2x, got {} -> {}",
input.len(),
compressed.len()
);
let decompressed = codec
.decompress(compressed, &manifest)
.await
.expect("decompress");
assert_eq!(decompressed, input);
}
}
#[cfg(test)]
mod manifest_validate_tests {
use super::{MAX_DECOMPRESSED_BYTES, validate_decompress_manifest};
use crate::{ChunkManifest, CodecError, CodecKind};
fn manifest(original: u64, compressed: u64) -> ChunkManifest {
ChunkManifest {
codec: CodecKind::NvcompZstd,
original_size: original,
compressed_size: compressed,
crc32c: 0,
}
}
#[test]
fn decompress_rejects_manifest_original_size_over_limit() {
let m = manifest(MAX_DECOMPRESSED_BYTES + 1, 1024);
let err = validate_decompress_manifest(&m, 1024).unwrap_err();
match err {
CodecError::ManifestSizeExceedsLimit { requested, limit } => {
assert_eq!(requested, MAX_DECOMPRESSED_BYTES + 1);
assert_eq!(limit, MAX_DECOMPRESSED_BYTES);
}
other => panic!("expected ManifestSizeExceedsLimit, got {other:?}"),
}
}
#[test]
fn decompress_rejects_manifest_compressed_size_mismatch() {
let m = manifest(1024, 2048);
let err = validate_decompress_manifest(&m, 1024).unwrap_err();
match err {
CodecError::ManifestSizeMismatch {
manifest: m_size,
actual,
} => {
assert_eq!(m_size, 2048);
assert_eq!(actual, 1024);
}
other => panic!("expected ManifestSizeMismatch, got {other:?}"),
}
}
#[test]
fn decompress_validates_well_formed_manifest() {
let m = manifest(MAX_DECOMPRESSED_BYTES, 1024);
let n = validate_decompress_manifest(&m, 1024)
.expect("well-formed manifest at the ceiling must pass");
assert_eq!(n as u64, MAX_DECOMPRESSED_BYTES);
}
#[cfg(target_pointer_width = "32")]
#[test]
fn decompress_rejects_u64_to_usize_overflow_on_32bit_targets() {
let m = manifest(MAX_DECOMPRESSED_BYTES, 1024);
let err = validate_decompress_manifest(&m, 1024).unwrap_err();
assert!(matches!(err, CodecError::ManifestSizeExceedsLimit { .. }));
}
#[cfg(target_pointer_width = "64")]
#[test]
fn decompress_rejects_u64_to_usize_overflow_on_32bit_targets() {
let m = manifest(MAX_DECOMPRESSED_BYTES, 16);
let n = validate_decompress_manifest(&m, 16).expect("limit value narrows on 64-bit");
assert_eq!(n as u64, MAX_DECOMPRESSED_BYTES);
}
}