1use std::io::Read;
11use std::path::Path;
12
13use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
14
15pub const SERVICE_NAME: &str = "com.apple.streaming_zip_conduit";
16pub const RSD_SERVICE_NAME: &str = "com.apple.streaming_zip_conduit.shim.remote";
17
18#[derive(Debug, thiserror::Error)]
19pub enum ZipConduitError {
20 #[error("IO error: {0}")]
21 Io(#[from] std::io::Error),
22 #[error("plist error: {0}")]
23 Plist(String),
24 #[error("zip error: {0}")]
25 Zip(String),
26 #[error("install error: {0}")]
27 Install(String),
28}
29
30pub type ProgressCallback = Box<dyn Fn(u32, &str) + Send>;
32
33pub async fn install_ipa<S>(
39 stream: &mut S,
40 ipa_path: &Path,
41 progress: Option<ProgressCallback>,
42) -> Result<(), ZipConduitError>
43where
44 S: AsyncRead + AsyncWrite + Unpin,
45{
46 let filename = ipa_path
47 .file_name()
48 .and_then(|n| n.to_str())
49 .unwrap_or("app.ipa");
50
51 let ipa_data = tokio::fs::read(ipa_path).await?;
53 let entries = extract_zip_entries(&ipa_data)?;
54
55 let total_uncompressed: u64 = entries.iter().map(|e| e.data.len() as u64).sum();
57 let record_count = 2 + entries.len() as u64;
59
60 let init_plist = build_init_transfer(filename);
62 send_plist(stream, &init_plist).await?;
63
64 write_zip_dir_entry(stream, "META-INF/").await?;
68
69 let metadata = build_zip_metadata(record_count, total_uncompressed);
71 let metadata_bytes = plist_to_xml_bytes(&metadata)?;
72 write_zip_file_entry(
73 stream,
74 "META-INF/com.apple.ZipMetadata.plist",
75 &metadata_bytes,
76 )
77 .await?;
78
79 for entry in &entries {
81 if entry.is_dir {
82 write_zip_dir_entry(stream, &entry.name).await?;
83 } else {
84 write_zip_file_entry(stream, &entry.name, &entry.data).await?;
85 }
86 }
87
88 stream.write_all(&[0x50, 0x4b, 0x01, 0x02]).await?;
90 stream.flush().await?;
91
92 loop {
94 let resp = recv_plist(stream).await?;
95
96 if let Some(status) = resp
97 .as_dictionary()
98 .and_then(|d| d.get("Status"))
99 .and_then(|v| v.as_string())
100 {
101 if status == "DataComplete" {
102 return Ok(());
103 }
104 }
105
106 if let Some(progress_dict) = resp
107 .as_dictionary()
108 .and_then(|d| d.get("InstallProgressDict"))
109 .and_then(|v| v.as_dictionary())
110 {
111 if let Some(error) = progress_dict.get("Error").and_then(|v| v.as_string()) {
112 let desc = progress_dict
113 .get("ErrorDescription")
114 .and_then(|v| v.as_string())
115 .unwrap_or("unknown");
116 return Err(ZipConduitError::Install(format!("{error}: {desc}")));
117 }
118
119 let percent = progress_dict
120 .get("PercentComplete")
121 .and_then(|v| v.as_unsigned_integer())
122 .unwrap_or(0) as u32;
123 let status = progress_dict
124 .get("Status")
125 .and_then(|v| v.as_string())
126 .unwrap_or("Unknown");
127
128 if status == "Complete" {
129 return Ok(());
130 }
131
132 if let Some(ref cb) = progress {
133 cb(percent, status);
134 }
135 }
136 }
137}
138
139struct ZipEntry {
142 name: String,
143 is_dir: bool,
144 data: Vec<u8>,
145}
146
147fn extract_zip_entries(data: &[u8]) -> Result<Vec<ZipEntry>, ZipConduitError> {
149 let reader = std::io::Cursor::new(data);
150 let mut archive =
151 zip::ZipArchive::new(reader).map_err(|e| ZipConduitError::Zip(e.to_string()))?;
152
153 let mut entries = Vec::with_capacity(archive.len());
154 for i in 0..archive.len() {
155 let mut file = archive
156 .by_index(i)
157 .map_err(|e| ZipConduitError::Zip(e.to_string()))?;
158 let name = file.name().to_string();
159 let is_dir = file.is_dir();
160
161 let mut file_data = Vec::new();
162 if !is_dir {
163 file.read_to_end(&mut file_data)
164 .map_err(|e| ZipConduitError::Zip(format!("failed to read {name}: {e}")))?;
165 }
166
167 entries.push(ZipEntry {
168 name,
169 is_dir,
170 data: file_data,
171 });
172 }
173 Ok(entries)
174}
175
176const FIXED_MOD_TIME: u16 = 0xBDEF;
180const FIXED_MOD_DATE: u16 = 0x52EC;
181
182const EXTRA_FIELD: [u8; 32] = [
184 0x55, 0x54, 0x0D, 0x00, 0x07, 0xF3, 0xA2, 0xEC, 0x60, 0xF6, 0xA2, 0xEC, 0x60, 0xF3, 0xA2, 0xEC,
185 0x60, 0x75, 0x78, 0x0B, 0x00, 0x01, 0x04, 0xF5, 0x01, 0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00,
186];
187
188async fn write_zip_dir_entry<S: AsyncWrite + Unpin>(
189 stream: &mut S,
190 name: &str,
191) -> Result<(), ZipConduitError> {
192 write_local_file_header(stream, name, 0, 0, 0).await
193}
194
195async fn write_zip_file_entry<S: AsyncWrite + Unpin>(
196 stream: &mut S,
197 name: &str,
198 data: &[u8],
199) -> Result<(), ZipConduitError> {
200 let crc = crc32fast::hash(data);
201 let size = data.len() as u32;
202 write_local_file_header(stream, name, crc, size, size).await?;
203 stream.write_all(data).await?;
204 Ok(())
205}
206
207async fn write_local_file_header<S: AsyncWrite + Unpin>(
208 stream: &mut S,
209 filename: &str,
210 crc32: u32,
211 compressed_size: u32,
212 uncompressed_size: u32,
213) -> Result<(), ZipConduitError> {
214 let name_bytes = filename.as_bytes();
215 let mut header = Vec::with_capacity(30 + name_bytes.len() + EXTRA_FIELD.len());
216
217 header.extend_from_slice(&0x04034b50u32.to_le_bytes());
219 header.extend_from_slice(&20u16.to_le_bytes());
221 header.extend_from_slice(&0u16.to_le_bytes());
223 header.extend_from_slice(&0u16.to_le_bytes());
225 header.extend_from_slice(&FIXED_MOD_TIME.to_le_bytes());
227 header.extend_from_slice(&FIXED_MOD_DATE.to_le_bytes());
229 header.extend_from_slice(&crc32.to_le_bytes());
231 header.extend_from_slice(&compressed_size.to_le_bytes());
233 header.extend_from_slice(&uncompressed_size.to_le_bytes());
235 header.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
237 header.extend_from_slice(&(EXTRA_FIELD.len() as u16).to_le_bytes());
239 header.extend_from_slice(name_bytes);
241 header.extend_from_slice(&EXTRA_FIELD);
243
244 stream.write_all(&header).await?;
245 Ok(())
246}
247
248fn build_init_transfer(filename: &str) -> plist::Value {
251 let mut dict = plist::Dictionary::new();
252 dict.insert(
253 "InstallTransferredDirectory".to_string(),
254 plist::Value::Integer(1.into()),
255 );
256 dict.insert(
257 "UserInitiatedTransfer".to_string(),
258 plist::Value::Integer(0.into()),
259 );
260 dict.insert(
261 "MediaSubdir".to_string(),
262 plist::Value::String(format!("PublicStaging/{filename}")),
263 );
264
265 let mut options = plist::Dictionary::new();
266 options.insert(
267 "InstallDeltaTypeKey".to_string(),
268 plist::Value::String("InstallDeltaTypeSparseIPAFiles".to_string()),
269 );
270 options.insert(
271 "DisableDeltaTransfer".to_string(),
272 plist::Value::Integer(1.into()),
273 );
274 options.insert(
275 "IsUserInitiated".to_string(),
276 plist::Value::Integer(1.into()),
277 );
278 options.insert("PreferWifi".to_string(), plist::Value::Integer(1.into()));
279 options.insert(
280 "PackageType".to_string(),
281 plist::Value::String("Customer".to_string()),
282 );
283 dict.insert(
284 "InstallOptionsDictionary".to_string(),
285 plist::Value::Dictionary(options),
286 );
287
288 plist::Value::Dictionary(dict)
289}
290
291fn build_zip_metadata(record_count: u64, total_uncompressed: u64) -> plist::Value {
292 let mut dict = plist::Dictionary::new();
293 dict.insert(
294 "RecordCount".to_string(),
295 plist::Value::Integer((record_count as i64).into()),
296 );
297 dict.insert(
298 "StandardDirectoryPerms".to_string(),
299 plist::Value::Integer(16877.into()), );
301 dict.insert(
302 "StandardFilePerms".to_string(),
303 plist::Value::Integer((-32348i64).into()), );
305 dict.insert(
306 "TotalUncompressedBytes".to_string(),
307 plist::Value::Integer((total_uncompressed as i64).into()),
308 );
309 dict.insert("Version".to_string(), plist::Value::Integer(2.into()));
310 plist::Value::Dictionary(dict)
311}
312
313fn plist_to_xml_bytes(value: &plist::Value) -> Result<Vec<u8>, ZipConduitError> {
314 let mut buf = Vec::new();
315 plist::to_writer_xml(&mut buf, value).map_err(|e| ZipConduitError::Plist(e.to_string()))?;
316 Ok(buf)
317}
318
319async fn send_plist<S: AsyncWrite + Unpin>(
320 stream: &mut S,
321 value: &plist::Value,
322) -> Result<(), ZipConduitError> {
323 let buf = plist_to_xml_bytes(value)?;
324 stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
325 stream.write_all(&buf).await?;
326 stream.flush().await?;
327 Ok(())
328}
329
330async fn recv_plist<S: AsyncRead + Unpin>(stream: &mut S) -> Result<plist::Value, ZipConduitError> {
331 let mut len_buf = [0u8; 4];
332 stream.read_exact(&mut len_buf).await?;
333 let len = u32::from_be_bytes(len_buf) as usize;
334 if len > 4 * 1024 * 1024 {
335 return Err(ZipConduitError::Plist(format!("plist too large: {len}")));
336 }
337 let mut buf = vec![0u8; len];
338 stream.read_exact(&mut buf).await?;
339 plist::from_bytes(&buf).map_err(|e| ZipConduitError::Plist(e.to_string()))
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn build_init_transfer_has_correct_fields() {
348 let plist = build_init_transfer("Example.ipa");
349 let dict = plist.as_dictionary().unwrap();
350 assert_eq!(
351 dict["MediaSubdir"].as_string(),
352 Some("PublicStaging/Example.ipa")
353 );
354 assert_eq!(
355 dict["InstallTransferredDirectory"].as_signed_integer(),
356 Some(1)
357 );
358 let opts = dict["InstallOptionsDictionary"].as_dictionary().unwrap();
359 assert_eq!(opts["PackageType"].as_string(), Some("Customer"));
360 assert_eq!(
361 opts["InstallDeltaTypeKey"].as_string(),
362 Some("InstallDeltaTypeSparseIPAFiles")
363 );
364 }
365
366 #[test]
367 fn build_zip_metadata_has_correct_structure() {
368 let meta = build_zip_metadata(42, 1_000_000);
369 let dict = meta.as_dictionary().unwrap();
370 assert_eq!(dict["RecordCount"].as_signed_integer(), Some(42));
371 assert_eq!(dict["Version"].as_signed_integer(), Some(2));
372 assert_eq!(
373 dict["TotalUncompressedBytes"].as_signed_integer(),
374 Some(1_000_000)
375 );
376 assert_eq!(
377 dict["StandardDirectoryPerms"].as_signed_integer(),
378 Some(16877)
379 );
380 }
381
382 #[test]
383 fn local_file_header_has_correct_signature() {
384 let rt = tokio::runtime::Builder::new_current_thread()
385 .build()
386 .unwrap();
387 rt.block_on(async {
388 let mut buf = Vec::new();
389 write_local_file_header(&mut buf, "test.txt", 0x12345678, 100, 100)
390 .await
391 .unwrap();
392 assert_eq!(&buf[0..4], &[0x50, 0x4b, 0x03, 0x04]);
394 assert_eq!(u16::from_le_bytes([buf[4], buf[5]]), 20);
396 assert_eq!(u16::from_le_bytes([buf[8], buf[9]]), 0);
398 assert_eq!(
400 u32::from_le_bytes([buf[14], buf[15], buf[16], buf[17]]),
401 0x12345678
402 );
403 });
404 }
405
406 #[test]
407 fn extra_field_starts_with_ut_signature() {
408 assert_eq!(EXTRA_FIELD[0], 0x55); assert_eq!(EXTRA_FIELD[1], 0x54); }
412}