ios_core/services/simlocation/
mod.rs1use 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
32pub 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
48pub 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}