mlt-core 0.12.7

MapLibre Tile library code
Documentation
use fastpfor::{AnyLenCodec as _, FastPFor256};
use usize_cast::IntoUsize as _;

use crate::{Decoder, MltError, MltResult};

/// Decode `FastPFOR`-compressed data using the composite codec protocol.
///
/// The Java MLT encoder uses `Composition(FastPFOR(), VariableByte())`, matching
/// the C++ `CompositeCodec<FastPFor<8>, VariableByte>`. The wire format is:
///
/// 1. First u32 = number of compressed u32 words from the primary codec (`FastPFor`)
/// 2. Next N u32 words = primary codec (`FastPFor`) compressed data
/// 3. Remaining u32 words = secondary codec (`VByte`) compressed data
///
/// The compressed bytes are stored as big-endian u32 values by the Java encoder.
pub fn decode_fastpfor(data: &[u8], num_values: u32, dec: &mut Decoder) -> MltResult<Vec<u32>> {
    if num_values == 0 {
        // FIXME: eventually there should not be a header anywhere at all
        return if data.is_empty() {
            Ok(vec![])
        } else {
            Err(MltError::InvalidFastPforByteLength(0))
        };
    }

    // Convert big-endian bytes to u32 values
    if !data.len().is_multiple_of(4) {
        return Err(MltError::InvalidFastPforByteLength(data.len()));
    }
    // The Java MLT encoder writes compressed int[] -> byte[] in big-endian order.
    // We must convert BE bytes -> u32 to reconstruct the original integer values
    // that the Composition(FastPFOR, VariableByte) codec produced.
    let num_words = data.len() / 4;
    dec.consume_items::<u32>(num_words)?;
    let input: Vec<u32> = (0..num_words)
        .map(|i| {
            let o = i * 4;
            u32::from_be_bytes([data[o], data[o + 1], data[o + 2], data[o + 3]])
        })
        .collect();

    let mut result = Vec::new();
    FastPFor256::default().decode(&input, &mut result, Some(num_values))?;

    let Some(adjustment) = result
        .len()
        .checked_sub(num_values.into_usize())
        .and_then(|v| u32::try_from(v).ok())
    else {
        return Err(MltError::FastPforDecode(num_values, result.len()));
    };

    dec.adjust(adjustment);
    result.truncate(num_values.into_usize());

    Ok(result)
}

#[cfg(test)]
mod tests {
    use proptest::prelude::*;

    use super::*;
    use crate::test_helpers::dec;

    proptest! {
        #[test]
        fn test_fastpfor_roundtrip(data: Vec<u32>) {
            // FastPFor256 produces a non-empty output (VByte header) even for empty input,
            // but decode_fastpfor requires zero bytes when num_values == 0 - consistent
            // with how PhysicalEncoder guards `if !values.is_empty()`.
            prop_assume!(!data.is_empty());
            let mut encoded = Vec::new();
            FastPFor256::default().encode(&data, &mut encoded).unwrap();
            // Convert u32 words to big-endian bytes to match the wire format.
            let mut encoded2 = Vec::with_capacity(encoded.len() * 4);
            for word in &encoded {
                encoded2.extend_from_slice(&word.to_be_bytes());
            }
            let decoded = decode_fastpfor(&encoded2, data.len().try_into().unwrap(), &mut dec()).unwrap();
            prop_assert_eq!(data, decoded);
        }
    }

    #[test]
    fn test_decode_fastpfor_empty() {
        let decoded = decode_fastpfor(&[], 0, &mut dec()).unwrap();
        assert_eq!(decoded, [] as [u32; 0]);
    }
}