extern crate regex;
extern crate serde;
extern crate serde_json;
use self::serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path;
use std::sync;
pub static LOWER_FILENAMES_TO_IMPLEMENTATIONS: sync::LazyLock<HashMap<String, String>> =
sync::LazyLock::new(|| {
vec![
("bsdmakefile", "bmake"),
("gnumakefile", "gmake"),
("makefile", "make"),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>()
});
pub static LOWER_FILE_EXTENSIONS_TO_IMPLEMENTATIONS: sync::LazyLock<HashMap<String, String>> =
sync::LazyLock::new(|| {
vec![
("bsdmakefile", "bmake"),
("gnumakefile", "gmake"),
("makefile", "make"),
("mk", "make"),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>()
});
pub static LOWER_FILENAMES_TO_PARENT_BUILD_SYSTEMS: sync::LazyLock<HashMap<String, String>> =
sync::LazyLock::new(|| {
vec![
("cmakelists.txt", "cmake"),
("configure", "autotools"),
(".gyp", "gyp"),
("makefile.pl", "perl"),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>()
});
pub static LOWER_INCLUDE_FILENAME_PATTERN: sync::LazyLock<regex::Regex> =
sync::LazyLock::new(|| {
regex::Regex::new(r"^sys\.mk|(.*\.)?include\.(bsdmakefile|gnumakefile|makefile|mk)$")
.unwrap()
});
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct Metadata {
pub path: String,
pub filename: String,
pub is_makefile: bool,
pub build_system: String,
pub is_machine_generated: bool,
pub is_include_file: bool,
pub is_empty: bool,
pub lines: usize,
pub has_final_eol: bool,
}
impl Metadata {
pub fn new() -> Metadata {
Metadata {
path: String::new(),
filename: String::new(),
is_makefile: false,
build_system: String::new(),
is_machine_generated: false,
is_include_file: false,
is_empty: true,
lines: 0,
has_final_eol: false,
}
}
}
impl Default for Metadata {
fn default() -> Self {
Metadata::new()
}
}
impl fmt::Display for Metadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
serde_json::to_string(&self).map_err(|_| fmt::Error)?
)
}
}
pub fn analyze(pth: &path::Path) -> Result<Metadata, String> {
let pth_abs: path::PathBuf = pth
.canonicalize()
.map_err(|err| format!("error: {}: {}", pth.display(), err))?;
let mut metadata: Metadata = Metadata::new();
metadata.path = pth.display().to_string();
let filename: String = pth
.file_name()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_string();
metadata.filename = filename;
let filename_lower: String = metadata.filename.to_lowercase();
let file_extension_lower: String = pth
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_string()
.to_lowercase();
if !LOWER_FILE_EXTENSIONS_TO_IMPLEMENTATIONS.contains_key(&file_extension_lower)
&& !LOWER_FILENAMES_TO_IMPLEMENTATIONS.contains_key(&filename_lower)
{
return Ok(metadata);
}
if let Some(implementation) = LOWER_FILENAMES_TO_IMPLEMENTATIONS.get(&filename_lower) {
metadata.is_makefile = true;
metadata.build_system = implementation.to_string();
}
if let Some(implementation) =
LOWER_FILE_EXTENSIONS_TO_IMPLEMENTATIONS.get(&file_extension_lower)
{
metadata.is_makefile = true;
metadata.build_system = implementation.to_string();
}
metadata.is_include_file =
LOWER_INCLUDE_FILENAME_PATTERN.is_match(&metadata.filename.to_lowercase());
let byte_len: u64 = fs::metadata(&pth_abs)
.map_err(|err| format!("error: {}: {}", pth_abs.display(), err))?
.len();
metadata.is_empty = byte_len == 0;
if !metadata.is_makefile || metadata.build_system != "make" {
return Ok(metadata);
}
let parent_dir_option: Option<&path::Path> = pth_abs.parent();
if parent_dir_option.is_none() {
return Ok(metadata);
}
if !LOWER_FILE_EXTENSIONS_TO_IMPLEMENTATIONS.contains_key(&file_extension_lower) {
let parent_dir: &path::Path = parent_dir_option.unwrap();
for sibling_entry_result in parent_dir
.read_dir()
.map_err(|err| format!("error: {}: {}", parent_dir.display(), err))?
{
let sibling_entry: fs::DirEntry = sibling_entry_result
.map_err(|err| format!("error: {}: {}", parent_dir.display(), err))?;
let sibling_string: String = sibling_entry
.path()
.file_name()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if let Some(parent_build_system) =
LOWER_FILENAMES_TO_PARENT_BUILD_SYSTEMS.get(&sibling_string)
{
metadata.is_machine_generated = true;
metadata.build_system = parent_build_system.to_string();
return Ok(metadata);
}
}
let grandparent_dir_option: Option<&path::Path> = parent_dir.parent();
if grandparent_dir_option.is_none() {
return Ok(metadata);
}
let grandparent_dir: &path::Path = grandparent_dir_option.unwrap();
for aunt_entry_result in grandparent_dir
.read_dir()
.map_err(|err| format!("error: {}: {}", grandparent_dir.display(), err))?
{
let aunt_entry: fs::DirEntry = aunt_entry_result
.map_err(|err| format!("error: {}: {}", grandparent_dir.display(), err))?;
let aunt_string: String = aunt_entry
.path()
.file_name()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if let Some(grandparent_build_system) =
LOWER_FILENAMES_TO_PARENT_BUILD_SYSTEMS.get(&aunt_string)
{
metadata.is_machine_generated = true;
metadata.build_system = grandparent_build_system.to_string();
return Ok(metadata);
}
}
}
if !metadata.is_empty {
let makefile_str: &str = &fs::read_to_string(&pth_abs)
.map_err(|err| format!("error: {}: {}", pth_abs.display(), err))?;
metadata.lines = 1 + makefile_str.matches('\n').count();
let last_char: char = makefile_str.chars().last().unwrap_or(' ');
metadata.has_final_eol = last_char == '\n';
}
Ok(metadata)
}