Task 086: .CHM File Format
Task 086: .CHM File Format
- The specifications for the .CHM file format, also known as Microsoft Compiled HTML Help, describe a container format that stores a collection of HTML pages, images, and other data in a compressed binary structure. It utilizes the ITSF (IT Storage Format) as its underlying file system, incorporating LZX compression for content sections. The format includes an initial header, header sections for metadata and directory listings, and content sections. Detailed structures are derived from reverse-engineered documentation, as Microsoft has not officially released the full specification. The intrinsic properties, which are fixed elements in the headers and sections defining the file's structure and metadata, are listed below:
- Signature: The file signature "ITSF" (4 bytes).
- Version: The format version, typically 3 (4 bytes).
- Total Header Length: The length of the entire header, including the section table and additional data (4 bytes).
- Unknown1: A fixed value of 1 (4 bytes).
- Timestamp: A big-endian timestamp representing seconds and fractional seconds (4 bytes).
- Language ID: The Windows Language ID, such as 0x0409 for English (US) (4 bytes).
- GUID1: A 16-byte GUID, typically {7C01FD10-7BAA-11D0-9E0C-00A0-C922-E6EC}.
- GUID2: A 16-byte GUID, typically {7C01FD11-7BAA-11D0-9E0C-00A0-C922-E6EC}.
- Section 0 Offset: The offset to Header Section 0 from the file start (8 bytes).
- Section 0 Length: The length of Header Section 0 (8 bytes).
- Section 1 Offset: The offset to Header Section 1 (directory) from the file start (8 bytes).
- Section 1 Length: The length of Header Section 1 (8 bytes).
- Content Section 0 Offset: The offset to the first content section (8 bytes, version 3 specific).
- Section0 Unknown1: A fixed value, typically 0x01FE (4 bytes).
- Section0 Unknown2: A fixed value of 0 (4 bytes).
- File Size: The total size of the .CHM file (8 bytes).
- Section0 Unknown3: A fixed value of 0 (4 bytes).
- Section0 Unknown4: A fixed value of 0 (4 bytes).
- Directory Signature: The directory header signature "ITSP" (4 bytes).
- Directory Version: The directory version, typically 1 (4 bytes).
- Directory Header Length: The length of the directory header (4 bytes).
- Directory Unknown1: A fixed value of 0x0A (4 bytes).
- Directory Chunk Size: The size of directory chunks, typically 0x1000 (4 bytes).
- Quickref Density: The density of the quick reference section, usually 2 (4 bytes).
- Index Tree Depth: The depth of the index tree, 1 (no index) or 2 (one level) (4 bytes).
- Root Index Chunk Number: The chunk number of the root index, -1 if none (4 bytes).
- First Listing Chunk Number: The chunk number of the first PMGL listing chunk (4 bytes).
- Last Listing Chunk Number: The chunk number of the last PMGL listing chunk (4 bytes).
- Directory Unknown2: A fixed value of -1 (4 bytes).
- Number of Directory Chunks: The total number of directory chunks (4 bytes).
- Directory Language ID: The Windows Language ID (repeated) (4 bytes).
- Directory GUID: A 16-byte GUID, typically {5D02926A-212E-11D0-9DF9-00A0C922E6EC}.
Two direct download links for .CHM files are:
- https://raw.githubusercontent.com/daishisystems/Daishi.PaySharp/master/Daishi.PaySharp/PaySharp.NET API DOC.chm
- https://raw.githubusercontent.com/kbandla/sysinternals/master/Tools/Pstools.chm
The following is an HTML snippet with embedded JavaScript suitable for embedding in a Ghost blog post. It provides a drag-and-drop interface for a .CHM file, parses the binary data to extract the listed properties, and displays them on the screen.
Drag and drop .CHM file here
- The following Python class can open a .CHM file, decode and read the properties, print them to the console, and includes a method to write modified properties back to a new file.
import struct
import uuid
import os
class CHMParser:
def __init__(self, filepath):
self.filepath = filepath
self.properties = {}
self._read_properties()
def _read_properties(self):
with open(self.filepath, 'rb') as f:
data = f.read()
# Initial Header
self.properties['Signature'] = data[0:4].decode('ascii')
self.properties['Version'] = struct.unpack('<I', data[4:8])[0]
self.properties['Total Header Length'] = struct.unpack('<I', data[8:12])[0]
self.properties['Unknown1'] = struct.unpack('<I', data[12:16])[0]
self.properties['Timestamp'] = struct.unpack('>I', data[16:20])[0] # big-endian
self.properties['Language ID'] = struct.unpack('<I', data[20:24])[0]
self.properties['GUID1'] = uuid.UUID(bytes_le=data[24:40])
self.properties['GUID2'] = uuid.UUID(bytes_le=data[40:56])
section_table_offset = 0x38
self.properties['Section 0 Offset'] = struct.unpack('<Q', data[section_table_offset:section_table_offset+8])[0]
self.properties['Section 0 Length'] = struct.unpack('<Q', data[section_table_offset+8:section_table_offset+16])[0]
self.properties['Section 1 Offset'] = struct.unpack('<Q', data[section_table_offset+16:section_table_offset+24])[0]
self.properties['Section 1 Length'] = struct.unpack('<Q', data[section_table_offset+24:section_table_offset+32])[0]
self.properties['Content Section 0 Offset'] = struct.unpack('<Q', data[section_table_offset+32:section_table_offset+40])[0]
# Section 0
sec0_offset = self.properties['Section 0 Offset']
self.properties['Section0 Unknown1'] = struct.unpack('<I', data[sec0_offset:sec0_offset+4])[0]
self.properties['Section0 Unknown2'] = struct.unpack('<I', data[sec0_offset+4:sec0_offset+8])[0]
self.properties['File Size'] = struct.unpack('<Q', data[sec0_offset+8:sec0_offset+16])[0]
self.properties['Section0 Unknown3'] = struct.unpack('<I', data[sec0_offset+16:sec0_offset+20])[0]
self.properties['Section0 Unknown4'] = struct.unpack('<I', data[sec0_offset+20:sec0_offset+24])[0]
# Section 1 Directory Header
sec1_offset = self.properties['Section 1 Offset']
self.properties['Directory Signature'] = data[sec1_offset:sec1_offset+4].decode('ascii')
self.properties['Directory Version'] = struct.unpack('<I', data[sec1_offset+4:sec1_offset+8])[0]
self.properties['Directory Header Length'] = struct.unpack('<I', data[sec1_offset+8:sec1_offset+12])[0]
self.properties['Directory Unknown1'] = struct.unpack('<I', data[sec1_offset+12:sec1_offset+16])[0]
self.properties['Directory Chunk Size'] = struct.unpack('<I', data[sec1_offset+16:sec1_offset+20])[0]
self.properties['Quickref Density'] = struct.unpack('<I', data[sec1_offset+20:sec1_offset+24])[0]
self.properties['Index Tree Depth'] = struct.unpack('<I', data[sec1_offset+24:sec1_offset+28])[0]
self.properties['Root Index Chunk Number'] = struct.unpack('<i', data[sec1_offset+28:sec1_offset+32])[0]
self.properties['First Listing Chunk Number'] = struct.unpack('<I', data[sec1_offset+32:sec1_offset+36])[0]
self.properties['Last Listing Chunk Number'] = struct.unpack('<I', data[sec1_offset+36:sec1_offset+40])[0]
self.properties['Directory Unknown2'] = struct.unpack('<i', data[sec1_offset+40:sec1_offset+44])[0]
self.properties['Number of Directory Chunks'] = struct.unpack('<I', data[sec1_offset+44:sec1_offset+48])[0]
self.properties['Directory Language ID'] = struct.unpack('<I', data[sec1_offset+48:sec1_offset+52])[0]
self.properties['Directory GUID'] = uuid.UUID(bytes_le=data[sec1_offset+52:sec1_offset+68])
def print_properties(self):
for key, value in self.properties.items():
print(f"{key}: {value}")
def write_modified(self, output_path, modifications=None):
with open(self.filepath, 'rb') as f:
data = bytearray(f.read())
if modifications:
for key, value in modifications.items():
# Example: Modify language ID (implement others as needed)
if key == 'Language ID':
struct.pack_into('<I', data, 20, value)
# Add packing for other fields similarly
with open(output_path, 'wb') as f:
f.write(data)
# Usage example:
# parser = CHMParser('example.chm')
# parser.print_properties()
# parser.write_modified('modified.chm', {'Language ID': 0x0407})
- The following Java class can open a .CHM file, decode and read the properties, print them to the console, and includes a method to write modified properties back to a new file.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.*;
public class CHMParser {
private String filepath;
private Map<String, Object> properties = new HashMap<>();
public CHMParser(String filepath) {
this.filepath = filepath;
readProperties();
}
private void readProperties() {
try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
ByteBuffer buffer = ByteBuffer.allocate((int) raf.length()).order(ByteOrder.LITTLE_ENDIAN);
raf.getChannel().read(buffer);
buffer.flip();
// Initial Header
properties.put("Signature", new String(new byte[]{buffer.get(0), buffer.get(1), buffer.get(2), buffer.get(3)}));
properties.put("Version", buffer.getInt(4));
properties.put("Total Header Length", buffer.getInt(8));
properties.put("Unknown1", buffer.getInt(12));
buffer.order(ByteOrder.BIG_ENDIAN);
properties.put("Timestamp", buffer.getInt(16));
buffer.order(ByteOrder.LITTLE_ENDIAN);
properties.put("Language ID", buffer.getInt(20));
properties.put("GUID1", getUUID(buffer, 24));
properties.put("GUID2", getUUID(buffer, 40));
int sectionTableOffset = 0x38;
properties.put("Section 0 Offset", buffer.getLong(sectionTableOffset));
properties.put("Section 0 Length", buffer.getLong(sectionTableOffset + 8));
properties.put("Section 1 Offset", buffer.getLong(sectionTableOffset + 16));
properties.put("Section 1 Length", buffer.getLong(sectionTableOffset + 24));
properties.put("Content Section 0 Offset", buffer.getLong(sectionTableOffset + 32));
// Section 0
long sec0Offset = (long) properties.get("Section 0 Offset");
properties.put("Section0 Unknown1", buffer.getInt((int) sec0Offset));
properties.put("Section0 Unknown2", buffer.getInt((int) sec0Offset + 4));
properties.put("File Size", buffer.getLong((int) sec0Offset + 8));
properties.put("Section0 Unknown3", buffer.getInt((int) sec0Offset + 16));
properties.put("Section0 Unknown4", buffer.getInt((int) sec0Offset + 20));
// Section 1 Directory Header
long sec1Offset = (long) properties.get("Section 1 Offset");
properties.put("Directory Signature", new String(new byte[]{buffer.get((int) sec1Offset), buffer.get((int) sec1Offset + 1), buffer.get((int) sec1Offset + 2), buffer.get((int) sec1Offset + 3)}));
properties.put("Directory Version", buffer.getInt((int) sec1Offset + 4));
properties.put("Directory Header Length", buffer.getInt((int) sec1Offset + 8));
properties.put("Directory Unknown1", buffer.getInt((int) sec1Offset + 12));
properties.put("Directory Chunk Size", buffer.getInt((int) sec1Offset + 16));
properties.put("Quickref Density", buffer.getInt((int) sec1Offset + 20));
properties.put("Index Tree Depth", buffer.getInt((int) sec1Offset + 24));
properties.put("Root Index Chunk Number", buffer.getInt((int) sec1Offset + 28));
properties.put("First Listing Chunk Number", buffer.getInt((int) sec1Offset + 32));
properties.put("Last Listing Chunk Number", buffer.getInt((int) sec1Offset + 36));
properties.put("Directory Unknown2", buffer.getInt((int) sec1Offset + 40));
properties.put("Number of Directory Chunks", buffer.getInt((int) sec1Offset + 44));
properties.put("Directory Language ID", buffer.getInt((int) sec1Offset + 48));
properties.put("Directory GUID", getUUID(buffer, (int) sec1Offset + 52));
} catch (IOException e) {
e.printStackTrace();
}
}
private UUID getUUID(ByteBuffer buffer, int offset) {
long msb = ((long) buffer.getInt(offset) << 32) | (buffer.getShort(offset + 4) & 0xFFFFL) << 16 | (buffer.getShort(offset + 6) & 0xFFFFL);
long lsb = ((long) buffer.get(offset + 8) & 0xFFL) << 56 | ((long) buffer.get(offset + 9) & 0xFFL) << 48;
for (int i = 0; i < 6; i++) {
lsb |= ((long) buffer.get(offset + 10 + i) & 0xFFL) << (40 - 8 * i);
}
return new UUID(msb, lsb);
}
public void printProperties() {
properties.forEach((key, value) -> System.out.println(key + ": " + value));
}
public void writeModified(String outputPath, Map<String, Object> modifications) {
try {
byte[] data = Files.readAllBytes(Paths.get(filepath));
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
if (modifications != null) {
modifications.forEach((key, value) -> {
// Example: Modify Language ID
if ("Language ID".equals(key)) {
buffer.putInt(20, (int) value);
}
// Implement other modifications similarly
});
}
Files.write(Paths.get(outputPath), data);
} catch (IOException e) {
e.printStackTrace();
}
}
// Usage example:
// public static void main(String[] args) {
// CHMParser parser = new CHMParser("example.chm");
// parser.printProperties();
// Map<String, Object> mods = new HashMap<>();
// mods.put("Language ID", 0x0407);
// parser.writeModified("modified.chm", mods);
// }
}
- The following JavaScript class (Node.js compatible) can open a .CHM file, decode and read the properties, print them to the console, and includes a method to write modified properties back to a new file.
const fs = require('fs');
const { v4: uuidv4 } = require('uuid'); // Note: uuid for parsing, install if needed
class CHMParser {
constructor(filepath) {
this.filepath = filepath;
this.properties = {};
this.data = fs.readFileSync(filepath);
this.dv = new DataView(this.data.buffer);
this.readProperties();
}
readProperties() {
this.properties['Signature'] = String.fromCharCode(this.dv.getUint8(0), this.dv.getUint8(1), this.dv.getUint8(2), this.dv.getUint8(3));
this.properties['Version'] = this.dv.getUint32(4, true);
this.properties['Total Header Length'] = this.dv.getUint32(8, true);
this.properties['Unknown1'] = this.dv.getUint32(12, true);
this.properties['Timestamp'] = this.dv.getUint32(16, false); // big-endian
this.properties['Language ID'] = this.dv.getUint32(20, true);
this.properties['GUID1'] = this.getGUID(24);
this.properties['GUID2'] = this.getGUID(40);
const sectionTableOffset = 0x38;
this.properties['Section 0 Offset'] = this.getBigUint64(sectionTableOffset);
this.properties['Section 0 Length'] = this.getBigUint64(sectionTableOffset + 8);
this.properties['Section 1 Offset'] = this.getBigUint64(sectionTableOffset + 16);
this.properties['Section 1 Length'] = this.getBigUint64(sectionTableOffset + 24);
this.properties['Content Section 0 Offset'] = this.getBigUint64(sectionTableOffset + 32);
const sec0Offset = Number(this.properties['Section 0 Offset']);
this.properties['Section0 Unknown1'] = this.dv.getUint32(sec0Offset, true);
this.properties['Section0 Unknown2'] = this.dv.getUint32(sec0Offset + 4, true);
this.properties['File Size'] = this.getBigUint64(sec0Offset + 8);
this.properties['Section0 Unknown3'] = this.dv.getUint32(sec0Offset + 16, true);
this.properties['Section0 Unknown4'] = this.dv.getUint32(sec0Offset + 20, true);
const sec1Offset = Number(this.properties['Section 1 Offset']);
this.properties['Directory Signature'] = String.fromCharCode(this.dv.getUint8(sec1Offset), this.dv.getUint8(sec1Offset + 1), this.dv.getUint8(sec1Offset + 2), this.dv.getUint8(sec1Offset + 3));
this.properties['Directory Version'] = this.dv.getUint32(sec1Offset + 4, true);
this.properties['Directory Header Length'] = this.dv.getUint32(sec1Offset + 8, true);
this.properties['Directory Unknown1'] = this.dv.getUint32(sec1Offset + 12, true);
this.properties['Directory Chunk Size'] = this.dv.getUint32(sec1Offset + 16, true);
this.properties['Quickref Density'] = this.dv.getUint32(sec1Offset + 20, true);
this.properties['Index Tree Depth'] = this.dv.getUint32(sec1Offset + 24, true);
this.properties['Root Index Chunk Number'] = this.dv.getInt32(sec1Offset + 28, true);
this.properties['First Listing Chunk Number'] = this.dv.getUint32(sec1Offset + 32, true);
this.properties['Last Listing Chunk Number'] = this.dv.getUint32(sec1Offset + 36, true);
this.properties['Directory Unknown2'] = this.dv.getInt32(sec1Offset + 40, true);
this.properties['Number of Directory Chunks'] = this.dv.getUint32(sec1Offset + 44, true);
this.properties['Directory Language ID'] = this.dv.getUint32(sec1Offset + 48, true);
this.properties['Directory GUID'] = this.getGUID(sec1Offset + 52);
}
getGUID(offset) {
// Simplified UUID string construction
let guid = '';
for (let i = 0; i < 16; i++) {
guid += this.dv.getUint8(offset + i).toString(16).padStart(2, '0');
if (i === 3 || i === 5 || i === 7 || i === 9) guid += '-';
}
return guid.toUpperCase();
}
getBigUint64(offset) {
const high = this.dv.getUint32(offset, true);
const low = this.dv.getUint32(offset + 4, true);
return BigInt(high) + (BigInt(low) << 32n);
}
printProperties() {
console.log(JSON.stringify(this.properties, null, 2));
}
writeModified(outputPath, modifications) {
const newData = Buffer.from(this.data);
const newDv = new DataView(newData.buffer);
if (modifications) {
for (const [key, value] of Object.entries(modifications)) {
// Example: Modify Language ID
if (key === 'Language ID') {
newDv.setUint32(20, value, true);
}
// Implement other modifications
}
}
fs.writeFileSync(outputPath, newData);
}
}
// Usage example:
// const parser = new CHMParser('example.chm');
// parser.printProperties();
// parser.writeModified('modified.chm', { 'Language ID': 0x0407 });
- The following C++ class can open a .CHM file, decode and read the properties, print them to the console, and includes a method to write modified properties back to a new file.
#include <iostream>
#include <fstream>
#include <vector>
#include <iomanip>
#include <cstring>
#include <cstdint>
class CHMParser {
private:
std::string filepath;
std::vector<uint8_t> data;
std::map<std::string, std::string> properties;
std::string toHex(uint64_t value) {
std::stringstream ss;
ss << std::hex << std::setw(16) << std::setfill('0') << value;
return ss.str();
}
std::string getGUID(size_t offset) {
std::stringstream ss;
for (size_t i = 0; i < 4; ++i) ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[offset + i];
ss << "-";
for (size_t i = 4; i < 6; ++i) ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[offset + i];
ss << "-";
for (size_t i = 6; i < 8; ++i) ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[offset + i];
ss << "-";
for (size_t i = 8; i < 10; ++i) ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[offset + i];
ss << "-";
for (size_t i = 10; i < 16; ++i) ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[offset + i];
return ss.str();
}
public:
CHMParser(const std::string& fp) : filepath(fp) {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
data.resize(size);
file.read(reinterpret_cast<char*>(data.data()), size);
readProperties();
}
void readProperties() {
char sig[5] = {0};
memcpy(sig, data.data(), 4);
properties["Signature"] = sig;
uint32_t version; memcpy(&version, data.data() + 4, 4);
properties["Version"] = std::to_string(version);
uint32_t headerLen; memcpy(&headerLen, data.data() + 8, 4);
properties["Total Header Length"] = std::to_string(headerLen);
uint32_t unknown1; memcpy(&unknown1, data.data() + 12, 4);
properties["Unknown1"] = std::to_string(unknown1);
uint32_t timestamp; memcpy(×tamp, data.data() + 16, 4); // big-endian, but for string no swap
properties["Timestamp"] = std::to_string(__builtin_bswap32(timestamp));
uint32_t langId; memcpy(&langId, data.data() + 20, 4);
properties["Language ID"] = std::to_string(langId);
properties["GUID1"] = getGUID(24);
properties["GUID2"] = getGUID(40);
size_t sectionTableOffset = 0x38;
uint64_t sec0Off; memcpy(&sec0Off, data.data() + sectionTableOffset, 8);
properties["Section 0 Offset"] = std::to_string(sec0Off);
uint64_t sec0Len; memcpy(&sec0Len, data.data() + sectionTableOffset + 8, 8);
properties["Section 0 Length"] = std::to_string(sec0Len);
uint64_t sec1Off; memcpy(&sec1Off, data.data() + sectionTableOffset + 16, 8);
properties["Section 1 Offset"] = std::to_string(sec1Off);
uint64_t sec1Len; memcpy(&sec1Len, data.data() + sectionTableOffset + 24, 8);
properties["Section 1 Length"] = std::to_string(sec1Len);
uint64_t contentOff; memcpy(&contentOff, data.data() + sectionTableOffset + 32, 8);
properties["Content Section 0 Offset"] = std::to_string(contentOff);
size_t s0 = sec0Off;
uint32_t s0u1; memcpy(&s0u1, data.data() + s0, 4);
properties["Section0 Unknown1"] = std::to_string(s0u1);
uint32_t s0u2; memcpy(&s0u2, data.data() + s0 + 4, 4);
properties["Section0 Unknown2"] = std::to_string(s0u2);
uint64_t fileSize; memcpy(&fileSize, data.data() + s0 + 8, 8);
properties["File Size"] = std::to_string(fileSize);
uint32_t s0u3; memcpy(&s0u3, data.data() + s0 + 16, 4);
properties["Section0 Unknown3"] = std::to_string(s0u3);
uint32_t s0u4; memcpy(&s0u4, data.data() + s0 + 20, 4);
properties["Section0 Unknown4"] = std::to_string(s0u4);
size_t s1 = sec1Off;
char dsig[5] = {0}; memcpy(dsig, data.data() + s1, 4);
properties["Directory Signature"] = dsig;
uint32_t dver; memcpy(&dver, data.data() + s1 + 4, 4);
properties["Directory Version"] = std::to_string(dver);
uint32_t dhlen; memcpy(&dhlen, data.data() + s1 + 8, 4);
properties["Directory Header Length"] = std::to_string(dhlen);
uint32_t du1; memcpy(&du1, data.data() + s1 + 12, 4);
properties["Directory Unknown1"] = std::to_string(du1);
uint32_t chsize; memcpy(&chsize, data.data() + s1 + 16, 4);
properties["Directory Chunk Size"] = std::to_string(chsize);
uint32_t qdens; memcpy(&qdens, data.data() + s1 + 20, 4);
properties["Quickref Density"] = std::to_string(qdens);
uint32_t idepth; memcpy(&idepth, data.data() + s1 + 24, 4);
properties["Index Tree Depth"] = std::to_string(idepth);
int32_t rootidx; memcpy(&rootidx, data.data() + s1 + 28, 4);
properties["Root Index Chunk Number"] = std::to_string(rootidx);
uint32_t firstlst; memcpy(&firstlst, data.data() + s1 + 32, 4);
properties["First Listing Chunk Number"] = std::to_string(firstlst);
uint32_t lastlst; memcpy(&lastlst, data.data() + s1 + 36, 4);
properties["Last Listing Chunk Number"] = std::to_string(lastlst);
int32_t du2; memcpy(&du2, data.data() + s1 + 40, 4);
properties["Directory Unknown2"] = std::to_string(du2);
uint32_t numchunks; memcpy(&numchunks, data.data() + s1 + 44, 4);
properties["Number of Directory Chunks"] = std::to_string(numchunks);
uint32_t dlang; memcpy(&dlang, data.data() + s1 + 48, 4);
properties["Directory Language ID"] = std::to_string(dlang);
properties["Directory GUID"] = getGUID(s1 + 52);
}
void printProperties() {
for (const auto& p : properties) {
std::cout << p.first << ": " << p.second << std::endl;
}
}
void writeModified(const std::string& outputPath, const std::map<std::string, std::string>& modifications) {
std::vector<uint8_t> newData = data;
for (const auto& mod : modifications) {
// Example: Modify Language ID
if (mod.first == "Language ID") {
uint32_t value = std::stoul(mod.second);
memcpy(newData.data() + 20, &value, 4);
}
// Implement other modifications
}
std::ofstream out(outputPath, std::ios::binary);
out.write(reinterpret_cast<const char*>(newData.data()), newData.size());
}
};
// Usage example:
// int main() {
// CHMParser parser("example.chm");
// parser.printProperties();
// std::map<std::string, std::string> mods = {{"Language ID", "1031"}};
// parser.writeModified("modified.chm", mods);
// return 0;
// }