apple_clis/codesign/display/output/
signed_keys.rs

1use crate::prelude::*;
2use time::macros::format_description;
3
4/// This will not parse some multi-key value things
5/// e.g. "Sealed Resources version=2 rules=10 files=0"
6/// becomes => "Sealed Resources version": "2 rules=10 files=0"
7#[instrument(level = "trace")]
8fn parse_display_output(input: &str) -> IResult<&str, HashMap<Cow<str>, &str>> {
9	let parse_key_value = pair(
10		terminated(take_till1(|c| c == '='), tag("=")),
11		terminated(take_till1(|c| c == '\n'), multispace0),
12	);
13	let (_, result) = all_consuming(fold_many1(
14		parse_key_value,
15		HashMap::<Cow<str>, &str>::new,
16		|mut acc: HashMap<_, _>, (key, value)| {
17			let key = if key == "Authority" {
18				let mut num = 1;
19				let new_key: String = loop {
20					let new_key = format!("Authority_{}", num);
21					if !acc.contains_key(&Cow::<str>::Owned(new_key.clone())) {
22						break new_key.clone();
23					} else {
24						num += 1;
25					}
26				};
27				Cow::Owned(new_key)
28			} else {
29				Cow::Borrowed(key)
30			};
31			acc.insert(key, value);
32			acc
33		},
34	))(input)?;
35
36	Ok(("", result))
37}
38
39#[derive(Debug, Serialize)]
40pub struct SignedKeys {
41	authority_1: String,
42	executable: Utf8PathBuf,
43	identifier: String,
44	signed_time: time::PrimitiveDateTime,
45
46	/// Includes the parsed keys above as well
47	raw: HashMap<String, String>,
48}
49
50impl FromStr for SignedKeys {
51	type Err = error::Error;
52
53	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
54		trace!(?s, "Parsing SignedKeys from string");
55		match parse_display_output(s) {
56			Ok((_, result)) => Self::from_parsed(result),
57			Err(err) => {
58				trace!(?err, "Failed to parse SignedKeys from string");
59				Err(Error::NomParsingFailed {
60					name: "SignedKeys".to_owned(),
61					err: err.to_owned(),
62				})
63			}
64		}
65	}
66}
67
68impl SignedKeys {
69	pub fn authority_1(&self) -> &str {
70		&self.authority_1
71	}
72
73	pub fn executable(&self) -> &Utf8PathBuf {
74		&self.executable
75	}
76
77	pub fn identifier(&self) -> &str {
78		&self.identifier
79	}
80
81	pub fn signed_time(&self) -> &time::PrimitiveDateTime {
82		&self.signed_time
83	}
84
85	/// Even include the already parsed keys like [Self::identifier]
86	pub fn raw(&self) -> HashMap<&str, &str> {
87		HashMap::from_iter(self.raw.iter().map(|(k, v)| (k.as_str(), v.as_str())))
88	}
89
90	#[instrument(level = "trace", skip(input))]
91	pub(crate) fn from_raw(input: &str) -> error::Result<Self> {
92		input.parse()
93	}
94
95	const DATE_FORMAT: &'static [time::format_description::FormatItem<'static>] =
96		format_description!(version = 2, "[day] [month repr:short] [year] at [hour padding:none]:[minute]:[second] [period case_sensitive:false]");
97
98	#[instrument(level = "trace", skip(raw), ret)]
99	fn from_parsed(raw: HashMap<Cow<str>, &str>) -> error::Result<Self> {
100		debug!(?raw, "Extracting SignedKeys from parsed input");
101		Ok(SignedKeys {
102			authority_1: raw
103				.get("Authority_1")
104				.ok_or_else(|| error::Error::SigningPropertyNotFound {
105					missing_key: "Authority".into(),
106				})?
107				.to_string(),
108			executable: raw
109				.get("Executable")
110				.ok_or_else(|| error::Error::SigningPropertyNotFound {
111					missing_key: "Executable".into(),
112				})?
113				.into(),
114			identifier: raw
115				.get("Identifier")
116				.ok_or_else(|| error::Error::SigningPropertyNotFound {
117					missing_key: "Identifier".into(),
118				})?
119				.to_string(),
120			signed_time: time::PrimitiveDateTime::parse(
121				&raw
122					.get("Signed Time")
123					.ok_or_else(|| error::Error::SigningPropertyNotFound {
124						missing_key: "Signed Time".into(),
125					})?
126					.to_string()
127					.replace(' ', " "), // replace stupid space with good space to [time::PrimitiveDateTime::parse] works
128				&Self::DATE_FORMAT,
129			)?,
130			raw: raw
131				.into_iter()
132				.map(|(k, v)| (k.into_owned(), v.to_string()))
133				.collect(),
134		})
135	}
136}
137
138#[cfg(test)]
139mod test {
140	use super::*;
141
142	#[test]
143	fn test_parse_raw_display_output() {
144		let test_input = include_str!(concat!(
145			env!("CARGO_MANIFEST_DIR"),
146			"/tests/codesign-display.txt"
147		));
148		match parse_display_output(test_input) {
149			Ok((_, result)) => {
150				println!("Parsed: {:#?}", result);
151			}
152			Err(err) => {
153				panic!("Failed to parse: {:?}", err);
154			}
155		}
156	}
157
158	#[test]
159	fn test_basic_times_parse() {
160		let fmt =
161			format_description!("[hour padding:none]:[minute]:[second] [period case_sensitive:false]");
162		let examples = ["2:41:28 pm"];
163		for example in examples {
164			match time::Time::parse(example, &fmt) {
165				Ok(result) => {
166					println!("Parsed: {:#?}", result);
167				}
168				Err(err) => {
169					panic!("Failed to parse {}: {:?}", example, err);
170				}
171			}
172		}
173
174		let fmt = format_description!(
175			" at [hour padding:none]:[minute]:[second] [period case_sensitive:false]"
176		);
177		let examples = [" at 2:41:28 pm"];
178		for example in examples {
179			match time::Time::parse(example, &fmt) {
180				Ok(result) => {
181					println!("Parsed: {:#?}", result);
182				}
183				Err(err) => {
184					panic!("Failed to parse {}: {:?}", example, err);
185				}
186			}
187		}
188	}
189
190	#[test]
191	fn test_basic_dates_parse() {
192		let fmt = format_description!(version = 2, "[day] [month repr:short] [year] at [hour padding:none]:[minute]:[second] [period case_sensitive:false]");
193		let examples = ["16 Mar 2024 at 2:41:28 pm"];
194		for example in examples {
195			match time::PrimitiveDateTime::parse(example, &fmt) {
196				Ok(result) => {
197					println!("Parsed: {:#?}", result);
198				}
199				Err(err) => {
200					panic!("Failed to parse {}: {:?}", example, err);
201				}
202			}
203		}
204	}
205
206	#[test]
207	fn test_date_parses() {
208		println!(
209			"this took me an hour to debug: char: {} and {}",
210			' ' as u32, ' ' as u32
211		);
212		let fmt = SignedKeys::DATE_FORMAT;
213		let examples = ["16 Mar 2024 at 2:41:28 pm"];
214		for example in examples {
215			match time::PrimitiveDateTime::parse(example, &fmt) {
216				Ok(result) => {
217					println!("Parsed: {:#?}", result);
218				}
219				Err(err) => {
220					panic!("Failed to parse {}: {:?}", example, err);
221				}
222			}
223		}
224	}
225
226	#[test]
227	fn test_parse_extracted_display_output() {
228		let test_input = include_str!(concat!(
229			env!("CARGO_MANIFEST_DIR"),
230			"/tests/codesign-display.txt"
231		));
232		match SignedKeys::from_str(test_input) {
233			Ok(result) => {
234				println!("Parsed: {:#?}", result);
235			}
236			Err(err) => {
237				panic!("Failed to parse: {:?}", err);
238			}
239		}
240	}
241}