use super::bits::read_bits;
pub const PAGE_BITS: usize = 240;
const ODD_START: usize = 120;
pub const GST_LIMIT: u32 = 4096 * super::WEEK_S;
const OSNMA_OFFSET: usize = ODD_START + 18;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PageError {
BadHex,
BadTime,
BadLength(usize),
BadFields(String),
}
impl std::fmt::Display for PageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BadHex => write!(f, "page is not valid hex"),
Self::BadTime => write!(
f,
"page time is outside the 4096 weeks of a Galileo week number"
),
Self::BadLength(n) => write!(f, "page has {n} hex digits, expected 60"),
Self::BadFields(s) => write!(f, "malformed page line: {s}"),
}
}
}
impl std::error::Error for PageError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InavPage {
pub svid: u8,
pub gst: u32,
bits: [u8; PAGE_BITS / 8],
}
pub(crate) fn crc24q(bits: impl Iterator<Item = u32>) -> u32 {
let mut crc = 0u32;
for bit in bits {
let fb = ((crc >> 23) & 1) ^ bit;
crc = (crc << 1) & 0x00FF_FFFF;
if fb == 1 {
crc ^= 0x0086_4CFB;
}
}
crc
}
impl InavPage {
pub fn from_hex(svid: u8, gst: u32, hex_digits: &str) -> Result<Self, PageError> {
if gst >= GST_LIMIT {
return Err(PageError::BadTime);
}
if !hex_digits.is_ascii() {
return Err(PageError::BadHex);
}
if hex_digits.len() != PAGE_BITS / 4 {
return Err(PageError::BadLength(hex_digits.len()));
}
let raw = hex::decode(hex_digits).map_err(|_| PageError::BadHex)?;
let mut bits = [0u8; PAGE_BITS / 8];
bits.copy_from_slice(&raw);
Ok(Self { svid, gst, bits })
}
pub fn from_bytes(svid: u8, gst: u32, bytes: &[u8; PAGE_BITS / 8]) -> Self {
Self {
svid,
gst,
bits: *bytes,
}
}
pub fn crc_ok(&self) -> bool {
self.computed_crc() == read_bits(&self.bits, ODD_START + 82, 24).unwrap_or(u64::MAX) as u32
}
fn computed_crc(&self) -> u32 {
let covered = (0..114).chain(ODD_START..ODD_START + 82);
crc24q(covered.map(|i| u32::from((self.bits[i / 8] >> (7 - i % 8)) & 1)))
}
pub fn is_nominal(&self) -> bool {
self.bits[0] & 0xC0 == 0 && self.bits[ODD_START / 8] & 0xC0 == 0x80
}
pub fn with_valid_crc(mut self) -> Self {
let crc = self.computed_crc();
for k in 0..24 {
let i = ODD_START + 82 + k;
let bit = ((crc >> (23 - k)) & 1) as u8;
self.bits[i / 8] = (self.bits[i / 8] & !(1 << (7 - i % 8))) | (bit << (7 - i % 8));
}
self
}
pub fn parse_line(line: &str) -> Result<Option<Self>, PageError> {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
return Ok(None);
}
let mut it = line.split_whitespace();
let (Some(s), Some(t), Some(h), None) = (it.next(), it.next(), it.next(), it.next()) else {
return Err(PageError::BadFields(echo(line)));
};
let svid = s.parse().map_err(|_| PageError::BadFields(echo(line)))?;
let gst = t.parse().map_err(|_| PageError::BadFields(echo(line)))?;
Self::from_hex(svid, gst, h).map(Some)
}
pub fn bytes(&self) -> &[u8] {
&self.bits
}
pub fn osnma_field(&self) -> u64 {
read_bits(&self.bits, OSNMA_OFFSET, 40).unwrap_or(0)
}
pub fn data_word(&self) -> [u8; 16] {
let mut w = super::bits::BitWriter::new();
w.push(read_bits(&self.bits, 2, 56).unwrap_or(0), 56);
w.push(read_bits(&self.bits, 58, 56).unwrap_or(0), 56);
w.push(read_bits(&self.bits, ODD_START + 2, 16).unwrap_or(0), 16);
let mut out = [0u8; 16];
out.copy_from_slice(&w.into_bytes());
out
}
pub fn hkroot_section(&self) -> u8 {
(self.osnma_field() >> 32) as u8
}
pub fn mack_section(&self) -> u32 {
(self.osnma_field() & 0xFFFF_FFFF) as u32
}
pub fn has_osnma(&self) -> bool {
self.osnma_field() != 0
}
}
fn echo(line: &str) -> String {
let mut it = line.chars();
let head: String = it.by_ref().take(80).collect();
if it.next().is_some() {
format!("{head}…")
} else {
head
}
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn synth(svid: u8, gst: u32, field: u64) -> InavPage {
let mut w = super::super::bits::BitWriter::new();
w.push(0, ODD_START);
w.push(0b10, 2);
w.push(0, 16);
w.push(field, 40);
w.push(0, PAGE_BITS - w.bit_len());
InavPage::from_hex(svid, gst, &hex::encode(w.into_bytes())).unwrap()
}
#[test]
fn field_split_is_8_plus_32() {
let p = synth(2, 0, 0x72_7A12_5EE9);
assert_eq!(p.osnma_field(), 0x72_7A12_5EE9);
assert_eq!(p.hkroot_section(), 0x72);
assert_eq!(p.mack_section(), 0x7A12_5EE9);
assert!(p.has_osnma());
assert!(!synth(2, 0, 0).has_osnma());
}
#[test]
fn only_nominal_pages_and_valid_times_are_accepted() {
let p = synth(2, 0, 0x72_7A12_5EE9);
assert!(p.is_nominal());
for (byte, bit) in [(0usize, 0x80u8), (0, 0x40), (15, 0x80), (15, 0x40)] {
let mut raw = p.bytes().to_vec();
raw[byte] ^= bit;
let q = InavPage::from_hex(2, 0, &hex::encode(raw)).unwrap();
assert!(!q.is_nominal(), "byte {byte} bit {bit:#x}");
}
assert!(!InavPage::from_hex(2, 0, &"00".repeat(30))
.unwrap()
.is_nominal());
let hexd = hex::encode(p.bytes());
assert!(InavPage::from_hex(2, GST_LIMIT - 1, &hexd).is_ok());
assert!(matches!(
InavPage::from_hex(2, GST_LIMIT, &hexd),
Err(PageError::BadTime)
));
assert!(matches!(
InavPage::from_hex(2, u32::MAX, &hexd),
Err(PageError::BadTime)
));
assert!(InavPage::from_hex(2, 0, &"é".repeat(30)).is_err());
}
#[test]
fn crc24q_known_answer() {
let bits = b"123456789"
.iter()
.flat_map(|b| (0..8).rev().map(move |k| u32::from((b >> k) & 1)));
assert_eq!(crc24q(bits), 0xCDE703);
}
#[test]
fn crc_round_trip_and_corruption() {
let p = synth(2, 0, 0x72_7A12_5EE9);
assert!(!p.crc_ok()); let q = p.clone().with_valid_crc();
assert!(q.crc_ok());
assert_eq!(q.osnma_field(), p.osnma_field());
let mut raw = q.bytes().to_vec();
raw[20] ^= 0x01; let bad = InavPage::from_hex(2, 0, &hex::encode(raw)).unwrap();
assert!(!bad.crc_ok());
}
#[test]
fn a_malformed_line_is_echoed_in_at_most_eighty_characters() {
let line = "x".repeat(8 * 1024 * 1024);
let e = InavPage::parse_line(&line).unwrap_err().to_string();
assert!(e.chars().count() < 120 && e.ends_with('…'), "{}", e.len());
let short = InavPage::parse_line("not a page").unwrap_err().to_string();
assert!(short.ends_with("not a page"));
}
#[test]
fn parses_plain_lines() {
let line = format!("5 1234 {}", "00".repeat(30));
let p = InavPage::parse_line(&line).unwrap().unwrap();
assert_eq!((p.svid, p.gst), (5, 1234));
assert_eq!(InavPage::parse_line(" # note").unwrap(), None);
assert!(InavPage::parse_line("5 1234 ab").is_err());
assert!(InavPage::parse_line("5 x 00").is_err());
}
}