Task 769: .VBX File Format

Task 769: .VBX File Format

.VBX File Format Specifications

The .VBX file format is a binary format used for Visual Basic Extension custom controls, primarily in older versions of Visual Basic (versions 1.0 to 3.0). It is essentially a 16-bit dynamic-link library (DLL) based on the New Executable (NE) file format, which is a segmented executable format for 16-bit Windows applications. The NE format includes a DOS MZ stub for compatibility, followed by the NE header and associated tables. The format supports dynamic linking, resources, relocations, and entry points. All numeric values are stored in little-endian byte order.

The NE header is located at the offset specified by the MZ header's e_lfanew field. The header contains metadata about the executable, including flags, entry points, and offsets to various tables such as the segment table, resource table, resident names table, module reference table, imported names table, entry table, and non-resident names table. Segments can contain code, data, or resources, with support for relocations and movable entries.

1. List of All Properties Intrinsic to the File Format

The properties are derived from the NE header and its associated structures. These include the main header fields, flag enumerations, and table structures. The list is structured by section for clarity.

NE Header Properties (Fixed Layout)

  • sig: 2 bytes, char[2], must be 'N' and 'E'.
  • MajLinkerVersion: 1 byte, uint8_t, major version of the linker.
  • MinLinkerVersion: 1 byte, uint8_t, minor version of the linker.
  • EntryTableOffset: 2 bytes, uint16_t, offset to entry table from NE header start.
  • EntryTableLength: 2 bytes, uint16_t, length of entry table in bytes.
  • FileLoadCRC: 4 bytes, uint32_t, CRC-32 of the entire file.
  • FlagWord: 2 bytes, uint16_t, bitfield for module type and data handling (see FlagWord enumeration below).
  • AutoDataSegIndex: 2 bytes, uint16_t, index into segment table for automatic data segment.
  • InitHeapSize: 2 bytes, uint16_t, initial local heap size in bytes.
  • InitStackSize: 2 bytes, uint16_t, initial stack size in bytes.
  • EntryPoint: 4 bytes, uint32_t, CS:IP entry point (CS is segment index).
  • InitStack: 4 bytes, uint32_t, SS:SP initial stack pointer (SS is segment index).
  • SegCount: 2 bytes, uint16_t, number of segment table entries.
  • ModRefs: 2 bytes, uint16_t, number of module references (imported DLLs).
  • NoResNamesTabSiz: 2 bytes, uint16_t, size of non-resident names table in bytes.
  • SegTableOffset: 2 bytes, uint16_t, offset to segment table from NE header.
  • ResTableOffset: 2 bytes, uint16_t, offset to resource table from NE header.
  • ResidNamTable: 2 bytes, uint16_t, offset to resident names table from NE header.
  • ModRefTable: 2 bytes, uint16_t, offset to module reference table from NE header.
  • ImportNameTable: 2 bytes, uint16_t, offset to imported names table from NE header.
  • OffStartNonResTab: 4 bytes, uint32_t, file offset to non-resident names table.
  • MovEntryCount: 2 bytes, uint16_t, number of movable entry points in entry table.
  • FileAlnSzShftCnt: 2 bytes, uint16_t, log2 of file alignment unit (0 defaults to 512 bytes).
  • nResTabEntries: 2 bytes, uint16_t, number of resource table entries (may be unreliable).
  • targOS: 1 byte, uint8_t, target operating system (e.g., 1 for OS/2, 2 for Windows).
  • OS2EXEFlags: 1 byte, uint8_t, OS/2-specific flags.
  • retThunkOffset: 2 bytes, uint16_t, offset to return thunks or gangload area start.
  • segrefthunksoff: 2 bytes, uint16_t, offset to segment reference thunks or gangload size.
  • mincodeswap: 2 bytes, uint16_t, minimum code swap area size.
  • expctwinver: 2 bytes, uint8_t[2], expected Windows version (minor, major).

FlagWord Enumeration Properties

  • NOAUTODATA (0x0000): No automatic data segment.
  • SINGLEDATA (0x0001): Single shared data segment.
  • MULTIPLEDATA (0x0002): Separate data segment per instance.
  • LINKERROR (0x2000): Linker error; module cannot load.
  • LIBMODULE (0x8000): Module is a DLL.
  • GLOBINIT (1<<2): Global initialization.
  • PMODEONLY (1<<3): Protected mode only.
  • INSTRUC86 (1<<4): Uses 8086 instructions.
  • INSTRU286 (1<<5): Uses 80286 instructions.
  • INSTRU386 (1<<6): Uses 80386 instructions.
  • INSTRUx87 (1<<7): Uses 80x87 FPU instructions.

Segment Table Entry Properties (Repeated for SegCount Entries, 8 Bytes Each)

  • SectorBase: 2 bytes, uint16_t, sector offset from file start (multiplied by alignment).
  • SegBytes: 2 bytes, uint16_t, length of segment data in bytes.
  • SegFlags: 2 bytes, uint16_t, segment attributes (see SegFlags bitfield below).
  • MinAlloc: 2 bytes, uint16_t, minimum allocation size in bytes.

SegFlags Bitfield Properties

  • TYPE_MASK (0x0007): Segment type (0 = code, 1 = data).
  • MOVABLE (0x0010): Movable segment.
  • PRELOAD (0x0040): Preload segment.
  • HAS_RELOCS (0x0100): Has relocation entries.
  • DISCARD (0xF000): Discard priority (higher value = lower priority).

Resource Table Properties

  • AlignmentShiftCount: 2 bytes, uint16_t, log2 of resource alignment.
  • TypeID: 2 bytes, uint16_t, resource type (ID or string offset).
  • ResourceCount: 2 bytes, uint16_t, number of resources per type.
  • Reserved: 4 bytes, uint32_t, typically 0.
  • FileOffset: 2 bytes, uint16_t, resource file offset (multiplied by alignment).
  • ResourceLength: 2 bytes, uint16_t, resource length (multiplied by alignment).
  • ResourceFlags: 2 bytes, uint16_t, resource attributes (MOVEABLE 0x0010, PURE 0x0020, PRELOAD 0x0040).
  • ResourceID: 2 bytes, uint16_t, resource ID (ID or string offset).

Resident/Non-Resident Name Table Entry Properties (Variable Length)

  • NameLength: 1 byte, uint8_t, length of name.
  • Name: Variable, ASCII string of NameLength bytes.
  • OrdinalNumber: 2 bytes, uint16_t, export ordinal (0 ends table).

Module-Reference Table Properties

  • Array of uint16_t entries (length = ModRefs), each referencing imported module names.

Imported-Name Table Entry Properties (Variable Length)

  • NameLength: 1 byte, uint8_t, length of name.
  • Name: Variable, ASCII string of NameLength bytes.

Entry Table Bundle Properties (Variable)

  • EntryCount: 1 byte, uint8_t, number of entries in bundle.
  • SegmentIndicator: 1 byte, uint8_t, segment type (0x00 unused, 0xFF movable, other fixed).
  • EntryFlags: 1 byte, uint8_t, flags (EXPORTED, GLOBALDATA).
  • EntryPointOffset: 2 bytes, uint16_t, offset within segment.
  • Int3fh (movable only): 2 bytes, uint16_t, must be 0x3D00.
  • SegmentNumber (movable only): 1 byte, uint8_t, movable segment index.

Relocation Data Properties (Per Segment if HAS_RELOCS Set)

  • RelocEntryCount: 2 bytes, uint16_t, number of relocations.
  • Source: 1 byte, uint8_t, relocation source type (e.g., 0x00 low byte, 0x02 segment).
  • FlagsAndTarget: 1 byte, uint8_t, target type and flags.
  • SourceChainOffset: 2 bytes, uint16_t, chaining offset.
  • Value: Variable, depending on target (e.g., segment index or offset).

3. Ghost Blog Embedded HTML JavaScript for Drag and Drop .VBX File Dump

The following HTML and JavaScript code creates a simple web page where a user can drag and drop a .VBX file. The script reads the file as binary, parses the NE header (assuming the MZ stub is present), extracts the properties, and displays them on the screen.

.VBX File Properties Dumper

Drag and Drop .VBX File

Drop .VBX file here

4. Python Class for .VBX File Handling

The following Python class opens a .VBX file, decodes the NE header, reads and prints the properties to the console, and supports writing modified properties back to a new file.

import struct

class VBXParser:
    def __init__(self, filename):
        self.filename = filename
        self.ne_offset = 0
        self.properties = {}
        self.read()

    def read(self):
        with open(self.filename, 'rb') as f:
            data = f.read()
            # Find NE offset from MZ e_lfanew at 0x3C
            (self.ne_offset,) = struct.unpack_from('<I', data, 0x3C)
            sig = data[self.ne_offset:self.ne_offset+2].decode('ascii')
            if sig != 'NE':
                raise ValueError("Invalid NE format")
            
            # Parse header
            offset = self.ne_offset
            self.properties['sig'] = sig
            self.properties['MajLinkerVersion'] = struct.unpack_from('<B', data, offset + 2)[0]
            self.properties['MinLinkerVersion'] = struct.unpack_from('<B', data, offset + 3)[0]
            self.properties['EntryTableOffset'] = struct.unpack_from('<H', data, offset + 4)[0]
            self.properties['EntryTableLength'] = struct.unpack_from('<H', data, offset + 6)[0]
            self.properties['FileLoadCRC'] = struct.unpack_from('<I', data, offset + 8)[0]
            self.properties['FlagWord'] = struct.unpack_from('<H', data, offset + 0x0C)[0]
            self.properties['AutoDataSegIndex'] = struct.unpack_from('<H', data, offset + 0x0E)[0]
            self.properties['InitHeapSize'] = struct.unpack_from('<H', data, offset + 0x10)[0]
            self.properties['InitStackSize'] = struct.unpack_from('<H', data, offset + 0x12)[0]
            self.properties['EntryPoint'] = struct.unpack_from('<I', data, offset + 0x14)[0]
            self.properties['InitStack'] = struct.unpack_from('<I', data, offset + 0x18)[0]
            self.properties['SegCount'] = struct.unpack_from('<H', data, offset + 0x1C)[0]
            self.properties['ModRefs'] = struct.unpack_from('<H', data, offset + 0x1E)[0]
            self.properties['NoResNamesTabSiz'] = struct.unpack_from('<H', data, offset + 0x20)[0]
            self.properties['SegTableOffset'] = struct.unpack_from('<H', data, offset + 0x22)[0]
            self.properties['ResTableOffset'] = struct.unpack_from('<H', data, offset + 0x24)[0]
            self.properties['ResidNamTable'] = struct.unpack_from('<H', data, offset + 0x26)[0]
            self.properties['ModRefTable'] = struct.unpack_from('<H', data, offset + 0x28)[0]
            self.properties['ImportNameTable'] = struct.unpack_from('<H', data, offset + 0x2A)[0]
            self.properties['OffStartNonResTab'] = struct.unpack_from('<I', data, offset + 0x2C)[0]
            self.properties['MovEntryCount'] = struct.unpack_from('<H', data, offset + 0x30)[0]
            self.properties['FileAlnSzShftCnt'] = struct.unpack_from('<H', data, offset + 0x32)[0]
            self.properties['nResTabEntries'] = struct.unpack_from('<H', data, offset + 0x34)[0]
            self.properties['targOS'] = struct.unpack_from('<B', data, offset + 0x36)[0]
            self.properties['OS2EXEFlags'] = struct.unpack_from('<B', data, offset + 0x37)[0]
            self.properties['retThunkOffset'] = struct.unpack_from('<H', data, offset + 0x38)[0]
            self.properties['segrefthunksoff'] = struct.unpack_from('<H', data, offset + 0x3A)[0]
            self.properties['mincodeswap'] = struct.unpack_from('<H', data, offset + 0x3C)[0]
            minor, major = struct.unpack_from('<BB', data, offset + 0x3E)
            self.properties['expctwinver'] = f"{minor}.{major}"

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, output_filename):
        with open(self.filename, 'rb') as f:
            data = bytearray(f.read())
        # Example modification: update MajLinkerVersion (can extend for all)
        offset = self.ne_offset + 2
        struct.pack_into('<B', data, offset, self.properties['MajLinkerVersion'])
        # Repeat for other fields as needed
        with open(output_filename, 'wb') as f:
            f.write(data)

# Usage example:
# parser = VBXParser('example.vbx')
# parser.print_properties()
# parser.properties['MajLinkerVersion'] = 5  # Modify
# parser.write('modified.vbx')

5. Java Class for .VBX File Handling

The following Java class opens a .VBX file, decodes the NE header, reads and prints the properties to the console, and supports writing modified properties back to a new file. It uses ByteBuffer for parsing.

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class VBXParser {
    private String filename;
    private long neOffset;
    private ByteBuffer buffer;
    private final java.util.Map<String, Object> properties = new java.util.HashMap<>();

    public VBXParser(String filename) throws IOException {
        this.filename = filename;
        read();
    }

    private void read() throws IOException {
        buffer = ByteBuffer.wrap(Files.readAllBytes(Paths.get(filename))).order(ByteOrder.LITTLE_ENDIAN);
        neOffset = Integer.toUnsignedLong(buffer.getInt(0x3C));
        buffer.position((int) neOffset);
        String sig = "" + (char) buffer.get() + (char) buffer.get();
        if (!sig.equals("NE")) {
            throw new IllegalArgumentException("Invalid NE format");
        }

        buffer.position((int) neOffset + 2);
        properties.put("sig", sig);
        properties.put("MajLinkerVersion", Byte.toUnsignedInt(buffer.get()));
        properties.put("MinLinkerVersion", Byte.toUnsignedInt(buffer.get()));
        properties.put("EntryTableOffset", Short.toUnsignedInt(buffer.getShort()));
        properties.put("EntryTableLength", Short.toUnsignedInt(buffer.getShort()));
        properties.put("FileLoadCRC", Integer.toUnsignedLong(buffer.getInt()));
        properties.put("FlagWord", Short.toUnsignedInt(buffer.getShort()));
        properties.put("AutoDataSegIndex", Short.toUnsignedInt(buffer.getShort()));
        properties.put("InitHeapSize", Short.toUnsignedInt(buffer.getShort()));
        properties.put("InitStackSize", Short.toUnsignedInt(buffer.getShort()));
        properties.put("EntryPoint", Integer.toUnsignedLong(buffer.getInt()));
        properties.put("InitStack", Integer.toUnsignedLong(buffer.getInt()));
        properties.put("SegCount", Short.toUnsignedInt(buffer.getShort()));
        properties.put("ModRefs", Short.toUnsignedInt(buffer.getShort()));
        properties.put("NoResNamesTabSiz", Short.toUnsignedInt(buffer.getShort()));
        properties.put("SegTableOffset", Short.toUnsignedInt(buffer.getShort()));
        properties.put("ResTableOffset", Short.toUnsignedInt(buffer.getShort()));
        properties.put("ResidNamTable", Short.toUnsignedInt(buffer.getShort()));
        properties.put("ModRefTable", Short.toUnsignedInt(buffer.getShort()));
        properties.put("ImportNameTable", Short.toUnsignedInt(buffer.getShort()));
        properties.put("OffStartNonResTab", Integer.toUnsignedLong(buffer.getInt()));
        properties.put("MovEntryCount", Short.toUnsignedInt(buffer.getShort()));
        properties.put("FileAlnSzShftCnt", Short.toUnsignedInt(buffer.getShort()));
        properties.put("nResTabEntries", Short.toUnsignedInt(buffer.getShort()));
        properties.put("targOS", Byte.toUnsignedInt(buffer.get()));
        properties.put("OS2EXEFlags", Byte.toUnsignedInt(buffer.get()));
        properties.put("retThunkOffset", Short.toUnsignedInt(buffer.getShort()));
        properties.put("segrefthunksoff", Short.toUnsignedInt(buffer.getShort()));
        properties.put("mincodeswap", Short.toUnsignedInt(buffer.getShort()));
        int minor = Byte.toUnsignedInt(buffer.get());
        int major = Byte.toUnsignedInt(buffer.get());
        properties.put("expctwinver", minor + "." + major);
    }

    public void printProperties() {
        properties.forEach((key, value) -> System.out.println(key + ": " + value));
    }

    public void write(String outputFilename) throws IOException {
        // Load data
        byte[] data = Files.readAllBytes(Paths.get(filename));
        ByteBuffer outBuffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        // Example: Update MajLinkerVersion
        outBuffer.position((int) neOffset + 2);
        outBuffer.put(((Number) properties.get("MajLinkerVersion")).byteValue());
        // Repeat for other fields as needed
        Files.write(Paths.get(outputFilename), data);
    }

    // Usage example:
    // public static void main(String[] args) throws IOException {
    //     VBXParser parser = new VBXParser("example.vbx");
    //     parser.printProperties();
    //     parser.properties.put("MajLinkerVersion", 5);  // Modify
    //     parser.write("modified.vbx");
    // }
}

6. JavaScript Class for .VBX File Handling

The following JavaScript class opens a .VBX file (using Node.js fs module), decodes the NE header, reads and prints the properties to the console, and supports writing modified properties back to a new file.

const fs = require('fs');

class VBXParser {
    constructor(filename) {
        this.filename = filename;
        this.neOffset = 0;
        this.properties = {};
        this.buffer = null;
        this.read();
    }

    read() {
        this.buffer = fs.readFileSync(this.filename);
        this.neOffset = this.buffer.readUInt32LE(0x3C);
        const sig = this.buffer.toString('ascii', this.neOffset, this.neOffset + 2);
        if (sig !== 'NE') {
            throw new Error('Invalid NE format');
        }

        let offset = this.neOffset + 2;
        this.properties.sig = sig;
        this.properties.MajLinkerVersion = this.buffer.readUInt8(offset++);
        this.properties.MinLinkerVersion = this.buffer.readUInt8(offset++);
        this.properties.EntryTableOffset = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.EntryTableLength = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.FileLoadCRC = this.buffer.readUInt32LE(offset); offset += 4;
        this.properties.FlagWord = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.AutoDataSegIndex = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.InitHeapSize = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.InitStackSize = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.EntryPoint = this.buffer.readUInt32LE(offset); offset += 4;
        this.properties.InitStack = this.buffer.readUInt32LE(offset); offset += 4;
        this.properties.SegCount = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.ModRefs = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.NoResNamesTabSiz = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.SegTableOffset = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.ResTableOffset = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.ResidNamTable = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.ModRefTable = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.ImportNameTable = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.OffStartNonResTab = this.buffer.readUInt32LE(offset); offset += 4;
        this.properties.MovEntryCount = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.FileAlnSzShftCnt = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.nResTabEntries = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.targOS = this.buffer.readUInt8(offset++);
        this.properties.OS2EXEFlags = this.buffer.readUInt8(offset++);
        this.properties.retThunkOffset = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.segrefthunksoff = this.buffer.readUInt16LE(offset); offset += 2;
        this.properties.mincodeswap = this.buffer.readUInt16LE(offset); offset += 2;
        const minor = this.buffer.readUInt8(offset++);
        const major = this.buffer.readUInt8(offset);
        this.properties.expctwinver = `${minor}.${major}`;
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${value}`);
        }
    }

    write(outputFilename) {
        let data = Buffer.from(this.buffer);
        // Example: Update MajLinkerVersion
        data.writeUInt8(this.properties.MajLinkerVersion, this.neOffset + 2);
        // Repeat for other fields
        fs.writeFileSync(outputFilename, data);
    }
}

// Usage example:
// const parser = new VBXParser('example.vbx');
// parser.printProperties();
// parser.properties.MajLinkerVersion = 5;  // Modify
// parser.write('modified.vbx');

7. C "Class" for .VBX File Handling

Since C does not have classes, the following implementation uses a struct with associated functions to open a .VBX file, decode the NE header, read and print the properties to the console, and support writing modified properties back to a new file.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    char* filename;
    long ne_offset;
    uint8_t* data;
    size_t size;
    // Properties (use a struct or map, but for simplicity, individual fields)
    char sig[3];
    uint8_t MajLinkerVersion;
    uint8_t MinLinkerVersion;
    uint16_t EntryTableOffset;
    uint16_t EntryTableLength;
    uint32_t FileLoadCRC;
    uint16_t FlagWord;
    uint16_t AutoDataSegIndex;
    uint16_t InitHeapSize;
    uint16_t InitStackSize;
    uint32_t EntryPoint;
    uint32_t InitStack;
    uint16_t SegCount;
    uint16_t ModRefs;
    uint16_t NoResNamesTabSiz;
    uint16_t SegTableOffset;
    uint16_t ResTableOffset;
    uint16_t ResidNamTable;
    uint16_t ModRefTable;
    uint16_t ImportNameTable;
    uint32_t OffStartNonResTab;
    uint16_t MovEntryCount;
    uint16_t FileAlnSzShftCnt;
    uint16_t nResTabEntries;
    uint8_t targOS;
    uint8_t OS2EXEFlags;
    uint16_t retThunkOffset;
    uint16_t segrefthunksoff;
    uint16_t mincodeswap;
    char expctwinver[6];  // "minor.major"
} VBXParser;

VBXParser* vbx_parser_create(const char* filename) {
    VBXParser* parser = malloc(sizeof(VBXParser));
    parser->filename = strdup(filename);
    FILE* f = fopen(filename, "rb");
    if (!f) {
        free(parser);
        return NULL;
    }
    fseek(f, 0, SEEK_END);
    parser->size = ftell(f);
    fseek(f, 0, SEEK_SET);
    parser->data = malloc(parser->size);
    fread(parser->data, 1, parser->size, f);
    fclose(f);

    // Read ne_offset
    memcpy(&parser->ne_offset, parser->data + 0x3C, 4);
    memcpy(parser->sig, parser->data + parser->ne_offset, 2);
    parser->sig[2] = '\0';
    if (strcmp(parser->sig, "NE") != 0) {
        free(parser->data);
        free(parser->filename);
        free(parser);
        return NULL;
    }

    long offset = parser->ne_offset + 2;
    parser->MajLinkerVersion = parser->data[offset++];
    parser->MinLinkerVersion = parser->data[offset++];
    memcpy(&parser->EntryTableOffset, parser->data + offset, 2); offset += 2;
    memcpy(&parser->EntryTableLength, parser->data + offset, 2); offset += 2;
    memcpy(&parser->FileLoadCRC, parser->data + offset, 4); offset += 4;
    memcpy(&parser->FlagWord, parser->data + offset, 2); offset += 2;
    memcpy(&parser->AutoDataSegIndex, parser->data + offset, 2); offset += 2;
    memcpy(&parser->InitHeapSize, parser->data + offset, 2); offset += 2;
    memcpy(&parser->InitStackSize, parser->data + offset, 2); offset += 2;
    memcpy(&parser->EntryPoint, parser->data + offset, 4); offset += 4;
    memcpy(&parser->InitStack, parser->data + offset, 4); offset += 4;
    memcpy(&parser->SegCount, parser->data + offset, 2); offset += 2;
    memcpy(&parser->ModRefs, parser->data + offset, 2); offset += 2;
    memcpy(&parser->NoResNamesTabSiz, parser->data + offset, 2); offset += 2;
    memcpy(&parser->SegTableOffset, parser->data + offset, 2); offset += 2;
    memcpy(&parser->ResTableOffset, parser->data + offset, 2); offset += 2;
    memcpy(&parser->ResidNamTable, parser->data + offset, 2); offset += 2;
    memcpy(&parser->ModRefTable, parser->data + offset, 2); offset += 2;
    memcpy(&parser->ImportNameTable, parser->data + offset, 2); offset += 2;
    memcpy(&parser->OffStartNonResTab, parser->data + offset, 4); offset += 4;
    memcpy(&parser->MovEntryCount, parser->data + offset, 2); offset += 2;
    memcpy(&parser->FileAlnSzShftCnt, parser->data + offset, 2); offset += 2;
    memcpy(&parser->nResTabEntries, parser->data + offset, 2); offset += 2;
    parser->targOS = parser->data[offset++];
    parser->OS2EXEFlags = parser->data[offset++];
    memcpy(&parser->retThunkOffset, parser->data + offset, 2); offset += 2;
    memcpy(&parser->segrefthunksoff, parser->data + offset, 2); offset += 2;
    memcpy(&parser->mincodeswap, parser->data + offset, 2); offset += 2;
    uint8_t minor = parser->data[offset++];
    uint8_t major = parser->data[offset];
    sprintf(parser->expctwinver, "%u.%u", minor, major);

    return parser;
}

void vbx_parser_print(VBXParser* parser) {
    printf("sig: %s\n", parser->sig);
    printf("MajLinkerVersion: %u\n", parser->MajLinkerVersion);
    printf("MinLinkerVersion: %u\n", parser->MinLinkerVersion);
    printf("EntryTableOffset: %u\n", parser->EntryTableOffset);
    printf("EntryTableLength: %u\n", parser->EntryTableLength);
    printf("FileLoadCRC: %u\n", parser->FileLoadCRC);
    printf("FlagWord: %u\n", parser->FlagWord);
    printf("AutoDataSegIndex: %u\n", parser->AutoDataSegIndex);
    printf("InitHeapSize: %u\n", parser->InitHeapSize);
    printf("InitStackSize: %u\n", parser->InitStackSize);
    printf("EntryPoint: %u\n", parser->EntryPoint);
    printf("InitStack: %u\n", parser->InitStack);
    printf("SegCount: %u\n", parser->SegCount);
    printf("ModRefs: %u\n", parser->ModRefs);
    printf("NoResNamesTabSiz: %u\n", parser->NoResNamesTabSiz);
    printf("SegTableOffset: %u\n", parser->SegTableOffset);
    printf("ResTableOffset: %u\n", parser->ResTableOffset);
    printf("ResidNamTable: %u\n", parser->ResidNamTable);
    printf("ModRefTable: %u\n", parser->ModRefTable);
    printf("ImportNameTable: %u\n", parser->ImportNameTable);
    printf("OffStartNonResTab: %u\n", parser->OffStartNonResTab);
    printf("MovEntryCount: %u\n", parser->MovEntryCount);
    printf("FileAlnSzShftCnt: %u\n", parser->FileAlnSzShftCnt);
    printf("nResTabEntries: %u\n", parser->nResTabEntries);
    printf("targOS: %u\n", parser->targOS);
    printf("OS2EXEFlags: %u\n", parser->OS2EXEFlags);
    printf("retThunkOffset: %u\n", parser->retThunkOffset);
    printf("segrefthunksoff: %u\n", parser->segrefthunksoff);
    printf("mincodeswap: %u\n", parser->mincodeswap);
    printf("expctwinver: %s\n", parser->expctwinver);
}

void vbx_parser_write(VBXParser* parser, const char* output_filename) {
    // Example: Update MajLinkerVersion in data
    parser->data[parser->ne_offset + 2] = parser->MajLinkerVersion;
    // Repeat for other fields
    FILE* f = fopen(output_filename, "wb");
    if (f) {
        fwrite(parser->data, 1, parser->size, f);
        fclose(f);
    }
}

void vbx_parser_destroy(VBXParser* parser) {
    free(parser->data);
    free(parser->filename);
    free(parser);
}

// Usage example:
// int main() {
//     VBXParser* parser = vbx_parser_create("example.vbx");
//     if (parser) {
//         vbx_parser_print(parser);
//         parser->MajLinkerVersion = 5;  // Modify
//         vbx_parser_write(parser, "modified.vbx");
//         vbx_parser_destroy(parser);
//     }
//     return 0;
// }