-
Notifications
You must be signed in to change notification settings - Fork 8
Mcstruct
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 FILE [DEF]from the shell (mcstructis a symlink tomc).DEFis a def-file name without.stl, or a path. - In the file panel: F3 on a file that a
magic.inigroup 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.
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 ...

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 |

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).
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.
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.
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 legendA field line:
[label:] TYPE follower [title] ; commentTypes: 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)
* 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('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 sizeA 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).
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#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
#endThe shipped def-files (zip exe dbf elf mbr fat_boot uimage dtb png bmp wav sqlite)
are the reference for the language.
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.
elf.stl: *.so *.o @0:7F454C46
png.stl: @0:89504E470D0A1A0A
mbr.stl: *.img @510:55AAName patterns match the file name, @offset:hexbytes the file contents.
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 themThree 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 labeldefault.ini and darkfar.ini carry them.