datacard-rs 1.0.0

Generic binary card format library with checksums and pluggable format traits
Documentation
//! Card implementation

use crate::checksum::calculate_crc32;
use crate::error::{CardError, Result};
use crate::header::{CardHeader, FLAG_HAS_CHECKSUM};
use crate::metadata::CardMetadata;
use bytepunch_rs::{Compressor, Decompressor, Dictionary};
use std::fs;
use std::io::{self, Cursor, Read, Write};
use std::path::Path;

/// A DataCard: BytePunch-compressed CML document with metadata
#[derive(Debug, Clone)]
pub struct Card {
    /// Card header
    pub header: CardHeader,

    /// Document metadata
    pub metadata: CardMetadata,

    /// Compressed CML data
    pub payload: Vec<u8>,
}

impl Card {
    /// Create a card from CML content
    pub fn from_cml(
        cml: &str,
        mut metadata: CardMetadata,
        dictionary: &Dictionary,
    ) -> Result<Self> {
        let compressor = Compressor::new(dictionary.clone());
        let payload = compressor.compress(cml)?;

        // Update metadata with actual sizes
        metadata.compressed_size = payload.len() as u64;
        if metadata.original_size.is_none() {
            metadata.original_size = Some(cml.len() as u64);
        }

        Ok(Self {
            header: CardHeader::new(),
            metadata,
            payload,
        })
    }

    /// Create a card from CML content with checksum
    pub fn from_cml_with_checksum(
        cml: &str,
        metadata: CardMetadata,
        dictionary: &Dictionary,
    ) -> Result<Self> {
        let mut card = Self::from_cml(cml, metadata, dictionary)?;
        card.header = CardHeader::with_flags(FLAG_HAS_CHECKSUM);
        Ok(card)
    }

    /// Create card from pre-compressed data
    pub fn from_compressed(metadata: CardMetadata, payload: Vec<u8>) -> Result<Self> {
        // Validate payload size matches metadata
        if payload.len() as u64 != metadata.compressed_size {
            return Err(CardError::PayloadSizeMismatch {
                expected: metadata.compressed_size,
                actual: payload.len(),
            });
        }

        Ok(Self {
            header: CardHeader::new(),
            metadata,
            payload,
        })
    }

    /// Create card from pre-compressed data with checksum
    pub fn from_compressed_with_checksum(
        metadata: CardMetadata,
        payload: Vec<u8>,
    ) -> Result<Self> {
        let mut card = Self::from_compressed(metadata, payload)?;
        card.header = CardHeader::with_flags(FLAG_HAS_CHECKSUM);
        Ok(card)
    }

    /// Decompress card back to CML content
    pub fn to_cml(&self, dictionary: &Dictionary) -> Result<String> {
        let decompressor = Decompressor::new(dictionary.clone());
        let decompressed = decompressor.decompress(&self.payload)?;
        Ok(decompressed)
    }

    /// Load card from file
    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
        let data = fs::read(path)?;
        Self::from_bytes(&data)
    }

    /// Save card to file
    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        let bytes = self.to_bytes()?;
        fs::write(path, bytes)?;
        Ok(())
    }

    /// Calculate CRC32 checksum for this card
    pub fn calculate_checksum(&self) -> u32 {
        let header_bytes = self.header.to_bytes();
        let meta_json = self.metadata.to_json().unwrap();
        let meta_len_bytes = (meta_json.len() as u32).to_le_bytes();

        calculate_crc32(&[&header_bytes, &meta_len_bytes, &meta_json, &self.payload])
    }

    /// Serialize card to bytes
    pub fn to_bytes(&self) -> Result<Vec<u8>> {
        let mut buffer = Vec::new();

        // Write header
        self.header.write_to(&mut buffer)?;

        // Write metadata
        let meta_json = self.metadata.to_json()?;
        if meta_json.len() > 65536 {
            return Err(CardError::MetadataTooLarge(meta_json.len()));
        }

        buffer.write_all(&(meta_json.len() as u32).to_le_bytes())?;
        buffer.write_all(&meta_json)?;

        // Write payload
        buffer.write_all(&self.payload)?;

        // Write checksum if enabled
        if self.header.has_checksum() {
            let checksum = self.calculate_checksum();
            buffer.write_all(&checksum.to_le_bytes())?;
        }

        Ok(buffer)
    }

    /// Deserialize card from bytes
    pub fn from_bytes(data: &[u8]) -> Result<Self> {
        let mut cursor = Cursor::new(data);

        // Read and validate header
        let header = CardHeader::read_from(&mut cursor)?;
        header.validate()?;

        // Read metadata
        let mut meta_len_bytes = [0u8; 4];
        cursor.read_exact(&mut meta_len_bytes)?;
        let meta_len = u32::from_le_bytes(meta_len_bytes) as usize;

        let mut meta_json = vec![0u8; meta_len];
        cursor.read_exact(&mut meta_json)?;
        let metadata = CardMetadata::from_json(&meta_json)?;

        // Read payload
        let payload_len = metadata.compressed_size as usize;
        let mut payload = vec![0u8; payload_len];
        cursor.read_exact(&mut payload)?;

        // Validate checksum if present
        if header.has_checksum() {
            let mut checksum_bytes = [0u8; 4];
            cursor.read_exact(&mut checksum_bytes)?;
            let stored_checksum = u32::from_le_bytes(checksum_bytes);

            let card = Self {
                header,
                metadata,
                payload,
            };

            let calculated_checksum = card.calculate_checksum();
            if calculated_checksum != stored_checksum {
                return Err(CardError::ChecksumMismatch {
                    expected: stored_checksum,
                    actual: calculated_checksum,
                });
            }

            Ok(card)
        } else {
            Ok(Self {
                header,
                metadata,
                payload,
            })
        }
    }

    /// Get compressed size in bytes
    pub fn compressed_size(&self) -> usize {
        self.payload.len()
    }

    /// Get original size if available
    pub fn original_size(&self) -> Option<u64> {
        self.metadata.original_size
    }

    /// Get compression ratio (original / compressed)
    pub fn compression_ratio(&self) -> Option<f64> {
        self.metadata
            .original_size
            .map(|orig| orig as f64 / self.payload.len() as f64)
    }

    /// Get document ID
    pub fn id(&self) -> &str {
        &self.metadata.id
    }

    /// Get CML profile if set
    pub fn profile(&self) -> Option<&str> {
        self.metadata.profile.as_deref()
    }

    /// Check if card has checksum
    pub fn has_checksum(&self) -> bool {
        self.header.has_checksum()
    }
}

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

    #[test]
    fn test_card_from_compressed() {
        let metadata = CardMetadata::new("test", 10);
        let payload = vec![0u8; 10];

        let card = Card::from_compressed(metadata, payload).unwrap();
        assert_eq!(card.compressed_size(), 10);
        assert!(!card.has_checksum());
    }

    #[test]
    fn test_card_size_mismatch() {
        let metadata = CardMetadata::new("test", 10);
        let payload = vec![0u8; 5]; // Wrong size

        let result = Card::from_compressed(metadata, payload);
        assert!(result.is_err());
    }

    #[test]
    fn test_card_bytes_roundtrip() {
        let metadata = CardMetadata::new("test::roundtrip", 5).with_profile("test");
        let payload = vec![1, 2, 3, 4, 5];

        let card = Card::from_compressed(metadata, payload).unwrap();
        let bytes = card.to_bytes().unwrap();
        let loaded = Card::from_bytes(&bytes).unwrap();

        assert_eq!(loaded.id(), "test::roundtrip");
        assert_eq!(loaded.compressed_size(), 5);
        assert_eq!(loaded.payload, vec![1, 2, 3, 4, 5]);
    }

    #[test]
    fn test_card_with_checksum() {
        let metadata = CardMetadata::new("test::checksum", 5);
        let payload = vec![1, 2, 3, 4, 5];

        let card = Card::from_compressed_with_checksum(metadata, payload).unwrap();
        assert!(card.has_checksum());

        let bytes = card.to_bytes().unwrap();
        let loaded = Card::from_bytes(&bytes).unwrap();

        assert_eq!(loaded.id(), "test::checksum");
        assert_eq!(loaded.payload, vec![1, 2, 3, 4, 5]);
        assert!(loaded.has_checksum());
    }

    #[test]
    fn test_card_checksum_validation() {
        let metadata = CardMetadata::new("test", 5);
        let payload = vec![1, 2, 3, 4, 5];

        let card = Card::from_compressed_with_checksum(metadata, payload).unwrap();
        let mut bytes = card.to_bytes().unwrap();

        // Corrupt the checksum
        let len = bytes.len();
        bytes[len - 1] ^= 0xFF;

        let result = Card::from_bytes(&bytes);
        assert!(matches!(result, Err(CardError::ChecksumMismatch { .. })));
    }
}