ios_core/lockdown/
pair_record.rs1use std::path::PathBuf;
2
3use serde::Deserialize;
4
5#[derive(Debug, thiserror::Error)]
6pub enum PairRecordError {
7 #[error("pair record not found for UDID: {0}")]
8 NotFound(String),
9 #[error("failed to read pair record {path}: {source}")]
10 Read {
11 path: PathBuf,
12 source: std::io::Error,
13 },
14 #[error("failed to parse pair record: {0}")]
15 Parse(String),
16}
17
18#[derive(Debug, Deserialize)]
20#[serde(rename_all = "PascalCase")]
21pub struct PairRecord {
22 #[serde(with = "serde_bytes")]
24 pub device_certificate: Vec<u8>,
25 #[serde(with = "serde_bytes")]
27 pub host_certificate: Vec<u8>,
28 #[serde(with = "serde_bytes")]
30 pub host_private_key: Vec<u8>,
31 #[serde(with = "serde_bytes")]
33 pub root_certificate: Vec<u8>,
34 #[serde(rename = "HostID")]
36 pub host_id: String,
37 #[serde(rename = "SystemBUID")]
39 pub system_buid: String,
40 pub wifi_mac_address: Option<String>,
42}
43
44impl PairRecord {
45 pub fn load(udid: &str) -> Result<Self, PairRecordError> {
47 let path = default_pair_record_path(udid);
48 Self::load_from_path(&path, udid)
49 }
50
51 pub fn load_from_path(path: &std::path::Path, udid: &str) -> Result<Self, PairRecordError> {
53 let data = match std::fs::read(path) {
54 Ok(data) => data,
55 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
56 return Err(PairRecordError::NotFound(udid.to_string()));
57 }
58 Err(source) => {
59 return Err(PairRecordError::Read {
60 path: path.to_path_buf(),
61 source,
62 });
63 }
64 };
65 plist::from_bytes(&data).map_err(|e| PairRecordError::Parse(e.to_string()))
66 }
67}
68
69pub fn default_pair_record_path(udid: &str) -> PathBuf {
70 default_pair_record_dir().join(format!("{udid}.plist"))
71}
72
73pub fn default_pair_record_dir() -> PathBuf {
74 pair_record_dir_for_platform(
75 cfg!(target_os = "macos"),
76 cfg!(windows),
77 &std::env::var("ALLUSERSPROFILE").unwrap_or_default(),
78 )
79}
80
81#[cfg(test)]
82pub(crate) fn pair_record_path_for_platform(
83 udid: &str,
84 is_macos: bool,
85 is_windows: bool,
86 all_users_profile: &str,
87) -> PathBuf {
88 pair_record_dir_for_platform(is_macos, is_windows, all_users_profile)
89 .join(format!("{udid}.plist"))
90}
91
92fn pair_record_dir_for_platform(
93 is_macos: bool,
94 is_windows: bool,
95 all_users_profile: &str,
96) -> PathBuf {
97 if is_windows {
98 PathBuf::from(all_users_profile)
99 .join("Apple")
100 .join("Lockdown")
101 } else if is_macos {
102 PathBuf::from("/var/db/lockdown")
103 } else {
104 PathBuf::from("/var/lib/lockdown")
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_pair_record_path_macos() {
114 let path = pair_record_path_for_platform("ABC123DEF", true, false, "");
115 assert_eq!(path, PathBuf::from("/var/db/lockdown/ABC123DEF.plist"));
116 }
117
118 #[test]
119 fn test_pair_record_path_windows() {
120 let path = pair_record_path_for_platform("ABC123DEF", false, true, "C:\\ProgramData");
121 let s = path.to_string_lossy();
122 assert!(s.contains("ABC123DEF"));
123 assert!(s.contains("Apple"));
124 assert!(s.contains("Lockdown"));
125 }
126
127 #[test]
128 fn test_pair_record_path_linux() {
129 let path = pair_record_path_for_platform("ABC123DEF", false, false, "");
130 assert_eq!(path, PathBuf::from("/var/lib/lockdown/ABC123DEF.plist"));
131 }
132
133 #[test]
134 fn test_pair_record_dir_windows() {
135 let path = pair_record_dir_for_platform(false, true, "C:\\ProgramData");
136 assert!(path.starts_with("C:\\ProgramData"));
137 assert!(path.ends_with(PathBuf::from("Apple").join("Lockdown")));
138 }
139
140 #[test]
141 fn load_from_path_preserves_non_missing_read_errors() {
142 let dir =
143 std::env::temp_dir().join(format!("ios-rs-pair-record-dir-{}", std::process::id()));
144 let _ = std::fs::remove_dir_all(&dir);
145 std::fs::create_dir_all(&dir).unwrap();
146
147 let err = PairRecord::load_from_path(&dir, "UDID").unwrap_err();
148
149 assert!(matches!(err, PairRecordError::Read { path, .. } if path == dir));
150 let _ = std::fs::remove_dir_all(&dir);
151 }
152}