litcheck_core/
input.rs

1use std::{
2    borrow::Cow,
3    path::{Path, PathBuf},
4};
5
6use crate::diagnostics::{ArcSource, FileName, Source};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Input(PathBuf);
10impl From<std::ffi::OsString> for Input {
11    fn from(s: std::ffi::OsString) -> Self {
12        Self(PathBuf::from(s))
13    }
14}
15impl From<&std::ffi::OsStr> for Input {
16    fn from(s: &std::ffi::OsStr) -> Self {
17        Self(PathBuf::from(s))
18    }
19}
20impl From<&Path> for Input {
21    fn from(path: &Path) -> Self {
22        Self(path.to_path_buf())
23    }
24}
25impl From<PathBuf> for Input {
26    fn from(path: PathBuf) -> Self {
27        Self(path)
28    }
29}
30impl Input {
31    pub fn exists(&self) -> bool {
32        self.0.exists()
33    }
34
35    pub fn is_file(&self) -> bool {
36        self.0.is_file()
37    }
38
39    pub fn is_directory(&self) -> bool {
40        self.0.is_dir()
41    }
42
43    pub fn path(&self) -> &Path {
44        self.0.as_ref()
45    }
46
47    pub fn filename(&self) -> FileName {
48        if self.0.as_os_str() == "-" {
49            FileName::Stdin
50        } else {
51            FileName::Path(self.0.clone().into_boxed_path())
52        }
53    }
54
55    pub fn into_arc_source(&self, strict: bool) -> std::io::Result<ArcSource> {
56        self.into_source(strict).map(ArcSource::new)
57    }
58
59    pub fn into_source(&self, strict: bool) -> std::io::Result<Source<'static>> {
60        let name = self.filename();
61        let code = self.read_to_string(strict).map(Cow::Owned)?;
62        Ok(Source { name, code })
63    }
64
65    pub fn get_file_types(&self, file_types: &[String]) -> Result<Vec<Input>, walkdir::Error> {
66        use walkdir::WalkDir;
67
68        let mut inputs = vec![];
69        let walker = WalkDir::new(&self.0).into_iter();
70        for entry in walker.filter_entry(|e| is_matching_file_type_or_dir(e, file_types)) {
71            let input = Self(entry?.into_path());
72            if input.is_directory() {
73                let mut children = input.get_file_types(file_types)?;
74                inputs.append(&mut children);
75            } else {
76                inputs.push(input);
77            }
78        }
79
80        Ok(inputs)
81    }
82
83    pub fn glob(&self, pattern: &str) -> Result<Vec<Input>, walkdir::Error> {
84        use glob::Pattern;
85        assert!(
86            self.is_directory(),
87            "cannot call `glob` on a non-directory path: {}",
88            self.0.display()
89        );
90
91        // We need to create a pattern that extends the current path, while treating
92        // the path as a literal match. We do that by first converting the path to a string,
93        // escaping the string of any pattern meta characters, then joining the provided
94        // glob pattern to the path so that it matches anything underneath the directory
95        // represented by `self`
96        let path = self.0.as_os_str().to_string_lossy();
97        let mut pat = Pattern::escape(&path);
98        pat.push_str(std::path::MAIN_SEPARATOR_STR);
99        pat.push_str(pattern);
100        let pattern = Pattern::new(&pat).expect("invalid glob pattern");
101        self.glob_pattern(&pattern)
102    }
103
104    fn glob_pattern(&self, pattern: &glob::Pattern) -> Result<Vec<Input>, walkdir::Error> {
105        use walkdir::WalkDir;
106
107        let mut inputs = vec![];
108        let walker = WalkDir::new(&self.0).into_iter();
109        for entry in walker.filter_entry(|e| is_dir_or_pattern_match(e, pattern)) {
110            let input = Self(entry?.into_path());
111            if input.is_directory() {
112                let mut children = input.glob_pattern(pattern)?;
113                inputs.append(&mut children);
114            } else {
115                inputs.push(input);
116            }
117        }
118
119        Ok(inputs)
120    }
121
122    pub fn open(&self) -> std::io::Result<impl std::io::BufRead> {
123        use either::Either;
124        use std::fs::File;
125
126        Ok(if self.0.as_os_str() == "-" {
127            Either::Left(std::io::stdin().lock())
128        } else {
129            let file = if self.0.is_absolute() {
130                File::open(&self.0)?
131            } else {
132                let path = self.0.canonicalize()?;
133                File::open(path)?
134            };
135            Either::Right(std::io::BufReader::new(file))
136        })
137    }
138
139    pub fn read_to_string(&self, strict: bool) -> std::io::Result<String> {
140        use std::io::{BufRead, Read};
141
142        let mut buf = self.open()?;
143        let mut content = String::with_capacity(1024);
144        if strict {
145            buf.read_to_string(&mut content)?;
146            Ok(content)
147        } else {
148            // Normalize line endings via `lines`
149            for line in buf.lines() {
150                let mut line = line?;
151                // SAFETY: We are able to guarantee that we do not violate
152                // the utf-8 property of `content` here, because both lines
153                // are known to be valid utf-8, and we're joining them with
154                // another valid utf-8 character, as '\n' has the same
155                // representation in ascii and utf-8
156                unsafe {
157                    let bytes = content.as_mut_vec();
158                    bytes.push(b'\n');
159                    bytes.append(line.as_mut_vec());
160                }
161            }
162
163            Ok(content)
164        }
165    }
166}
167
168fn is_matching_file_type_or_dir(entry: &walkdir::DirEntry, file_types: &[String]) -> bool {
169    let path = entry.path();
170    if path.is_dir() {
171        return true;
172    }
173    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
174        file_types.iter().any(|ft| ft == ext)
175    } else {
176        false
177    }
178}
179
180fn is_dir_or_pattern_match(entry: &walkdir::DirEntry, pattern: &glob::Pattern) -> bool {
181    let path = entry.path();
182    if path.is_dir() {
183        return true;
184    }
185    pattern.matches_path(path)
186}