1#[derive(Debug)]
7pub enum DirDiffError {
8 Io(std::io::Error),
9 StripPrefix(std::path::StripPrefixError),
10 WalkDir(walkdir::Error),
11 UTF8Parsing(std::string::FromUtf8Error),
12}
13
14#[derive(Debug)]
15pub enum DirDiff {
16 ExpectedFileMissing {
17 expected: std::path::PathBuf,
18 },
19 ExpectedFolderMissing {
20 expected: std::path::PathBuf,
21 },
22 UnexpectedFileFound {
23 found: std::path::PathBuf,
24 },
25 UnexpectedFolderFound {
26 found: std::path::PathBuf,
27 },
28 FileTypeMismatch {
29 file: std::path::PathBuf,
30 expected: String,
31 found: String,
32 },
33 ContentMismatch {
34 file: std::path::PathBuf,
35 expected: String,
36 found: String,
37 },
38 NonContentFileMismatch {
39 file: std::path::PathBuf,
40 },
41}
42
43pub(crate) fn diff<A: AsRef<std::path::Path>, B: AsRef<std::path::Path>>(
44 a_base: A,
45 b_base: B,
46) -> Result<Option<DirDiff>, DirDiffError> {
47 use sha2::Digest;
48 let mut a_walker = walk_dir(a_base)?;
49 let mut b_walker = walk_dir(b_base)?;
50
51 loop {
52 match (a_walker.next(), b_walker.next()) {
53 (Some(a), Some(b)) => {
54 let a = a?;
68 let b = b?;
69 if a.metadata()
70 .expect("Unable to retrieve metadata for found file/folder")
71 .is_dir()
72 && b.metadata()
73 .expect("Unable to retrieve metadata for found file/folder")
74 .is_dir()
75 {
76 return diff(a.path(), b.path());
78 }
79
80 let found: std::path::PathBuf = b.path().into();
81
82 if a.file_name() != b.file_name() {
83 return Ok(Some(if found.is_dir() {
84 DirDiff::UnexpectedFolderFound { found }
85 } else {
86 DirDiff::UnexpectedFileFound { found }
87 }));
88 }
89 if let (Ok(a_content), Ok(b_content)) = (
90 std::fs::read_to_string(a.path()),
91 std::fs::read_to_string(b.path()),
92 ) {
93 if a_content != b_content {
94 return Ok(Some(DirDiff::ContentMismatch {
95 expected: b_content,
96 found: a_content,
97 file: found,
98 }));
99 }
100 } else if let (Ok(a_content), Ok(b_content)) =
101 (std::fs::read(a.path()), std::fs::read(b.path()))
102 {
103 if !sha2::Sha256::digest(a_content).eq(&sha2::Sha256::digest(b_content)) {
104 return Ok(Some(DirDiff::NonContentFileMismatch { file: found }));
105 }
106 }
107 }
108 (None, Some(b)) => {
109 let expected: std::path::PathBuf = b?.path().into();
113 return Ok(Some(if expected.is_dir() {
114 DirDiff::ExpectedFolderMissing { expected }
115 } else {
116 DirDiff::ExpectedFileMissing { expected }
117 }));
118 }
119 (Some(a), None) => {
120 let found: std::path::PathBuf = a?.path().into();
124 return Ok(Some(if found.is_dir() {
125 DirDiff::UnexpectedFolderFound { found }
126 } else {
127 DirDiff::UnexpectedFileFound { found }
128 }));
129 }
130 (None, None) => break,
131 }
132 }
133
134 Ok(None)
135}
136
137pub(crate) fn fix<A: AsRef<std::path::Path>, B: AsRef<std::path::Path>>(
138 a_base: A,
139 b_base: B,
140) -> Result<(), DirDiffError> {
141 fix_(a_base, b_base)?;
142 Ok(())
143}
144
145fn fix_(src: impl AsRef<std::path::Path>, dst: impl AsRef<std::path::Path>) -> std::io::Result<()> {
146 if dst.as_ref().exists() {
147 std::fs::remove_dir_all(&dst)?;
148 }
149 std::fs::create_dir_all(&dst)?;
150 let dir = std::fs::read_dir(src)?;
151 for child in dir {
152 let child = child?;
153 if child.metadata()?.is_dir() {
154 fix_(child.path(), dst.as_ref().join(child.file_name()))?;
155 } else {
156 std::fs::copy(child.path(), dst.as_ref().join(child.file_name()))?;
157 }
158 }
159 Ok(())
160}
161
162fn walk_dir<P: AsRef<std::path::Path>>(path: P) -> Result<walkdir::IntoIter, std::io::Error> {
163 let mut walkdir = walkdir::WalkDir::new(path)
164 .sort_by(compare_by_file_name)
165 .into_iter();
166 if let Some(Err(e)) = walkdir.next() {
167 Err(e.into())
168 } else {
169 Ok(walkdir)
170 }
171}
172
173fn compare_by_file_name(a: &walkdir::DirEntry, b: &walkdir::DirEntry) -> std::cmp::Ordering {
174 a.file_name().cmp(b.file_name())
175}
176
177impl From<std::io::Error> for DirDiffError {
178 fn from(e: std::io::Error) -> DirDiffError {
179 DirDiffError::Io(e)
180 }
181}
182
183impl From<std::path::StripPrefixError> for DirDiffError {
184 fn from(e: std::path::StripPrefixError) -> DirDiffError {
185 DirDiffError::StripPrefix(e)
186 }
187}
188
189impl From<walkdir::Error> for DirDiffError {
190 fn from(e: walkdir::Error) -> DirDiffError {
191 DirDiffError::WalkDir(e)
192 }
193}