imessage-database 4.2.0

Parsers and tools to interact with iMessage SQLite data
Documentation
/*!
 Maps link previews stored in URL balloon payloads.
*/

use plist::Value;

use crate::{
    error::plist::PlistParseError,
    message_types::variants::{BalloonProvider, HasUrl},
    util::plist::{
        get_string_from_dict, get_string_from_nested_dict, rich_link_metadata_and_nested,
    },
};

/// Representation of Apple's [`CLPlacemark`](https://developer.apple.com/documentation/corelocation/clplacemark) object
#[derive(Debug, PartialEq, Eq, Default)]
pub struct Placemark<'a> {
    /// Placemark name.
    pub name: Option<&'a str>,
    /// Formatted address.
    pub address: Option<&'a str>,
    /// State or province.
    pub state: Option<&'a str>,
    /// City.
    pub city: Option<&'a str>,
    /// ISO country or region code.
    pub iso_country_code: Option<&'a str>,
    /// Postal code.
    pub postal_code: Option<&'a str>,
    /// Country or region name.
    pub country: Option<&'a str>,
    /// Street.
    pub street: Option<&'a str>,
    /// Sub-administrative area.
    pub sub_administrative_area: Option<&'a str>,
    /// Sub-locality.
    pub sub_locality: Option<&'a str>,
}

impl<'a> Placemark<'a> {
    /// Parse a placemark from a `specialization2` payload.
    fn new(payload: &'a Value) -> Result<Self, PlistParseError> {
        let address_components = payload
            .as_dictionary()
            .ok_or_else(|| {
                PlistParseError::InvalidType(
                    "specialization2".to_string(),
                    "dictionary".to_string(),
                )
            })?
            .get("addressComponents")
            .ok_or_else(|| PlistParseError::MissingKey("addressComponents".to_string()))?;
        Ok(Self {
            name: get_string_from_dict(payload, "name"),
            address: get_string_from_dict(payload, "address"),
            state: get_string_from_dict(address_components, "_state"),
            city: get_string_from_dict(address_components, "_city"),
            iso_country_code: get_string_from_dict(address_components, "_ISOCountryCode"),
            postal_code: get_string_from_dict(address_components, "_postalCode"),
            country: get_string_from_dict(address_components, "_country"),
            street: get_string_from_dict(address_components, "_street"),
            sub_administrative_area: get_string_from_dict(
                address_components,
                "_subAdministrativeArea",
            ),
            sub_locality: get_string_from_dict(address_components, "_subLocality"),
        })
    }
}

/// This struct is not documented by Apple, but represents messages displayed as
/// `com.apple.messages.URLBalloonProvider` but for the Maps app
#[derive(Debug, PartialEq, Eq)]
pub struct PlacemarkMessage<'a> {
    /// URL that served the preview content.
    pub url: Option<&'a str>,
    /// Original URL before redirects.
    pub original_url: Option<&'a str>,
    /// Location display name.
    pub place_name: Option<&'a str>,
    /// Placemark data for the location.
    pub placemark: Placemark<'a>,
}

impl<'a> BalloonProvider<'a> for PlacemarkMessage<'a> {
    fn from_map(payload: &'a Value) -> Result<Self, PlistParseError> {
        if let Ok((body, placemark)) = rich_link_metadata_and_nested(payload, "specialization2") {
            // Placemark payloads carry an address.
            if get_string_from_dict(placemark, "address").is_none() {
                return Err(PlistParseError::WrongMessageType);
            }

            return Ok(Self {
                url: get_string_from_nested_dict(body, "URL"),
                original_url: get_string_from_nested_dict(body, "originalURL"),
                place_name: get_string_from_dict(body, "title"),
                placemark: Placemark::new(placemark).unwrap_or_default(),
            });
        }
        Err(PlistParseError::NoPayload)
    }
}

impl<'a> PlacemarkMessage<'a> {
    /// Resolve this message's URL via [`HasUrl::get_url`].
    #[must_use]
    pub fn get_url(&self) -> Option<&str> {
        <Self as HasUrl>::get_url(self)
    }
}

impl HasUrl for PlacemarkMessage<'_> {
    fn url(&self) -> Option<&str> {
        self.url
    }

    fn original_url(&self) -> Option<&str> {
        self.original_url
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        message_types::{
            placemark::{Placemark, PlacemarkMessage},
            variants::BalloonProvider,
        },
        util::plist::{parse_ns_keyed_archiver, rich_link_metadata_and_nested},
    };
    use plist::Value;
    use std::env::current_dir;
    use std::fs::File;

    #[test]
    fn test_parse_app_store_link() {
        let plist_path = current_dir()
            .unwrap()
            .as_path()
            .join("test_data/shared_placemark/SharedPlacemark.plist");
        let plist_data = File::open(plist_path).unwrap();
        let plist = Value::from_reader(plist_data).unwrap();
        let parsed = parse_ns_keyed_archiver(&plist).unwrap();

        let balloon = PlacemarkMessage::from_map(&parsed).unwrap();
        let expected = PlacemarkMessage {
            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",
            ),
            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",
            ),
            place_name: Some("Cherry Cove Avalon CA 90704 United States"),
            placemark: Placemark {
                name: Some("Cherry Cove"),
                address: Some("Cherry Cove, Avalon"),
                state: Some("CA"),
                city: Some("Avalon"),
                iso_country_code: Some("US"),
                postal_code: Some("90704"),
                country: Some("United States"),
                street: Some("Cherry Cove"),
                sub_administrative_area: Some("Los Angeles County"),
                sub_locality: Some("Santa Catalina Island"),
            },
        };

        assert_eq!(balloon, expected);
    }

    #[test]
    fn can_parse_placemark() {
        let plist_path = current_dir()
            .unwrap()
            .as_path()
            .join("test_data/shared_placemark/SharedPlacemark.plist");
        let plist_data = File::open(plist_path).unwrap();
        let plist = Value::from_reader(plist_data).unwrap();
        let parsed = parse_ns_keyed_archiver(&plist).unwrap();

        let (_, placemark_data) =
            rich_link_metadata_and_nested(&parsed, "specialization2").unwrap();

        let placemark = Placemark::new(placemark_data).unwrap();
        let expected = Placemark {
            name: Some("Cherry Cove"),
            address: Some("Cherry Cove, Avalon"),
            state: Some("CA"),
            city: Some("Avalon"),
            iso_country_code: Some("US"),
            postal_code: Some("90704"),
            country: Some("United States"),
            street: Some("Cherry Cove"),
            sub_administrative_area: Some("Los Angeles County"),
            sub_locality: Some("Santa Catalina Island"),
        };

        assert_eq!(placemark, expected);
    }
}