#![allow(clippy::useless_conversion)]
use std::sync::{Arc, OnceLock};
use bytes::Bytes;
use pyo3::create_exception;
use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyDict, PyList};
use s4_codec_rs::index::{FrameIndex, IndexError};
use s4_codec_rs::multipart::{FrameError, FrameHeader, FrameIter};
use s4_codec_rs::{cpu_gzip, cpu_zstd, cpu_zstd_dict, ChunkManifest, Codec, CodecError, CodecKind};
use tokio::runtime::{Builder, Runtime};
create_exception!(s4_codec, S4Error, PyValueError);
create_exception!(s4_codec, S4CrcMismatchError, S4Error);
create_exception!(s4_codec, S4SizeMismatchError, S4Error);
create_exception!(s4_codec, S4CodecMismatchError, S4Error);
create_exception!(s4_codec, S4UnregisteredCodecError, S4Error);
create_exception!(s4_codec, S4ManifestSizeExceedsLimitError, S4Error);
create_exception!(s4_codec, S4ManifestSizeMismatchError, S4Error);
create_exception!(s4_codec, S4BackendError, PyRuntimeError);
create_exception!(s4_codec, S4IoError, PyIOError);
create_exception!(s4_codec, S4FrameError, S4Error);
create_exception!(s4_codec, S4IndexError, S4Error);
fn runtime() -> &'static Runtime {
static RT: OnceLock<Runtime> = OnceLock::new();
RT.get_or_init(|| {
Builder::new_multi_thread()
.enable_all()
.thread_name("s4-codec-py")
.build()
.expect("failed to start tokio runtime for s4_codec python binding")
})
}
fn codec_err_to_py(e: CodecError) -> PyErr {
use s4_codec_rs::CodecError::*;
match e {
SizeMismatch { expected, got } => {
S4SizeMismatchError::new_err(format!("size mismatch: expected {expected}, got {got}"))
}
CrcMismatch { expected, got } => S4CrcMismatchError::new_err(format!(
"crc32c mismatch: expected {expected:#010x}, got {got:#010x}"
)),
CodecMismatch { expected, got } => S4CodecMismatchError::new_err(format!(
"codec mismatch: expected {expected:?}, got {got:?}"
)),
UnregisteredCodec(k) => {
S4UnregisteredCodecError::new_err(format!("codec {k:?} not registered"))
}
ManifestSizeExceedsLimit { requested, limit } => S4ManifestSizeExceedsLimitError::new_err(
format!("manifest claims {requested} bytes but limit is {limit}"),
),
ManifestSizeMismatch { manifest, actual } => S4ManifestSizeMismatchError::new_err(format!(
"manifest claims {manifest} bytes but body is {actual}"
)),
Backend(msg) => S4BackendError::new_err(format!("backend: {msg}")),
Io(e) => S4IoError::new_err(format!("io: {e}")),
TruncatedStream { expected, got } => S4Error::new_err(format!(
"stream truncated: expected {expected} input bytes, got {got}"
)),
OverlengthStream { expected, got } => S4Error::new_err(format!(
"stream over-length: expected {expected} input bytes, got at least {got}"
)),
Join(e) => S4BackendError::new_err(format!("backend (worker join): {e}")),
other => S4Error::new_err(format!("codec error: {other}")),
}
}
fn manifest_from_parts(
kind: CodecKind,
payload_len: u64,
original_size: u64,
crc32c: u32,
) -> ChunkManifest {
ChunkManifest {
codec: kind,
original_size,
compressed_size: payload_len,
crc32c,
}
}
fn block_on<F, T>(py: Python<'_>, fut: F) -> T
where
F: std::future::Future<Output = T> + Send,
T: Send,
{
py.allow_threads(|| runtime().block_on(fut))
}
#[pyclass(name = "CpuZstd", module = "s4_codec")]
struct PyCpuZstd {
inner: cpu_zstd::CpuZstd,
}
#[pymethods]
impl PyCpuZstd {
#[new]
#[pyo3(signature = (level = 3))]
fn new(level: i32) -> Self {
Self {
inner: cpu_zstd::CpuZstd::new(level),
}
}
fn compress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
) -> PyResult<(Bound<'py, PyBytes>, u64, u32)> {
let input = Bytes::copy_from_slice(data.as_bytes());
let codec = self.inner.clone();
let (out, manifest) =
block_on(py, async move { codec.compress(input).await }).map_err(codec_err_to_py)?;
Ok((
PyBytes::new(py, &out),
manifest.original_size,
manifest.crc32c,
))
}
fn decompress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
original_size: u64,
crc32c: u32,
) -> PyResult<Bound<'py, PyBytes>> {
let input = Bytes::copy_from_slice(data.as_bytes());
let manifest = manifest_from_parts(
CodecKind::CpuZstd,
input.len() as u64,
original_size,
crc32c,
);
let codec = self.inner.clone();
let out = block_on(py, async move { codec.decompress(input, &manifest).await })
.map_err(codec_err_to_py)?;
Ok(PyBytes::new(py, &out))
}
fn __repr__(&self) -> String {
format!("CpuZstd(level={})", cpu_zstd::CpuZstd::DEFAULT_LEVEL)
}
}
#[pyclass(name = "CpuGzip", module = "s4_codec")]
struct PyCpuGzip {
inner: cpu_gzip::CpuGzip,
}
#[pymethods]
impl PyCpuGzip {
#[new]
#[pyo3(signature = (level = 6))]
fn new(level: u32) -> Self {
Self {
inner: cpu_gzip::CpuGzip::new(level),
}
}
fn compress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
) -> PyResult<(Bound<'py, PyBytes>, u64, u32)> {
let input = Bytes::copy_from_slice(data.as_bytes());
let codec = self.inner.clone();
let (out, manifest) =
block_on(py, async move { codec.compress(input).await }).map_err(codec_err_to_py)?;
Ok((
PyBytes::new(py, &out),
manifest.original_size,
manifest.crc32c,
))
}
fn decompress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
original_size: u64,
crc32c: u32,
) -> PyResult<Bound<'py, PyBytes>> {
let input = Bytes::copy_from_slice(data.as_bytes());
let manifest = manifest_from_parts(
CodecKind::CpuGzip,
input.len() as u64,
original_size,
crc32c,
);
let codec = self.inner.clone();
let out = block_on(py, async move { codec.decompress(input, &manifest).await })
.map_err(codec_err_to_py)?;
Ok(PyBytes::new(py, &out))
}
fn __repr__(&self) -> String {
format!("CpuGzip(level={})", cpu_gzip::CpuGzip::DEFAULT_LEVEL)
}
}
#[pyclass(name = "CpuZstdDict", module = "s4_codec")]
struct PyCpuZstdDict {
inner: cpu_zstd_dict::CpuZstdDict,
level: i32,
}
#[pymethods]
impl PyCpuZstdDict {
#[new]
#[pyo3(signature = (dict_bytes, level = 3))]
fn new(dict_bytes: &Bound<'_, PyBytes>, level: i32) -> PyResult<Self> {
let dict: Arc<[u8]> = Arc::from(dict_bytes.as_bytes().to_vec().into_boxed_slice());
let inner = cpu_zstd_dict::CpuZstdDict::new(dict, level).map_err(codec_err_to_py)?;
Ok(Self {
inner,
level: level.clamp(1, 22),
})
}
fn compress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
) -> PyResult<(Bound<'py, PyBytes>, u64, u32)> {
let input = Bytes::copy_from_slice(data.as_bytes());
let codec = self.inner.clone();
let (out, manifest) =
block_on(py, async move { codec.compress(input).await }).map_err(codec_err_to_py)?;
Ok((
PyBytes::new(py, &out),
manifest.original_size,
manifest.crc32c,
))
}
fn decompress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
original_size: u64,
crc32c: u32,
) -> PyResult<Bound<'py, PyBytes>> {
let input = Bytes::copy_from_slice(data.as_bytes());
let manifest = manifest_from_parts(
CodecKind::CpuZstdDict,
input.len() as u64,
original_size,
crc32c,
);
let codec = self.inner.clone();
let out = block_on(py, async move { codec.decompress(input, &manifest).await })
.map_err(codec_err_to_py)?;
Ok(PyBytes::new(py, &out))
}
fn __repr__(&self) -> String {
format!(
"CpuZstdDict(dict_len={}, level={})",
self.inner.dict().len(),
self.level
)
}
}
#[pyfunction]
fn gpu_available() -> bool {
s4_codec_rs::nvcomp::is_gpu_available()
}
fn frame_err_to_py(e: FrameError) -> PyErr {
S4FrameError::new_err(format!("frame error: {e}"))
}
fn index_err_to_py(e: IndexError) -> PyErr {
S4IndexError::new_err(format!("index error: {e}"))
}
fn frame_header_dict<'py>(py: Python<'py>, h: &FrameHeader) -> PyResult<Bound<'py, PyDict>> {
let d = PyDict::new(py);
d.set_item("codec", h.codec.as_str())?;
d.set_item("original_size", h.original_size)?;
d.set_item("compressed_size", h.compressed_size)?;
d.set_item("crc32c", h.crc32c)?;
Ok(d)
}
#[pyfunction]
fn read_frame<'py>(
py: Python<'py>,
data: &Bound<'py, PyBytes>,
) -> PyResult<(Bound<'py, PyDict>, Bound<'py, PyBytes>, Bound<'py, PyBytes>)> {
let input = Bytes::copy_from_slice(data.as_bytes());
let (header, payload, rest) =
s4_codec_rs::multipart::read_frame(input).map_err(frame_err_to_py)?;
Ok((
frame_header_dict(py, &header)?,
PyBytes::new(py, &payload),
PyBytes::new(py, &rest),
))
}
#[pyfunction]
fn frame_iter<'py>(py: Python<'py>, data: &Bound<'py, PyBytes>) -> PyResult<Bound<'py, PyList>> {
let input = Bytes::copy_from_slice(data.as_bytes());
let out = PyList::empty(py);
for item in FrameIter::new(input) {
let (header, payload) = item.map_err(frame_err_to_py)?;
out.append((frame_header_dict(py, &header)?, PyBytes::new(py, &payload)))?;
}
Ok(out)
}
#[pyfunction]
fn decode_index<'py>(py: Python<'py>, data: &Bound<'py, PyBytes>) -> PyResult<Bound<'py, PyDict>> {
let input = Bytes::copy_from_slice(data.as_bytes());
let idx: FrameIndex = s4_codec_rs::index::decode_index(input).map_err(index_err_to_py)?;
let d = PyDict::new(py);
d.set_item("total_padded_size", idx.total_padded_size)?;
d.set_item("total_original_size", idx.total_original_size())?;
d.set_item("source_etag", idx.source_etag.as_deref())?;
d.set_item("source_compressed_size", idx.source_compressed_size)?;
let entries = PyList::empty(py);
for e in &idx.entries {
let ed = PyDict::new(py);
ed.set_item("original_offset", e.original_offset)?;
ed.set_item("original_size", e.original_size)?;
ed.set_item("compressed_offset", e.compressed_offset)?;
ed.set_item("compressed_size", e.compressed_size)?;
entries.append(ed)?;
}
d.set_item("entries", entries)?;
match idx.sse_v3 {
Some(sse) => {
let sd = PyDict::new(py);
sd.set_item("enc_chunk_size", sse.enc_chunk_size)?;
sd.set_item("enc_chunk_count", sse.enc_chunk_count)?;
sd.set_item("enc_key_id", sse.enc_key_id)?;
sd.set_item("enc_salt", PyBytes::new(py, &sse.enc_salt))?;
sd.set_item("enc_plaintext_len", sse.enc_plaintext_len)?;
sd.set_item("enc_header_bytes", sse.enc_header_bytes)?;
d.set_item("sse", sd)?;
}
None => d.set_item("sse", py.None())?,
}
Ok(d)
}
#[pyfunction]
#[pyo3(name = "crc32c")]
fn crc32c_py(data: &Bound<'_, PyBytes>) -> u32 {
::crc32c::crc32c(data.as_bytes())
}
#[pymodule]
fn s4_codec(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyCpuZstd>()?;
m.add_class::<PyCpuGzip>()?;
m.add_class::<PyCpuZstdDict>()?;
m.add_function(wrap_pyfunction!(gpu_available, m)?)?;
m.add_function(wrap_pyfunction!(read_frame, m)?)?;
m.add_function(wrap_pyfunction!(frame_iter, m)?)?;
m.add_function(wrap_pyfunction!(decode_index, m)?)?;
m.add_function(wrap_pyfunction!(crc32c_py, m)?)?;
m.add(
"FRAME_MAGIC",
PyBytes::new(py, s4_codec_rs::multipart::FRAME_MAGIC),
)?;
m.add(
"PADDING_MAGIC",
PyBytes::new(py, s4_codec_rs::multipart::PADDING_MAGIC),
)?;
m.add(
"FRAME_HEADER_BYTES",
s4_codec_rs::multipart::FRAME_HEADER_BYTES,
)?;
m.add("SIDECAR_SUFFIX", s4_codec_rs::index::SIDECAR_SUFFIX)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add("S4Error", py.get_type::<S4Error>())?;
m.add("S4CrcMismatchError", py.get_type::<S4CrcMismatchError>())?;
m.add("S4SizeMismatchError", py.get_type::<S4SizeMismatchError>())?;
m.add(
"S4CodecMismatchError",
py.get_type::<S4CodecMismatchError>(),
)?;
m.add(
"S4UnregisteredCodecError",
py.get_type::<S4UnregisteredCodecError>(),
)?;
m.add(
"S4ManifestSizeExceedsLimitError",
py.get_type::<S4ManifestSizeExceedsLimitError>(),
)?;
m.add(
"S4ManifestSizeMismatchError",
py.get_type::<S4ManifestSizeMismatchError>(),
)?;
m.add("S4BackendError", py.get_type::<S4BackendError>())?;
m.add("S4IoError", py.get_type::<S4IoError>())?;
m.add("S4FrameError", py.get_type::<S4FrameError>())?;
m.add("S4IndexError", py.get_type::<S4IndexError>())?;
Ok(())
}