dvpl-engine 0.1.1

DVPL file format engine for World of Tanks Blitz
Documentation
//! `PyO3` bindings for [`dvpl_engine`](crate).
//!
//! Thin wrappers around [`crate::encode`] and [`crate::decode`] that convert
//! between Rust types and Python `bytes`. Enabled by the `python` feature.

use pyo3::create_exception;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;

use crate::DvplError;

// Python exception hierarchy - PyDvplError is the Rust-side name,
// exported to Python as "DvplError" in the module registration below
create_exception!(
    dvpl_engine,
    PyDvplError,
    PyValueError,
    "Base error for all DVPL operations."
);
create_exception!(
    dvpl_engine,
    TooSmallError,
    PyDvplError,
    "Input shorter than 20 bytes (footer size)."
);
create_exception!(
    dvpl_engine,
    BadMagicError,
    PyDvplError,
    "Footer magic does not match b\"DVPL\"."
);
create_exception!(
    dvpl_engine,
    SizeMismatchError,
    PyDvplError,
    "Payload length disagrees with footer."
);
create_exception!(
    dvpl_engine,
    CrcMismatchError,
    PyDvplError,
    "CRC32 of payload does not match footer checksum."
);
create_exception!(
    dvpl_engine,
    DecompressedSizeMismatchError,
    PyDvplError,
    "Decompressed output length disagrees with footer."
);
create_exception!(
    dvpl_engine,
    UnknownCompressionError,
    PyDvplError,
    "Unrecognized compression type value."
);
create_exception!(
    dvpl_engine,
    Lz4Error,
    PyDvplError,
    "Upstream lz4 error during compress/decompress."
);

/// Maps every [`DvplError`] variant to its corresponding Python exception.
impl From<DvplError> for PyErr {
    fn from(err: DvplError) -> Self {
        let msg = err.to_string();
        match err {
            DvplError::TooSmall(_) => TooSmallError::new_err(msg),
            DvplError::BadMagic(_) => BadMagicError::new_err(msg),
            DvplError::SizeMismatch { .. } => SizeMismatchError::new_err(msg),
            DvplError::CrcMismatch { .. } => CrcMismatchError::new_err(msg),
            DvplError::DecompressedSizeMismatch { .. } => {
                DecompressedSizeMismatchError::new_err(msg)
            }
            DvplError::UnknownCompression(_) => UnknownCompressionError::new_err(msg),
            DvplError::Lz4(_) => Lz4Error::new_err(msg),
        }
    }
}

/// Decode a DVPL-wrapped blob, verifying integrity.
///
/// Takes raw `.dvpl` file contents (payload + 20-byte footer) and returns
/// the decompressed original payload as `bytes`.
///
/// # Errors
///
/// Raises a `DvplError` subclass on bad magic, CRC mismatch, size mismatch,
/// or unknown compression type.
#[pyfunction]
fn decode<'py>(py: Python<'py>, data: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
    let result = crate::decode(data)?;
    Ok(PyBytes::new(py, &result))
}

/// Encode raw data into DVPL format.
///
/// Compresses `data` and returns a DVPL blob (compressed payload + 20-byte footer).
///
/// # Arguments
///
/// * `data` - Uncompressed payload to wrap.
/// * `comp_type` - Compression mode: [`crate::COMP_NONE`] (0), [`crate::COMP_LZ4`] (1), or
///   [`crate::COMP_LZ4_HC`] (2, default).
///
/// # Errors
///
/// Raises [`UnknownCompressionError`] if `comp_type` is not a recognized value.
#[pyfunction]
#[pyo3(signature = (data, comp_type=2))]
fn encode<'py>(py: Python<'py>, data: &[u8], comp_type: u32) -> PyResult<Bound<'py, PyBytes>> {
    let result = crate::encode(data, comp_type)?;
    Ok(PyBytes::new(py, &result))
}

/// Python module exposing DVPL [`encode`]/[`decode`], compression constants, and exceptions.
#[pymodule]
fn dvpl_engine(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add("COMP_NONE", crate::COMP_NONE)?;
    m.add("COMP_LZ4", crate::COMP_LZ4)?;
    m.add("COMP_LZ4_HC", crate::COMP_LZ4_HC)?;
    m.add_function(wrap_pyfunction!(decode, m)?)?;
    m.add_function(wrap_pyfunction!(encode, m)?)?;
    // export PyDvplError as "DvplError" to Python
    m.add("DvplError", m.py().get_type::<PyDvplError>())?;
    m.add("TooSmallError", m.py().get_type::<TooSmallError>())?;
    m.add("BadMagicError", m.py().get_type::<BadMagicError>())?;
    m.add("SizeMismatchError", m.py().get_type::<SizeMismatchError>())?;
    m.add("CrcMismatchError", m.py().get_type::<CrcMismatchError>())?;
    m.add(
        "DecompressedSizeMismatchError",
        m.py().get_type::<DecompressedSizeMismatchError>(),
    )?;
    m.add(
        "UnknownCompressionError",
        m.py().get_type::<UnknownCompressionError>(),
    )?;
    m.add("Lz4Error", m.py().get_type::<Lz4Error>())?;
    Ok(())
}