mod eventcode;
mod originator;
mod phenomenon;
mod significance;
use std::convert::TryFrom;
use std::fmt;
#[cfg(feature = "chrono")]
use chrono::{DateTime, Datelike, Duration, NaiveDate, TimeZone, Utc};
use lazy_static::lazy_static;
use regex::Regex;
use thiserror::Error;
pub use eventcode::EventCode;
pub use originator::Originator;
pub use phenomenon::Phenomenon;
pub use significance::SignificanceLevel;
pub type MessageResult = Result<Message, MessageDecodeErr>;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Message {
StartOfMessage(MessageHeader),
EndOfMessage,
}
#[derive(Error, Clone, Debug, PartialEq, Eq, Hash)]
pub enum MessageDecodeErr {
#[error("invalid SAME header: unrecognized prefix")]
UnrecognizedPrefix,
#[error("invalid SAME header: message contains non-ASCII characters")]
NotAscii,
#[error("invalid SAME header: message text does not match required pattern")]
Malformed,
}
impl Message {
pub fn as_str(&self) -> &str {
match self {
Self::StartOfMessage(m) => m.as_str(),
Self::EndOfMessage => PREFIX_MESSAGE_END,
}
}
pub fn parity_error_count(&self) -> usize {
match self {
Self::StartOfMessage(m) => m.parity_error_count(),
Self::EndOfMessage => 0,
}
}
pub fn voting_byte_count(&self) -> usize {
match self {
Self::StartOfMessage(m) => m.voting_byte_count(),
Self::EndOfMessage => 0,
}
}
}
#[derive(Error, Clone, Debug, PartialEq, Eq, Hash)]
#[error("message issuance time not valid for its receive time")]
pub struct InvalidDateErr {}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct MessageHeader {
message: String,
offset_time: usize,
parity_error_count: usize,
voting_byte_count: usize,
}
impl MessageHeader {
pub fn new<S>(message: S) -> Result<Self, MessageDecodeErr>
where
S: Into<String>,
{
let mut message: String = message.into();
if !message.is_ascii() {
return Err(MessageDecodeErr::NotAscii);
}
let (offset_time, hdr_length) = check_header(&message)?;
message.truncate(hdr_length);
Ok(Self {
message,
offset_time,
parity_error_count: 0,
voting_byte_count: 0,
})
}
pub fn new_with_errors<S>(message: S, error_counts: &[u8]) -> Result<Self, MessageDecodeErr>
where
S: Into<String>,
{
let mut out = Self::new(message)?;
let mut parity_error_count = 0;
for (&e, _m) in error_counts.iter().zip(out.message().as_bytes().iter()) {
parity_error_count += e as usize;
}
out.parity_error_count = parity_error_count;
Ok(out)
}
pub fn new_with_error_info<S>(
message: S,
error_counts: &[u8],
burst_counts: &[u8],
) -> Result<Self, MessageDecodeErr>
where
S: Into<String>,
{
const MIN_BURSTS_FOR_VOTING: u8 = 3;
let mut out = Self::new_with_errors(message, error_counts)?;
let mut voting_byte_count = 0;
for (&e, _m) in burst_counts.iter().zip(out.message().as_bytes().iter()) {
voting_byte_count += (e >= MIN_BURSTS_FOR_VOTING) as usize;
}
out.voting_byte_count = voting_byte_count;
Ok(out)
}
pub fn message(&self) -> &str {
&self.message
}
pub fn as_str(&self) -> &str {
&self.message
}
pub fn originator(&self) -> Originator {
Originator::from_org_and_call(self.originator_str(), self.callsign())
}
pub fn originator_str(&self) -> &str {
&self.message[Self::OFFSET_ORG..Self::OFFSET_ORG + 3]
}
pub fn event(&self) -> EventCode {
EventCode::from(self.event_str())
}
pub fn event_str(&self) -> &str {
&self.message[Self::OFFSET_EVT..Self::OFFSET_EVT + 3]
}
pub fn location_str_iter<'m>(&'m self) -> std::str::Split<'m, char> {
self.location_str().split('-')
}
#[cfg(feature = "chrono")]
pub fn valid_duration(&self) -> Duration {
let (hrs, mins) = self.valid_duration_fields();
Duration::hours(hrs as i64) + Duration::minutes(mins as i64)
}
pub fn valid_duration_fields(&self) -> (u8, u8) {
let dur_str = &self.message[self.offset_time + Self::OFFSET_FROMPLUS_VALIDTIME
..self.offset_time + Self::OFFSET_FROMPLUS_VALIDTIME + 4];
(
dur_str[0..2].parse().expect(Self::PANIC_MSG),
dur_str[2..4].parse().expect(Self::PANIC_MSG),
)
}
#[cfg(feature = "chrono")]
pub fn issue_datetime(
&self,
received: &DateTime<Utc>,
) -> Result<DateTime<Utc>, InvalidDateErr> {
calculate_issue_time(
self.issue_daytime_fields(),
(received.year(), received.ordinal()),
)
}
#[cfg(feature = "chrono")]
pub fn purge_datetime(
&self,
received: &DateTime<Utc>,
) -> Result<DateTime<Utc>, InvalidDateErr> {
calculate_expire_time(&self.issue_datetime(received)?, &self.valid_duration())
}
#[cfg(feature = "chrono")]
pub fn is_expired_at(&self, now: &DateTime<Utc>) -> bool {
if let Ok(purge) = self.purge_datetime(&now) {
purge < *now
} else {
false
}
}
pub fn issue_daytime_fields(&self) -> (u16, u8, u8) {
let issue = &self.message[self.offset_time + Self::OFFSET_FROMPLUS_ISSUETIME
..self.offset_time + Self::OFFSET_FROMPLUS_ISSUETIME + 7];
(
issue[0..3].parse().expect(Self::PANIC_MSG),
issue[3..5].parse().expect(Self::PANIC_MSG),
issue[5..7].parse().expect(Self::PANIC_MSG),
)
}
pub fn callsign(&self) -> &str {
let end = self.message.len();
&self.message[self.offset_time + Self::OFFSET_FROMPLUS_CALLSIGN
..end - Self::OFFSET_FROMEND_CALLSIGN_END]
}
pub fn parity_error_count(&self) -> usize {
self.parity_error_count
}
pub fn voting_byte_count(&self) -> usize {
self.voting_byte_count
}
pub fn is_national(&self) -> bool {
self.location_str() == Self::LOCATION_NATIONAL && self.event().phenomenon().is_national()
}
pub fn release(self) -> String {
self.message
}
fn location_str(&self) -> &str {
&self.message[Self::OFFSET_AREA_START..self.offset_time]
}
const OFFSET_ORG: usize = 5;
const OFFSET_EVT: usize = 9;
const OFFSET_AREA_START: usize = 13;
const OFFSET_FROMPLUS_VALIDTIME: usize = 1;
const OFFSET_FROMPLUS_ISSUETIME: usize = 6;
const OFFSET_FROMPLUS_CALLSIGN: usize = 14;
const OFFSET_FROMEND_CALLSIGN_END: usize = 1;
const PANIC_MSG: &'static str = "MessageHeader validity check admitted a malformed message";
const LOCATION_NATIONAL: &'static str = "000000";
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl AsRef<str> for Message {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl TryFrom<String> for Message {
type Error = MessageDecodeErr;
#[inline]
fn try_from(inp: String) -> Result<Self, Self::Error> {
if inp.starts_with(PREFIX_MESSAGE_START) {
Ok(Message::StartOfMessage(MessageHeader::try_from(inp)?))
} else if inp.starts_with(&PREFIX_MESSAGE_END[0..2]) {
Ok(Message::EndOfMessage)
} else {
Err(MessageDecodeErr::UnrecognizedPrefix)
}
}
}
impl TryFrom<(String, &[u8])> for Message {
type Error = MessageDecodeErr;
#[inline]
fn try_from(inp: (String, &[u8])) -> Result<Self, Self::Error> {
if inp.0.starts_with(PREFIX_MESSAGE_START) {
Ok(Message::StartOfMessage(MessageHeader::try_from(inp)?))
} else if inp.0.starts_with(&PREFIX_MESSAGE_END[0..2]) {
Ok(Message::EndOfMessage)
} else {
Err(MessageDecodeErr::UnrecognizedPrefix)
}
}
}
impl TryFrom<(&[u8], &[u8], &[u8])> for Message {
type Error = MessageDecodeErr;
#[inline]
fn try_from(inp: (&[u8], &[u8], &[u8])) -> Result<Self, Self::Error> {
let instr = std::str::from_utf8(inp.0).map_err(|_e| MessageDecodeErr::NotAscii)?;
if instr.starts_with(PREFIX_MESSAGE_START) {
Ok(Message::StartOfMessage(MessageHeader::try_from((
instr.to_owned(),
inp.1,
inp.2,
))?))
} else if instr.starts_with(&PREFIX_MESSAGE_END[0..2]) {
Ok(Message::EndOfMessage)
} else {
Err(MessageDecodeErr::UnrecognizedPrefix)
}
}
}
impl fmt::Display for MessageHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
impl AsRef<str> for MessageHeader {
#[inline]
fn as_ref(&self) -> &str {
self.message()
}
}
impl AsRef<[u8]> for MessageHeader {
#[inline]
fn as_ref(&self) -> &[u8] {
self.message().as_bytes()
}
}
impl From<MessageHeader> for String {
#[inline]
fn from(msg: MessageHeader) -> String {
msg.release()
}
}
impl TryFrom<String> for MessageHeader {
type Error = MessageDecodeErr;
#[inline]
fn try_from(inp: String) -> Result<Self, Self::Error> {
Self::new(inp)
}
}
impl TryFrom<(String, &[u8])> for MessageHeader {
type Error = MessageDecodeErr;
#[inline]
fn try_from(inp: (String, &[u8])) -> Result<Self, Self::Error> {
Self::new_with_errors(inp.0, inp.1)
}
}
impl TryFrom<(String, &[u8], &[u8])> for MessageHeader {
type Error = MessageDecodeErr;
#[inline]
fn try_from(inp: (String, &[u8], &[u8])) -> Result<Self, Self::Error> {
Self::new_with_error_info(inp.0, inp.1, inp.2)
}
}
const PREFIX_MESSAGE_START: &str = "ZCZC-";
const PREFIX_MESSAGE_END: &str = "NNNN";
fn check_header(hdr: &str) -> Result<(usize, usize), MessageDecodeErr> {
lazy_static! {
static ref RE: Regex = Regex::new(
r"^ZCZC-[[:alpha:]]{3}-[[:alpha:]]{3}(-[0-9]{6})+(\+[0-9]{4}-[0-9]{7}-.{3,8}-)"
)
.expect("bad SAME regexp");
}
let mtc = RE
.captures(hdr)
.ok_or(MessageDecodeErr::Malformed)?
.get(2)
.ok_or(MessageDecodeErr::Malformed)?;
Ok((mtc.start(), mtc.end()))
}
#[cfg(feature = "chrono")]
fn calculate_issue_time(
message: (u16, u8, u8),
received: (i32, u32),
) -> Result<DateTime<Utc>, InvalidDateErr> {
let (day_of_year, hour, minute) = message;
let (rx_year, rx_day_of_year) = received;
let daydiff = rx_day_of_year as i32 - day_of_year as i32;
let msg_year = if daydiff >= 180 {
rx_year.saturating_add(1)
} else if daydiff <= -180 {
rx_year.saturating_sub(1)
} else {
rx_year
};
yo_hms_to_utc(msg_year, day_of_year as u32, hour as u32, minute as u32, 0)
.ok_or(InvalidDateErr {})
}
#[cfg(feature = "chrono")]
fn calculate_expire_time(
issued: &DateTime<Utc>,
purge: &Duration,
) -> Result<DateTime<Utc>, InvalidDateErr> {
use chrono::DurationRound;
const FIFTEEN_MINUTES: Duration = Duration::minutes(15);
const THIRTY_MINUTES: Duration = Duration::minutes(30);
const ONE_HOUR: Duration = Duration::hours(1);
issued
.checked_add_signed(*purge)
.and_then(|purge_unrounded| {
if *purge <= ONE_HOUR {
purge_unrounded.duration_round(FIFTEEN_MINUTES)
} else {
purge_unrounded.duration_round(THIRTY_MINUTES)
}
.ok()
})
.ok_or(InvalidDateErr {})
}
#[cfg(feature = "chrono")]
#[inline]
fn yo_hms_to_utc(
year: i32,
ordinal: u32,
hour: u32,
minute: u32,
second: u32,
) -> Option<DateTime<Utc>> {
Some(Utc.from_utc_datetime(
&NaiveDate::from_yo_opt(year, ordinal)?.and_hms_opt(hour, minute, second)?,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "chrono")]
use chrono::{TimeZone, Utc};
#[test]
fn test_check_header() {
const INVALID_SHORT: &str = "ZCZC-ORG-EEE-+0000-0001122-NOCALL00-";
const VALID_ONE: &str = "ZCZC-ORG-EEE-012345+0000-0001122-NOCALL00-";
const VALID_TWO: &str = "ZCZC-ORG-EEE-012345-567890+0000-0001122-NOCALL00-garbage";
assert_eq!(
Err(MessageDecodeErr::Malformed),
check_header(INVALID_SHORT)
);
assert_eq!(Ok((19, 42)), check_header(VALID_ONE));
assert_eq!(VALID_ONE.as_bytes()[19], '+' as u8);
assert_eq!(Ok((26, 49)), check_header(VALID_TWO));
assert_eq!(VALID_TWO.as_bytes()[26], '+' as u8);
}
#[test]
#[cfg(feature = "chrono")]
fn test_calculate_issue_time() {
let d = calculate_issue_time((83, 2, 53), (2021, 1)).unwrap();
assert_eq!(d, Utc.with_ymd_and_hms(2021, 3, 24, 2, 53, 0).unwrap());
let d = calculate_issue_time((84, 23, 59), (2021, 1)).unwrap();
assert_eq!(d, Utc.with_ymd_and_hms(2021, 3, 25, 23, 59, 0).unwrap());
let d = calculate_issue_time((1, 10, 00), (2021, 1)).unwrap();
assert_eq!(d, Utc.with_ymd_and_hms(2021, 1, 1, 10, 00, 0).unwrap());
let d = calculate_issue_time((1, 10, 00), (2021, 200)).unwrap();
assert_eq!(d, Utc.with_ymd_and_hms(2022, 1, 1, 10, 00, 0).unwrap());
let d = calculate_issue_time((1, 10, 00), (2021, 365)).unwrap();
assert_eq!(d, Utc.with_ymd_and_hms(2022, 1, 1, 10, 00, 0).unwrap());
let d = calculate_issue_time((366, 10, 00), (2021, 1)).unwrap();
assert_eq!(d, Utc.with_ymd_and_hms(2020, 12, 31, 10, 00, 0).unwrap());
calculate_issue_time((366, 10, 00), (1971, 364)).expect_err("should not succeed");
calculate_issue_time((0, 10, 00), (1971, 364)).expect_err("should not succeed");
calculate_issue_time((84, 25, 59), (2021, 84)).expect_err("should not succeed");
}
#[cfg(feature = "chrono")]
#[test]
fn test_calculate_expire_time_short() {
const FIFTEEN_MINUTES: Duration = Duration::minutes(15);
let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 44, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 0, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);
let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 46, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 0, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);
let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 55, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 15, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);
let issued = Utc.with_ymd_and_hms(2021, 3, 24, 3, 00, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 15, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);
}
#[cfg(feature = "chrono")]
#[test]
fn test_calculate_expire_time_long() {
let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 53, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 15, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(15)).unwrap()
);
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 30, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(30)).unwrap()
);
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 45, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(45)).unwrap()
);
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 4, 00, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(60)).unwrap()
);
}
#[test]
fn test_message_header() {
const THREE_LOCATIONS: &str = "ZCZC-WXR-RWT-012345-567890-888990+0330-3662322-NOCALL00-@@@";
let mut errs = vec![0u8; THREE_LOCATIONS.len()];
errs[0] = 1u8;
errs[20] = 5u8;
errs[THREE_LOCATIONS.len() - 1] = 8u8;
let burst_count = vec![3u8; THREE_LOCATIONS.len()];
let msg = MessageHeader::try_from((
THREE_LOCATIONS.to_owned(),
errs.as_slice(),
burst_count.as_slice(),
))
.expect("bad msg");
assert_eq!(msg.originator_str(), "WXR");
assert_eq!(Originator::NationalWeatherService, msg.originator());
assert_eq!(msg.event_str(), "RWT");
assert_eq!(msg.event().phenomenon(), Phenomenon::RequiredWeeklyTest);
assert_eq!(msg.valid_duration_fields(), (3, 30));
assert_eq!(msg.issue_daytime_fields(), (366, 23, 22));
assert_eq!(msg.callsign(), "NOCALL00");
assert_eq!(msg.parity_error_count(), 6);
assert_eq!(msg.voting_byte_count(), msg.as_str().len());
assert!(!msg.is_national());
let loc: Vec<&str> = msg.location_str_iter().collect();
assert_eq!(loc.as_slice(), &["012345", "567890", "888990"]);
#[cfg(feature = "chrono")]
{
let received = Utc.with_ymd_and_hms(2020, 12, 31, 11, 30, 34).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2020, 12, 31, 23, 22, 00).unwrap(),
msg.issue_datetime(&received).unwrap()
);
assert_eq!(
msg.valid_duration(),
Duration::hours(3) + Duration::minutes(30)
);
assert_eq!(
Utc.with_ymd_and_hms(2021, 1, 1, 3, 0, 00).unwrap(),
msg.purge_datetime(&received).unwrap()
);
assert!(!msg.is_expired_at(&Utc.with_ymd_and_hms(2020, 12, 31, 23, 59, 0).unwrap()));
assert!(!msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 1, 20, 30).unwrap()));
assert!(!msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 2, 59, 59).unwrap()));
assert!(msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 3, 0, 01).unwrap()));
}
let msg = Message::try_from(THREE_LOCATIONS.to_owned()).expect("bad msg");
match &msg {
Message::StartOfMessage(m) => assert_eq!(m.issue_daytime_fields(), (366, 23, 22)),
_ => unreachable!(),
}
assert_eq!(&THREE_LOCATIONS[0..56], &format!("{}", msg));
}
#[test]
fn test_message() {
let msg = Message::try_from("NNNN".to_owned()).expect("bad msg");
assert_eq!(Message::EndOfMessage, msg);
assert_eq!("NNNN", &format!("{}", msg));
let msg = Message::try_from("NN".to_owned()).expect("bad msg");
assert_eq!(Message::EndOfMessage, msg);
}
#[test]
fn test_is_national() {
let national = MessageHeader::new("ZCZC-PEP-NPT-000000+0030-2771820-TEST -").unwrap();
assert!(national.is_national());
let national = MessageHeader::new("ZCZC-PEP-EAN-000000+0030-2771820-TEST -").unwrap();
assert!(national.is_national());
let not_national =
MessageHeader::new("ZCZC-PEP-NPT-000001+0030-2771820-TEST -").unwrap();
assert!(!not_national.is_national());
let not_national =
MessageHeader::new("ZCZC-PEP-NPT-000000-000001+0030-2771820-TEST -").unwrap();
assert!(!not_national.is_national());
}
}