use crate::checksum::calculate_crc32;
use crate::error::{CardError, Result};
use crate::format::CardFormat;
use crate::header::FLAG_HAS_CHECKSUM;
use std::fs;
use std::io::{Cursor, Read, Write};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GenericHeader {
pub magic: [u8; 4],
pub major: u8,
pub minor: u8,
pub flags: u16,
}
impl GenericHeader {
pub fn for_format<F: CardFormat>() -> Self {
Self {
magic: F::MAGIC,
major: F::VERSION_MAJOR,
minor: F::VERSION_MINOR,
flags: 0,
}
}
pub fn for_format_with_checksum<F: CardFormat>() -> Self {
Self {
magic: F::MAGIC,
major: F::VERSION_MAJOR,
minor: F::VERSION_MINOR,
flags: FLAG_HAS_CHECKSUM,
}
}
pub fn has_checksum(&self) -> bool {
self.flags & FLAG_HAS_CHECKSUM != 0
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
writer.write_all(&self.magic)?;
writer.write_all(&[self.major])?;
writer.write_all(&[self.minor])?;
writer.write_all(&self.flags.to_le_bytes())?;
Ok(())
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(8);
bytes.extend_from_slice(&self.magic);
bytes.push(self.major);
bytes.push(self.minor);
bytes.extend_from_slice(&self.flags.to_le_bytes());
bytes
}
pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
let mut version = [0u8; 2];
reader.read_exact(&mut version)?;
let mut flags_bytes = [0u8; 2];
reader.read_exact(&mut flags_bytes)?;
Ok(Self {
magic,
major: version[0],
minor: version[1],
flags: u16::from_le_bytes(flags_bytes),
})
}
pub fn validate<F: CardFormat>(&self) -> Result<()> {
F::validate_magic(&self.magic)?;
F::validate_version(self.major, self.minor)?;
Ok(())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct GenericMetadata {
pub id: String,
pub payload_size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ext: Option<serde_json::Value>,
}
impl GenericMetadata {
pub fn new(id: impl Into<String>, payload_size: u64) -> Self {
Self {
id: id.into(),
payload_size,
original_size: None,
ext: None,
}
}
pub fn with_original_size(mut self, size: u64) -> Self {
self.original_size = Some(size);
self
}
pub fn with_ext(mut self, ext: serde_json::Value) -> Self {
self.ext = Some(ext);
self
}
pub fn to_json(&self) -> Result<Vec<u8>> {
Ok(serde_json::to_vec(self)?)
}
pub fn from_json(bytes: &[u8]) -> Result<Self> {
Ok(serde_json::from_slice(bytes)?)
}
}
#[derive(Debug, Clone)]
pub struct GenericCard<F: CardFormat> {
pub header: GenericHeader,
pub metadata: GenericMetadata,
pub payload: Vec<u8>,
_format: PhantomData<F>,
}
impl<F: CardFormat> GenericCard<F> {
pub fn new(id: impl Into<String>, payload: Vec<u8>) -> Self {
let metadata = GenericMetadata::new(id, payload.len() as u64);
Self {
header: GenericHeader::for_format::<F>(),
metadata,
payload,
_format: PhantomData,
}
}
pub fn new_with_checksum(id: impl Into<String>, payload: Vec<u8>) -> Self {
let metadata = GenericMetadata::new(id, payload.len() as u64);
Self {
header: GenericHeader::for_format_with_checksum::<F>(),
metadata,
payload,
_format: PhantomData,
}
}
pub fn from_parts(metadata: GenericMetadata, payload: Vec<u8>) -> Result<Self> {
if payload.len() as u64 != metadata.payload_size {
return Err(CardError::PayloadSizeMismatch {
expected: metadata.payload_size,
actual: payload.len(),
});
}
Ok(Self {
header: GenericHeader::for_format::<F>(),
metadata,
payload,
_format: PhantomData,
})
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = Self::enforce_card_extension(path.as_ref())?;
let data = fs::read(&path)?;
Self::from_bytes(&data)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = Self::enforce_card_extension(path.as_ref())?;
let bytes = self.to_bytes()?;
fs::write(&path, bytes)?;
Ok(())
}
fn enforce_card_extension(path: &Path) -> Result<PathBuf> {
if let Some(ext) = path.extension() {
if ext == "card" {
return Err(CardError::InvalidFormat(
"Do not include .card extension in path - it is added automatically".to_string()
));
}
}
let mut path_buf = path.to_path_buf();
let mut new_name = path_buf
.file_name()
.map(|s| s.to_os_string())
.unwrap_or_default();
new_name.push(".card");
path_buf.set_file_name(new_name);
Ok(path_buf)
}
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 = GenericHeader::read_from(&mut cursor)?;
header.validate::<F>()?;
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 = GenericMetadata::from_json(&meta_json)?;
let payload_len = metadata.payload_size as usize;
let mut payload = vec![0u8; payload_len];
cursor.read_exact(&mut payload)?;
F::validate_payload(&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,
_format: PhantomData,
};
let calculated = card.calculate_checksum();
if calculated != stored_checksum {
return Err(CardError::ChecksumMismatch {
expected: stored_checksum,
actual: calculated,
});
}
Ok(card)
} else {
Ok(Self {
header,
metadata,
payload,
_format: PhantomData,
})
}
}
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn id(&self) -> &str {
&self.metadata.id
}
pub fn has_checksum(&self) -> bool {
self.header.has_checksum()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestFormat;
impl CardFormat for TestFormat {
const MAGIC: [u8; 4] = *b"TEST";
const VERSION_MAJOR: u8 = 1;
const VERSION_MINOR: u8 = 0;
fn format_name() -> &'static str {
"Test"
}
}
#[test]
fn test_generic_card_roundtrip() {
let payload = vec![1, 2, 3, 4, 5];
let card: GenericCard<TestFormat> = GenericCard::new("test::roundtrip", payload.clone());
let bytes = card.to_bytes().unwrap();
let loaded: GenericCard<TestFormat> = GenericCard::from_bytes(&bytes).unwrap();
assert_eq!(loaded.id(), "test::roundtrip");
assert_eq!(loaded.payload(), &payload);
}
#[test]
fn test_generic_card_with_checksum() {
let payload = vec![1, 2, 3, 4, 5];
let card: GenericCard<TestFormat> =
GenericCard::new_with_checksum("test::checksum", payload.clone());
assert!(card.has_checksum());
let bytes = card.to_bytes().unwrap();
let loaded: GenericCard<TestFormat> = GenericCard::from_bytes(&bytes).unwrap();
assert!(loaded.has_checksum());
assert_eq!(loaded.payload(), &payload);
}
#[test]
fn test_wrong_magic_fails() {
struct OtherFormat;
impl CardFormat for OtherFormat {
const MAGIC: [u8; 4] = *b"OTHE";
const VERSION_MAJOR: u8 = 1;
const VERSION_MINOR: u8 = 0;
fn format_name() -> &'static str {
"Other"
}
}
let card: GenericCard<TestFormat> = GenericCard::new("test", vec![1, 2, 3]);
let bytes = card.to_bytes().unwrap();
let result: Result<GenericCard<OtherFormat>> = GenericCard::from_bytes(&bytes);
assert!(matches!(result, Err(CardError::InvalidMagic(_))));
}
}