Reader and writer for Claris FileMaker (.fmp12) files
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-26 08:52:08 +01:00
docs/fmp12 Read the auto-enter preset from flag byte 3 2026-10-26 08:36:31 +01:00
src Read the auto-enter preset from flag byte 3 2026-10-26 08:36:31 +01:00
tests Read the auto-enter preset from flag byte 3 2026-10-26 08:36:31 +01:00
.gitignore Initial commit for fmp, 2026 version! 2026-10-25 16:49:06 +01:00
AGENTS.md Update readme 2026-10-26 08:52:08 +01:00
Cargo.lock Initial commit for fmp, 2026 version! 2026-10-25 16:49:06 +01:00
Cargo.toml Initial commit for fmp, 2026 version! 2026-10-25 16:49:06 +01:00
LICENSE.md Add readme and license 2026-10-25 17:06:36 +01:00
README.md Update readme 2026-10-26 08:52:08 +01:00

fmp12

A Rust library for reading Claris FileMaker Pro .fmp12 database files, without FileMaker.

FileMaker's .fmp12 format is proprietary and (intended to be :p) undocumented. If you have a file but no FileMaker license, or you want to pull data into another tool, there is little to reach for. This crate decodes the format by building upon prior community efforts (fmptools, fmplib, and fp5dump).

Point it at a file and you get the tables, the field definitions, the record data, and the scripts with their steps. It is read-only and still early: it covers the core of a database (tables, fields, records) plus the outline of scripts. More features are on the way!

If your file fails to parse, please let me know.

use fmp12::FmpFile;

fn main() -> Result<(), fmp12::FmpError> {
    let file = FmpFile::open("database.fmp12")?;

    if let Some(table) = file.table("Contacts") {
        for record in table.records() {
            println!("{:?}", record.value(table, "Name"));
        }
    }
    Ok(())
}

Contents

Installing

The crate is named fmp12, MIT licensed, and uses Rust edition 2024 (so it needs a recent Rust toolchain). It is not yet published to crates.io, but it can be installed directly from the git repo:

[dependencies]
fmp12 = { git = "https://forge.smartyellow.nl/romein/fmp" }

Usage

Everything starts with FmpFile::open, which reads the whole file into memory and parses it.

File information

let file = fmp12::FmpFile::open("database.fmp12")?;

file.file_size();     // size on disk, in bytes
file.creator_name();  // the app that created it, e.g. "Pro 12.0"
file.version_date();  // a chrono::NaiveDate: the FileMaker version's save date

Tables

for table in file.tables() {
    println!("{} (id {})", table.name(), table.id());
}

let contacts = file.table("Contacts");   // Option<&Table>, looked up by name

Columns (fields)

let table = file.table("Contacts").unwrap();

for column in table.columns() {
    println!("{}: {:?}", column.name(), column.data_type());
}

let column = table.column("Email").unwrap();
column.field_type();    // FieldType: Simple, Calculation, Summary
column.data_type();     // DataType: Text, Number, Date, Time, Timestamp, Container
column.storage_type();  // FieldStorageType: Regular, Global, Calculation, UnstoredCalculation
column.auto_enter();    // AutoEnterOption: serial number, creation/modification stamps, calculations, ...
column.repetitions();   // how many repetitions the field has (1 for a normal field)
column.indexed();       // whether the field is always indexed

Anything the crate doesn't recognise comes back as an Unknown(u8) variant carrying the raw byte, so a file from a newer FileMaker version still opens.

Records

// All records. The order is not defined.
for record in table.records() {
    let name: Option<&str> = record.value(table, "Name");
}

// One record by its index. Indexes start at 1.
let first = table.record(1);

record.value(table, "Column") returns None when the column doesn't exist or the record has no value stored for it. Values are always returned as strings, including numbers, dates and timestamps; the crate does not convert them to typed values.

Scripts

for script in file.scripts() {          // ordered by id
    println!("{} (id {})", script.name(), script.id());

    for step in script.steps() {        // in execution order
        println!("  {:?} (opcode {})", step.name(), step.opcode());
    }
}

let script = file.script("Script 1");   // Option<&Script>, looked up by name

Each ScriptStep has:

step.index();    // u16: the step's index, which keys its arguments in the file
step.opcode();   // u8: the step type's numeric id (e.g. 141 = Set Variable)
step.name();     // Option<&'static str>: "Set Variable", ...; None for an unrecognised opcode
step.record();   // &[u8]: the step's raw 28-byte record

Only the outline of a script is read: which steps it has and what type each one is. The steps' arguments (the variable name and value of a Set Variable, the title and buttons of a Show Custom Dialog, calculations, ...) are not decoded yet; record() is the raw data they are derived from. Script folders and separators haven't been verified against a real file, so they may show up as scripts with an empty name.

Adding records (in memory only)

let mut file = fmp12::FmpFile::open("database.fmp12")?;
let table = file.table_mut("Contacts").unwrap();

let new_index = table
    .new_record([("Name", "Ada Lovelace"), ("Email", "ada@example.com")])?
    .index();

This adds the record to the in-memory Table only. Nothing is written back to the file. An unknown column name returns FmpError::UnknownColumn.

Text

FileMaker stores strings obfuscated with a simple XOR (0x5A) and compressed with the Standard Compression Scheme for Unicode (SCSU). This crate undoes the XOR and decompresses the SCSU, so text in any script comes out as regular UTF-8 strings. Malformed sequences are replaced with U+FFFD rather than failing the read.

Errors

Every fallible call returns FmpError:

Variant Meaning
BadMagic The file does not start with the .fmp12 signature, so it is probably not an .fmp12 file.
BadHeader The header couldn't be parsed (e.g. an unreadable version date).
BadSectorHeader The first sector claims a previous sector, which should never happen.
SectorOutOfRange(id) The sector chain points past the end of the file.
UnknownChunkCode(code) A data chunk uses a code this crate can't decode.
MalformedChunk A chunk's declared length doesn't match the data after it.
UnexpectedEof The file ends before the format says it should.
UnknownColumn { table, column } new_record was given a column the table doesn't have.
Io(..) The file couldn't be read.

Roadmap

  • File header: creator app, version date, size
  • Tables and their names
  • Field definitions: type, data type, storage, auto-enter, repetitions, indexing
  • Record values, as text
  • Scripts: names, and each step's type and index
  • Adding records to an in-memory table
  • Forward compatibility: unrecognised values come back as Unknown(..) variants

Next up

  • Script step arguments (variable names, dialog text, script parameters, ...)
  • Calculations, which are stored as bytecode (needed for script arguments, calculation fields and auto-enter calculations)

Later

  • Relationships and table occurrences
  • Layouts
  • Value lists
  • Field validation rules
  • Container data (images, files): the field type is reported today, but not its contents
  • Script folders and separators, and empty scripts (not yet checked against a real file)
  • Writing or saving changes back to a file

Non-goals

  • The older .fp3/.fp5 formats, which are structured differently

How it works

You don't need this section to use the library, but it helps when something fails to parse.

An .fmp12 file is a chain of 4096-byte sectors:

  1. The header (sector 0) starts with a magic number plus the text HBAM7, and holds the creator app name and the version date. A leading sector 1 is unused.
  2. Body sectors form a linked list: each says which sector comes next, and they may be stored out of order. FmpFile::open follows the chain from the start.
  3. Each sector's payload is a stream of small encoded chunks: push or pop a path segment, store a value at the current path, store a key/value pair, or store one segment of a large value.
  4. Replaying every chunk builds a dictionary: a tree, like a tiny file system, where a path such as [3].[16].[5] holds the table definitions. Values are plain bytes until something interprets them.
  5. The table reader looks at well-known paths in that tree, decodes the text, reads each field's flag bytes into the FieldType/DataType/etc. enums, and gathers the records. The script reader does the same for [17].[5]: it splits each script's step code into 28-byte records and reads the step type from byte 21.

Only the final step's results are public API. The earlier layers are internal to the crate.

Project layout

src/
  lib.rs          crate docs and public exports
  file.rs         FmpFile: opening a file, header parsing, walking the sector chain
  sector.rs       reading one 4096-byte sector and its chunks
  chunk.rs        decoding the variable-length chunk encoding
  dictionary.rs   the path-addressed tree that chunks are replayed into
  table.rs        Table, Column, Record and the field-flag enums
  script.rs       Script, ScriptStep and the step type names
  bytes.rs        big-endian integers and the XOR text decoding
  scsu.rs         SCSU (Unicode compression) decoder
  error.rs        FmpError
tests/
  open_file.rs    integration tests against a real file
  fixtures/       Untitled.fmp12 (a small FileMaker 12 sample database)
                  and Scripts.fmp12 (one script with one Show Custom Dialog step)
docs/fmp12/
  README.md       index of our notes on the file format, and how far to trust them
  file.md         header and sectors
  chunks.md       chunk encoding, integers and text
  dictionary.md   the path tree and a map of known paths
  tables.md       tables, fields and records
  scripts.md      scripts and script steps
  _fmplib.md      community notes on the format (Rasmus20B), unmodified
  _fmptools.txt   community notes on the format (Evan Miller), unmodified

Development

cargo build
cargo test      # unit tests, integration tests against the fixture, and the doc example
cargo clippy

The integration tests open tests/fixtures/Untitled.fmp12, a one-table database with three records, and check the header, the table and column metadata, record reading, and in-memory record creation. Scripts are tested against tests/fixtures/Scripts.fmp12.

Because the format is reverse-engineered, chunk decoding in chunk.rs mirrors the community notes byte for byte. When a real-world file fails to parse, the docs/fmp12/ notes are the first place to look for what the format does there.

About

Thanks for your interest in fmplib! In case you have any questions, ideas, or remarks, please feel free to drop a line to romein@smartyellow.nl. Also do let me know what you're using fmplib for.

Released under the MIT License. Copyright © 2025-present Romein van Buren.