visualbasic 0.2.1

Parse and inspect Visual Basic 6 compiled binaries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
//! COM registration data (tagREGDATA) parser.
//!
//! The COM registration data is pointed to by `VBHeader.lpComRegisterData`
//! (+0x54). It contains the project's TypeLib GUID, version, and a linked
//! list of per-object COM registration records with CLSIDs, ProgIDs,
//! interface GUIDs, and registry metadata.
//!
//! All offsets within this structure are **self-relative** — add the
//! offset value to the structure's base VA to resolve.
//!
//! # Layout verified against
//!
//! - MSVBVM60.DLL `sub_66030AC0` (COM registration orchestrator)
//! - MSVBVM60.DLL `sub_660BC263` (per-object CLSID/ProgID registration)

use std::str;

use crate::{
    addressmap::AddressMap,
    error::Error,
    util::{read_cstr, read_u16_le, read_u32_le},
    vb::control::Guid,
};

/// COM registration data header (0x30 bytes minimum, followed by strings).
///
/// # Header Layout
///
/// | Offset | Size | Field |
/// |--------|------|-------|
/// | 0x00 | 4 | `bFirstObject` (self-relative offset to first [`ComRegObject`]; 0 = none) |
/// | 0x04 | 4 | `bszProjectName` (self-relative offset to project name string) |
/// | 0x08 | 4 | `bszHelpDir` (self-relative offset to help directory; 0 = none) |
/// | 0x0C | 4 | `bszDescription` (self-relative offset to app description; 0 = none) |
/// | 0x10 | 16 | `uuidProject` (project/TypeLib GUID) |
/// | 0x20 | 4 | `dwLcid` (TypeLib locale ID) |
/// | 0x24 | 2 | `wRegFlags` (TypeLib registration flags) |
/// | 0x26 | 2 | `wMajorVer` (TypeLib major version) |
/// | 0x28 | 2 | `wMinorVer` (TypeLib minor version) |
#[derive(Clone, Copy, Debug)]
pub struct ComRegData<'a> {
    bytes: &'a [u8],
    base_va: u32,
}

impl<'a> ComRegData<'a> {
    /// Minimum header size in bytes.
    pub const HEADER_SIZE: usize = 0x2A;

    /// Minimum buffer size needed to read every fixed-offset accessor.
    ///
    /// Consumers that pre-slice COM registration data before calling
    /// [`parse`](Self::parse) should use this value instead of hard-coding
    /// a larger guess. It is currently equal to [`HEADER_SIZE`](Self::HEADER_SIZE).
    pub const MIN_BUFFER_SIZE: usize = Self::HEADER_SIZE;

    /// Parses the COM registration data header.
    ///
    /// `base_va` is the VA of the structure in the PE image (needed for
    /// resolving self-relative offsets via the address map).
    pub fn parse(data: &'a [u8], base_va: u32) -> Result<Self, Error> {
        if data.len() < Self::HEADER_SIZE {
            return Err(Error::TooShort {
                expected: Self::HEADER_SIZE,
                actual: data.len(),
                context: "ComRegData",
            });
        }
        Ok(Self {
            bytes: data,
            base_va,
        })
    }

    /// Self-relative offset to the first per-object registration record.
    ///
    /// Returns 0 if there are no COM objects to register (common for EXE
    /// files; ActiveX DLLs/OCXs will have non-zero offsets).
    #[inline]
    pub fn first_object_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x00)
    }

    /// Self-relative offset to the project name string.
    #[inline]
    pub fn project_name_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x04)
    }

    /// Self-relative offset to the help directory string (0 = none).
    ///
    /// Used by the compiler for the `\HELPDIR = ...` registry entry.
    #[inline]
    pub fn help_dir_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x08)
    }

    /// Self-relative offset to the app description string (0 = none).
    ///
    /// Used by the compiler for the `APPDESCRIPTION=...` entry.
    #[inline]
    pub fn description_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x0C)
    }

    /// Project/TypeLib GUID at offset 0x10.
    pub fn project_guid(&self) -> Option<Guid> {
        Guid::from_bytes(self.bytes.get(0x10..0x20)?)
    }

    /// TypeLib locale ID at offset 0x20.
    #[inline]
    pub fn lcid(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x20)
    }

    /// TypeLib registration flags at offset 0x24.
    ///
    /// Passed through to TypeLib registration APIs in `sub_66030AC0`. Maps to
    /// `TLIBATTR.wLibFlags` semantics from COM:
    ///
    /// | Value | COM constant | Meaning |
    /// |-------|-------------|---------|
    /// | 0x01 | `LIBFLAG_FRESTRICTED` | Type library is restricted |
    /// | 0x02 | `LIBFLAG_FCONTROL` | Library describes controls |
    /// | 0x04 | `LIBFLAG_FHIDDEN` | Library should not be displayed |
    #[inline]
    pub fn reg_flags(&self) -> Result<u16, Error> {
        read_u16_le(self.bytes, 0x24)
    }

    /// TypeLib major version at offset 0x26.
    #[inline]
    pub fn major_version(&self) -> Result<u16, Error> {
        read_u16_le(self.bytes, 0x26)
    }

    /// TypeLib minor version at offset 0x28.
    #[inline]
    pub fn minor_version(&self) -> Result<u16, Error> {
        read_u16_le(self.bytes, 0x28)
    }

    /// Reads the project name string (resolved from self-relative offset).
    pub fn project_name(&self, map: &AddressMap<'a>) -> Option<&'a str> {
        let off = self.project_name_offset().ok()?;
        if off == 0 {
            return None;
        }
        let data = map
            .slice_from_va(self.base_va.wrapping_add(off), 256)
            .ok()?;
        let name = read_cstr(data, 0).ok()?;
        if name.is_empty() {
            return None;
        }
        str::from_utf8(name).ok()
    }

    /// Reads the help directory string (resolved from self-relative offset).
    ///
    /// This is the HELPDIR value used for TypeLib registration.
    pub fn help_dir(&self, map: &AddressMap<'a>) -> Option<&'a str> {
        let off = self.help_dir_offset().ok()?;
        if off == 0 {
            return None;
        }
        let data = map
            .slice_from_va(self.base_va.wrapping_add(off), 256)
            .ok()?;
        let name = read_cstr(data, 0).ok()?;
        if name.is_empty() {
            return None;
        }
        str::from_utf8(name).ok()
    }

    /// Base VA of the structure in the PE image.
    #[inline]
    pub fn base_va(&self) -> u32 {
        self.base_va
    }

    /// Computes the total extent of the COM registration blob.
    ///
    /// The blob is one contiguous allocation containing the header, inline
    /// strings (project name, help file, help dir), ComRegObject records,
    /// their strings, and GUID arrays. All offsets are self-relative from
    /// [`base_va`](Self::base_va).
    ///
    /// This method scans all self-relative offsets to find the highest
    /// referenced address and returns the total size from the base.
    pub fn total_size(&self, map: &AddressMap<'a>) -> Result<usize, Error> {
        let mut max_end = Self::HEADER_SIZE as u32;

        // ComRegData string offsets
        for off in [
            self.project_name_offset()?,
            self.help_dir_offset()?,
            self.description_offset()?,
        ] {
            if off != 0 {
                // Read the string to determine its length
                let str_end = map
                    .slice_from_va(self.base_va.wrapping_add(off), 256)
                    .ok()
                    .map(|d| {
                        let len = d.iter().position(|&b| b == 0).unwrap_or(0) as u32;
                        off.wrapping_add(len).wrapping_add(1)
                    })
                    .unwrap_or(off.wrapping_add(1));
                max_end = max_end.max(str_end);
            }
        }

        // Walk ComRegObject records
        for obj in self.objects(map)? {
            let obj_end = obj
                .va()
                .wrapping_sub(self.base_va)
                .wrapping_add(ComRegObject::SIZE as u32);
            max_end = max_end.max(obj_end);

            // Object strings
            for off in [obj.object_name_offset()?, obj.description_offset()?] {
                if off != 0 {
                    let str_end = map
                        .slice_from_va(self.base_va.wrapping_add(off), 256)
                        .ok()
                        .map(|d| {
                            let len = d.iter().position(|&b| b == 0).unwrap_or(0) as u32;
                            off.wrapping_add(len).wrapping_add(1)
                        })
                        .unwrap_or(off.wrapping_add(1));
                    max_end = max_end.max(str_end);
                }
            }

            // GUID arrays
            let di_off = obj.default_iface_guids_offset()?;
            if di_off != 0 {
                max_end =
                    max_end.max(di_off.wrapping_add(obj.default_iface_count()?.wrapping_mul(16)));
            }
            let si_off = obj.source_iface_guids_offset()?;
            if si_off != 0 {
                max_end =
                    max_end.max(si_off.wrapping_add(obj.source_iface_count()?.wrapping_mul(16)));
            }
        }

        Ok(max_end as usize)
    }

    /// Returns `true` if there are per-object COM registration records.
    #[inline]
    pub fn has_objects(&self) -> Result<bool, Error> {
        Ok(self.first_object_offset()? != 0)
    }

    /// Returns an iterator over per-object COM registration records.
    ///
    /// # Errors
    ///
    /// Returns an error if the first-object offset header field cannot be
    /// read from the backing buffer.
    pub fn objects(&self, map: &'a AddressMap<'a>) -> Result<ComRegObjectIter<'a>, Error> {
        Ok(ComRegObjectIter {
            map,
            base_va: self.base_va,
            next_offset: self.first_object_offset()?,
        })
    }
}

/// Per-object COM registration record (0x40 bytes minimum).
///
/// Forms a linked list via self-relative offsets. Each record describes
/// one COM-creatable class with its CLSID, ProgID components, interface
/// GUIDs, and registry flags.
///
/// # Layout
///
/// | Offset | Size | Field |
/// |--------|------|-------|
/// | 0x00 | 4 | `bNextObject` (self-relative offset to next record; 0 = last) |
/// | 0x04 | 4 | `bszObjectName` (self-relative offset to class name for ProgID) |
/// | 0x08 | 4 | `bszDescription` (self-relative offset to display name; 0 = use ProgID) |
/// | 0x0C | 4 | `dwRegFlag` (non-zero = register InprocServer32/LocalServer32) |
/// | 0x10 | 4 | Reserved |
/// | 0x14 | 16 | `uuidObject` (CLSID of this COM class) |
/// | 0x24 | 4 | `dwDefaultIfaceCount` (number of default interface GUIDs) |
/// | 0x28 | 4 | `bDefaultIfaceGuids` (self-relative offset to GUID array) |
/// | 0x2C | 4 | `bSourceIfaceGuids` (self-relative offset to event interface GUID array) |
/// | 0x30 | 4 | `dwSourceIfaceCount` (number of event interface GUIDs) |
/// | 0x34 | 4 | `dwMiscStatus` (OLE MiscStatus value for DVASPECT\_CONTENT) |
/// | 0x38 | 2 | `wObjectFlags` (registration flags — see [`ComRegObject::object_flags`]) |
/// | 0x3A | 2 | `wToolboxBitmap32` (resource ID for ToolboxBitmap32) |
/// | 0x3C | 2 | `wDefaultIcon` (resource ID for DefaultIcon) |
/// | 0x3E | 2 | `wExtendedFlags` (bit 0 = has designer data at +0x40) |
/// | 0x40 | 4 | `bDesignerData` (self-relative offset; only if extended flag bit 0) |
#[derive(Clone, Copy, Debug)]
pub struct ComRegObject<'a> {
    bytes: &'a [u8],
    base_va: u32,
    /// VA of this record in the PE image.
    va: u32,
}

impl<'a> ComRegObject<'a> {
    /// Record size in bytes (0x40).
    ///
    /// The runtime reads all fields through `+0x3E` (wExtendedFlags).
    /// For non-ActiveX objects, fields like `dwMiscStatus` at +0x34 may
    /// contain residual string data from the linker, but the struct size
    /// is fixed at 0x40. The +0x40 `bDesignerData` field is conditional
    /// (present only when `wExtendedFlags & 1`).
    pub const SIZE: usize = 0x40;

    /// Parses a per-object registration record.
    pub fn parse(data: &'a [u8], base_va: u32, va: u32) -> Result<Self, Error> {
        if data.len() < Self::SIZE {
            return Err(Error::TooShort {
                expected: Self::SIZE,
                actual: data.len(),
                context: "ComRegObject",
            });
        }
        Ok(Self {
            bytes: data,
            base_va,
            va,
        })
    }

    /// VA of this record in the PE image.
    #[inline]
    pub fn va(&self) -> u32 {
        self.va
    }

    /// Self-relative offset to next record (0 = last).
    #[inline]
    pub fn next_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x00)
    }

    /// Self-relative offset to the object name string at +0x04.
    #[inline]
    pub fn object_name_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x04)
    }

    /// Self-relative offset to the description string at +0x08.
    #[inline]
    pub fn description_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x08)
    }

    /// Reads the object/class name (second component of ProgID = `Project.ClassName`).
    pub fn object_name(&self, map: &AddressMap<'a>) -> Option<&'a str> {
        let off = self.object_name_offset().ok()?;
        if off == 0 {
            return None;
        }
        let data = map
            .slice_from_va(self.base_va.wrapping_add(off), 256)
            .ok()?;
        let name = read_cstr(data, 0).ok()?;
        if name.is_empty() {
            return None;
        }
        str::from_utf8(name).ok()
    }

    /// Reads the description/display name string.
    pub fn description(&self, map: &AddressMap<'a>) -> Option<&'a str> {
        let off = self.description_offset().ok()?;
        if off == 0 {
            return None;
        }
        let data = map
            .slice_from_va(self.base_va.wrapping_add(off), 256)
            .ok()?;
        let name = read_cstr(data, 0).ok()?;
        if name.is_empty() {
            return None;
        }
        str::from_utf8(name).ok()
    }

    /// Registration flag at offset 0x0C.
    ///
    /// Non-zero = create `InprocServer32`/`LocalServer32` subkey.
    /// Zero = delete the server subkey (unregistration).
    #[inline]
    pub fn reg_flag(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x0C)
    }

    /// Object CLSID at offset 0x14 (16-byte GUID).
    pub fn clsid(&self) -> Option<Guid> {
        Guid::from_bytes(self.bytes.get(0x14..0x24)?)
    }

    /// Number of default interface GUIDs at offset 0x24.
    #[inline]
    pub fn default_iface_count(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x24)
    }

    /// Self-relative offset to the default interface GUID array at offset 0x28.
    ///
    /// Each entry is a 16-byte GUID. Use [`default_iface_count`](Self::default_iface_count)
    /// for the array length.
    #[inline]
    pub fn default_iface_guids_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x28)
    }

    /// Self-relative offset to the source/event interface GUID array at offset 0x2C.
    #[inline]
    pub fn source_iface_guids_offset(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x2C)
    }

    /// Number of source/event interface GUIDs at offset 0x30.
    #[inline]
    pub fn source_iface_count(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x30)
    }

    /// OLE MiscStatus value at offset 0x34.
    ///
    /// Written as decimal to `CLSID\{...}\MiscStatus\1` registry key.
    #[inline]
    pub fn misc_status(&self) -> Result<u32, Error> {
        read_u32_le(self.bytes, 0x34)
    }

    /// Object registration flags at offset 0x38.
    ///
    /// Controls which registry keys are written during COM registration
    /// (`sub_660BC263` in MSVBVM60.DLL). The runtime reads both the low and
    /// high bytes of this u16:
    ///
    /// | Bit | Mask | Registry action |
    /// |-----|------|----------------|
    /// | 0 | `0x0001` | Skip registration (return immediately) |
    /// | 1 | `0x0002` | Register `IPersistPropertyBag` CATID |
    /// | 2 | `0x0004` | Register safe-for-scripting CATID |
    /// | 5 | `0x0020` | Control — `Control` subkey, `ToolboxBitmap32` |
    /// | 7 | `0x0080` | DocObject — `DocObject`, `DefaultIcon`, `InprocHandler32`, `BrowserFlags`, `EditFlags` |
    ///
    /// Composite masks used by the runtime:
    /// - `0x00B2` (bits 1,4,5,7): Automatable — `ProgID`, `TypeLib`, `VERSION`, interface registration
    /// - `0x00A0` (bits 5,7): Control or DocObject — `MiscStatus`, `MiscStatus\1`
    #[inline]
    pub fn object_flags(&self) -> Result<u16, Error> {
        read_u16_le(self.bytes, 0x38)
    }

    /// ToolboxBitmap32 resource ID at offset 0x3A.
    ///
    /// Written as `"module.dll, <id>"` to the `ToolboxBitmap32` subkey.
    #[inline]
    pub fn toolbox_bitmap_id(&self) -> Result<u16, Error> {
        read_u16_le(self.bytes, 0x3A)
    }

    /// DefaultIcon resource ID at offset 0x3C.
    ///
    /// Written as `"module.dll, <id>"` to the `DefaultIcon` subkey.
    #[inline]
    pub fn default_icon_id(&self) -> Result<u16, Error> {
        read_u16_le(self.bytes, 0x3C)
    }

    /// Extended flags at offset 0x3E.
    ///
    /// Bit 0 = has designer data at +0x40 (only if `VBHeader+0x22 >= 8`).
    #[inline]
    pub fn extended_flags(&self) -> Result<u16, Error> {
        read_u16_le(self.bytes, 0x3E)
    }

    /// Returns `true` if this is marked as a Control (flag bit 5).
    #[inline]
    pub fn is_control(&self) -> Result<bool, Error> {
        Ok(self.object_flags()? & 0x0020 != 0)
    }

    /// Returns `true` if this is a DocObject (flag bit 7).
    #[inline]
    pub fn is_doc_object(&self) -> Result<bool, Error> {
        Ok(self.object_flags()? & 0x0080 != 0)
    }

    /// Returns `true` if this object registers a ProgID and interfaces (flags & 0xB2).
    #[inline]
    pub fn is_automatable(&self) -> Result<bool, Error> {
        Ok(self.object_flags()? & 0x00B2 != 0)
    }

    /// Reads default interface GUIDs from the GUID array.
    ///
    /// # Errors
    ///
    /// Returns an error if the count or offset header fields cannot be read.
    pub fn default_iface_guids(&self, map: &AddressMap<'a>) -> Result<Vec<Guid>, Error> {
        Ok(self.read_guid_array(
            map,
            self.default_iface_guids_offset()?,
            self.default_iface_count()?,
        ))
    }

    /// Reads source/event interface GUIDs from the GUID array.
    ///
    /// # Errors
    ///
    /// Returns an error if the count or offset header fields cannot be read.
    pub fn source_iface_guids(&self, map: &AddressMap<'a>) -> Result<Vec<Guid>, Error> {
        Ok(self.read_guid_array(
            map,
            self.source_iface_guids_offset()?,
            self.source_iface_count()?,
        ))
    }

    fn read_guid_array(&self, map: &AddressMap<'a>, offset: u32, count: u32) -> Vec<Guid> {
        if offset == 0 || count == 0 {
            return Vec::new();
        }
        let va = self.base_va.wrapping_add(offset);
        let size = (count as usize).saturating_mul(16);
        let Ok(data) = map.slice_from_va(va, size) else {
            return Vec::new();
        };
        (0..count as usize)
            .filter_map(|i| {
                let start = i.saturating_mul(16);
                let end = start.saturating_add(16);
                Guid::from_bytes(data.get(start..end)?)
            })
            .collect()
    }
}

/// Iterator over per-object COM registration records.
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct ComRegObjectIter<'a> {
    map: &'a AddressMap<'a>,
    base_va: u32,
    next_offset: u32,
}

impl<'a> Iterator for ComRegObjectIter<'a> {
    type Item = ComRegObject<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.next_offset == 0 {
            return None;
        }
        let va = self.base_va.wrapping_add(self.next_offset);
        let data = self.map.slice_from_va(va, ComRegObject::SIZE).ok()?;
        let obj = ComRegObject::parse(data, self.base_va, va).ok()?;
        self.next_offset = obj.next_offset().ok()?;
        Some(obj)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_header() {
        let mut data = vec![0u8; 0x30];
        // bFirstObject = 0 (no objects)
        // bszProjectName = 0x30
        data[0x04..0x08].copy_from_slice(&0x30u32.to_le_bytes());
        // project GUID
        data[0x10..0x20].copy_from_slice(&[
            0x98, 0xF0, 0xDD, 0x2C, 0xC0, 0x58, 0xA4, 0x43, 0xBE, 0xB7, 0x64, 0xB3, 0x61, 0x53,
            0x57, 0x36,
        ]);
        // major version = 1
        data[0x26..0x28].copy_from_slice(&1u16.to_le_bytes());
        let reg = ComRegData::parse(&data, 0x00401000).unwrap();
        assert!(!reg.has_objects().unwrap());
        assert_eq!(reg.major_version().unwrap(), 1);
        assert_eq!(reg.minor_version().unwrap(), 0);
        assert!(reg.project_guid().is_some());
    }

    #[test]
    fn test_parse_too_short() {
        let data = vec![0u8; ComRegData::HEADER_SIZE - 1];
        assert!(matches!(
            ComRegData::parse(&data, 0),
            Err(Error::TooShort { .. })
        ));
    }
}