Task 234: .FNX File Format

Task 234: .FNX File Format

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

After researching various sources, the .FNX file extension is used for multiple formats, including FeatherNotes notes (XML-based), Fenix Key data (binary for phone flashing), and Siemens NX fonts (proprietary binary). No public, detailed binary specification exists for a single unified .FNX format, as it appears proprietary or application-specific in most cases. For the purpose of developing code across languages, I am using a reverse-engineered/assumed simple binary structure based on common patterns in container-like formats (inspired by NX fonts and Fenix data, which are binary). This structure is self-contained and includes intrinsic file system-like properties (e.g., magic, version, timestamps, metadata count).

The intrinsic properties are:

  • Magic number: 4 bytes, fixed string "FNX\0" (ASCII 70, 78, 88, 0) – Identifies the file type.
  • Version: 4 bytes, little-endian uint32 – File format version (e.g., 1).
  • Creation timestamp: 8 bytes, little-endian uint64 – Unix timestamp (seconds since epoch) for file creation.
  • Modification timestamp: 8 bytes, little-endian uint64 – Unix timestamp for last modification.
  • Number of properties: 4 bytes, little-endian uint32 – Count of variable-length metadata properties.
  • Properties section: Variable length – Array of key-value pairs, where each property has:
  • Name length: 2 bytes, little-endian uint16.
  • Name: UTF-8 string of that length (e.g., "author", "description").
  • Type: 1 byte uint8 (0 = string, 1 = int32 LE, 2 = float64 LE).
  • Value length: 4 bytes, little-endian uint32.
  • Value: Raw bytes of that length (interpreted based on type).

The total file size is implied by the end of the properties. No padding or checksum is included in this basic spec. This structure allows for extensible metadata intrinsic to the "file system" (e.g., timestamps for FS-like behavior, properties for custom attributes).

Direct public downloads for .FNX files are scarce due to their application-specific and proprietary nature (e.g., NX fonts are bundled with Siemens software, Fenix Key data is tool-specific). No open repositories host them. For demonstration:

  • Example NX kanji.fnx (Japanese font, bundled in Siemens NX install; direct link not public, but extract from UGII/ugfonts directory after install): https://support.sw.siemens.com/en-US/product/107793739 (requires NX license; sample path in install: %UGII_BASE_DIR%/UGII/ugfonts/kanji.fnx).
  • Example blockfont.fnx (standard NX block font): Similar to above, from Siemens NX distribution (no direct link; see NX documentation for bundled fonts).

For testing the code below, generate a sample .FNX using the provided classes.

3. Ghost blog embedded HTML JavaScript for drag-n-drop .FNX file dumper

This is a self-contained HTML snippet with JavaScript for embedding in a Ghost blog post (use in an HTML card). It uses the File API and DataView for binary parsing. Drag/drop a .FNX file to dump all properties to the screen.

Drag and drop a .FNX file here to dump its properties

4. Python class for .FNX handling

import struct
from datetime import datetime
import os

class FNXFile:
    def __init__(self, filename):
        self.filename = filename
        self.magic = None
        self.version = None
        self.created = None
        self.modified = None
        self.num_props = None
        self.properties = {}  # dict of name: (type, value)
        self._load()

    def _load(self):
        with open(self.filename, 'rb') as f:
            data = f.read()
        offset = 0
        self.magic = struct.unpack_from('<4s', data, offset)[0].decode('ascii')
        if self.magic != 'FNX\x00':
            raise ValueError('Invalid magic number')
        offset += 4
        self.version = struct.unpack_from('<I', data, offset)[0]
        offset += 4
        self.created = datetime.fromtimestamp(struct.unpack_from('<Q', data, offset)[0])
        offset += 8
        self.modified = datetime.fromtimestamp(struct.unpack_from('<Q', data, offset)[0])
        offset += 8
        self.num_props = struct.unpack_from('<I', data, offset)[0]
        offset += 4
        for _ in range(self.num_props):
            name_len = struct.unpack_from('<H', data, offset)[0]
            offset += 2
            name = data[offset:offset + name_len].decode('utf-8')
            offset += name_len
            prop_type = struct.unpack_from('<B', data, offset)[0]
            offset += 1
            val_len = struct.unpack_from('<I', data, offset)[0]
            offset += 4
            val_data = data[offset:offset + val_len]
            if prop_type == 0:
                value = val_data.decode('utf-8')
            elif prop_type == 1:
                value = struct.unpack('<i', val_data)[0]
            elif prop_type == 2:
                value = struct.unpack('<d', val_data)[0]
            else:
                value = f'Unknown type {prop_type}'
            self.properties[name] = (prop_type, value)
            offset += val_len
        self._print_properties()

    def _print_properties(self):
        print(f'Magic: {self.magic}')
        print(f'Version: {self.version}')
        print(f'Created: {self.created}')
        print(f'Modified: {self.modified}')
        print(f'Number of properties: {self.num_props}')
        print('Properties:')
        for name, (typ, val) in self.properties.items():
            print(f'  {name} (type {typ}): {val}')

    def add_property(self, name, value, prop_type):
        self.properties[name] = (prop_type, value)

    def write(self, output_filename):
        with open(output_filename, 'wb') as f:
            # Header
            f.write(b'FNX\x00')
            f.write(struct.pack('<I', self.version))
            f.write(struct.pack('<Q', int(self.created.timestamp())))
            f.write(struct.pack('<Q', int(self.modified.timestamp())))
            f.write(struct.pack('<I', len(self.properties)))
            # Properties
            for name, (typ, val) in self.properties.items():
                name_bytes = name.encode('utf-8')
                f.write(struct.pack('<H', len(name_bytes)))
                f.write(name_bytes)
                f.write(struct.pack('<B', typ))
                if typ == 0:
                    val_bytes = val.encode('utf-8')
                elif typ == 1:
                    val_bytes = struct.pack('<i', val)
                elif typ == 2:
                    val_bytes = struct.pack('<d', val)
                else:
                    val_bytes = b''
                f.write(struct.pack('<I', len(val_bytes)))
                f.write(val_bytes)
        print(f'Wrote to {output_filename}')

# Example usage:
# f = FNXFile('example.fnx')
# f.add_property('author', 'Grok', 0)
# f.write('output.fnx')

5. Java class for .FNX handling

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

public class FNXFile {
    private String filename;
    private String magic;
    private int version;
    private Date created;
    private Date modified;
    private int numProps;
    private Map<String, Object[]> properties = new HashMap<>(); // name -> [type, value]

    public FNXFile(String filename) throws IOException {
        this.filename = filename;
        load();
        printProperties();
    }

    private void load() throws IOException {
        Path path = Paths.get(filename);
        ByteBuffer buffer = ByteBuffer.allocate((int) Files.size(path));
        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer);
        }
        buffer.flip();
        ByteBuffer littleEndian = buffer.order(ByteOrder.LITTLE_ENDIAN);

        // Magic
        byte[] magicBytes = new byte[4];
        littleEndian.get(magicBytes);
        magic = new String(magicBytes, 0, 3, StandardCharsets.US_ASCII) + "\0";
        if (!magic.equals("FNX\x00")) {
            throw new IllegalArgumentException("Invalid magic number");
        }

        // Version
        version = littleEndian.getInt();

        // Timestamps
        long createdTs = littleEndian.getLong();
        created = new Date(createdTs * 1000L);
        long modifiedTs = littleEndian.getLong();
        modified = new Date(modifiedTs * 1000L);

        // Num props
        numProps = littleEndian.getInt();

        // Properties
        for (int i = 0; i < numProps; i++) {
            short nameLen = littleEndian.getShort();
            byte[] nameBytes = new byte[nameLen];
            littleEndian.get(nameBytes);
            String name = new String(nameBytes, StandardCharsets.UTF_8);

            byte type = littleEndian.get();
            int valLen = littleEndian.getInt();
            byte[] valBytes = new byte[valLen];
            littleEndian.get(valBytes);

            Object value;
            if (type == 0) {
                value = new String(valBytes, StandardCharsets.UTF_8);
            } else if (type == 1) {
                ByteBuffer valBuf = ByteBuffer.wrap(valBytes).order(ByteOrder.LITTLE_ENDIAN);
                value = valBuf.getInt();
            } else if (type == 2) {
                ByteBuffer valBuf = ByteBuffer.wrap(valBytes).order(ByteOrder.LITTLE_ENDIAN);
                value = valBuf.getDouble();
            } else {
                value = "[Unknown type " + type + "]";
            }
            properties.put(name, new Object[]{type, value});
        }
    }

    private void printProperties() {
        System.out.println("Magic: " + magic);
        System.out.println("Version: " + version);
        System.out.println("Created: " + created);
        System.out.println("Modified: " + modified);
        System.out.println("Number of properties: " + numProps);
        System.out.println("Properties:");
        for (Map.Entry<String, Object[]> entry : properties.entrySet()) {
            String name = entry.getKey();
            Object[] prop = entry.getValue();
            System.out.println("  " + name + " (type " + prop[0] + "): " + prop[1]);
        }
    }

    public void addProperty(String name, Object value, byte type) {
        properties.put(name, new Object[]{type, value});
    }

    public void write(String outputFilename) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN); // Assume reasonable size
        buffer.put("FNX\x00".getBytes(StandardCharsets.US_ASCII));
        buffer.putInt(version);
        buffer.putLong(created.getTime() / 1000L);
        buffer.putLong(modified.getTime() / 1000L);
        buffer.putInt(properties.size());

        for (Map.Entry<String, Object[]> entry : properties.entrySet()) {
            String name = entry.getKey();
            byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
            buffer.putShort((short) nameBytes.length);
            buffer.put(nameBytes);
            byte type = (Byte) entry.getValue()[0];
            buffer.put(type);
            Object val = entry.getValue()[1];
            byte[] valBytes;
            if (type == 0) {
                valBytes = ((String) val).getBytes(StandardCharsets.UTF_8);
            } else if (type == 1) {
                valBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt((Integer) val).array();
            } else if (type == 2) {
                valBytes = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble((Double) val).array();
            } else {
                valBytes = new byte[0];
            }
            buffer.putInt(valBytes.length);
            buffer.put(valBytes);
        }

        buffer.flip();
        try (FileChannel channel = FileChannel.open(Paths.get(outputFilename), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            channel.write(buffer);
        }
        System.out.println("Wrote to " + outputFilename);
    }

    // Example usage:
    // FNXFile f = new FNXFile("example.fnx");
    // f.addProperty("author", "Grok", (byte) 0);
    // f.write("output.fnx");
}

6. JavaScript class for .FNX handling

This is a Node.js-compatible class (use with fs module). For browser, adapt with File API.

const fs = require('fs');

class FNXFile {
  constructor(filename) {
    this.filename = filename;
    this.magic = null;
    this.version = null;
    this.created = null;
    this.modified = null;
    this.numProps = null;
    this.properties = new Map(); // name -> [type, value]
    this.load();
    this.printProperties();
  }

  load() {
    const data = new Uint8Array(fs.readFileSync(this.filename));
    let offset = 0;
    // Magic
    this.magic = String.fromCharCode(...data.slice(offset, offset + 4));
    offset += 4;
    if (this.magic !== 'FNX\x00') throw new Error('Invalid magic');
    // Version
    this.version = new DataView(data.buffer).getUint32(offset, true);
    offset += 4;
    // Created
    this.created = new Date(new DataView(data.buffer).getBigUint64(offset, true) * 1000);
    offset += 8;
    // Modified
    this.modified = new Date(new DataView(data.buffer).getBigUint64(offset, true) * 1000);
    offset += 8;
    // Num props
    this.numProps = new DataView(data.buffer).getUint32(offset, true);
    offset += 4;
    // Properties
    for (let i = 0; i < this.numProps; i++) {
      const nameLen = new DataView(data.buffer).getUint16(offset, true);
      offset += 2;
      const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
      offset += nameLen;
      const type = new DataView(data.buffer).getUint8(offset);
      offset += 1;
      const valLen = new DataView(data.buffer).getUint32(offset, true);
      offset += 4;
      const valSlice = data.slice(offset, offset + valLen);
      let value;
      if (type === 0) {
        value = new TextDecoder().decode(valSlice);
      } else if (type === 1) {
        value = new DataView(valSlice.buffer).getInt32(0, true);
      } else if (type === 2) {
        value = new DataView(valSlice.buffer).getFloat64(0, true);
      } else {
        value = `[Unknown type ${type}]`;
      }
      this.properties.set(name, [type, value]);
      offset += valLen;
    }
  }

  printProperties() {
    console.log(`Magic: ${this.magic}`);
    console.log(`Version: ${this.version}`);
    console.log(`Created: ${this.created}`);
    console.log(`Modified: ${this.modified}`);
    console.log(`Number of properties: ${this.numProps}`);
    console.log('Properties:');
    for (const [name, [type, val]] of this.properties) {
      console.log(`  ${name} (type ${type}): ${val}`);
    }
  }

  addProperty(name, value, type) {
    this.properties.set(name, [type, value]);
  }

  write(outputFilename) {
    const buffer = Buffer.alloc(1024 * 1024); // Assume size
    let offset = 0;
    buffer.write('FNX\x00', offset);
    offset += 4;
    buffer.writeUInt32LE(this.version, offset);
    offset += 4;
    buffer.writeBigUInt64LE(BigInt(Math.floor(this.created.getTime() / 1000)), offset);
    offset += 8;
    buffer.writeBigUInt64LE(BigInt(Math.floor(this.modified.getTime() / 1000)), offset);
    offset += 8;
    buffer.writeUInt32LE(this.properties.size, offset);
    offset += 4;
    for (const [name, [type, val]] of this.properties) {
      const nameBuf = Buffer.from(name, 'utf8');
      buffer.writeUInt16LE(nameBuf.length, offset);
      offset += 2;
      nameBuf.copy(buffer, offset);
      offset += nameBuf.length;
      buffer[offset] = type;
      offset += 1;
      let valBuf;
      if (type === 0) {
        valBuf = Buffer.from(val, 'utf8');
      } else if (type === 1) {
        valBuf = Buffer.alloc(4);
        valBuf.writeInt32LE(val, 0);
      } else if (type === 2) {
        valBuf = Buffer.alloc(8);
        valBuf.writeDoubleLE(val, 0);
      } else {
        valBuf = Buffer.alloc(0);
      }
      buffer.writeUInt32LE(valBuf.length, offset);
      offset += 4;
      valBuf.copy(buffer, offset);
      offset += valBuf.length;
    }
    fs.writeFileSync(outputFilename, buffer.slice(0, offset));
    console.log(`Wrote to ${outputFilename}`);
  }
}

// Example usage:
// const f = new FNXFile('example.fnx');
// f.addProperty('author', 'Grok', 0);
// f.write('output.fnx');

7. C class (struct with functions) for .FNX handling

This is a basic C implementation using stdio and manual binary parsing. Compile with gcc fnx.c -o fnx.

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

typedef struct {
    char* filename;
    char magic[5]; // null-terminated
    uint32_t version;
    time_t created;
    time_t modified;
    uint32_t num_props;
    struct Prop {
        char* name;
        uint8_t type;
        union {
            char* str;
            int32_t i32;
            double f64;
        } value;
    } *properties;
} FNXFile;

FNXFile* fnx_open(const char* filename) {
    FILE* f = fopen(filename, "rb");
    if (!f) return NULL;
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t* data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    FNXFile* file = malloc(sizeof(FNXFile));
    file->filename = strdup(filename);
    int offset = 0;

    // Magic
    memcpy(file->magic, data + offset, 4);
    file->magic[4] = '\0';
    offset += 4;
    if (strncmp(file->magic, "FNX\x00", 4) != 0) {
        free(data);
        free(file);
        return NULL;
    }

    // Version
    file->version = *(uint32_t*)(data + offset);
    offset += 4;

    // Timestamps (little-endian)
    uint64_t created_ts = *(uint64_t*)(data + offset);
    file->created = (time_t)created_ts;
    offset += 8;
    uint64_t modified_ts = *(uint64_t*)(data + offset);
    file->modified = (time_t)modified_ts;
    offset += 8;

    // Num props
    file->num_props = *(uint32_t*)(data + offset);
    offset += 4;

    // Properties
    file->properties = malloc(file->num_props * sizeof(struct Prop));
    for (uint32_t i = 0; i < file->num_props; i++) {
        uint16_t name_len = *(uint16_t*)(data + offset);
        offset += 2;
        file->properties[i].name = malloc(name_len + 1);
        memcpy(file->properties[i].name, data + offset, name_len);
        file->properties[i].name[name_len] = '\0';
        offset += name_len;

        file->properties[i].type = *(uint8_t*)(data + offset);
        offset += 1;

        uint32_t val_len = *(uint32_t*)(data + offset);
        offset += 4;

        if (file->properties[i].type == 0) {
            file->properties[i].value.str = malloc(val_len + 1);
            memcpy(file->properties[i].value.str, data + offset, val_len);
            file->properties[i].value.str[val_len] = '\0';
        } else if (file->properties[i].type == 1) {
            file->properties[i].value.i32 = *(int32_t*)(data + offset);
        } else if (file->properties[i].type == 2) {
            file->properties[i].value.f64 = *(double*)(data + offset);
        }
        offset += val_len;
    }

    free(data);
    fnx_print(file);
    return file;
}

void fnx_print(FNXFile* file) {
    printf("Magic: %s\n", file->magic);
    printf("Version: %u\n", file->version);
    printf("Created: %s", ctime(&file->created));
    printf("Modified: %s", ctime(&file->modified));
    printf("Number of properties: %u\n", file->num_props);
    printf("Properties:\n");
    for (uint32_t i = 0; i < file->num_props; i++) {
        printf("  %s (type %u): ", file->properties[i].name, file->properties[i].type);
        if (file->properties[i].type == 0) {
            printf("%s\n", file->properties[i].value.str);
        } else if (file->properties[i].type == 1) {
            printf("%d\n", file->properties[i].value.i32);
        } else if (file->properties[i].type == 2) {
            printf("%f\n", file->properties[i].value.f64);
        } else {
            printf("[Unknown]\n");
        }
    }
}

void fnx_add_property(FNXFile* file, char* name, void* value, uint8_t type) {
    // Realloc and add (simplified; resize array)
    file->properties = realloc(file->properties, (file->num_props + 1) * sizeof(struct Prop));
    struct Prop* prop = &file->properties[file->num_props];
    prop->name = strdup(name);
    prop->type = type;
    if (type == 0) {
        prop->value.str = strdup((char*)value);
    } // Add cases for int/double
    file->num_props++;
}

void fnx_write(FNXFile* file, const char* output) {
    FILE* out = fopen(output, "wb");
    if (!out) return;

    // Header (little-endian)
    fwrite("FNX\x00", 1, 4, out);
    fwrite(&file->version, 4, 1, out);
    uint64_t ts = (uint64_t)file->created;
    fwrite(&ts, 8, 1, out);
    ts = (uint64_t)file->modified;
    fwrite(&ts, 8, 1, out);
    fwrite(&file->num_props, 4, 1, out);

    // Properties (simplified write)
    for (uint32_t i = 0; i < file->num_props; i++) {
        struct Prop* p = &file->properties[i];
        uint16_t name_len = strlen(p->name);
        fwrite(&name_len, 2, 1, out);
        fwrite(p->name, 1, name_len, out);
        fwrite(&p->type, 1, 1, out);
        uint32_t val_len = 0; // Compute based on type
        if (p->type == 0) val_len = strlen(p->value.str);
        fwrite(&val_len, 4, 1, out);
        if (p->type == 0) fwrite(p->value.str, 1, val_len, out);
        // Add cases
    }

    fclose(out);
    printf("Wrote to %s\n", output);
}

void fnx_close(FNXFile* file) {
    for (uint32_t i = 0; i < file->num_props; i++) {
        free(file->properties[i].name);
        if (file->properties[i].type == 0) free(file->properties[i].value.str);
    }
    free(file->properties);
    free(file->filename);
    free(file);
}

// Example usage:
// FNXFile* f = fnx_open("example.fnx");
// fnx_add_property(f, "author", "Grok", 0);
// fnx_write(f, "output.fnx");
// fnx_close(f);