1#![allow(clippy::useless_conversion)]
5use std::sync::OnceLock;
23
24use bytes::Bytes;
25use pyo3::create_exception;
26use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError};
27use pyo3::prelude::*;
28use pyo3::types::PyBytes;
29use s4_codec_rs::{cpu_gzip, cpu_zstd, ChunkManifest, Codec, CodecError, CodecKind};
30use tokio::runtime::{Builder, Runtime};
31
32create_exception!(s4_codec, S4Error, PyValueError);
53create_exception!(s4_codec, S4CrcMismatchError, S4Error);
54create_exception!(s4_codec, S4SizeMismatchError, S4Error);
55create_exception!(s4_codec, S4CodecMismatchError, S4Error);
56create_exception!(s4_codec, S4UnregisteredCodecError, S4Error);
57create_exception!(s4_codec, S4ManifestSizeExceedsLimitError, S4Error);
58create_exception!(s4_codec, S4ManifestSizeMismatchError, S4Error);
59create_exception!(s4_codec, S4BackendError, PyRuntimeError);
60create_exception!(s4_codec, S4IoError, PyIOError);
61
62fn runtime() -> &'static Runtime {
63 static RT: OnceLock<Runtime> = OnceLock::new();
64 RT.get_or_init(|| {
65 Builder::new_multi_thread()
66 .enable_all()
67 .thread_name("s4-codec-py")
68 .build()
69 .expect("failed to start tokio runtime for s4_codec python binding")
70 })
71}
72
73fn codec_err_to_py(e: CodecError) -> PyErr {
74 use s4_codec_rs::CodecError::*;
75 match e {
76 SizeMismatch { expected, got } => {
77 S4SizeMismatchError::new_err(format!("size mismatch: expected {expected}, got {got}"))
78 }
79 CrcMismatch { expected, got } => S4CrcMismatchError::new_err(format!(
80 "crc32c mismatch: expected {expected:#010x}, got {got:#010x}"
81 )),
82 CodecMismatch { expected, got } => S4CodecMismatchError::new_err(format!(
83 "codec mismatch: expected {expected:?}, got {got:?}"
84 )),
85 UnregisteredCodec(k) => {
86 S4UnregisteredCodecError::new_err(format!("codec {k:?} not registered"))
87 }
88 ManifestSizeExceedsLimit { requested, limit } => S4ManifestSizeExceedsLimitError::new_err(
89 format!("manifest claims {requested} bytes but limit is {limit}"),
90 ),
91 ManifestSizeMismatch { manifest, actual } => S4ManifestSizeMismatchError::new_err(format!(
92 "manifest claims {manifest} bytes but body is {actual}"
93 )),
94 Backend(msg) => S4BackendError::new_err(format!("backend: {msg}")),
95 Io(e) => S4IoError::new_err(format!("io: {e}")),
96 TruncatedStream { expected, got } => S4Error::new_err(format!(
97 "stream truncated: expected {expected} input bytes, got {got}"
98 )),
99 OverlengthStream { expected, got } => S4Error::new_err(format!(
105 "stream over-length: expected {expected} input bytes, got at least {got}"
106 )),
107 Join(e) => S4BackendError::new_err(format!("backend (worker join): {e}")),
111 other => S4Error::new_err(format!("codec error: {other}")),
117 }
118}
119
120fn manifest_from_parts(
121 kind: CodecKind,
122 payload_len: u64,
123 original_size: u64,
124 crc32c: u32,
125) -> ChunkManifest {
126 ChunkManifest {
127 codec: kind,
128 original_size,
129 compressed_size: payload_len,
130 crc32c,
131 }
132}
133
134fn block_on<F, T>(py: Python<'_>, fut: F) -> T
138where
139 F: std::future::Future<Output = T> + Send,
140 T: Send,
141{
142 py.allow_threads(|| runtime().block_on(fut))
143}
144
145#[pyclass(name = "CpuZstd", module = "s4_codec")]
148struct PyCpuZstd {
149 inner: cpu_zstd::CpuZstd,
150}
151
152#[pymethods]
153impl PyCpuZstd {
154 #[new]
155 #[pyo3(signature = (level = 3))]
156 fn new(level: i32) -> Self {
157 Self {
158 inner: cpu_zstd::CpuZstd::new(level),
159 }
160 }
161
162 fn compress<'py>(
166 &self,
167 py: Python<'py>,
168 data: &Bound<'py, PyBytes>,
169 ) -> PyResult<(Bound<'py, PyBytes>, u64, u32)> {
170 let input = Bytes::copy_from_slice(data.as_bytes());
171 let codec = self.inner.clone();
172 let (out, manifest) =
173 block_on(py, async move { codec.compress(input).await }).map_err(codec_err_to_py)?;
174 Ok((
175 PyBytes::new(py, &out),
176 manifest.original_size,
177 manifest.crc32c,
178 ))
179 }
180
181 fn decompress<'py>(
184 &self,
185 py: Python<'py>,
186 data: &Bound<'py, PyBytes>,
187 original_size: u64,
188 crc32c: u32,
189 ) -> PyResult<Bound<'py, PyBytes>> {
190 let input = Bytes::copy_from_slice(data.as_bytes());
191 let manifest = manifest_from_parts(
192 CodecKind::CpuZstd,
193 input.len() as u64,
194 original_size,
195 crc32c,
196 );
197 let codec = self.inner.clone();
198 let out = block_on(py, async move { codec.decompress(input, &manifest).await })
199 .map_err(codec_err_to_py)?;
200 Ok(PyBytes::new(py, &out))
201 }
202
203 fn __repr__(&self) -> String {
204 format!("CpuZstd(level={})", cpu_zstd::CpuZstd::DEFAULT_LEVEL)
205 }
206}
207
208#[pyclass(name = "CpuGzip", module = "s4_codec")]
212struct PyCpuGzip {
213 inner: cpu_gzip::CpuGzip,
214}
215
216#[pymethods]
217impl PyCpuGzip {
218 #[new]
219 #[pyo3(signature = (level = 6))]
220 fn new(level: u32) -> Self {
221 Self {
222 inner: cpu_gzip::CpuGzip::new(level),
223 }
224 }
225
226 fn compress<'py>(
227 &self,
228 py: Python<'py>,
229 data: &Bound<'py, PyBytes>,
230 ) -> PyResult<(Bound<'py, PyBytes>, u64, u32)> {
231 let input = Bytes::copy_from_slice(data.as_bytes());
232 let codec = self.inner.clone();
233 let (out, manifest) =
234 block_on(py, async move { codec.compress(input).await }).map_err(codec_err_to_py)?;
235 Ok((
236 PyBytes::new(py, &out),
237 manifest.original_size,
238 manifest.crc32c,
239 ))
240 }
241
242 fn decompress<'py>(
243 &self,
244 py: Python<'py>,
245 data: &Bound<'py, PyBytes>,
246 original_size: u64,
247 crc32c: u32,
248 ) -> PyResult<Bound<'py, PyBytes>> {
249 let input = Bytes::copy_from_slice(data.as_bytes());
250 let manifest = manifest_from_parts(
251 CodecKind::CpuGzip,
252 input.len() as u64,
253 original_size,
254 crc32c,
255 );
256 let codec = self.inner.clone();
257 let out = block_on(py, async move { codec.decompress(input, &manifest).await })
258 .map_err(codec_err_to_py)?;
259 Ok(PyBytes::new(py, &out))
260 }
261
262 fn __repr__(&self) -> String {
263 format!("CpuGzip(level={})", cpu_gzip::CpuGzip::DEFAULT_LEVEL)
264 }
265}
266
267#[pyfunction]
270fn gpu_available() -> bool {
271 s4_codec_rs::nvcomp::is_gpu_available()
272}
273
274#[pymodule]
275fn s4_codec(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
276 m.add_class::<PyCpuZstd>()?;
277 m.add_class::<PyCpuGzip>()?;
278 m.add_function(wrap_pyfunction!(gpu_available, m)?)?;
279 m.add("__version__", env!("CARGO_PKG_VERSION"))?;
280 m.add("S4Error", py.get_type::<S4Error>())?;
284 m.add("S4CrcMismatchError", py.get_type::<S4CrcMismatchError>())?;
285 m.add("S4SizeMismatchError", py.get_type::<S4SizeMismatchError>())?;
286 m.add(
287 "S4CodecMismatchError",
288 py.get_type::<S4CodecMismatchError>(),
289 )?;
290 m.add(
291 "S4UnregisteredCodecError",
292 py.get_type::<S4UnregisteredCodecError>(),
293 )?;
294 m.add(
295 "S4ManifestSizeExceedsLimitError",
296 py.get_type::<S4ManifestSizeExceedsLimitError>(),
297 )?;
298 m.add(
299 "S4ManifestSizeMismatchError",
300 py.get_type::<S4ManifestSizeMismatchError>(),
301 )?;
302 m.add("S4BackendError", py.get_type::<S4BackendError>())?;
303 m.add("S4IoError", py.get_type::<S4IoError>())?;
304 Ok(())
305}