cot_proto/tak/
detect.rs

1use quick_xml::Reader;
2
3/// Support for detecting common variants of CoT messages used by TAK, and parsing them into
4/// strongly-typed structs.
5use crate::{
6    detail::{parse, CotUnparsedDetail},
7    Error,
8};
9
10/// An enum of expected message types.
11#[derive(Copy, Clone, Debug, PartialEq)]
12pub enum TakCotType {
13    GeoFence,
14    Marker,
15    RangeBearing,
16    Route,
17    Shape,
18    Other,
19}
20
21/// Result of parsing a TAK CoT message and attempting to detect a message type.
22///
23/// We don't actually parse the `<detail>` section, since it is so dynamic in practice.
24/// Thus, callers are responsible for further parsing of that section, for example using
25/// [`quick_xml::Reader`].
26pub struct TakCotMessage {
27    pub cot_type: TakCotType,
28    pub cot_msg: CotUnparsedDetail,
29}
30
31/// Use a heuristic to detect the type of TAK XML CoT message contained in supplied text.
32///
33/// Warning: This is based on known example messages from ATAK repository.
34///   To get the raw type string, use [`parse_cot_msg_type()`].
35pub fn detect_tak_cot_type(input: &str) -> Result<TakCotMessage, Error> {
36    let cot_msg = parse(input)?;
37    // List of tuples of <string to search for> -> <implied message type if found>
38    // Order matters. Separated into base event `type` values and detail section values to search
39    // for.
40    let type_tokens = [
41        ("u-r-b-", TakCotType::RangeBearing),
42        ("u-rb-", TakCotType::RangeBearing),
43        ("b-m-r", TakCotType::Route),
44        ("u-d-", TakCotType::Shape),
45    ];
46    let detail_tokens = [
47        ("__geofence", TakCotType::GeoFence),
48        ("usericon", TakCotType::Marker),
49    ];
50    // First, search detail section for clues
51    for line in &cot_msg.detail {
52        for (search, msg_type) in detail_tokens.iter() {
53            if line.contains(search) {
54                return Ok(TakCotMessage {
55                    cot_type: *msg_type,
56                    cot_msg,
57                });
58            }
59        }
60    }
61    // Next check the base event type
62    for (search, msg_type) in type_tokens.iter() {
63        if cot_msg.cot_type.contains(search) {
64            return Ok(TakCotMessage {
65                cot_type: *msg_type,
66                cot_msg,
67            });
68        }
69    }
70    Ok(TakCotMessage {
71        cot_type: TakCotType::Other,
72        cot_msg,
73    })
74}
75
76/// Parse `type` attribute from a CoT message XML string.
77pub fn parse_cot_msg_type(text: &str) -> Result<String, Error> {
78    match xml_first_element_w_attr(text, "event", "type") {
79        Ok(Some(val)) => Ok(val),
80        _ => Err(Error::BadField("No element 'event' with attribute 'type'")),
81    }
82}
83
84/// XML parsing convenience
85pub fn xml_first_element_w_attr(
86    text: &str,
87    elt_name: &str,
88    attr_name: &str,
89) -> Result<Option<String>, Error> {
90    let mut reader = Reader::from_str(text);
91    reader.config_mut().trim_text(true);
92    loop {
93        match reader.read_event()? {
94            // Parse attribute `type` in the `event` element.
95            quick_xml::events::Event::Start(ref e) => {
96                if e.name().into_inner() == elt_name.as_bytes() {
97                    for attr in e.attributes() {
98                        let attr = attr?;
99                        if attr.key.into_inner() == attr_name.as_bytes() {
100                            return Ok(Some(String::from_utf8_lossy(&attr.value).to_string()));
101                        }
102                    }
103                }
104            }
105            quick_xml::events::Event::Eof => break,
106            _ => {}
107        }
108    }
109    Ok(None)
110}
111
112#[cfg(test)]
113mod test {
114    use crate::tak::test::get_xml_examples;
115
116    use super::{detect_tak_cot_type, TakCotType};
117
118    #[test]
119    fn test_tak_cot_detect() {
120        let examples = get_xml_examples().unwrap();
121        for res in examples {
122            let (filename, cot_xml) = res.unwrap();
123            let cot = detect_tak_cot_type(&cot_xml).unwrap();
124            if filename.starts_with("geo-fence") {
125                assert_type(&filename, cot.cot_type, TakCotType::GeoFence);
126            } else if filename.starts_with("marker-") {
127                assert_type(&filename, cot.cot_type, TakCotType::Marker);
128            } else if filename.starts_with("range-bearing") {
129                assert_type(&filename, cot.cot_type, TakCotType::RangeBearing);
130            } else if filename.starts_with("route") {
131                assert_type(&filename, cot.cot_type, TakCotType::Route);
132            } else if filename.starts_with("shape-") {
133                assert_type(&filename, cot.cot_type, TakCotType::Shape);
134            }
135        }
136    }
137
138    fn assert_type(filename: &str, actual: TakCotType, expected: TakCotType) {
139        assert_eq!(
140            actual, expected,
141            "{}: expected {:?}, got {:?}",
142            filename, expected, actual
143        );
144    }
145}