ovba_tonysd_fork/lib.rs
1//! An Office VBA project parser written in 100% safe Rust.
2//!
3//! This is a (partial) implementation of the [\[MS-OVBA\]: Office VBA File Format
4//! Structure][MS-OVBA] protocol (Revision 9.1, published 2020-02-19).
5//!
6//! The main entry point into the API is the [`Project`] type, returned by the
7//! [`open_project`] function.
8//!
9//! # Usage
10//!
11//! Opening a project:
12//!
13//! ```rust,no_run
14//! use std::fs::read;
15//! use ovba::open_project;
16//!
17//! let data = read("vbaProject.bin")?;
18//! let project = open_project(data)?;
19//! # Ok::<(), ovba::Error>(())
20//! ```
21//!
22//! A more complete example that dumps an entire VBA project's source code:
23//!
24//! ```rust,no_run
25//! use std::fs::{read, write};
26//! use ovba::open_project;
27//!
28//! let data = read("vbaProject.bin")?;
29//! let project = open_project(data)?;
30//!
31//! for module in &project.modules {
32//! let src_code = project.module_source_raw(&module.name)?;
33//! write("./out/".to_string() + &module.name, src_code)?;
34//! }
35//! # Ok::<(), ovba::Error>(())
36//! ```
37//!
38//! The API also supports low-level access to the [\[MS-CFB\]: Compound File Binary File
39//! Format][MS-CFB] data. The following example lists all CFB entries:
40//!
41//! ```rust,no_run
42//! use std::fs::read;
43//! use ovba::open_project;
44//!
45//! let data = read("vbaProject.bin")?;
46//! let project = open_project(data)?;
47//! for (name, path) in &project.list()? {
48//! println!(r#"Name: "{}"; Path: "{}""#, name, path);
49//! }
50//! # Ok::<(), ovba::Error>(())
51//! ```
52//!
53//! [MS-OVBA]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc
54//! [MS-CFB]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b
55
56#![forbid(unsafe_code)]
57#![warn(rust_2018_idioms, missing_docs)]
58
59mod error;
60pub use crate::error::{Error, Result};
61
62mod parser;
63
64use cfb::CompoundFile;
65use parser::cp_to_string;
66use std::collections::HashMap;
67use std::convert::TryInto;
68use std::fs;
69use log::{debug, info, error};
70
71use std::{
72 cell::RefCell,
73 io::{Cursor, Read, Write},
74 path::{Path, PathBuf},
75 str::FromStr,
76};
77
78/// Represents a VBA project.
79///
80/// This type serves as the entry point into this crate's functionality and exposes the
81/// public API surface.
82pub struct Project {
83 /// Specifies version-independent information for the VBA project.
84 pub information: Information,
85 /// Specifies the external references of the VBA project.
86 pub references: Vec<Reference>,
87 /// Specifies the modules in the project.
88 pub modules: Vec<Module>,
89 container: RefCell<CompoundFile<Cursor<Vec<u8>>>>,
90 /// The original raw data of the VBA project file (e.g., vbaProject.bin).
91 /// This can be used for operations that require the untouched source or for full rebuilds.
92 pub original_raw_data: Vec<u8>,
93}
94
95/// Specifies the platform for which the VBA project is created.
96#[derive(Debug)]
97pub enum SysKind {
98 /// For 16-bit Windows Platforms.
99 Win16,
100 /// For 32-bit Windows Platforms.
101 Win32,
102 /// For Macintosh Platforms.
103 MacOs,
104 /// For 64-bit Windows Platforms.
105 Win64,
106}
107
108// TODO: Remove exemption once the implementation is complete.
109#[allow(dead_code)]
110/// Specifies a reference to a twiddled type library and its extended type library.
111#[derive(Debug)]
112pub struct ReferenceControl {
113 /// (Optional) Name entry
114 name: Option<String>,
115 libid_original: Option<String>,
116 libid_twiddled: String,
117 name_extended: Option<String>,
118 libid_extended: String,
119 guid: Vec<u8>, // Should be an `[u8; 16]`, though I'm not sure how to convert &[u8] returned by the parser into an array.
120 /// MUST be Unique for each `ReferenceControl` in the VBA projectwith the same
121 /// libid_original.
122 cookie: u32,
123}
124
125// TODO: Remove exemption once the implementation is complete.
126#[allow(dead_code)]
127/// Specifies the identifier of the Automation type library the containing
128/// [`ReferenceControl`]'s twiddled type library was generated from.
129#[derive(Debug)]
130pub struct ReferenceOriginal {
131 /// (Optional) Name entry
132 name: Option<String>,
133 libid_original: String,
134}
135
136// TODO: Remove exemption once the implementation is complete.
137#[allow(dead_code)]
138/// Specifies a reference to an Automation type library.
139#[derive(Debug)]
140pub struct ReferenceRegistered {
141 name: Option<String>,
142 libid: String,
143}
144
145// TODO: Remove exemption once the implementation is complete.
146#[allow(dead_code)]
147/// Specifies a reference to an external VBA project.
148#[derive(Debug)]
149pub struct ReferenceProject {
150 name: Option<String>,
151 libid_absolute: String,
152 libid_relative: String,
153 major_version: u32,
154 minor_version: u16,
155}
156
157/// Specifies a reference to an Automation type library or VBA project.
158#[derive(Debug)]
159pub enum Reference {
160 /// The `Reference` is a [`ReferenceControl`].
161 Control(ReferenceControl),
162 /// The `Reference` is a [`ReferenceOriginal`].
163 Original(ReferenceOriginal),
164 /// The `Reference` is a [`ReferenceRegistered`].
165 Registered(ReferenceRegistered),
166 /// The `Reference` is a [`ReferenceProject`].
167 Project(ReferenceProject),
168}
169
170// TODO: Remove exemption once the implementation is complete.
171#[allow(dead_code)]
172/// Specifies version-independent information for the VBA project.
173#[derive(Debug)]
174pub struct Information {
175 /// Specifies the platform for which the VBA project is created.
176 pub sys_kind: SysKind,
177 compat: Option<u32>,
178 lcid: u32,
179 lcid_invoke: u32,
180 /// Specifies the code page for the VBA project.
181 ///
182 pub code_page: u16,
183 name: String,
184 doc_string: String,
185 help_file_1: String,
186 help_context: u32,
187 lib_flags: u32,
188 version_major: u32,
189 version_minor: u16,
190 constants: Option<String>,
191}
192
193/// Specifies the containing module's type.
194#[derive(Debug)]
195pub enum ModuleType {
196 /// Specifies a procedural module.
197 ///
198 /// A procedural module is a collection of subroutines and functions.
199 Procedural,
200 /// Specifies a document module, class module, or designer module.
201 ///
202 /// A document module is a type of VBA project item that specifies a module for
203 /// embedded macros and programmatic access operations that are associated with a
204 /// document.
205 ///
206 /// A class module is a module that contains the definition for a new object. Each
207 /// instance of a class creates a new object, and procedures that are defined in the
208 /// module become properties and methods of the object.
209 ///
210 /// A designer module is a VBA module that extends the methods and properties of an
211 /// ActiveX control that has been registered with the project.
212 ///
213 /// The file format specification doesn't distinguish between these three module
214 /// types and encodes them using a single umbrella type ID.
215 DocClsDesigner,
216}
217
218/// Specifies data for a module.
219#[derive(Debug)]
220pub struct Module {
221 /// Specifies a VBA identifier as the name of the containing `Module`.
222 pub name: String,
223 /// Specifies the stream name in the VBA storage corresponding to the containing
224 /// `Module`.
225 pub stream_name: String,
226 /// Specifies the description for the containing `Module`.
227 pub doc_string: String,
228 /// Specifies the location of the source code within the stream that corresponds to
229 /// the containing `Module`.
230 pub text_offset: usize,
231 /// Specifies the Help topic identifier for the containing `Module`.
232 pub help_context: u32,
233 /// Specifies whether the containing `Module` is a procedural module, document
234 /// module, class module, or designer module.
235 pub module_type: ModuleType,
236 /// Specifies that the containing `Module` is read-only.
237 pub read_only: bool,
238 /// Specifies that the containing `Module` is only usable from within the current VBA
239 /// project.
240 pub private: bool,
241}
242
243impl Project {
244 /// Returns a stream's decompressed data.
245 ///
246 /// This function reads a stream referenced by `stream_path` and passes the data
247 /// starting at `offset` into the RLE decompressor.
248 ///
249 /// The primary use case for this function is to extract source code from VBA
250 /// [`Module`]s. The respective `offset` is reported by [`Module::text_offset`].
251 ///
252 /// This is a low-level function that is useful for very specific use cases only.
253 /// Client code that needs to read source code should use [`Project::module_source`]
254 /// or [`Project::module_source_raw`] instead.
255 // TODO: Code example
256 pub fn decompress_stream_from<P>(&self, stream_path: P, offset: usize) -> Result<Vec<u8>>
257 where
258 P: AsRef<Path> + std::fmt::Debug,
259 {
260 debug!("decompress_stream_from: Reading stream: {:?}, offset: {}", stream_path, offset);
261 let data = self.read_stream(stream_path.as_ref())?;
262 debug!("decompress_stream_from: Read {} bytes from stream. Data (first ~20 bytes after offset, if any): {:?}", data.len(), data.get(offset..std::cmp::min(data.len(), offset + 20)));
263
264 if offset > data.len() {
265 error!("decompress_stream_from: offset ({}) > data length ({})", offset, data.len());
266 return Err(Error::Decompressor);
267 }
268
269 if data.get(offset..).map_or(true, |s| s.is_empty()) {
270 error!("decompress_stream_from: Data slice for decompression is empty (offset: {}, data.len(): {})", offset, data.len());
271 return Err(Error::Decompressor);
272 }
273
274 if data.get(offset) != Some(&0x01) {
275 error!("decompress_stream_from: Expected SigByte 0x01 at offset {}, but found {:X?}", offset, data.get(offset));
276 }
277
278 match parser::decompress(&data[offset..]) {
279 Ok((remainder, decompressed_data)) => {
280 debug!("decompress_stream_from: Decompressed {} bytes. Remainder after decompression (nom): {} bytes.", decompressed_data.len(), remainder.len());
281 if !remainder.is_empty() {
282 debug!("decompress_stream_from: Warning: after decompression, there is an unprocessed remainder of {} bytes: {:?}", remainder.len(), remainder.get(..std::cmp::min(remainder.len(), 20)));
283 }
284 Ok(decompressed_data)
285 }
286 Err(e) => {
287 error!("decompress_stream_from: Error nom during decompression: {:?}", e);
288 Err(Error::Decompressor)
289 }
290 }
291 }
292
293 // TODO: This should probably live someplace else. It exposes information internal to
294 // the CFB implementation, that's not *immediately* useful or related to this
295 // library's primary responsibility.
296
297 /// Returns a list of entries (storages and streams) in the raw binary data. Each
298 /// entry is represented as a tuple of two `String`s, where the first element
299 /// contains the entry's name and the second element the entry's path inside the
300 /// CFB.
301 ///
302 /// The raw binary data is encoded as a [Compound File Binary][MS-CFB]
303 ///
304 /// [MS-CFB]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b
305 pub fn list(&self) -> Result<Vec<(String, String)>> {
306 let mut result = Vec::new();
307 for entry in self
308 .container
309 .borrow()
310 .walk_storage("/")
311 .map_err(Error::Cfb)?
312 {
313 result.push((
314 entry.name().to_owned(),
315 entry.path().to_str().unwrap_or_default().to_owned(),
316 ));
317 }
318 Ok(result)
319 }
320
321 /// Returns a module's source code.
322 ///
323 /// Similar to [`Project::module_source_raw`] this function returns the source code
324 /// of a project's module. After the raw source code has been decoded it is then
325 /// converted to a `String` using the project's code page.
326 pub fn module_source(&self, name: &str) -> Result<String> {
327 let source_raw = self.module_source_raw(name)?;
328 let source = cp_to_string(&source_raw, self.information.code_page);
329
330 Ok(source)
331 }
332
333 /// Returns the raw source code from a module.
334 ///
335 /// The result contains a module's source code as is. No character encoding conversion
336 /// is done. The data is encoded using the project's code page available through
337 /// [`Information::code_page`].
338 pub fn module_source_raw(&self, name: &str) -> Result<Vec<u8>> {
339 let module = self
340 .modules
341 .iter()
342 .find(|&module| module.name == name)
343 .ok_or_else(|| Error::ModuleNotFound(name.to_owned()))?;
344
345 // `PathBuf::from_str` cannot fail (`type Err = Infallible`). The subsequent
346 // `unwrap` thus won't `panic!`. No path separator normalization is done in the
347 // process; this is intentional.
348 let path = PathBuf::from_str("/VBA").unwrap().join(&module.stream_name);
349 let offset = module.text_offset;
350 let src_code = self.decompress_stream_from(path, offset)?;
351
352 Ok(src_code)
353 }
354
355 /// Returns a stream's contents.
356 ///
357 /// This is a low-level function operating on the CFB data. The CFB is the storage
358 /// container of the raw binary VBA project.
359 pub fn read_stream<P>(&self, stream_path: P) -> Result<Vec<u8>>
360 where
361 P: AsRef<Path>,
362 {
363 let mut buffer = Vec::new();
364 self.container
365 .borrow_mut()
366 .open_stream(stream_path)
367 .map_err(Error::Cfb)?
368 .read_to_end(&mut buffer)
369 .map_err(Error::Cfb)?;
370
371 Ok(buffer)
372 }
373
374 /// Sets the source code for a given module.
375 ///
376 /// This function attempts an "in-place" modification of the CFB container.
377 /// It modifies the `container` field directly.
378 /// The `dir` stream is NOT updated by this function, which might lead to
379 /// inconsistencies if module sizes/counts change significantly.
380 /// Call `save()` to get the modified project bytes.
381 pub fn set_module_source(&mut self, module_name: &str, new_source: &str) -> Result<()> {
382 debug!("set_module_source: Starting for module: {}", module_name);
383 let module = self
384 .modules
385 .iter()
386 .find(|m| m.name == module_name)
387 .ok_or_else(|| Error::ModuleNotFound(module_name.to_owned()))?;
388 debug!("set_module_source: Module found: {:?}, text_offset: {}", module.stream_name, module.text_offset);
389
390 let stream_path = PathBuf::from("/VBA").join(&module.stream_name);
391
392 // 1. Прочитать оригинальный поток модуля, чтобы получить PerformanceCache
393 let original_module_stream_data = self.read_stream(&stream_path)?;
394 debug!("set_module_source: Original module stream size: {}", original_module_stream_data.len());
395
396 if module.text_offset > original_module_stream_data.len() {
397 error!("set_module_source: text_offset ({}) > stream data length ({})", module.text_offset, original_module_stream_data.len());
398 return Err(Error::Generic(
399 "Module text_offset exceeds stream data length".to_string(),
400 ));
401 }
402 let performance_cache = &original_module_stream_data[..module.text_offset];
403 debug!("set_module_source: PerformanceCache size: {}", performance_cache.len());
404
405 // 2. Преобразовать новый исходный код в байты
406 debug!("set_module_source: New source code (first 50 characters): {:.50}", new_source.replace('\n', "\\n"));
407 let new_source_bytes = parser::string_to_cp(new_source, self.information.code_page);
408 debug!("set_module_source: New source code size (before compression): {}", new_source_bytes.len());
409
410 // 3. Сжать новые байты в CompressedData (это результат parser::compress, он включает FlagByte и TokenData)
411 let compressed_module_code_data = parser::compress(&new_source_bytes)?;
412 debug!("set_module_source: Compressed module code data size (FlagByte + TokenData): {}", compressed_module_code_data.len());
413
414 // 4. Сформировать CompressedChunkHeader (u16)
415 // CompressedChunkHeader: [F S S S] [S S S S] [S S S S S S S S]
416 // F: бит 15 (CompressedFlag: 1 if compressed, 0 if raw)
417 // SSS: биты 12-14 (Signature: 0b011)
418 // SSSSSSSSSSSSS: биты 0-11 (Size: actual_compressed_data_length_in_chunk - 1)
419
420 let actual_compressed_data_length_in_chunk = compressed_module_code_data.len();
421 if actual_compressed_data_length_in_chunk == 0 {
422 // Не должно быть 0, т.к. parser::compress должен вернуть хотя бы FlagByte
423 // или если вход пустой, то это особый случай (но VBA модули редко пустые)
424 debug!("set_module_source: compressed_module_code_data has zero length. This is unexpected.");
425 // Если это возможно, нужно решить, какой заголовок ставить. Пока что оставим ошибку.
426 return Err(Error::Generic("Compressed data part is empty, cannot form header".to_string()));
427 }
428 if actual_compressed_data_length_in_chunk > 0xFFF + 1 { // 0xFFF (4095) + 1 = 4096
429 return Err(Error::Generic(
430 format!("Compressed data part too large ({}) for chunk size field (max 4096)", actual_compressed_data_length_in_chunk)
431 ));
432 }
433 let size_for_header_field = (actual_compressed_data_length_in_chunk - 1) as u16; // Max 0xFFF (4095)
434
435 const COMPRESSED_FLAG_BIT: u16 = 1 << 15; // 1 для сжатого
436 const SIGNATURE_BITS: u16 = 0b011 << 12;
437
438 let chunk_header: u16 = COMPRESSED_FLAG_BIT | SIGNATURE_BITS | size_for_header_field;
439
440 debug!("set_module_source: CompressedChunkHeader: Value=0x{:04X} (Flag:1, Sig:0b011, SizeField:{})",
441 chunk_header, size_for_header_field
442 );
443
444 // 5. Собрать новое содержимое потока модуля: PerformanceCache + SigByte (для всего контейнера) + CompressedChunkHeader + CompressedData
445 const SIG_BYTE_CONTAINER: u8 = 0x01;
446 let mut new_module_stream_content = Vec::with_capacity(
447 performance_cache.len() + 1 + 2 + compressed_module_code_data.len() // PerfCache + SigByteContainer + ChunkHeader(u16) + CompressedData
448 );
449 new_module_stream_content.extend_from_slice(performance_cache);
450 new_module_stream_content.push(SIG_BYTE_CONTAINER);
451 new_module_stream_content.extend_from_slice(&chunk_header.to_le_bytes()); // CompressedChunkHeader (2 байта)
452 new_module_stream_content.extend_from_slice(&compressed_module_code_data); // CompressedData (FlagByte + TokenData)
453
454 debug!("set_module_source: Total size of new_module_stream_content for writing to stream: {}", new_module_stream_content.len());
455
456 // 6. Заменить поток в CFB контейнере
457 debug!("set_module_source: Attempting to write to CFB stream: {:?}", stream_path);
458 let mut cfb = self.container.borrow_mut();
459
460 if cfb.exists(&stream_path) {
461 debug!("set_module_source: Removing existing stream: {:?}", stream_path);
462 cfb.remove_stream(&stream_path).map_err(Error::Cfb)?;
463 } else {
464 debug!("set_module_source: Stream {:?} does not exist, will be created.", stream_path);
465 }
466
467 let mut stream_writer = cfb.create_stream(&stream_path).map_err(Error::Cfb)?;
468 debug!("set_module_source: Stream created, writing {} bytes...", new_module_stream_content.len());
469 stream_writer
470 .write_all(&new_module_stream_content)
471 .map_err(Error::Io)?;
472 debug!("set_module_source: Writing to stream completed.");
473
474 debug!("set_module_source: Completion.");
475 Ok(())
476 }
477
478 /// Saves the project to the specified file path.
479 /// After this operation, the internal CFB container of this `Project` instance
480 /// will be consumed and no longer usable for further CFB operations.
481 pub fn save(&mut self, output_path: &Path) -> Result<()> {
482 // It is crucial to manage the borrow of self.container carefully.
483 // First, we borrow mutably to flush.
484 let mut cfb_guard = self.container.borrow_mut();
485 cfb_guard.flush().map_err(Error::Cfb)?;
486 // Then, we must drop the guard *before* calling `self.container.replace()`,
487 // as `replace()` also needs to borrow mutably.
488 drop(cfb_guard);
489
490 // To move the `CompoundFile` out of the `RefCell`, we replace it with a dummy.
491 // `CompoundFile::create` will give us an empty but valid CFB in memory.
492 let dummy_cfb = match CompoundFile::create(Cursor::new(Vec::new())) {
493 Ok(cfb) => cfb,
494 Err(e) => {
495 // This should ideally not happen with a Vec-backed cursor for create.
496 // If it does, it's a more fundamental issue with the cfb crate or environment.
497 eprintln!("[ERROR save] Failed to create dummy CompoundFile: {:?}", e);
498 return Err(Error::Cfb(e)); // Convert cfb::Error to our Error::Cfb
499 }
500 };
501 let original_compound_file = self.container.replace(dummy_cfb);
502
503 // `original_compound_file.into_inner()` should directly return `Cursor<Vec<u8>>` (the W type).
504 // If there were an error here (e.g., if W was a file and couldn't be finalized),
505 // `cfb` crate design implies it would panic or handle error during flush/write, not in `into_inner()` for `Cursor<Vec<u8>>`.
506 let cursor: Cursor<Vec<u8>> = original_compound_file.into_inner();
507
508 let updated_project_bytes = cursor.into_inner();
509
510 debug!("save: Updated project size for writing: {} bytes", updated_project_bytes.len());
511
512 fs::write(output_path, updated_project_bytes).map_err(Error::Io)?;
513
514 debug!("save: Project successfully saved to: {:?}", output_path);
515 Ok(())
516 }
517}
518
519/// Opens a VBA project.
520///
521/// This function consumes `raw` and returns a [`Project`] struct on success, populated
522/// with data from the parsed binary input.
523pub fn open_project(raw: Vec<u8>) -> Result<Project> {
524 // `raw` будет использован для создания Cursor для CompoundFile.
525 // Также сохраним копию `raw` в структуре Project для возможной полной пересборки
526 // или если понадобится оригинальный, нетронутый файл.
527 let original_data_clone = raw.clone(); // Клонируем для хранения в Project
528 let data_for_reading_cursor = Cursor::new(raw); // `raw` перемещается сюда
529
530 let mut cfb_container = CompoundFile::open(data_for_reading_cursor).map_err(Error::Cfb)?;
531
532 // Read *dir* stream
533 #[cfg(target_family = "windows")]
534 const DIR_STREAM_PATH: &str = "/VBA\\dir";
535 #[cfg(not(target_family = "windows"))]
536 const DIR_STREAM_PATH: &str = "/VBA/dir";
537
538 let mut buffer = Vec::new();
539 cfb_container
540 .open_stream(DIR_STREAM_PATH)
541 .map_err(Error::Cfb)?
542 .read_to_end(&mut buffer)
543 .map_err(Error::Cfb)?;
544
545 // Decompress stream
546 let (_remainder_after_decompress, decompressed_buffer) =
547 match parser::decompress(&buffer) {
548 Ok((remainder, buffer_content)) => (remainder, buffer_content),
549 Err(_nom_err) => {
550 // Можно добавить логирование _nom_err здесь, если необходимо
551 // eprintln!("Decompression error: {:?}", _nom_err);
552 return Err(Error::Decompressor);
553 }
554 };
555 // Убедимся, что весь буфер был использован, если это важно для логики парсера.
556 // Оригинальный код использовал debug_assert!(_remainder_after_decompress.is_empty());
557 // Если parser::decompress должен всегда потреблять весь ввод или это ошибка,
558 // то здесь можно добавить проверку if !_remainder_after_decompress.is_empty() { return Err(Error::Decompressor); }
559
560 // Parse binary data
561 let (_remainder_after_parse, project_info_data) =
562 match parser::parse_project_information(&decompressed_buffer) {
563 Ok((remainder, info_content)) => (remainder, info_content),
564 Err(_nom_err) => {
565 // Можно добавить логирование _nom_err здесь
566 // eprintln!("Parsing error: {:?}", _nom_err);
567 return Err(Error::Parser);
568 }
569 };
570 // Аналогично, проверка остатка, если необходимо.
571 // if !_remainder_after_parse.is_empty() { return Err(Error::Parser); }
572
573 Ok(Project {
574 information: project_info_data.information,
575 references: project_info_data.references,
576 modules: project_info_data.modules,
577 container: RefCell::new(cfb_container),
578 original_raw_data: original_data_clone,
579 })
580}
581
582#[cfg(test)]
583mod main_test {
584 use super::*; // Импорт всего из lib.rs
585 use std::fs;
586 use std::path::Path;
587
588 fn run_vba_modification_simulation(project_path: &Path, module_to_modify: &str) -> Result<()> {
589 colog::init();
590 debug!("run_vba_modification_simulation: Loading VBA project from: {:?}", project_path);
591 let initial_data = fs::read(project_path).map_err(Error::Io)?;
592
593 let mut project = open_project(initial_data.clone())?;
594 debug!("run_vba_modification_simulation: Project successfully opened.");
595
596 debug!("run_vba_modification_simulation: Reading source code of module '{}' (before modification):", module_to_modify);
597 match project.module_source(module_to_modify) {
598 Ok(source) => {
599 debug!("------------------------------------");
600 debug!("{}", source);
601 debug!("------------------------------------");
602 }
603 Err(e) => {
604 error!("run_vba_modification_simulation: Error reading source code of module '{}': {:?}", module_to_modify, e);
605 if project.modules.is_empty() {
606 error!("run_vba_modification_simulation: No parsed modules found in the project.");
607 } else {
608 error!("run_vba_modification_simulation: Parsed modules in the project:");
609 for m in &project.modules {
610 error!(" - Имя: {}, Поток: {}", m.name, m.stream_name);
611 }
612 }
613 return Err(e);
614 }
615 }
616
617 let original_source = project.module_source(module_to_modify)?;
618 let modified_source = format!("{}\n' This is a test\n", original_source);
619 debug!("run_vba_modification_simulation: Modifying source code of module '{}'...", module_to_modify);
620 project.set_module_source(module_to_modify, &modified_source)?;
621 debug!("run_vba_modification_simulation: Source code of module '{}' updated in memory.", module_to_modify);
622
623 debug!("run_vba_modification_simulation: Saving changes to file...");
624 let output_file_path = project_path.with_file_name(format!("{}_patched.bin", project_path.file_stem().unwrap_or_default().to_string_lossy()));
625 project.save(&output_file_path)?;
626 debug!("run_vba_modification_simulation: Changes saved to file: {:?}", output_file_path);
627 debug!("run_vba_modification_simulation: Original 'project' instance should no longer be used for CFB operations.");
628
629 // ТЕПЕРЬ ПРОВЕРЯЕМ ЗАГРУЗКОЙ ИЗ СОХРАНЕННОГО ФАЙЛА
630 debug!("run_vba_modification_simulation: Reading and checking saved file: {:?}", output_file_path);
631 let patched_data = fs::read(&output_file_path).map_err(Error::Io)?;
632 let patched_project = open_project(patched_data)?; // Загружаем в новый экземпляр
633
634 debug!("run_vba_modification_simulation: Reading source code of module '{}' from reloaded project:", module_to_modify);
635 match patched_project.module_source(module_to_modify) {
636 Ok(source) => {
637 debug!("------------------------------------");
638 debug!("{}", source);
639 debug!("------------------------------------");
640
641 if source.contains("' This is a test") {
642 debug!("run_vba_modification_simulation: Success: Changes found in saved and reloaded file!");
643 } else {
644 error!("run_vba_modification_simulation: Error: Changes NOT found in saved and reloaded file!");
645 return Err(Error::Generic("Verification failed: Changes not found in reloaded file".to_string())); // Возвращаем ошибку, чтобы тест упал
646 }
647 }
648 Err(e) => {
649 error!("run_vba_modification_simulation: Error reading source code of module from saved file: {:?}", e);
650 return Err(e);
651 }
652 }
653
654 Ok(())
655 }
656
657 #[test]
658 fn test_vba_modification() {
659 // !!! ЗАМЕНИТЕ ЭТИ ЗНАЧЕНИЯ !!!
660 let project_file_path_str = "test/vbaProject.bin"; // Например, "tests/test-files/vbaProject.bin"
661 let module_name = "NewMacros"; // Например, "Module1" или имя существующего модуля
662 // !!! КОНЕЦ ЗАМЕНЫ !!!
663
664 let project_file_path = Path::new(project_file_path_str);
665
666 if !project_file_path.exists() {
667 error!("test_vba_modification: Test VBA project file {:?} not found. Test will be ignored.", project_file_path);
668 // Чтобы тест не падал, а просто игнорировался, если файла нет, можно сделать так:
669 //eprintln!("Пожалуйста, создайте файл или укажите правильный путь.");
670 //return; // Игнорировать тест, если файла нет.
671 // Или паниковать, чтобы CI видел проблему:
672 panic!("Test file vbaProject.bin not found at path: {:?}. Please create it or fix the path.", project_file_path);
673 }
674
675 match run_vba_modification_simulation(project_file_path, module_name) {
676 Ok(_) => debug!("test_vba_modification: VBA modification simulation successfully completed for module '{}'.", module_name),
677 Err(e) => {
678 error!("test_vba_modification: Error in VBA modification simulation for module '{}': {:?}", module_name, e);
679 panic!("Test modification of VBA failed: {:?}", e); // Падение теста при ошибке
680 }
681 }
682 }
683}
684