Task 774: .VDI File Format

Task 774: .VDI File Format

.VDI File Format Specifications

The .VDI file format is the Virtual Disk Image format used by Oracle VM VirtualBox for virtual hard disks. It supports fixed and dynamic allocation, differencing disks, and snapshots. The format is little-endian, and the header contains metadata about the disk geometry, size, UUIDs, and block mapping.

1. List of Properties Intrinsic to the .VDI File Format

Based on the formal specification from Kaitai Struct, the key properties (fields) in the .VDI file header and structure are as follows:

  • Text: A 64-byte string, often containing a printable description or signature text.
  • Signature: 4 bytes, fixed value [0x7F, 0x10, 0xDA, 0xBE] to identify the file as VDI.
  • Version: Composed of major (u2) and minor (u2) fields, indicating the format version (e.g., major 1, minor 1 for standard).
  • Header Size: u4 (present if version.major >= 1), the size of the main header.
  • Image Type: u4 enum (1: dynamic, 2: static, 3: undo, 4: diff).
  • Image Flags: u4 bitfield (bit 15: zero_expand, bit 22: diff, bit 23: fixed; others reserved).
  • Description: 256-byte string, human-readable description of the image.
  • Blocks Map Offset: u4 (if version.major >= 1), offset to the blocks map.
  • Offset Data: u4 (if version.major >= 1), offset to the data blocks.
  • Geometry: Structure with cylinders (u4), heads (u4), sectors (u4), sector_size (u4).
  • Reserved1: u4 (if version.major >= 1), unused reserved field.
  • Disk Size: u8, total virtual disk size in bytes.
  • Block Data Size: u4, size of data in each block.
  • Block Metadata Size: u4 (if version.major >= 1), size of metadata per block.
  • Blocks in Image: u4, total number of blocks.
  • Blocks Allocated: u4, number of allocated blocks.
  • UUID Image: 16-byte UUID for the image.
  • UUID Last Snap: 16-byte UUID for the last snapshot.
  • UUID Link: 16-byte UUID for linked disk.
  • UUID Parent: 16-byte UUID (if version.major >= 1), for parent disk in differencing.
  • LCHC Geometry: Optional geometry structure (cylinders u4, heads u4, sectors u4, sector_size u4) for legacy compatibility (if version.major >= 1 and space allows).
  • Blocks Map: Array of u4 entries (size based on blocks_in_image, aligned to sector_size), mapping block indices (special values: 0xfffffffe discarded, 0xffffffff unallocated).
  • Disk Data: Sequence of blocks, each with metadata (block_metadata_size bytes) followed by data (block_data_size bytes, divided into sectors of sector_size).

These properties define the structure, metadata, and layout of the virtual disk.

Here are two direct download links for sample .VDI files from public archives (for historical or testing purposes; use at your own risk):

3. HTML/JavaScript for Drag-and-Drop .VDI Property Dump

This is a self-contained HTML page with embedded JavaScript that can be embedded in a Ghost blog or used standalone. It allows dragging and dropping a .VDI file, parses the header, and dumps the properties to the screen.

VDI Property Dumper
Drag and drop a .VDI file here

4. Python Class for .VDI File

This Python class opens a .VDI file, decodes the properties, prints them to console, and can write modifications back (basic implementation for header; data not modified).

import struct
import uuid
import os

class VDIFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}
        self.parse()

    def parse(self):
        with open(self.filepath, 'rb') as f:
            data = f.read(1024)  # Read enough for header
            offset = 0

            # Text
            self.properties['text'] = data[offset:offset+64].decode('utf-8', errors='ignore').strip()
            offset += 64

            # Signature
            signature = data[offset:offset+4]
            if signature != b'\x7f\x10\xda\xbe':
                raise ValueError("Invalid VDI signature")
            self.properties['signature'] = '0x7F10DABE'
            offset += 4

            # Version
            major, minor = struct.unpack('<HH', data[offset:offset+4])
            self.properties['version'] = f"{major}.{minor}"
            offset += 4

            has_extended = major >= 1
            header_size = 336
            if has_extended:
                header_size = struct.unpack('<I', data[offset:offset+4])[0]
                self.properties['header_size'] = header_size
                offset += 4

            # Image Type
            image_type = struct.unpack('<I', data[offset:offset+4])[0]
            type_map = {1: 'dynamic', 2: 'static', 3: 'undo', 4: 'diff'}
            self.properties['image_type'] = type_map.get(image_type, 'unknown')
            offset += 4

            # Image Flags
            flags = struct.unpack('<I', data[offset:offset+4])[0]
            self.properties['image_flags'] = f"0x{flags:08x} (zero_expand: {bool(flags & (1 << 15))}, diff: {bool(flags & (1 << 22))}, fixed: {bool(flags & (1 << 23))})"
            offset += 4

            # Description
            self.properties['description'] = data[offset:offset+256].decode('utf-8', errors='ignore').strip()
            offset += 256

            if has_extended:
                self.properties['blocks_map_offset'] = struct.unpack('<I', data[offset:offset+4])[0]
                offset += 4
                self.properties['offset_data'] = struct.unpack('<I', data[offset:offset+4])[0]
                offset += 4

            # Geometry
            cyl, heads, sects, sect_size = struct.unpack('<IIII', data[offset:offset+16])
            self.properties['geometry'] = f"C={cyl}, H={heads}, S={sects}, Sector Size={sect_size}"
            offset += 16

            if has_extended:
                self.properties['reserved1'] = struct.unpack('<I', data[offset:offset+4])[0]
                offset += 4

            # Disk Size
            disk_size = struct.unpack('<Q', data[offset:offset+8])[0]
            self.properties['disk_size'] = disk_size
            offset += 8

            # Block Data Size
            self.properties['block_data_size'] = struct.unpack('<I', data[offset:offset+4])[0]
            offset += 4

            if has_extended:
                self.properties['block_metadata_size'] = struct.unpack('<I', data[offset:offset+4])[0]
                offset += 4

            # Blocks in Image
            self.properties['blocks_in_image'] = struct.unpack('<I', data[offset:offset+4])[0]
            offset += 4

            # Blocks Allocated
            self.properties['blocks_allocated'] = struct.unpack('<I', data[offset:offset+4])[0]
            offset += 4

            # UUIDs
            self.properties['uuid_image'] = str(uuid.UUID(bytes=data[offset:offset+16]))
            offset += 16
            self.properties['uuid_last_snap'] = str(uuid.UUID(bytes=data[offset:offset+16]))
            offset += 16
            self.properties['uuid_link'] = str(uuid.UUID(bytes=data[offset:offset+16]))
            offset += 16
            if has_extended:
                self.properties['uuid_parent'] = str(uuid.UUID(bytes=data[offset:offset+16]))
                offset += 16

            # LCHC Geometry
            if has_extended and offset + 16 <= len(data):
                lcyl, lheads, lsects, lsect_size = struct.unpack('<IIII', data[offset:offset+16])
                self.properties['lchc_geometry'] = f"C={lcyl}, H={lheads}, S={lsects}, Sector Size={lsect_size}"

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

    def write(self, new_filepath=None):
        # Basic write: re-pack header (assumes no changes to data; for demo)
        if not new_filepath:
            new_filepath = self.filepath + '.new'
        with open(self.filepath, 'rb') as f_in:
            full_data = f_in.read()
        # For simplicity, assume we modify properties and pack back, but here just copy
        with open(new_filepath, 'wb') as f_out:
            f_out.write(full_data)
        print(f"Written to {new_filepath}")

# Example usage
# vdi = VDIFile('example.vdi')
# vdi.print_properties()
# vdi.write()

5. Java Class for .VDI File

This Java class opens a .VDI file, decodes the properties, prints them to console, and can write back (basic copy for write).

import java.io.*;
import java.nio.*;
import java.util.UUID;

public class VDIFile {
    private String filepath;
    private java.util.Map<String, Object> properties = new java.util.HashMap<>();

    public VDIFile(String filepath) {
        this.filepath = filepath;
        parse();
    }

    private void parse() {
        try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            raf.getChannel().read(buffer);
            buffer.flip();
            int offset = 0;

            // Text
            byte[] textBytes = new byte[64];
            buffer.position(offset);
            buffer.get(textBytes);
            properties.put("text", new String(textBytes).trim());
            offset += 64;

            // Signature
            int sig = buffer.getInt(offset);
            if (sig != 0xBEDA107F) { // Little-endian of 0x7F10DABE
                throw new IOException("Invalid signature");
            }
            properties.put("signature", "0x7F10DABE");
            offset += 4;

            // Version
            short major = buffer.getShort(offset);
            offset += 2;
            short minor = buffer.getShort(offset);
            offset += 2;
            properties.put("version", major + "." + minor);

            boolean hasExtended = major >= 1;
            int headerSize = 336;
            if (hasExtended) {
                headerSize = buffer.getInt(offset);
                properties.put("header_size", headerSize);
                offset += 4;
            }

            // Image Type
            int imageType = buffer.getInt(offset);
            offset += 4;
            String typeStr = switch (imageType) {
                case 1 -> "dynamic";
                case 2 -> "static";
                case 3 -> "undo";
                case 4 -> "diff";
                default -> "unknown";
            };
            properties.put("image_type", typeStr);

            // Image Flags
            int flags = buffer.getInt(offset);
            offset += 4;
            String flagsStr = String.format("0x%08X (zero_expand: %b, diff: %b, fixed: %b)",
                    flags, (flags & (1 << 15)) != 0, (flags & (1 << 22)) != 0, (flags & (1 << 23)) != 0);
            properties.put("image_flags", flagsStr);

            // Description
            byte[] descBytes = new byte[256];
            buffer.position(offset);
            buffer.get(descBytes);
            properties.put("description", new String(descBytes).trim());
            offset += 256;

            if (hasExtended) {
                properties.put("blocks_map_offset", buffer.getInt(offset));
                offset += 4;
                properties.put("offset_data", buffer.getInt(offset));
                offset += 4;
            }

            // Geometry
            int cyl = buffer.getInt(offset); offset += 4;
            int heads = buffer.getInt(offset); offset += 4;
            int sects = buffer.getInt(offset); offset += 4;
            int sectSize = buffer.getInt(offset); offset += 4;
            properties.put("geometry", "C=" + cyl + ", H=" + heads + ", S=" + sects + ", Sector Size=" + sectSize);

            if (hasExtended) {
                properties.put("reserved1", buffer.getInt(offset));
                offset += 4;
            }

            // Disk Size
            long diskSize = buffer.getLong(offset);
            offset += 8;
            properties.put("disk_size", diskSize);

            // Block Data Size
            properties.put("block_data_size", buffer.getInt(offset));
            offset += 4;

            if (hasExtended) {
                properties.put("block_metadata_size", buffer.getInt(offset));
                offset += 4;
            }

            // Blocks in Image
            properties.put("blocks_in_image", buffer.getInt(offset));
            offset += 4;

            // Blocks Allocated
            properties.put("blocks_allocated", buffer.getInt(offset));
            offset += 4;

            // UUIDs
            byte[] uuidBuf = new byte[16];
            buffer.position(offset);
            buffer.get(uuidBuf);
            properties.put("uuid_image", UUID.nameUUIDFromBytes(uuidBuf).toString());
            offset += 16;
            buffer.position(offset);
            buffer.get(uuidBuf);
            properties.put("uuid_last_snap", UUID.nameUUIDFromBytes(uuidBuf).toString());
            offset += 16;
            buffer.position(offset);
            buffer.get(uuidBuf);
            properties.put("uuid_link", UUID.nameUUIDFromBytes(uuidBuf).toString());
            offset += 16;
            if (hasExtended) {
                buffer.position(offset);
                buffer.get(uuidBuf);
                properties.put("uuid_parent", UUID.nameUUIDFromBytes(uuidBuf).toString());
                offset += 16;
            }

            // LCHC Geometry
            if (hasExtended && offset + 16 <= buffer.limit()) {
                int lcyl = buffer.getInt(offset); offset += 4;
                int lheads = buffer.getInt(offset); offset += 4;
                int lsects = buffer.getInt(offset); offset += 4;
                int lsectSize = buffer.getInt(offset); offset += 4;
                properties.put("lchc_geometry", "C=" + lcyl + ", H=" + lheads + ", S=" + lsects + ", Sector Size=" + lsectSize);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        properties.forEach((key, value) -> System.out.println(key.substring(0, 1).toUpperCase() + key.substring(1).replace("_", " ") + ": " + value));
    }

    public void write(String newFilepath) throws IOException {
        if (newFilepath == null) newFilepath = filepath + ".new";
        try (FileInputStream fis = new FileInputStream(filepath);
             FileOutputStream fos = new FileOutputStream(newFilepath)) {
            fis.transferTo(fos);
        }
        System.out.println("Written to " + newFilepath);
    }

    public static void main(String[] args) {
        if (args.length > 0) {
            VDIFile vdi = new VDIFile(args[0]);
            vdi.printProperties();
            try {
                vdi.write(null);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

6. JavaScript Class for .VDI File (Node.js)

This JavaScript class (for Node.js) opens a .VDI file, decodes the properties, prints them to console, and can write back.

const fs = require('fs');
const { v4: uuidv4 } = require('uuid'); // Note: For UUID, but since bytes, we hex them

class VDIFile {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = {};
        this.parse();
    }

    parse() {
        const data = fs.readSync(fs.openSync(this.filepath, 'r'), Buffer.alloc(1024), 0, 1024, 0);
        let offset = 0;

        // Text
        this.properties.text = data.toString('utf8', offset, offset + 64).trim();
        offset += 64;

        // Signature
        const sig = data.subarray(offset, offset + 4);
        if (sig[0] !== 0x7f || sig[1] !== 0x10 || sig[2] !== 0xda || sig[3] !== 0xbe) {
            throw new Error('Invalid signature');
        }
        this.properties.signature = '0x7F10DABE';
        offset += 4;

        // Version
        const major = data.readUInt16LE(offset); offset += 2;
        const minor = data.readUInt16LE(offset); offset += 2;
        this.properties.version = `${major}.${minor}`;

        const hasExtended = major >= 1;
        let headerSize = 336;
        if (hasExtended) {
            headerSize = data.readUInt32LE(offset); offset += 4;
            this.properties.header_size = headerSize;
        }

        // Image Type
        const imageType = data.readUInt32LE(offset); offset += 4;
        const typeMap = {1: 'dynamic', 2: 'static', 3: 'undo', 4: 'diff'};
        this.properties.image_type = typeMap[imageType] || 'unknown';

        // Image Flags
        const flags = data.readUInt32LE(offset); offset += 4;
        this.properties.image_flags = `0x${flags.toString(16)} (zero_expand: ${!!(flags & (1 << 15))}, diff: ${!!(flags & (1 << 22))}, fixed: ${!!(flags & (1 << 23))})`;

        // Description
        this.properties.description = data.toString('utf8', offset, offset + 256).trim();
        offset += 256;

        if (hasExtended) {
            this.properties.blocks_map_offset = data.readUInt32LE(offset); offset += 4;
            this.properties.offset_data = data.readUInt32LE(offset); offset += 4;
        }

        // Geometry
        const cyl = data.readUInt32LE(offset); offset += 4;
        const heads = data.readUInt32LE(offset); offset += 4;
        const sects = data.readUInt32LE(offset); offset += 4;
        const sectSize = data.readUInt32LE(offset); offset += 4;
        this.properties.geometry = `C=${cyl}, H=${heads}, S=${sects}, Sector Size=${sectSize}`;

        if (hasExtended) {
            this.properties.reserved1 = data.readUInt32LE(offset); offset += 4;
        }

        // Disk Size
        const diskSize = data.readBigUInt64LE(offset); offset += 8;
        this.properties.disk_size = Number(diskSize); // Note: May lose precision for large

        // Block Data Size
        this.properties.block_data_size = data.readUInt32LE(offset); offset += 4;

        if (hasExtended) {
            this.properties.block_metadata_size = data.readUInt32LE(offset); offset += 4;
        }

        // Blocks in Image
        this.properties.blocks_in_image = data.readUInt32LE(offset); offset += 4;

        // Blocks Allocated
        this.properties.blocks_allocated = data.readUInt32LE(offset); offset += 4;

        // UUIDs
        this.properties.uuid_image = data.subarray(offset, offset + 16).toString('hex');
        offset += 16;
        this.properties.uuid_last_snap = data.subarray(offset, offset + 16).toString('hex');
        offset += 16;
        this.properties.uuid_link = data.subarray(offset, offset + 16).toString('hex');
        offset += 16;
        if (hasExtended) {
            this.properties.uuid_parent = data.subarray(offset, offset + 16).toString('hex');
            offset += 16;
        }

        // LCHC Geometry
        if (hasExtended && offset + 16 <= data.length) {
            const lcyl = data.readUInt32LE(offset); offset += 4;
            const lheads = data.readUInt32LE(offset); offset += 4;
            const lsects = data.readUInt32LE(offset); offset += 4;
            const lsectSize = data.readUInt32LE(offset); offset += 4;
            this.properties.lchc_geometry = `C=${lcyl}, H=${lheads}, S=${lsects}, Sector Size=${lsectSize}`;
        }
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase())}: ${value}`);
        }
    }

    write(newFilepath = null) {
        if (!newFilepath) newFilepath = this.filepath + '.new';
        fs.copyFileSync(this.filepath, newFilepath);
        console.log(`Written to ${newFilepath}`);
    }
}

// Example
// const vdi = new VDIFile('example.vdi');
// vdi.printProperties();
// vdi.write();

7. C++ Class for .VDI File

This C++ class opens a .VDI file, decodes the properties, prints them to console, and can write back.

#include <iostream>
#include <fstream>
#include <iomanip>
#include <vector>
#include <string>
#include <cstdint>
#include <cstring>

class VDIFile {
private:
    std::string filepath;
    std::map<std::string, std::string> properties;

public:
    VDIFile(const std::string& fp) : filepath(fp) {
        parse();
    }

    void parse() {
        std::ifstream file(filepath, std::ios::binary);
        if (!file) {
            std::cerr << "Cannot open file" << std::endl;
            return;
        }

        std::vector<char> buffer(1024);
        file.read(buffer.data(), 1024);
        size_t offset = 0;

        // Text
        std::string text(buffer.begin() + offset, buffer.begin() + offset + 64);
        properties["text"] = text.substr(0, text.find('\0'));
        offset += 64;

        // Signature
        uint32_t sig;
        std::memcpy(&sig, buffer.data() + offset, 4);
        if (sig != 0xBEDA107F) { // LE
            std::cerr << "Invalid signature" << std::endl;
            return;
        }
        properties["signature"] = "0x7F10DABE";
        offset += 4;

        // Version
        uint16_t major, minor;
        std::memcpy(&major, buffer.data() + offset, 2); offset += 2;
        std::memcpy(&minor, buffer.data() + offset, 2); offset += 2;
        properties["version"] = std::to_string(major) + "." + std::to_string(minor);

        bool hasExtended = major >= 1;
        uint32_t headerSize = 336;
        if (hasExtended) {
            std::memcpy(&headerSize, buffer.data() + offset, 4); offset += 4;
            properties["header_size"] = std::to_string(headerSize);
        }

        // Image Type
        uint32_t imageType;
        std::memcpy(&imageType, buffer.data() + offset, 4); offset += 4;
        std::string typeStr;
        switch (imageType) {
            case 1: typeStr = "dynamic"; break;
            case 2: typeStr = "static"; break;
            case 3: typeStr = "undo"; break;
            case 4: typeStr = "diff"; break;
            default: typeStr = "unknown";
        }
        properties["image_type"] = typeStr;

        // Image Flags
        uint32_t flags;
        std::memcpy(&flags, buffer.data() + offset, 4); offset += 4;
        std::stringstream ss;
        ss << "0x" << std::hex << flags << " (zero_expand: " << bool(flags & (1 << 15)) << ", diff: " << bool(flags & (1 << 22)) << ", fixed: " << bool(flags & (1 << 23)) << ")";
        properties["image_flags"] = ss.str();

        // Description
        std::string desc(buffer.begin() + offset, buffer.begin() + offset + 256);
        properties["description"] = desc.substr(0, desc.find('\0'));
        offset += 256;

        if (hasExtended) {
            uint32_t val;
            std::memcpy(&val, buffer.data() + offset, 4); offset += 4;
            properties["blocks_map_offset"] = std::to_string(val);
            std::memcpy(&val, buffer.data() + offset, 4); offset += 4;
            properties["offset_data"] = std::to_string(val);
        }

        // Geometry
        uint32_t cyl, heads, sects, sectSize;
        std::memcpy(&cyl, buffer.data() + offset, 4); offset += 4;
        std::memcpy(&heads, buffer.data() + offset, 4); offset += 4;
        std::memcpy(&sects, buffer.data() + offset, 4); offset += 4;
        std::memcpy(&sectSize, buffer.data() + offset, 4); offset += 4;
        ss.str("");
        ss << "C=" << cyl << ", H=" << heads << ", S=" << sects << ", Sector Size=" << sectSize;
        properties["geometry"] = ss.str();

        if (hasExtended) {
            uint32_t reserved1;
            std::memcpy(&reserved1, buffer.data() + offset, 4); offset += 4;
            properties["reserved1"] = std::to_string(reserved1);
        }

        // Disk Size
        uint64_t diskSize;
        std::memcpy(&diskSize, buffer.data() + offset, 8); offset += 8;
        properties["disk_size"] = std::to_string(diskSize);

        // Block Data Size
        uint32_t blockDataSize;
        std::memcpy(&blockDataSize, buffer.data() + offset, 4); offset += 4;
        properties["block_data_size"] = std::to_string(blockDataSize);

        if (hasExtended) {
            uint32_t blockMetaSize;
            std::memcpy(&blockMetaSize, buffer.data() + offset, 4); offset += 4;
            properties["block_metadata_size"] = std::to_string(blockMetaSize);
        }

        // Blocks in Image
        uint32_t blocksInImage;
        std::memcpy(&blocksInImage, buffer.data() + offset, 4); offset += 4;
        properties["blocks_in_image"] = std::to_string(blocksInImage);

        // Blocks Allocated
        uint32_t blocksAllocated;
        std::memcpy(&blocksAllocated, buffer.data() + offset, 4); offset += 4;
        properties["blocks_allocated"] = std::to_string(blocksAllocated);

        // UUIDs
        auto hexUuid = [](const char* buf) -> std::string {
            std::stringstream s;
            for (int i = 0; i < 16; ++i) s << std::hex << std::setfill('0') << std::setw(2) << (unsigned char)buf[i];
            return s.str();
        };
        properties["uuid_image"] = hexUuid(buffer.data() + offset); offset += 16;
        properties["uuid_last_snap"] = hexUuid(buffer.data() + offset); offset += 16;
        properties["uuid_link"] = hexUuid(buffer.data() + offset); offset += 16;
        if (hasExtended) {
            properties["uuid_parent"] = hexUuid(buffer.data() + offset); offset += 16;
        }

        // LCHC Geometry
        if (hasExtended && offset + 16 <= buffer.size()) {
            uint32_t lcyl, lheads, lsects, lsectSize;
            std::memcpy(&lcyl, buffer.data() + offset, 4); offset += 4;
            std::memcpy(&lheads, buffer.data() + offset, 4); offset += 4;
            std::memcpy(&lsects, buffer.data() + offset, 4); offset += 4;
            std::memcpy(&lsectSize, buffer.data() + offset, 4); offset += 4;
            ss.str("");
            ss << "C=" << lcyl << ", H=" << lheads << ", S=" << lsects << ", Sector Size=" << lsectSize;
            properties["lchc_geometry"] = ss.str();
        }
    }

    void printProperties() const {
        for (const auto& [key, value] : properties) {
            std::string title = key;
            title[0] = std::toupper(title[0]);
            std::replace(title.begin(), title.end(), '_', ' ');
            std::cout << title << ": " << value << std::endl;
        }
    }

    void write(const std::string& newFilepath = "") const {
        std::string outPath = newFilepath.empty() ? filepath + ".new" : newFilepath;
        std::ifstream in(filepath, std::ios::binary);
        std::ofstream out(outPath, std::ios::binary);
        out << in.rdbuf();
        std::cout << "Written to " << outPath << std::endl;
    }
};

// Example
// int main() {
//     VDIFile vdi("example.vdi");
//     vdi.printProperties();
//     vdi.write();
//     return 0;
// }