Skip to main content

imessage_database/message_types/
sticker.rs

1/*!
2These are [sticker messages](https://support.apple.com/guide/iphone/send-stickers-iph37b0bfe7b/ios), either from the user's sticker library or sticker apps.
3*/
4
5use std::fmt::{Display, Formatter};
6
7use crate::util::bundle_id::parse_balloon_bundle_id;
8
9/// Bytes for `stickerEffect:type="`
10const STICKER_EFFECT_PREFIX: [u8; 20] = [
11    115, 116, 105, 99, 107, 101, 114, 69, 102, 102, 101, 99, 116, 58, 116, 121, 112, 101, 61, 34,
12];
13/// Bytes for `"/>`
14const STICKER_EFFECT_SUFFIX: [u8; 3] = [34, 47, 62];
15
16/// Represents the source that created a sticker attachment
17#[derive(Debug, PartialEq, Eq)]
18pub enum StickerSource {
19    /// A [Genmoji](https://support.apple.com/guide/iphone/create-genmoji-with-apple-intelligence-iph4e76f5667/ios)
20    Genmoji,
21    /// A [Memoji](https://support.apple.com/en-us/111115)
22    Memoji,
23    /// User-created stickers
24    UserGenerated,
25    /// Application provided stickers
26    App(String),
27}
28
29impl StickerSource {
30    /// Given an application's bundle ID, determine the source
31    ///
32    /// # Example
33    ///
34    /// ```rust
35    /// use imessage_database::message_types::sticker::StickerSource;
36    ///
37    /// println!("{:?}", StickerSource::from_bundle_id("com.apple.messages.genmoji")); // Some(Genmoji)
38    /// ```
39    #[must_use]
40    pub fn from_bundle_id(bundle_id: &str) -> Option<Self> {
41        match parse_balloon_bundle_id(Some(bundle_id)) {
42            Some("com.apple.messages.genmoji") => Some(StickerSource::Genmoji),
43            Some(
44                "com.apple.Animoji.StickersApp.MessagesExtension" | "com.apple.Jellyfish.Animoji",
45            ) => Some(StickerSource::Memoji),
46            Some("com.apple.Stickers.UserGenerated.MessagesExtension") => {
47                Some(StickerSource::UserGenerated)
48            }
49            Some(other) => Some(StickerSource::App(other.to_string())),
50            None => None,
51        }
52    }
53}
54
55/// Format-agnostic description of a sticker's source, ready for rendering by
56/// callers. Produced by [`Attachment::get_sticker_decoration`](crate::tables::attachment::Attachment::get_sticker_decoration).
57///
58/// `None` from `get_sticker_decoration` means one of:
59/// - The sticker has no readable source (missing `STICKER_USER_INFO`,
60///   malformed plist, or unrecognized bundle id).
61/// - The source is [`StickerSource::Genmoji`] but no description is stored.
62/// - The source is [`StickerSource::UserGenerated`] but the effect blob is
63///   missing or unreadable.
64#[derive(Debug, PartialEq, Eq)]
65pub enum StickerDecoration {
66    /// A [`StickerSource::Genmoji`] with the user-supplied prompt.
67    GenmojiPrompt(String),
68    /// A [`StickerSource::Memoji`]; no further data.
69    Memoji,
70    /// A [`StickerSource::UserGenerated`] sticker with the parsed [`StickerEffect`].
71    Effect(StickerEffect),
72    /// A [`StickerSource::App`] sticker; the string is the resolved app name
73    /// or, if that lookup fails, the bundle id.
74    AppName(String),
75}
76
77/// Represents different types of [sticker effects](https://www.macrumors.com/how-to/add-effects-to-stickers-in-messages/) that can be applied to sticker iMessage balloons.
78#[derive(Debug, PartialEq, Eq, Default)]
79pub enum StickerEffect {
80    /// Sticker sent with no effect
81    #[default]
82    Normal,
83    /// Internally referred to as `stroke`
84    Outline,
85    /// Comic effect applied to the sticker
86    Comic,
87    /// Puffy effect applied to the sticker
88    Puffy,
89    /// Internally referred to as `iridescent`
90    Shiny,
91    /// Other unrecognized sticker effect
92    Other(String),
93}
94
95impl StickerEffect {
96    /// Determine the type of a sticker from parsed `HEIC` `EXIF` data
97    fn from_exif(sticker_effect_type: &str) -> Self {
98        match sticker_effect_type {
99            "stroke" => Self::Outline,
100            "comic" => Self::Comic,
101            "puffy" => Self::Puffy,
102            "iridescent" => Self::Shiny,
103            other => Self::Other(other.to_owned()),
104        }
105    }
106}
107
108impl Display for StickerEffect {
109    fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
110        match self {
111            StickerEffect::Normal => write!(fmt, "Normal"),
112            StickerEffect::Outline => write!(fmt, "Outline"),
113            StickerEffect::Comic => write!(fmt, "Comic"),
114            StickerEffect::Puffy => write!(fmt, "Puffy"),
115            StickerEffect::Shiny => write!(fmt, "Shiny"),
116            StickerEffect::Other(name) => write!(fmt, "{name}"),
117        }
118    }
119}
120
121/// Parse the sticker effect type from the EXIF data of a HEIC blob
122#[must_use]
123pub fn get_sticker_effect(mut heic_data: &[u8]) -> StickerEffect {
124    // Find the start index and drain
125    for idx in 0..heic_data.len() {
126        if idx + STICKER_EFFECT_PREFIX.len() < heic_data.len() {
127            let part = &heic_data[idx..idx + STICKER_EFFECT_PREFIX.len()];
128            if part == STICKER_EFFECT_PREFIX {
129                // Remove the start pattern from the blob
130                heic_data = &heic_data[idx + STICKER_EFFECT_PREFIX.len()..];
131                break;
132            }
133        } else {
134            return StickerEffect::Normal;
135        }
136    }
137
138    // Find the end index and truncate
139    for idx in 1..heic_data.len() {
140        if idx >= heic_data.len() - 2 {
141            return StickerEffect::Other("Unknown".to_string());
142        }
143        let part = &heic_data[idx..idx + STICKER_EFFECT_SUFFIX.len()];
144
145        if part == STICKER_EFFECT_SUFFIX {
146            // Remove the end pattern from the string
147            heic_data = &heic_data[..idx];
148            break;
149        }
150    }
151    StickerEffect::from_exif(&String::from_utf8_lossy(heic_data))
152}
153
154#[cfg(test)]
155mod tests {
156    use std::env::current_dir;
157    use std::fs::File;
158    use std::io::Read;
159
160    use crate::message_types::sticker::{StickerEffect, get_sticker_effect};
161
162    #[test]
163    fn test_parse_sticker_normal() {
164        let sticker_path = current_dir()
165            .unwrap()
166            .as_path()
167            .join("test_data/stickers/no_effect.heic");
168        let mut file = File::open(sticker_path).unwrap();
169        let mut bytes = vec![];
170        file.read_to_end(&mut bytes).unwrap();
171
172        let effect = get_sticker_effect(&bytes);
173
174        assert_eq!(effect, StickerEffect::Normal);
175    }
176
177    #[test]
178    fn test_parse_sticker_outline() {
179        let sticker_path = current_dir()
180            .unwrap()
181            .as_path()
182            .join("test_data/stickers/outline.heic");
183        let mut file = File::open(sticker_path).unwrap();
184        let mut bytes = vec![];
185        file.read_to_end(&mut bytes).unwrap();
186
187        let effect = get_sticker_effect(&bytes);
188
189        assert_eq!(effect, StickerEffect::Outline);
190    }
191
192    #[test]
193    fn test_parse_sticker_comic() {
194        let sticker_path = current_dir()
195            .unwrap()
196            .as_path()
197            .join("test_data/stickers/comic.heic");
198        let mut file = File::open(sticker_path).unwrap();
199        let mut bytes = vec![];
200        file.read_to_end(&mut bytes).unwrap();
201
202        let effect = get_sticker_effect(&bytes);
203
204        assert_eq!(effect, StickerEffect::Comic);
205    }
206
207    #[test]
208    fn test_parse_sticker_puffy() {
209        let sticker_path = current_dir()
210            .unwrap()
211            .as_path()
212            .join("test_data/stickers/puffy.heic");
213        let mut file = File::open(sticker_path).unwrap();
214        let mut bytes = vec![];
215        file.read_to_end(&mut bytes).unwrap();
216
217        let effect = get_sticker_effect(&bytes);
218
219        assert_eq!(effect, StickerEffect::Puffy);
220    }
221
222    #[test]
223    fn test_parse_sticker_shiny() {
224        let sticker_path = current_dir()
225            .unwrap()
226            .as_path()
227            .join("test_data/stickers/shiny.heic");
228        let mut file = File::open(sticker_path).unwrap();
229        let mut bytes = vec![];
230        file.read_to_end(&mut bytes).unwrap();
231
232        let effect = get_sticker_effect(&bytes);
233
234        assert_eq!(effect, StickerEffect::Shiny);
235    }
236}