use bytemuck::{Pod, Zeroable};
use crate::IbuError;
pub const MAGIC: u32 = 0x21554249; pub const VERSION: u32 = 2;
pub const HEADER_SIZE: usize = std::mem::size_of::<Header>();
#[derive(Copy, Clone, Pod, Zeroable, Debug, PartialEq, Eq, Hash)]
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
#[repr(C)]
pub struct Header {
pub magic: u32,
pub version: u32,
pub bc_len: u32,
pub umi_len: u32,
pub flags: u64,
pub reserved: [u8; 8],
}
impl Header {
pub fn new(bc_len: u32, umi_len: u32) -> Self {
Self {
magic: MAGIC,
version: VERSION,
bc_len,
umi_len,
flags: 0,
reserved: [0; 8],
}
}
pub fn set_sorted(&mut self) {
self.flags |= 1;
}
pub fn sorted(&self) -> bool {
self.flags & 1 != 0
}
pub fn validate(&self) -> crate::Result<()> {
if self.magic != MAGIC {
return Err(IbuError::InvalidMagicNumber {
expected: MAGIC,
actual: self.magic,
});
}
if self.version != VERSION {
return Err(IbuError::InvalidVersion {
expected: VERSION,
actual: self.version,
});
}
if self.bc_len == 0 || self.bc_len > 32 {
return Err(IbuError::InvalidBarcodeLength(self.bc_len));
}
if self.umi_len == 0 || self.umi_len > 32 {
return Err(IbuError::InvalidUmiLength(self.umi_len));
}
Ok(())
}
pub fn as_bytes(&self) -> &[u8] {
bytemuck::bytes_of(self)
}
pub fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::from_bytes(bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_creation() {
let header = Header::new(16, 12);
assert_eq!(header.magic, MAGIC);
assert_eq!(header.version, VERSION);
assert_eq!(header.bc_len, 16);
assert_eq!(header.umi_len, 12);
assert_eq!(header.flags, 0);
assert_eq!(header.reserved, [0; 8]);
}
#[test]
fn test_header_size() {
assert_eq!(HEADER_SIZE, 32);
assert_eq!(std::mem::size_of::<Header>(), HEADER_SIZE);
}
#[test]
fn test_sorted_flag() {
let mut header = Header::new(16, 12);
assert!(!header.sorted());
assert_eq!(header.flags, 0);
header.set_sorted();
assert!(header.sorted());
assert_eq!(header.flags, 1);
header.set_sorted();
assert!(header.sorted());
assert_eq!(header.flags, 1);
}
#[test]
fn test_validation_valid_header() {
let header = Header::new(16, 12);
assert!(header.validate().is_ok());
let header = Header::new(1, 1);
assert!(header.validate().is_ok());
let header = Header::new(32, 32);
assert!(header.validate().is_ok());
}
#[test]
fn test_validation_invalid_magic() {
let mut header = Header::new(16, 12);
header.magic = 0x12345678;
match header.validate() {
Err(IbuError::InvalidMagicNumber { expected, actual }) => {
assert_eq!(expected, MAGIC);
assert_eq!(actual, 0x12345678);
}
other => panic!("Expected InvalidMagicNumber, got: {:?}", other),
}
}
#[test]
fn test_validation_invalid_version() {
let mut header = Header::new(16, 12);
header.version = 1;
match header.validate() {
Err(IbuError::InvalidVersion { expected, actual }) => {
assert_eq!(expected, VERSION);
assert_eq!(actual, 1);
}
other => panic!("Expected InvalidVersion, got: {:?}", other),
}
}
#[test]
fn test_validation_invalid_barcode_length() {
let mut header = Header::new(16, 12);
header.bc_len = 0;
match header.validate() {
Err(IbuError::InvalidBarcodeLength(len)) => assert_eq!(len, 0),
other => panic!("Expected InvalidBarcodeLength(0), got: {:?}", other),
}
header.bc_len = 33;
match header.validate() {
Err(IbuError::InvalidBarcodeLength(len)) => assert_eq!(len, 33),
other => panic!("Expected InvalidBarcodeLength(33), got: {:?}", other),
}
}
#[test]
fn test_validation_invalid_umi_length() {
let mut header = Header::new(16, 12);
header.umi_len = 0;
match header.validate() {
Err(IbuError::InvalidUmiLength(len)) => assert_eq!(len, 0),
other => panic!("Expected InvalidUmiLength(0), got: {:?}", other),
}
header.umi_len = 33;
match header.validate() {
Err(IbuError::InvalidUmiLength(len)) => assert_eq!(len, 33),
other => panic!("Expected InvalidUmiLength(33), got: {:?}", other),
}
}
#[test]
fn test_byte_conversion_roundtrip() {
let original = Header::new(20, 10);
let bytes = original.as_bytes();
assert_eq!(bytes.len(), HEADER_SIZE);
let reconstructed = Header::from_bytes(bytes);
assert_eq!(original, reconstructed);
}
#[test]
fn test_byte_conversion_with_sorted_flag() {
let mut original = Header::new(16, 12);
original.set_sorted();
let bytes = original.as_bytes();
let reconstructed = Header::from_bytes(bytes);
assert_eq!(original, reconstructed);
assert!(reconstructed.sorted());
}
#[test]
fn test_magic_constant() {
let magic_bytes = MAGIC.to_le_bytes();
assert_eq!(magic_bytes, [b'I', b'B', b'U', b'!']);
}
#[test]
fn test_version_constant() {
assert_eq!(VERSION, 2);
}
#[test]
fn test_header_derives() {
let header1 = Header::new(16, 12);
let header2 = Header::new(16, 12);
let header3 = Header::new(20, 10);
assert_eq!(header1, header2);
assert_ne!(header1, header3);
let cloned = header1.clone();
assert_eq!(header1, cloned);
let copied = header1;
assert_eq!(header1, copied);
let debug_str = format!("{:?}", header1);
assert!(debug_str.contains("Header"));
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(header1, "value");
assert_eq!(map.get(&header2), Some(&"value"));
}
}