malbox_plugin_sdk/runtime/guest/
collector.rs1use crate::context::Context;
8use crate::result::PluginResult;
9use globset::{Glob, GlobSetBuilder};
10use std::collections::HashSet;
11use std::path::{Path, PathBuf};
12use tracing::{debug, warn};
13
14#[derive(Debug, Clone)]
16pub(crate) struct AutoCollectSection {
17 pub enabled: bool,
18 pub include: Vec<String>,
19 pub exclude: Vec<String>,
20 pub max_file_size: u64,
21}
22
23pub(crate) fn auto_collect(
29 ctx: &Context,
30 dir: &Path,
31 config: &AutoCollectSection,
32 claimed_paths: Option<&HashSet<PathBuf>>,
33 result_prefix: &str,
34) {
35 if !config.enabled {
36 return;
37 }
38
39 if !dir.exists() {
40 debug!(dir = %dir.display(), "auto-collect directory does not exist, skipping");
41 return;
42 }
43
44 let include_set = match build_globset(&config.include) {
45 Ok(s) => s,
46 Err(e) => {
47 warn!(error = %e, "failed to compile auto-collect include patterns, skipping");
48 return;
49 }
50 };
51
52 let exclude_set = match build_globset(&config.exclude) {
53 Ok(s) => s,
54 Err(e) => {
55 warn!(error = %e, "failed to compile auto-collect exclude patterns, skipping");
56 return;
57 }
58 };
59
60 let entries = match collect_files(dir) {
61 Ok(e) => e,
62 Err(e) => {
63 warn!(error = %e, dir = %dir.display(), "failed to walk auto-collect directory");
64 return;
65 }
66 };
67
68 for file_path in entries {
69 let relative = match file_path.strip_prefix(dir) {
70 Ok(r) => r,
71 Err(_) => continue,
72 };
73
74 let rel_str = relative.to_string_lossy().into_owned();
75
76 if !include_set.is_match(relative) {
77 continue;
78 }
79 if exclude_set.is_match(relative) {
80 continue;
81 }
82
83 if let Some(claimed) = claimed_paths
84 && let Ok(canonical) = std::fs::canonicalize(&file_path)
85 && claimed.contains(&canonical)
86 {
87 debug!(path = %rel_str, "skipping already-claimed artifact");
88 continue;
89 }
90
91 let size = match std::fs::metadata(&file_path) {
92 Ok(m) => m.len(),
93 Err(e) => {
94 warn!(path = %rel_str, error = %e, "failed to stat file, skipping");
95 continue;
96 }
97 };
98
99 if size > config.max_file_size {
100 debug!(
101 path = %rel_str,
102 size,
103 max = config.max_file_size,
104 "file exceeds max_file_size, skipping"
105 );
106 continue;
107 }
108
109 let result_name = if result_prefix.is_empty() {
110 rel_str.to_string()
111 } else {
112 format!("{result_prefix}/{rel_str}")
113 };
114
115 if let Err(e) = ctx
116 .results()
117 .push(PluginResult::file(result_name.clone(), &file_path))
118 {
119 warn!(
120 path = %file_path.display(),
121 error = %e,
122 "failed to auto-collect file"
123 );
124 } else {
125 debug!(result_name, "auto-collected file");
126 }
127 }
128}
129
130fn build_globset(patterns: &[String]) -> std::result::Result<globset::GlobSet, globset::Error> {
131 let mut builder = GlobSetBuilder::new();
132 for pattern in patterns {
133 builder.add(Glob::new(pattern)?);
134 }
135 builder.build()
136}
137
138fn collect_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
139 let mut files = Vec::new();
140 collect_files_recursive(dir, &mut files)?;
141 files.sort();
142 Ok(files)
143}
144
145fn collect_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
146 for entry in std::fs::read_dir(dir)? {
147 let entry = entry?;
148 let path = entry.path();
149 if path.is_dir() {
150 collect_files_recursive(&path, files)?;
151 } else if path.is_file() {
152 files.push(path);
153 }
154 }
155 Ok(())
156}