dvpl-engine 0.1.3

dvpl-converter encodes and decodes DVPL-compressed World of Tanks Blitz assets with LZ4 and LZ4-HC support
Documentation
// docstrings here target Python hover, not rustdoc
#![allow(clippy::doc_markdown)]

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

use crate::DvplError;

// PyDvplError is the Rust-side name, exported to Python as "DvplError"
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"
);

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.
///
/// Parameters
/// ----------
/// data : bytes
///     Raw contents of a `.dvpl` file (payload + 20-byte footer).
///
/// Returns
/// -------
/// bytes
///     Decompressed original payload.
///
/// Raises
/// ------
/// DvplError
///     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.
///
/// Parameters
/// ----------
/// data : bytes
///     Uncompressed payload to wrap.
/// comp_type : int
///     Compression mode: `COMP_NONE` (0), `COMP_LZ4` (1), or `COMP_LZ4_HC` (2, default).
///
/// Returns
/// -------
/// bytes
///     DVPL blob (compressed payload + 20-byte footer).
///
/// 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))
}

#[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)?)?;

    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(())
}