1use plist::{Dictionary, Uid, Value};
9use uuid::Uuid;
10
11#[derive(Debug, Clone)]
12pub struct NsUrl {
13 pub path: String,
14}
15
16#[derive(Debug, Clone)]
17pub struct XctCapabilities {
18 pub capabilities: Vec<(String, Value)>,
19}
20
21#[derive(Debug, Clone)]
22pub struct XcTestConfiguration {
23 pub session_identifier: Uuid,
24 pub test_bundle_url: NsUrl,
25 pub ide_capabilities: XctCapabilities,
26 pub automation_framework_path: String,
27 pub initialize_for_ui_testing: bool,
28 pub report_results_to_ide: bool,
29 pub tests_must_run_on_main_thread: bool,
30 pub test_timeouts_enabled: bool,
31 pub additional_fields: Vec<(String, Value)>,
32}
33
34pub fn archive_string(s: &str) -> Vec<u8> {
36 archive_value(Value::String(s.to_string()))
37}
38
39pub fn archive_int(n: i64) -> Vec<u8> {
41 archive_value(Value::Integer(n.into()))
42}
43
44pub fn archive_float(f: f64) -> Vec<u8> {
46 archive_value(Value::Real(f))
47}
48
49pub fn archive_bool(b: bool) -> Vec<u8> {
51 archive_value(Value::Boolean(b))
52}
53
54pub fn archive_null() -> Vec<u8> {
56 let mut objects = vec![Value::String("$null".to_string())];
57
58 let mut object = Dictionary::new();
59 object.insert("$class".to_string(), Value::Uid(Uid::new(2)));
60 objects.push(Value::Dictionary(object));
61 objects.push(class_descriptor("NSNull", &["NSNull", "NSObject"]));
62
63 let root_doc = build_keyed_archive(Value::Uid(Uid::new(1)), objects);
64 to_binary_plist(&root_doc)
65}
66
67pub fn archive_data(data: &[u8]) -> Vec<u8> {
69 archive_value(Value::Data(data.to_vec()))
70}
71
72pub fn archive_uuid(uuid: Uuid) -> Vec<u8> {
74 let mut objects = vec![Value::String("$null".to_string())];
75 let root_uid = archive_nsuuid_into(uuid, &mut objects);
76 let root_doc = build_keyed_archive(root_uid, objects);
77 to_binary_plist(&root_doc)
78}
79
80pub fn archive_nsurl(url: NsUrl) -> Vec<u8> {
82 let mut objects = vec![Value::String("$null".to_string())];
83 let root_uid = archive_nsurl_into(url, &mut objects);
84 let root_doc = build_keyed_archive(root_uid, objects);
85 to_binary_plist(&root_doc)
86}
87
88pub fn archive_xct_capabilities(capabilities: XctCapabilities) -> Vec<u8> {
90 let mut objects = vec![Value::String("$null".to_string())];
91 let root_uid = archive_xct_capabilities_into(capabilities, &mut objects);
92 let root_doc = build_keyed_archive(root_uid, objects);
93 to_binary_plist(&root_doc)
94}
95
96pub fn archive_xctest_configuration(config: XcTestConfiguration) -> Vec<u8> {
98 let mut objects = vec![Value::String("$null".to_string())];
99 let root_uid = archive_xctest_configuration_into(config, &mut objects);
100 let root_doc = build_keyed_archive(root_uid, objects);
101 to_binary_plist(&root_doc)
102}
103
104pub fn archive_array(items: Vec<Value>) -> Vec<u8> {
108 let count = items.len();
110 let mut objects = vec![Value::String("$null".to_string())];
111
112 let mut arr_obj = Dictionary::new();
114 arr_obj.insert("$class".to_string(), Value::Uid(Uid::new(2 + count as u64)));
115 let ns_objects: Vec<Value> = (0..count)
116 .map(|i| Value::Uid(Uid::new((2 + i) as u64)))
117 .collect();
118 arr_obj.insert("NS.objects".to_string(), Value::Array(ns_objects));
119 objects.push(Value::Dictionary(arr_obj));
120
121 for item in items {
123 objects.push(item);
124 }
125
126 let mut class_obj = Dictionary::new();
128 class_obj.insert(
129 "$classname".to_string(),
130 Value::String("NSArray".to_string()),
131 );
132 class_obj.insert(
133 "$classes".to_string(),
134 Value::Array(vec![
135 Value::String("NSArray".to_string()),
136 Value::String("NSObject".to_string()),
137 ]),
138 );
139 objects.push(Value::Dictionary(class_obj));
140
141 let root_doc = build_keyed_archive(Value::Uid(Uid::new(1)), objects);
142 to_binary_plist(&root_doc)
143}
144
145pub fn archive_dict(pairs: Vec<(String, Value)>) -> Vec<u8> {
147 let mut objects: Vec<Value> = vec![Value::String("$null".to_string())];
148 let root_uid = archive_dict_into(&pairs, &mut objects);
149 let root_doc = build_keyed_archive(root_uid, objects);
150 to_binary_plist(&root_doc)
151}
152
153fn archive_value_into(val: Value, objects: &mut Vec<Value>) -> Value {
155 match val {
156 Value::String(_)
158 | Value::Integer(_)
159 | Value::Real(_)
160 | Value::Boolean(_)
161 | Value::Data(_) => {
162 let idx = objects.len();
163 objects.push(val);
164 Value::Uid(Uid::new(idx as u64))
165 }
166 Value::Array(items) => {
167 let item_uids: Vec<Value> = items
169 .into_iter()
170 .map(|v| archive_value_into(v, objects))
171 .collect();
172
173 let arr_idx = objects.len();
174 let class_idx = arr_idx + 1;
175
176 let mut arr_obj = Dictionary::new();
177 arr_obj.insert("$class".to_string(), Value::Uid(Uid::new(class_idx as u64)));
178 arr_obj.insert("NS.objects".to_string(), Value::Array(item_uids));
179 objects.push(Value::Dictionary(arr_obj));
180
181 let mut class_obj = Dictionary::new();
182 class_obj.insert(
183 "$classname".to_string(),
184 Value::String("NSArray".to_string()),
185 );
186 class_obj.insert(
187 "$classes".to_string(),
188 Value::Array(vec![
189 Value::String("NSArray".to_string()),
190 Value::String("NSObject".to_string()),
191 ]),
192 );
193 objects.push(Value::Dictionary(class_obj));
194
195 Value::Uid(Uid::new(arr_idx as u64))
196 }
197 Value::Dictionary(dict) => {
198 let pairs = dict.into_iter().collect::<Vec<_>>();
199 archive_dict_into(&pairs, objects)
200 }
201 other => {
202 let idx = objects.len();
203 objects.push(other);
204 Value::Uid(Uid::new(idx as u64))
205 }
206 }
207}
208
209fn archive_dict_into(pairs: &[(String, Value)], objects: &mut Vec<Value>) -> Value {
210 let dict_idx = objects.len();
211 objects.push(Value::Boolean(false));
213
214 let mut key_uids = Vec::new();
215 let mut val_uids = Vec::new();
216 for (k, v) in pairs {
217 let k_uid = archive_value_into(Value::String(k.clone()), objects);
218 let v_uid = archive_value_into(v.clone(), objects);
219 key_uids.push(k_uid);
220 val_uids.push(v_uid);
221 }
222
223 let class_idx = objects.len();
224 let mut class_obj = Dictionary::new();
225 class_obj.insert(
226 "$classname".to_string(),
227 Value::String("NSDictionary".to_string()),
228 );
229 class_obj.insert(
230 "$classes".to_string(),
231 Value::Array(vec![
232 Value::String("NSDictionary".to_string()),
233 Value::String("NSObject".to_string()),
234 ]),
235 );
236 objects.push(Value::Dictionary(class_obj));
237
238 let mut dict_obj = Dictionary::new();
239 dict_obj.insert("$class".to_string(), Value::Uid(Uid::new(class_idx as u64)));
240 dict_obj.insert("NS.keys".to_string(), Value::Array(key_uids));
241 dict_obj.insert("NS.objects".to_string(), Value::Array(val_uids));
242 objects[dict_idx] = Value::Dictionary(dict_obj);
243
244 Value::Uid(Uid::new(dict_idx as u64))
245}
246
247fn archive_nsuuid_into(uuid: Uuid, objects: &mut Vec<Value>) -> Value {
248 let object_idx = objects.len();
249 let class_idx = object_idx + 1;
250
251 let mut object = Dictionary::new();
252 object.insert("$class".to_string(), Value::Uid(Uid::new(class_idx as u64)));
253 object.insert(
254 "NS.uuidbytes".to_string(),
255 Value::Data(uuid.into_bytes().to_vec()),
256 );
257 objects.push(Value::Dictionary(object));
258
259 objects.push(class_descriptor("NSUUID", &["NSUUID", "NSObject"]));
260 Value::Uid(Uid::new(object_idx as u64))
261}
262
263fn archive_nsurl_into(url: NsUrl, objects: &mut Vec<Value>) -> Value {
264 let object_idx = objects.len();
265 let class_idx = object_idx + 1;
266 let relative_idx = object_idx + 2;
267
268 let mut object = Dictionary::new();
269 object.insert("$class".to_string(), Value::Uid(Uid::new(class_idx as u64)));
270 object.insert("NS.base".to_string(), Value::Uid(Uid::new(0)));
271 object.insert(
272 "NS.relative".to_string(),
273 Value::Uid(Uid::new(relative_idx as u64)),
274 );
275 objects.push(Value::Dictionary(object));
276 objects.push(class_descriptor("NSURL", &["NSURL", "NSObject"]));
277 objects.push(Value::String(format!("file://{}", url.path)));
278
279 Value::Uid(Uid::new(object_idx as u64))
280}
281
282fn archive_xct_capabilities_into(capabilities: XctCapabilities, objects: &mut Vec<Value>) -> Value {
283 let dict_uid = archive_dict_into(&capabilities.capabilities, objects);
284 let object_idx = objects.len();
285 let class_idx = object_idx + 1;
286
287 let mut object = Dictionary::new();
288 object.insert("$class".to_string(), Value::Uid(Uid::new(class_idx as u64)));
289 object.insert("capabilities-dictionary".to_string(), dict_uid);
290 objects.push(Value::Dictionary(object));
291 objects.push(class_descriptor(
292 "XCTCapabilities",
293 &["XCTCapabilities", "NSObject"],
294 ));
295
296 Value::Uid(Uid::new(object_idx as u64))
297}
298
299fn archive_xctest_configuration_into(
300 config: XcTestConfiguration,
301 objects: &mut Vec<Value>,
302) -> Value {
303 let session_uid = archive_nsuuid_into(config.session_identifier, objects);
304 let bundle_uid = archive_nsurl_into(config.test_bundle_url, objects);
305 let caps_uid = archive_xct_capabilities_into(config.ide_capabilities, objects);
306 let automation_uid =
307 archive_value_into(Value::String(config.automation_framework_path), objects);
308
309 let object_idx = objects.len();
310 let class_idx = object_idx + 1;
311
312 let mut object = Dictionary::new();
313 object.insert("$class".to_string(), Value::Uid(Uid::new(class_idx as u64)));
314 object.insert("sessionIdentifier".to_string(), session_uid);
315 object.insert("testBundleURL".to_string(), bundle_uid);
316 object.insert("IDECapabilities".to_string(), caps_uid);
317 object.insert("automationFrameworkPath".to_string(), automation_uid);
318 object.insert(
319 "initializeForUITesting".to_string(),
320 Value::Boolean(config.initialize_for_ui_testing),
321 );
322 object.insert(
323 "reportResultsToIDE".to_string(),
324 Value::Boolean(config.report_results_to_ide),
325 );
326 object.insert(
327 "testsMustRunOnMainThread".to_string(),
328 Value::Boolean(config.tests_must_run_on_main_thread),
329 );
330 object.insert(
331 "testTimeoutsEnabled".to_string(),
332 Value::Boolean(config.test_timeouts_enabled),
333 );
334 for (key, value) in config.additional_fields {
335 object.insert(key, value);
336 }
337 objects.push(Value::Dictionary(object));
338 objects.push(class_descriptor(
339 "XCTestConfiguration",
340 &["XCTestConfiguration", "NSObject"],
341 ));
342
343 Value::Uid(Uid::new(object_idx as u64))
344}
345
346fn class_descriptor(classname: &str, classes: &[&str]) -> Value {
347 let mut class_obj = Dictionary::new();
348 class_obj.insert(
349 "$classname".to_string(),
350 Value::String(classname.to_string()),
351 );
352 class_obj.insert(
353 "$classes".to_string(),
354 Value::Array(
355 classes
356 .iter()
357 .map(|name| Value::String((*name).to_string()))
358 .collect(),
359 ),
360 );
361 Value::Dictionary(class_obj)
362}
363
364fn archive_value(val: Value) -> Vec<u8> {
368 let objects = vec![Value::String("$null".to_string()), val];
369 let root_doc = build_keyed_archive(Value::Uid(Uid::new(1)), objects);
370 to_binary_plist(&root_doc)
371}
372
373fn build_keyed_archive(root_uid: Value, objects: Vec<Value>) -> Value {
374 let mut top = Dictionary::new();
375 top.insert("root".to_string(), root_uid);
376
377 let mut doc = Dictionary::new();
378 doc.insert(
379 "$archiver".to_string(),
380 Value::String("NSKeyedArchiver".to_string()),
381 );
382 doc.insert("$version".to_string(), Value::Integer(100000.into()));
383 doc.insert("$top".to_string(), Value::Dictionary(top));
384 doc.insert("$objects".to_string(), Value::Array(objects));
385 Value::Dictionary(doc)
386}
387
388fn to_binary_plist(val: &Value) -> Vec<u8> {
392 let mut buf = Vec::new();
393 plist::to_writer_binary(&mut buf, val).unwrap();
394 buf
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 fn plist_doc(data: &[u8]) -> Value {
402 plist::from_bytes(data).unwrap()
403 }
404
405 fn objects(data: &[u8]) -> Vec<Value> {
406 let plist = plist_doc(data);
407 plist.as_dictionary().unwrap()["$objects"]
408 .as_array()
409 .unwrap()
410 .clone()
411 }
412
413 fn root_index(data: &[u8]) -> usize {
414 let plist = plist_doc(data);
415 let top = plist.as_dictionary().unwrap()["$top"]
416 .as_dictionary()
417 .unwrap();
418 match &top["root"] {
419 Value::Uid(uid) => uid.get() as usize,
420 other => panic!("unexpected root reference: {other:?}"),
421 }
422 }
423
424 fn root_object<'a>(data: &[u8], objects: &'a [Value]) -> &'a Dictionary {
425 objects[root_index(data)].as_dictionary().unwrap()
426 }
427
428 #[test]
429 fn test_archive_string_is_valid_plist() {
430 let data = archive_string("_requestChannelWithCode:identifier:");
431 assert_eq!(&data[..6], b"bplist");
433 let _val: Value = plist::from_bytes(&data).unwrap();
435 let recovered = crate::proto::nskeyedarchiver::unarchive(&data).unwrap();
437 assert_eq!(
438 recovered.as_str(),
439 Some("_requestChannelWithCode:identifier:")
440 );
441 }
442
443 #[test]
444 fn test_archive_int() {
445 let data = archive_int(42);
446 let recovered = crate::proto::nskeyedarchiver::unarchive(&data).unwrap();
447 assert_eq!(recovered.as_int(), Some(42));
448 }
449
450 #[test]
451 fn test_archive_null_stores_nsnull_class_descriptor() {
452 let data = archive_null();
453 let objects = objects(&data);
454 let root = root_object(&data, &objects);
455 let class_ref = match &root["$class"] {
456 Value::Uid(uid) => uid.get() as usize,
457 _ => panic!("expected uid"),
458 };
459 assert_eq!(
460 objects[class_ref].as_dictionary().unwrap()["$classname"].as_string(),
461 Some("NSNull")
462 );
463 }
464
465 #[test]
466 fn test_archive_null_roundtrips_to_null() {
467 let data = archive_null();
468 let recovered = crate::proto::nskeyedarchiver::unarchive(&data).unwrap();
469 assert!(matches!(
470 recovered,
471 crate::proto::nskeyedarchiver::ArchiveValue::Null
472 ));
473 }
474
475 #[test]
476 fn test_archive_roundtrip_nonempty() {
477 let s = archive_string("com.apple.instruments.server.services.sysmontap");
478 assert!(!s.is_empty());
479 assert!(s.len() > 8);
480 }
481
482 #[test]
483 fn test_archive_array_preserves_item_order() {
484 let data = archive_array(vec![
485 Value::Integer(12.into()),
486 Value::Integer(34.into()),
487 Value::Integer(56.into()),
488 ]);
489 let recovered = crate::proto::nskeyedarchiver::unarchive(&data).unwrap();
490 let values = recovered.as_array().unwrap();
491 assert_eq!(values[0].as_int(), Some(12));
492 assert_eq!(values[1].as_int(), Some(34));
493 assert_eq!(values[2].as_int(), Some(56));
494 }
495
496 #[test]
497 fn test_archive_dict_roundtrips_nested_dictionary_values() {
498 let nested = Dictionary::from_iter([
499 (
500 "inner-key".to_string(),
501 Value::String("inner-value".to_string()),
502 ),
503 ("inner-int".to_string(), Value::Integer(7.into())),
504 ]);
505 let data = archive_dict(vec![(
506 "outer".to_string(),
507 Value::Array(vec![Value::Dictionary(nested)]),
508 )]);
509
510 let recovered = crate::proto::nskeyedarchiver::unarchive(&data).unwrap();
511 let dict = recovered.as_dict().expect("root should be a dictionary");
512 let outer = dict.get("outer").expect("outer key should exist");
513 let outer_items = outer.as_array().expect("outer should be an array");
514 let first = outer_items.first().expect("outer should contain one item");
515 let nested = first
516 .as_dict()
517 .expect("nested dictionary should survive archiving");
518
519 assert_eq!(
520 nested.get("inner-key").and_then(|value| value.as_str()),
521 Some("inner-value")
522 );
523 assert_eq!(
524 nested.get("inner-int").and_then(|value| value.as_int()),
525 Some(7)
526 );
527 }
528
529 #[test]
530 fn test_archive_uuid_stores_nsuuid_class_and_bytes() {
531 let uuid = Uuid::parse_str("00112233-4455-6677-8899-aabbccddeeff").unwrap();
532 let data = archive_uuid(uuid);
533 let objects = objects(&data);
534 let root = root_object(&data, &objects);
535 assert_eq!(
536 root["NS.uuidbytes"].as_data().unwrap(),
537 &uuid.into_bytes().to_vec()
538 );
539 let class_ref = match &root["$class"] {
540 Value::Uid(uid) => uid.get() as usize,
541 _ => panic!("expected uid"),
542 };
543 let class = objects[class_ref].as_dictionary().unwrap();
544 assert_eq!(class["$classname"].as_string(), Some("NSUUID"));
545 }
546
547 #[test]
548 fn test_archive_nsurl_stores_file_relative_path() {
549 let data = archive_nsurl(NsUrl {
550 path: "/private/tmp/TestBundle.xctest".to_string(),
551 });
552 let objects = objects(&data);
553 let root = root_object(&data, &objects);
554 let rel_ref = match &root["NS.relative"] {
555 Value::Uid(uid) => uid.get() as usize,
556 _ => panic!("expected uid"),
557 };
558 assert_eq!(
559 objects[rel_ref].as_string(),
560 Some("file:///private/tmp/TestBundle.xctest")
561 );
562 }
563
564 #[test]
565 fn test_archive_xct_capabilities_stores_capabilities_dictionary() {
566 let data = archive_xct_capabilities(XctCapabilities {
567 capabilities: vec![(
568 "expected failure test capability".to_string(),
569 Value::Boolean(true),
570 )],
571 });
572 let objects = objects(&data);
573 let root = root_object(&data, &objects);
574 let class_ref = match &root["$class"] {
575 Value::Uid(uid) => uid.get() as usize,
576 _ => panic!("expected uid"),
577 };
578 assert_eq!(
579 objects[class_ref].as_dictionary().unwrap()["$classname"].as_string(),
580 Some("XCTCapabilities")
581 );
582 let dict_ref = match &root["capabilities-dictionary"] {
583 Value::Uid(uid) => uid.get() as usize,
584 _ => panic!("expected uid"),
585 };
586 let dict = objects[dict_ref].as_dictionary().unwrap();
587 assert!(dict.contains_key("NS.keys"));
588 assert!(dict.contains_key("NS.objects"));
589 }
590
591 #[test]
592 fn test_archive_xctest_configuration_stores_nested_testmanager_objects() {
593 let data = archive_xctest_configuration(XcTestConfiguration {
594 session_identifier: Uuid::parse_str("00112233-4455-6677-8899-aabbccddeeff").unwrap(),
595 test_bundle_url: NsUrl {
596 path: "/private/tmp/WebDriverAgentRunner.xctest".to_string(),
597 },
598 ide_capabilities: XctCapabilities {
599 capabilities: vec![("XCTIssue capability".to_string(), Value::Boolean(true))],
600 },
601 automation_framework_path:
602 "/System/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework"
603 .to_string(),
604 initialize_for_ui_testing: true,
605 report_results_to_ide: true,
606 tests_must_run_on_main_thread: true,
607 test_timeouts_enabled: false,
608 additional_fields: Vec::new(),
609 });
610
611 let objects = objects(&data);
612 let root = root_object(&data, &objects);
613 let class_ref = match &root["$class"] {
614 Value::Uid(uid) => uid.get() as usize,
615 _ => panic!("expected uid"),
616 };
617 assert_eq!(
618 objects[class_ref].as_dictionary().unwrap()["$classname"].as_string(),
619 Some("XCTestConfiguration")
620 );
621 assert!(matches!(root.get("sessionIdentifier"), Some(Value::Uid(_))));
622 assert!(matches!(root.get("testBundleURL"), Some(Value::Uid(_))));
623 assert!(matches!(root.get("IDECapabilities"), Some(Value::Uid(_))));
624 assert_eq!(root["reportResultsToIDE"].as_boolean(), Some(true));
625 assert_eq!(root["testsMustRunOnMainThread"].as_boolean(), Some(true));
626 }
627}