Skip to main content

ios_core/services/simlocation/
mod.rs

1//! Raw location simulation service client.
2//!
3//! Talks to `com.apple.dt.simulatelocation` over a lockdown-started service
4//! stream. The protocol is intentionally tiny for this first slice:
5//! - `set(lat, lon)` sends mode `0`, followed by two length-prefixed strings
6//! - `reset()` sends mode `1` only
7
8use std::time::Duration;
9
10use quick_xml::de::from_str;
11use serde::Deserialize;
12use time::{format_description::well_known::Rfc3339, OffsetDateTime};
13use tokio::io::{AsyncWrite, AsyncWriteExt};
14
15pub const SERVICE_NAME: &str = "com.apple.dt.simulatelocation";
16
17#[derive(Debug, thiserror::Error)]
18pub enum SimLocationError {
19    #[error("IO error: {0}")]
20    Io(#[from] std::io::Error),
21    #[error("GPX parse error: {0}")]
22    GpxParse(String),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct GpxRoutePoint {
27    pub latitude: String,
28    pub longitude: String,
29    pub delay_from_previous: Duration,
30}
31
32/// Send a location simulation request with raw latitude/longitude strings.
33pub async fn set_location<S>(
34    stream: &mut S,
35    latitude: &str,
36    longitude: &str,
37) -> Result<(), SimLocationError>
38where
39    S: AsyncWrite + Unpin + ?Sized,
40{
41    write_u32_le(stream, 0).await?;
42    write_prefixed_string(stream, latitude).await?;
43    write_prefixed_string(stream, longitude).await?;
44    stream.flush().await?;
45    Ok(())
46}
47
48/// Reset the device back to its actual GPS location.
49pub async fn reset_location<S>(stream: &mut S) -> Result<(), SimLocationError>
50where
51    S: AsyncWrite + Unpin + ?Sized,
52{
53    write_u32_le(stream, 1).await?;
54    stream.flush().await?;
55    Ok(())
56}
57
58pub fn parse_gpx_route(gpx: &str) -> Result<Vec<GpxRoutePoint>, SimLocationError> {
59    let parsed: Gpx = from_str(gpx).map_err(|e| SimLocationError::GpxParse(e.to_string()))?;
60    let mut points = Vec::new();
61    let mut previous_time: Option<String> = None;
62
63    for track in parsed.tracks {
64        for segment in track.segments {
65            for point in segment.points {
66                let delay_from_previous = match (&previous_time, point.time.as_deref()) {
67                    (Some(previous), Some(current)) => parse_delay(previous, current)?,
68                    _ => Duration::ZERO,
69                };
70                if let Some(current) = point.time.as_deref() {
71                    previous_time = Some(current.to_string());
72                }
73                points.push(GpxRoutePoint {
74                    latitude: point.latitude,
75                    longitude: point.longitude,
76                    delay_from_previous,
77                });
78            }
79        }
80    }
81
82    Ok(points)
83}
84
85pub async fn replay_gpx_route<S>(stream: &mut S, gpx: &str) -> Result<usize, SimLocationError>
86where
87    S: AsyncWrite + Unpin + ?Sized,
88{
89    let route = parse_gpx_route(gpx)?;
90    for point in &route {
91        if !point.delay_from_previous.is_zero() {
92            tokio::time::sleep(point.delay_from_previous).await;
93        }
94        set_location(stream, &point.latitude, &point.longitude).await?;
95    }
96    Ok(route.len())
97}
98
99async fn write_prefixed_string<S>(stream: &mut S, value: &str) -> Result<(), std::io::Error>
100where
101    S: AsyncWrite + Unpin + ?Sized,
102{
103    write_u32_le(stream, value.len() as u32).await?;
104    stream.write_all(value.as_bytes()).await
105}
106
107async fn write_u32_le<S>(stream: &mut S, value: u32) -> Result<(), std::io::Error>
108where
109    S: AsyncWrite + Unpin + ?Sized,
110{
111    stream.write_all(&value.to_le_bytes()).await
112}
113
114fn parse_delay(previous: &str, current: &str) -> Result<Duration, SimLocationError> {
115    let previous = OffsetDateTime::parse(previous, &Rfc3339)
116        .map_err(|e| SimLocationError::GpxParse(e.to_string()))?;
117    let current = OffsetDateTime::parse(current, &Rfc3339)
118        .map_err(|e| SimLocationError::GpxParse(e.to_string()))?;
119    let delta = current - previous;
120    if delta.is_negative() {
121        Ok(Duration::ZERO)
122    } else {
123        Ok(Duration::from_secs(delta.whole_seconds() as u64))
124    }
125}
126
127#[derive(Debug, Deserialize)]
128struct Gpx {
129    #[serde(rename = "trk", default)]
130    tracks: Vec<GpxTrack>,
131}
132
133#[derive(Debug, Deserialize)]
134struct GpxTrack {
135    #[serde(rename = "trkseg", default)]
136    segments: Vec<GpxSegment>,
137}
138
139#[derive(Debug, Deserialize)]
140struct GpxSegment {
141    #[serde(rename = "trkpt", default)]
142    points: Vec<GpxPoint>,
143}
144
145#[derive(Debug, Deserialize)]
146struct GpxPoint {
147    #[serde(rename = "@lat")]
148    latitude: String,
149    #[serde(rename = "@lon")]
150    longitude: String,
151    #[serde(rename = "time")]
152    time: Option<String>,
153}
154
155#[cfg(test)]
156mod tests {
157    use std::pin::Pin;
158    use std::task::{Context, Poll};
159
160    use tokio::io::AsyncWrite;
161
162    use super::*;
163
164    struct MockWriter {
165        bytes: Vec<u8>,
166    }
167
168    impl MockWriter {
169        fn new() -> Self {
170            Self { bytes: Vec::new() }
171        }
172    }
173
174    impl AsyncWrite for MockWriter {
175        fn poll_write(
176            self: Pin<&mut Self>,
177            _cx: &mut Context<'_>,
178            buf: &[u8],
179        ) -> Poll<std::io::Result<usize>> {
180            self.get_mut().bytes.extend_from_slice(buf);
181            Poll::Ready(Ok(buf.len()))
182        }
183
184        fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
185            Poll::Ready(Ok(()))
186        }
187
188        fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
189            Poll::Ready(Ok(()))
190        }
191    }
192
193    #[tokio::test]
194    async fn encodes_set_payload_as_expected() {
195        let mut buf = MockWriter::new();
196        set_location(&mut buf, "48.856614", "2.3522219")
197            .await
198            .unwrap();
199
200        let mut expected = Vec::new();
201        expected.extend_from_slice(&0u32.to_le_bytes());
202        expected.extend_from_slice(&(9u32).to_le_bytes());
203        expected.extend_from_slice(b"48.856614");
204        expected.extend_from_slice(&(9u32).to_le_bytes());
205        expected.extend_from_slice(b"2.3522219");
206
207        assert_eq!(buf.bytes, expected);
208    }
209
210    #[tokio::test]
211    async fn encodes_reset_payload_as_expected() {
212        let mut buf = MockWriter::new();
213        reset_location(&mut buf).await.unwrap();
214
215        assert_eq!(buf.bytes, 1u32.to_le_bytes());
216    }
217
218    #[tokio::test]
219    async fn strings_are_written_without_extra_framing() {
220        let mut buf = MockWriter::new();
221        write_prefixed_string(&mut buf, "abc").await.unwrap();
222        assert_eq!(buf.bytes, vec![3, 0, 0, 0, b'a', b'b', b'c']);
223    }
224
225    #[test]
226    fn parses_gpx_route_and_preserves_timing_deltas() {
227        let gpx = r#"
228            <gpx>
229              <trk>
230                <trkseg>
231                  <trkpt lat="48.856614" lon="2.3522219">
232                    <time>2026-04-03T00:00:00Z</time>
233                  </trkpt>
234                  <trkpt lat="48.857000" lon="2.353000">
235                    <time>2026-04-03T00:00:03Z</time>
236                  </trkpt>
237                </trkseg>
238              </trk>
239            </gpx>
240        "#;
241
242        let route = parse_gpx_route(gpx).unwrap();
243        assert_eq!(route.len(), 2);
244        assert_eq!(route[0].latitude, "48.856614");
245        assert_eq!(route[0].longitude, "2.3522219");
246        assert_eq!(route[0].delay_from_previous, Duration::ZERO);
247        assert_eq!(route[1].delay_from_previous, Duration::from_secs(3));
248    }
249
250    #[tokio::test]
251    async fn replay_gpx_route_sends_each_point_in_order() {
252        let gpx = r#"
253            <gpx>
254              <trk>
255                <trkseg>
256                  <trkpt lat="48.856614" lon="2.3522219" />
257                  <trkpt lat="48.857000" lon="2.353000" />
258                </trkseg>
259              </trk>
260            </gpx>
261        "#;
262
263        let mut buf = MockWriter::new();
264        let count = replay_gpx_route(&mut buf, gpx).await.unwrap();
265        assert_eq!(count, 2);
266
267        let mut expected = Vec::new();
268        expected.extend_from_slice(&0u32.to_le_bytes());
269        expected.extend_from_slice(&(9u32).to_le_bytes());
270        expected.extend_from_slice(b"48.856614");
271        expected.extend_from_slice(&(9u32).to_le_bytes());
272        expected.extend_from_slice(b"2.3522219");
273        expected.extend_from_slice(&0u32.to_le_bytes());
274        expected.extend_from_slice(&(9u32).to_le_bytes());
275        expected.extend_from_slice(b"48.857000");
276        expected.extend_from_slice(&(8u32).to_le_bytes());
277        expected.extend_from_slice(b"2.353000");
278
279        assert_eq!(buf.bytes, expected);
280    }
281}