use std::mem::{MaybeUninit, size_of};
use std::sync::atomic;
use crate::{shm::ShmError, syserror};
use tracing::error;
pub const SHM_MAGIC: [u32; 2] = [0x414D_5A4E, 0x4342_0200];
pub const CLOCKBOUND_SHM_SUPPORTED_VERSION: u16 = 2_u16;
pub const CLOCKBOUND_SHM_LATEST_VERSION: u16 = 3_u16;
#[repr(C, align(8))]
#[derive(Debug)]
pub struct ShmHeader {
pub magic: [u32; 2],
pub segsize: atomic::AtomicU32,
pub version: atomic::AtomicU16,
pub generation: atomic::AtomicU16,
}
impl ShmHeader {
#[expect(clippy::cast_sign_loss, reason = "guarded")]
pub fn read(fdesc: i32) -> Result<Self, ShmError> {
let mut header_buf: MaybeUninit<ShmHeader> = MaybeUninit::uninit();
match unsafe {
libc::read(
fdesc,
header_buf.as_mut_ptr().cast(),
size_of::<ShmHeader>(),
)
} {
ret if ret < 0 => return syserror!(String::from("Failed to read SHM segment")),
ret if (ret as usize) < size_of::<ShmHeader>() => {
return Err(ShmError::SegmentNotInitialized(format!(
"SHM segment too short [{} < {}]",
ret,
size_of::<ShmHeader>(),
)));
}
_ => (),
}
let header = unsafe { header_buf.assume_init() };
header.is_valid()?;
Ok(header)
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "bulk expect lints. Can fix later"
)]
fn matches_magic(&self, magic: &[u32; 2]) -> bool {
self.magic == *magic
}
fn has_valid_version(&self) -> bool {
let version = self.version.load(atomic::Ordering::Relaxed);
let min_version = version >> 8;
let cur_version = version & 0x00ff;
match cur_version {
2 => min_version == 0,
3 => min_version == 3,
_ => false,
}
}
fn is_initialized(&self) -> bool {
let generation = self.generation.load(atomic::Ordering::Relaxed);
generation > 0
}
fn is_well_formed(&self) -> bool {
let segsize = self.segsize.load(atomic::Ordering::Relaxed);
segsize as usize >= size_of::<Self>()
}
fn is_valid(&self) -> Result<(), ShmError> {
if !self.matches_magic(&SHM_MAGIC) {
let msg = String::from("ClockBound SHM header does not have a matching magic number.");
error!(msg);
return Err(ShmError::SegmentMalformed(msg));
}
if !self.has_valid_version() {
let msg = String::from("ClockBound SHM header does not have a valid version number.");
error!(msg);
return Err(ShmError::SegmentVersionNotSupported(msg));
}
if !self.is_initialized() {
let msg = String::from("ClockBound SHM header is not initialized.");
error!(msg);
return Err(ShmError::SegmentNotInitialized(msg));
}
if !self.is_well_formed() {
let msg = String::from("ClockBound SHM segment is not well formed.");
error!(msg);
return Err(ShmError::SegmentMalformed(msg));
}
Ok(())
}
}
#[cfg(test)]
mod t_shm_header {
use super::*;
use byteorder::{NativeEndian, WriteBytesExt};
use std::ffi::CString;
use std::fs::OpenOptions;
use tempfile::NamedTempFile;
macro_rules! write_shm_header {
($file:ident,
$magic_0:literal,
$magic_1:literal,
$segsize:literal,
$version:literal,
$generation:literal) => {
$file
.write_u32::<NativeEndian>($magic_0)
.expect("Write failed magic_0");
$file
.write_u32::<NativeEndian>($magic_1)
.expect("Write failed magic_1");
$file
.write_u32::<NativeEndian>($segsize)
.expect("Write failed segsize");
$file
.write_u16::<NativeEndian>($version)
.expect("Write failed version");
$file
.write_u16::<NativeEndian>($generation)
.expect("Write failed generation");
$file.sync_all().expect("Sync to disk failed");
};
}
#[test]
fn test_header_valid() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_shm_header!(clockbound_shm_file, 0x414D5A4E, 0x43420200, 16, 2, 99);
let path = CString::new(clockbound_shm_path).expect("CString failed");
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
let reader = ShmHeader::read(fd).expect("SHM Reader read");
assert_eq!(reader.segsize.into_inner(), 16);
assert_eq!(reader.version.into_inner(), 2);
assert_eq!(reader.generation.into_inner(), 99);
}
#[test]
fn test_header_bad_magic() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_shm_header!(clockbound_shm_file, 0xdeadbeef, 0x0badcafe, 16, 2, 99);
let path = CString::new(clockbound_shm_path).expect("CString failed");
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
assert!(ShmHeader::read(fd).is_err());
}
#[test]
fn test_header_bad_segsize() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_shm_header!(clockbound_shm_file, 0x414D5A4E, 0x43420200, 4, 2, 99);
let path = CString::new(clockbound_shm_path).expect("CString failed");
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
assert!(ShmHeader::read(fd).is_err());
}
#[test]
fn test_header_bad_version_zero() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_shm_header!(clockbound_shm_file, 0x414D5A4E, 0x43420200, 16, 0, 99);
let path = CString::new(clockbound_shm_path).expect("CString failed");
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
assert!(ShmHeader::read(fd).is_err());
}
#[test]
fn test_header_bad_version_unsupported() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_shm_header!(clockbound_shm_file, 0x414D5A4E, 0x43420200, 16, 9999, 99);
let path = CString::new(clockbound_shm_path).expect("CString failed");
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
assert!(ShmHeader::read(fd).is_err());
}
#[test]
fn test_header_bad_generation() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_shm_header!(clockbound_shm_file, 0x414D5A4E, 0x43420200, 16, 2, 0);
let path = CString::new(clockbound_shm_path).expect("CString failed");
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
assert!(ShmHeader::read(fd).is_err());
}
}