file_expert/
expert.rs

1//
2// $COPYRIGHT$: 794d8002d1b6d954e2302879cb69c215d893c60c
3use crate::data_structures::Content;
4use crate::heuristic::{
5    guess_by_extensions, guess_by_filename, guess_by_heuristic, guess_by_interpreter,
6    guess_by_modeline,
7};
8use std::fmt::{Display, Formatter};
9use std::path::Path;
10
11#[derive(Debug, Eq, PartialEq)]
12pub enum Guess {
13    Kind(String),
14    Unknown,
15}
16
17#[cfg(not(tarpaulin_include))]
18impl Display for Guess {
19    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
20        match self {
21            Guess::Kind(lang) => f.write_str(lang),
22            Guess::Unknown => f.write_str("Unknown file"),
23        }
24    }
25}
26
27///
28/// # Errors
29/// Will return [`std::io::Error`] if there're issues with reading the file
30pub fn guess(path: &Path) -> Result<Guess, std::io::Error> {
31    let metadata = path.metadata()?;
32    if metadata.is_dir() {
33        return Ok(Guess::Kind("Directory".to_string()));
34    }
35
36    if let Some(lang) = guess_by_filename(path) {
37        return Ok(Guess::Kind(lang.to_string()));
38    }
39    let optional_extensions = extensions(path);
40    let content = Content::new(path)?;
41    match content {
42        Content::Binary(_) => return Ok(Guess::Kind("Binary".to_string())),
43        Content::Empty => {
44            if let Some(ext_vec) = optional_extensions {
45                for ext in ext_vec {
46                    if let Some(lang) = guess_by_extensions(&ext) {
47                        return Ok(Guess::Kind(lang.to_string()));
48                    }
49                }
50            }
51            return Ok(Guess::Kind("Unknown file".to_string()));
52        }
53        Content::Text { modelines, body } => {
54            if let Some(interpreter) = guess_by_interpreter(&body) {
55                return Ok(Guess::Kind(interpreter.to_string()));
56            }
57            if let Some(lang) = guess_by_modeline(&modelines) {
58                return Ok(Guess::Kind(lang.to_string()));
59            }
60
61            if let Some(ext_vec) = optional_extensions {
62                for ext in ext_vec {
63                    if let Some(lang) = guess_by_heuristic(&ext, &body) {
64                        return Ok(Guess::Kind(lang.to_string()));
65                    }
66
67                    if let Some(lang) = guess_by_extensions(&ext) {
68                        return Ok(Guess::Kind(lang.to_string()));
69                    }
70                }
71            }
72        }
73    }
74    Ok(Guess::Unknown)
75}
76
77fn extensions(path: &Path) -> Option<Vec<String>> {
78    if let Some(os_str) = path.file_name() {
79        if let Some(filename) = os_str.to_str() {
80            let mut result = Vec::with_capacity(2);
81            let mut ext1 = String::new();
82            for c in filename.chars().rev() {
83                ext1.insert(0, c);
84                if c == '.' {
85                    break;
86                }
87            }
88            if ext1.len() == filename.len() {
89                return None;
90            }
91
92            let mut ext2 = ext1.clone();
93            let new_end = filename.len() - ext1.len();
94            for c in filename[0..new_end].chars().rev() {
95                ext2.insert(0, c);
96                if c == '.' {
97                    break;
98                }
99            }
100
101            if !ext2.is_empty() && ext2.len() != filename.len() {
102                ext2 = ext2.to_lowercase();
103                result.push(ext2);
104            }
105            ext1 = ext1.to_lowercase();
106            result.push(ext1);
107            return Some(result);
108        }
109    }
110    None
111}
112
113#[cfg(test)]
114#[cfg(not(tarpaulin_include))]
115#[test]
116fn test_extensions() {
117    let path = Path::new("foo/bar.js");
118    let expected = ".js";
119    let actual = &extensions(&path).unwrap()[0];
120    assert_eq!(expected, actual);
121
122    let path = Path::new("foo/bar.Js");
123    let expected = ".js";
124    let actual = &extensions(&path).unwrap()[0];
125    assert_eq!(expected, actual);
126
127    let path = Path::new(".gitignore");
128    let expected: Option<Vec<String>> = None;
129    let actual = extensions(&path);
130    assert_eq!(expected, actual);
131
132    let path = Path::new(".gitignore.js");
133    let expected = ".js";
134    let actual = &extensions(&path).unwrap()[0];
135    assert_eq!(expected, actual);
136
137    let path = Path::new("libfoo.dll.config");
138    let expected = vec![".dll.config".to_string(), ".config".to_string()];
139    let actual = &extensions(&path).unwrap();
140    assert_eq!(expected[0], actual[0]);
141    assert_eq!(expected[1], actual[1]);
142}