1use std::borrow::Cow;
126use std::ffi::{OsStr, OsString};
127use std::os::unix::ffi::OsStrExt;
128use std::path::{Path, PathBuf};
129use std::str::Utf8Error;
130use thiserror::Error;
131
132#[cfg(test)]
133use indoc::indoc;
134
135pub type Result<T> = std::result::Result<T, PlistError>;
140
141#[derive(Debug, Error)]
145pub enum PlistError {
146 #[error("unsupported plist command: {cmd}", cmd = .0.to_string_lossy())]
151 UnsupportedCommand(OsString),
152 #[error("incorrect command arguments: {args}", args = .0.to_string_lossy())]
157 IncorrectArguments(OsString),
158 #[error("invalid UTF-8 sequence: {0}")]
162 Utf8(#[from] Utf8Error),
163}
164
165#[derive(Clone, Debug, Eq, Hash, PartialEq)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189pub enum PlistEntry<'a> {
190 File(Cow<'a, Path>),
194 Cwd(Cow<'a, Path>),
199 Exec(Cow<'a, OsStr>),
203 UnExec(Cow<'a, OsStr>),
207 Mode(Option<Cow<'a, str>>),
211 PkgOpt(PlistOption),
216 Owner(Option<Cow<'a, str>>),
221 Group(Option<Cow<'a, str>>),
226 Comment(Option<Cow<'a, OsStr>>),
231 Ignore,
235 Name(Cow<'a, str>),
239 PkgDir(Cow<'a, Path>),
243 DirRm(Cow<'a, Path>),
247 Display(Cow<'a, Path>),
251 PkgDep(Cow<'a, str>),
255 BldDep(Cow<'a, str>),
259 PkgCfl(Cow<'a, str>),
263 FileChecksum(Cow<'a, str>),
268 SymlinkTarget(Cow<'a, Path>),
273}
274
275#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
280#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
281pub enum PlistOption {
282 Preserve,
288}
289
290impl<'a> PlistEntry<'a> {
291 pub fn from_bytes(bytes: &'a [u8]) -> Result<Self> {
297 parse_line(bytes)
298 }
299
300 #[must_use]
305 pub fn into_owned(self) -> PlistEntry<'static> {
306 use PlistEntry as P;
307 match self {
308 P::File(p) => P::File(own(p)),
309 P::Cwd(p) => P::Cwd(own(p)),
310 P::Exec(o) => P::Exec(own(o)),
311 P::UnExec(o) => P::UnExec(own(o)),
312 P::Mode(s) => P::Mode(own_opt(s)),
313 P::PkgOpt(o) => P::PkgOpt(o),
314 P::Owner(s) => P::Owner(own_opt(s)),
315 P::Group(s) => P::Group(own_opt(s)),
316 P::Comment(o) => P::Comment(own_opt(o)),
317 P::Ignore => P::Ignore,
318 P::Name(s) => P::Name(own(s)),
319 P::PkgDir(p) => P::PkgDir(own(p)),
320 P::DirRm(p) => P::DirRm(own(p)),
321 P::Display(p) => P::Display(own(p)),
322 P::PkgDep(s) => P::PkgDep(own(s)),
323 P::BldDep(s) => P::BldDep(own(s)),
324 P::PkgCfl(s) => P::PkgCfl(own(s)),
325 P::FileChecksum(s) => P::FileChecksum(own(s)),
326 P::SymlinkTarget(p) => P::SymlinkTarget(own(p)),
327 }
328 }
329}
330
331#[derive(Clone, Debug)]
342pub struct Parser<'a> {
343 rest: &'a [u8],
344}
345
346impl<'a> Iterator for Parser<'a> {
347 type Item = Result<PlistEntry<'a>>;
348
349 fn next(&mut self) -> Option<Self::Item> {
350 loop {
351 let line = next_line(&mut self.rest)?;
352 if line.iter().all(u8::is_ascii_whitespace) {
353 continue;
354 }
355 return Some(parse_line(line));
356 }
357 }
358}
359
360impl std::iter::FusedIterator for Parser<'_> {}
361
362#[must_use]
375pub fn parse(bytes: &[u8]) -> Parser<'_> {
376 Parser { rest: bytes }
377}
378
379fn next_line<'a>(rest: &mut &'a [u8]) -> Option<&'a [u8]> {
380 if rest.is_empty() {
381 return None;
382 }
383 match rest.iter().position(|&b| b == b'\n') {
384 Some(i) => {
385 let line = &rest[..i];
386 *rest = &rest[i + 1..];
387 Some(line)
388 }
389 None => {
390 let line = *rest;
391 *rest = &[];
392 Some(line)
393 }
394 }
395}
396
397fn parse_line(line: &[u8]) -> Result<PlistEntry<'_>> {
398 let line = line.trim_ascii_end();
399 let (cmd, args) = split_cmd_args(line);
400
401 if !cmd.starts_with(b"@") {
402 return Ok(PlistEntry::File(borrow_path(line)));
403 }
404
405 match cmd {
406 b"@cwd" | b"@src" | b"@cd" => {
410 required_path(args, line, PlistEntry::Cwd)
411 }
412 b"@exec" => required_osstr(args, line, PlistEntry::Exec),
413 b"@unexec" => required_osstr(args, line, PlistEntry::UnExec),
414
415 b"@mode" => Ok(PlistEntry::Mode(optional_str(args)?)),
420 b"@owner" => Ok(PlistEntry::Owner(optional_str(args)?)),
421 b"@group" => Ok(PlistEntry::Group(optional_str(args)?)),
422
423 b"@option" => match args {
427 Some(b"preserve") => Ok(PlistEntry::PkgOpt(PlistOption::Preserve)),
428 Some(_) => Err(PlistError::UnsupportedCommand(os(line))),
429 None => Err(PlistError::IncorrectArguments(os(line))),
430 },
431
432 b"@comment" => parse_comment(args),
443
444 b"@ignore" => match args {
448 None => Ok(PlistEntry::Ignore),
449 Some(_) => Err(PlistError::IncorrectArguments(os(line))),
450 },
451
452 b"@name" => required_str(args, line, PlistEntry::Name),
453 b"@pkgdep" => required_str(args, line, PlistEntry::PkgDep),
454 b"@blddep" => required_str(args, line, PlistEntry::BldDep),
455 b"@pkgcfl" => required_str(args, line, PlistEntry::PkgCfl),
456
457 b"@pkgdir" => required_path(args, line, PlistEntry::PkgDir),
458 b"@dirrm" => required_path(args, line, PlistEntry::DirRm),
459 b"@display" => required_path(args, line, PlistEntry::Display),
460
461 _ => Err(PlistError::UnsupportedCommand(os(cmd))),
462 }
463}
464
465fn split_cmd_args(line: &[u8]) -> (&[u8], Option<&[u8]>) {
466 match line.iter().position(|&b| b == b' ') {
467 None => (line, None),
468 Some(i) => {
469 let args = line[i + 1..].trim_ascii_start();
470 (&line[..i], (!args.is_empty()).then_some(args))
471 }
472 }
473}
474
475fn required_path<'a>(
476 args: Option<&'a [u8]>,
477 line: &[u8],
478 ctor: fn(Cow<'a, Path>) -> PlistEntry<'a>,
479) -> Result<PlistEntry<'a>> {
480 match args {
481 Some(a) => Ok(ctor(borrow_path(a))),
482 None => Err(PlistError::IncorrectArguments(os(line))),
483 }
484}
485
486fn required_osstr<'a>(
487 args: Option<&'a [u8]>,
488 line: &[u8],
489 ctor: fn(Cow<'a, OsStr>) -> PlistEntry<'a>,
490) -> Result<PlistEntry<'a>> {
491 match args {
492 Some(a) => Ok(ctor(borrow_osstr(a))),
493 None => Err(PlistError::IncorrectArguments(os(line))),
494 }
495}
496
497fn required_str<'a>(
498 args: Option<&'a [u8]>,
499 line: &[u8],
500 ctor: fn(Cow<'a, str>) -> PlistEntry<'a>,
501) -> Result<PlistEntry<'a>> {
502 match args {
503 Some(a) => Ok(ctor(Cow::Borrowed(std::str::from_utf8(a)?))),
504 None => Err(PlistError::IncorrectArguments(os(line))),
505 }
506}
507
508fn optional_str(args: Option<&[u8]>) -> Result<Option<Cow<'_, str>>> {
509 Ok(args
510 .map(|a| std::str::from_utf8(a).map(Cow::Borrowed))
511 .transpose()?)
512}
513
514fn parse_comment(args: Option<&[u8]>) -> Result<PlistEntry<'_>> {
515 let Some(a) = args else {
516 return Ok(PlistEntry::Comment(None));
517 };
518 if let Some(rest) = a.strip_prefix(b"MD5:")
519 && rest.len() == 32
520 && rest.iter().all(u8::is_ascii_hexdigit)
521 {
522 return Ok(PlistEntry::FileChecksum(Cow::Borrowed(
523 std::str::from_utf8(rest)?,
524 )));
525 }
526 if let Some(rest) = a.strip_prefix(b"Symlink:") {
527 return Ok(PlistEntry::SymlinkTarget(borrow_path(rest)));
528 }
529 Ok(PlistEntry::Comment(Some(borrow_osstr(a))))
530}
531
532#[inline]
533fn borrow_osstr(bytes: &[u8]) -> Cow<'_, OsStr> {
534 Cow::Borrowed(OsStr::from_bytes(bytes))
535}
536
537#[inline]
538fn borrow_path(bytes: &[u8]) -> Cow<'_, Path> {
539 Cow::Borrowed(Path::new(OsStr::from_bytes(bytes)))
540}
541
542#[inline]
543fn os(bytes: &[u8]) -> OsString {
544 OsStr::from_bytes(bytes).to_os_string()
545}
546
547#[inline]
548fn own<T: ToOwned + ?Sized + 'static>(c: Cow<'_, T>) -> Cow<'static, T> {
549 Cow::Owned(c.into_owned())
550}
551
552#[inline]
553fn own_opt<T: ToOwned + ?Sized + 'static>(
554 c: Option<Cow<'_, T>>,
555) -> Option<Cow<'static, T>> {
556 c.map(own)
557}
558
559#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
569#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
570pub struct FileInfo {
571 pub path: PathBuf,
573 pub checksum: Option<String>,
575 pub symlink_target: Option<PathBuf>,
577 pub mode: Option<String>,
579 pub owner: Option<String>,
581 pub group: Option<String>,
583}
584
585#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
607#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
608pub struct Plist {
609 entries: Vec<PlistEntry<'static>>,
610}
611
612impl Plist {
613 #[must_use]
617 pub fn new() -> Self {
618 Self::default()
619 }
620
621 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
632 let mut entries = Vec::new();
633 for r in parse(bytes) {
634 entries.push(r?.into_owned());
635 }
636 Ok(Self { entries })
637 }
638
639 #[must_use]
646 pub fn pkgname(&self) -> Option<&str> {
647 self.entries.iter().find_map(|e| match e {
648 PlistEntry::Name(s) => Some(s.as_ref()),
649 _ => None,
650 })
651 }
652
653 #[must_use]
659 pub fn display(&self) -> Option<&Path> {
660 self.entries.iter().find_map(|e| match e {
661 PlistEntry::Display(p) => Some(p.as_ref()),
662 _ => None,
663 })
664 }
665
666 pub fn depends(&self) -> impl Iterator<Item = &str> + '_ {
670 self.entries.iter().filter_map(|e| match e {
671 PlistEntry::PkgDep(s) => Some(s.as_ref()),
672 _ => None,
673 })
674 }
675
676 pub fn build_depends(&self) -> impl Iterator<Item = &str> + '_ {
680 self.entries.iter().filter_map(|e| match e {
681 PlistEntry::BldDep(s) => Some(s.as_ref()),
682 _ => None,
683 })
684 }
685
686 pub fn conflicts(&self) -> impl Iterator<Item = &str> + '_ {
690 self.entries.iter().filter_map(|e| match e {
691 PlistEntry::PkgCfl(s) => Some(s.as_ref()),
692 _ => None,
693 })
694 }
695
696 pub fn pkgdirs(&self) -> impl Iterator<Item = &Path> + '_ {
700 self.entries.iter().filter_map(|e| match e {
701 PlistEntry::PkgDir(p) => Some(p.as_ref()),
702 _ => None,
703 })
704 }
705
706 pub fn pkgrmdirs(&self) -> impl Iterator<Item = &Path> + '_ {
710 self.entries.iter().filter_map(|e| match e {
711 PlistEntry::DirRm(p) => Some(p.as_ref()),
712 _ => None,
713 })
714 }
715
716 pub fn files(&self) -> impl Iterator<Item = &Path> + '_ {
721 let mut ignore = false;
722 self.entries.iter().filter_map(move |entry| match entry {
723 PlistEntry::Ignore => {
724 ignore = true;
725 None
726 }
727 PlistEntry::File(file) => {
728 if std::mem::take(&mut ignore) {
729 None
730 } else {
731 Some(file.as_ref())
732 }
733 }
734 _ => None,
735 })
736 }
737
738 pub fn files_prefixed(&self) -> impl Iterator<Item = PathBuf> + '_ {
744 let mut ignore = false;
745 let mut prefix: Option<&Path> = None;
746 self.entries.iter().filter_map(move |entry| match entry {
747 PlistEntry::Cwd(dir) => {
748 prefix = Some(dir.as_ref());
749 None
750 }
751 PlistEntry::Ignore => {
752 ignore = true;
753 None
754 }
755 PlistEntry::File(file) => {
756 if std::mem::take(&mut ignore) {
757 return None;
758 }
759 let file: &Path = file.as_ref();
760 Some(match prefix {
761 Some(pfx) => pfx.join(file),
762 None => file.to_path_buf(),
763 })
764 }
765 _ => None,
766 })
767 }
768
769 pub fn files_with_info(&self) -> impl Iterator<Item = FileInfo> + '_ {
782 FilesWithInfo::new(&self.entries)
783 }
784
785 pub fn install_cmds(
791 &self,
792 ) -> impl Iterator<Item = &PlistEntry<'static>> + '_ {
793 let mut ignore = false;
794 self.entries.iter().filter(move |entry| match entry {
795 PlistEntry::Ignore => {
799 ignore = true;
800 false
801 }
802 PlistEntry::File(_) => !std::mem::take(&mut ignore),
803 PlistEntry::Cwd(_)
804 | PlistEntry::Exec(_)
805 | PlistEntry::Mode(_)
806 | PlistEntry::Owner(_)
807 | PlistEntry::Group(_)
808 | PlistEntry::PkgDir(_) => true,
809 _ => false,
810 })
811 }
812
813 pub fn uninstall_cmds(
819 &self,
820 ) -> impl Iterator<Item = &PlistEntry<'static>> + '_ {
821 let mut ignore = false;
822 self.entries.iter().filter(move |entry| match entry {
823 PlistEntry::Ignore => {
827 ignore = true;
828 false
829 }
830 PlistEntry::File(_) => !std::mem::take(&mut ignore),
831 PlistEntry::Cwd(_)
832 | PlistEntry::UnExec(_)
833 | PlistEntry::Mode(_)
834 | PlistEntry::Owner(_)
835 | PlistEntry::Group(_)
836 | PlistEntry::PkgDir(_)
837 | PlistEntry::DirRm(_) => true,
838 _ => false,
839 })
840 }
841
842 #[must_use]
846 pub fn is_preserve(&self) -> bool {
847 self.entries
848 .iter()
849 .any(|e| matches!(e, PlistEntry::PkgOpt(PlistOption::Preserve)))
850 }
851}
852
853struct FilesWithInfo<'a> {
854 entries: &'a [PlistEntry<'static>],
855 i: usize,
856 ignore: bool,
857 mode: Option<String>,
858 owner: Option<String>,
859 group: Option<String>,
860}
861
862impl<'a> FilesWithInfo<'a> {
863 fn new(entries: &'a [PlistEntry<'static>]) -> Self {
864 Self {
865 entries,
866 i: 0,
867 ignore: false,
868 mode: None,
869 owner: None,
870 group: None,
871 }
872 }
873}
874
875impl Iterator for FilesWithInfo<'_> {
876 type Item = FileInfo;
877
878 fn next(&mut self) -> Option<FileInfo> {
879 while self.i < self.entries.len() {
880 match &self.entries[self.i] {
881 PlistEntry::Mode(m) => {
882 self.mode = m.as_deref().map(str::to_owned);
883 }
884 PlistEntry::Owner(o) => {
885 self.owner = o.as_deref().map(str::to_owned);
886 }
887 PlistEntry::Group(g) => {
888 self.group = g.as_deref().map(str::to_owned);
889 }
890 PlistEntry::Ignore => self.ignore = true,
891 PlistEntry::File(path) => {
892 self.i += 1;
893 if std::mem::take(&mut self.ignore) {
894 continue;
895 }
896 let mut info = FileInfo {
897 path: path.as_ref().to_path_buf(),
898 checksum: None,
899 symlink_target: None,
900 mode: self.mode.clone(),
901 owner: self.owner.clone(),
902 group: self.group.clone(),
903 };
904 while self.i < self.entries.len() {
905 match &self.entries[self.i] {
906 PlistEntry::FileChecksum(hash) => {
907 info.checksum = Some(hash.as_ref().to_owned());
908 self.i += 1;
909 }
910 PlistEntry::SymlinkTarget(target) => {
911 info.symlink_target =
912 Some(target.as_ref().to_path_buf());
913 self.i += 1;
914 }
915 _ => break,
916 }
917 }
918 return Some(info);
919 }
920 _ => {}
921 }
922 self.i += 1;
923 }
924 None
925 }
926}
927
928impl IntoIterator for Plist {
929 type Item = PlistEntry<'static>;
930 type IntoIter = std::vec::IntoIter<PlistEntry<'static>>;
931
932 fn into_iter(self) -> Self::IntoIter {
933 self.entries.into_iter()
934 }
935}
936
937impl<'a> IntoIterator for &'a Plist {
938 type Item = &'a PlistEntry<'static>;
939 type IntoIter = std::slice::Iter<'a, PlistEntry<'static>>;
940
941 fn into_iter(self) -> Self::IntoIter {
942 self.entries.iter()
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949
950 macro_rules! plist {
954 ($s:expr) => {
955 Plist::from_bytes(String::from($s).as_bytes())
956 };
957 }
958 macro_rules! plist_entry {
959 ($s:expr) => {
960 PlistEntry::from_bytes(String::from($s).as_bytes())
961 .map(PlistEntry::into_owned)
962 };
963 }
964 macro_rules! plist_match_ok {
965 ($s:expr, $p:path) => {
966 let plist = plist_entry!($s)?;
967 assert_eq!(plist, $p);
968 };
969 }
970 macro_rules! plist_match_ok_arg {
971 ($s:expr, $p:path) => {
972 match plist_entry!($s) {
973 Ok(e) => match e {
974 $p(_) => {}
975 _ => panic!("should be a valid {} entry", stringify!($p)),
976 },
977 Err(_) => panic!("should be a valid {} entry", stringify!($p)),
978 }
979 };
980 }
981 macro_rules! plist_match_error {
982 ($s:expr, $p:path) => {
983 match plist!($s) {
984 Ok(_) => panic!("should return {} error", stringify!($p)),
985 Err(e) => match e {
986 $p(_) => {}
987 _ => panic!("should return {} error", stringify!($p)),
988 },
989 }
990 };
991 }
992
993 macro_rules! valid_utf8 {
997 ($s:expr, $p:path) => {
998 let heart = vec![240, 159, 146, 150];
1003 let oe = vec![0xf8];
1004
1005 let mut t = String::from($s).into_bytes();
1009 t.extend_from_slice(&heart);
1010 assert_eq!(PlistEntry::from_bytes(&t)?, $p(Cow::Borrowed("π")));
1011
1012 let mut t = String::from($s).into_bytes();
1016 t.extend_from_slice(&oe);
1017 match PlistEntry::from_bytes(&t) {
1018 Ok(p) => panic!(
1019 "should be an invalid {} entry, not {:?}",
1020 stringify!($p),
1021 p
1022 ),
1023 Err(e) => match e {
1024 PlistError::Utf8(_) => {}
1025 _ => panic!(
1026 "should be an invalid {} entry: {}",
1027 stringify!($p),
1028 e
1029 ),
1030 },
1031 }
1032 };
1033 }
1034
1035 macro_rules! valid_utf8_opt {
1039 ($s:expr, $p:path) => {
1040 let heart = vec![240, 159, 146, 150];
1045 let oe = vec![0xf8];
1046
1047 let mut t = String::from($s).into_bytes();
1051 t.extend_from_slice(&heart);
1052 assert_eq!(
1053 PlistEntry::from_bytes(&t)?,
1054 $p(Some(Cow::Borrowed("π")))
1055 );
1056
1057 let mut t = String::from($s).into_bytes();
1061 t.extend_from_slice(&oe);
1062 match PlistEntry::from_bytes(&t) {
1063 Ok(p) => panic!(
1064 "should be an invalid {} entry, not {:?}",
1065 stringify!($p),
1066 p
1067 ),
1068 Err(e) => match e {
1069 PlistError::Utf8(_) => {}
1070 _ => panic!(
1071 "should be an invalid {} entry: {}",
1072 stringify!($p),
1073 e
1074 ),
1075 },
1076 }
1077 };
1078 }
1079
1080 macro_rules! valid_path {
1084 ($s:expr, $p:path) => {
1085 let heart = vec![240, 159, 146, 150];
1090 let oe = vec![0xf8];
1091
1092 let mut t = String::from($s).into_bytes();
1093 t.extend_from_slice(&heart);
1094 assert_eq!(
1095 PlistEntry::from_bytes(&t)?,
1096 $p(Cow::Borrowed(Path::new("π")))
1097 );
1098 let mut t = String::from($s).into_bytes();
1099 t.extend_from_slice(&oe);
1100 match PlistEntry::from_bytes(&t) {
1101 Ok(e) => match e {
1102 $p(_) => {}
1103 _ => panic!("should be a valid {} entry", stringify!($p)),
1104 },
1105 Err(_) => panic!("should be a valid {} entry", stringify!($p)),
1106 }
1107 };
1108 }
1109
1110 macro_rules! valid_osstr {
1114 ($s:expr, $p:path) => {
1115 let heart = vec![240, 159, 146, 150];
1120 let oe = vec![0xf8];
1121
1122 let mut t = String::from($s).into_bytes();
1123 t.extend_from_slice(&heart);
1124 assert_eq!(
1125 PlistEntry::from_bytes(&t)?,
1126 $p(Cow::Borrowed(OsStr::new("π")))
1127 );
1128 let mut t = String::from($s).into_bytes();
1129 t.extend_from_slice(&oe);
1130 match PlistEntry::from_bytes(&t) {
1131 Ok(e) => match e {
1132 $p(_) => {}
1133 _ => panic!("should be a valid {} entry", stringify!($p)),
1134 },
1135 Err(_) => panic!("should be a valid {} entry", stringify!($p)),
1136 }
1137 };
1138 }
1139
1140 macro_rules! valid_osstr_opt {
1144 ($s:expr, $p:path) => {
1145 let heart = vec![240, 159, 146, 150];
1150 let oe = vec![0xf8];
1151
1152 let mut t = String::from($s).into_bytes();
1153 t.extend_from_slice(&heart);
1154 assert_eq!(
1155 PlistEntry::from_bytes(&t)?,
1156 $p(Some(Cow::Borrowed(OsStr::new("π"))))
1157 );
1158 let mut t = String::from($s).into_bytes();
1159 t.extend_from_slice(&oe);
1160 match PlistEntry::from_bytes(&t) {
1161 Ok(e) => match e {
1162 $p(_) => {}
1163 _ => panic!("should be a valid {} entry", stringify!($p)),
1164 },
1165 Err(_) => panic!("should be a valid {} entry", stringify!($p)),
1166 }
1167 };
1168 }
1169
1170 #[test]
1175 fn test_full_plist() -> Result<()> {
1176 let input = indoc! {"
1177 @comment $NetBSD$
1178 @name pkgtest-1.0
1179 @pkgdep dep-pkg1-[0-9]*
1180 @pkgdep dep-pkg2>=2.0
1181 @blddep dep-pkg1-1.0nb2
1182 @blddep dep-pkg2-2.1
1183 @pkgcfl cfl-pkg1-[0-9]*
1184 @pkgcfl cfl-pkg2>=2.0
1185 @display MESSAGE
1186 @option preserve
1187 @cwd /
1188 @src /
1189 @cd /
1190 @mode 0644
1191 @owner root
1192 @group wheel
1193 bin/foo
1194 @exec touch F=%F D=%D B=%B f=%f
1195 @unexec rm F=%F D=%D B=%B f=%f
1196 @mode
1197 @owner
1198 @group
1199 bin/bar
1200 @pkgdir /var/db/pkgsrc-rs
1201 @dirrm /var/db/pkgsrc-rs-legacy
1202 @ignore
1203 +BUILD_INFO
1204 "};
1205 let plist = Plist::from_bytes(input.as_bytes())?;
1206 assert_eq!(plist.depends().count(), 2);
1207 assert_eq!(plist.build_depends().count(), 2);
1208 assert_eq!(plist.conflicts().count(), 2);
1209 Ok(())
1210 }
1211
1212 #[test]
1222 fn test_line_input() -> Result<()> {
1223 assert_eq!(plist_entry!("@comment \n")?, PlistEntry::Comment(None));
1228 assert_eq!(plist_entry!("@mode ")?, PlistEntry::Mode(None));
1229 assert_eq!(plist_entry!("@owner \t ")?, PlistEntry::Owner(None));
1230 assert_eq!(plist_entry!("@group \t\n ")?, PlistEntry::Group(None));
1231
1232 let p1 = plist_entry!("@comment hi")?;
1236 let p2 = PlistEntry::Comment(Some(Cow::Borrowed(OsStr::new("hi"))));
1237 assert_eq!(p1, p2);
1238
1239 let p1 = plist_entry!(" @comment ")?;
1243 let p2 = PlistEntry::File(Cow::Borrowed(Path::new(" @comment")));
1244 assert_eq!(p1, p2);
1245
1246 Ok(())
1247 }
1248
1249 #[test]
1253 fn test_utf8() -> Result<()> {
1254 valid_utf8_opt!("@mode ", PlistEntry::Mode);
1255 valid_utf8_opt!("@owner ", PlistEntry::Owner);
1256 valid_utf8_opt!("@group ", PlistEntry::Group);
1257
1258 valid_utf8!("@name ", PlistEntry::Name);
1259 valid_utf8!("@pkgdep ", PlistEntry::PkgDep);
1260 valid_utf8!("@blddep ", PlistEntry::BldDep);
1261 valid_utf8!("@pkgcfl ", PlistEntry::PkgCfl);
1262
1263 Ok(())
1264 }
1265
1266 #[test]
1272 fn test_8859() -> Result<()> {
1273 valid_path!("", PlistEntry::File);
1274 valid_path!("@cwd ", PlistEntry::Cwd);
1275 valid_osstr!("@exec ", PlistEntry::Exec);
1276 valid_osstr!("@unexec ", PlistEntry::UnExec);
1277 valid_path!("@pkgdir ", PlistEntry::PkgDir);
1278 valid_path!("@dirrm ", PlistEntry::DirRm);
1279 valid_path!("@display ", PlistEntry::Display);
1280 valid_osstr_opt!("@comment ", PlistEntry::Comment);
1281
1282 Ok(())
1283 }
1284
1285 #[test]
1289 fn test_args() -> Result<()> {
1290 plist_match_ok!("@ignore", PlistEntry::Ignore);
1294 plist_match_error!("@ignore hi", PlistError::IncorrectArguments);
1295
1296 plist_match_ok_arg!("@cwd /cwd", PlistEntry::Cwd);
1300 plist_match_ok_arg!("@src /cwd", PlistEntry::Cwd);
1301 plist_match_ok_arg!("@cd /cwd", PlistEntry::Cwd);
1302 plist_match_ok_arg!("@exec echo hi", PlistEntry::Exec);
1303 plist_match_ok_arg!("@unexec echo lo", PlistEntry::UnExec);
1304 plist_match_ok_arg!("@name pkgname", PlistEntry::Name);
1305 plist_match_ok_arg!("@pkgdir /dirname", PlistEntry::PkgDir);
1306 plist_match_ok_arg!("@dirrm /dirname", PlistEntry::DirRm);
1307 plist_match_ok_arg!("@display MESSAGE", PlistEntry::Display);
1308 plist_match_ok_arg!("@pkgdep pkgname", PlistEntry::PkgDep);
1309 plist_match_ok_arg!("@blddep pkgname", PlistEntry::BldDep);
1310 plist_match_ok_arg!("@pkgcfl pkgname", PlistEntry::PkgCfl);
1311 plist_match_error!("@cwd", PlistError::IncorrectArguments);
1312 plist_match_error!("@src", PlistError::IncorrectArguments);
1313 plist_match_error!("@cd", PlistError::IncorrectArguments);
1314 plist_match_error!("@exec", PlistError::IncorrectArguments);
1315 plist_match_error!("@unexec", PlistError::IncorrectArguments);
1316 plist_match_error!("@name", PlistError::IncorrectArguments);
1317 plist_match_error!("@pkgdir", PlistError::IncorrectArguments);
1318 plist_match_error!("@dirrm", PlistError::IncorrectArguments);
1319 plist_match_error!("@display", PlistError::IncorrectArguments);
1320 plist_match_error!("@pkgdep", PlistError::IncorrectArguments);
1321 plist_match_error!("@blddep", PlistError::IncorrectArguments);
1322 plist_match_error!("@pkgcfl", PlistError::IncorrectArguments);
1323
1324 plist_match_ok_arg!("@comment", PlistEntry::Comment);
1328 plist_match_ok_arg!("@comment hi there", PlistEntry::Comment);
1329 plist_match_ok_arg!("@mode", PlistEntry::Mode);
1330 plist_match_ok_arg!("@mode 0644", PlistEntry::Mode);
1331 plist_match_ok_arg!("@owner", PlistEntry::Owner);
1332 plist_match_ok_arg!("@owner root", PlistEntry::Owner);
1333 plist_match_ok_arg!("@group", PlistEntry::Group);
1334 plist_match_ok_arg!("@group wheel", PlistEntry::Group);
1335
1336 plist_match_ok_arg!("@option preserve", PlistEntry::PkgOpt);
1340 plist_match_error!("@option", PlistError::IncorrectArguments);
1341 plist_match_error!("@option invalid", PlistError::UnsupportedCommand);
1342
1343 Ok(())
1344 }
1345
1346 #[test]
1350 fn test_vecs() -> Result<()> {
1351 let plist = plist!("@pkgdir one\n@pkgdir two\n@pkgdir three")?;
1352 assert_eq!(
1353 plist.pkgdirs().collect::<Vec<_>>(),
1354 ["one", "two", "three"].map(Path::new)
1355 );
1356
1357 let plist = plist!("@dirrm one\n@dirrm two\n@dirrm three")?;
1358 assert_eq!(
1359 plist.pkgrmdirs().collect::<Vec<_>>(),
1360 ["one", "two", "three"].map(Path::new)
1361 );
1362
1363 let plist = plist!("@pkgdep one\n@pkgdep two\n@pkgdep three")?;
1364 assert_eq!(
1365 plist.depends().collect::<Vec<_>>(),
1366 ["one", "two", "three"]
1367 );
1368
1369 let plist = plist!("@blddep one\n@blddep two\n@blddep three")?;
1370 assert_eq!(
1371 plist.build_depends().collect::<Vec<_>>(),
1372 ["one", "two", "three"]
1373 );
1374
1375 let plist = plist!("@pkgcfl one\n@pkgcfl two\n@pkgcfl three")?;
1376 assert_eq!(
1377 plist.conflicts().collect::<Vec<_>>(),
1378 ["one", "two", "three"]
1379 );
1380
1381 Ok(())
1382 }
1383
1384 #[test]
1388 fn test_files() -> Result<()> {
1389 let input = indoc! {"
1390 @cwd /opt/pkg
1391 bin/good
1392 @cwd /
1393 bin/evil
1394 @ignore
1395 @cwd /tmp
1396 +IGNORE_ME
1397 @cwd /opt/pkg
1398 bin/ok
1399 "};
1400 let plist = Plist::from_bytes(input.as_bytes())?;
1401 let files: Vec<&Path> = plist.files().collect();
1402 assert_eq!(files, ["bin/good", "bin/evil", "bin/ok"].map(Path::new));
1403 let prefixed: Vec<PathBuf> = plist.files_prefixed().collect();
1404 assert_eq!(
1405 prefixed,
1406 ["/opt/pkg/bin/good", "/bin/evil", "/opt/pkg/bin/ok"]
1407 .map(PathBuf::from)
1408 );
1409
1410 let plist = Plist::from_bytes(b"bin/relative\n")?;
1411 let files: Vec<&Path> = plist.files().collect();
1412 assert_eq!(files, [Path::new("bin/relative")]);
1413 let prefixed: Vec<PathBuf> = plist.files_prefixed().collect();
1414 assert_eq!(prefixed, [PathBuf::from("bin/relative")]);
1415 Ok(())
1416 }
1417
1418 #[test]
1422 fn test_first_match() -> Result<()> {
1423 let plist = plist!("@comment not a pkgname")?;
1424 assert_eq!(plist.pkgname(), None);
1425
1426 let plist = plist!("@name one\n@name two\n@name three")?;
1427 assert_eq!(plist.pkgname(), Some("one"));
1428
1429 let plist = plist!("@comment not a display")?;
1430 assert_eq!(plist.display(), None);
1431
1432 let plist = plist!("@display one\n@display two\n@display three")?;
1433 assert_eq!(plist.display(), Some(Path::new("one")));
1434
1435 Ok(())
1436 }
1437
1438 #[test]
1442 fn test_preserve() -> Result<()> {
1443 assert!(!plist!("@comment not set")?.is_preserve());
1444 assert!(plist!("@option preserve")?.is_preserve());
1445
1446 Ok(())
1447 }
1448
1449 #[test]
1453 fn test_file_checksum() -> Result<()> {
1454 let entry =
1456 plist_entry!("@comment MD5:d41d8cd98f00b204e9800998ecf8427e")?;
1457 assert_eq!(
1458 entry,
1459 PlistEntry::FileChecksum(Cow::Borrowed(
1460 "d41d8cd98f00b204e9800998ecf8427e"
1461 ))
1462 );
1463
1464 let entry = plist_entry!("@comment MD5:abc123")?;
1466 assert!(matches!(entry, PlistEntry::Comment(_)));
1467
1468 let entry =
1470 plist_entry!("@comment MD5:d41d8cd98f00b204e9800998ecf8427g")?;
1471 assert!(matches!(entry, PlistEntry::Comment(_)));
1472
1473 let entry = plist_entry!("@comment This is a comment")?;
1475 assert!(matches!(entry, PlistEntry::Comment(_)));
1476
1477 Ok(())
1478 }
1479
1480 #[test]
1484 fn test_symlink_target() -> Result<()> {
1485 let entry = plist_entry!("@comment Symlink:/usr/bin/target")?;
1486 assert_eq!(
1487 entry,
1488 PlistEntry::SymlinkTarget(Cow::Borrowed(Path::new(
1489 "/usr/bin/target"
1490 )))
1491 );
1492
1493 let entry = plist_entry!("@comment Symlink:")?;
1495 assert_eq!(
1496 entry,
1497 PlistEntry::SymlinkTarget(Cow::Borrowed(Path::new("")))
1498 );
1499
1500 Ok(())
1501 }
1502
1503 #[test]
1507 fn test_files_with_info() -> Result<()> {
1508 let input = indoc! {"
1509 @mode 0755
1510 @owner root
1511 @group wheel
1512 bin/myapp
1513 @comment MD5:d41d8cd98f00b204e9800998ecf8427e
1514 @mode 0644
1515 etc/myapp.conf
1516 @comment MD5:098f6bcd4621d373cade4e832627b4f6
1517 lib/libfoo.so
1518 @comment Symlink:libfoo.so.1
1519 @ignore
1520 +BUILD_INFO
1521 "};
1522
1523 let plist = Plist::from_bytes(input.as_bytes())?;
1524 let files: Vec<FileInfo> = plist.files_with_info().collect();
1525
1526 assert_eq!(files.len(), 3);
1527
1528 assert_eq!(files[0].path, PathBuf::from("bin/myapp"));
1530 assert_eq!(
1531 files[0].checksum,
1532 Some("d41d8cd98f00b204e9800998ecf8427e".to_string())
1533 );
1534 assert_eq!(files[0].symlink_target, None);
1535 assert_eq!(files[0].mode, Some("0755".to_string()));
1536 assert_eq!(files[0].owner, Some("root".to_string()));
1537 assert_eq!(files[0].group, Some("wheel".to_string()));
1538
1539 assert_eq!(files[1].path, PathBuf::from("etc/myapp.conf"));
1541 assert_eq!(
1542 files[1].checksum,
1543 Some("098f6bcd4621d373cade4e832627b4f6".to_string())
1544 );
1545 assert_eq!(files[1].mode, Some("0644".to_string()));
1546
1547 assert_eq!(files[2].path, PathBuf::from("lib/libfoo.so"));
1549 assert_eq!(files[2].checksum, None);
1550 assert_eq!(files[2].symlink_target, Some(PathBuf::from("libfoo.so.1")));
1551
1552 Ok(())
1553 }
1554
1555 #[test]
1556 fn test_into_iterator() -> Result<()> {
1557 let plist =
1558 plist!("@name pkg-1.0\nbin/foo\n@pkgdep dep-[0-9]*\nbin/bar")?;
1559
1560 let entries: Vec<_> = plist.into_iter().collect();
1561 assert_eq!(entries.len(), 4);
1562 assert!(matches!(entries[0], PlistEntry::Name(_)));
1563 assert!(matches!(entries[1], PlistEntry::File(_)));
1564 assert!(matches!(entries[2], PlistEntry::PkgDep(_)));
1565 assert!(matches!(entries[3], PlistEntry::File(_)));
1566
1567 Ok(())
1568 }
1569
1570 #[test]
1571 fn test_iter_by_ref() -> Result<()> {
1572 let plist = plist!("@name pkg-1.0\nbin/foo\nbin/bar")?;
1573
1574 let file_count = (&plist)
1575 .into_iter()
1576 .filter(|e| matches!(e, PlistEntry::File(_)))
1577 .count();
1578 assert_eq!(file_count, 2);
1579
1580 assert_eq!(plist.pkgname(), Some("pkg-1.0"));
1582
1583 Ok(())
1584 }
1585
1586 #[test]
1591 fn test_parse_iter() -> Result<()> {
1592 let input = b"@name pkg-1.0\nbin/foo\n@pkgdir /var/db/x\n";
1593 let entries: Vec<_> = parse(input).collect::<Result<Vec<_>>>()?;
1594 assert_eq!(entries.len(), 3);
1595 assert_eq!(entries[0], PlistEntry::Name(Cow::Borrowed("pkg-1.0")));
1596 assert_eq!(
1597 entries[1],
1598 PlistEntry::File(Cow::Borrowed(Path::new("bin/foo")))
1599 );
1600 assert_eq!(
1601 entries[2],
1602 PlistEntry::PkgDir(Cow::Borrowed(Path::new("/var/db/x")))
1603 );
1604 Ok(())
1605 }
1606
1607 #[test]
1608 fn test_parse_iter_no_trailing_newline() -> Result<()> {
1609 let input = b"@name pkg-1.0\nbin/foo";
1610 let entries: Vec<_> = parse(input).collect::<Result<Vec<_>>>()?;
1611 assert_eq!(entries.len(), 2);
1612 Ok(())
1613 }
1614
1615 #[test]
1616 fn test_parse_iter_skips_blanks() -> Result<()> {
1617 let input = b"@name pkg-1.0\n\n \nbin/foo\n";
1618 let entries: Vec<_> = parse(input).collect::<Result<Vec<_>>>()?;
1619 assert_eq!(entries.len(), 2);
1620 Ok(())
1621 }
1622
1623 #[test]
1624 fn test_parse_comment_special_forms() -> Result<()> {
1625 let entries: Vec<_> = parse(
1626 b"@comment MD5:d41d8cd98f00b204e9800998ecf8427e\n\
1627 @comment Symlink:/usr/bin/target\n\
1628 @comment plain comment\n",
1629 )
1630 .collect::<Result<Vec<_>>>()?;
1631 assert_eq!(
1632 entries[0],
1633 PlistEntry::FileChecksum(Cow::Borrowed(
1634 "d41d8cd98f00b204e9800998ecf8427e"
1635 ))
1636 );
1637 assert_eq!(
1638 entries[1],
1639 PlistEntry::SymlinkTarget(Cow::Borrowed(Path::new(
1640 "/usr/bin/target"
1641 )))
1642 );
1643 assert!(matches!(entries[2], PlistEntry::Comment(Some(_))));
1644 Ok(())
1645 }
1646
1647 #[test]
1653 fn test_parse_validates_utf8() -> Result<()> {
1654 let input = b"@name \xff-bad\nbin/foo\n";
1655 match parse(input).next() {
1656 Some(Err(PlistError::Utf8(_))) => Ok(()),
1657 other => panic!("expected Utf8 error from parse(), got {other:?}"),
1658 }
1659 }
1660
1661 #[test]
1666 fn test_into_owned() -> Result<()> {
1667 let owned: PlistEntry<'static> = {
1668 let bytes: Vec<u8> = b"@name pkg-1.0".to_vec();
1669 PlistEntry::from_bytes(&bytes)?.into_owned()
1670 };
1671 assert_eq!(owned, PlistEntry::Name(Cow::Owned("pkg-1.0".to_owned())));
1672 Ok(())
1673 }
1674}