Skip to main content

dirpack/
config.rs

1use std::fs;
2use std::path::Path;
3
4use clap::ValueEnum;
5use serde::{Deserialize, Serialize};
6
7use crate::budget::BudgetTarget;
8use crate::error::Result;
9use crate::limits;
10
11#[derive(Debug, Clone, Deserialize)]
12#[serde(default)]
13pub struct Config {
14    pub output: OutputConfig,
15    pub scanning: ScanningConfig,
16    pub categories: CategoryConfig,
17    pub priority: PriorityWeights,
18    pub priority_rules: Vec<PriorityRule>,
19    pub exclude: ExcludeConfig,
20    pub signatures: SignatureConfig,
21}
22
23pub use crate::limits::{
24    MAX_BUDGET_BYTES as SAFE_MAX_BUDGET_BYTES, MAX_BUDGET_TOKENS as SAFE_MAX_BUDGET_TOKENS,
25    MAX_SCAN_DEPTH as SAFE_MAX_SCAN_DEPTH,
26};
27
28impl Config {
29    pub fn from_str(contents: &str) -> Result<Self> {
30        Ok(toml::from_str(contents)?)
31    }
32
33    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
34        let contents = fs::read_to_string(path)?;
35        Self::from_str(&contents)
36    }
37}
38
39pub fn clamp_budget_target(target: BudgetTarget) -> BudgetTarget {
40    match target {
41        BudgetTarget::Tokens(tokens) => {
42            let clamped = limits::clamp_budget_tokens(tokens);
43            if clamped != tokens {
44                eprintln!(
45                    "SECURITY: token budget clamped to {}",
46                    SAFE_MAX_BUDGET_TOKENS
47                );
48            }
49            BudgetTarget::Tokens(clamped)
50        }
51        BudgetTarget::Bytes(bytes) => {
52            let clamped = limits::clamp_budget_bytes(bytes);
53            if clamped != bytes {
54                eprintln!(
55                    "SECURITY: byte budget clamped to {}",
56                    SAFE_MAX_BUDGET_BYTES
57                );
58            }
59            BudgetTarget::Bytes(clamped)
60        }
61    }
62}
63
64pub fn apply_security_overrides(config: &mut Config) {
65    if config.scanning.follow_symlinks {
66        eprintln!("SECURITY: follow_symlinks forced off");
67        config.scanning.follow_symlinks = false;
68    }
69
70    if config.scanning.include_hidden {
71        eprintln!("SECURITY: include_hidden forced off");
72        config.scanning.include_hidden = false;
73    }
74
75    if config.scanning.max_depth == 0 || config.scanning.max_depth > SAFE_MAX_SCAN_DEPTH {
76        eprintln!(
77            "SECURITY: max_depth clamped to {}",
78            SAFE_MAX_SCAN_DEPTH
79        );
80        config.scanning.max_depth = SAFE_MAX_SCAN_DEPTH;
81    }
82}
83
84impl Default for Config {
85    fn default() -> Self {
86        Self {
87            output: OutputConfig::default(),
88            scanning: ScanningConfig::default(),
89            categories: CategoryConfig::default(),
90            priority: PriorityWeights::default(),
91            priority_rules: default_priority_rules(),
92            exclude: ExcludeConfig::default(),
93            signatures: SignatureConfig::default(),
94        }
95    }
96}
97
98#[derive(Debug, Clone, Copy, Deserialize, Serialize, ValueEnum, PartialEq, Eq)]
99#[serde(rename_all = "lowercase")]
100pub enum OutputFormat {
101    Pipe,
102    Full,
103    Json,
104}
105
106impl Default for OutputFormat {
107    fn default() -> Self {
108        Self::Pipe
109    }
110}
111
112#[derive(Debug, Clone, Deserialize)]
113#[serde(default)]
114pub struct OutputConfig {
115    pub format: OutputFormat,
116    pub default_budget_tokens: usize,
117    pub default_budget_bytes: usize,
118}
119
120impl Default for OutputConfig {
121    fn default() -> Self {
122        Self {
123            format: OutputFormat::Pipe,
124            default_budget_tokens: 4000,
125            default_budget_bytes: 16_000,
126        }
127    }
128}
129
130#[derive(Debug, Clone, Deserialize)]
131#[serde(default)]
132pub struct ScanningConfig {
133    pub use_gitignore: bool,
134    pub include_hidden: bool,
135    pub max_depth: usize,
136    pub follow_symlinks: bool,
137    pub no_git_safety: bool,
138}
139
140impl Default for ScanningConfig {
141    fn default() -> Self {
142        Self {
143            use_gitignore: true,
144            include_hidden: false,
145            max_depth: 20,
146            follow_symlinks: false,
147            no_git_safety: true,
148        }
149    }
150}
151
152#[derive(Debug, Clone, Deserialize)]
153#[serde(default)]
154pub struct CategoryConfig {
155    pub code: Category,
156    pub docs: Category,
157    pub config: Category,
158    pub build: Category,
159    pub data: Category,
160}
161
162impl Default for CategoryConfig {
163    fn default() -> Self {
164        Self {
165            code: Category::new(
166                &[
167                    "rs", "go", "py", "ts", "tsx", "js", "jsx", "c", "cpp", "h", "hpp", "java",
168                    "rb", "ex", "exs",
169                ],
170                100,
171            ),
172            docs: Category::new(&["md", "mdx", "txt", "rst", "adoc"], 90),
173            config: Category::new(&["toml", "yaml", "yml", "json", "ini", "cfg"], 80),
174            build: Category::new(&["lock", "sum"], 20),
175            data: Category::new(&["csv", "sql"], 30),
176        }
177    }
178}
179
180#[derive(Debug, Clone, Deserialize)]
181pub struct Category {
182    pub extensions: Vec<String>,
183    pub priority: i32,
184}
185
186impl Category {
187    fn new(extensions: &[&str], priority: i32) -> Self {
188        Self {
189            extensions: extensions.iter().map(|ext| ext.to_string()).collect(),
190            priority,
191        }
192    }
193}
194
195#[derive(Debug, Clone, Deserialize)]
196pub struct PriorityRule {
197    pub pattern: String,
198    pub priority: i32,
199}
200
201/// Configurable priority weight adjustments.
202/// These modify the base priority score for files based on their characteristics.
203#[derive(Debug, Clone, Deserialize)]
204#[serde(default)]
205pub struct PriorityWeights {
206    /// Default priority for files that don't match any rule (default: 50)
207    pub default_priority: i32,
208    /// Boost for entry point files like main.rs, lib.rs, index.ts (default: 40)
209    pub entrypoint_boost: i32,
210    /// Boost for code files at repository root (default: 20)
211    pub root_code_boost: i32,
212    /// Boost for files in focus directories like src/, lib/, cmd/ (default: 15)
213    pub focus_dir_boost: i32,
214    /// Penalty for test files and directories (default: -40)
215    pub test_penalty: i32,
216    /// Penalty for fixture/mock files and directories (default: -25)
217    pub fixture_penalty: i32,
218    /// Penalty per depth level beyond 2 (default: -5)
219    pub depth_penalty_step: i32,
220    /// Maximum depth penalty (default: -30)
221    pub max_depth_penalty: i32,
222}
223
224impl Default for PriorityWeights {
225    fn default() -> Self {
226        Self {
227            default_priority: 50,
228            entrypoint_boost: 40,
229            root_code_boost: 20,
230            focus_dir_boost: 15,
231            test_penalty: -40,
232            fixture_penalty: -25,
233            depth_penalty_step: -5,
234            max_depth_penalty: -30,
235        }
236    }
237}
238
239#[derive(Debug, Clone, Deserialize)]
240#[serde(default)]
241pub struct ExcludeConfig {
242    pub patterns: Vec<String>,
243}
244
245impl Default for ExcludeConfig {
246    fn default() -> Self {
247        Self {
248            patterns: vec![
249                "target/".to_string(),
250                "node_modules/".to_string(),
251                "dist/".to_string(),
252                "build/".to_string(),
253                ".git/".to_string(),
254                "__pycache__/".to_string(),
255                "*.pyc".to_string(),
256                ".DS_Store".to_string(),
257                "*.min.js".to_string(),
258                "*.min.css".to_string(),
259                "vendor/".to_string(),
260                ".venv/".to_string(),
261                "venv/".to_string(),
262            ],
263        }
264    }
265}
266
267#[derive(Debug, Clone, Deserialize)]
268#[serde(default)]
269pub struct SignatureConfig {
270    pub enabled: bool,
271    pub languages: Vec<String>,
272    pub include_functions: bool,
273    pub include_structs: bool,
274    pub include_traits: bool,
275    pub include_interfaces: bool,
276    pub include_classes: bool,
277    pub include_types: bool,
278    pub include_constants: bool,
279    pub max_signature_length: usize,
280}
281
282impl Default for SignatureConfig {
283    fn default() -> Self {
284        Self {
285            enabled: true,
286            languages: vec![
287                "rust".to_string(),
288                "go".to_string(),
289                "python".to_string(),
290                "typescript".to_string(),
291                "javascript".to_string(),
292                "c".to_string(),
293                "cpp".to_string(),
294            ],
295            include_functions: true,
296            include_structs: true,
297            include_traits: true,
298            include_interfaces: true,
299            include_classes: true,
300            include_types: true,
301            include_constants: true,
302            max_signature_length: 200,
303        }
304    }
305}
306
307fn default_priority_rules() -> Vec<PriorityRule> {
308    vec![
309        PriorityRule {
310            pattern: "README*".to_string(),
311            priority: 200,
312        },
313        PriorityRule {
314            pattern: "AGENTS.md".to_string(),
315            priority: 200,
316        },
317        PriorityRule {
318            pattern: "CLAUDE.md".to_string(),
319            priority: 200,
320        },
321        PriorityRule {
322            pattern: "Cargo.toml".to_string(),
323            priority: 150,
324        },
325        PriorityRule {
326            pattern: "package.json".to_string(),
327            priority: 150,
328        },
329        PriorityRule {
330            pattern: "go.mod".to_string(),
331            priority: 150,
332        },
333        PriorityRule {
334            pattern: "src/main.*".to_string(),
335            priority: 140,
336        },
337        PriorityRule {
338            pattern: "src/lib.*".to_string(),
339            priority: 140,
340        },
341        PriorityRule {
342            pattern: "**/mod.rs".to_string(),
343            priority: 130,
344        },
345        PriorityRule {
346            pattern: "**/*_test.*".to_string(),
347            priority: 50,
348        },
349        PriorityRule {
350            pattern: "**/test_*".to_string(),
351            priority: 50,
352        },
353        PriorityRule {
354            pattern: "**/*.lock".to_string(),
355            priority: 10,
356        },
357    ]
358}
359
360const SECURITY_EXCLUDE_PATTERNS: &[&str] = &[
361    ".env",
362    ".env.*",
363    "*.pem",
364    "*.key",
365    "credentials*",
366];
367
368pub fn security_exclude_patterns() -> Vec<String> {
369    SECURITY_EXCLUDE_PATTERNS
370        .iter()
371        .map(|pattern| pattern.to_string())
372        .collect()
373}