alint_rules/
file_min_size.rs1use alint_core::{Context, Error, Level, Result, Rule, RuleSpec, Scope, Violation};
15use serde::Deserialize;
16
17#[derive(Debug, Deserialize)]
18struct Options {
19 min_bytes: u64,
20}
21
22#[derive(Debug)]
23pub struct FileMinSizeRule {
24 id: String,
25 level: Level,
26 policy_url: Option<String>,
27 message: Option<String>,
28 scope: Scope,
29 min_bytes: u64,
30}
31
32impl Rule for FileMinSizeRule {
33 fn id(&self) -> &str {
34 &self.id
35 }
36 fn level(&self) -> Level {
37 self.level
38 }
39 fn policy_url(&self) -> Option<&str> {
40 self.policy_url.as_deref()
41 }
42
43 fn evaluate(&self, ctx: &Context<'_>) -> Result<Vec<Violation>> {
44 let mut violations = Vec::new();
45 for entry in ctx.index.files() {
46 if !self.scope.matches(&entry.path) {
47 continue;
48 }
49 if entry.size < self.min_bytes {
50 let msg = self.message.clone().unwrap_or_else(|| {
51 format!(
52 "file below {} byte(s) (actual: {})",
53 self.min_bytes, entry.size,
54 )
55 });
56 violations.push(Violation::new(msg).with_path(entry.path.clone()));
57 }
58 }
59 Ok(violations)
60 }
61}
62
63pub fn build(spec: &RuleSpec) -> Result<Box<dyn Rule>> {
64 let Some(paths) = &spec.paths else {
65 return Err(Error::rule_config(
66 &spec.id,
67 "file_min_size requires a `paths` field",
68 ));
69 };
70 let opts: Options = spec
71 .deserialize_options()
72 .map_err(|e| Error::rule_config(&spec.id, format!("invalid options: {e}")))?;
73 Ok(Box::new(FileMinSizeRule {
74 id: spec.id.clone(),
75 level: spec.level,
76 policy_url: spec.policy_url.clone(),
77 message: spec.message.clone(),
78 scope: Scope::from_paths_spec(paths)?,
79 min_bytes: opts.min_bytes,
80 }))
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::test_support::{ctx, spec_yaml};
87 use alint_core::{FileEntry, FileIndex};
88 use std::path::Path;
89
90 fn idx_with_size(path: &str, size: u64) -> FileIndex {
91 FileIndex {
92 entries: vec![FileEntry {
93 path: std::path::Path::new(path).into(),
94 is_dir: false,
95 size,
96 }],
97 }
98 }
99
100 #[test]
101 fn build_rejects_missing_paths_field() {
102 let spec = spec_yaml(
103 "id: t\n\
104 kind: file_min_size\n\
105 min_bytes: 100\n\
106 level: warning\n",
107 );
108 assert!(build(&spec).is_err());
109 }
110
111 #[test]
112 fn evaluate_passes_when_size_above_minimum() {
113 let spec = spec_yaml(
114 "id: t\n\
115 kind: file_min_size\n\
116 paths: \"README.md\"\n\
117 min_bytes: 100\n\
118 level: warning\n",
119 );
120 let rule = build(&spec).unwrap();
121 let idx = idx_with_size("README.md", 1024);
122 let v = rule.evaluate(&ctx(Path::new("/fake"), &idx)).unwrap();
123 assert!(v.is_empty());
124 }
125
126 #[test]
127 fn evaluate_fires_when_size_below_minimum() {
128 let spec = spec_yaml(
129 "id: t\n\
130 kind: file_min_size\n\
131 paths: \"README.md\"\n\
132 min_bytes: 100\n\
133 level: warning\n",
134 );
135 let rule = build(&spec).unwrap();
136 let idx = idx_with_size("README.md", 10);
137 let v = rule.evaluate(&ctx(Path::new("/fake"), &idx)).unwrap();
138 assert_eq!(v.len(), 1);
139 }
140
141 #[test]
142 fn evaluate_size_at_exact_minimum_passes() {
143 let spec = spec_yaml(
146 "id: t\n\
147 kind: file_min_size\n\
148 paths: \"a.bin\"\n\
149 min_bytes: 100\n\
150 level: warning\n",
151 );
152 let rule = build(&spec).unwrap();
153 let idx = idx_with_size("a.bin", 100);
154 let v = rule.evaluate(&ctx(Path::new("/fake"), &idx)).unwrap();
155 assert!(v.is_empty(), "size == min should pass: {v:?}");
156 }
157
158 #[test]
159 fn evaluate_zero_byte_file_fires_when_minimum_positive() {
160 let spec = spec_yaml(
161 "id: t\n\
162 kind: file_min_size\n\
163 paths: \"empty.txt\"\n\
164 min_bytes: 1\n\
165 level: warning\n",
166 );
167 let rule = build(&spec).unwrap();
168 let idx = idx_with_size("empty.txt", 0);
169 let v = rule.evaluate(&ctx(Path::new("/fake"), &idx)).unwrap();
170 assert_eq!(v.len(), 1);
171 }
172}