Task 833: .XLS File Format

Task 833: .XLS File Format

.XLS File Format Specifications

The .XLS file format (Excel Binary File Format, or BIFF) is a proprietary binary format used by Microsoft Excel versions 97 through 2003. It is built on the Compound File Binary Format (CFB), also known as OLE Structured Storage, which organizes data in a file-system-like structure with storages and streams. The file begins with a fixed 512-byte header that defines key structural elements. The primary content is in a stream named "Workbook" (or "Book" in older versions), which contains BIFF records for spreadsheet data. The format supports little-endian byte order and includes metadata in property set streams like "\005SummaryInformation" and "\005DocumentSummaryInformation".

  1. List of all the properties of this file format intrinsic to its file system:

These are the fields from the CFB header, which define the file's internal "file system" structure:

  • Signature: 8 bytes, fixed value 0xD0 CF 11 E0 A1 B1 1A E1 (magic number identifying CFB files).
  • CLSID: 16 bytes, typically all zeros (0x00000000-0000-0000-0000-000000000000).
  • Minor Version: 2 bytes (unsigned short), usually 0x003E.
  • Major Version: 2 bytes (unsigned short), 0x0003 (version 3, sector size 512 bytes) or 0x0004 (version 4, sector size 4096 bytes).
  • Byte Order: 2 bytes (unsigned short), fixed 0xFFFE (indicating little-endian).
  • Sector Shift: 2 bytes (unsigned short), 9 (for 512-byte sectors in v3) or 12 (for 4096-byte sectors in v4).
  • Mini Sector Shift: 2 bytes (unsigned short), fixed 6 (for 64-byte mini-sectors).
  • Reserved: 2 bytes, must be 0.
  • Reserved1: 4 bytes (unsigned long), must be 0.
  • Number of Directory Sectors: 4 bytes (unsigned long), 0 for major version 3.
  • Number of FAT Sectors: 4 bytes (unsigned long), number of sectors in the FAT chain.
  • First Directory Sector Location: 4 bytes (unsigned long), starting sector ID for the directory stream.
  • Transaction Signature Number: 4 bytes (unsigned long), for file versioning/transactions.
  • Mini Stream Cutoff Size: 4 bytes (unsigned long), fixed 0x00001000 (4096 bytes).
  • First Mini FAT Sector Location: 4 bytes (unsigned long), starting sector ID for mini FAT.
  • Number of Mini FAT Sectors: 4 bytes (unsigned long), count of mini FAT sectors.
  • First DIFAT Sector Location: 4 bytes (unsigned long), starting sector ID for DIFAT (or 0xFFFFFFFE if none).
  • Number of DIFAT Sectors: 4 bytes (unsigned long), count of DIFAT sectors.
  • DIFAT: 436 bytes (109 unsigned longs), array of FAT sector locations (first 109 entries; padded with 0xFFFFFFFF if unused).
  1. Two direct download links for .XLS files:
  1. Ghost blog embedded HTML/JavaScript for drag-and-drop .XLS file to dump properties:

Here's an embeddable HTML snippet with JavaScript that can be placed in a Ghost blog post (or any HTML-enabled blog). It creates a drop zone; when a .XLS file is dropped, it reads the first 512 bytes, parses the header properties, and displays them on the screen.

Drag and drop a .XLS file here
  1. Python class for .XLS handling:
import struct
import os

class XLSHeaderParser:
    def __init__(self, filename):
        self.filename = filename
        self.properties = {}
        self._read_header()

    def _read_header(self):
        with open(self.filename, 'rb') as f:
            header = f.read(512)
            if len(header) < 512:
                raise ValueError("File too small to be a valid .XLS")
            
            # Unpack properties
            self.properties['Signature'] = ' '.join(f'0x{byte:02X}' for byte in header[0:8])
            self.properties['CLSID'] = ' '.join(f'0x{byte:02X}' for byte in header[8:24])
            self.properties['Minor Version'] = f'0x{struct.unpack_from("<H", header, 24)[0]:04X}'
            self.properties['Major Version'] = f'0x{struct.unpack_from("<H", header, 26)[0]:04X}'
            self.properties['Byte Order'] = f'0x{struct.unpack_from("<H", header, 28)[0]:04X}'
            self.properties['Sector Shift'] = struct.unpack_from("<H", header, 30)[0]
            self.properties['Mini Sector Shift'] = struct.unpack_from("<H", header, 32)[0]
            self.properties['Reserved'] = f'0x{struct.unpack_from("<H", header, 34)[0]:04X}'
            self.properties['Reserved1'] = f'0x{struct.unpack_from("<I", header, 36)[0]:08X}'
            self.properties['Number of Directory Sectors'] = struct.unpack_from("<I", header, 40)[0]
            self.properties['Number of FAT Sectors'] = struct.unpack_from("<I", header, 44)[0]
            self.properties['First Directory Sector Location'] = f'0x{struct.unpack_from("<I", header, 48)[0]:08X}'
            self.properties['Transaction Signature Number'] = struct.unpack_from("<I", header, 52)[0]
            self.properties['Mini Stream Cutoff Size'] = struct.unpack_from("<I", header, 56)[0]
            self.properties['First Mini FAT Sector Location'] = f'0x{struct.unpack_from("<I", header, 60)[0]:08X}'
            self.properties['Number of Mini FAT Sectors'] = struct.unpack_from("<I", header, 64)[0]
            self.properties['First DIFAT Sector Location'] = f'0x{struct.unpack_from("<I", header, 68)[0]:08X}'
            self.properties['Number of DIFAT Sectors'] = struct.unpack_from("<I", header, 72)[0]
            
            # DIFAT: 109 entries
            difat = []
            for i in range(109):
                val = struct.unpack_from("<I", header, 76 + i * 4)[0]
                difat.append(f'0x{val:08X}')
            self.properties['DIFAT'] = ' '.join(difat)

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

    def write_header(self, new_filename=None):
        # Reads original file, replaces header with current properties, writes to new file
        if not new_filename:
            new_filename = self.filename + '.modified'
        with open(self.filename, 'rb') as f_in:
            data = f_in.read()
        header = self._pack_header()
        with open(new_filename, 'wb') as f_out:
            f_out.write(header + data[512:])
        print(f"Modified file written to {new_filename}")

    def _pack_header(self):
        header = bytearray(512)
        
        # Signature
        sig_bytes = bytes.fromhex(self.properties['Signature'].replace('0x', ' '))
        header[0:8] = sig_bytes
        
        # CLSID
        clsid_bytes = bytes.fromhex(self.properties['CLSID'].replace('0x', ' '))
        header[8:24] = clsid_bytes
        
        # Minor Version
        struct.pack_into("<H", header, 24, int(self.properties['Minor Version'], 16))
        
        # Major Version
        struct.pack_into("<H", header, 26, int(self.properties['Major Version'], 16))
        
        # Byte Order
        struct.pack_into("<H", header, 28, int(self.properties['Byte Order'], 16))
        
        # Sector Shift
        struct.pack_into("<H", header, 30, self.properties['Sector Shift'])
        
        # Mini Sector Shift
        struct.pack_into("<H", header, 32, self.properties['Mini Sector Shift'])
        
        # Reserved
        struct.pack_into("<H", header, 34, int(self.properties['Reserved'], 16))
        
        # Reserved1
        struct.pack_into("<I", header, 36, int(self.properties['Reserved1'], 16))
        
        # Number of Directory Sectors
        struct.pack_into("<I", header, 40, self.properties['Number of Directory Sectors'])
        
        # Number of FAT Sectors
        struct.pack_into("<I", header, 44, self.properties['Number of FAT Sectors'])
        
        # First Directory Sector Location
        struct.pack_into("<I", header, 48, int(self.properties['First Directory Sector Location'], 16))
        
        # Transaction Signature Number
        struct.pack_into("<I", header, 52, self.properties['Transaction Signature Number'])
        
        # Mini Stream Cutoff Size
        struct.pack_into("<I", header, 56, self.properties['Mini Stream Cutoff Size'])
        
        # First Mini FAT Sector Location
        struct.pack_into("<I", header, 60, int(self.properties['First Mini FAT Sector Location'], 16))
        
        # Number of Mini FAT Sectors
        struct.pack_into("<I", header, 64, self.properties['Number of Mini FAT Sectors'])
        
        # First DIFAT Sector Location
        struct.pack_into("<I", header, 68, int(self.properties['First DIFAT Sector Location'], 16))
        
        # Number of DIFAT Sectors
        struct.pack_into("<I", header, 72, self.properties['Number of DIFAT Sectors'])
        
        # DIFAT
        difat_vals = self.properties['DIFAT'].split()
        for i in range(109):
            struct.pack_into("<I", header, 76 + i * 4, int(difat_vals[i], 16))
        
        return header

# Example usage:
# parser = XLSHeaderParser('example.xls')
# parser.print_properties()
# parser.write_header()
  1. Java class for .XLS handling:
import java.io.*;
import java.nio.*;
import java.util.*;

public class XLSHeaderParser {
    private String filename;
    private Map<String, Object> properties = new HashMap<>();

    public XLSHeaderParser(String filename) throws IOException {
        this.filename = filename;
        readHeader();
    }

    private void readHeader() throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filename, "r")) {
            byte[] header = new byte[512];
            if (raf.read(header) < 512) {
                throw new IOException("File too small to be a valid .XLS");
            }
            ByteBuffer bb = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);

            // Signature
            StringBuilder sig = new StringBuilder();
            for (int i = 0; i < 8; i++) {
                sig.append(String.format("0x%02X ", bb.get(i) & 0xFF));
            }
            properties.put("Signature", sig.toString().trim());

            // CLSID
            StringBuilder clsid = new StringBuilder();
            for (int i = 8; i < 24; i++) {
                clsid.append(String.format("0x%02X ", bb.get(i) & 0xFF));
            }
            properties.put("CLSID", clsid.toString().trim());

            // Minor Version
            properties.put("Minor Version", String.format("0x%04X", bb.getShort(24) & 0xFFFF));

            // Major Version
            properties.put("Major Version", String.format("0x%04X", bb.getShort(26) & 0xFFFF));

            // Byte Order
            properties.put("Byte Order", String.format("0x%04X", bb.getShort(28) & 0xFFFF));

            // Sector Shift
            properties.put("Sector Shift", bb.getShort(30) & 0xFFFF);

            // Mini Sector Shift
            properties.put("Mini Sector Shift", bb.getShort(32) & 0xFFFF);

            // Reserved
            properties.put("Reserved", String.format("0x%04X", bb.getShort(34) & 0xFFFF));

            // Reserved1
            properties.put("Reserved1", String.format("0x%08X", bb.getInt(36)));

            // Number of Directory Sectors
            properties.put("Number of Directory Sectors", bb.getInt(40));

            // Number of FAT Sectors
            properties.put("Number of FAT Sectors", bb.getInt(44));

            // First Directory Sector Location
            properties.put("First Directory Sector Location", String.format("0x%08X", bb.getInt(48)));

            // Transaction Signature Number
            properties.put("Transaction Signature Number", bb.getInt(52));

            // Mini Stream Cutoff Size
            properties.put("Mini Stream Cutoff Size", bb.getInt(56));

            // First Mini FAT Sector Location
            properties.put("First Mini FAT Sector Location", String.format("0x%08X", bb.getInt(60)));

            // Number of Mini FAT Sectors
            properties.put("Number of Mini FAT Sectors", bb.getInt(64));

            // First DIFAT Sector Location
            properties.put("First DIFAT Sector Location", String.format("0x%08X", bb.getInt(68)));

            // Number of DIFAT Sectors
            properties.put("Number of DIFAT Sectors", bb.getInt(72));

            // DIFAT
            StringBuilder difat = new StringBuilder();
            for (int i = 0; i < 109; i++) {
                difat.append(String.format("0x%08X ", bb.getInt(76 + i * 4)));
            }
            properties.put("DIFAT", difat.toString().trim());
        }
    }

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

    public void writeHeader(String newFilename) throws IOException {
        if (newFilename == null) {
            newFilename = filename + ".modified";
        }
        byte[] header = packHeader();
        try (FileInputStream fis = new FileInputStream(filename);
             FileOutputStream fos = new FileOutputStream(newFilename)) {
            fos.write(header);
            fis.skip(512);
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
        }
        System.out.println("Modified file written to " + newFilename);
    }

    private byte[] packHeader() {
        ByteBuffer bb = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);

        // Signature
        String[] sigStr = ((String) properties.get("Signature")).split(" ");
        for (String s : sigStr) {
            bb.put((byte) Integer.parseInt(s.substring(2), 16));
        }

        // CLSID
        String[] clsidStr = ((String) properties.get("CLSID")).split(" ");
        for (String s : clsidStr) {
            bb.put((byte) Integer.parseInt(s.substring(2), 16));
        }

        // Minor Version
        bb.putShort((short) Integer.parseInt(((String) properties.get("Minor Version")).substring(2), 16));

        // Major Version
        bb.putShort((short) Integer.parseInt(((String) properties.get("Major Version")).substring(2), 16));

        // Byte Order
        bb.putShort((short) Integer.parseInt(((String) properties.get("Byte Order")).substring(2), 16));

        // Sector Shift
        bb.putShort(((Integer) properties.get("Sector Shift")).shortValue());

        // Mini Sector Shift
        bb.putShort(((Integer) properties.get("Mini Sector Shift")).shortValue());

        // Reserved
        bb.putShort((short) Integer.parseInt(((String) properties.get("Reserved")).substring(2), 16));

        // Reserved1
        bb.putInt(Integer.parseInt(((String) properties.get("Reserved1")).substring(2), 16));

        // Number of Directory Sectors
        bb.putInt((Integer) properties.get("Number of Directory Sectors"));

        // Number of FAT Sectors
        bb.putInt((Integer) properties.get("Number of FAT Sectors"));

        // First Directory Sector Location
        bb.putInt(Integer.parseInt(((String) properties.get("First Directory Sector Location")).substring(2), 16));

        // Transaction Signature Number
        bb.putInt((Integer) properties.get("Transaction Signature Number"));

        // Mini Stream Cutoff Size
        bb.putInt((Integer) properties.get("Mini Stream Cutoff Size"));

        // First Mini FAT Sector Location
        bb.putInt(Integer.parseInt(((String) properties.get("First Mini FAT Sector Location")).substring(2), 16));

        // Number of Mini FAT Sectors
        bb.putInt((Integer) properties.get("Number of Mini FAT Sectors"));

        // First DIFAT Sector Location
        bb.putInt(Integer.parseInt(((String) properties.get("First DIFAT Sector Location")).substring(2), 16));

        // Number of DIFAT Sectors
        bb.putInt((Integer) properties.get("Number of DIFAT Sectors"));

        // DIFAT
        String[] difatStr = ((String) properties.get("DIFAT")).split(" ");
        for (String s : difatStr) {
            bb.putInt(Integer.parseInt(s.substring(2), 16));
        }

        return bb.array();
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     XLSHeaderParser parser = new XLSHeaderParser("example.xls");
    //     parser.printProperties();
    //     parser.writeHeader(null);
    // }
}
  1. JavaScript class for .XLS handling (Node.js, using fs module):
const fs = require('fs');

class XLSHeaderParser {
  constructor(filename) {
    this.filename = filename;
    this.properties = {};
    this.readHeader();
  }

  readHeader() {
    const buffer = fs.readFileSync(this.filename, { encoding: null });
    if (buffer.length < 512) {
      throw new Error('File too small to be a valid .XLS');
    }
    const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);

    // Signature
    let sig = '';
    for (let i = 0; i < 8; i++) sig += `0x${dataView.getUint8(i).toString(16).padStart(2, '0')} `;
    this.properties['Signature'] = sig.trim();

    // CLSID
    let clsid = '';
    for (let i = 8; i < 24; i++) clsid += `0x${dataView.getUint8(i).toString(16).padStart(2, '0')} `;
    this.properties['CLSID'] = clsid.trim();

    // Minor Version
    this.properties['Minor Version'] = `0x${dataView.getUint16(24, true).toString(16).padStart(4, '0')}`;

    // Major Version
    this.properties['Major Version'] = `0x${dataView.getUint16(26, true).toString(16).padStart(4, '0')}`;

    // Byte Order
    this.properties['Byte Order'] = `0x${dataView.getUint16(28, true).toString(16).padStart(4, '0')}`;

    // Sector Shift
    this.properties['Sector Shift'] = dataView.getUint16(30, true);

    // Mini Sector Shift
    this.properties['Mini Sector Shift'] = dataView.getUint16(32, true);

    // Reserved
    this.properties['Reserved'] = `0x${dataView.getUint16(34, true).toString(16).padStart(4, '0')}`;

    // Reserved1
    this.properties['Reserved1'] = `0x${dataView.getUint32(36, true).toString(16).padStart(8, '0')}`;

    // Number of Directory Sectors
    this.properties['Number of Directory Sectors'] = dataView.getUint32(40, true);

    // Number of FAT Sectors
    this.properties['Number of FAT Sectors'] = dataView.getUint32(44, true);

    // First Directory Sector Location
    this.properties['First Directory Sector Location'] = `0x${dataView.getUint32(48, true).toString(16).padStart(8, '0')}`;

    // Transaction Signature Number
    this.properties['Transaction Signature Number'] = dataView.getUint32(52, true);

    // Mini Stream Cutoff Size
    this.properties['Mini Stream Cutoff Size'] = dataView.getUint32(56, true);

    // First Mini FAT Sector Location
    this.properties['First Mini FAT Sector Location'] = `0x${dataView.getUint32(60, true).toString(16).padStart(8, '0')}`;

    // Number of Mini FAT Sectors
    this.properties['Number of Mini FAT Sectors'] = dataView.getUint32(64, true);

    // First DIFAT Sector Location
    this.properties['First DIFAT Sector Location'] = `0x${dataView.getUint32(68, true).toString(16).padStart(8, '0')}`;

    // Number of DIFAT Sectors
    this.properties['Number of DIFAT Sectors'] = dataView.getUint32(72, true);

    // DIFAT
    let difat = '';
    for (let i = 0; i < 109; i++) {
      difat += `0x${dataView.getUint32(76 + i * 4, true).toString(16).padStart(8, '0')} `;
    }
    this.properties['DIFAT'] = difat.trim();
  }

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

  writeHeader(newFilename = null) {
    if (!newFilename) {
      newFilename = this.filename + '.modified';
    }
    const originalBuffer = fs.readFileSync(this.filename);
    const headerBuffer = this.packHeader();
    const newBuffer = Buffer.concat([headerBuffer, originalBuffer.slice(512)]);
    fs.writeFileSync(newFilename, newBuffer);
    console.log(`Modified file written to ${newFilename}`);
  }

  packHeader() {
    const buffer = Buffer.alloc(512);
    const dataView = new DataView(buffer.buffer);

    // Signature
    const sigParts = this.properties['Signature'].split(' ');
    sigParts.forEach((part, i) => dataView.setUint8(i, parseInt(part, 16)));

    // CLSID
    const clsidParts = this.properties['CLSID'].split(' ');
    clsidParts.forEach((part, i) => dataView.setUint8(8 + i, parseInt(part, 16)));

    // Minor Version
    dataView.setUint16(24, parseInt(this.properties['Minor Version'], 16), true);

    // Major Version
    dataView.setUint16(26, parseInt(this.properties['Major Version'], 16), true);

    // Byte Order
    dataView.setUint16(28, parseInt(this.properties['Byte Order'], 16), true);

    // Sector Shift
    dataView.setUint16(30, this.properties['Sector Shift'], true);

    // Mini Sector Shift
    dataView.setUint16(32, this.properties['Mini Sector Shift'], true);

    // Reserved
    dataView.setUint16(34, parseInt(this.properties['Reserved'], 16), true);

    // Reserved1
    dataView.setUint32(36, parseInt(this.properties['Reserved1'], 16), true);

    // Number of Directory Sectors
    dataView.setUint32(40, this.properties['Number of Directory Sectors'], true);

    // Number of FAT Sectors
    dataView.setUint32(44, this.properties['Number of FAT Sectors'], true);

    // First Directory Sector Location
    dataView.setUint32(48, parseInt(this.properties['First Directory Sector Location'], 16), true);

    // Transaction Signature Number
    dataView.setUint32(52, this.properties['Transaction Signature Number'], true);

    // Mini Stream Cutoff Size
    dataView.setUint32(56, this.properties['Mini Stream Cutoff Size'], true);

    // First Mini FAT Sector Location
    dataView.setUint32(60, parseInt(this.properties['First Mini FAT Sector Location'], 16), true);

    // Number of Mini FAT Sectors
    dataView.setUint32(64, this.properties['Number of Mini FAT Sectors'], true);

    // First DIFAT Sector Location
    dataView.setUint32(68, parseInt(this.properties['First DIFAT Sector Location'], 16), true);

    // Number of DIFAT Sectors
    dataView.setUint32(72, this.properties['Number of DIFAT Sectors'], true);

    // DIFAT
    const difatParts = this.properties['DIFAT'].split(' ');
    difatParts.forEach((part, i) => dataView.setUint32(76 + i * 4, parseInt(part, 16), true));

    return buffer;
  }
}

// Example usage:
// const parser = new XLSHeaderParser('example.xls');
// parser.printProperties();
// parser.writeHeader();
  1. C++ class for .XLS handling (using  and ):
#include <iostream>
#include <fstream>
#include <iomanip>
#include <string>
#include <map>
#include <vector>
#include <cstdint>

class XLSHeaderParser {
private:
    std::string filename;
    std::map<std::string, std::string> properties;

public:
    XLSHeaderParser(const std::string& fn) : filename(fn) {
        readHeader();
    }

    void readHeader() {
        std::ifstream file(filename, std::ios::binary);
        if (!file) {
            throw std::runtime_error("Cannot open file");
        }
        std::vector<uint8_t> header(512);
        file.read(reinterpret_cast<char*>(header.data()), 512);
        if (file.gcount() < 512) {
            throw std::runtime_error("File too small to be a valid .XLS");
        }

        // Signature
        std::ostringstream sig;
        for (int i = 0; i < 8; ++i) {
            sig << "0x" << std::hex << std::setw(2) << std::setfill('0') << (header[i] & 0xFF) << " ";
        }
        properties["Signature"] = sig.str().substr(0, sig.str().size() - 1);

        // CLSID
        std::ostringstream clsid;
        for (int i = 8; i < 24; ++i) {
            clsid << "0x" << std::hex << std::setw(2) << std::setfill('0') << (header[i] & 0xFF) << " ";
        }
        properties["CLSID"] = clsid.str().substr(0, clsid.str().size() - 1);

        // Helper to get little-endian values
        auto getUint16 = [&header](int offset) { return *reinterpret_cast<const uint16_t*>(&header[offset]); };
        auto getUint32 = [&header](int offset) { return *reinterpret_cast<const uint32_t*>(&header[offset]); };

        // Minor Version
        properties["Minor Version"] = "0x" + std::to_string(getUint16(24));

        std::ostringstream oss;
        oss << "0x" << std::hex << std::setw(4) << std::setfill('0') << getUint16(24);
        properties["Minor Version"] = oss.str();

        // Major Version
        oss.str("");
        oss << "0x" << std::hex << std::setw(4) << std::setfill('0') << getUint16(26);
        properties["Major Version"] = oss.str();

        // Byte Order
        oss.str("");
        oss << "0x" << std::hex << std::setw(4) << std::setfill('0') << getUint16(28);
        properties["Byte Order"] = oss.str();

        // Sector Shift
        properties["Sector Shift"] = std::to_string(getUint16(30));

        // Mini Sector Shift
        properties["Mini Sector Shift"] = std::to_string(getUint16(32));

        // Reserved
        oss.str("");
        oss << "0x" << std::hex << std::setw(4) << std::setfill('0') << getUint16(34);
        properties["Reserved"] = oss.str();

        // Reserved1
        oss.str("");
        oss << "0x" << std::hex << std::setw(8) << std::setfill('0') << getUint32(36);
        properties["Reserved1"] = oss.str();

        // Number of Directory Sectors
        properties["Number of Directory Sectors"] = std::to_string(getUint32(40));

        // Number of FAT Sectors
        properties["Number of FAT Sectors"] = std::to_string(getUint32(44));

        // First Directory Sector Location
        oss.str("");
        oss << "0x" << std::hex << std::setw(8) << std::setfill('0') << getUint32(48);
        properties["First Directory Sector Location"] = oss.str();

        // Transaction Signature Number
        properties["Transaction Signature Number"] = std::to_string(getUint32(52));

        // Mini Stream Cutoff Size
        properties["Mini Stream Cutoff Size"] = std::to_string(getUint32(56));

        // First Mini FAT Sector Location
        oss.str("");
        oss << "0x" << std::hex << std::setw(8) << std::setfill('0') << getUint32(60);
        properties["First Mini FAT Sector Location"] = oss.str();

        // Number of Mini FAT Sectors
        properties["Number of Mini FAT Sectors"] = std::to_string(getUint32(64));

        // First DIFAT Sector Location
        oss.str("");
        oss << "0x" << std::hex << std::setw(8) << std::setfill('0') << getUint32(68);
        properties["First DIFAT Sector Location"] = oss.str();

        // Number of DIFAT Sectors
        properties["Number of DIFAT Sectors"] = std::to_string(getUint32(72));

        // DIFAT
        std::ostringstream difat;
        for (int i = 0; i < 109; ++i) {
            uint32_t val = getUint32(76 + i * 4);
            difat << "0x" << std::hex << std::setw(8) << std::setfill('0') << val << " ";
        }
        properties["DIFAT"] = difat.str().substr(0, difat.str().size() - 1);
    }

    void printProperties() const {
        for (const auto& prop : properties) {
            std::cout << prop.first << ": " << prop.second << std::endl;
        }
    }

    void writeHeader(const std::string& newFilename = "") const {
        std::string outFn = newFilename.empty() ? filename + ".modified" : newFilename;
        std::ifstream inFile(filename, std::ios::binary);
        std::ofstream outFile(outFn, std::ios::binary);
        if (!inFile || !outFile) {
            throw std::runtime_error("File I/O error");
        }

        std::vector<uint8_t> header(512, 0);

        // Signature
        auto parseHexBytes = [](const std::string& s, uint8_t* dest) {
            std::istringstream iss(s);
            std::string token;
            int idx = 0;
            while (iss >> token) {
                dest[idx++] = static_cast<uint8_t>(std::stoul(token.substr(2), nullptr, 16));
            }
        };
        parseHexBytes(properties.at("Signature"), header.data());

        // CLSID
        parseHexBytes(properties.at("CLSID"), header.data() + 8);

        // Minor Version
        *reinterpret_cast<uint16_t*>(&header[24]) = static_cast<uint16_t>(std::stoul(properties.at("Minor Version").substr(2), nullptr, 16));

        // Major Version
        *reinterpret_cast<uint16_t*>(&header[26]) = static_cast<uint16_t>(std::stoul(properties.at("Major Version").substr(2), nullptr, 16));

        // Byte Order
        *reinterpret_cast<uint16_t*>(&header[28]) = static_cast<uint16_t>(std::stoul(properties.at("Byte Order").substr(2), nullptr, 16));

        // Sector Shift
        *reinterpret_cast<uint16_t*>(&header[30]) = static_cast<uint16_t>(std::stoi(properties.at("Sector Shift")));

        // Mini Sector Shift
        *reinterpret_cast<uint16_t*>(&header[32]) = static_cast<uint16_t>(std::stoi(properties.at("Mini Sector Shift")));

        // Reserved
        *reinterpret_cast<uint16_t*>(&header[34]) = static_cast<uint16_t>(std::stoul(properties.at("Reserved").substr(2), nullptr, 16));

        // Reserved1
        *reinterpret_cast<uint32_t*>(&header[36]) = std::stoul(properties.at("Reserved1").substr(2), nullptr, 16);

        // Number of Directory Sectors
        *reinterpret_cast<uint32_t*>(&header[40]) = std::stoul(properties.at("Number of Directory Sectors"));

        // Number of FAT Sectors
        *reinterpret_cast<uint32_t*>(&header[44]) = std::stoul(properties.at("Number of FAT Sectors"));

        // First Directory Sector Location
        *reinterpret_cast<uint32_t*>(&header[48]) = std::stoul(properties.at("First Directory Sector Location").substr(2), nullptr, 16);

        // Transaction Signature Number
        *reinterpret_cast<uint32_t*>(&header[52]) = std::stoul(properties.at("Transaction Signature Number"));

        // Mini Stream Cutoff Size
        *reinterpret_cast<uint32_t*>(&header[56]) = std::stoul(properties.at("Mini Stream Cutoff Size"));

        // First Mini FAT Sector Location
        *reinterpret_cast<uint32_t*>(&header[60]) = std::stoul(properties.at("First Mini FAT Sector Location").substr(2), nullptr, 16);

        // Number of Mini FAT Sectors
        *reinterpret_cast<uint32_t*>(&header[64]) = std::stoul(properties.at("Number of Mini FAT Sectors"));

        // First DIFAT Sector Location
        *reinterpret_cast<uint32_t*>(&header[68]) = std::stoul(properties.at("First DIFAT Sector Location").substr(2), nullptr, 16);

        // Number of DIFAT Sectors
        *reinterpret_cast<uint32_t*>(&header[72]) = std::stoul(properties.at("Number of DIFAT Sectors"));

        // DIFAT
        std::istringstream difatIss(properties.at("DIFAT"));
        std::string token;
        int idx = 76;
        while (difatIss >> token) {
            *reinterpret_cast<uint32_t*>(&header[idx]) = std::stoul(token.substr(2), nullptr, 16);
            idx += 4;
        }

        outFile.write(reinterpret_cast<const char*>(header.data()), 512);

        inFile.seekg(512);
        char buf[4096];
        while (inFile.read(buf, sizeof(buf))) {
            outFile.write(buf, inFile.gcount());
        }
        outFile.write(buf, inFile.gcount());  // Last chunk

        std::cout << "Modified file written to " << outFn << std::endl;
    }
};

// Example usage:
// int main() {
//     try {
//         XLSHeaderParser parser("example.xls");
//         parser.printProperties();
//         parser.writeHeader();
//     } catch (const std::exception& e) {
//         std::cerr << e.what() << std::endl;
//     }
//     return 0;
// }