Skip to main content

datacard_rs/
card.rs

1//! Card implementation
2
3use crate::checksum::calculate_crc32;
4use crate::error::{CardError, Result};
5use crate::header::{CardHeader, FLAG_HAS_CHECKSUM};
6use crate::metadata::CardMetadata;
7use bytepunch_rs::{Compressor, Decompressor, Dictionary};
8use std::fs;
9use std::io::{self, Cursor, Read, Write};
10use std::path::Path;
11
12/// A DataCard: BytePunch-compressed CML document with metadata
13#[derive(Debug, Clone)]
14pub struct Card {
15    /// Card header
16    pub header: CardHeader,
17
18    /// Document metadata
19    pub metadata: CardMetadata,
20
21    /// Compressed CML data
22    pub payload: Vec<u8>,
23}
24
25impl Card {
26    /// Create a card from CML content
27    pub fn from_cml(
28        cml: &str,
29        mut metadata: CardMetadata,
30        dictionary: &Dictionary,
31    ) -> Result<Self> {
32        let compressor = Compressor::new(dictionary.clone());
33        let payload = compressor.compress(cml)?;
34
35        // Update metadata with actual sizes
36        metadata.compressed_size = payload.len() as u64;
37        if metadata.original_size.is_none() {
38            metadata.original_size = Some(cml.len() as u64);
39        }
40
41        Ok(Self {
42            header: CardHeader::new(),
43            metadata,
44            payload,
45        })
46    }
47
48    /// Create a card from CML content with checksum
49    pub fn from_cml_with_checksum(
50        cml: &str,
51        metadata: CardMetadata,
52        dictionary: &Dictionary,
53    ) -> Result<Self> {
54        let mut card = Self::from_cml(cml, metadata, dictionary)?;
55        card.header = CardHeader::with_flags(FLAG_HAS_CHECKSUM);
56        Ok(card)
57    }
58
59    /// Create card from pre-compressed data
60    pub fn from_compressed(metadata: CardMetadata, payload: Vec<u8>) -> Result<Self> {
61        // Validate payload size matches metadata
62        if payload.len() as u64 != metadata.compressed_size {
63            return Err(CardError::PayloadSizeMismatch {
64                expected: metadata.compressed_size,
65                actual: payload.len(),
66            });
67        }
68
69        Ok(Self {
70            header: CardHeader::new(),
71            metadata,
72            payload,
73        })
74    }
75
76    /// Create card from pre-compressed data with checksum
77    pub fn from_compressed_with_checksum(
78        metadata: CardMetadata,
79        payload: Vec<u8>,
80    ) -> Result<Self> {
81        let mut card = Self::from_compressed(metadata, payload)?;
82        card.header = CardHeader::with_flags(FLAG_HAS_CHECKSUM);
83        Ok(card)
84    }
85
86    /// Decompress card back to CML content
87    pub fn to_cml(&self, dictionary: &Dictionary) -> Result<String> {
88        let decompressor = Decompressor::new(dictionary.clone());
89        let decompressed = decompressor.decompress(&self.payload)?;
90        Ok(decompressed)
91    }
92
93    /// Load card from file
94    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
95        let data = fs::read(path)?;
96        Self::from_bytes(&data)
97    }
98
99    /// Save card to file
100    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
101        let bytes = self.to_bytes()?;
102        fs::write(path, bytes)?;
103        Ok(())
104    }
105
106    /// Calculate CRC32 checksum for this card
107    pub fn calculate_checksum(&self) -> u32 {
108        let header_bytes = self.header.to_bytes();
109        let meta_json = self.metadata.to_json().unwrap();
110        let meta_len_bytes = (meta_json.len() as u32).to_le_bytes();
111
112        calculate_crc32(&[&header_bytes, &meta_len_bytes, &meta_json, &self.payload])
113    }
114
115    /// Serialize card to bytes
116    pub fn to_bytes(&self) -> Result<Vec<u8>> {
117        let mut buffer = Vec::new();
118
119        // Write header
120        self.header.write_to(&mut buffer)?;
121
122        // Write metadata
123        let meta_json = self.metadata.to_json()?;
124        if meta_json.len() > 65536 {
125            return Err(CardError::MetadataTooLarge(meta_json.len()));
126        }
127
128        buffer.write_all(&(meta_json.len() as u32).to_le_bytes())?;
129        buffer.write_all(&meta_json)?;
130
131        // Write payload
132        buffer.write_all(&self.payload)?;
133
134        // Write checksum if enabled
135        if self.header.has_checksum() {
136            let checksum = self.calculate_checksum();
137            buffer.write_all(&checksum.to_le_bytes())?;
138        }
139
140        Ok(buffer)
141    }
142
143    /// Deserialize card from bytes
144    pub fn from_bytes(data: &[u8]) -> Result<Self> {
145        let mut cursor = Cursor::new(data);
146
147        // Read and validate header
148        let header = CardHeader::read_from(&mut cursor)?;
149        header.validate()?;
150
151        // Read metadata
152        let mut meta_len_bytes = [0u8; 4];
153        cursor.read_exact(&mut meta_len_bytes)?;
154        let meta_len = u32::from_le_bytes(meta_len_bytes) as usize;
155
156        let mut meta_json = vec![0u8; meta_len];
157        cursor.read_exact(&mut meta_json)?;
158        let metadata = CardMetadata::from_json(&meta_json)?;
159
160        // Read payload
161        let payload_len = metadata.compressed_size as usize;
162        let mut payload = vec![0u8; payload_len];
163        cursor.read_exact(&mut payload)?;
164
165        // Validate checksum if present
166        if header.has_checksum() {
167            let mut checksum_bytes = [0u8; 4];
168            cursor.read_exact(&mut checksum_bytes)?;
169            let stored_checksum = u32::from_le_bytes(checksum_bytes);
170
171            let card = Self {
172                header,
173                metadata,
174                payload,
175            };
176
177            let calculated_checksum = card.calculate_checksum();
178            if calculated_checksum != stored_checksum {
179                return Err(CardError::ChecksumMismatch {
180                    expected: stored_checksum,
181                    actual: calculated_checksum,
182                });
183            }
184
185            Ok(card)
186        } else {
187            Ok(Self {
188                header,
189                metadata,
190                payload,
191            })
192        }
193    }
194
195    /// Get compressed size in bytes
196    pub fn compressed_size(&self) -> usize {
197        self.payload.len()
198    }
199
200    /// Get original size if available
201    pub fn original_size(&self) -> Option<u64> {
202        self.metadata.original_size
203    }
204
205    /// Get compression ratio (original / compressed)
206    pub fn compression_ratio(&self) -> Option<f64> {
207        self.metadata
208            .original_size
209            .map(|orig| orig as f64 / self.payload.len() as f64)
210    }
211
212    /// Get document ID
213    pub fn id(&self) -> &str {
214        &self.metadata.id
215    }
216
217    /// Get CML profile if set
218    pub fn profile(&self) -> Option<&str> {
219        self.metadata.profile.as_deref()
220    }
221
222    /// Check if card has checksum
223    pub fn has_checksum(&self) -> bool {
224        self.header.has_checksum()
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_card_from_compressed() {
234        let metadata = CardMetadata::new("test", 10);
235        let payload = vec![0u8; 10];
236
237        let card = Card::from_compressed(metadata, payload).unwrap();
238        assert_eq!(card.compressed_size(), 10);
239        assert!(!card.has_checksum());
240    }
241
242    #[test]
243    fn test_card_size_mismatch() {
244        let metadata = CardMetadata::new("test", 10);
245        let payload = vec![0u8; 5]; // Wrong size
246
247        let result = Card::from_compressed(metadata, payload);
248        assert!(result.is_err());
249    }
250
251    #[test]
252    fn test_card_bytes_roundtrip() {
253        let metadata = CardMetadata::new("test::roundtrip", 5).with_profile("test");
254        let payload = vec![1, 2, 3, 4, 5];
255
256        let card = Card::from_compressed(metadata, payload).unwrap();
257        let bytes = card.to_bytes().unwrap();
258        let loaded = Card::from_bytes(&bytes).unwrap();
259
260        assert_eq!(loaded.id(), "test::roundtrip");
261        assert_eq!(loaded.compressed_size(), 5);
262        assert_eq!(loaded.payload, vec![1, 2, 3, 4, 5]);
263    }
264
265    #[test]
266    fn test_card_with_checksum() {
267        let metadata = CardMetadata::new("test::checksum", 5);
268        let payload = vec![1, 2, 3, 4, 5];
269
270        let card = Card::from_compressed_with_checksum(metadata, payload).unwrap();
271        assert!(card.has_checksum());
272
273        let bytes = card.to_bytes().unwrap();
274        let loaded = Card::from_bytes(&bytes).unwrap();
275
276        assert_eq!(loaded.id(), "test::checksum");
277        assert_eq!(loaded.payload, vec![1, 2, 3, 4, 5]);
278        assert!(loaded.has_checksum());
279    }
280
281    #[test]
282    fn test_card_checksum_validation() {
283        let metadata = CardMetadata::new("test", 5);
284        let payload = vec![1, 2, 3, 4, 5];
285
286        let card = Card::from_compressed_with_checksum(metadata, payload).unwrap();
287        let mut bytes = card.to_bytes().unwrap();
288
289        // Corrupt the checksum
290        let len = bytes.len();
291        bytes[len - 1] ^= 0xFF;
292
293        let result = Card::from_bytes(&bytes);
294        assert!(matches!(result, Err(CardError::ChecksumMismatch { .. })));
295    }
296}