use std::fs;
use std::path::{Component, Path, PathBuf};
pub struct MakeRule {
pub target: String,
pub source: Option<String>,
pub includes: Vec<String>,
}
impl MakeRule {
pub fn new(data: &[u8], use_wibo: bool) -> Result<Self, Box<dyn std::error::Error>> {
let raw_str = String::from_utf8_lossy(data);
let rule_str = raw_str.replace("\\\n", " ").replace("\\\r\n", " ");
let parts: Vec<&str> = rule_str.splitn(2, ": ").collect();
if parts.len() < 2 {
return Err("No valid dependency list".into());
}
let target = path_from_wibo(parts[0]).to_string_lossy().to_string();
let remaining = parts[1];
let mut files: Vec<String> = remaining
.split_whitespace()
.map(|s| s.to_string())
.collect();
if use_wibo {
files = files
.into_iter()
.map(|p| path_from_wibo(&p).to_string_lossy().to_string())
.collect();
}
let source = if !files.is_empty() {
Some(files.remove(0))
} else {
None
};
Ok(MakeRule {
target,
source,
includes: files,
})
}
pub fn as_str(&self) -> String {
let mut rule = format!("{}: ", self.target);
if let Some(src) = &self.source {
rule.push_str(src);
rule.push(' ');
}
for include in &self.includes {
rule.push_str(&format!("\\\n\t{} ", include));
}
rule.push('\n');
rule
}
}
pub fn path_from_wibo(path_str: &str) -> PathBuf {
let normalized = normalize_wibo_prefix(path_str);
let path = PathBuf::from(&normalized);
if path.is_file() {
return path;
}
resolve_actual_path(&path)
}
fn normalize_wibo_prefix(path_str: &str) -> String {
let mut normalized = path_str.replace('\\', "/");
if normalized.starts_with("//?/") {
normalized = normalized[4..].to_string();
}
if normalized.to_lowercase().starts_with("z:/") {
normalized = normalized[2..].to_string();
}
normalized
}
fn resolve_actual_path(path: &Path) -> PathBuf {
let mut resolved_path = PathBuf::new();
for component in path.components() {
match component {
Component::RootDir => resolved_path.push("/"),
Component::Normal(os_name) => {
let actual_name = resolve_component(&resolved_path, os_name.to_str().unwrap_or(""));
resolved_path.push(actual_name);
}
Component::CurDir => {}
Component::ParentDir => {
resolved_path.pop();
}
Component::Prefix(_) => {
}
}
}
resolved_path
}
fn resolve_component(base: &Path, name: &str) -> String {
let exact_candidate = base.join(name);
if exact_candidate.exists() {
return name.to_string();
}
let search_dir = if base.as_os_str().is_empty() {
Path::new(".")
} else {
base
};
let name_to_find = name.to_lowercase();
if let Ok(entries) = fs::read_dir(search_dir) {
for entry in entries.flatten() {
if let Some(entry_name_str) = entry.file_name().to_str() {
if entry_name_str.to_lowercase() == name_to_find {
return entry_name_str.to_string();
}
}
}
}
name.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_make_rule_parsing() {
let data = b"target.o: source.c include.h";
let rule = MakeRule::new(data, false).unwrap();
assert_eq!(rule.target, "target.o");
assert_eq!(rule.source, Some("source.c".to_string()));
assert_eq!(rule.includes[0], "include.h");
}
#[test]
fn test_no_dependencies() {
let data = b"target.o: ";
let rule = MakeRule::new(data, false).unwrap();
assert_eq!(rule.target, "target.o");
assert!(rule.includes.is_empty());
}
#[test]
fn test_make_rule_empty() {
let empty_data = b"";
let rule = MakeRule::new(empty_data, false);
assert!(rule.is_err());
}
#[test]
fn test_make_rule_simple() {
let wibo_make_rule = b"Z:\\tmp\\test_dir\\result.o: test.c \r\n";
let rule = MakeRule::new(wibo_make_rule, true).expect("Failed to parse wibo rule");
assert_eq!(rule.target, "/tmp/test_dir/result.o");
assert_eq!(rule.source, Some("test.c".to_string()));
assert!(rule.includes.is_empty());
let output = rule.as_str();
assert!(output.contains("/tmp/test_dir/result.o: test.c"));
}
#[test]
fn test_make_rule_with_includes() {
let wibo_make_rule = b"Z:\\tmp\\result.o: test2.c \\\r\n\tZ:\\home\\user\\decl.h \\\r\n\t\\\\?\\Z:\\home\\user\\lib.h \r\n";
let rule = MakeRule::new(wibo_make_rule, true).expect("Failed to parse complex rule");
assert_eq!(rule.source, Some("test2.c".to_string()));
assert_eq!(rule.includes.len(), 2);
assert_eq!(rule.includes[0], "/home/user/decl.h");
assert_eq!(rule.includes[1], "/home/user/lib.h");
assert_eq!(
rule.as_str(),
"/tmp/result.o: test2.c \\\n\t/home/user/decl.h \\\n\t/home/user/lib.h \n"
);
}
#[test]
fn test_unix_deps() {
let unix_data = b"test.o: test.c test.h";
let rule = MakeRule::new(unix_data, false).expect("Failed to parse unix rule");
assert_eq!(rule.target, "test.o");
assert_eq!(rule.includes[0], "test.h");
}
#[test]
fn test_path_from_wibo_translation() {
assert_eq!(
path_from_wibo("Z:\\home\\user\\file.c"),
PathBuf::from("/home/user/file.c")
);
assert_eq!(
path_from_wibo("\\\\?\\Z:\\home\\user\\file.c"),
PathBuf::from("/home/user/file.c")
);
assert_eq!(
path_from_wibo("relative\\path.h"),
PathBuf::from("relative/path.h")
);
}
#[test]
fn test_path_from_wibo_case_insensitive() {
let path = path_from_wibo("sRc/MaKeRule.RS");
assert!(path.to_string_lossy().contains("akerule"));
let bad_path = path_from_wibo("nOnExiStEnT/fIlE.c");
assert_eq!(bad_path, PathBuf::from("nOnExiStEnT/fIlE.c"));
}
#[test]
fn test_path_from_wibo_curdir() {
let path = path_from_wibo("./src/./makerule.rs");
assert_eq!(path, PathBuf::from("./src/./makerule.rs"));
}
#[test]
fn test_path_from_wibo_parentdir() {
let path = path_from_wibo("src/../src/makerule.rs");
assert_eq!(path, PathBuf::from("src/../src/makerule.rs"));
}
#[test]
fn test_path_from_wibo_prefix() {
let path = path_from_wibo("C:\\Test\\path");
assert!(
path.to_string_lossy()
.to_string()
.to_lowercase()
.contains("test")
);
}
}