Task 342: .KMZ File Format

Task 342: .KMZ File Format

KMZ File Format Specifications

The .KMZ file format is a compressed archive format based on the ZIP file format, specifically designed for packaging Keyhole Markup Language (KML) files along with optional supporting resources like images, models, or other data. It is defined by the Open Geospatial Consortium (OGC) as part of the KML specification (Annex C). KMZ files use standard ZIP compression (typically no compression or DEFLATE, compatible with PKWARE ZIP version 2.0 or later) to bundle a primary KML file (often named "doc.kml" as the root document, though the first .kml file in the archive is used if not specified) and any referenced assets. The format supports dynamic remote links but focuses on efficient local storage and transfer, offering roughly 10:1 compression ratios for typical KML data. Metadata support is limited, with a fixed coordinate reference system (WGS84 for longitude/latitude in decimal degrees, EGM96 Geoid for altitude in meters). KMZ files are cross-platform and widely used in GIS applications like Google Earth.

List of Properties Intrinsic to the .KMZ File Format
These properties are derived from the underlying ZIP structure, as KMZ is essentially a ZIP archive with specific content constraints. They include archive-level metadata and per-entry (file) metadata, which are stored in binary headers within the file. KMZ-specific properties (e.g., root KML identification) are inferred from the contents but are not separate binary fields. Properties do not include the actual compressed data or external OS-dependent attributes like permissions (which vary by file system).

Archive-Level Properties:

  • ZIP file comment (variable-length string, often empty in KMZ files).
  • Number of central directory entries (total number of files in the archive, 16-bit integer).
  • Total number of entries in the archive (16-bit integer; matches the above in single-disk archives).
  • Size of the central directory (32-bit integer, in bytes).
  • Offset of the start of the central directory (32-bit integer, from the beginning of the file).
  • Disk number (16-bit integer; usually 0 for non-spanned archives).
  • Disk number where central directory starts (16-bit integer; usually 0).
  • ZIP64 extensions (if present: 64-bit versions of sizes, offsets, and entry counts for large files >4GB).

Per-Entry (File) Properties:

  • File name (variable-length UTF-8 string).
  • Version made by (16-bit integer, e.g., 20 for ZIP 2.0).
  • Version needed to extract (16-bit integer, minimum required ZIP version).
  • General purpose bit flag (16-bit integer, bits indicating encryption, data descriptor usage, UTF-8 naming, etc.).
  • Compression method (16-bit integer, e.g., 0 for stored/no compression, 8 for DEFLATE).
  • Last modification time (16-bit integer, in DOS format: bits 0-4=second/2, 5-10=minute, 11-15=hour).
  • Last modification date (16-bit integer, in DOS format: bits 0-4=day, 5-8=month, 9-15=year-1980).
  • CRC-32 checksum of uncompressed data (32-bit integer).
  • Compressed size (32-bit integer; or ZIP64 64-bit if >4GB).
  • Uncompressed size (32-bit integer; or ZIP64 64-bit if >4GB).
  • File comment (variable-length string, often empty).
  • Disk number start (16-bit integer; usually 0).
  • Internal file attributes (16-bit integer, e.g., bit 0 for text/binary).
  • External file attributes (32-bit integer, OS-specific, e.g., Unix permissions or DOS read-only).
  • Relative offset of local file header (32-bit integer; or ZIP64 64-bit).
  • Extra field (variable-length, optional extensions like timestamps or ZIP64 sizes).

KMZ-Specific Inferred Properties (Derived from Entries):

  • Root KML file name (string: typically "doc.kml", or the first entry ending in ".kml").
  • List of supporting files (array of file names excluding the root KML, e.g., images or models referenced in KML).

Two Direct Download Links for .KMZ Files

Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop .KMZ Property Dump
This is a self-contained HTML page with embedded JavaScript that allows dragging and dropping a .KMZ file. It parses the file as a ZIP archive (without decompressing data) and dumps the properties to the screen. Uses DataView for binary parsing. Assumes no ZIP64 for simplicity; errors if signatures not found.

KMZ Property Dumper
Drag and drop a .KMZ file here

Python Class for .KMZ Handling
This class uses Python's built-in struct for binary decoding and io for handling. It can read/decode properties from a .KMZ file, print them to console, and write a simple new KMZ (with one uncompressed KML file for demonstration). Assumes no ZIP64.

import struct
import io
import datetime

class KMZParser:
    def __init__(self, filename=None):
        self.properties = None
        if filename:
            self.load(filename)

    def load(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        self.data = data
        self.properties = self._parse()
        return self.properties

    def _find_eocd(self):
        for i in range(len(self.data) - 22, -1, -1):
            if struct.unpack_from('<I', self.data, i)[0] == 0x06054B50:
                return i
        raise ValueError("EOCD signature not found")

    def _parse(self):
        eocd_offset = self._find_eocd()
        archive = struct.unpack_from('<HHHHIIH', self.data, eocd_offset + 4)
        props = {
            'archive': {
                'diskNum': archive[0],
                'cdDiskNum': archive[1],
                'entriesThisDisk': archive[2],
                'totalEntries': archive[3],
                'cdSize': archive[4],
                'cdOffset': archive[5],
                'commentLen': archive[6],
                'comment': self.data[eocd_offset + 22 : eocd_offset + 22 + archive[6]].decode('utf-8', errors='ignore')
            },
            'entries': []
        }

        offset = props['archive']['cdOffset']
        for _ in range(props['archive']['totalEntries']):
            sig = struct.unpack_from('<I', self.data, offset)[0]
            if sig != 0x02014B50:
                raise ValueError("CD signature not found")
            entry_data = struct.unpack_from('<HHHHHHHHIIIHH', self.data, offset + 4)
            entry = {
                'versionMade': entry_data[0],
                'versionNeeded': entry_data[1],
                'bitFlag': entry_data[2],
                'compMethod': entry_data[3],
                'modTime': entry_data[4],
                'modDate': entry_data[5],
                'crc32': entry_data[6],
                'compSize': entry_data[7],
                'uncompSize': entry_data[8],
                'nameLen': entry_data[9],
                'extraLen': entry_data[10],
                'commentLen': entry_data[11],
                'diskStart': entry_data[12],
                'intAttr': entry_data[13],
                'extAttr': entry_data[14],
                'localOffset': entry_data[15]
            }
            base = offset + 46
            entry['name'] = self.data[base : base + entry['nameLen']].decode('utf-8')
            base += entry['nameLen']
            entry['extra'] = self.data[base : base + entry['extraLen']]
            base += entry['extraLen']
            entry['comment'] = self.data[base : base + entry['commentLen']].decode('utf-8', errors='ignore')
            props['entries'].append(entry)
            offset += 46 + entry['nameLen'] + entry['extraLen'] + entry['commentLen']

        # KMZ-specific
        props['kmzRoot'] = next((e['name'] for e in props['entries'] if e['name'].endswith('.kml')), 'doc.kml')
        props['kmzSupport'] = [e['name'] for e in props['entries'] if e['name'] != props['kmzRoot']]

        return props

    def print_properties(self):
        if not self.properties:
            print("No properties loaded")
            return
        print("Archive Properties:")
        for k, v in self.properties['archive'].items():
            print(f"  {k}: {v}")
        print("\nEntries:")
        for i, e in enumerate(self.properties['entries'], 1):
            print(f"Entry {i}:")
            for k, v in e.items():
                print(f"  {k}: {v}")
        print("\nKMZ-Specific:")
        print(f"  Root KML: {self.properties['kmzRoot']}")
        print(f"  Supporting Files: {self.properties['kmzSupport']}")

    def write_simple_kmz(self, output_filename, kml_content=b'<kml></kml>', root_name='doc.kml'):
        # Simple write: one uncompressed file, no extra/comment
        local_header = struct.pack('<IHHHHIIIIIHH', 0x04034B50, 10, 0, 0, 0, 0, 0, len(kml_content), len(kml_content), len(root_name), 0, 0)
        local_header += root_name.encode('utf-8')

        cd_header = struct.pack('<IHHHHHHIIIHHHHII', 0x02014B50, 10, 10, 0, 0, 0, 0, 0, len(kml_content), len(kml_content), len(root_name), 0, 0, 0, 0, 0, 0)
        cd_header += root_name.encode('utf-8')

        eocd = struct.pack('<IHHHHIIH', 0x06054B50, 0, 0, 1, 1, len(cd_header), len(local_header) + len(kml_content), 0)

        with open(output_filename, 'wb') as f:
            f.write(local_header)
            f.write(kml_content)
            f.write(cd_header)
            f.write(eocd)

# Example usage:
# parser = KMZParser('example.kmz')
# parser.print_properties()
# parser.write_simple_kmz('new.kmz')

Java Class for .KMZ Handling
This class uses ByteBuffer for decoding. It can read/decode properties, print to console, and write a simple new KMZ. Assumes little-endian and no ZIP64.

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

public class KMZParser {
    private ByteBuffer buffer;
    private String archiveProps;
    private String entriesProps;
    private String kmzProps;

    public KMZParser(String filename) throws IOException {
        load(filename);
    }

    public void load(String filename) throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filename));
        buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        parse();
    }

    private int findEOCD() {
        for (int i = buffer.capacity() - 22; i >= 0; i--) {
            if (buffer.getInt(i) == 0x06054B50) return i;
        }
        throw new RuntimeException("EOCD signature not found");
    }

    private void parse() {
        int eocdOffset = findEOCD();
        buffer.position(eocdOffset + 4);
        short diskNum = buffer.getShort();
        short cdDiskNum = buffer.getShort();
        short entriesThisDisk = buffer.getShort();
        short totalEntries = buffer.getShort();
        int cdSize = buffer.getInt();
        int cdOffset = buffer.getInt();
        short commentLen = buffer.getShort();
        byte[] commentBytes = new byte[commentLen];
        buffer.get(commentBytes);
        String comment = new String(commentBytes);

        StringBuilder sb = new StringBuilder();
        sb.append("Archive Properties:\n")
          .append("  diskNum: ").append(diskNum).append("\n")
          .append("  cdDiskNum: ").append(cdDiskNum).append("\n")
          .append("  entriesThisDisk: ").append(entriesThisDisk).append("\n")
          .append("  totalEntries: ").append(totalEntries).append("\n")
          .append("  cdSize: ").append(cdSize).append("\n")
          .append("  cdOffset: ").append(cdOffset).append("\n")
          .append("  comment: ").append(comment).append("\n");
        archiveProps = sb.toString();

        sb = new StringBuilder("\nEntries:\n");
        String rootKml = "doc.kml";
        StringBuilder support = new StringBuilder();
        buffer.position(cdOffset);
        for (int i = 0; i < totalEntries; i++) {
            if (buffer.getInt() != 0x02014B50) throw new RuntimeException("CD signature not found");
            short versionMade = buffer.getShort();
            short versionNeeded = buffer.getShort();
            short bitFlag = buffer.getShort();
            short compMethod = buffer.getShort();
            short modTime = buffer.getShort();
            short modDate = buffer.getShort();
            int crc32 = buffer.getInt();
            int compSize = buffer.getInt();
            int uncompSize = buffer.getInt();
            short nameLen = buffer.getShort();
            short extraLen = buffer.getShort();
            short commentLen = buffer.getShort();
            short diskStart = buffer.getShort();
            short intAttr = buffer.getShort();
            int extAttr = buffer.getInt();
            int localOffset = buffer.getInt();
            byte[] nameBytes = new byte[nameLen];
            buffer.get(nameBytes);
            String name = new String(nameBytes);
            byte[] extra = new byte[extraLen];
            buffer.get(extra);
            byte[] entryCommentBytes = new byte[commentLen];
            buffer.get(entryCommentBytes);
            String entryComment = new String(entryCommentBytes);

            sb.append("Entry ").append(i + 1).append(":\n")
              .append("  name: ").append(name).append("\n")
              .append("  versionMade: ").append(versionMade).append("\n")
              .append("  versionNeeded: ").append(versionNeeded).append("\n")
              .append("  bitFlag: ").append(bitFlag).append("\n")
              .append("  compMethod: ").append(compMethod).append("\n")
              .append("  modTime: ").append(modTime).append("\n")
              .append("  modDate: ").append(modDate).append("\n")
              .append("  crc32: ").append(crc32).append("\n")
              .append("  compSize: ").append(compSize).append("\n")
              .append("  uncompSize: ").append(uncompSize).append("\n")
              .append("  comment: ").append(entryComment).append("\n")
              .append("  diskStart: ").append(diskStart).append("\n")
              .append("  intAttr: ").append(intAttr).append("\n")
              .append("  extAttr: ").append(extAttr).append("\n")
              .append("  localOffset: ").append(localOffset).append("\n");

            if (name.endsWith(".kml") && rootKml.equals("doc.kml")) rootKml = name;
            else support.append(name).append(", ");
        }
        entriesProps = sb.toString();

        kmzProps = "\nKMZ-Specific:\n  Root KML: " + rootKml + "\n  Supporting Files: " + (support.length() > 0 ? support.substring(0, support.length() - 2) : "[]");
    }

    public void printProperties() {
        System.out.print(archiveProps + entriesProps + kmzProps);
    }

    public void writeSimpleKmz(String outputFilename, byte[] kmlContent, String rootName) throws IOException {
        // Simple uncompressed KMZ with one file
        ByteBuffer local = ByteBuffer.allocate(30 + rootName.length()).order(ByteOrder.LITTLE_ENDIAN);
        local.putInt(0x04034B50);
        local.putShort((short)10); // version needed
        local.putShort((short)0); // bit flag
        local.putShort((short)0); // comp method (stored)
        local.putShort((short)0); // time
        local.putShort((short)0); // date
        local.putInt(0); // crc (dummy)
        local.putInt(kmlContent.length); // comp size
        local.putInt(kmlContent.length); // uncomp size
        local.putShort((short)rootName.length());
        local.putShort((short)0); // extra len
        local.put(rootName.getBytes());

        ByteBuffer cd = ByteBuffer.allocate(46 + rootName.length()).order(ByteOrder.LITTLE_ENDIAN);
        cd.putInt(0x02014B50);
        cd.putShort((short)10); // version made
        cd.putShort((short)10); // version needed
        cd.putShort((short)0); // bit flag
        cd.putShort((short)0); // comp method
        cd.putShort((short)0); // time
        cd.putShort((short)0); // date
        cd.putInt(0); // crc
        cd.putInt(kmlContent.length);
        cd.putInt(kmlContent.length);
        cd.putShort((short)rootName.length());
        cd.putShort((short)0); // extra
        cd.putShort((short)0); // comment
        cd.putShort((short)0); // disk start
        cd.putShort((short)0); // int attr
        cd.putInt(0); // ext attr
        cd.putInt(0); // local offset
        cd.put(rootName.getBytes());

        ByteBuffer eocd = ByteBuffer.allocate(22).order(ByteOrder.LITTLE_ENDIAN);
        eocd.putInt(0x06054B50);
        eocd.putShort((short)0); // disk num
        eocd.putShort((short)0); // cd disk
        eocd.putShort((short)1); // entries this disk
        eocd.putShort((short)1); // total entries
        eocd.putInt(cd.capacity()); // cd size
        eocd.putInt(local.capacity() + kmlContent.length); // cd offset
        eocd.putShort((short)0); // comment len

        try (FileOutputStream fos = new FileOutputStream(outputFilename)) {
            fos.write(local.array());
            fos.write(kmlContent);
            fos.write(cd.array());
            fos.write(eocd.array());
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     KMZParser parser = new KMZParser("example.kmz");
    //     parser.printProperties();
    //     parser.writeSimpleKmz("new.kmz", "<kml></kml>".getBytes(), "doc.kml");
    // }
}

JavaScript Class for .KMZ Handling
This class is for Node.js (uses fs), parses .KMZ, prints properties to console, and can write a simple KMZ. Assumes no ZIP64.

const fs = require('fs');

class KMZParser {
    constructor(filename) {
        if (filename) this.load(filename);
    }

    load(filename) {
        this.data = fs.readFileSync(filename);
        this.view = new DataView(this.data.buffer);
        this.properties = this.parse();
        return this.properties;
    }

    findEOCD() {
        for (let i = this.data.length - 22; i >= 0; i--) {
            if (this.view.getUint32(i, true) === 0x06054B50) return i;
        }
        throw new Error("EOCD signature not found");
    }

    parse() {
        const eocdOffset = this.findEOCD();
        this.view.setPosition = offset => {}; // DataView no position, use per call
        const archive = {
            diskNum: this.view.getUint16(eocdOffset + 4, true),
            cdDiskNum: this.view.getUint16(eocdOffset + 6, true),
            entriesThisDisk: this.view.getUint16(eocdOffset + 8, true),
            totalEntries: this.view.getUint16(eocdOffset + 10, true),
            cdSize: this.view.getUint32(eocdOffset + 12, true),
            cdOffset: this.view.getUint32(eocdOffset + 16, true),
            commentLen: this.view.getUint16(eocdOffset + 20, true),
            comment: new TextDecoder().decode(this.data.subarray(eocdOffset + 22, eocdOffset + 22 + this.view.getUint16(eocdOffset + 20, true)))
        };

        const entries = [];
        let offset = archive.cdOffset;
        for (let i = 0; i < archive.totalEntries; i++) {
            if (this.view.getUint32(offset, true) !== 0x02014B50) throw new Error("CD signature not found");
            const entry = {
                versionMade: this.view.getUint16(offset + 4, true),
                versionNeeded: this.view.getUint16(offset + 6, true),
                bitFlag: this.view.getUint16(offset + 8, true),
                compMethod: this.view.getUint16(offset + 10, true),
                modTime: this.view.getUint16(offset + 12, true),
                modDate: this.view.getUint16(offset + 14, true),
                crc32: this.view.getUint32(offset + 16, true),
                compSize: this.view.getUint32(offset + 20, true),
                uncompSize: this.view.getUint32(offset + 24, true),
                nameLen: this.view.getUint16(offset + 28, true),
                extraLen: this.view.getUint16(offset + 30, true),
                commentLen: this.view.getUint16(offset + 32, true),
                diskStart: this.view.getUint16(offset + 34, true),
                intAttr: this.view.getUint16(offset + 36, true),
                extAttr: this.view.getUint32(offset + 38, true),
                localOffset: this.view.getUint32(offset + 42, true)
            };
            entry.name = new TextDecoder().decode(this.data.subarray(offset + 46, offset + 46 + entry.nameLen));
            entry.extra = this.data.subarray(offset + 46 + entry.nameLen, offset + 46 + entry.nameLen + entry.extraLen);
            entry.comment = new TextDecoder().decode(this.data.subarray(offset + 46 + entry.nameLen + entry.extraLen, offset + 46 + entry.nameLen + entry.extraLen + entry.commentLen));
            entries.push(entry);
            offset += 46 + entry.nameLen + entry.extraLen + entry.commentLen;
        }

        const kmzRoot = entries.find(e => e.name.endsWith('.kml'))?.name || 'doc.kml';
        const kmzSupport = entries.filter(e => e.name !== kmzRoot).map(e => e.name);

        return { archive, entries, kmzRoot, kmzSupport };
    }

    printProperties() {
        console.log('Archive Properties:');
        console.log(this.properties.archive);
        console.log('\nEntries:');
        this.properties.entries.forEach((e, i) => {
            console.log(`Entry ${i + 1}:`);
            console.log(e);
        });
        console.log('\nKMZ-Specific:');
        console.log('Root KML:', this.properties.kmzRoot);
        console.log('Supporting Files:', this.properties.kmzSupport);
    }

    writeSimpleKmz(outputFilename, kmlContent, rootName = 'doc.kml') {
        const kmlBuf = Buffer.from(kmlContent);
        const localHeader = Buffer.alloc(30 + rootName.length);
        const view = new DataView(localHeader.buffer);
        view.setUint32(0, 0x04034B50, true);
        view.setUint16(4, 10, true); // version needed
        view.setUint16(6, 0, true); // bit flag
        view.setUint16(8, 0, true); // comp method
        view.setUint16(10, 0, true); // time
        view.setUint16(12, 0, true); // date
        view.setUint32(14, 0, true); // crc
        view.setUint32(18, kmlBuf.length, true); // comp size
        view.setUint32(22, kmlBuf.length, true); // uncomp size
        view.setUint16(26, rootName.length, true);
        view.setUint16(28, 0, true); // extra len
        localHeader.write(rootName, 30);

        const cdHeader = Buffer.alloc(46 + rootName.length);
        const cdView = new DataView(cdHeader.buffer);
        cdView.setUint32(0, 0x02014B50, true);
        cdView.setUint16(4, 10, true); // version made
        cdView.setUint16(6, 10, true); // version needed
        cdView.setUint16(8, 0, true);
        cdView.setUint16(10, 0, true);
        cdView.setUint16(12, 0, true);
        cdView.setUint16(14, 0, true);
        cdView.setUint32(16, 0, true);
        cdView.setUint32(20, kmlBuf.length, true);
        cdView.setUint32(24, kmlBuf.length, true);
        cdView.setUint16(28, rootName.length, true);
        cdView.setUint16(30, 0, true);
        cdView.setUint16(32, 0, true);
        cdView.setUint16(34, 0, true);
        cdView.setUint16(36, 0, true);
        cdView.setUint32(38, 0, true);
        cdView.setUint32(42, 0, true);
        cdHeader.write(rootName, 46);

        const eocd = Buffer.alloc(22);
        const eocdView = new DataView(eocd.buffer);
        eocdView.setUint32(0, 0x06054B50, true);
        eocdView.setUint16(4, 0, true);
        eocdView.setUint16(6, 0, true);
        eocdView.setUint16(8, 1, true);
        eocdView.setUint16(10, 1, true);
        eocdView.setUint32(12, cdHeader.length, true);
        eocdView.setUint32(16, localHeader.length + kmlBuf.length, true);
        eocdView.setUint16(20, 0, true);

        fs.writeFileSync(outputFilename, Buffer.concat([localHeader, kmlBuf, cdHeader, eocd]));
    }
}

// Example usage:
// const parser = new KMZParser('example.kmz');
// parser.printProperties();
// parser.writeSimpleKmz('new.kmz', '<kml></kml>');

C Class for .KMZ Handling
This is a C++ class (as "class" implies OO; pure C would use structs/functions). Uses fstream for I/O, parses .KMZ, prints to console, and can write a simple KMZ. Assumes little-endian host and no ZIP64.

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

class KMZParser {
private:
    std::vector<uint8_t> data;
    struct ArchiveProps {
        uint16_t diskNum, cdDiskNum, entriesThisDisk, totalEntries;
        uint32_t cdSize, cdOffset;
        std::string comment;
    };
    struct Entry {
        uint16_t versionMade, versionNeeded, bitFlag, compMethod, modTime, modDate;
        uint32_t crc32, compSize, uncompSize;
        std::string name, comment;
        uint16_t diskStart, intAttr;
        uint32_t extAttr, localOffset;
        std::vector<uint8_t> extra;
    };
    ArchiveProps archive;
    std::vector<Entry> entries;
    std::string kmzRoot;
    std::vector<std::string> kmzSupport;

public:
    KMZParser(const std::string& filename = "") {
        if (!filename.empty()) load(filename);
    }

    void load(const std::string& filename) {
        std::ifstream file(filename, std::ios::binary | std::ios::ate);
        if (!file) throw std::runtime_error("Cannot open file");
        size_t size = file.tellg();
        data.resize(size);
        file.seekg(0);
        file.read(reinterpret_cast<char*>(data.data()), size);
        parse();
    }

    size_t findEOCD() {
        for (size_t i = data.size() - 22; i != static_cast<size_t>(-1); --i) {
            uint32_t sig;
            std::memcpy(&sig, &data[i], 4);
            if (sig == 0x06054B50) return i;
        }
        throw std::runtime_error("EOCD signature not found");
    }

    void parse() {
        size_t eocdOffset = findEOCD();
        std::memcpy(&archive.diskNum, &data[eocdOffset + 4], 2);
        std::memcpy(&archive.cdDiskNum, &data[eocdOffset + 6], 2);
        std::memcpy(&archive.entriesThisDisk, &data[eocdOffset + 8], 2);
        std::memcpy(&archive.totalEntries, &data[eocdOffset + 10], 2);
        std::memcpy(&archive.cdSize, &data[eocdOffset + 12], 4);
        std::memcpy(&archive.cdOffset, &data[eocdOffset + 16], 4);
        uint16_t commentLen;
        std::memcpy(&commentLen, &data[eocdOffset + 20], 2);
        archive.comment = std::string(reinterpret_cast<char*>(&data[eocdOffset + 22]), commentLen);

        size_t offset = archive.cdOffset;
        entries.resize(archive.totalEntries);
        for (size_t i = 0; i < archive.totalEntries; ++i) {
            uint32_t sig;
            std::memcpy(&sig, &data[offset], 4);
            if (sig != 0x02014B50) throw std::runtime_error("CD signature not found");
            std::memcpy(&entries[i].versionMade, &data[offset + 4], 2);
            std::memcpy(&entries[i].versionNeeded, &data[offset + 6], 2);
            std::memcpy(&entries[i].bitFlag, &data[offset + 8], 2);
            std::memcpy(&entries[i].compMethod, &data[offset + 10], 2);
            std::memcpy(&entries[i].modTime, &data[offset + 12], 2);
            std::memcpy(&entries[i].modDate, &data[offset + 14], 2);
            std::memcpy(&entries[i].crc32, &data[offset + 16], 4);
            std::memcpy(&entries[i].compSize, &data[offset + 20], 4);
            std::memcpy(&entries[i].uncompSize, &data[offset + 24], 4);
            uint16_t nameLen, extraLen, commentLen;
            std::memcpy(&nameLen, &data[offset + 28], 2);
            std::memcpy(&extraLen, &data[offset + 30], 2);
            std::memcpy(&commentLen, &data[offset + 32], 2);
            std::memcpy(&entries[i].diskStart, &data[offset + 34], 2);
            std::memcpy(&entries[i].intAttr, &data[offset + 36], 2);
            std::memcpy(&entries[i].extAttr, &data[offset + 38], 4);
            std::memcpy(&entries[i].localOffset, &data[offset + 42], 4);
            entries[i].name = std::string(reinterpret_cast<char*>(&data[offset + 46]), nameLen);
            entries[i].extra.assign(&data[offset + 46 + nameLen], &data[offset + 46 + nameLen + extraLen]);
            entries[i].comment = std::string(reinterpret_cast<char*>(&data[offset + 46 + nameLen + extraLen]), commentLen);
            offset += 46 + nameLen + extraLen + commentLen;
        }

        kmzRoot = "doc.kml";
        for (const auto& e : entries) {
            if (e.name.find(".kml") != std::string::npos) {
                kmzRoot = e.name;
                break;
            }
        }
        kmzSupport.clear();
        for (const auto& e : entries) {
            if (e.name != kmzRoot) kmzSupport.push_back(e.name);
        }
    }

    void printProperties() const {
        std::cout << "Archive Properties:" << std::endl;
        std::cout << "  diskNum: " << archive.diskNum << std::endl;
        std::cout << "  cdDiskNum: " << archive.cdDiskNum << std::endl;
        std::cout << "  entriesThisDisk: " << archive.entriesThisDisk << std::endl;
        std::cout << "  totalEntries: " << archive.totalEntries << std::endl;
        std::cout << "  cdSize: " << archive.cdSize << std::endl;
        std::cout << "  cdOffset: " << archive.cdOffset << std::endl;
        std::cout << "  comment: " << archive.comment << std::endl;

        std::cout << "\nEntries:" << std::endl;
        for (size_t i = 0; i < entries.size(); ++i) {
            std::cout << "Entry " << i + 1 << ":" << std::endl;
            std::cout << "  name: " << entries[i].name << std::endl;
            std::cout << "  versionMade: " << entries[i].versionMade << std::endl;
            std::cout << "  versionNeeded: " << entries[i].versionNeeded << std::endl;
            std::cout << "  bitFlag: " << entries[i].bitFlag << std::endl;
            std::cout << "  compMethod: " << entries[i].compMethod << std::endl;
            std::cout << "  modTime: " << entries[i].modTime << std::endl;
            std::cout << "  modDate: " << entries[i].modDate << std::endl;
            std::cout << "  crc32: " << entries[i].crc32 << std::endl;
            std::cout << "  compSize: " << entries[i].compSize << std::endl;
            std::cout << "  uncompSize: " << entries[i].uncompSize << std::endl;
            std::cout << "  comment: " << entries[i].comment << std::endl;
            std::cout << "  diskStart: " << entries[i].diskStart << std::endl;
            std::cout << "  intAttr: " << entries[i].intAttr << std::endl;
            std::cout << "  extAttr: " << entries[i].extAttr << std::endl;
            std::cout << "  localOffset: " << entries[i].localOffset << std::endl;
        }

        std::cout << "\nKMZ-Specific:" << std::endl;
        std::cout << "  Root KML: " << kmzRoot << std::endl;
        std::cout << "  Supporting Files: ";
        for (const auto& s : kmzSupport) std::cout << s << " ";
        std::cout << std::endl;
    }

    void writeSimpleKmz(const std::string& outputFilename, const std::vector<uint8_t>& kmlContent, const std::string& rootName = "doc.kml") {
        std::ofstream file(outputFilename, std::ios::binary);
        if (!file) throw std::runtime_error("Cannot write file");

        // Local header
        uint32_t sig = 0x04034B50;
        uint16_t version = 10, flag = 0, method = 0, time = 0, date = 0;
        uint32_t crc = 0, compSize = kmlContent.size(), uncompSize = kmlContent.size();
        uint16_t nameLen = rootName.length(), extraLen = 0;
        file.write(reinterpret_cast<const char*>(&sig), 4);
        file.write(reinterpret_cast<const char*>(&version), 2);
        file.write(reinterpret_cast<const char*>(&flag), 2);
        file.write(reinterpret_cast<const char*>(&method), 2);
        file.write(reinterpret_cast<const char*>(&time), 2);
        file.write(reinterpret_cast<const char*>(&date), 2);
        file.write(reinterpret_cast<const char*>(&crc), 4);
        file.write(reinterpret_cast<const char*>(&compSize), 4);
        file.write(reinterpret_cast<const char*>(&uncompSize), 4);
        file.write(reinterpret_cast<const char*>(&nameLen), 2);
        file.write(reinterpret_cast<const char*>(&extraLen), 2);
        file << rootName;
        file.write(reinterpret_cast<const char*>(kmlContent.data()), kmlContent.size());

        // CD header
        sig = 0x02014B50;
        uint16_t versionMade = 10;
        uint16_t commentLen = 0, diskStart = 0, intAttr = 0;
        uint32_t extAttr = 0, localOffset = 0;
        file.write(reinterpret_cast<const char*>(&sig), 4);
        file.write(reinterpret_cast<const char*>(&versionMade), 2);
        file.write(reinterpret_cast<const char*>(&version), 2);
        file.write(reinterpret_cast<const char*>(&flag), 2);
        file.write(reinterpret_cast<const char*>(&method), 2);
        file.write(reinterpret_cast<const char*>(&time), 2);
        file.write(reinterpret_cast<const char*>(&date), 2);
        file.write(reinterpret_cast<const char*>(&crc), 4);
        file.write(reinterpret_cast<const char*>(&compSize), 4);
        file.write(reinterpret_cast<const char*>(&uncompSize), 4);
        file.write(reinterpret_cast<const char*>(&nameLen), 2);
        file.write(reinterpret_cast<const char*>(&extraLen), 2);
        file.write(reinterpret_cast<const char*>(&commentLen), 2);
        file.write(reinterpret_cast<const char*>(&diskStart), 2);
        file.write(reinterpret_cast<const char*>(&intAttr), 2);
        file.write(reinterpret_cast<const char*>(&extAttr), 4);
        file.write(reinterpret_cast<const char*>(&localOffset), 4);
        file << rootName;

        // EOCD
        sig = 0x06054B50;
        uint16_t diskNum = 0, cdDisk = 0, entriesDisk = 1, totalEntries = 1;
        uint32_t cdSize = 46 + nameLen, cdOffset = 30 + nameLen + kmlContent.size();
        uint16_t eCommentLen = 0;
        file.write(reinterpret_cast<const char*>(&sig), 4);
        file.write(reinterpret_cast<const char*>(&diskNum), 2);
        file.write(reinterpret_cast<const char*>(&cdDisk), 2);
        file.write(reinterpret_cast<const char*>(&entriesDisk), 2);
        file.write(reinterpret_cast<const char*>(&totalEntries), 2);
        file.write(reinterpret_cast<const char*>(&cdSize), 4);
        file.write(reinterpret_cast<const char*>(&cdOffset), 4);
        file.write(reinterpret_cast<const char*>(&eCommentLen), 2);
    }
};

// Example usage:
// int main() {
//     try {
//         KMZParser parser("example.kmz");
//         parser.printProperties();
//         std::vector<uint8_t> kml = {'<', 'k', 'm', 'l', '>', '<', '/', 'k', 'm', 'l', '>'};
//         parser.writeSimpleKmz("new.kmz", kml);
//     } catch (const std::exception& e) {
//         std::cerr << e.what() << std::endl;
//     }
//     return 0;
// }