use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::string::FromUtf8Error;
use thiserror::Error;
#[cfg(test)]
use indoc::indoc;
pub type Result<T> = std::result::Result<T, PlistError>;
#[derive(Debug, Error)]
pub enum PlistError {
#[error("unsupported plist command: {cmd}", cmd = .0.to_string_lossy())]
UnsupportedCommand(OsString),
#[error("incorrect command arguments: {args}", args = .0.to_string_lossy())]
IncorrectArguments(OsString),
#[error("invalid UTF-8 sequence: {}", .0.utf8_error())]
Utf8(#[from] FromUtf8Error),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PlistEntry {
File(OsString),
Cwd(OsString),
Exec(OsString),
UnExec(OsString),
Mode(Option<String>),
PkgOpt(PlistOption),
Owner(Option<String>),
Group(Option<String>),
Comment(Option<OsString>),
Ignore,
Name(String),
PkgDir(OsString),
DirRm(OsString),
Display(OsString),
PkgDep(String),
BldDep(String),
PkgCfl(String),
FileChecksum(String),
SymlinkTarget(OsString),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PlistOption {
Preserve,
}
macro_rules! plist_args_str {
($s:ident, $p:path, $l:ident) => {
match $s {
Some(s) => Ok($p(String::from_utf8(s.as_bytes().to_vec())?)),
None => Err(PlistError::IncorrectArguments(OsString::from($l))),
}
};
}
macro_rules! plist_args_osstr {
($s:ident, $p:path, $l:ident) => {
match $s {
Some(dir) => Ok($p(OsString::from(dir))),
None => Err(PlistError::IncorrectArguments(OsString::from($l))),
}
};
}
macro_rules! plist_args_str_opt {
($s:ident, $p:path) => {
match $s {
Some(s) => Ok($p(Some(String::from_utf8(s.as_bytes().to_vec())?))),
None => Ok($p(None)),
}
};
}
macro_rules! plist_args_osstr_opt {
($s:ident, $p:path) => {
match $s {
Some(s) => Ok($p(Some(OsString::from(s)))),
None => Ok($p(None)),
}
};
}
impl PlistEntry {
pub fn from_bytes(bytes: &[u8]) -> Result<PlistEntry> {
let line = OsStr::from_bytes(bytes);
let end = bytes.len();
let bytes = &bytes[0..end];
let (mut idx, cmd) = match bytes.iter().position(|&c| c == b' ') {
Some(i) => (i, String::from_utf8_lossy(&bytes[0..i]).into_owned()),
None => (0, String::from_utf8_lossy(bytes).into_owned()),
};
let args = if idx == 0 || idx + 1 >= end {
None
} else {
for c in &bytes[idx..end] {
if (*c as char).is_whitespace() {
idx += 1;
continue;
}
break;
}
if idx == end {
None
} else {
Some(OsStr::from_bytes(&bytes[idx..end]))
}
};
if cmd.starts_with('@') {
match cmd.as_str() {
"@cwd" | "@src" | "@cd" => {
plist_args_osstr!(args, PlistEntry::Cwd, line)
}
"@exec" => plist_args_osstr!(args, PlistEntry::Exec, line),
"@unexec" => plist_args_osstr!(args, PlistEntry::UnExec, line),
"@option" => match args.and_then(OsStr::to_str) {
Some("preserve") => {
Ok(PlistEntry::PkgOpt(PlistOption::Preserve))
}
Some(_) => {
Err(PlistError::UnsupportedCommand(OsString::from(cmd)))
}
None => Err(PlistError::IncorrectArguments(
OsString::from(line),
)),
},
"@mode" => plist_args_str_opt!(args, PlistEntry::Mode),
"@owner" => plist_args_str_opt!(args, PlistEntry::Owner),
"@group" => plist_args_str_opt!(args, PlistEntry::Group),
"@comment" => {
match args.and_then(OsStr::to_str) {
Some(s) if s.starts_with("MD5:") => {
let hash = &s[4..];
if hash.len() == 32
&& hash.chars().all(|c| c.is_ascii_hexdigit())
{
Ok(PlistEntry::FileChecksum(hash.to_string()))
} else {
plist_args_osstr_opt!(args, PlistEntry::Comment)
}
}
Some(s) if s.starts_with("Symlink:") => {
let target = &s[8..];
Ok(PlistEntry::SymlinkTarget(OsString::from(
target,
)))
}
_ => plist_args_osstr_opt!(args, PlistEntry::Comment),
}
}
"@ignore" => match args {
Some(_) => Err(PlistError::IncorrectArguments(
OsString::from(line),
)),
None => Ok(PlistEntry::Ignore),
},
"@name" => plist_args_str!(args, PlistEntry::Name, line),
"@pkgdep" => plist_args_str!(args, PlistEntry::PkgDep, line),
"@blddep" => plist_args_str!(args, PlistEntry::BldDep, line),
"@pkgcfl" => plist_args_str!(args, PlistEntry::PkgCfl, line),
"@pkgdir" => plist_args_osstr!(args, PlistEntry::PkgDir, line),
"@dirrm" => plist_args_osstr!(args, PlistEntry::DirRm, line),
"@display" => {
plist_args_osstr!(args, PlistEntry::Display, line)
}
_ => Err(PlistError::UnsupportedCommand(OsString::from(cmd))),
}
} else {
Ok(PlistEntry::File(OsString::from(OsStr::from_bytes(bytes))))
}
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileInfo {
pub path: OsString,
pub checksum: Option<String>,
pub symlink_target: Option<OsString>,
pub mode: Option<String>,
pub owner: Option<String>,
pub group: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Plist {
entries: Vec<PlistEntry>,
}
macro_rules! plist_match_filter_str {
($s:ident, $p:path) => {
$s.entries.iter().filter_map(|entry| match entry {
$p(s) => Some(s.as_str()),
_ => None,
})
};
}
macro_rules! plist_match_filter_osstr {
($s:ident, $p:path) => {
$s.entries.iter().filter_map(|entry| match entry {
$p(s) => Some(s.as_os_str()),
_ => None,
})
};
}
macro_rules! plist_find_first_str {
($s:ident, $p:path) => {
$s.entries.iter().find_map(|entry| match entry {
$p(s) => Some(s.as_str()),
_ => None,
})
};
}
macro_rules! plist_find_first_osstr {
($s:ident, $p:path) => {
$s.entries.iter().find_map(|entry| match entry {
$p(s) => Some(s.as_os_str()),
_ => None,
})
};
}
impl Plist {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let mut plist = Self::new();
let mut lines: Vec<(usize, usize)> = Vec::new();
let mut start = 0;
let mut tstart = 0;
let mut trim = true;
let mut end = 0;
for (idx, ch) in bytes.iter().enumerate() {
if *ch == b'\n' {
if start < idx && tstart + 1 < idx {
lines.push((start, idx));
}
start = idx + 1;
end = start;
tstart = start;
trim = true;
} else if trim && (*ch as char).is_whitespace() {
tstart += 1;
} else {
trim = false;
}
}
if end < bytes.len() && tstart < bytes.len() {
lines.push((start, bytes.len()));
}
for (start, end) in lines {
plist
.entries
.push(PlistEntry::from_bytes(&bytes[start..end])?);
}
Ok(plist)
}
#[must_use]
pub fn pkgname(&self) -> Option<&str> {
plist_find_first_str!(self, PlistEntry::Name)
}
#[must_use]
pub fn display(&self) -> Option<&OsStr> {
plist_find_first_osstr!(self, PlistEntry::Display)
}
pub fn depends(&self) -> impl Iterator<Item = &str> + '_ {
plist_match_filter_str!(self, PlistEntry::PkgDep)
}
pub fn build_depends(&self) -> impl Iterator<Item = &str> + '_ {
plist_match_filter_str!(self, PlistEntry::BldDep)
}
pub fn conflicts(&self) -> impl Iterator<Item = &str> + '_ {
plist_match_filter_str!(self, PlistEntry::PkgCfl)
}
pub fn pkgdirs(&self) -> impl Iterator<Item = &OsStr> + '_ {
plist_match_filter_osstr!(self, PlistEntry::PkgDir)
}
pub fn pkgrmdirs(&self) -> impl Iterator<Item = &OsStr> + '_ {
plist_match_filter_osstr!(self, PlistEntry::DirRm)
}
#[must_use]
pub fn files(&self) -> Vec<&OsStr> {
let mut ignore = false;
self.entries
.iter()
.filter_map(|entry| match entry {
PlistEntry::Ignore => {
ignore = true;
None
}
PlistEntry::File(file) => {
if ignore {
ignore = false;
None
} else {
Some(file.as_os_str())
}
}
_ => None,
})
.collect()
}
#[must_use]
pub fn files_prefixed(&self) -> Vec<OsString> {
let mut ignore = false;
let mut prefix: Option<OsString> = None;
self.entries
.iter()
.filter_map(|entry| match entry {
PlistEntry::Cwd(dir) => {
prefix = Some(dir.to_os_string());
None
}
PlistEntry::Ignore => {
ignore = true;
None
}
PlistEntry::File(file) => {
if ignore {
ignore = false;
None
} else {
let mut path = OsString::new();
if let Some(pfx) = &prefix {
path.push(pfx);
}
if !path.to_string_lossy().ends_with('/') {
path.push("/");
}
path.push(file);
Some(path)
}
}
_ => None,
})
.collect()
}
#[must_use]
pub fn files_with_info(&self) -> Vec<FileInfo> {
let mut result = Vec::new();
let mut ignore = false;
let mut current_mode: Option<String> = None;
let mut current_owner: Option<String> = None;
let mut current_group: Option<String> = None;
let mut i = 0;
while i < self.entries.len() {
match &self.entries[i] {
PlistEntry::Mode(m) => current_mode = m.clone(),
PlistEntry::Owner(o) => current_owner = o.clone(),
PlistEntry::Group(g) => current_group = g.clone(),
PlistEntry::Ignore => ignore = true,
PlistEntry::File(path) => {
if ignore {
ignore = false;
} else {
let mut info = FileInfo {
path: path.clone(),
checksum: None,
symlink_target: None,
mode: current_mode.clone(),
owner: current_owner.clone(),
group: current_group.clone(),
};
let mut j = i + 1;
while j < self.entries.len() {
match &self.entries[j] {
PlistEntry::FileChecksum(hash) => {
info.checksum = Some(hash.clone());
j += 1;
}
PlistEntry::SymlinkTarget(target) => {
info.symlink_target = Some(target.clone());
j += 1;
}
_ => break,
}
}
result.push(info);
}
}
_ => {}
}
i += 1;
}
result
}
#[must_use]
pub fn install_cmds(&self) -> Vec<&PlistEntry> {
let mut ignore = false;
self.entries
.iter()
.filter(|entry| match entry {
PlistEntry::Ignore => {
ignore = true;
false
}
PlistEntry::File(_) => !std::mem::take(&mut ignore),
PlistEntry::Cwd(_)
| PlistEntry::Exec(_)
| PlistEntry::Mode(_)
| PlistEntry::Owner(_)
| PlistEntry::Group(_)
| PlistEntry::PkgDir(_) => true,
_ => false,
})
.collect()
}
#[must_use]
pub fn uninstall_cmds(&self) -> Vec<&PlistEntry> {
let mut ignore = false;
self.entries
.iter()
.filter(|entry| match entry {
PlistEntry::Ignore => {
ignore = true;
false
}
PlistEntry::File(_) => !std::mem::take(&mut ignore),
PlistEntry::Cwd(_)
| PlistEntry::UnExec(_)
| PlistEntry::Mode(_)
| PlistEntry::Owner(_)
| PlistEntry::Group(_)
| PlistEntry::PkgDir(_)
| PlistEntry::DirRm(_) => true,
_ => false,
})
.collect()
}
#[must_use]
pub fn is_preserve(&self) -> bool {
self.entries
.iter()
.filter(|entry| {
matches!(entry, PlistEntry::PkgOpt(PlistOption::Preserve))
})
.count()
> 0
}
}
impl IntoIterator for Plist {
type Item = PlistEntry;
type IntoIter = std::vec::IntoIter<PlistEntry>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl<'a> IntoIterator for &'a Plist {
type Item = &'a PlistEntry;
type IntoIter = std::slice::Iter<'a, PlistEntry>;
fn into_iter(self) -> Self::IntoIter {
self.entries.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! plist {
($s:expr) => {
Plist::from_bytes(String::from($s).as_bytes())
};
}
macro_rules! plist_entry {
($s:expr) => {
PlistEntry::from_bytes(String::from($s).as_bytes())
};
}
macro_rules! plist_match_ok {
($s:expr, $p:path) => {
let plist = plist_entry!($s)?;
assert_eq!(plist, $p);
};
}
macro_rules! plist_match_ok_arg {
($s:expr, $p:path) => {
match plist_entry!($s) {
Ok(e) => match e {
$p(_) => {}
_ => panic!("should be a valid {} entry", stringify!($p)),
},
Err(_) => panic!("should be a valid {} entry", stringify!($p)),
}
};
}
macro_rules! plist_match_error {
($s:expr, $p:path) => {
match plist!($s) {
Ok(_) => panic!("should return {} error", stringify!($p)),
Err(e) => match e {
$p(_) => {}
_ => panic!("should return {} error", stringify!($p)),
},
}
};
}
macro_rules! valid_utf8 {
($s:expr, $p:path) => {
let heart = vec![240, 159, 146, 150];
let oe = vec![0xf8];
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&heart);
assert_eq!(PlistEntry::from_bytes(&t)?, $p(String::from("💖")));
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&oe);
match PlistEntry::from_bytes(&t) {
Ok(p) => panic!(
"should be an invalid {} entry, not {:?}",
stringify!($p),
p
),
Err(e) => match e {
PlistError::Utf8(_) => {}
_ => panic!(
"should be an invalid {} entry: {}",
stringify!($p),
e
),
},
}
};
}
macro_rules! valid_utf8_opt {
($s:expr, $p:path) => {
let heart = vec![240, 159, 146, 150];
let oe = vec![0xf8];
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&heart);
assert_eq!(
PlistEntry::from_bytes(&t)?,
$p(Some(String::from("💖")))
);
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&oe);
match PlistEntry::from_bytes(&t) {
Ok(p) => panic!(
"should be an invalid {} entry, not {:?}",
stringify!($p),
p
),
Err(e) => match e {
PlistError::Utf8(_) => {}
_ => panic!(
"should be an invalid {} entry: {}",
stringify!($p),
e
),
},
}
};
}
macro_rules! valid_8859 {
($s:expr, $p:path) => {
let heart = vec![240, 159, 146, 150];
let oe = vec![0xf8];
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&heart);
assert_eq!(PlistEntry::from_bytes(&t)?, $p(OsString::from("💖")));
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&oe);
match PlistEntry::from_bytes(&t) {
Ok(e) => match e {
$p(_) => {}
_ => panic!("should be a valid {} entry", stringify!($p)),
},
Err(_) => panic!("should be a valid {} entry", stringify!($p)),
}
};
}
macro_rules! valid_8859_opt {
($s:expr, $p:path) => {
let heart = vec![240, 159, 146, 150];
let oe = vec![0xf8];
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&heart);
assert_eq!(
PlistEntry::from_bytes(&t)?,
$p(Some(OsString::from("💖")))
);
let mut t = String::from($s).into_bytes();
t.extend_from_slice(&oe);
match PlistEntry::from_bytes(&t) {
Ok(e) => match e {
$p(_) => {}
_ => panic!("should be a valid {} entry", stringify!($p)),
},
Err(_) => panic!("should be a valid {} entry", stringify!($p)),
}
};
}
#[test]
fn test_full_plist() -> Result<()> {
let input = indoc! {"
@comment $NetBSD$
@name pkgtest-1.0
@pkgdep dep-pkg1-[0-9]*
@pkgdep dep-pkg2>=2.0
@blddep dep-pkg1-1.0nb2
@blddep dep-pkg2-2.1
@pkgcfl cfl-pkg1-[0-9]*
@pkgcfl cfl-pkg2>=2.0
@display MESSAGE
@option preserve
@cwd /
@src /
@cd /
@mode 0644
@owner root
@group wheel
bin/foo
@exec touch F=%F D=%D B=%B f=%f
@unexec rm F=%F D=%D B=%B f=%f
@mode
@owner
@group
bin/bar
@pkgdir /var/db/pkgsrc-rs
@dirrm /var/db/pkgsrc-rs-legacy
@ignore
+BUILD_INFO
"};
let plist = Plist::from_bytes(input.as_bytes())?;
assert_eq!(plist.depends().count(), 2);
assert_eq!(plist.build_depends().count(), 2);
assert_eq!(plist.conflicts().count(), 2);
Ok(())
}
#[test]
fn test_line_input() -> Result<()> {
assert_eq!(plist_entry!("@comment \n")?, PlistEntry::Comment(None));
assert_eq!(plist_entry!("@mode ")?, PlistEntry::Mode(None));
assert_eq!(plist_entry!("@owner \t ")?, PlistEntry::Owner(None));
assert_eq!(plist_entry!("@group \t\n ")?, PlistEntry::Group(None));
let p1 = plist_entry!("@comment hi")?;
let p2 = PlistEntry::Comment(Some(OsString::from("hi")));
assert_eq!(p1, p2);
let p1 = plist_entry!(" @comment ")?;
let p2 = PlistEntry::File(OsString::from(" @comment "));
assert_eq!(p1, p2);
Ok(())
}
#[test]
fn test_utf8() -> Result<()> {
valid_utf8_opt!("@mode ", PlistEntry::Mode);
valid_utf8_opt!("@owner ", PlistEntry::Owner);
valid_utf8_opt!("@group ", PlistEntry::Group);
valid_utf8!("@name ", PlistEntry::Name);
valid_utf8!("@pkgdep ", PlistEntry::PkgDep);
valid_utf8!("@blddep ", PlistEntry::BldDep);
valid_utf8!("@pkgcfl ", PlistEntry::PkgCfl);
Ok(())
}
#[test]
fn test_8859() -> Result<()> {
valid_8859!("", PlistEntry::File);
valid_8859!("@cwd ", PlistEntry::Cwd);
valid_8859!("@exec ", PlistEntry::Exec);
valid_8859!("@unexec ", PlistEntry::UnExec);
valid_8859!("@pkgdir ", PlistEntry::PkgDir);
valid_8859!("@dirrm ", PlistEntry::DirRm);
valid_8859!("@display ", PlistEntry::Display);
valid_8859_opt!("@comment ", PlistEntry::Comment);
Ok(())
}
#[test]
fn test_args() -> Result<()> {
plist_match_ok!("@ignore", PlistEntry::Ignore);
plist_match_error!("@ignore hi", PlistError::IncorrectArguments);
plist_match_ok_arg!("@cwd /cwd", PlistEntry::Cwd);
plist_match_ok_arg!("@src /cwd", PlistEntry::Cwd);
plist_match_ok_arg!("@cd /cwd", PlistEntry::Cwd);
plist_match_ok_arg!("@exec echo hi", PlistEntry::Exec);
plist_match_ok_arg!("@unexec echo lo", PlistEntry::UnExec);
plist_match_ok_arg!("@name pkgname", PlistEntry::Name);
plist_match_ok_arg!("@pkgdir /dirname", PlistEntry::PkgDir);
plist_match_ok_arg!("@dirrm /dirname", PlistEntry::DirRm);
plist_match_ok_arg!("@display MESSAGE", PlistEntry::Display);
plist_match_ok_arg!("@pkgdep pkgname", PlistEntry::PkgDep);
plist_match_ok_arg!("@blddep pkgname", PlistEntry::BldDep);
plist_match_ok_arg!("@pkgcfl pkgname", PlistEntry::PkgCfl);
plist_match_error!("@cwd", PlistError::IncorrectArguments);
plist_match_error!("@src", PlistError::IncorrectArguments);
plist_match_error!("@cd", PlistError::IncorrectArguments);
plist_match_error!("@exec", PlistError::IncorrectArguments);
plist_match_error!("@unexec", PlistError::IncorrectArguments);
plist_match_error!("@name", PlistError::IncorrectArguments);
plist_match_error!("@pkgdir", PlistError::IncorrectArguments);
plist_match_error!("@dirrm", PlistError::IncorrectArguments);
plist_match_error!("@display", PlistError::IncorrectArguments);
plist_match_error!("@pkgdep", PlistError::IncorrectArguments);
plist_match_error!("@blddep", PlistError::IncorrectArguments);
plist_match_error!("@pkgcfl", PlistError::IncorrectArguments);
plist_match_ok_arg!("@comment", PlistEntry::Comment);
plist_match_ok_arg!("@comment hi there", PlistEntry::Comment);
plist_match_ok_arg!("@mode", PlistEntry::Mode);
plist_match_ok_arg!("@mode 0644", PlistEntry::Mode);
plist_match_ok_arg!("@owner", PlistEntry::Owner);
plist_match_ok_arg!("@owner root", PlistEntry::Owner);
plist_match_ok_arg!("@group", PlistEntry::Group);
plist_match_ok_arg!("@group wheel", PlistEntry::Group);
plist_match_ok_arg!("@option preserve", PlistEntry::PkgOpt);
plist_match_error!("@option", PlistError::IncorrectArguments);
plist_match_error!("@option invalid", PlistError::UnsupportedCommand);
Ok(())
}
#[test]
fn test_vecs() -> Result<()> {
let plist = plist!("@pkgdir one\n@pkgdir two\n@pkgdir three")?;
assert_eq!(
plist.pkgdirs().collect::<Vec<_>>(),
["one", "two", "three"]
);
let plist = plist!("@dirrm one\n@dirrm two\n@dirrm three")?;
assert_eq!(
plist.pkgrmdirs().collect::<Vec<_>>(),
["one", "two", "three"]
);
let plist = plist!("@pkgdep one\n@pkgdep two\n@pkgdep three")?;
assert_eq!(
plist.depends().collect::<Vec<_>>(),
["one", "two", "three"]
);
let plist = plist!("@blddep one\n@blddep two\n@blddep three")?;
assert_eq!(
plist.build_depends().collect::<Vec<_>>(),
["one", "two", "three"]
);
let plist = plist!("@pkgcfl one\n@pkgcfl two\n@pkgcfl three")?;
assert_eq!(
plist.conflicts().collect::<Vec<_>>(),
["one", "two", "three"]
);
Ok(())
}
#[test]
fn test_files() -> Result<()> {
let input = indoc! {"
@cwd /opt/pkg
bin/good
@cwd /
bin/evil
@ignore
@cwd /tmp
+IGNORE_ME
@cwd /opt/pkg
bin/ok
"};
let plist = Plist::from_bytes(input.as_bytes())?;
assert_eq!(plist.files(), ["bin/good", "bin/evil", "bin/ok"]);
assert_eq!(
plist.files_prefixed(),
["/opt/pkg/bin/good", "/bin/evil", "/opt/pkg/bin/ok"]
);
Ok(())
}
#[test]
fn test_first_match() -> Result<()> {
let plist = plist!("@comment not a pkgname")?;
assert_eq!(plist.pkgname(), None);
let plist = plist!("@name one\n@name two\n@name three")?;
assert_eq!(plist.pkgname(), Some("one"));
let plist = plist!("@comment not a display")?;
assert_eq!(plist.display(), None);
let plist = plist!("@display one\n@display two\n@display three")?;
assert_eq!(plist.display(), Some(OsString::from("one").as_os_str()));
Ok(())
}
#[test]
fn test_preserve() -> Result<()> {
assert!(!plist!("@comment not set")?.is_preserve());
assert!(plist!("@option preserve")?.is_preserve());
Ok(())
}
#[test]
fn test_file_checksum() -> Result<()> {
let entry =
plist_entry!("@comment MD5:d41d8cd98f00b204e9800998ecf8427e")?;
assert_eq!(
entry,
PlistEntry::FileChecksum(
"d41d8cd98f00b204e9800998ecf8427e".to_string()
)
);
let entry = plist_entry!("@comment MD5:abc123")?;
assert!(matches!(entry, PlistEntry::Comment(_)));
let entry =
plist_entry!("@comment MD5:d41d8cd98f00b204e9800998ecf8427g")?;
assert!(matches!(entry, PlistEntry::Comment(_)));
let entry = plist_entry!("@comment This is a comment")?;
assert!(matches!(entry, PlistEntry::Comment(_)));
Ok(())
}
#[test]
fn test_symlink_target() -> Result<()> {
let entry = plist_entry!("@comment Symlink:/usr/bin/target")?;
assert_eq!(
entry,
PlistEntry::SymlinkTarget(OsString::from("/usr/bin/target"))
);
let entry = plist_entry!("@comment Symlink:")?;
assert_eq!(entry, PlistEntry::SymlinkTarget(OsString::from("")));
Ok(())
}
#[test]
fn test_files_with_info() -> Result<()> {
let input = indoc! {"
@mode 0755
@owner root
@group wheel
bin/myapp
@comment MD5:d41d8cd98f00b204e9800998ecf8427e
@mode 0644
etc/myapp.conf
@comment MD5:098f6bcd4621d373cade4e832627b4f6
lib/libfoo.so
@comment Symlink:libfoo.so.1
@ignore
+BUILD_INFO
"};
let plist = Plist::from_bytes(input.as_bytes())?;
let files = plist.files_with_info();
assert_eq!(files.len(), 3);
assert_eq!(files[0].path, OsString::from("bin/myapp"));
assert_eq!(
files[0].checksum,
Some("d41d8cd98f00b204e9800998ecf8427e".to_string())
);
assert_eq!(files[0].symlink_target, None);
assert_eq!(files[0].mode, Some("0755".to_string()));
assert_eq!(files[0].owner, Some("root".to_string()));
assert_eq!(files[0].group, Some("wheel".to_string()));
assert_eq!(files[1].path, OsString::from("etc/myapp.conf"));
assert_eq!(
files[1].checksum,
Some("098f6bcd4621d373cade4e832627b4f6".to_string())
);
assert_eq!(files[1].mode, Some("0644".to_string()));
assert_eq!(files[2].path, OsString::from("lib/libfoo.so"));
assert_eq!(files[2].checksum, None);
assert_eq!(
files[2].symlink_target,
Some(OsString::from("libfoo.so.1"))
);
Ok(())
}
#[test]
fn test_into_iterator() -> Result<()> {
let plist =
plist!("@name pkg-1.0\nbin/foo\n@pkgdep dep-[0-9]*\nbin/bar")?;
let entries: Vec<_> = plist.into_iter().collect();
assert_eq!(entries.len(), 4);
assert!(matches!(entries[0], PlistEntry::Name(_)));
assert!(matches!(entries[1], PlistEntry::File(_)));
assert!(matches!(entries[2], PlistEntry::PkgDep(_)));
assert!(matches!(entries[3], PlistEntry::File(_)));
Ok(())
}
#[test]
fn test_iter_by_ref() -> Result<()> {
let plist = plist!("@name pkg-1.0\nbin/foo\nbin/bar")?;
let file_count = (&plist)
.into_iter()
.filter(|e| matches!(e, PlistEntry::File(_)))
.count();
assert_eq!(file_count, 2);
assert_eq!(plist.pkgname(), Some("pkg-1.0"));
Ok(())
}
}