Skip to main content

imessage_database/message_types/
placemark.rs

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