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))
    }
}