Skip to main content

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