use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
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(#[from] Utf8Error),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PlistEntry<'a> {
File(Cow<'a, Path>),
Cwd(Cow<'a, Path>),
Exec(Cow<'a, OsStr>),
UnExec(Cow<'a, OsStr>),
Mode(Option<Cow<'a, str>>),
PkgOpt(PlistOption),
Owner(Option<Cow<'a, str>>),
Group(Option<Cow<'a, str>>),
Comment(Option<Cow<'a, OsStr>>),
Ignore,
Name(Cow<'a, str>),
PkgDir(Cow<'a, Path>),
DirRm(Cow<'a, Path>),
Display(Cow<'a, Path>),
PkgDep(Cow<'a, str>),
BldDep(Cow<'a, str>),
PkgCfl(Cow<'a, str>),
FileChecksum(Cow<'a, str>),
SymlinkTarget(Cow<'a, Path>),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PlistOption {
Preserve,
}
impl<'a> PlistEntry<'a> {
pub fn from_bytes(bytes: &'a [u8]) -> Result<Self> {
parse_line(bytes)
}
#[must_use]
pub fn into_owned(self) -> PlistEntry<'static> {
use PlistEntry as P;
match self {
P::File(p) => P::File(own(p)),
P::Cwd(p) => P::Cwd(own(p)),
P::Exec(o) => P::Exec(own(o)),
P::UnExec(o) => P::UnExec(own(o)),
P::Mode(s) => P::Mode(own_opt(s)),
P::PkgOpt(o) => P::PkgOpt(o),
P::Owner(s) => P::Owner(own_opt(s)),
P::Group(s) => P::Group(own_opt(s)),
P::Comment(o) => P::Comment(own_opt(o)),
P::Ignore => P::Ignore,
P::Name(s) => P::Name(own(s)),
P::PkgDir(p) => P::PkgDir(own(p)),
P::DirRm(p) => P::DirRm(own(p)),
P::Display(p) => P::Display(own(p)),
P::PkgDep(s) => P::PkgDep(own(s)),
P::BldDep(s) => P::BldDep(own(s)),
P::PkgCfl(s) => P::PkgCfl(own(s)),
P::FileChecksum(s) => P::FileChecksum(own(s)),
P::SymlinkTarget(p) => P::SymlinkTarget(own(p)),
}
}
}
#[derive(Clone, Debug)]
pub struct Parser<'a> {
rest: &'a [u8],
}
impl<'a> Iterator for Parser<'a> {
type Item = Result<PlistEntry<'a>>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let line = next_line(&mut self.rest)?;
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
return Some(parse_line(line));
}
}
}
impl std::iter::FusedIterator for Parser<'_> {}
#[must_use]
pub fn parse(bytes: &[u8]) -> Parser<'_> {
Parser { rest: bytes }
}
fn next_line<'a>(rest: &mut &'a [u8]) -> Option<&'a [u8]> {
if rest.is_empty() {
return None;
}
match rest.iter().position(|&b| b == b'\n') {
Some(i) => {
let line = &rest[..i];
*rest = &rest[i + 1..];
Some(line)
}
None => {
let line = *rest;
*rest = &[];
Some(line)
}
}
}
fn parse_line(line: &[u8]) -> Result<PlistEntry<'_>> {
let line = line.trim_ascii_end();
let (cmd, args) = split_cmd_args(line);
if !cmd.starts_with(b"@") {
return Ok(PlistEntry::File(borrow_path(line)));
}
match cmd {
b"@cwd" | b"@src" | b"@cd" => {
required_path(args, line, PlistEntry::Cwd)
}
b"@exec" => required_osstr(args, line, PlistEntry::Exec),
b"@unexec" => required_osstr(args, line, PlistEntry::UnExec),
b"@mode" => Ok(PlistEntry::Mode(optional_str(args)?)),
b"@owner" => Ok(PlistEntry::Owner(optional_str(args)?)),
b"@group" => Ok(PlistEntry::Group(optional_str(args)?)),
b"@option" => match args {
Some(b"preserve") => Ok(PlistEntry::PkgOpt(PlistOption::Preserve)),
Some(_) => Err(PlistError::UnsupportedCommand(os(line))),
None => Err(PlistError::IncorrectArguments(os(line))),
},
b"@comment" => parse_comment(args),
b"@ignore" => match args {
None => Ok(PlistEntry::Ignore),
Some(_) => Err(PlistError::IncorrectArguments(os(line))),
},
b"@name" => required_str(args, line, PlistEntry::Name),
b"@pkgdep" => required_str(args, line, PlistEntry::PkgDep),
b"@blddep" => required_str(args, line, PlistEntry::BldDep),
b"@pkgcfl" => required_str(args, line, PlistEntry::PkgCfl),
b"@pkgdir" => required_path(args, line, PlistEntry::PkgDir),
b"@dirrm" => required_path(args, line, PlistEntry::DirRm),
b"@display" => required_path(args, line, PlistEntry::Display),
_ => Err(PlistError::UnsupportedCommand(os(cmd))),
}
}
fn split_cmd_args(line: &[u8]) -> (&[u8], Option<&[u8]>) {
match line.iter().position(|&b| b == b' ') {
None => (line, None),
Some(i) => {
let args = line[i + 1..].trim_ascii_start();
(&line[..i], (!args.is_empty()).then_some(args))
}
}
}
fn required_path<'a>(
args: Option<&'a [u8]>,
line: &[u8],
ctor: fn(Cow<'a, Path>) -> PlistEntry<'a>,
) -> Result<PlistEntry<'a>> {
match args {
Some(a) => Ok(ctor(borrow_path(a))),
None => Err(PlistError::IncorrectArguments(os(line))),
}
}
fn required_osstr<'a>(
args: Option<&'a [u8]>,
line: &[u8],
ctor: fn(Cow<'a, OsStr>) -> PlistEntry<'a>,
) -> Result<PlistEntry<'a>> {
match args {
Some(a) => Ok(ctor(borrow_osstr(a))),
None => Err(PlistError::IncorrectArguments(os(line))),
}
}
fn required_str<'a>(
args: Option<&'a [u8]>,
line: &[u8],
ctor: fn(Cow<'a, str>) -> PlistEntry<'a>,
) -> Result<PlistEntry<'a>> {
match args {
Some(a) => Ok(ctor(Cow::Borrowed(std::str::from_utf8(a)?))),
None => Err(PlistError::IncorrectArguments(os(line))),
}
}
fn optional_str(args: Option<&[u8]>) -> Result<Option<Cow<'_, str>>> {
Ok(args
.map(|a| std::str::from_utf8(a).map(Cow::Borrowed))
.transpose()?)
}
fn parse_comment(args: Option<&[u8]>) -> Result<PlistEntry<'_>> {
let Some(a) = args else {
return Ok(PlistEntry::Comment(None));
};
if let Some(rest) = a.strip_prefix(b"MD5:")
&& rest.len() == 32
&& rest.iter().all(u8::is_ascii_hexdigit)
{
return Ok(PlistEntry::FileChecksum(Cow::Borrowed(
std::str::from_utf8(rest)?,
)));
}
if let Some(rest) = a.strip_prefix(b"Symlink:") {
return Ok(PlistEntry::SymlinkTarget(borrow_path(rest)));
}
Ok(PlistEntry::Comment(Some(borrow_osstr(a))))
}
#[inline]
fn borrow_osstr(bytes: &[u8]) -> Cow<'_, OsStr> {
Cow::Borrowed(OsStr::from_bytes(bytes))
}
#[inline]
fn borrow_path(bytes: &[u8]) -> Cow<'_, Path> {
Cow::Borrowed(Path::new(OsStr::from_bytes(bytes)))
}
#[inline]
fn os(bytes: &[u8]) -> OsString {
OsStr::from_bytes(bytes).to_os_string()
}
#[inline]
fn own<T: ToOwned + ?Sized + 'static>(c: Cow<'_, T>) -> Cow<'static, T> {
Cow::Owned(c.into_owned())
}
#[inline]
fn own_opt<T: ToOwned + ?Sized + 'static>(
c: Option<Cow<'_, T>>,
) -> Option<Cow<'static, T>> {
c.map(own)
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileInfo {
pub path: PathBuf,
pub checksum: Option<String>,
pub symlink_target: Option<PathBuf>,
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<'static>>,
}
impl Plist {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let mut entries = Vec::new();
for r in parse(bytes) {
entries.push(r?.into_owned());
}
Ok(Self { entries })
}
#[must_use]
pub fn pkgname(&self) -> Option<&str> {
self.entries.iter().find_map(|e| match e {
PlistEntry::Name(s) => Some(s.as_ref()),
_ => None,
})
}
#[must_use]
pub fn display(&self) -> Option<&Path> {
self.entries.iter().find_map(|e| match e {
PlistEntry::Display(p) => Some(p.as_ref()),
_ => None,
})
}
pub fn depends(&self) -> impl Iterator<Item = &str> + '_ {
self.entries.iter().filter_map(|e| match e {
PlistEntry::PkgDep(s) => Some(s.as_ref()),
_ => None,
})
}
pub fn build_depends(&self) -> impl Iterator<Item = &str> + '_ {
self.entries.iter().filter_map(|e| match e {
PlistEntry::BldDep(s) => Some(s.as_ref()),
_ => None,
})
}
pub fn conflicts(&self) -> impl Iterator<Item = &str> + '_ {
self.entries.iter().filter_map(|e| match e {
PlistEntry::PkgCfl(s) => Some(s.as_ref()),
_ => None,
})
}
pub fn pkgdirs(&self) -> impl Iterator<Item = &Path> + '_ {
self.entries.iter().filter_map(|e| match e {
PlistEntry::PkgDir(p) => Some(p.as_ref()),
_ => None,
})
}
pub fn pkgrmdirs(&self) -> impl Iterator<Item = &Path> + '_ {
self.entries.iter().filter_map(|e| match e {
PlistEntry::DirRm(p) => Some(p.as_ref()),
_ => None,
})
}
pub fn files(&self) -> impl Iterator<Item = &Path> + '_ {
let mut ignore = false;
self.entries.iter().filter_map(move |entry| match entry {
PlistEntry::Ignore => {
ignore = true;
None
}
PlistEntry::File(file) => {
if std::mem::take(&mut ignore) {
None
} else {
Some(file.as_ref())
}
}
_ => None,
})
}
pub fn files_prefixed(&self) -> impl Iterator<Item = PathBuf> + '_ {
let mut ignore = false;
let mut prefix: Option<&Path> = None;
self.entries.iter().filter_map(move |entry| match entry {
PlistEntry::Cwd(dir) => {
prefix = Some(dir.as_ref());
None
}
PlistEntry::Ignore => {
ignore = true;
None
}
PlistEntry::File(file) => {
if std::mem::take(&mut ignore) {
return None;
}
let file: &Path = file.as_ref();
Some(match prefix {
Some(pfx) => pfx.join(file),
None => file.to_path_buf(),
})
}
_ => None,
})
}
pub fn files_with_info(&self) -> impl Iterator<Item = FileInfo> + '_ {
FilesWithInfo::new(&self.entries)
}
pub fn install_cmds(
&self,
) -> impl Iterator<Item = &PlistEntry<'static>> + '_ {
let mut ignore = false;
self.entries.iter().filter(move |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,
})
}
pub fn uninstall_cmds(
&self,
) -> impl Iterator<Item = &PlistEntry<'static>> + '_ {
let mut ignore = false;
self.entries.iter().filter(move |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,
})
}
#[must_use]
pub fn is_preserve(&self) -> bool {
self.entries
.iter()
.any(|e| matches!(e, PlistEntry::PkgOpt(PlistOption::Preserve)))
}
}
struct FilesWithInfo<'a> {
entries: &'a [PlistEntry<'static>],
i: usize,
ignore: bool,
mode: Option<String>,
owner: Option<String>,
group: Option<String>,
}
impl<'a> FilesWithInfo<'a> {
fn new(entries: &'a [PlistEntry<'static>]) -> Self {
Self {
entries,
i: 0,
ignore: false,
mode: None,
owner: None,
group: None,
}
}
}
impl Iterator for FilesWithInfo<'_> {
type Item = FileInfo;
fn next(&mut self) -> Option<FileInfo> {
while self.i < self.entries.len() {
match &self.entries[self.i] {
PlistEntry::Mode(m) => {
self.mode = m.as_deref().map(str::to_owned);
}
PlistEntry::Owner(o) => {
self.owner = o.as_deref().map(str::to_owned);
}
PlistEntry::Group(g) => {
self.group = g.as_deref().map(str::to_owned);
}
PlistEntry::Ignore => self.ignore = true,
PlistEntry::File(path) => {
self.i += 1;
if std::mem::take(&mut self.ignore) {
continue;
}
let mut info = FileInfo {
path: path.as_ref().to_path_buf(),
checksum: None,
symlink_target: None,
mode: self.mode.clone(),
owner: self.owner.clone(),
group: self.group.clone(),
};
while self.i < self.entries.len() {
match &self.entries[self.i] {
PlistEntry::FileChecksum(hash) => {
info.checksum = Some(hash.as_ref().to_owned());
self.i += 1;
}
PlistEntry::SymlinkTarget(target) => {
info.symlink_target =
Some(target.as_ref().to_path_buf());
self.i += 1;
}
_ => break,
}
}
return Some(info);
}
_ => {}
}
self.i += 1;
}
None
}
}
impl IntoIterator for Plist {
type Item = PlistEntry<'static>;
type IntoIter = std::vec::IntoIter<PlistEntry<'static>>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl<'a> IntoIterator for &'a Plist {
type Item = &'a PlistEntry<'static>;
type IntoIter = std::slice::Iter<'a, PlistEntry<'static>>;
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())
.map(PlistEntry::into_owned)
};
}
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(Cow::Borrowed("💖")));
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(Cow::Borrowed("💖")))
);
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_path {
($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(Cow::Borrowed(Path::new("💖")))
);
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_osstr {
($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(Cow::Borrowed(OsStr::new("💖")))
);
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_osstr_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(Cow::Borrowed(OsStr::new("💖"))))
);
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(Cow::Borrowed(OsStr::new("hi"))));
assert_eq!(p1, p2);
let p1 = plist_entry!(" @comment ")?;
let p2 = PlistEntry::File(Cow::Borrowed(Path::new(" @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_path!("", PlistEntry::File);
valid_path!("@cwd ", PlistEntry::Cwd);
valid_osstr!("@exec ", PlistEntry::Exec);
valid_osstr!("@unexec ", PlistEntry::UnExec);
valid_path!("@pkgdir ", PlistEntry::PkgDir);
valid_path!("@dirrm ", PlistEntry::DirRm);
valid_path!("@display ", PlistEntry::Display);
valid_osstr_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"].map(Path::new)
);
let plist = plist!("@dirrm one\n@dirrm two\n@dirrm three")?;
assert_eq!(
plist.pkgrmdirs().collect::<Vec<_>>(),
["one", "two", "three"].map(Path::new)
);
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())?;
let files: Vec<&Path> = plist.files().collect();
assert_eq!(files, ["bin/good", "bin/evil", "bin/ok"].map(Path::new));
let prefixed: Vec<PathBuf> = plist.files_prefixed().collect();
assert_eq!(
prefixed,
["/opt/pkg/bin/good", "/bin/evil", "/opt/pkg/bin/ok"]
.map(PathBuf::from)
);
let plist = Plist::from_bytes(b"bin/relative\n")?;
let files: Vec<&Path> = plist.files().collect();
assert_eq!(files, [Path::new("bin/relative")]);
let prefixed: Vec<PathBuf> = plist.files_prefixed().collect();
assert_eq!(prefixed, [PathBuf::from("bin/relative")]);
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(Path::new("one")));
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(Cow::Borrowed(
"d41d8cd98f00b204e9800998ecf8427e"
))
);
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(Cow::Borrowed(Path::new(
"/usr/bin/target"
)))
);
let entry = plist_entry!("@comment Symlink:")?;
assert_eq!(
entry,
PlistEntry::SymlinkTarget(Cow::Borrowed(Path::new("")))
);
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: Vec<FileInfo> = plist.files_with_info().collect();
assert_eq!(files.len(), 3);
assert_eq!(files[0].path, PathBuf::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, PathBuf::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, PathBuf::from("lib/libfoo.so"));
assert_eq!(files[2].checksum, None);
assert_eq!(files[2].symlink_target, Some(PathBuf::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(())
}
#[test]
fn test_parse_iter() -> Result<()> {
let input = b"@name pkg-1.0\nbin/foo\n@pkgdir /var/db/x\n";
let entries: Vec<_> = parse(input).collect::<Result<Vec<_>>>()?;
assert_eq!(entries.len(), 3);
assert_eq!(entries[0], PlistEntry::Name(Cow::Borrowed("pkg-1.0")));
assert_eq!(
entries[1],
PlistEntry::File(Cow::Borrowed(Path::new("bin/foo")))
);
assert_eq!(
entries[2],
PlistEntry::PkgDir(Cow::Borrowed(Path::new("/var/db/x")))
);
Ok(())
}
#[test]
fn test_parse_iter_no_trailing_newline() -> Result<()> {
let input = b"@name pkg-1.0\nbin/foo";
let entries: Vec<_> = parse(input).collect::<Result<Vec<_>>>()?;
assert_eq!(entries.len(), 2);
Ok(())
}
#[test]
fn test_parse_iter_skips_blanks() -> Result<()> {
let input = b"@name pkg-1.0\n\n \nbin/foo\n";
let entries: Vec<_> = parse(input).collect::<Result<Vec<_>>>()?;
assert_eq!(entries.len(), 2);
Ok(())
}
#[test]
fn test_parse_comment_special_forms() -> Result<()> {
let entries: Vec<_> = parse(
b"@comment MD5:d41d8cd98f00b204e9800998ecf8427e\n\
@comment Symlink:/usr/bin/target\n\
@comment plain comment\n",
)
.collect::<Result<Vec<_>>>()?;
assert_eq!(
entries[0],
PlistEntry::FileChecksum(Cow::Borrowed(
"d41d8cd98f00b204e9800998ecf8427e"
))
);
assert_eq!(
entries[1],
PlistEntry::SymlinkTarget(Cow::Borrowed(Path::new(
"/usr/bin/target"
)))
);
assert!(matches!(entries[2], PlistEntry::Comment(Some(_))));
Ok(())
}
#[test]
fn test_parse_validates_utf8() -> Result<()> {
let input = b"@name \xff-bad\nbin/foo\n";
match parse(input).next() {
Some(Err(PlistError::Utf8(_))) => Ok(()),
other => panic!("expected Utf8 error from parse(), got {other:?}"),
}
}
#[test]
fn test_into_owned() -> Result<()> {
let owned: PlistEntry<'static> = {
let bytes: Vec<u8> = b"@name pkg-1.0".to_vec();
PlistEntry::from_bytes(&bytes)?.into_owned()
};
assert_eq!(owned, PlistEntry::Name(Cow::Owned("pkg-1.0".to_owned())));
Ok(())
}
}