Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Documentation for the library is located [here](imessage-database/README.md).

### Supported Features

This crate supports every iMessage feature as of macOS Tahoe 26.5 (25F71) and iOS 26.5.1 (23F81):
This crate supports every iMessage feature as of macOS Tahoe 26.5.1 (25F80) and iOS 26.5.1 (23F81):

- iMessage, RCS, SMS, and MMS
- Multi-part messages
Expand Down
12 changes: 11 additions & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,17 @@ This tool targets the current latest public release for Messages.app. It may wor
- `clone, basic, full`: saved as an `svg` file
- Digital Touch
- Parses the protobuf payload to extract [Digital Touch](https://support.apple.com/guide/ipod-touch/send-a-digital-touch-effect-iph3fadba219/ios) message data
- Displayed as text that describes the type of message sent in HTML and TXT exports
- Supports all Digital Touch effects:
- Taps
- Sketches
- Kisses
- Heartbeats, including heartbreaks
- Fireballs
- Photos and videos
- HTML exports render a static frame depicting the captured data as an embedded `svg` on a black `4:5` canvas
- A photo is shown as the canvas backdrop, with any sketch drawn over it
- A video is shown as an embedded video player
- TXT exports describe each effect on a single line, including stroke and point counts or beats per minute
- Duplicated group chats
- Handles (participants) and chats (threads) can become duplicated
- On startup:
Expand Down
56 changes: 56 additions & 0 deletions imessage-database/src/error/digital_touch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*!
Errors that can happen when parsing `digital touch` data.
*/

use std::fmt::{Display, Formatter, Result};

/// Errors that can happen when parsing [`digital touch`](crate::message_types::digital_touch) data.
#[derive(Debug)]
pub enum DigitalTouchError {
/// Wraps an error returned by the protobuf parser.
ProtobufError(protobuf::Error),
/// The `TouchKind` discriminant was not a value we know how to parse.
UnknownDigitalTouchKind(i32),
/// Two parallel arrays that are expected to describe the same events had
/// different lengths (name, length, other name, other length).
ArraysDoNotMatch(&'static str, usize, &'static str, usize),
/// A length-prefixed stroke ran past the end of its buffer (needed, available).
InvalidStrokesLength(usize, usize),
/// Wraps an error returned while reading an embedded `NSKeyedArchiver` archive.
ArchiveError(plist::Error),
}

impl std::error::Error for DigitalTouchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DigitalTouchError::ProtobufError(why) => Some(why),
DigitalTouchError::ArchiveError(why) => Some(why),
_ => None,
}
}
}

impl Display for DigitalTouchError {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
match self {
DigitalTouchError::ProtobufError(why) => {
write!(fmt, "failed to parse digital touch protobuf: {why}")
}
DigitalTouchError::UnknownDigitalTouchKind(kind) => {
write!(fmt, "unknown digital touch kind: {kind}")
}
DigitalTouchError::ArraysDoNotMatch(n1, v1, n2, v2) => {
write!(fmt, "mismatched array lengths: {n1} ({v1}) != {n2} ({v2})")
}
DigitalTouchError::InvalidStrokesLength(needed, available) => {
write!(
fmt,
"stroke needs {needed} bytes but only {available} remain"
)
}
DigitalTouchError::ArchiveError(why) => {
write!(fmt, "failed to read digital touch media archive: {why}")
}
}
}
}
1 change: 1 addition & 0 deletions imessage-database/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

pub mod attachment;
pub mod digital_touch;
pub mod handwriting;
pub mod message;
pub mod plist;
Expand Down
10 changes: 5 additions & 5 deletions imessage-database/src/error/plist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use crabstep::error::TypedStreamError;

use crate::error::digital_touch::DigitalTouchError;
use crate::error::handwriting::HandwritingError;
use crate::error::streamtyped::StreamTypedError;
use std::fmt::{Display, Formatter, Result};
Expand Down Expand Up @@ -33,8 +34,8 @@ pub enum PlistParseError {
TypedStreamError(TypedStreamError),
/// Error from handwriting data parsing
HandwritingError(HandwritingError),
/// Error parsing Digital Touch message
DigitalTouchError,
/// Error from Digital Touch data parsing
DigitalTouchError(DigitalTouchError),
/// Error parsing a poll message
PollError,
/// Exceeded the maximum UID-reference resolution depth (likely a reference cycle)
Expand Down Expand Up @@ -68,9 +69,7 @@ impl Display for PlistParseError {
}
PlistParseError::StreamTypedError(why) => write!(fmt, "{why}"),
PlistParseError::HandwritingError(why) => write!(fmt, "{why}"),
PlistParseError::DigitalTouchError => {
write!(fmt, "Unable to parse Digital Touch Message!")
}
PlistParseError::DigitalTouchError(why) => write!(fmt, "{why}"),
PlistParseError::TypedStreamError(typed_stream_error) => {
write!(fmt, "TypedStream error: {typed_stream_error}")
}
Expand All @@ -89,6 +88,7 @@ impl std::error::Error for PlistParseError {
PlistParseError::StreamTypedError(e) => Some(e),
PlistParseError::TypedStreamError(e) => Some(e),
PlistParseError::HandwritingError(e) => Some(e),
PlistParseError::DigitalTouchError(e) => Some(e),
_ => None,
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
syntax = 'proto3';
package digital_touch;

// Outer envelope shared by every Digital Touch effect.
//
// The raw `payload_data` blob decodes into this message. `TouchPayload` holds a
// second, effect-specific protobuf selected by `TouchKind`. The sender also
// stores a creation timestamp, a global color, and two trailing fields that are
// always zero in observed data; those are left out here and parsed as unknown
// fields because the per-effect payloads carry the meaningful state.
message BaseMessage {
TouchKind TouchKind = 1;
bytes TouchPayload = 3;
Expand All @@ -14,39 +21,57 @@ enum TouchKind {
Heartbeat = 3; // Also broken heart
Sketch = 4;
// 5?
// 6?
Media = 6; // Still image with optional overlays, or video
Kiss = 7;
Fireball = 8;
}

// Coordinates throughout are stored as little-endian `uint16` pairs (x, y) where
// the full `0..=65535` range maps to `0.0..=1.0` of the canvas (a 4:5 portrait
// on device), origin bottom-left (y grows upward). Colors are four bytes in RGBA
// order. Delays are little-endian `uint16` values in milliseconds.

message TapMessage {
bytes Delays = 2;
bytes Location = 3;
bytes Color = 4;
bytes Delays = 2; // [uint16] one delay (ms) per tap
bytes Location = 3; // [(uint16 x, uint16 y)] one point per tap
bytes Color = 4; // [RGBA] one color per tap
}

message SketchMessage {
int64 StrokesCount = 1;
int64 StrokesCount = 1; // number of strokes encoded in `Strokes`
// Concatenated strokes. Each stroke is:
// uint16 ?? (per-stroke header, observed as 0)
// uint16 Count (number of points in the stroke)
// [Count](uint16 x, uint16 y)
bytes Strokes = 2;
bytes Colors = 3;
bytes Colors = 3; // [RGBA] one color per stroke
}

message KissMessage {
bytes Delays = 1;
bytes Points = 2;
bytes Rotations = 3;
bytes Delays = 1; // [uint16] one delay (ms) per kiss
bytes Points = 2; // [(uint16 x, uint16 y)] one point per kiss
bytes Rotations = 3; // [uint16] one rotation per kiss, in milliradians
}

message HeartbeatMessage {
float BPM = 1;
uint64 Duration = 2;
float HeartBrokenAt = 6;
uint64 Duration = 2; // seconds
float HeartBrokenAt = 6; // seconds into the animation, 0 when not broken
}

message FireballMessage {
float Duration = 1;
float StartX = 2;
float Duration = 1; // seconds
float StartX = 2; // start offset, centered (roughly -1.0..=1.0)
float StartY = 3;
bytes Delays = 4;
bytes Points = 5;
}
bytes Delays = 4; // [uint16] one delay (ms) per point
bytes Points = 5; // [(uint16 x, uint16 y)] the dragged path
}

message MediaMessage {
// For image media, an `NSKeyedArchiver` archive holding an `NSMutableArray`
// of overlay effects. Each element is `NSData` containing a complete nested
// `BaseMessage`; supported overlays are any non-media `TouchKind`. The array
// is empty when nothing was drawn on top.
bytes Archive = 2;
uint64 MediaType = 4; // 1 = video, 2 = image
}
Loading
Loading