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