diff --git a/src/planner/macos/profile.sample.empty-with-leading-text.plist b/src/planner/macos/profile.sample.empty-with-leading-text.plist new file mode 100644 index 0000000..1d8a31d --- /dev/null +++ b/src/planner/macos/profile.sample.empty-with-leading-text.plist @@ -0,0 +1,6 @@ +There are no configuration profiles installed + + + + + diff --git a/src/planner/macos/profile.sample.empty-with-trailing-text.plist b/src/planner/macos/profile.sample.empty-with-trailing-text.plist new file mode 100644 index 0000000..cc43ba5 --- /dev/null +++ b/src/planner/macos/profile.sample.empty-with-trailing-text.plist @@ -0,0 +1,6 @@ + + + + + +There are no configuration profiles installed diff --git a/src/planner/macos/profiles.rs b/src/planner/macos/profiles.rs index 5d4f702..94dcad7 100644 --- a/src/planner/macos/profiles.rs +++ b/src/planner/macos/profiles.rs @@ -11,6 +11,28 @@ pub enum LoadError { ProfileListing(#[from] crate::ActionErrorKind), } +/// Extract just the XML plist content from the output. +/// +/// `/usr/bin/profiles` may emit non-XML text before or after the plist +/// (e.g. "There are no configuration profiles installed"). The position +/// varies across macOS versions. We extract the `` range +/// to avoid plist parse failures from surrounding plain text. +fn extract_plist(buf: &[u8]) -> &[u8] { + const START_TAG: &[u8] = b""; + + let start = buf.windows(START_TAG.len()).position(|w| w == START_TAG); + let end = buf + .windows(END_TAG.len()) + .rposition(|w| w == END_TAG) + .map(|pos| pos + END_TAG.len()); + + match (start, end) { + (Some(s), Some(e)) if s < e => &buf[s..e], + _ => buf, + } +} + pub fn load() -> Result { let buf = execute_command( std::process::Command::new("/usr/bin/profiles") @@ -23,7 +45,12 @@ pub fn load() -> Result { )? .stdout; - Ok(plist::from_reader(std::io::Cursor::new(buf))?) + parse(&buf) +} + +pub fn parse(buf: &[u8]) -> Result { + let xml = extract_plist(buf); + Ok(plist::from_reader(std::io::Cursor::new(xml))?) } pub type Policies = HashMap>; @@ -125,6 +152,34 @@ mod tests { ); } + /// Regression test: `/usr/bin/profiles -P -o stdout-xml` emits + /// "There are no configuration profiles installed" as plain text + /// surrounding the plist XML when no profiles exist. The position + /// varies across macOS versions (before or after the XML). This + /// caused a `Parse(UnexpectedXmlCharactersExpectedElement)` error + /// that made `check_suis()` skip the SystemUIServer policy check. + #[test] + fn parse_empty_profiles_with_trailing_text() { + let raw = include_bytes!("./profile.sample.empty-with-trailing-text.plist"); + assert!( + plist::from_reader::<_, Policies>(std::io::Cursor::new(raw)).is_err(), + "raw input should fail to parse without extraction" + ); + let parsed = parse(raw).unwrap(); + assert!(parsed.is_empty()); + } + + #[test] + fn parse_empty_profiles_with_leading_text() { + let raw = include_bytes!("./profile.sample.empty-with-leading-text.plist"); + assert!( + plist::from_reader::<_, Policies>(std::io::Cursor::new(raw)).is_err(), + "raw input should fail to parse without extraction" + ); + let parsed = parse(raw).unwrap(); + assert!(parsed.is_empty()); + } + #[test] fn try_parse_unknown() { let parsed: Policies = plist::from_reader(std::io::Cursor::new(include_str!(