1use crate::model::{
2 VbaModuleDescriptor, VbaModuleSourceResponse, VbaProjectSummaryResponse,
3 VbaReferenceDescriptor, WorkbookId,
4};
5use crate::state::AppState;
6use anyhow::{Result, anyhow, bail};
7use schemars::JsonSchema;
8use serde::Deserialize;
9use std::fs::File;
10use std::io::Read;
11use std::path::Path;
12use std::sync::Arc;
13use zip::result::ZipError;
14
15const MAX_VBA_PROJECT_BYTES: u64 = 20 * 1024 * 1024;
16const DEFAULT_MAX_MODULES: u32 = 200;
17const DEFAULT_INCLUDE_REFERENCES: bool = true;
18
19const DEFAULT_OFFSET_LINES: u32 = 0;
20const DEFAULT_LIMIT_LINES: u32 = 200;
21const MAX_LIMIT_LINES: u32 = 5_000;
22
23#[derive(Debug, Deserialize, JsonSchema)]
24pub struct VbaProjectSummaryParams {
25 #[serde(alias = "workbook_id")]
26 pub workbook_or_fork_id: WorkbookId,
27 #[serde(default)]
28 pub max_modules: Option<u32>,
29 #[serde(default)]
30 pub include_references: Option<bool>,
31}
32
33pub async fn vba_project_summary(
34 state: Arc<AppState>,
35 params: VbaProjectSummaryParams,
36) -> Result<VbaProjectSummaryResponse> {
37 let workbook = state.open_workbook(¶ms.workbook_or_fork_id).await?;
38 let raw = extract_vba_project_bin(&workbook.path)?;
39
40 if raw.is_none() {
41 return Ok(VbaProjectSummaryResponse {
42 workbook_id: workbook.id.clone(),
43 has_vba: false,
44 code_page: None,
45 sys_kind: None,
46 modules: Vec::new(),
47 modules_truncated: false,
48 references: Vec::new(),
49 references_truncated: false,
50 notes: vec!["No xl/vbaProject.bin found in workbook".to_string()],
51 });
52 }
53
54 let project = ovba::open_project(raw.unwrap())?;
55
56 let max_modules = params.max_modules.unwrap_or(DEFAULT_MAX_MODULES).max(1);
57 let include_references = params
58 .include_references
59 .unwrap_or(DEFAULT_INCLUDE_REFERENCES);
60
61 let mut modules: Vec<VbaModuleDescriptor> = Vec::new();
62 for module in project.modules.iter().take(max_modules as usize) {
63 let module_type = match module.module_type {
64 ovba::ModuleType::Procedural => "procedural",
65 ovba::ModuleType::DocClsDesigner => "doc_cls_designer",
66 }
67 .to_string();
68
69 modules.push(VbaModuleDescriptor {
70 name: module.name.clone(),
71 stream_name: module.stream_name.clone(),
72 doc_string: module.doc_string.clone(),
73 text_offset: module.text_offset as u64,
74 help_context: module.help_context,
75 module_type,
76 read_only: module.read_only,
77 private: module.private,
78 });
79 }
80
81 let modules_truncated = project.modules.len() > max_modules as usize;
82
83 let mut references: Vec<VbaReferenceDescriptor> = Vec::new();
84 let mut references_truncated = false;
85 if include_references {
86 for reference in project.references.iter() {
87 let (kind, debug) = summarize_reference(reference);
88 references.push(VbaReferenceDescriptor { kind, debug });
89 if references.len() >= 200 {
90 references_truncated = project.references.len() > references.len();
91 break;
92 }
93 }
94 }
95
96 let sys_kind = Some(
97 match project.information.sys_kind {
98 ovba::SysKind::Win16 => "win16",
99 ovba::SysKind::Win32 => "win32",
100 ovba::SysKind::MacOs => "macos",
101 ovba::SysKind::Win64 => "win64",
102 }
103 .to_string(),
104 );
105
106 Ok(VbaProjectSummaryResponse {
107 workbook_id: workbook.id.clone(),
108 has_vba: true,
109 code_page: Some(project.information.code_page),
110 sys_kind,
111 modules,
112 modules_truncated,
113 references,
114 references_truncated,
115 notes: Vec::new(),
116 })
117}
118
119#[derive(Debug, Deserialize, JsonSchema)]
120pub struct VbaModuleSourceParams {
121 #[serde(alias = "workbook_id")]
122 pub workbook_or_fork_id: WorkbookId,
123 pub module_name: String,
124 #[serde(default = "default_offset_lines")]
125 pub offset_lines: u32,
126 #[serde(default = "default_limit_lines")]
127 pub limit_lines: u32,
128}
129
130fn default_offset_lines() -> u32 {
131 DEFAULT_OFFSET_LINES
132}
133
134fn default_limit_lines() -> u32 {
135 DEFAULT_LIMIT_LINES
136}
137
138pub async fn vba_module_source(
139 state: Arc<AppState>,
140 params: VbaModuleSourceParams,
141) -> Result<VbaModuleSourceResponse> {
142 let workbook = state.open_workbook(¶ms.workbook_or_fork_id).await?;
143 let raw = extract_vba_project_bin(&workbook.path)?
144 .ok_or_else(|| anyhow!("No xl/vbaProject.bin found in workbook"))?;
145
146 let project = ovba::open_project(raw)?;
147 let source = project.module_source(¶ms.module_name)?;
148
149 let offset = params.offset_lines;
150 let limit = params.limit_lines.clamp(1, MAX_LIMIT_LINES);
151
152 let mut total_lines: u32 = 0;
153 let mut selected: Vec<&str> = Vec::new();
154
155 for (idx, line) in source.lines().enumerate() {
156 let idx = idx as u32;
157 total_lines = total_lines.saturating_add(1);
158 if idx < offset {
159 continue;
160 }
161 if selected.len() >= limit as usize {
162 continue;
163 }
164 selected.push(line);
165 }
166
167 if total_lines == 0 && !source.is_empty() {
168 total_lines = 1;
169 }
170
171 let truncated = total_lines.saturating_sub(offset) > limit;
172
173 let mut page = selected.join("\n");
174 if !page.is_empty() {
175 page.push('\n');
176 }
177
178 Ok(VbaModuleSourceResponse {
179 workbook_id: workbook.id.clone(),
180 module_name: params.module_name,
181 offset_lines: offset,
182 limit_lines: limit,
183 total_lines,
184 truncated,
185 source: page,
186 })
187}
188
189fn extract_vba_project_bin(path: &Path) -> Result<Option<Vec<u8>>> {
190 let file = File::open(path)
191 .map_err(|e| anyhow!("failed to open workbook {}: {}", path.display(), e))?;
192
193 let mut archive = zip::ZipArchive::new(file)
194 .map_err(|e| anyhow!("failed to open workbook zip {}: {}", path.display(), e))?;
195
196 let mut entry = match archive.by_name("xl/vbaProject.bin") {
197 Ok(f) => f,
198 Err(ZipError::FileNotFound) => return Ok(None),
199 Err(e) => return Err(anyhow!("failed to locate xl/vbaProject.bin: {}", e)),
200 };
201
202 let declared_size = entry.size();
203 if declared_size > MAX_VBA_PROJECT_BYTES {
204 bail!(
205 "xl/vbaProject.bin too large ({} bytes; max {} bytes)",
206 declared_size,
207 MAX_VBA_PROJECT_BYTES
208 );
209 }
210
211 let mut buf: Vec<u8> = Vec::with_capacity(declared_size.min(1024 * 1024) as usize);
212 entry
213 .read_to_end(&mut buf)
214 .map_err(|e| anyhow!("failed to read xl/vbaProject.bin: {}", e))?;
215
216 if buf.len() as u64 > MAX_VBA_PROJECT_BYTES {
217 bail!(
218 "xl/vbaProject.bin too large after read ({} bytes; max {} bytes)",
219 buf.len(),
220 MAX_VBA_PROJECT_BYTES
221 );
222 }
223
224 Ok(Some(buf))
225}
226
227fn summarize_reference(reference: &ovba::Reference) -> (String, String) {
228 let kind = match reference {
229 ovba::Reference::Control(_) => "control",
230 ovba::Reference::Original(_) => "original",
231 ovba::Reference::Registered(_) => "registered",
232 ovba::Reference::Project(_) => "project",
233 }
234 .to_string();
235
236 let mut debug = format!("{:?}", reference);
237 const MAX_DEBUG_BYTES: usize = 4096;
238 if debug.len() > MAX_DEBUG_BYTES {
239 debug.truncate(MAX_DEBUG_BYTES);
240 debug.push_str("...[truncated]");
241 }
242
243 (kind, debug)
244}