Skip to content
Ilia Maslakov edited this page Sep 2, 2026 · 16 revisions

mcstruct: structured binary viewer

mcstruct shows a binary file as a tree of named fields instead of a hex dump. The layout of the file comes from a text "def-file" written in STL5. Fields can be edited in place and written back. The tool is a panel plugin with its own screen, in the spirit of the DOS program Struct Look; its def-file format is compatible with Struct Look 4.00.

mcstruct on a DBF file: header fields, hex dump and def-file in sync, then the records as a grid

Starting it

  • mcstruct FILE [DEF] from the shell (mcstruct is a symlink to mc). DEF is a def-file name without .stl, or a path.
  • In the file panel: F3 on a file that a magic.ini group hands to the plugin (uImage, DTB, MBR out of the box), or F9 -> Command -> Struct look for any file.
  • In the viewer: Shift-F4 opens the file in mcstruct at the current offset; Shift-F4 in mcstruct goes back to the viewer at the byte you were on, F10 closes both.

Which def-file is used, in this order: the name given on the command line or in the magic.ini group ([mcstruct-mbr] means mbr.stl), a signature or a name pattern from stl.als, <extension>.stl, else a list to choose from.

Def-files are searched in ~/.config/mc6/mcstruct/, /etc/mc/mcstruct/ and the plugin's own data directory, the first hit wins. Put your own files and your stl.als into the first one.

The screen

 mcstruct  sample.dbf  dbf.stl  DBF
 +-- DBF: 252 bytes at 00000000  3/25  records ----+ +-- dbf.stl  line 9/134 ---------+
 | 00000000  version        b     03 (dBASE III)   | |  7 ver:   b   'dbf_version     |
 | 00000001  last update    u8[3] 126 8 28         | |  8        u8  3                |
 | 00000004  records        u32   3                | |  9 nrec:  u32 1  records       |
 +-------------------------------------------------+ +--------------------------------+
 offset 00000004 (4)  local +4  field 4 bytes  file 252 bytes
 00000000 03 7E 08 1C  03 00 00 00  A1 00 1E 00 ...

The tree of header fields with the hex dump and the def-file zone

Three zones, all synced: the structure tree, the hex dump and the def-file. Moving in the tree highlights the field's bytes in the dump and the def-file line that produced it; moving the hex cursor selects the field under it.

On terminals narrower than 120 columns the def-file goes below the hex dump.

key action
Up Down PgUp PgDn Home End move
Right, Enter expand; on an array, table or #repeat open it as a grid; on a jump follow it; on a buffer (via) open the structure over the bytes; on a string show the whole text
Left collapse or go to the parent
Backspace back from a jump or a buffer
1..9, * expand to depth, expand all
Tab next zone (tree, hex, def-file)
F2 write pending edits
Shift-F2 save the bytes of the current row (field, structure, table, array element) as a numbered fragment, or to a file
Ctrl-F2 a saved fragment, or a file, into the current row; the size does not change, F2 writes the result
F3 choose a structure and an offset to apply it at
F4 edit the current field
F5 go to an offset (an expression, labels of the current structure allowed)
F6, Shift-F6 choose a def-file; open the current one in mcedit at the current line
F7, Shift-F7 search a field name or value; next. In the hex zone: bytes (hex) or text from the cursor, the hit becomes the block
F8 hide or show the hex zone
F9 parse errors of the def-file, Enter opens mcedit at the line
Alt-= calculator with the labels of the current structure
Alt-F9 zoom the focused zone
Ctrl-PgDn, Ctrl-PgUp the same structure right after / before this one
F10, Esc quit; when opened from the viewer, the viewer closes too (asks when edits are pending)
Shift-F4 back to the viewer at the current byte

The records of a DBF as a grid, columns named from the field descriptors

In the grid: Left/Right and Tab move between cells, < and > scroll the columns, F4 edits the cell, Enter follows a jump cell, Esc returns to the tree. The first column is the row number or the file offset of the row (GridRowColumn).

In the hex zone: Tab switches between the hex and the text column, typing edits the byte under the cursor. [ and ] set the start and the end of a block at the cursor, * clears it; Shift-F2 and Ctrl-F2 then write or read the block instead of the current row. F7 searches bytes (hex) or text from the cursor, the hit becomes the block.

In the def-file zone: Enter goes to the structure the line refers to (* N Name, jNN Name=expr, :Table).

Saved fragments

Shift-F2 keeps the bytes of the current row under a number that is never reused, with a name (the structure by default), the file they came from and the time. The set lives in ~/.cache/mc6/mcstruct/bytes/ and is shared by every mcstruct, so bytes taken from one file go into another opened later: Ctrl-F2 lists them newest first (#7 512 fat_boot FatBoot disk.img 2026-08-30 02:10, ! when the size differs from the target row), Enter puts the fragment over the row, Delete drops it, the last item reads a file. Fragments older than FragmentDays (7 by default, 0 keeps them) are dropped.

Editing

F4 shows the value in the form of its type: hex for b w d q, decimal for u* i*, text for strings, a bit string for t*, SEG:OFF for pointers, YYYY-MM-DD HH:MM:SS for dates. The bytes are replaced in place, the structure is re-evaluated, F2 writes the file. Only fixed-width edits: the file never changes size. The first edit takes the file lock as mcedit does.

STL5, the def-file language

A def-file starts with a version line and holds structures, tables and legends. Files tagged STL 4.00 are read in strict compatibility mode; STL 5.00 enables the extensions marked below.

STL 5.00
; a comment
/Name                ; a structure
/_Hidden             ; not offered in the F3 list, but usable
/:Table              ; a table: one row per record
/'legend             ; value legend (enum)
/`flags              ; bit legend

A field line:

[label:]  TYPE  follower  [title]   ; comment

Types: c chars, b w d q hex 1/2/4/8 bytes, i8..i64 and u8..u64 decimal, t8..t64 bit fields, m8..m64 big-endian hex, f32 f64 f80 float, sp Pascal string, sc C string (sc.0A ends at byte 0x0A instead of NUL, 5.00), sz16 UTF-16 up to a NUL word (5.00), td tu DOS and unix time, tu64 tf 64-bit unix time and Windows FILETIME (5.00), j8..j64 jumps, s8 s16 fixed-size UTF-8 and UTF-16 strings, v vl SQLite varint and LEB128 (5.00). Any numeric type takes a .le or .be suffix (5.00).

The follower is a count (for c a length, for sc a maximum), a label, an expression, a legend reference ('name or `name), or for t* a bit split like 745 (7+4+5 bits); t8.lsb 44 counts the split from bit 0 (5.00). 0 reads the field but hides it.

Other lines:

:text                     a remark row
+ N   - N                 skip forward or back
. N                       seek to a local offset (from the structure start)
.. N                      seek to an absolute file offset (5.00)
* N Struct                N nested structures, or  * N :Table
* N Struct "caption"      the same, the row titled by the caption (5.00)
* N Struct step expr      records expr bytes apart, the entry size a field
                          of the header; the grid works as usual (5.00)
* 1 Struct via call(...)  the structure read from the bytes a provider
                          gives, see below (5.00)
j32 Struct=expr title     a jump: the value, Enter opens Struct at expr
= expr title              a computed value as a row, no bytes; =x hex,
                          =t unix time; a label on it gets the value (5.00)
check expr title          a row marked OK or MISMATCH by the expression (5.00)

Directives:

#if expr / #elseif expr / #else / #fi
#endian big|little        byte order from here on (5.00)
#repeat N ... #end        N passes (5.00)
#repeat while expr ... #end
#set label = expr         a label without bytes (5.00)
#switch expr / #case v1, v2 / #default / #end
                          an #if chain on one value (5.00)
#encoding cp866           text fields that follow are converted from that
                          charset; #encoding utf-8 turns it off (5.00)
#include "file"           (5.00)
crc32 a..b == label       a check field, also sum8 and sum16 (5.00)

Expressions: integers, + - * / % & | ^ ~ << >> ! && ||, comparisons, labels, @ the value at the current offset, @u32 read as that type, @u32(addr) read at an absolute offset (5.00), ^label the file offset of a label, ^ the structure start, $size the file size, $pos the current offset. "PK" is a packed literal that compares equal to a little-endian read of the bytes PK; 'PK' (5.00) is the value of the bytes as they lie in the file. An expression with spaces is written in quotes; "$size" in quotes is still an expression, only text that looks like a literal ("PK") is one. A label of a text field compared with a literal compares the text: kt == 'ssh-rsa' (5.00).

call() and buffers (5.00)

call('name', args) in an expression asks a provider for a value:

crc32 / crc16 / adler32 / sum8 / sum16 / sum32 (from, len)   checksums of a range
crc32z (from, len, hole_from, hole_len)   crc32 with a field inside taken as zero
find (from, len, value, width)     offset of the little-endian value, -1 when absent
exec:command (from, len)           the range on stdin, a number on stdout

* 1 Struct via call('name', args) reads a structure from the bytes a provider gives instead of the file: raw (from, len) a bounded slice, inflate / gunzip / deflate (from, len) zlib, gzip or raw deflate data, xor (from, len, key), rol (from, len, bits), exec:command (from, len) the range on stdin and the bytes on stdout. The row shows the size; Enter opens the structure over the bytes (the hex zone shows the buffer, $size inside is its length), Backspace comes back. A buffer is read-only.

exec: runs a shell command and is allowed only for def-files in ~/.config/mc6/mcstruct/; shipped and system def-files get the built-in providers only. inflate needs zlib at build time.

STL 5.00
/GZTEXT
        w       1               signature
        *       1       _Text via call('gunzip', 0, $size)
/_Text
        c       64              first line
        =       $size           unpacked size

A title written as =expr is read from the file as a C string (5.00), so a record layout that comes from the file can name its fields. Nested structures, jump targets and buffers can read the labels of the structure they are in or were opened from (5.00).

Example: a linked list

STL 5.00
/Walker
#set ptr = 0x10
#repeat while ptr != 0
        ..      ptr
        *       1       Node
#set    ptr = @u32(ptr)
#end
/Node
next:   u32     1       next
        u8      1       value

Example: chunks with a checksum (PNG)

#endian big
        c       8               signature
#repeat while ^@ + 12 <= $size
len:    u32     1               length
type:   c       4               type
        +       len
crc:    d       1               crc
        crc32   ^type..^type+4+len == crc     crc check
#end

The shipped def-files (zip exe dbf elf mbr fat_boot uimage dtb png bmp wav sqlite) are the reference for the language.

Shipped def-files

About 140 formats come with the plugin, most of them written after the Kaitai Struct formats gallery: archives (zip, rar, lzh, cpio, gzip, xar, mar, cab-like game packs), executables (exe/pe, elf, mach-o, dex, java class, uefi te, swf), file systems (mbr, gpt, apm, fat boot, ext2, iso9660, cramfs, luks, lvm2, vdi, vmdk, android super/sparse/boot), images (png, bmp, gif, jpeg, tga, pcx, ico, tiff/exif, icc, dicom, wmf, xwd), media (wav, avi, midi, ogg, mov/mp4, s3m, xm, id3v1/v2, voc, au), networks (pcap, ethernet with ipv4/ipv6/tcp/udp, dns, rtp/rtcp, tls hello, some/ip), and more: sqlite, dbf, regf, lnk, evt, minidump, cfb, ttf, pcf, pff2, btrfs stream, systemd journal, utmp. stl.als maps them to signatures and name patterns; the full list with sources and notes is on Mcstruct-formats.

Aliases and signatures: stl.als

elf.stl: *.so *.o @0:7F454C46
png.stl: @0:89504E470D0A1A0A
mbr.stl: *.img @510:55AA

Name patterns match the file name, @offset:hexbytes the file contents.

Settings

Manage plugins -> mcstruct, or ~/.config/mc6/mcstruct/mcstruct.ini:

[mcstruct]
TreeLines=14            ; rows of the tree, 0 = the rest
HexLines=4              ; rows of the hex zone, 0 hides it
DefLines=0              ; rows of the def-file zone, 0 = the rest
DefLayout=auto          ; auto (right of the tree when wide), right, bottom
DefWidth=45             ; percent, when on the right
NameWidth=16            ; the field name column
OffsetColumn=global     ; none, local, global
GridRowColumn=offset    ; first grid column: none, number, offset
ShowHiddenStructures=false
LazyRows=64             ; arrays longer than this are built on expand
FloatingPointFormat=%g
FragmentDays=7          ; saved byte fragments older than this are dropped, 0 keeps them

Skin

Three sections, one per zone; a missing section falls back to the core colors.

[mcstruct-tree]  _default_ frame frame-active head selected
                 offset name type value struct jump remark error
[mcstruct-hex]   _default_ head offset mark changed cursor frame
[mcstruct-def]   _default_ frame frame-active head selected
                 lineno directive comment label

default.ini and darkfar.ini carry them.

Clone this wiki locally