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 if parent != empty_parent && parent != root_parent && !path_set.contains(parent) {
429 errors.add_without_parent(path.to_path_buf());
430 }
431 }
432
433 if !path_set.insert(path.to_path_buf()) {
435 errors.add_duplicate(path.to_path_buf());
436 }
437 }
438
439 errors.fail()?;
440
441 let mut backup_errors = BackupV1Errors::new();
442 let mut backup_set: HashSet<RelativeFilePath> = HashSet::new();
443
444 for entry in backup.iter() {
445 if !path_set.contains(entry.path.inner()) {
446 backup_errors.add_not_in_files(entry.path.clone());
447 }
448
449 if !backup_set.insert(entry.path.clone()) {
450 backup_errors.add_duplicate(entry.path.clone());
451 }
452 }
453
454 backup_errors.fail()?;
455
456 backup.sort_unstable_by(|a, b| a.path.inner().cmp(b.path.inner()));
457
458 Ok(Self {
459 files: paths,
460 backup,
461 })
462 }
463}
464
465impl Display for DbFilesV1 {
466 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498 if self.files.is_empty() && self.backup.is_empty() {
500 return Ok(());
501 }
502
503 writeln!(f, "{}", FilesSection::SECTION_KEYWORD)?;
505
506 for path in &self.files {
507 writeln!(f, "{}", path.to_string_lossy())?;
508 }
509
510 writeln!(f)?;
512
513 if !self.backup.is_empty() {
515 writeln!(f, "{}", BackupSection::SECTION_KEYWORD)?;
516
517 for entry in &self.backup {
518 writeln!(f, "{}\t{}", entry.path, entry.md5)?;
519 }
520 }
521
522 Ok(())
523 }
524}
525
526impl FromStr for DbFilesV1 {
527 type Err = Error;
528
529 fn from_str(s: &str) -> Result<Self, Self::Err> {
593 let (files_section, backup_section) =
594 (|input: &mut &str| -> ModalResult<(FilesSection, BackupSection)> {
595 let files_section = FilesSection::parser.parse_next(input)?;
596 let backup_section = if input.is_empty() {
597 BackupSection(Vec::new())
598 } else {
599 BackupSection::parser.parse_next(input)?
600 };
601 Ok((files_section, backup_section))
602 })
603 .parse(s)?;
604
605 DbFilesV1::try_from_parts(files_section.paths(), backup_section.entries())
606 }
607}
608
609impl TryFrom<PathBuf> for DbFilesV1 {
610 type Error = Error;
611
612 fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
657 DbFilesV1::try_from_parts(relative_files(value, &[])?, Vec::new())
658 }
659}
660
661impl TryFrom<Vec<PathBuf>> for DbFilesV1 {
662 type Error = Error;
663
664 fn try_from(value: Vec<PathBuf>) -> Result<Self, Self::Error> {
716 DbFilesV1::try_from_parts(value, Vec::new())
717 }
718}
719
720impl TryFrom<(Vec<PathBuf>, Vec<BackupEntry>)> for DbFilesV1 {
721 type Error = Error;
722
723 fn try_from(value: (Vec<PathBuf>, Vec<BackupEntry>)) -> Result<Self, Self::Error> {
725 let (paths, backup) = value;
726 DbFilesV1::try_from_parts(paths, backup)
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use std::{
733 fs::{File, create_dir_all},
734 str::FromStr,
735 };
736
737 use alpm_types::{Md5Checksum, RelativeFilePath};
738 use rstest::rstest;
739 use tempfile::tempdir;
740 use testresult::TestResult;
741
742 use super::*;
743
744 #[test]
746 fn filesv1_try_from_pathbuf_succeeds() -> TestResult {
747 let temp_dir = tempdir()?;
748 let path = temp_dir.path();
749 create_dir_all(path.join("usr/bin/"))?;
750 File::create(path.join("usr/bin/foo"))?;
751
752 let files = DbFilesV1::try_from(path.to_path_buf())?;
753
754 assert_eq!(
755 files.as_ref(),
756 vec![
757 PathBuf::from("usr/"),
758 PathBuf::from("usr/bin/"),
759 PathBuf::from("usr/bin/foo")
760 ]
761 );
762
763 Ok(())
764 }
765
766 #[rstest]
767 #[case::dirs_and_files(vec![PathBuf::from("usr/"), PathBuf::from("usr/bin/"), PathBuf::from("usr/bin/foo")], 3)]
768 #[case::empty(Vec::new(), 0)]
769 fn filesv1_try_from_pathbufs_succeeds(
770 #[case] paths: Vec<PathBuf>,
771 #[case] len: usize,
772 ) -> TestResult {
773 let files = DbFilesV1::try_from(paths)?;
774
775 assert_eq!(files.as_ref().len(), len);
776
777 Ok(())
778 }
779
780 #[rstest]
781 #[case::absolute_paths(
782 vec![
783 PathBuf::from("/usr/"), PathBuf::from("/usr/bin/"), PathBuf::from("/usr/bin/foo")
784 ],
785 FilesV1PathErrors{
786 absolute: HashSet::from_iter([
787 PathBuf::from("/usr/"),
788 PathBuf::from("/usr/bin/"),
789 PathBuf::from("/usr/bin/foo"),
790 ]),
791 without_parent: HashSet::new(),
792 duplicate: HashSet::new(),
793 }
794 )]
795 #[case::without_parents(
796 vec![PathBuf::from("usr/bin/"), PathBuf::from("usr/bin/foo")],
797 FilesV1PathErrors{
798 absolute: HashSet::new(),
799 without_parent: HashSet::from_iter([
800 PathBuf::from("usr/bin/"),
801 ]),
802 duplicate: HashSet::new(),
803 }
804 )]
805 #[case::duplicates(
806 vec![PathBuf::from("usr/"), PathBuf::from("usr/")],
807 FilesV1PathErrors{
808 absolute: HashSet::new(),
809 without_parent: HashSet::new(),
810 duplicate: HashSet::from_iter([
811 PathBuf::from("usr/"),
812 ]),
813 }
814 )]
815 fn filesv1_try_from_pathbufs_fails(
816 #[case] paths: Vec<PathBuf>,
817 #[case] expected_errors: FilesV1PathErrors,
818 ) -> TestResult {
819 let result = DbFilesV1::try_from(paths);
820 let errors = match result {
821 Ok(files) => panic!(
822 "Should have failed with an Error::InvalidFilesPaths, but succeeded to create a DbFilesV1: {files:?}"
823 ),
824 Err(Error::InvalidFilesPaths { message }) => message,
825 Err(error) => panic!("Expected an Error::InvalidFilesPaths, but got: {error}"),
826 };
827
828 eprintln!("{errors}");
829 assert_eq!(errors, expected_errors.to_string());
830
831 Ok(())
832 }
833
834 #[test]
835 fn filesv1_try_from_paths_and_backups_succeeds() -> TestResult {
836 let paths = vec![
837 PathBuf::from("usr/"),
838 PathBuf::from("usr/bin/"),
839 PathBuf::from("usr/bin/foo"),
840 ];
841 let backup = vec![BackupEntry {
842 path: RelativeFilePath::from_str("usr/bin/foo")?,
843 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e")?,
844 }];
845
846 let files = DbFilesV1::try_from((paths, backup))?;
847
848 assert_eq!(files.backups().len(), 1);
849
850 Ok(())
851 }
852
853 #[rstest]
854 #[case::backup_not_in_files(
855 vec![PathBuf::from("usr/")],
856 vec![BackupEntry {
857 path: RelativeFilePath::from_str("usr/bin/foo").unwrap(),
858 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e").unwrap(),
859 }],
860 BackupV1Errors{
861 not_in_files: HashSet::from_iter([RelativeFilePath::from_str("usr/bin/foo").unwrap()]),
862 duplicate: HashSet::new(),
863 }
864 )]
865 #[case::duplicate_backup_entries(
866 vec![
867 PathBuf::from("usr/"),
868 PathBuf::from("usr/bin/"),
869 PathBuf::from("usr/bin/foo")
870 ],
871 vec![
872 BackupEntry {
873 path: RelativeFilePath::from_str("usr/bin/foo").unwrap(),
874 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e").unwrap(),
875 },
876 BackupEntry {
877 path: RelativeFilePath::from_str("usr/bin/foo").unwrap(),
878 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e").unwrap(),
879 }
880 ],
881 BackupV1Errors{
882 not_in_files: HashSet::new(),
883 duplicate: HashSet::from_iter([RelativeFilePath::from_str("usr/bin/foo").unwrap()]),
884 }
885 )]
886 fn filesv1_try_from_paths_and_backups_fails(
887 #[case] paths: Vec<PathBuf>,
888 #[case] backup: Vec<BackupEntry>,
889 #[case] expected_errors: BackupV1Errors,
890 ) -> TestResult {
891 let result = DbFilesV1::try_from((paths, backup));
892 let errors = match result {
893 Ok(files) => panic!(
894 "Should have failed with an Error::InvalidBackupEntries, but succeeded to create a DbFilesV1: {files:?}"
895 ),
896 Err(Error::InvalidBackupEntries { message }) => message,
897 Err(error) => panic!("Expected an Error::InvalidBackupEntries, but got: {error}"),
898 };
899
900 eprintln!("{errors}");
901 assert_eq!(errors, expected_errors.to_string());
902
903 Ok(())
904 }
905
906 #[test]
907 fn filesv1_from_str_rejects_absolute_paths() -> TestResult {
908 let data = "%FILES%\n/usr/bin/foo\n";
909
910 match DbFilesV1::from_str(data) {
911 Err(Error::ParseError(_)) => Ok(()),
912 Err(error) => panic!("expected ParseError, got {error}"),
913 Ok(files) => panic!("expected parse failure, got {files:?}"),
914 }
915 }
916
917 #[test]
918 fn filesv1_from_str_skips_null_backup_entries() -> TestResult {
919 let data = r#"%FILES%
920etc/
921etc/foo/
922etc/foo/foo.conf
923
924%BACKUP%
925etc/foo/foo.conf d41d8cd98f00b204e9800998ecf8427e
926etc/foo/bar.conf (null)
927"#;
928
929 let files = DbFilesV1::from_str(data)?;
930
931 assert_eq!(
932 files.backups(),
933 &[BackupEntry {
934 path: RelativeFilePath::from_str("etc/foo/foo.conf")?,
935 md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e")?
936 }]
937 );
938
939 Ok(())
940 }
941}