imessage_database/message_types/
placemark.rs

1/*!
2 These are the link previews that iMessage generates when sending locations or points of interest from the Maps app.
3*/
4
5use plist::Value;
6
7use crate::{
8    error::plist::PlistParseError,
9    message_types::variants::BalloonProvider,
10    util::plist::{get_string_from_dict, get_string_from_nested_dict},
11};
12
13/// Representation of Apple's [`CLPlacemark`](https://developer.apple.com/documentation/corelocation/clplacemark) object
14#[derive(Debug, PartialEq, Eq, Default)]
15pub struct Placemark<'a> {
16    /// The name of the placemark
17    pub name: Option<&'a str>,
18    /// The full address formatted associated with the placemark
19    pub address: Option<&'a str>,
20    /// The state or province associated with the placemark
21    pub state: Option<&'a str>,
22    /// The city associated with the placemark
23    pub city: Option<&'a str>,
24    /// The abbreviated country or region name
25    pub iso_country_code: Option<&'a str>,
26    /// The postal code associated with the placemark
27    pub postal_code: Option<&'a str>,
28    /// The name of the country or region associated with the placemark
29    pub country: Option<&'a str>,
30    /// The street associated with the placemark
31    pub street: Option<&'a str>,
32    /// Additional administrative area information for the placemark
33    pub sub_administrative_area: Option<&'a str>,
34    /// Additional city-level information for the placemark
35    pub sub_locality: Option<&'a str>,
36}
37
38impl<'a> Placemark<'a> {
39    /// Create a Placemark from a `specialization2` payload
40    fn new(payload: &'a Value) -> Result<Self, PlistParseError> {
41        // Parse out the address components dict
42        let address_components = payload
43            .as_dictionary()
44            .ok_or_else(|| {
45                PlistParseError::InvalidType(
46                    "specialization2".to_string(),
47                    "dictionary".to_string(),
48                )
49            })?
50            .get("addressComponents")
51            .ok_or_else(|| PlistParseError::MissingKey("addressComponents".to_string()))?;
52        Ok(Self {
53            name: get_string_from_dict(payload, "name"),
54            address: get_string_from_dict(payload, "address"),
55            state: get_string_from_dict(address_components, "_state"),
56            city: get_string_from_dict(address_components, "_city"),
57            iso_country_code: get_string_from_dict(address_components, "_ISOCountryCode"),
58            postal_code: get_string_from_dict(address_components, "_postalCode"),
59            country: get_string_from_dict(address_components, "_country"),
60            street: get_string_from_dict(address_components, "_street"),
61            sub_administrative_area: get_string_from_dict(
62                address_components,
63                "_subAdministrativeArea",
64            ),
65            sub_locality: get_string_from_dict(address_components, "_subLocality"),
66        })
67    }
68}
69
70/// This struct is not documented by Apple, but represents messages displayed as
71/// `com.apple.messages.URLBalloonProvider` but for the Maps app
72#[derive(Debug, PartialEq, Eq)]
73pub struct PlacemarkMessage<'a> {
74    /// The URL that ended up serving content, after all redirects
75    pub url: Option<&'a str>,
76    /// The original url, before any redirects
77    pub original_url: Option<&'a str>,
78    /// The full street address of the location
79    pub place_name: Option<&'a str>,
80    /// [Placemark] data for the specified location
81    pub placemark: Placemark<'a>,
82}
83
84impl<'a> BalloonProvider<'a> for PlacemarkMessage<'a> {
85    fn from_map(payload: &'a Value) -> Result<Self, PlistParseError> {
86        if let Ok((placemark, body)) = PlacemarkMessage::get_body_and_url(payload) {
87            // Ensure the message is a placemark
88            if get_string_from_dict(placemark, "address").is_none() {
89                return Err(PlistParseError::WrongMessageType);
90            }
91
92            return Ok(Self {
93                url: get_string_from_nested_dict(body, "URL"),
94                original_url: get_string_from_nested_dict(body, "originalURL"),
95                place_name: get_string_from_dict(body, "title"),
96                placemark: Placemark::new(placemark).unwrap_or_default(),
97            });
98        }
99        Err(PlistParseError::NoPayload)
100    }
101}
102
103impl<'a> PlacemarkMessage<'a> {
104    /// Extract the main dictionary of data from the body of the payload
105    ///
106    /// Placemark messages store the URL under `richLinkMetadata` like a normal URL, but has some
107    /// extra data stored under `specialization2` that contains the placemark's metadata.
108    fn get_body_and_url(payload: &'a Value) -> Result<(&'a Value, &'a Value), PlistParseError> {
109        let base = payload
110            .as_dictionary()
111            .ok_or_else(|| {
112                PlistParseError::InvalidType("root".to_string(), "dictionary".to_string())
113            })?
114            .get("richLinkMetadata")
115            .ok_or_else(|| PlistParseError::MissingKey("richLinkMetadata".to_string()))?;
116        Ok((
117            base.as_dictionary()
118                .ok_or_else(|| {
119                    PlistParseError::InvalidType("root".to_string(), "dictionary".to_string())
120                })?
121                .get("specialization2")
122                .ok_or_else(|| PlistParseError::MissingKey("specialization2".to_string()))?,
123            base,
124        ))
125    }
126
127    /// Get the redirected URL from a URL message, falling back to the original URL, if it exists
128    pub fn get_url(&self) -> Option<&str> {
129        self.url.or(self.original_url)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::{
136        message_types::{
137            placemark::{Placemark, PlacemarkMessage},
138            variants::BalloonProvider,
139        },
140        util::plist::parse_ns_keyed_archiver,
141    };
142    use plist::Value;
143    use std::env::current_dir;
144    use std::fs::File;
145
146    #[test]
147    fn test_parse_app_store_link() {
148        let plist_path = current_dir()
149            .unwrap()
150            .as_path()
151            .join("test_data/shared_placemark/SharedPlacemark.plist");
152        let plist_data = File::open(plist_path).unwrap();
153        let plist = Value::from_reader(plist_data).unwrap();
154        let parsed = parse_ns_keyed_archiver(&plist).unwrap();
155
156        let balloon = PlacemarkMessage::from_map(&parsed).unwrap();
157        let expected = PlacemarkMessage {
158            url: Some("https://maps.apple.com/?address=Cherry%20Cove,%20Avalon,%20CA%20%2090704,%20United%20States&ll=33.450858,-118.508212&q=Cherry%20Cove&t=m"),
159            original_url: Some("https://maps.apple.com/?address=Cherry%20Cove,%20Avalon,%20CA%20%2090704,%20United%20States&ll=33.450858,-118.508212&q=Cherry%20Cove&t=m"),
160            place_name: Some("Cherry Cove Avalon CA 90704 United States"),
161            placemark: Placemark {
162                name: Some("Cherry Cove"),
163                address: Some("Cherry Cove, Avalon"),
164                state: Some("CA"),
165                city: Some("Avalon"),
166                iso_country_code: Some("US"),
167                postal_code: Some("90704"),
168                country: Some("United States"),
169                street: Some("Cherry Cove"),
170                sub_administrative_area: Some("Los Angeles County"),
171                sub_locality: Some("Santa Catalina Island"),
172            },
173        };
174
175        assert_eq!(balloon, expected);
176    }
177
178    #[test]
179    fn can_parse_placemark() {
180        let plist_path = current_dir()
181            .unwrap()
182            .as_path()
183            .join("test_data/shared_placemark/SharedPlacemark.plist");
184        let plist_data = File::open(plist_path).unwrap();
185        let plist = Value::from_reader(plist_data).unwrap();
186        let parsed = parse_ns_keyed_archiver(&plist).unwrap();
187
188        let (placemark_data, _) = PlacemarkMessage::get_body_and_url(&parsed).unwrap();
189
190        let placemark = Placemark::new(placemark_data).unwrap();
191        let expected = Placemark {
192            name: Some("Cherry Cove"),
193            address: Some("Cherry Cove, Avalon"),
194            state: Some("CA"),
195            city: Some("Avalon"),
196            iso_country_code: Some("US"),
197            postal_code: Some("90704"),
198            country: Some("United States"),
199            street: Some("Cherry Cove"),
200            sub_administrative_area: Some("Los Angeles County"),
201            sub_locality: Some("Santa Catalina Island"),
202        };
203
204        assert_eq!(placemark, expected);
205    }
206}