use std::path::Path;
use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::{frx_load::LoadedRules, frx_scan::rule_token};
use crate::scan_finding::ScanFinding;
use crate::path_name_bytes::{is_slash, normalized_path, prefix_parts, safe_component};
const REDACTED: &str = "[REDACTED]";
pub(crate) struct PathScan {
pub(crate) display: String,
pub(crate) findings: Vec<String>,
}
pub(crate) struct PathScanRecords {
pub(crate) display: String,
pub(crate) findings: Vec<ScanFinding>,
}
pub(crate) fn repository_root() -> Option<std::path::PathBuf> {
let cwd = std::env::current_dir().ok()?;
for ancestor in cwd.ancestors() {
if ancestor.join(".git").exists() {
return Some(ancestor.to_path_buf());
}
}
return None;
}
pub(crate) fn logical_path(path: &str, root: Option<&Path>) -> String {
let Some(repository) = root else {
return path.to_string();
};
let input = Path::new(path);
let absolute = if input.is_absolute() {
input.to_path_buf()
} else {
let Ok(cwd) = std::env::current_dir() else {
return path.to_string();
};
cwd.join(input)
};
let mut normalized = std::path::PathBuf::new();
for part in absolute.components() {
if part == std::path::Component::ParentDir {
normalized.pop();
} else if part != std::path::Component::CurDir {
normalized.push(part.as_os_str());
}
}
if let Ok(relative) = normalized.strip_prefix(repository)
&& let Some(name) = relative.to_str() {
return name.to_string();
}
return path.to_string();
}
fn matching_rules(component: &[u8], loaded: &LoadedRules) -> Result<Vec<String>, ()> {
let mut rules: Vec<String> = Vec::new();
for set in loaded.iter_sets() {
let matcher = AssertUnwindSafe(|| return set.matcher.line_matches(component, &[0]));
let Ok(ids) = catch_unwind(matcher) else {
return Err(());
};
rules.extend(ids.into_iter().map(|(_, id)| return rule_token(set.base, &set.names, id)));
}
return Ok(rules);
}
pub(crate) fn scan_path_records(path: &Path, loaded: &LoadedRules) -> PathScanRecords {
let normalized: Vec<u8> = normalized_path(path);
return scan_normalized_records(&normalized, prefix_parts(path), loaded);
}
fn scan_normalized_records(normalized: &[u8], prefix_count: usize, loaded: &LoadedRules) -> PathScanRecords {
let mut remaining_prefix: usize = prefix_count;
let mut displayed: Vec<String> = Vec::<String>::new();
let mut matches: Vec<(usize, Vec<String>)> = Vec::new();
let mut position = 0;
for component in normalized.split(is_slash) {
if component.contains(&b'\n') || component.contains(&b'\r') {
return PathScanRecords {
display: REDACTED.to_string(),
findings: vec![ScanFinding::PathnameLineBreak],
};
}
if component.is_empty() {
displayed.push(safe_component(component));
continue;
}
if remaining_prefix > 0 {
remaining_prefix -= 1;
displayed.push(safe_component(component));
continue;
}
if component == b"." || component == b".." {
displayed.push(safe_component(component));
continue;
}
position += 1;
let Ok(rules) = matching_rules(component, loaded) else {
return PathScanRecords {
display: REDACTED.to_string(),
findings: vec![ScanFinding::EngineError],
};
};
if rules.is_empty() {
displayed.push(safe_component(component));
} else {
displayed.push(REDACTED.to_string());
matches.push((position, rules));
}
}
let display = displayed.join("/");
let mut findings: Vec<ScanFinding> = Vec::<ScanFinding>::new();
for (segment, rules) in matches {
for rule in rules {
findings.push(ScanFinding::Name { component: segment, rule });
}
}
return PathScanRecords { display, findings };
}
pub(crate) fn scan_path(path: &str, loaded: &LoadedRules) -> PathScan {
let records: PathScanRecords = scan_path_records(Path::new(path), loaded);
let mut findings: Vec<String> = Vec::<String>::new();
for finding in records.findings {
findings.push(finding.render(records.display.as_str()));
}
return PathScan { display: records.display, findings };
}
#[cfg(test)]
#[path = "path_scan_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "path_scan_prefix_tests.rs"]
mod prefix_tests;