1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
//! Types for the OASIS [Common Alerting Protocol].
//!
//! # Example
//!
//! ```rust
//! let alert: oasiscap::Alert = r#"
//! <?xml version = "1.0" encoding = "UTF-8"?>
//! <alert xmlns = "urn:oasis:names:tc:emergency:cap:1.2">
//! <identifier>43b080713727</identifier>
//! <sender>hsas@dhs.gov</sender>
//! <sent>2003-04-02T14:39:01-05:00</sent>
//! <status>Actual</status>
//! <msgType>Alert</msgType>
//! <scope>Public</scope>
//! <info>
//! <!-- … -->
//!# <category>Security</category>
//!# <event>Homeland Security Advisory System Update</event>
//!# <urgency>Immediate</urgency>
//!# <severity>Severe</severity>
//!# <certainty>Likely</certainty>
//!# <senderName>U.S. Government, Department of Homeland Security</senderName>
//!# <headline>Homeland Security Sets Code ORANGE</headline>
//!# <description>The Department of Homeland Security has elevated the Homeland Security Advisory System threat level to ORANGE / High in response to intelligence which may indicate a heightened threat of terrorism.</description>
//!# <instruction> A High Condition is declared when there is a high risk of terrorist attacks. In addition to the Protective Measures taken in the previous Threat Conditions, Federal departments and agencies should consider agency-specific Protective Measures in accordance with their existing plans.</instruction>
//!# <web>http://www.dhs.gov/dhspublic/display?theme=29</web>
//! </info>
//! </alert>
//! "#.parse()?;
//!
//! // Handle CAP alerts of various versions
//! match &alert {
//! oasiscap::Alert::V1dot0(alert) => println!("CAP v1.0: {:?}", alert),
//! oasiscap::Alert::V1dot1(alert) => println!("CAP v1.1: {:?}", alert),
//! oasiscap::Alert::V1dot2(alert) => println!("CAP v1.2: {:?}", alert),
//! }
//!
//! // Upgrade to the latest CAP version
//! let alert: oasiscap::v1dot2::Alert = alert.into_latest();
//!
//! // Convert back to XML again
//! let alert_xml = alert.to_string();
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! # Conformance
//!
//! The CAP specifications are split between human- and machine-readable components. CAP v1.2 § 4.2
//! explains:
//!
//! > An XML 1.0 document is a conforming CAP V1.2 Message if and only if:
//! >
//! > a) it is valid according to [the schema] and
//! >
//! > b) the content of its elements and the values of its attributes meet all the additional
//! > mandatory requirements specified in Section 3.
//!
//! Consider the `<polygon>` element. The machine-readable XML schema says that a polygon is just a
//! string:
//!
//! ```xml
//! <element name = "polygon" type = "xs:string" minOccurs = "0" maxOccurs = "unbounded"/>
//! ```
//!
//! The human-readable document says that a polygon is specifically a string describing a closed
//! polyline in a particular geospatial reference frame, and imposes the following requirements
//! in section 3:
//!
//! > (1) Code Values: The geographic polygon is represented by a whitespace-delimited list of WGS
//! > 84 coordinate pairs. (See WGS 84 Note at end of this section)
//! >
//! > (2) A minimum of 4 coordinate pairs MUST be present and the first and last pairs of
//! > coordinates MUST be the same.
//!
//! This crate implements those rules from section 3:
//!
//! ```rust
//! use oasiscap::geo::Polygon;
//!
//! // 4 points, where the last point is the first point, makes a Polygon:
//! assert!("1,1 2,2 3,3 1,1".parse::<Polygon>().is_ok());
//!
//! // 4 points where the last point differs does not make a Polygon:
//! assert!("1,1 2,2 3,3 4,4".parse::<Polygon>().is_err());
//!
//! // 3 points does not make a Polygon:
//! assert!("1,1 2,2 1,1".parse::<Polygon>().is_err());
//!
//! // invalid WGS-84 coordinates do not make a Polygon:
//! assert!("100,100 200,200 300,300 100,100".parse::<Polygon>().is_err());
//! ```
//!
//! All of those strings are permitted by the XML schema, but only the first one makes sense as a
//! polygon. This crate therefore accepts the first string and rejects the others.
//!
//! Having said that, some real-world CAP alerts violate the requirements in section 3 but _do_
//! still make sense:
//!
//! ```xml
//! <polygon></polygon>
//! ```
//!
//! Polygons are optional, so the element can and should have been omitted in its entirety. On the
//! other hand, an empty string _is_ valid according to the XML schema, and its intent is
//! unambiguous even if it is technically non-conforming. This crate therefore accepts an empty
//! polygon element as a synonym for omitting the polygon, rather than returning an error.
//!
//! This crate intends to always parse conforming CAP messages and to always generate conforming CAP
//! messages. At the same time, this crate intends to be pedantic to preserve _meaning_, not to be
//! pendantic for pedantry's sake. It therefore does not reject all non-conforming CAP messages,
//! particularly for common implementation mistakes which have reasonable and unambiguous
//! interpretations.
//!
//! # Performance
//!
//! `oasiscap` prioritizes being correct over being fast, but it is still reasonably fast. On an
//! industry standard developer's laptop using unspecified versions of this library, Rust, and the
//! underlying operating system, parsing a typical `oasiscap::Alert` from XML takes approximately
//! 55µs, for a throughput of roughly 18,000 alerts per second per core. Generating XML from a
//! typical `oasiscap::Alert` takes approximately 27µs, for a throughput of roughly 38,000 alerts
// per second per core.
//!
//! Clone the repository and run `cargo bench` to see how it performs in your environment.
//!
//! # Protocol Buffers
//!
//! Google Public Alerts defines a [CAP Protocol Buffers representation], under the Java package
//! name `com.google.publicalerts.cap`. This crate optionally provides `oasiscap::protobuf` when
//! built with the `prost` feature. `oasiscap::protobuf` data types exactly correspond to these
//! Protocol Buffers message types.
//!
//! The Protocol Buffers representations are more permissive than the usual parsed `oasiscap` types:
//! timestamps can lack time zones, polygons don't have to be closed, required fields can be
//! missing, etc. This crate therefore also provides conversions:
//!
//! ```rust
//! # #[cfg(feature = "prost")]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! # let alert: oasiscap::Alert = include_str!("../fixtures/v1dot0_appendix_adot2.xml").parse().unwrap();
//! # let alert = oasiscap::protobuf::Alert::from(alert);
//! # let protobuf_encoded_bytes = prost::Message::encode_to_vec(&alert);
//! // Decoding from protobuf bytes can fail:
//! let protobuf_alert: oasiscap::protobuf::Alert = prost::Message::decode(
//! protobuf_encoded_bytes.as_slice()
//! )?;
//!
//! // Converting to an `oasiscap::Alert` can fail:
//! let alert: oasiscap::Alert = protobuf_alert.try_into()?;
//!
//! // Converting back to an `oasiscap::protobuf::Alert` cannot fail:
//! let alert: oasiscap::protobuf::Alert = alert.into();
//!
//! // Nor can encoding protobuf bytes:
//! let protobuf_encoded_bytes = prost::Message::encode_to_vec(&alert);
//! # Ok(()) }
//! # #[cfg(not(feature = "prost"))] fn test() -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
//! # test().unwrap()
//! ```
//!
//! Protocol Buffers offer substantially better performance than XML:
//!
//! * `&[u8]` to `oasiscap::protobuf::Alert`: 2µs
//! * `oasiscap::protobuf::Alert` to `oasiscap::Alert`: 2µs
//! * `oasiscap::Alert` to `oasiscap::protobuf::Alert`: 1µs
//! * `oasiscap::protobuf::Alert` to `Vec<u8>`: 0.3µs
//!
//! [Common Alerting Protocol]: https://en.wikipedia.org/wiki/Common_Alerting_Protocol
//! [xml_serde]: https://crates.io/crates/xml_serde
//! [the schema]: http://docs.oasis-open.org/emergency/cap/v1.2/CAP-v1.2.xsd
//! [CAP Protocol Buffers representation]: https://github.com/google/cap-library/blob/master/proto/cap.proto
#![forbid(unsafe_code)]
#![deny(missing_docs)]
extern crate core;
use serde::{Deserialize, Serialize};
mod datetime;
pub use datetime::DateTime;
pub mod digest;
mod embedded_data;
pub use embedded_data::EmbeddedContent;
pub mod delimited_items;
pub mod geo;
pub mod id;
pub mod language;
pub mod map;
pub mod references;
pub mod v1dot0;
pub mod v1dot1;
pub mod v1dot2;
#[cfg(feature = "prost")]
pub mod protobuf;
pub(crate) mod url;
pub use ::url::Url;
/// A CAP alert message.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Alert {
/// A CAP v1.0 alert message
#[serde(rename = "{http://www.incident.com/cap/1.0;}cap:alert")]
V1dot0(v1dot0::Alert),
/// A CAP v1.1 alert message
#[serde(
rename = "{urn:oasis:names:tc:emergency:cap:1.1;https://docs.oasis-open.org/emergency/cap/v1.1/errata/approved/cap.xsd}cap:alert"
)]
V1dot1(v1dot1::Alert),
/// A CAP v1.2 alert message
#[serde(rename = "{urn:oasis:names:tc:emergency:cap:1.2;}cap:alert")]
V1dot2(v1dot2::Alert),
}
impl From<v1dot0::Alert> for Alert {
fn from(v: v1dot0::Alert) -> Self {
Self::V1dot0(v)
}
}
impl From<v1dot1::Alert> for Alert {
fn from(v: v1dot1::Alert) -> Self {
Self::V1dot1(v)
}
}
impl From<v1dot2::Alert> for Alert {
fn from(v: v1dot2::Alert) -> Self {
Self::V1dot2(v)
}
}
impl Alert {
/// A unique identifier for this alert, assigned by the sender
pub fn identifier(&self) -> &crate::id::Id {
match self {
Alert::V1dot0(alert) => &alert.identifier,
Alert::V1dot1(alert) => &alert.identifier,
Alert::V1dot2(alert) => &alert.identifier,
}
}
/// A globally-unique identifier for the sender
pub fn sender(&self) -> &crate::id::Id {
match self {
Alert::V1dot0(alert) => &alert.sender,
Alert::V1dot1(alert) => &alert.sender,
Alert::V1dot2(alert) => &alert.sender,
}
}
/// The date and time at which this alert originated
pub fn sent(&self) -> crate::DateTime {
match self {
Alert::V1dot0(alert) => alert.sent,
Alert::V1dot1(alert) => alert.sent,
Alert::V1dot2(alert) => alert.sent,
}
}
/// Returns the XML namespace corresponding to the encapsulated CAP alert version.
pub fn xml_namespace(&self) -> &'static str {
match self {
Alert::V1dot0(_) => "http://www.incident.com/cap/1.0",
Alert::V1dot1(_) => "urn:oasis:names:tc:emergency:cap:1.1",
Alert::V1dot2(_) => "urn:oasis:names:tc:emergency:cap:1.2",
}
}
/// Return this alert as the latest supported alert version, upgrading it as necessary.
///
/// CAP v1.2 is mostly a superset of earlier versions, with two exceptions:
///
/// 1. CAP <= v1.1 `Resource` has an optional `mime_type`, whereas it's required for CAP v1.2.
/// This crate supplies `application/octet-stream` as a default if needed.
///
/// ```
/// # let input = include_str!("../fixtures/v1dot0_appendix_adot1.xml");
/// // let input: &str = /* CAP v1.0 appendix A.1 */;
/// let alert: oasiscap::Alert = input.parse().unwrap();
/// match &alert {
/// oasiscap::Alert::V1dot0(alert) => {
/// assert!(alert.info[0].resources[0].mime_type.is_none());
/// }
/// _ => unreachable!(),
/// }
///
/// let alert = alert.into_latest();
/// assert_eq!(alert.info[0].resources[0].mime_type, "application/octet-stream");
/// #
/// # let expected = include_str!("../fixtures/v1dot2_appendix_adot1.xml");
/// # let expected: oasiscap::v1dot2::Alert = expected.parse().unwrap();
/// # let mut alert = alert;
/// # alert.info[0].instruction = None;
/// # alert.info[0].description = None;
/// # alert.info[0].resources[0].mime_type = "image/gif".into();
/// # let mut expected = expected;
/// # expected.info[0].instruction = None;
/// # expected.info[0].description = None;
/// # assert_eq!(alert, expected);
/// ```
///
/// 2. CAP v1.0 has `Certainty::VeryLikely`, while later versions do not. The specification
/// recommends substituting `Certainty::Likely`, so this crate does.
///
/// ```
/// # let input = include_str!("../fixtures/v1dot0_appendix_adot3.xml");
/// // let input: &str = /* CAP v1.0 appendix A.3 */;
/// let alert: oasiscap::Alert = input.parse().unwrap();
/// match &alert {
/// oasiscap::Alert::V1dot0(alert) => {
/// assert_eq!(alert.info[0].certainty, oasiscap::v1dot0::Certainty::VeryLikely);
/// }
/// _ => unreachable!(),
/// }
///
/// let alert = alert.into_latest();
/// assert_eq!(alert.info[0].certainty, oasiscap::v1dot2::Certainty::Likely);
/// ```
pub fn into_latest(self) -> crate::v1dot2::Alert {
match self {
Alert::V1dot0(alert) => alert.into(),
Alert::V1dot1(alert) => alert.into(),
Alert::V1dot2(alert) => alert,
}
}
}
impl std::str::FromStr for Alert {
type Err = xml_serde::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
xml_serde::from_str(s)
}
}
impl std::fmt::Display for Alert {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
xml_serde::to_string(self)
.map_err(|_| std::fmt::Error)
.and_then(|str| f.write_str(&str))
}
}