Task 243: .FRM File Format

Task 243: .FRM File Format

File Format Specifications for .FRM

The .FRM file format refers to the MySQL Table Definition Format, a binary file used to store metadata for tables in MySQL databases (prior to MySQL 8.0, where it was deprecated in favor of data dictionary tables). It contains table structure details such as column definitions, indexes, row formats, and engine options. The format is little-endian and consists of fixed and variable-length sections. The primary sources for this specification are the dbsake documentation and MySQL internals references, which describe the binary layout decoded from MySQL source code.

1. List of All Properties Intrinsic to the File Format

The properties (fields) intrinsic to the .FRM format are derived from its core sections: the fixed 64-byte Fileinfo header (starting at offset 0x00) and the 288-byte Forminfo section (starting at offset 0x40). These encode table-level metadata, lengths for subsequent sections, and structural options. Variable sections (e.g., Keyinfo, Columninfo) follow based on lengths in these headers, but the listed properties are the foundational ones directly defining the file's structure and table characteristics. All multi-byte values are little-endian integers unless noted.

Fileinfo Section Properties (Offsets 0x00–0x3F)

  • Magic Identifier (offset 0x00, size 2 bytes): Fixed byte sequence 0xFE 0x01, identifying the file as a valid .FRM.
  • FRM Version (offset 0x02, size 1 byte): Version of the .FRM format (e.g., 9 or 10 in MySQL 5.0+).
  • Legacy DB Type (offset 0x03, size 1 byte): Enum value indicating the storage engine type (e.g., from sql/handler.h).
  • Names Length (offset 0x04, size 2 bytes): Length of section names (typically 3; legacy value was 1 in MySQL 3.23).
  • IO Size (offset 0x06, size 2 bytes): Fixed I/O block size (always 4096 or 0x1000).
  • Number of Forms (offset 0x08, size 2 bytes): Number of forms in the .FRM (always 1).
  • Unused Alignment Field (offset 0x0A, size 4 bytes): Legacy alignment padding; not used in modern MySQL.
  • Temporary Key Length (offset 0x0E, size 2 bytes): Length of temporary key data; 0xFFFF indicates a 4-byte value at 0x2F.
  • Record Length (offset 0x10, size 2 bytes): Size of the default values byte string (used in Defaults section).
  • Max Rows (offset 0x12, size 4 bytes): Table option for maximum rows (MAX_ROWS).
  • Min Rows (offset 0x16, size 4 bytes): Table option for minimum rows (MIN_ROWS).
  • Unused Flag (offset 0x1A, size 1 byte): Always 0; reserved.
  • Long Pack-Fields Flag (offset 0x1B, size 1 byte): Always 2; enables long packed fields.
  • Key Info Length (offset 0x1C, size 2 bytes): Size of the Keyinfo section in bytes.
  • Table Options (offset 0x1E, size 2 bytes): Bitmask of HA_OPTION_* flags (e.g., pack keys, checksums).
  • Unused Filename Field (offset 0x20, size 1 byte): Reserved; no longer used for filenames.
  • 5.0 Mark (offset 0x21, size 1 byte): Marker for MySQL 5.0+ .FRM files (value 5).
  • Average Row Length (offset 0x22, size 4 bytes): Table option for estimated average row length (AVG_ROW_LENGTH).
  • Default Character Set (offset 0x26, size 1 byte): ID of the default character set/collation.
  • Unused Transactional/Page Checksum (offset 0x27, size 1 byte): Reserved for future options like TRANSACTIONAL or PAGE_CHECKSUM.
  • Row Format (offset 0x28, size 1 byte): Table row format (e.g., fixed, dynamic, compressed).
  • Unused RAID Type (offset 0x29, size 1 byte): Legacy RAID chunk type; always 0.
  • Unused RAID Chunks (offset 0x2A, size 1 byte): Legacy RAID chunk count; always 0.
  • Unused RAID Chunk Size (offset 0x2B, size 4 bytes): Legacy RAID chunk size; always 0.
  • Extended Key Info Length (offset 0x2F, size 4 bytes): Full size of Keyinfo section for index metadata.
  • MySQL Version (offset 0x33, size 4 bytes): Encoded MYSQL_VERSION_ID (e.g., 0x0000C5B6 for 5.6.14).
  • Extra Info Size (offset 0x37, size 4 bytes): Size of table extra info (e.g., CONNECTION for FEDERATED, ENGINE, PARTITION).

Forminfo Section Properties (Offsets 0x40–0x15F, 288 bytes total)

  • Column Count (offset 0x102 relative to Forminfo / absolute 0x142, size 2 bytes): Number of columns in the table.
  • Screens Length (offset 0x104 relative / absolute 0x144, size 2 bytes): Bytes following Forminfo before column metadata.
  • Null Columns (offset 0x11A relative / absolute 0x15A, size 2 bytes): Number of nullable columns.
  • Names Length (offset 0x10C relative / absolute 0x14C, size 2 bytes): Total bytes for column names (including delimiters).
  • Interval Length (offset 0x112 relative / absolute 0x152, size 2 bytes): Total bytes for SET/ENUM labels (including delimiters).
  • Comments Length (offset 0x11C relative / absolute 0x15C, size 2 bytes): Total bytes for column comments.

These properties define the file's intrinsic structure, enabling parsing of subsequent variable sections like Keyinfo (indexes), Columninfo (column details), and Defaults (default values).

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .FRM Property Dump

Embed this as a Ghost blog post HTML block. It creates a drag-and-drop zone; dropping a .FRM file reads it as an ArrayBuffer, parses the properties from Fileinfo and Forminfo, and dumps them to a <pre> element on screen.

Drag and drop a .FRM file here to dump properties.


4. Python Class for .FRM Parsing

This class opens a .FRM file, decodes/reads the properties into a dict, prints them to console, and supports writing (reconstructing from properties dict or copying the original).

import struct

class FrmParser:
    def __init__(self, filename=None, data=None):
        if filename:
            with open(filename, 'rb') as f:
                self.data = f.read()
        elif data:
            self.data = data
        else:
            raise ValueError("Provide filename or data")

    def read_properties(self):
        properties = {}
        # Fileinfo (little-endian)
        properties['magic'] = hex(struct.unpack('<H', self.data[0:2])[0])
        properties['frm_version'] = self.data[2]
        properties['legacy_db_type'] = self.data[3]
        properties['names_length'] = struct.unpack('<H', self.data[4:6])[0]
        properties['io_size'] = struct.unpack('<H', self.data[6:8])[0]
        properties['num_forms'] = struct.unpack('<H', self.data[8:10])[0]
        properties['unused_alignment'] = struct.unpack('<I', self.data[10:14])[0]
        properties['tmp_key_length'] = struct.unpack('<H', self.data[14:16])[0]
        properties['rec_length'] = struct.unpack('<H', self.data[16:18])[0]
        properties['max_rows'] = struct.unpack('<I', self.data[18:22])[0]
        properties['min_rows'] = struct.unpack('<I', self.data[22:26])[0]
        properties['unused_flag'] = self.data[26]
        properties['long_pack_fields'] = self.data[27]
        properties['key_info_length'] = struct.unpack('<H', self.data[28:30])[0]
        properties['table_options'] = struct.unpack('<H', self.data[30:32])[0]
        properties['unused_filename'] = self.data[32]
        properties['mark_50'] = self.data[33]
        properties['avg_row_length'] = struct.unpack('<I', self.data[34:38])[0]
        properties['default_charset'] = self.data[38]
        properties['unused_transactional'] = self.data[39]
        properties['row_format'] = self.data[40]
        properties['unused_raid_type'] = self.data[41]
        properties['unused_raid_chunks'] = self.data[42]
        properties['unused_raid_chunk_size'] = struct.unpack('<I', self.data[43:47])[0]
        properties['ext_key_info_length'] = struct.unpack('<I', self.data[47:51])[0]
        properties['mysql_version'] = struct.unpack('<I', self.data[51:55])[0]
        properties['extra_info_size'] = struct.unpack('<I', self.data[55:59])[0]

        # Forminfo (offset 64)
        offset = 64
        properties['column_count'] = struct.unpack('<H', self.data[offset + 258:offset + 260])[0]
        properties['screens_length'] = struct.unpack('<H', self.data[offset + 260:offset + 262])[0]
        properties['null_columns'] = struct.unpack('<H', self.data[offset + 282:offset + 284])[0]
        properties['names_length_form'] = struct.unpack('<H', self.data[offset + 268:offset + 270])[0]
        properties['interval_length'] = struct.unpack('<H', self.data[offset + 274:offset + 276])[0]
        properties['comments_length'] = struct.unpack('<H', self.data[offset + 284:offset + 286])[0]

        # Print to console
        import pprint
        pprint.pprint(properties)
        return properties

    def write(self, filename, properties=None):
        if properties is None:
            # Copy original
            with open(filename, 'wb') as f:
                f.write(self.data)
        else:
            # Reconstruct simple (only Fileinfo/Forminfo; extend for full)
            data = bytearray(64 + 288)  # Minimal
            # Pack Fileinfo
            data[0:2] = struct.pack('<H', int(properties['magic'], 16))
            data[2] = properties['frm_version']
            data[3] = properties['legacy_db_type']
            data[4:6] = struct.pack('<H', properties['names_length'])
            # ... (similar for all fields; omitted for brevity, add as needed)
            data[47:51] = struct.pack('<I', properties['ext_key_info_length'])
            # Pack Forminfo at 64
            data[64 + 258:64 + 260] = struct.pack('<H', properties['column_count'])
            # ... (similar)
            with open(filename, 'wb') as f:
                f.write(data)
            print(f"Wrote to {filename}")

# Usage
# parser = FrmParser('sample.frm')
# props = parser.read_properties()
# parser.write('output.frm', props)

5. Java Class for .FRM Parsing

This class uses ByteBuffer for binary parsing. Read decodes properties, prints to console (System.out), and write reconstructs or copies.

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class FrmParser {
    private byte[] data;

    public FrmParser(String filename) throws IOException {
        this.data = Files.readAllBytes(Paths.get(filename));
    }

    public FrmParser(byte[] data) {
        this.data = data;
    }

    public Map<String, Object> readProperties() {
        Map<String, Object> properties = new HashMap<>();
        ByteBuffer buffer = ByteBuffer.wrap(this.data).order(ByteOrder.LITTLE_ENDIAN);

        properties.put("magic", String.format("%04X", buffer.getShort(0)));
        properties.put("frmVersion", buffer.get(2) & 0xFF);
        properties.put("legacyDbType", buffer.get(3) & 0xFF);
        properties.put("namesLength", buffer.getShort(4));
        properties.put("ioSize", buffer.getShort(6));
        properties.put("numForms", buffer.getShort(8));
        properties.put("unusedAlignment", buffer.getInt(10));
        properties.put("tmpKeyLength", buffer.getShort(14));
        properties.put("recLength", buffer.getShort(16));
        properties.put("maxRows", buffer.getInt(18));
        properties.put("minRows", buffer.getInt(22));
        properties.put("unusedFlag", buffer.get(26) & 0xFF);
        properties.put("longPackFields", buffer.get(27) & 0xFF);
        properties.put("keyInfoLength", buffer.getShort(28));
        properties.put("tableOptions", buffer.getShort(30));
        properties.put("unusedFilename", buffer.get(32) & 0xFF);
        properties.put("mark50", buffer.get(33) & 0xFF);
        properties.put("avgRowLength", buffer.getInt(34));
        properties.put("defaultCharset", buffer.get(38) & 0xFF);
        properties.put("unusedTransactional", buffer.get(39) & 0xFF);
        properties.put("rowFormat", buffer.get(40) & 0xFF);
        properties.put("unusedRaidType", buffer.get(41) & 0xFF);
        properties.put("unusedRaidChunks", buffer.get(42) & 0xFF);
        properties.put("unusedRaidChunkSize", buffer.getInt(43));
        properties.put("extKeyInfoLength", buffer.getInt(47));
        properties.put("mysqlVersion", buffer.getInt(51));
        properties.put("extraInfoSize", buffer.getInt(55));

        // Forminfo at offset 64
        buffer.position(64);
        properties.put("columnCount", buffer.getShort(258));
        properties.put("screensLength", buffer.getShort(260));
        properties.put("nullColumns", buffer.getShort(282));
        properties.put("namesLengthForm", buffer.getShort(268));
        properties.put("intervalLength", buffer.getShort(274));
        properties.put("commentsLength", buffer.getShort(284));

        // Print to console
        System.out.println(properties);
        return properties;
    }

    public void write(String filename, Map<String, Object> properties) throws IOException {
        if (properties == null) {
            // Copy original
            Files.write(Paths.get(filename), this.data);
        } else {
            // Reconstruct minimal (extend as needed)
            ByteBuffer out = ByteBuffer.allocate(64 + 288).order(ByteOrder.LITTLE_ENDIAN);
            out.putShort(0, (short) Integer.parseInt((String) properties.get("magic"), 16));
            out.put((byte) properties.get("frmVersion"));
            // ... (pack all fields similarly)
            out.position(64 + 258);
            out.putShort((short) (int) properties.get("columnCount"));
            // ...
            try (FileOutputStream fos = new FileOutputStream(filename)) {
                fos.write(out.array());
            }
        }
        System.out.println("Wrote to " + filename);
    }

    // Usage
    // FrmParser parser = new FrmParser("sample.frm");
    // Map props = parser.readProperties();
    // parser.write("output.frm", props);
}

6. JavaScript Class for .FRM Parsing (Node.js)

This Node.js class uses fs for file I/O. Read decodes to object, logs to console, write reconstructs or copies.

const fs = require('fs');

class FrmParser {
  constructor(filename = null, data = null) {
    if (filename) {
      this.data = new Uint8Array(fs.readFileSync(filename));
    } else if (data) {
      this.data = new Uint8Array(data);
    } else {
      throw new Error('Provide filename or data');
    }
  }

  readProperties() {
    const view = new DataView(this.data.buffer);
    const properties = {
      magic: view.getUint16(0, true).toString(16).padStart(4, '0').toUpperCase(),
      frmVersion: view.getUint8(2),
      legacyDbType: view.getUint8(3),
      namesLength: view.getUint16(4, true),
      ioSize: view.getUint16(6, true),
      numForms: view.getUint16(8, true),
      unusedAlignment: view.getUint32(10, true),
      tmpKeyLength: view.getUint16(14, true),
      recLength: view.getUint16(16, true),
      maxRows: view.getUint32(18, true),
      minRows: view.getUint32(22, true),
      unusedFlag: view.getUint8(26),
      longPackFields: view.getUint8(27),
      keyInfoLength: view.getUint16(28, true),
      tableOptions: view.getUint16(30, true),
      unusedFilename: view.getUint8(32),
      mark50: view.getUint8(33),
      avgRowLength: view.getUint32(34, true),
      defaultCharset: view.getUint8(38),
      unusedTransactional: view.getUint8(39),
      rowFormat: view.getUint8(40),
      unusedRaidType: view.getUint8(41),
      unusedRaidChunks: view.getUint8(42),
      unusedRaidChunkSize: view.getUint32(43, true),
      extKeyInfoLength: view.getUint32(47, true),
      mysqlVersion: view.getUint32(51, true),
      extraInfoSize: view.getUint32(55, true),
      // Forminfo at 64
      columnCount: view.getUint16(64 + 258, true),
      screensLength: view.getUint16(64 + 260, true),
      nullColumns: view.getUint16(64 + 282, true),
      namesLengthForm: view.getUint16(64 + 268, true),
      intervalLength: view.getUint16(64 + 274, true),
      commentsLength: view.getUint16(64 + 284, true)
    };

    console.log(properties);
    return properties;
  }

  write(filename, properties = null) {
    if (properties === null) {
      fs.writeFileSync(filename, this.data);
    } else {
      const out = new Uint8Array(64 + 288);
      const view = new DataView(out.buffer);
      view.setUint16(0, parseInt(properties.magic, 16), true);
      view.setUint8(2, properties.frmVersion);
      // ... (set all fields)
      view.setUint16(64 + 258, properties.columnCount, true);
      // ...
      fs.writeFileSync(filename, out);
    }
    console.log(`Wrote to ${filename}`);
  }
}

// Usage
// const parser = new FrmParser('sample.frm');
// const props = parser.readProperties();
// parser.write('output.frm', props);

7. C Class (Struct with Functions) for .FRM Parsing

This uses standard C with fread/fwrite. Compile with gcc frm.c -o frm. Read parses to struct, prints to stdout, write reconstructs or copies.

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

typedef struct {
    uint16_t magic;
    uint8_t frm_version;
    uint8_t legacy_db_type;
    uint16_t names_length;
    uint16_t io_size;
    uint16_t num_forms;
    uint32_t unused_alignment;
    uint16_t tmp_key_length;
    uint16_t rec_length;
    uint32_t max_rows;
    uint32_t min_rows;
    uint8_t unused_flag;
    uint8_t long_pack_fields;
    uint16_t key_info_length;
    uint16_t table_options;
    uint8_t unused_filename;
    uint8_t mark_50;
    uint32_t avg_row_length;
    uint8_t default_charset;
    uint8_t unused_transactional;
    uint8_t row_format;
    uint8_t unused_raid_type;
    uint8_t unused_raid_chunks;
    uint32_t unused_raid_chunk_size;
    uint32_t ext_key_info_length;
    uint32_t mysql_version;
    uint32_t extra_info_size;
    // Forminfo
    uint16_t column_count;
    uint16_t screens_length;
    uint16_t null_columns;
    uint16_t names_length_form;
    uint16_t interval_length;
    uint16_t comments_length;
} FrmProperties;

typedef struct {
    uint8_t *data;
    size_t size;
} FrmData;

FrmData *frm_load(const char *filename) {
    FILE *f = fopen(filename, "rb");
    if (!f) return NULL;
    fseek(f, 0, SEEK_END);
    size_t size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);
    FrmData *fd = malloc(sizeof(FrmData));
    fd->data = data;
    fd->size = size;
    return fd;
}

void frm_free(FrmData *fd) {
    free(fd->data);
    free(fd);
}

void frm_read_properties(FrmData *fd, FrmProperties *props) {
    uint8_t *d = fd->data;
    // Little-endian reads
    props->magic = (d[0] << 8) | d[1];
    props->frm_version = d[2];
    props->legacy_db_type = d[3];
    props->names_length = (d[4] << 8) | d[5];
    props->io_size = (d[6] << 8) | d[7];
    props->num_forms = (d[8] << 8) | d[9];
    props->unused_alignment = (d[10] << 24) | (d[11] << 16) | (d[12] << 8) | d[13];
    props->tmp_key_length = (d[14] << 8) | d[15];
    props->rec_length = (d[16] << 8) | d[17];
    props->max_rows = (d[18] << 24) | (d[19] << 16) | (d[20] << 8) | d[21];
    props->min_rows = (d[22] << 24) | (d[23] << 16) | (d[24] << 8) | d[25];
    props->unused_flag = d[26];
    props->long_pack_fields = d[27];
    props->key_info_length = (d[28] << 8) | d[29];
    props->table_options = (d[30] << 8) | d[31];
    props->unused_filename = d[32];
    props->mark_50 = d[33];
    props->avg_row_length = (d[34] << 24) | (d[35] << 16) | (d[36] << 8) | d[37];
    props->default_charset = d[38];
    props->unused_transactional = d[39];
    props->row_format = d[40];
    props->unused_raid_type = d[41];
    props->unused_raid_chunks = d[42];
    props->unused_raid_chunk_size = (d[43] << 24) | (d[44] << 16) | (d[45] << 8) | d[46];
    props->ext_key_info_length = (d[47] << 24) | (d[48] << 16) | (d[49] << 8) | d[50];
    props->mysql_version = (d[51] << 24) | (d[52] << 16) | (d[53] << 8) | d[54];
    props->extra_info_size = (d[55] << 24) | (d[56] << 16) | (d[57] << 8) | d[58];

    // Forminfo at 64
    int offset = 64;
    props->column_count = (d[offset + 258] << 8) | d[offset + 259];
    props->screens_length = (d[offset + 260] << 8) | d[offset + 261];
    props->null_columns = (d[offset + 282] << 8) | d[offset + 283];
    props->names_length_form = (d[offset + 268] << 8) | d[offset + 269];
    props->interval_length = (d[offset + 274] << 8) | d[offset + 275];
    props->comments_length = (d[offset + 284] << 8) | d[offset + 285];

    // Print to console
    printf("Magic: 0x%04X\n", props->magic);
    printf("FRM Version: %u\n", props->frm_version);
    // ... (print all; omitted for brevity)
    printf("Column Count: %u\n", props->column_count);
}

void frm_write(const char *filename, FrmData *fd, FrmProperties *props) {
    if (props == NULL) {
        // Copy
        FILE *f = fopen(filename, "wb");
        fwrite(fd->data, 1, fd->size, f);
        fclose(f);
    } else {
        // Reconstruct minimal
        uint8_t *out = malloc(64 + 288);
        // Pack (little-endian)
        out[0] = (props->magic >> 8) & 0xFF;
        out[1] = props->magic & 0xFF;
        out[2] = props->frm_version;
        // ... (pack all)
        out[47] = (props->ext_key_info_length >> 24) & 0xFF;
        // ...
        FILE *f = fopen(filename, "wb");
        fwrite(out, 1, 64 + 288, f);
        fclose(f);
        free(out);
    }
    printf("Wrote to %s\n", filename);
}

// Usage
// FrmData *fd = frm_load("sample.frm");
// FrmProperties props = {0};
// frm_read_properties(fd, &props);
// frm_write("output.frm", fd, &props);
// frm_free(fd);