use crate::formats::error::{FileLocation, FormatError};
pub const SCK_MAGIC: &[u8; 8] = b"SCKERN01";
pub const SCK_HEADER_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SckHeader {
pub ncoeff: u32,
pub record_count: u32,
pub center_id: u32,
pub target_id: u32,
pub frame_id: u32,
pub time_scale_id: u32,
pub valid_from_jd: f64,
pub valid_to_jd: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SckKernel {
pub header: SckHeader,
pub records: Vec<f64>,
}
impl SckKernel {
#[must_use]
pub fn record_stride(&self) -> usize {
2 + 3 * (self.header.ncoeff as usize)
}
}
pub fn parse_sck(bytes: &[u8]) -> Result<SckKernel, FormatError> {
if bytes.len() < SCK_HEADER_LEN {
return Err(FormatError::located(
"SCK v1 §header",
FileLocation::default(),
"buffer shorter than 64-byte header",
));
}
if &bytes[0..8] != SCK_MAGIC {
return Err(FormatError::located(
"SCK v1 §magic",
FileLocation::default(),
"magic mismatch (expected SCKERN01)",
));
}
let header = SckHeader {
ncoeff: read_u32(&bytes[8..12]),
record_count: read_u32(&bytes[12..16]),
center_id: read_u32(&bytes[16..20]),
target_id: read_u32(&bytes[20..24]),
frame_id: read_u32(&bytes[24..28]),
time_scale_id: read_u32(&bytes[28..32]),
valid_from_jd: read_f64(&bytes[32..40]),
valid_to_jd: read_f64(&bytes[40..48]),
};
let stride = 2 + 3 * (header.ncoeff as usize);
let expected = stride
.checked_mul(header.record_count as usize)
.ok_or_else(|| {
FormatError::located(
"SCK v1 §payload",
FileLocation::default(),
"record geometry overflow",
)
})?;
let payload = &bytes[SCK_HEADER_LEN..];
if payload.len() != expected * 8 {
return Err(FormatError::located(
"SCK v1 §payload",
FileLocation::default(),
"payload size mismatch with header",
));
}
let mut records = Vec::with_capacity(expected);
for chunk in payload.chunks_exact(8) {
records.push(read_f64(chunk));
}
Ok(SckKernel { header, records })
}
fn read_u32(bytes: &[u8]) -> u32 {
let mut out = [0u8; 4];
out.copy_from_slice(bytes);
u32::from_le_bytes(out)
}
fn read_f64(bytes: &[u8]) -> f64 {
let mut out = [0u8; 8];
out.copy_from_slice(bytes);
f64::from_le_bytes(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn build_fixture() -> Vec<u8> {
let ncoeff = 4_u32;
let records: u32 = 1;
let mut buf = Vec::new();
buf.extend_from_slice(SCK_MAGIC);
buf.extend_from_slice(&ncoeff.to_le_bytes());
buf.extend_from_slice(&records.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&3911u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&2451545.0_f64.to_le_bytes());
buf.extend_from_slice(&2451546.0_f64.to_le_bytes());
buf.extend_from_slice(&[0u8; 16]);
for v in [
0.5_f64, 0.5, 1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0,
] {
buf.extend_from_slice(&v.to_le_bytes());
}
buf
}
#[test]
fn parses_valid_fixture() {
let bytes = build_fixture();
let k = parse_sck(&bytes).expect("fixture must parse");
assert_eq!(k.header.ncoeff, 4);
assert_eq!(k.header.record_count, 1);
assert_eq!(k.header.target_id, 3911);
assert_eq!(k.records.len(), 14);
assert_eq!(k.records[0], 0.5);
assert_eq!(k.record_stride(), 14);
}
#[test]
fn rejects_short_buffer() {
assert!(parse_sck(&[0u8; 10]).is_err());
}
#[test]
fn rejects_bad_magic() {
let mut bytes = build_fixture();
bytes[0] = b'X';
assert!(parse_sck(&bytes).is_err());
}
#[test]
fn rejects_short_payload() {
let mut bytes = build_fixture();
bytes.truncate(bytes.len() - 8);
assert!(parse_sck(&bytes).is_err());
}
}