imessage_database/message_types/
placemark.rs1use 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#[derive(Debug, PartialEq, Eq, Default)]
15pub struct Placemark<'a> {
16 pub name: Option<&'a str>,
18 pub address: Option<&'a str>,
20 pub state: Option<&'a str>,
22 pub city: Option<&'a str>,
24 pub iso_country_code: Option<&'a str>,
26 pub postal_code: Option<&'a str>,
28 pub country: Option<&'a str>,
30 pub street: Option<&'a str>,
32 pub sub_administrative_area: Option<&'a str>,
34 pub sub_locality: Option<&'a str>,
36}
37
38impl<'a> Placemark<'a> {
39 fn new(payload: &'a Value) -> Result<Self, PlistParseError> {
41 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#[derive(Debug, PartialEq, Eq)]
73pub struct PlacemarkMessage<'a> {
74 pub url: Option<&'a str>,
76 pub original_url: Option<&'a str>,
78 pub place_name: Option<&'a str>,
80 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 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 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 #[must_use]
129 pub fn get_url(&self) -> Option<&str> {
130 self.url.or(self.original_url)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use crate::{
137 message_types::{
138 placemark::{Placemark, PlacemarkMessage},
139 variants::BalloonProvider,
140 },
141 util::plist::parse_ns_keyed_archiver,
142 };
143 use plist::Value;
144 use std::env::current_dir;
145 use std::fs::File;
146
147 #[test]
148 fn test_parse_app_store_link() {
149 let plist_path = current_dir()
150 .unwrap()
151 .as_path()
152 .join("test_data/shared_placemark/SharedPlacemark.plist");
153 let plist_data = File::open(plist_path).unwrap();
154 let plist = Value::from_reader(plist_data).unwrap();
155 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
156
157 let balloon = PlacemarkMessage::from_map(&parsed).unwrap();
158 let expected = PlacemarkMessage {
159 url: Some(
160 "https://maps.apple.com/?address=Cherry%20Cove,%20Avalon,%20CA%20%2090704,%20United%20States&ll=33.450858,-118.508212&q=Cherry%20Cove&t=m",
161 ),
162 original_url: Some(
163 "https://maps.apple.com/?address=Cherry%20Cove,%20Avalon,%20CA%20%2090704,%20United%20States&ll=33.450858,-118.508212&q=Cherry%20Cove&t=m",
164 ),
165 place_name: Some("Cherry Cove Avalon CA 90704 United States"),
166 placemark: Placemark {
167 name: Some("Cherry Cove"),
168 address: Some("Cherry Cove, Avalon"),
169 state: Some("CA"),
170 city: Some("Avalon"),
171 iso_country_code: Some("US"),
172 postal_code: Some("90704"),
173 country: Some("United States"),
174 street: Some("Cherry Cove"),
175 sub_administrative_area: Some("Los Angeles County"),
176 sub_locality: Some("Santa Catalina Island"),
177 },
178 };
179
180 assert_eq!(balloon, expected);
181 }
182
183 #[test]
184 fn can_parse_placemark() {
185 let plist_path = current_dir()
186 .unwrap()
187 .as_path()
188 .join("test_data/shared_placemark/SharedPlacemark.plist");
189 let plist_data = File::open(plist_path).unwrap();
190 let plist = Value::from_reader(plist_data).unwrap();
191 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
192
193 let (placemark_data, _) = PlacemarkMessage::get_body_and_url(&parsed).unwrap();
194
195 let placemark = Placemark::new(placemark_data).unwrap();
196 let expected = Placemark {
197 name: Some("Cherry Cove"),
198 address: Some("Cherry Cove, Avalon"),
199 state: Some("CA"),
200 city: Some("Avalon"),
201 iso_country_code: Some("US"),
202 postal_code: Some("90704"),
203 country: Some("United States"),
204 street: Some("Cherry Cove"),
205 sub_administrative_area: Some("Los Angeles County"),
206 sub_locality: Some("Santa Catalina Island"),
207 };
208
209 assert_eq!(placemark, expected);
210 }
211}