fbt_lib/
dir_diff.rs

1// Source: https://github.com/assert-rs/dir-diff (Apache/MIT)
2// Need to modify it so including it, will send PR and try to get it included
3// upstream.
4
5/// The various errors that can happen when diffing two directories
6#[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                // first lets check the depth:
55                // a > b: UnexpectedFileFound or UnexpectedFolderFound else
56                // b > a: ExpectedFileMissing or ExpectedFolderMissing
57
58                // if file names dont match how to find if we got a new entry
59                // on left or extra entry on right? how do people actually
60                // calculate diff?
61
62                // then check file type
63
64                // finally check file content if its a file
65
66                // TODO: this is dummy code to test stuff
67                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                    // Recursively check for the files in the directory
77                    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                // we have something in b, but a is done, lets iterate over all
110                // entries in b, and put them in UnexpectedFileFound and
111                // UnexpectedFolderFound
112                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                // we have something in a, but b is done, lets iterate over all
121                // entries in a, and put them in ExpectedFileMissing and
122                // ExpectedFolderMissing
123                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}