Skip to main content

litcheck_core/
input.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::Arc,
4};
5
6use crate::diagnostics::{FileName, SourceFile, SourceManager};
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::from(self.0.clone())
52        }
53    }
54
55    pub fn into_source(
56        &self,
57        strict: bool,
58        source_manager: &dyn SourceManager,
59    ) -> std::io::Result<Arc<SourceFile>> {
60        let name = self.filename();
61        let code = self.read_to_string(strict)?;
62        log::trace!(target: "input", "read '{name}': '{code}'");
63        Ok(source_manager.load(name.language(), name, code))
64    }
65
66    pub fn get_file_types(&self, file_types: &[String]) -> Result<Vec<Input>, walkdir::Error> {
67        use walkdir::WalkDir;
68
69        let mut inputs = vec![];
70        let walker = WalkDir::new(&self.0).into_iter();
71        for entry in walker.filter_entry(|e| is_matching_file_type_or_dir(e, file_types)) {
72            let input = Self(entry?.into_path());
73            if input.is_directory() {
74                let mut children = input.get_file_types(file_types)?;
75                inputs.append(&mut children);
76            } else {
77                inputs.push(input);
78            }
79        }
80
81        Ok(inputs)
82    }
83
84    pub fn glob(&self, pattern: &str) -> Result<Vec<Input>, walkdir::Error> {
85        use glob::Pattern;
86        assert!(
87            self.is_directory(),
88            "cannot call `glob` on a non-directory path: {}",
89            self.0.display()
90        );
91
92        // We need to create a pattern that extends the current path, while treating
93        // the path as a literal match. We do that by first converting the path to a string,
94        // escaping the string of any pattern meta characters, then joining the provided
95        // glob pattern to the path so that it matches anything underneath the directory
96        // represented by `self`
97        let path = self.0.as_os_str().to_string_lossy();
98        let mut pat = Pattern::escape(&path);
99        pat.push_str(std::path::MAIN_SEPARATOR_STR);
100        pat.push_str(pattern);
101        let pattern = Pattern::new(&pat).expect("invalid glob pattern");
102        self.glob_pattern(&pattern)
103    }
104
105    fn glob_pattern(&self, pattern: &glob::Pattern) -> Result<Vec<Input>, walkdir::Error> {
106        use walkdir::WalkDir;
107
108        let mut inputs = vec![];
109        let walker = WalkDir::new(&self.0).into_iter();
110        for entry in walker.filter_entry(|e| is_dir_or_pattern_match(e, pattern)) {
111            let input = Self(entry?.into_path());
112            if input.is_directory() {
113                let mut children = input.glob_pattern(pattern)?;
114                inputs.append(&mut children);
115            } else {
116                inputs.push(input);
117            }
118        }
119
120        Ok(inputs)
121    }
122
123    pub fn open(&self) -> std::io::Result<impl std::io::BufRead> {
124        use either::Either;
125        use std::fs::File;
126
127        Ok(if self.0.as_os_str() == "-" {
128            Either::Left(std::io::stdin().lock())
129        } else {
130            let file = if self.0.is_absolute() {
131                File::open(&self.0)?
132            } else {
133                let path = self.0.canonicalize()?;
134                File::open(path)?
135            };
136            Either::Right(std::io::BufReader::new(file))
137        })
138    }
139
140    pub fn read_to_string(&self, strict: bool) -> std::io::Result<String> {
141        use std::io::{BufRead, Read};
142
143        let mut buf = self.open()?;
144        let mut content = String::with_capacity(1024);
145        if strict {
146            buf.read_to_string(&mut content)?;
147            Ok(content)
148        } else {
149            // Normalize line endings via `lines`
150            for (i, line) in buf.lines().enumerate() {
151                let mut line = line?;
152                // SAFETY: We are able to guarantee that we do not violate
153                // the utf-8 property of `content` here, because both lines
154                // are known to be valid utf-8, and we're joining them with
155                // another valid utf-8 character, as '\n' has the same
156                // representation in ascii and utf-8
157                unsafe {
158                    let bytes = content.as_mut_vec();
159                    if i > 0 {
160                        bytes.push(b'\n');
161                    }
162                    bytes.append(line.as_mut_vec());
163                }
164            }
165
166            Ok(content)
167        }
168    }
169}
170
171fn is_matching_file_type_or_dir(entry: &walkdir::DirEntry, file_types: &[String]) -> bool {
172    let path = entry.path();
173    if path.is_dir() {
174        return true;
175    }
176    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
177        file_types.iter().any(|ft| ft == ext)
178    } else {
179        false
180    }
181}
182
183fn is_dir_or_pattern_match(entry: &walkdir::DirEntry, pattern: &glob::Pattern) -> bool {
184    let path = entry.path();
185    if path.is_dir() {
186        return true;
187    }
188    pattern.matches_path(path)
189}