1use 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#[derive(Debug, Clone)]
14pub struct Card {
15 pub header: CardHeader,
17
18 pub metadata: CardMetadata,
20
21 pub payload: Vec<u8>,
23}
24
25impl Card {
26 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 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 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 pub fn from_compressed(metadata: CardMetadata, payload: Vec<u8>) -> Result<Self> {
61 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 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 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 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
95 let data = fs::read(path)?;
96 Self::from_bytes(&data)
97 }
98
99 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 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 pub fn to_bytes(&self) -> Result<Vec<u8>> {
117 let mut buffer = Vec::new();
118
119 self.header.write_to(&mut buffer)?;
121
122 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 buffer.write_all(&self.payload)?;
133
134 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 pub fn from_bytes(data: &[u8]) -> Result<Self> {
145 let mut cursor = Cursor::new(data);
146
147 let header = CardHeader::read_from(&mut cursor)?;
149 header.validate()?;
150
151 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 let payload_len = metadata.compressed_size as usize;
162 let mut payload = vec![0u8; payload_len];
163 cursor.read_exact(&mut payload)?;
164
165 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 pub fn compressed_size(&self) -> usize {
197 self.payload.len()
198 }
199
200 pub fn original_size(&self) -> Option<u64> {
202 self.metadata.original_size
203 }
204
205 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 pub fn id(&self) -> &str {
214 &self.metadata.id
215 }
216
217 pub fn profile(&self) -> Option<&str> {
219 self.metadata.profile.as_deref()
220 }
221
222 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]; 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 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}