use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub analyzer: AnalyzerSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AnalyzerSettings {
pub enabled: bool,
pub timeout_ms: u64,
pub max_files: usize,
pub min_path_depth: usize,
pub ignore_dirs: Vec<String>,
pub skip_paths: Vec<String>,
pub extensions: Vec<String>,
pub include_hidden: bool,
}
impl Default for AnalyzerSettings {
fn default() -> Self {
Self {
enabled: true,
timeout_ms: 500, max_files: 1000,
min_path_depth: 4, ignore_dirs: vec![
"node_modules".into(),
".npm".into(),
".pnpm-store".into(),
"bower_components".into(),
".venv".into(),
"venv".into(),
"__pycache__".into(),
".pytest_cache".into(),
".mypy_cache".into(),
".tox".into(),
"site-packages".into(),
"target".into(),
"vendor".into(),
".gradle".into(),
".m2".into(),
".idea".into(),
".vscode".into(),
".vs".into(),
"build".into(),
"dist".into(),
"out".into(),
"_build".into(),
".cache".into(),
".parcel-cache".into(),
".next".into(),
".nuxt".into(),
".git".into(),
".svn".into(),
".hg".into(),
"deps".into(),
"_deps".into(),
"coverage".into(),
".coverage".into(),
"htmlcov".into(),
".eggs".into(),
"*.egg-info".into(),
],
skip_paths: vec![],
extensions: vec![], include_hidden: false,
}
}
}
impl Config {
pub fn load(locker_dir: &Path) -> Result<Self> {
let config_path = locker_dir.join("config.toml");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&content).unwrap_or_else(|e| {
eprintln!(
"Warning: Failed to parse config.toml: {}. Using defaults.",
e
);
Config::default()
});
Ok(config)
} else {
let config = Config::default();
config.save(locker_dir)?;
Ok(config)
}
}
pub fn save(&self, locker_dir: &Path) -> Result<()> {
let config_path = locker_dir.join("config.toml");
let content = Self::generate_config_with_comments(self)?;
std::fs::write(&config_path, content)?;
Ok(())
}
fn generate_config_with_comments(config: &Config) -> Result<String> {
let toml_content = toml::to_string_pretty(config)?;
let header = r#"# Lazy-Locker Configuration
#
# This file controls the behavior of lazy-locker.
# Edit these settings to customize the token security analyzer.
#
# Documentation: https://github.com/WillIsback/lazy-locker
"#;
let analyzer_comment = r#"
# Token Security Analyzer Settings
# The analyzer scans your codebase for exposed secrets.
# Customize these settings if analysis is slow or you want to exclude specific directories.
#
# Tips:
# - Set enabled = false to disable automatic analysis
# - Add large directories to ignore_dirs to speed up analysis
# - Decrease max_files if analysis is still slow
"#;
let content = format!("{}{}{}", header, analyzer_comment, toml_content);
Ok(content)
}
pub fn get_locker_dir() -> Result<PathBuf> {
let base_dirs = directories::BaseDirs::new()
.ok_or_else(|| anyhow::anyhow!("Unable to determine user directories"))?;
#[cfg(unix)]
let sub_dir = ".lazy-locker";
#[cfg(not(unix))]
let sub_dir = "lazy-locker";
let locker_dir = base_dirs.config_dir().join(sub_dir);
Ok(locker_dir)
}
}
impl AnalyzerSettings {
pub fn should_analyze(&self, path: &Path) -> bool {
if !self.enabled {
return false;
}
let depth = path.components().count();
if depth < self.min_path_depth {
return false;
}
let path_str = path.to_string_lossy();
for skip in &self.skip_paths {
if path_str.starts_with(skip) || path_str.ends_with(skip) {
return false;
}
}
if let Ok(home) = std::env::var("HOME")
&& path == Path::new(&home)
{
return false;
}
true
}
pub fn to_analyzer_config(&self) -> token_analyzer::AnalyzerConfig {
let mut config = token_analyzer::AnalyzerConfig::fast();
config.timeout_ms = self.timeout_ms;
config.max_files = self.max_files;
config.include_hidden = self.include_hidden;
config.ignore_dirs.extend(self.ignore_dirs.clone());
if !self.extensions.is_empty() {
config.extensions = self.extensions.clone();
}
config
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.analyzer.enabled);
assert_eq!(config.analyzer.timeout_ms, 500);
assert_eq!(config.analyzer.max_files, 1000);
assert!(config.analyzer.ignore_dirs.contains(&"node_modules".into()));
assert!(config.analyzer.ignore_dirs.contains(&".venv".into()));
}
#[test]
fn test_config_save_load() {
let dir = TempDir::new().unwrap();
let config = Config::default();
config.save(dir.path()).unwrap();
let loaded = Config::load(dir.path()).unwrap();
assert_eq!(loaded.analyzer.enabled, config.analyzer.enabled);
assert_eq!(loaded.analyzer.timeout_ms, config.analyzer.timeout_ms);
}
#[test]
fn test_should_analyze_depth() {
let settings = AnalyzerSettings::default();
assert!(!settings.should_analyze(Path::new("/home")));
assert!(!settings.should_analyze(Path::new("/home/user")));
assert!(settings.should_analyze(Path::new("/home/user/project")));
assert!(settings.should_analyze(Path::new("/home/user/project/src")));
}
#[test]
fn test_should_analyze_disabled() {
let settings = AnalyzerSettings {
enabled: false,
..Default::default()
};
assert!(!settings.should_analyze(Path::new("/home/user/project")));
}
#[test]
fn test_to_analyzer_config() {
let settings = AnalyzerSettings::default();
let config = settings.to_analyzer_config();
assert_eq!(config.timeout_ms, 500);
assert_eq!(config.max_files, 1000);
}
}