use crate::core::types::RiskAnalysis;
use std::path::Path;
pub fn analyze(path: &Path) -> RiskAnalysis {
let home_str = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).ok();
let home_path = home_str.as_deref().map(Path::new);
analyze_with_home(path, home_path)
}
pub fn analyze_with_home(path: &Path, home: Option<&Path>) -> RiskAnalysis {
let path_str = path.to_string_lossy();
let normalized = normalize_str(&path_str);
let original_norm = normalized.clone();
if let Some(home_path) = home {
let home_norm = normalize_str(&home_path.to_string_lossy());
if !home_norm.is_empty()
&& (normalized == home_norm || normalized.starts_with(&format!("{home_norm}/")))
{
let rel = normalized.strip_prefix(&home_norm).unwrap_or("").trim_start_matches('/');
if rel == ".config" || rel.starts_with(".config/") {
return RiskAnalysis::sensitive("Contains user configuration data (~/.config)");
}
if rel == ".local/share" || rel.starts_with(".local/share/") {
return RiskAnalysis::sensitive("User data folder (~/.local/share)");
}
if rel == ".cache" || rel.starts_with(".cache/") {
return RiskAnalysis::sensitive("System-wide cache directory (~/.cache)");
}
if is_npm_or_pnpm_prefix(rel) {
return RiskAnalysis::safe();
}
let top = rel.split('/').next().unwrap_or("");
if top.starts_with('.')
&& top != "."
&& top != ".."
&& top != ".npm"
&& top != ".pnpm"
&& !top.is_empty()
{
return RiskAnalysis::sensitive("Contains unsafe hidden folder");
}
}
}
if contains_app_bundle(&normalized) {
return RiskAnalysis::sensitive("Inside macOS .app package");
}
if original_norm.starts_with("//") && contains_hidden_segment(&original_norm) {
return RiskAnalysis::sensitive("Hidden path in network share");
}
if normalized.contains("/appdata/roaming") {
return RiskAnalysis::sensitive("Inside Windows AppData Roaming folder");
}
if normalized.contains("/appdata/local") {
if contains_safe_appdata_local(&normalized) {
return RiskAnalysis::safe();
}
return RiskAnalysis::sensitive("Inside Windows AppData Local folder");
}
if is_program_files(&normalized) {
return RiskAnalysis::sensitive("Inside Program Files folder");
}
RiskAnalysis::safe()
}
fn normalize_str(s: &str) -> String {
let lowered = s.to_lowercase().replace('\\', "/");
if lowered.len() >= 3 {
let b = lowered.as_bytes();
if b[0].is_ascii_lowercase() && b[1] == b':' && b[2] == b'/' {
return lowered[2..].to_string();
}
}
lowered
}
fn is_npm_or_pnpm_prefix(rel: &str) -> bool {
for prefix in [".npm", ".pnpm"] {
if rel == prefix || rel.starts_with(&format!("{prefix}/")) {
return true;
}
}
false
}
fn contains_app_bundle(s: &str) -> bool {
let needle = "/applications/";
if let Some(i) = s.find(needle) {
let rest = &s[i + needle.len()..];
if let Some(slash) = rest.find('/') {
return rest[..slash].ends_with(".app");
}
}
false
}
fn contains_hidden_segment(s: &str) -> bool {
s.split('/').any(|seg| seg.starts_with('.') && seg.len() > 1 && seg != "..")
}
fn contains_safe_appdata_local(s: &str) -> bool {
for name in ["/.cache", "/.npm", "/.pnpm"] {
if s.contains(&format!("{name}/")) || s.ends_with(name) {
return true;
}
}
false
}
fn is_program_files(s: &str) -> bool {
s.contains("program files/") || s.contains("program files (x86)/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_strips_drive_letter() {
assert_eq!(normalize_str("C:\\Users\\foo"), "/users/foo");
assert_eq!(normalize_str("c:/Windows"), "/windows");
}
#[test]
fn normalize_lowercases_and_converts_backslash() {
assert_eq!(normalize_str("C:\\Foo\\Bar"), "/foo/bar");
}
#[test]
fn npm_pnpm_prefix_exact_match() {
assert!(is_npm_or_pnpm_prefix(".npm"));
assert!(is_npm_or_pnpm_prefix(".pnpm"));
assert!(is_npm_or_pnpm_prefix(".npm/cache"));
assert!(is_npm_or_pnpm_prefix(".pnpm/store"));
assert!(!is_npm_or_pnpm_prefix(".npmrc"));
assert!(!is_npm_or_pnpm_prefix(".pnpmrc"));
}
#[test]
fn app_bundle_detection() {
assert!(contains_app_bundle("/applications/foo.app/contents/resources"));
assert!(!contains_app_bundle("/applications/foo/bar"));
assert!(!contains_app_bundle("/applications/foo.app")); }
#[test]
fn safe_appdata_local_whitelist() {
assert!(contains_safe_appdata_local("/appdata/local/.cache/x"));
assert!(contains_safe_appdata_local("/appdata/local/.npm"));
assert!(!contains_safe_appdata_local("/appdata/local/programs"));
}
}