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;
#[derive(Debug, Clone)]
pub struct Card {
pub header: CardHeader,
pub metadata: CardMetadata,
pub payload: Vec<u8>,
}
impl Card {
pub fn from_cml(
cml: &str,
mut metadata: CardMetadata,
dictionary: &Dictionary,
) -> Result<Self> {
let compressor = Compressor::new(dictionary.clone());
let payload = compressor.compress(cml)?;
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,
})
}
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)
}
pub fn from_compressed(metadata: CardMetadata, payload: Vec<u8>) -> Result<Self> {
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,
})
}
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)
}
pub fn to_cml(&self, dictionary: &Dictionary) -> Result<String> {
let decompressor = Decompressor::new(dictionary.clone());
let decompressed = decompressor.decompress(&self.payload)?;
Ok(decompressed)
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let data = fs::read(path)?;
Self::from_bytes(&data)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let bytes = self.to_bytes()?;
fs::write(path, bytes)?;
Ok(())
}
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])
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
self.header.write_to(&mut buffer)?;
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)?;
buffer.write_all(&self.payload)?;
if self.header.has_checksum() {
let checksum = self.calculate_checksum();
buffer.write_all(&checksum.to_le_bytes())?;
}
Ok(buffer)
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let mut cursor = Cursor::new(data);
let header = CardHeader::read_from(&mut cursor)?;
header.validate()?;
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)?;
let payload_len = metadata.compressed_size as usize;
let mut payload = vec![0u8; payload_len];
cursor.read_exact(&mut payload)?;
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,
})
}
}
pub fn compressed_size(&self) -> usize {
self.payload.len()
}
pub fn original_size(&self) -> Option<u64> {
self.metadata.original_size
}
pub fn compression_ratio(&self) -> Option<f64> {
self.metadata
.original_size
.map(|orig| orig as f64 / self.payload.len() as f64)
}
pub fn id(&self) -> &str {
&self.metadata.id
}
pub fn profile(&self) -> Option<&str> {
self.metadata.profile.as_deref()
}
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];
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();
let len = bytes.len();
bytes[len - 1] ^= 0xFF;
let result = Card::from_bytes(&bytes);
assert!(matches!(result, Err(CardError::ChecksumMismatch { .. })));
}
}