apple_clis/codesign/display/output/
signed_keys.rs1use crate::prelude::*;
2use time::macros::format_description;
3
4#[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 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 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(' ', " "), &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}