use deku::prelude::*;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tracing::{debug, trace};
#[cfg(feature = "bds-infer")]
use crate::data::patterns::PATTERNS;
#[derive(
Debug, PartialEq, Serialize, Deserialize, DekuRead, Clone, Default,
)]
#[serde(tag = "bds", rename = "21")]
pub struct AircraftAndAirlineRegistrationMarkings {
#[deku(bits = "1")]
#[serde(skip)]
pub ac_status: bool,
#[deku(reader = "aircraft_registration_read(deku::reader, *ac_status)")]
#[serde(rename = "registration")]
pub aircraft_registration: Option<String>,
#[deku(bits = "1")]
#[serde(skip)]
pub al_status: bool,
#[deku(reader = "airline_registration_read(deku::reader, *al_status)")]
#[serde(rename = "airline", skip_serializing_if = "Option::is_none")]
pub airline_registration: Option<String>,
}
const CHAR_LOOKUP: &[u8; 64] =
b" ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ############-##0123456789######";
#[inline]
fn is_dash_placeholder(c: u8) -> bool {
c == 0 || c == 32 || c == 45
}
pub fn aircraft_registration_read<
R: deku::no_std_io::Read + deku::no_std_io::Seek,
>(
reader: &mut Reader<R>,
status: bool,
) -> Result<Option<String>, DekuError> {
let mut chars = vec![];
for _ in 0..=6 {
let c = u8::from_reader_with_ctx(reader, deku::ctx::BitSize(6))?;
trace!("Reading letter {}", CHAR_LOOKUP[c as usize] as char);
if !is_dash_placeholder(c) {
chars.push(c);
}
}
let all_zeros = chars.is_empty();
let encoded = chars
.into_iter()
.map(|b| CHAR_LOOKUP[b as usize] as char)
.collect::<String>();
debug!("Decoded registration: {}", encoded);
if status {
#[cfg(feature = "bds-infer")]
if encoded.chars().filter(|&c| c == '#').count() > 2 {
return Err(DekuError::Assertion(
format!(
"BDS 21 registration {encoded:?} has > 2 '#' placeholders"
)
.into(),
));
}
Ok(Some(encoded))
} else if all_zeros {
Ok(None)
} else {
Err(DekuError::Assertion(
format!(
"Non-null value after invalid aircraft registration status: {encoded}"
)
.into(),
))
}
}
pub fn airline_registration_read<
R: deku::no_std_io::Read + deku::no_std_io::Seek,
>(
reader: &mut Reader<R>,
status: bool,
) -> Result<Option<String>, DekuError> {
let mut chars = vec![];
for _ in 0..2 {
let c = u8::from_reader_with_ctx(reader, deku::ctx::BitSize(6))?;
trace!("Reading letter {}", CHAR_LOOKUP[c as usize] as char);
if !is_dash_placeholder(c) {
chars.push(c);
}
}
let all_zeros = chars.is_empty();
let encoded = chars
.into_iter()
.map(|b| CHAR_LOOKUP[b as usize] as char)
.collect::<String>();
if status {
Err(DekuError::Assertion(
format!(
"Most transponders don't implement this field. (value = {encoded})"
)
.into(),
))
} else if all_zeros {
Ok(None)
} else {
Err(DekuError::Assertion(
format!(
"Non-null value after invalid airline registration status: {encoded}"
)
.into(),
))
}
}
#[cfg(feature = "bds-infer")]
pub fn validate_registration(reg: &str, icao24: u32) -> bool {
if reg.starts_with('#') {
return false;
}
static CALLSIGN_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[A-Z]{2,4}\d{2,5}[A-Z]{0,3}$").unwrap());
let country_pattern: Option<String> = PATTERNS
.registers
.iter()
.find(|r| {
if let (Some(s), Some(e)) = (&r.start, &r.end) {
let start =
u32::from_str_radix(&s[2..], 16).unwrap_or(u32::MAX);
let end = u32::from_str_radix(&e[2..], 16).unwrap_or(0);
icao24 >= start && icao24 <= end
} else {
false
}
})
.and_then(|r| r.pattern.clone());
let Some(raw_pattern) = country_pattern else {
return true;
};
let stripped_pattern = raw_pattern.replace('-', "");
let Ok(re) = Regex::new(&stripped_pattern) else {
return true; };
let country_match = |candidate: &str| -> bool {
let Some(m) = re.find(candidate) else {
return false;
};
if m.start() != 0 {
return false;
}
stripped_pattern.contains("\\d") || candidate.len() > m.end()
};
if country_match(reg) {
return true;
}
for strip_len in [2usize, 3] {
if reg.len() > strip_len && country_match(®[strip_len..]) {
return true;
}
if strip_len == 2 && reg.len() > strip_len {
let end = reg.len() - strip_len;
if country_match(®[..end]) {
return true;
}
}
}
if reg.len() >= 5
&& reg.len() <= 7
&& !reg.contains('#')
&& CALLSIGN_RE.is_match(reg)
{
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use hexlit::hex;
#[test]
fn test_valid_bds21() {
let bytes = hex!("a00002bf940f19680c0000000000");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
if let CommBAltitudeReply { bds, .. } = msg.df {
let AircraftAndAirlineRegistrationMarkings {
aircraft_registration,
..
} = bds.bds21.unwrap();
assert_eq!(aircraft_registration, Some("JA824A".to_string()));
} else {
unreachable!();
}
let bytes = hex!("a00002988230c3b470a000000000");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
if let CommBAltitudeReply { bds, .. } = msg.df {
let AircraftAndAirlineRegistrationMarkings {
aircraft_registration,
..
} = bds.bds21.unwrap();
assert_eq!(aircraft_registration, Some("AFFGZNE".to_string()));
} else {
unreachable!();
}
let bytes = hex!("a0000793ac45ab164c0000000000");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
if let CommBAltitudeReply { bds, .. } = msg.df {
let AircraftAndAirlineRegistrationMarkings {
aircraft_registration,
..
} = bds.bds21.unwrap();
assert_eq!(aircraft_registration, Some("VHVKI".to_string()));
} else {
unreachable!();
}
}
#[cfg(feature = "bds-infer")]
#[test]
fn test_validate_registration() {
assert!(validate_registration("JA824A", 0x843a1b));
assert!(validate_registration("N706CK", 0xa4a6fd));
assert!(validate_registration("B2487", 0x780123));
assert!(validate_registration("FGZHA", 0x3950ab));
assert!(validate_registration("SQ9VDHA", 0x76c123));
assert!(validate_registration("B2487CA", 0x780123));
assert!(validate_registration("BRB1672", 0x843a1b));
assert!(validate_registration("CIB1800", 0x843a1b));
assert!(!validate_registration("#0NF", 0x39c422));
assert!(!validate_registration("F", 0x39c422));
assert!(!validate_registration("XXXXXX", 0x843a1b));
assert!(!validate_registration("ZZZZZZ", 0x843a1b)); }
}