use std::io::Write;
use super::RecordType;
use crate::error::{Result, SclsError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header {
pub version: u32,
}
impl Header {
pub const MAGIC: &'static [u8; 4] = b"SCLS";
pub const CURRENT_VERSION: u32 = 1;
pub const fn new(version: u32) -> Self {
Self { version }
}
pub const fn current() -> Self {
Self::new(Self::CURRENT_VERSION)
}
pub fn is_supported(&self) -> bool {
self.version == Self::CURRENT_VERSION }
pub fn write(&self, writer: &mut impl Write) -> Result<()> {
writer.write_all(&9u32.to_be_bytes())?;
writer.write_all(&[RecordType::Header.to_byte()])?;
writer.write_all(Self::MAGIC)?;
writer.write_all(&self.version.to_be_bytes())?;
Ok(())
}
}
impl TryFrom<&[u8]> for Header {
type Error = SclsError;
fn try_from(value: &[u8]) -> Result<Self> {
if value.len() != 8 {
return Err(SclsError::MalformedRecord(format!(
"header must be exactly 8 bytes, found {}",
value.len()
)));
}
let magic = &value[0..4];
if magic != Header::MAGIC {
return Err(SclsError::InvalidMagic {
found: magic.to_vec(),
});
}
let version_bytes: [u8; 4] = value[4..8].try_into().unwrap();
let version = u32::from_be_bytes(version_bytes);
Ok(Self::new(version))
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use proptest::prelude::*;
use super::*;
use crate::reader::Record;
proptest! {
#[test]
fn rejects_wrong_length(bytes in prop::collection::vec(any::<u8>(),0..100)) {
prop_assume!(bytes.len() != 8);
let result = Header::try_from(bytes.as_slice());
prop_assert!(result.is_err());
}
#[test]
fn rejects_wrong_magic(
wrong_magic in prop::collection::vec(any::<u8>(), 4..=4),
version_bytes in prop::array::uniform4(any::<u8>())
) {
prop_assume!(wrong_magic.as_slice() != b"SCLS");
let mut bytes = wrong_magic;
bytes.extend_from_slice(&version_bytes);
let result = Header::try_from(bytes.as_slice());
let is_invalid_magic = matches!(result, Err(SclsError::InvalidMagic { .. }));
prop_assert!(is_invalid_magic);
}
#[test]
fn accepts_any_valid_version(version in any::<u32>()) {
let mut bytes = b"SCLS".to_vec();
bytes.extend_from_slice(&version.to_be_bytes());
let result = Header::try_from(bytes.as_slice());
prop_assert!(result.is_ok());
prop_assert_eq!(result.unwrap().version, version);
}
}
#[test]
fn roundtrip() -> Result<()> {
let mut sink: Vec<u8> = Vec::new();
let written = Header::current();
written.write(&mut sink)?;
let mut source = Cursor::new(sink);
let record = Record::read_next(&mut source)?;
let Some(Record::Header(read)) = record else {
panic!("expected header record, got {record:?}");
};
assert_eq!(read, written);
Ok(())
}
}