1use std::{collections::HashSet, fmt::Display, path::PathBuf, str::FromStr};
6
7use alpm_common::relative_files;
8use alpm_types::{Md5Checksum, RelativeFilePath, RelativePath};
9use fluent_i18n::t;
10use winnow::{
11 ModalResult,
12 Parser,
13 ascii::{line_ending, multispace0, space1, till_line_ending},
14 combinator::{alt, cut_err, eof, fail, not, opt, repeat, separated_pair, terminated},
15 error::{StrContext, StrContextValue},
16 token::take_while,
17};
18
19use crate::files::Error;
20
21#[derive(Debug)]
25pub(crate) struct FilesSection(Vec<RelativePath>);
26
27impl FilesSection {
28 pub(crate) const SECTION_KEYWORD: &str = "%FILES%";
30
31 fn parse_path(input: &mut &str) -> ModalResult<RelativePath> {
43 alt((
46 (space1, line_ending)
47 .take()
48 .and_then(cut_err(fail))
49 .context(StrContext::Expected(StrContextValue::Description(
50 "relative path not consisting of whitespaces and/or tabs",
51 ))),
52 till_line_ending,
53 ))
54 .verify(|s: &str| !s.is_empty())
55 .context(StrContext::Label("relative path"))
56 .parse_to()
57 .parse_next(input)
58 }
59
60 pub(crate) fn parser(input: &mut &str) -> ModalResult<Self> {
73 if input.is_empty() {
76 return Ok(Self(Vec::new()));
77 }
78
79 cut_err(terminated(Self::SECTION_KEYWORD, alt((line_ending, eof))))
82 .context(StrContext::Label("alpm-db-files section header"))
83 .context(StrContext::Expected(StrContextValue::Description(
84 Self::SECTION_KEYWORD,
85 )))
86 .parse_next(input)?;
87
88 if input.is_empty() {
90 return Ok(Self(Vec::new()));
91 }
92
93 let paths: Vec<RelativePath> =
96 repeat(0.., terminated(Self::parse_path, alt((line_ending, eof)))).parse_next(input)?;
97
98 multispace0.parse_next(input)?;
100
101 if input.is_empty() || input.starts_with(BackupSection::SECTION_KEYWORD) {
103 return Ok(Self(paths));
104 }
105
106 let _opt: Option<&str> =
108 opt(not(eof)
109 .take()
110 .and_then(cut_err(fail).context(StrContext::Expected(
111 StrContextValue::Description("no further path after newline"),
112 ))))
113 .parse_next(input)?;
114
115 Ok(Self(paths))
116 }
117
118 pub fn paths(self) -> Vec<PathBuf> {
120 self.0.into_iter().map(RelativePath::into_inner).collect()
121 }
122}
123
124#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
126pub struct BackupEntry {
127 pub path: RelativeFilePath,
129 pub md5: Md5Checksum,
131}
132
133impl BackupEntry {
134 pub(crate) fn parser(input: &mut &str) -> ModalResult<Option<Self>> {
151 let mut line = till_line_ending.parse_next(input)?;
152 separated_pair(
153 take_while(1.., |c: char| c != '\t' && c != '\n' && c != '\r')
154 .verify(|s: &str| !s.chars().all(|c| c.is_whitespace()))
155 .context(StrContext::Label("relative path"))
156 .parse_to(),
157 '\t',
158 alt((
159 "(null)".value(None),
164 Md5Checksum::parser.map(Some),
165 )),
166 )
167 .map(|(path, md5)| md5.map(|md5| BackupEntry { path, md5 }))
168 .parse_next(&mut line)
169 }
170}
171
172#[derive(Debug)]
176pub(crate) struct BackupSection(Vec<BackupEntry>);
177
178impl BackupSection {
179 pub(crate) const SECTION_KEYWORD: &str = "%BACKUP%";
181
182 pub(crate) fn parser(input: &mut &str) -> ModalResult<Self> {
189 cut_err(terminated(Self::SECTION_KEYWORD, alt((line_ending, eof))))
190 .context(StrContext::Label("alpm-db-files backup section header"))
191 .context(StrContext::Expected(StrContextValue::Description(
192 Self::SECTION_KEYWORD,
193 )))
194 .parse_next(input)?;
195
196 if input.is_empty() {
197 return Ok(Self(Vec::new()));
198 }
199
200 let entries: Vec<BackupEntry> = repeat(
201 0..,
202 terminated(BackupEntry::parser, alt((line_ending, eof))),
203 )
204 .map(|entries: Vec<Option<BackupEntry>>| entries.into_iter().flatten().collect::<Vec<_>>())
205 .parse_next(input)?;
206
207 multispace0.parse_next(input)?;
209
210 let _opt: Option<&str> =
212 opt(not(eof)
213 .take()
214 .and_then(cut_err(fail).context(StrContext::Expected(
215 StrContextValue::Description("no further backup entry after newline"),
216 ))))
217 .parse_next(input)?;
218
219 Ok(Self(entries))
220 }
221
222 pub fn entries(self) -> Vec<BackupEntry> {
224 self.0
225 }
226}
227
228#[derive(Clone, Debug, Eq, PartialEq)]
233pub(crate) struct FilesV1PathErrors {
234 pub(crate) absolute: HashSet<PathBuf>,
235 pub(crate) without_parent: HashSet<PathBuf>,
236 pub(crate) duplicate: HashSet<PathBuf>,
237}
238
239impl FilesV1PathErrors {
240 pub(crate) fn new() -> Self {
242 Self {
243 absolute: HashSet::new(),
244 without_parent: HashSet::new(),
245 duplicate: HashSet::new(),
246 }
247 }
248
249 pub(crate) fn add_absolute(&mut self, path: PathBuf) -> bool {
251 self.absolute.insert(path)
252 }
253
254 pub(crate) fn add_without_parent(&mut self, path: PathBuf) -> bool {
256 self.without_parent.insert(path)
257 }
258
259 pub(crate) fn add_duplicate(&mut self, path: PathBuf) -> bool {
261 self.duplicate.insert(path)
262 }
263
264 pub(crate) fn fail(&self) -> Result<(), Error> {
266 if !(self.absolute.is_empty()
267 && self.without_parent.is_empty()
268 && self.duplicate.is_empty())
269 {
270 Err(Error::InvalidFilesPaths {
271 message: self.to_string(),
272 })
273 } else {
274 Ok(())
275 }
276 }
277}
278
279impl Display for FilesV1PathErrors {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 fn write_invalid_set(
282 f: &mut std::fmt::Formatter<'_>,
283 message: String,
284 set: &HashSet<PathBuf>,
285 ) -> std::fmt::Result {
286 if !set.is_empty() {
287 writeln!(f, "{message}:")?;
288 let mut set = set.iter().collect::<Vec<_>>();
289 set.sort();
290 for path in set.iter() {
291 writeln!(f, "{}", path.as_path().display())?;
292 }
293 }
294 Ok(())
295 }
296
297 write_invalid_set(f, t!("filesv1-path-errors-absolute-paths"), &self.absolute)?;
298 write_invalid_set(
299 f,
300 t!("filesv1-path-errors-paths-without-a-parent"),
301 &self.without_parent,
302 )?;
303 write_invalid_set(
304 f,
305 t!("filesv1-path-errors-duplicate-paths"),
306 &self.duplicate,
307 )?;
308
309 Ok(())
310 }
311}
312
313#[derive(Clone, Debug, Eq, PartialEq)]
318pub(crate) struct BackupV1Errors {
319 pub(crate) not_in_files: HashSet<RelativeFilePath>,
320 pub(crate) duplicate: HashSet<RelativeFilePath>,
321}
322
323impl BackupV1Errors {
324 pub(crate) fn new() -> Self {
326 Self {
327 not_in_files: HashSet::new(),
328 duplicate: HashSet::new(),
329 }
330 }
331
332 pub(crate) fn add_not_in_files(&mut self, path: RelativeFilePath) -> bool {
334 self.not_in_files.insert(path)
335 }
336
337 pub(crate) fn add_duplicate(&mut self, path: RelativeFilePath) -> bool {
339 self.duplicate.insert(path)
340 }
341
342 pub(crate) fn fail(&self) -> Result<(), Error> {
344 if !(self.not_in_files.is_empty() && self.duplicate.is_empty()) {
345 Err(Error::InvalidBackupEntries {
346 message: self.to_string(),
347 })
348 } else {
349 Ok(())
350 }
351 }
352}
353
354impl Display for BackupV1Errors {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 fn write_invalid_set(
357 f: &mut std::fmt::Formatter<'_>,
358 message: String,
359 set: &HashSet<RelativeFilePath>,
360 ) -> std::fmt::Result {
361 if !set.is_empty() {
362 writeln!(f, "{message}:")?;
363 let mut set = set.iter().collect::<Vec<_>>();
364 set.sort_by(|a, b| a.inner().cmp(b.inner()));
365 for path in set.iter() {
366 writeln!(f, "{path}")?;
367 }
368 }
369 Ok(())
370 }
371
372 write_invalid_set(
373 f,
374 t!("backupv1-errors-not-in-files-section"),
375 &self.not_in_files,
376 )?;
377 write_invalid_set(f, t!("backupv1-errors-duplicate-paths"), &self.duplicate)?;
378
379 Ok(())
380 }
381}
382
383#[derive(Clone, Debug, serde::Serialize)]
387pub struct DbFilesV1 {
388 files: Vec<PathBuf>,
389 #[serde(default)]
390 #[serde(skip_serializing_if = "Vec::is_empty")]
391 backup: Vec<BackupEntry>,
392}
393
394impl AsRef<[PathBuf]> for DbFilesV1 {
395 fn as_ref(&self) -> &[PathBuf] {
397 &self.files
398 }
399}
400
401impl DbFilesV1 {
402 pub fn backups(&self) -> &[BackupEntry] {
404 &self.backup
405 }
406
407 fn try_from_parts(
408 mut paths: Vec<PathBuf>,
409 mut backup: Vec<BackupEntry>,
410 ) -> Result<Self, Error> {
411 paths.sort_unstable();
412
413 let mut errors = FilesV1PathErrors::new();
414 let mut path_set = HashSet::new();
415 let empty_parent = PathBuf::from("");
416 let root_parent = PathBuf::from("/");
417
418 for path in paths.iter() {
419 let path = path.as_path();
420
421 if path.is_absolute() {
423 errors.add_absolute(path.to_path_buf());
424 }
425
426 if let Some(parent) = path.parent()
428 && parent != empty_parent
429 && parent != root_parent
430 && !path_set.contains(parent)
431 {
432 errors.add_without_parent(path.to_path_buf());
433 }
434
435 if !path_set.insert(path.to_path_buf()) {
437 errors.add_duplicate(path.to_path_buf());
438 }
439 }
440
441 errors.fail()?;
442
443 let mut backup_errors = BackupV1Errors::new();
444 let mut backup_set: HashSet<RelativeFilePath> = HashSet::new();
445
446 for entry in backup.iter() {
447 if !path_set.contains(entry.path.inner()) {
448 backup_errors.add_not_in_files(entry.path.clone());
449 }
450
451 if !backup_set.insert(entry.path.clone()) {
452 backup_errors.add_duplicate(entry.path.clone());
453 }
454 }
455
456 backup_errors.fail()?;
457
458 backup.sort_unstable_by(|a, b| a.path.inner().cmp(b.path.inner()));
459
460 Ok(Self {
461 files: paths,
462 backup,
463 })
464 }
465}
466
467impl Display for DbFilesV1 {
468 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500 if self.files.is_empty() && self.backup.is_empty() {
502 return Ok(());
503 }
504
505 writeln!(f, "{}", FilesSection::SECTION_KEYWORD)?;
507
508 for path in &self.files {
509 writeln!(f, "{}", path.to_string_lossy())?;
510 }
511
512 writeln!(f)?;
514
515 if !self.backup.is_empty() {
517 writeln!(f, "{}", BackupSection::SECTION_KEYWORD)?;
518
519 for entry in &self.backup {
520 writeln!(f, "{}\t{}", entry.path, entry.md5)?;
521 }
522 }
523
524 Ok(())
525 }
526}
527
528impl FromStr for DbFilesV1 {
529 type Err = Error;
530
531 fn from_str(s: &str) -> Result<Self, Self::Err> {
595 let (files_section, backup_section) =
596 (|input: &mut &str| -> ModalResult<(FilesSection, BackupSection)> {
597 let files_section = FilesSection::parser.parse_next(input)?;
598 let backup_section = if input.is_empty() {
599 BackupSection(Vec::new())
600 } else {
601 BackupSection::parser.parse_next(input)?
602 };
603 Ok((files_section, backup_section))
604 })
605 .parse(s)?;
606
607 DbFilesV1::try_from_parts(files_section.paths(), backup_section.entries())
608 }
609}
610
611impl TryFrom<PathBuf> for DbFilesV1 {
612 type Error = Error;
613
614 fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
659 DbFilesV1::try_from_parts(relative_files(value, &[])?, Vec::new())
660 }
661}
662
663impl TryFrom<Vec<PathBuf>> for DbFilesV1 {
664 type Error = Error;
665
666 fn try_from(value: Vec<PathBuf>) -> Result<Self, Self::Error> {
718 DbFilesV1::try_from_parts(value, Vec::new())
719 }
720}
721
722impl TryFrom<(Vec<PathBuf>, Vec<BackupEntry>)> for DbFilesV1 {
723 type Error = Error;
724
725 fn try_from(value: (Vec<PathBuf>, Vec<BackupEntry>)) -> Result<Self, Self::Error> {
727 let (paths, backup) = value;
728 DbFilesV1::try_from_parts(paths, backup)
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use std::{
735 fs::{File, create_dir_all},
736 str::FromStr,
737 };
738
739 use alpm_types::{Md5Checksum, RelativeFilePath};
740 use rstest::rstest;
741 use tempfile::tempdir;
742 use testresult::TestResult;
743
744 use super::*;
745
746 #[test]
748 fn filesv1_try_from_pathbuf_succeeds() -> TestResult {
749 let temp_dir = tempdir()?;
750 let path = temp_dir.path();
751 create_dir_all(path.join("usr/bin/"))?;
752 File::create(path.join("usr/bin/foo"))?;
753
754 let files = DbFilesV1::try_from(path.to_path_buf())?;
755
756 assert_eq!(
757 files.as_ref(),
758 vec![
759 PathBuf::from("usr/"),
760 PathBuf::from("usr/bin/"),
761 PathBuf::from("usr/bin/foo")
762 ]
763 );
764
765 Ok(())
766 }
767
768 #[rstest]
769 #[case::dirs_and_files(vec![PathBuf::from("usr/"), PathBuf::from("usr/bin/"), PathBuf::from("usr/bin/foo")], 3)]
770 #[case::empty(Vec::new(), 0)]
771 fn filesv1_try_from_pathbufs_succeeds(
772 #[case] paths: Vec<PathBuf>,
773 #[case] len: usize,
774 ) -> TestResult {
775 let files = DbFilesV1::try_from(paths)?;
776
777 assert_eq!(files.as_ref().len(), len);
778
779 Ok(())
780 }
781
782 #[rstest]
783 #[case::absolute_paths(
784 vec![
785 PathBuf::from("/usr/"), PathBuf::from("/usr/bin/"), PathBuf::from("/usr/bin/foo")
786 ],
787 FilesV1PathErrors{
788 absolute: HashSet::from_iter([
789 PathBuf::from("/usr/"),
790 PathBuf::from("/usr/bin/"),
791 PathBuf::from("/usr/bin/foo"),
792 ]),
793 without_parent: HashSet::new(),
794 duplicate: HashSet::new(),
795 }
796 )]
797 #[case::without_parents(
798 vec![PathBuf::from("usr/bin/"), PathBuf::from("usr/bin/foo")],
799 FilesV1PathErrors{
800 absolute: HashSet::new(),
801 without_parent: HashSet::from_iter([
802 PathBuf::from("usr/bin/"),
803 ]),
804 duplicate: HashSet::new(),
805 }
806 )]
807 #[case::duplicates(
808 vec![PathBuf::from("usr/"), PathBuf::from("usr/")],
809 FilesV1PathErrors{
810 absolute: HashSet::new(),
811 without_parent: HashSet::new(),
812 duplicate: HashSet::from_iter([
813 PathBuf::from("usr/"),
814 ]),
815 }
816 )]
817 fn filesv1_try_from_pathbufs_fails(
818 #[case] paths: Vec<PathBuf>,
819 #[case] expected_errors: FilesV1PathErrors,
820 ) -> TestResult {
821 let result = DbFilesV1::try_from(paths);
822 let errors = match result {
823 Ok(files) => panic!(
824 "Should have failed with an Error::InvalidFilesPaths, but succeeded to create a DbFilesV1: {files:?}"
825 ),
826 Err(Error::InvalidFilesPaths { message }) => message,
827 Err(error) => panic!("Expected an Error::InvalidFilesPaths, but got: {error}"),
828 };
829
830 eprintln!("{errors}");
831 assert_eq!(errors, expected_errors.to_string());
832
833 Ok(())
834 }
835
836 #[test]
837 fn filesv1_try_from_paths_and_backups_succeeds() -> TestResult {
838 let paths = vec![
839 PathBuf::from("usr/"),
840 PathBuf::from("usr/bin/"),
841 PathBuf::from("usr/bin/foo"),
842 ];
843 let backup = vec![BackupEntry {
844 path: RelativeFilePath::from_str("usr/bin/foo")?,
845 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e")?,
846 }];
847
848 let files = DbFilesV1::try_from((paths, backup))?;
849
850 assert_eq!(files.backups().len(), 1);
851
852 Ok(())
853 }
854
855 #[rstest]
856 #[case::backup_not_in_files(
857 vec![PathBuf::from("usr/")],
858 vec![BackupEntry {
859 path: RelativeFilePath::from_str("usr/bin/foo").unwrap(),
860 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e").unwrap(),
861 }],
862 BackupV1Errors{
863 not_in_files: HashSet::from_iter([RelativeFilePath::from_str("usr/bin/foo").unwrap()]),
864 duplicate: HashSet::new(),
865 }
866 )]
867 #[case::duplicate_backup_entries(
868 vec![
869 PathBuf::from("usr/"),
870 PathBuf::from("usr/bin/"),
871 PathBuf::from("usr/bin/foo")
872 ],
873 vec![
874 BackupEntry {
875 path: RelativeFilePath::from_str("usr/bin/foo").unwrap(),
876 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e").unwrap(),
877 },
878 BackupEntry {
879 path: RelativeFilePath::from_str("usr/bin/foo").unwrap(),
880 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e").unwrap(),
881 }
882 ],
883 BackupV1Errors{
884 not_in_files: HashSet::new(),
885 duplicate: HashSet::from_iter([RelativeFilePath::from_str("usr/bin/foo").unwrap()]),
886 }
887 )]
888 fn filesv1_try_from_paths_and_backups_fails(
889 #[case] paths: Vec<PathBuf>,
890 #[case] backup: Vec<BackupEntry>,
891 #[case] expected_errors: BackupV1Errors,
892 ) -> TestResult {
893 let result = DbFilesV1::try_from((paths, backup));
894 let errors = match result {
895 Ok(files) => panic!(
896 "Should have failed with an Error::InvalidBackupEntries, but succeeded to create a DbFilesV1: {files:?}"
897 ),
898 Err(Error::InvalidBackupEntries { message }) => message,
899 Err(error) => panic!("Expected an Error::InvalidBackupEntries, but got: {error}"),
900 };
901
902 eprintln!("{errors}");
903 assert_eq!(errors, expected_errors.to_string());
904
905 Ok(())
906 }
907
908 #[test]
909 fn filesv1_from_str_rejects_absolute_paths() -> TestResult {
910 let data = "%FILES%\n/usr/bin/foo\n";
911
912 match DbFilesV1::from_str(data) {
913 Err(Error::ParseError(_)) => Ok(()),
914 Err(error) => panic!("expected ParseError, got {error}"),
915 Ok(files) => panic!("expected parse failure, got {files:?}"),
916 }
917 }
918
919 #[test]
920 fn filesv1_from_str_skips_null_backup_entries() -> TestResult {
921 let data = r#"%FILES%
922etc/
923etc/foo/
924etc/foo/foo.conf
925
926%BACKUP%
927etc/foo/foo.conf d41d8cd98f00b204e9800998ecf8427e
928etc/foo/bar.conf (null)
929"#;
930
931 let files = DbFilesV1::from_str(data)?;
932
933 assert_eq!(
934 files.backups(),
935 &[BackupEntry {
936 path: RelativeFilePath::from_str("etc/foo/foo.conf")?,
937 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e")?
938 }]
939 );
940
941 Ok(())
942 }
943}