diff options
-rw-r--r-- | doc/fspec-guide.adoc | 192 | ||||
-rw-r--r-- | spec/ability.fspec | 18 | ||||
-rw-r--r-- | spec/eaf.fspec | 24 | ||||
-rw-r--r-- | spec/elf.fspec | 57 | ||||
-rw-r--r-- | spec/emz.fspec | 10 | ||||
-rw-r--r-- | spec/ftable.fspec | 2 | ||||
-rw-r--r-- | spec/item.fspec | 16 | ||||
-rw-r--r-- | spec/model.fspec | 2 | ||||
-rw-r--r-- | spec/name.fspec | 6 | ||||
-rw-r--r-- | spec/spell.fspec | 34 | ||||
-rw-r--r-- | spec/vtable.fspec | 2 | ||||
-rw-r--r-- | vim/filespec.vim | 9 |
12 files changed, 220 insertions, 152 deletions
diff --git a/doc/fspec-guide.adoc b/doc/fspec-guide.adoc index 576c734..8a61609 100644 --- a/doc/fspec-guide.adoc +++ b/doc/fspec-guide.adoc @@ -29,33 +29,38 @@ Filespec Version 0.1 === Abstract -Writeup about how writing and reading structured data is mostly done manually. +Often while developing a software, it has to understand and access some sort of +binary data. Often there might be a library or something that helps you with +this task. However, many times these solutions are lacking or doesn't work +properly on different platforms. Sometimes there might not be solution at all +for your particular environment and you have to do it yourself again. Or even +if you are creating a new format, there's lack of tools to prototype it, and +every change needs refactoring of the packing/unpacking code. === Motivation -Writeup how boring it is to write similar code each time when trying to read or -write structured data. How easy it is to make mistakes or cause unportable and -unoptimized code. Write how filespec can help with reverse engineering and -figuring out data structures, how it can be used to generate both packers and -unpackers giving you powerful tools for working with structured data. +Filespec lets you describe the format itself, so that you can generate +portable and effecient code for reading and writing the specified data in +a simple way. It also provides bunch of utilities that helps you develop +formats, or even used as a tool for reading and writing binary files. === Overview Goal of Filespec is to document the structured data and relationships within, -so the data can be understood and accessed completely. +so the data can be understood completely. === Related Work ==== Kaitai Kaitai is probably not very well known utility, that has similar goal to -filespec. +Filespec. Explain cons: - Depends on runtime - Can only model data which runtime supports (only certain - compression/decompression available for example, while in filespec + compression/decompression available for example, while in Filespec filters can express anything) - Mainly designed for generated code, not general utility - Uses YAML for modelling structured data which is quite wordy and akward @@ -81,60 +86,115 @@ Brief of Filespec specifications and syntax include::../spec/elf.fspec[] ---- -=== Keywords +=== Top-level keywords -|============================================================================= +Top-level keywords, they can't be used inside struct declarations. + +[options="header"] +|======================================================== +| Keyword | Description +| enum { ... } | Declares enumeration | struct _name_ { ... } | Declares structured data -| enum _name_ { ... } | Declares enumeration -| union _name_ (_var_) { ... } | Declares union, can be used to model variants -|============================================================================= +|======================================================== + +=== Enums -.Struct member declaration syntax -Parenthesis indicate optional fields ---- -member_name: member_type (array ...) (| filter ...) (visual hint); +enum { + first, + second, + seventh = 7, + eight +}; ---- -=== Types +=== Structs + +---- +struct blob { + type name (array ...) (| filter ...) (visual hint); +}; +---- + +==== Types Basic types to express binary data. -|================================================================ -| struct _name_ | Named structured data (Struct member only) -| enum _name_ | Value range is limited to the named enumeration -| u8, s8 | Unsigned, signed 8bit integer -| u16, s16 | Unsigned, signed 16bit integer -| u32, s32 | Unsigned, signed 32bit integer -| u64, s64 | Unsigned, signed 64bit integer -|================================================================ +[options="header"] +|================================================================================================== +| Type | Description +| if (_expr_) { ... } else { ... } | Conditional +| select (_expr_) { ... } _name_ | Tagged union +| struct _name_ | Substructure +| u??, s?? | Unsigned, signed ??bit integer (e.g. u8 for 8bit unsigned integer) +|================================================================================================== + +==== If -=== Arrays +Conditional reading/writing of fields depending on the result of _expr_. -Valid values that can be used inside array subscript operation. +---- +u8 version; +if (version >= 2) { + u8 ver2_field; +} +if (version >= 3) { + u8 ver3_field; +} else { + u8 removed_old_field; +} +---- + +==== Select + +Conditionally pack/unpack field depending on the result of _expr_. +This is identical to tagged union, variant, etc... and generates into union in C. -|================================================= -| _expr_ | Uses result of expression as array size -| \'str' | Grow array until occurance of str -| $ | Grow array until end of data is reached -|================================================= +There may not be duplicate cases inside single select. + +---- +u8 type; +select (type) { + 0) struct string string (array ...) (| filter ...) (visual hint); + 1) u1 bool; + *) u32 any; +} value; +---- + +==== Arrays + +Valid expressions that can be used to define array size during declaration. + +[options="header"] +|======================================================================== +| Expression | Description +| _expr_ | Result of expression +| \'str' | Grow array until occurance of str in binary data +| until (_expr_) | Grow array until condition has been reached +|======================================================================== .Reading length prefixed data ---- -num_items: u16 dec; -items: struct item[num_items]; +u16 num_items dec; +struct item items[num_items]; ---- .Reading null terminated string ---- -cstr: u8['\0'] str; +u8 cstr['\0'] str; ---- -.Reading repeating pattern +.Reading repeating pattern until we hit stop condition ---- -pattern: struct pattern[$]; +struct pattern pattern[until (pattern.last_block)]; ---- -=== Filters +.Reading repeating pattern until the data ends +---- +struct pattern pattern[until (false)]; +---- + +==== Filters Filters can be used to sanity check and transform data into more sensible format while still maintaining compatible data layout for both packing and @@ -153,35 +213,40 @@ consider contributing your filter to the interpeter. Filters for official interepter are implemented as command pairs (Thus filters are merely optional dependency in interpeter) -|======================================================================== +[options="header"] +|============================================================================ +| Filter | Description | matches(_str_) | Data matches _str_ +| range(_min_, _max_) | Data is within the range of _min_ and _max_ | encoding(_str_, ...) | Data is encoded with algorithm _str_ | compression(_str_, ...) | Data is compressed with algorithm _str_ | encryption(_str_, _key_, ...) | Data is encrypted with algorithm _str_ -|======================================================================== +|============================================================================ .Validating file headers ---- -header: u8[4] | matches('\x7fELF') str; +u8 header[4] | matches('\x7fELF') str; ---- .Decoding strings ---- -name: u8[32] | encoding('sjis') str; +u8 name[32] | encoding('sjis') str; ---- .Decompressing data ---- -data_sz: u32; -data: u8[$] | compression('deflate', data_sz) hex; +u32 data_sz; +u8 data[until (false)] | compression('deflate', data_sz) hex; ---- -=== Visual hints +==== Visual hints Visual hints can be used to advice tools how data should be presented to human, as well as provide small documentation what kind of data to expect. +[options="header"] |=========================================== +| Hint | Description | nul | Do not visualize data | dec | Visualize data as decimal | hex | Visualize data as hexdecimal @@ -210,26 +275,18 @@ value of _len_ from the length of _str_ if it has been filled. We can also use this information to verify that length of _str_ matches the value of _len_, if both have been filled. ---- -len: u16; -str: u8[len] str; +u16 len; +u8 str[len] str; ---- .Parameter relationship In packing case, the same rules apply as in array relationship. Implicit relationship is formed between _decompressed_sz_ member and compression filter. ---- -decompressed_sz: u32 dec; -data: u8[$] | compression('zlib', decompressed_sz); +u32 decompressed_sz dec; +u8 data[until (false)] | compression('zlib', decompressed_sz); ---- -=== Explicit Relationships - -Sometimes we need to form explicit relationships when the structure is more -complicated. - -TODO: When we can actually model FFXI string tables correctly, it will be a -good example. - == Implementation === Compiler @@ -240,8 +297,8 @@ as optimizations would be done on the bytecode level instead the source level. === Validator -Validator takes the output of compiler and checks the bytecode follows a -standard pattern, and isn't invalid. Having validator pass simplifies the +Validator takes the output of compiler and checks the bytecode for validity +and that it follows a standard pattern. Having validator pass simplifies the code of translators, as they can assume their input is valid and don't need to do constant error checking. It also helps catch bugs from compiler early on. @@ -256,7 +313,20 @@ To make sure all source level attributes such as mathematical expressions can be translated losslessly to target language, the bytecode may contain special attributes. -TODO: Document bytecode operations and the predictable pattern here +[options="header"] +|===================================== +| Opcode | Decimal | Description +| OP_ADD | 0 | a + b +| OP_SUB | 1 | a - b +| OP_MUL | 2 | a * b +| OP_DIV | 3 | a / b +| OP_MOD | 4 | a % b +| OP_BIT_AND | 5 | a & b +| OP_BIT_OR | 6 | a \| b +| OP_BIT_XOR | 7 | a ^ b +| OP_BIT_LEFT | 8 | a << b +| OP_BIT_RIGHT | 9 | a >> b +|===================================== === Translators diff --git a/spec/ability.fspec b/spec/ability.fspec index 4498d69..67a9965 100644 --- a/spec/ability.fspec +++ b/spec/ability.fspec @@ -1,14 +1,14 @@ struct ability { - index: u16; - icon_id: u16; - mp_cost: u16; - unknown: u16; - targets: u16; - name: u8[32] | encoding('sjis') str; // The encoding actually depends on ROM region - description: u8[256] | encoding('sjis') str; // ^ Ditto, we can't express this (we need parser options) - padding: u8[726] nul; + u16 index; + u16 icon_id; + u16 mp_cost; + u16 unknown; + u16 targets; + u8 name[32] | necoding('sjis') str; // The encoding actually depends on ROM region + u8 description[256] | encoding('sjis') str; // ^ Ditto, we can't express this (we need parser options) + u8 padding[726] nul; }; struct dat { - ability: struct ability[$]; + struct ability ability[$]; }; diff --git a/spec/eaf.fspec b/spec/eaf.fspec index e9c5702..139539d 100644 --- a/spec/eaf.fspec +++ b/spec/eaf.fspec @@ -1,17 +1,17 @@ struct file { - path: u8[256] | encoding('ascii') str; - offset: u64; - size: u64; - padding: u8[16] nul; + u8 path[256] | encoding('ascii') str; + u64 offset; + u64 size; + u8 padding[16] nul; }; struct eaf { - header: u8[4] | matches('#EAF') str; - major: u16; - minor: u16; - size: u64; - count: u32; - unknown: u64; - padding: u8[100] nul; - files: struct file[count]; + u8 header[4] | matches('#EAF') str; + u16 major; + u16 minor; + u64 size; + u32 count; + u64 unknown; + u8 padding[100] nul; + struct file files[count]; }; diff --git a/spec/elf.fspec b/spec/elf.fspec index e6059c7..5bd9954 100644 --- a/spec/elf.fspec +++ b/spec/elf.fspec @@ -1,36 +1,35 @@ -1 + 5 + 2 * 5 / 2; - -enum foo { - foo: 0x1; - bar: 0x2; - eaf: 0x3; - eaf: 0xDEADBEEF; - bar; +struct elf32 { + u32 e_entry hex; + u32 e_phoff; + u32 e_shoff; }; struct elf64 { - e_entry: u64 hex; - e_phoff: u64; - e_shoff: u64; + u64 e_entry hex; + u64 e_phoff; + u64 e_shoff; }; struct elf { - ei_magic: u8[4] | matches('\x7fELF') str; - ei_class: u8 hex; // word size - ei_data: u8 hex; // endianess - ei_version: u8; - ei_osabi: u8; - ei_abi_version: u8; - padding: u8[7] nul; - e_type: u16 hex; - e_machine: u16 hex; - e_version: u32; - elf64: struct elf64; // fspec needs union to parse ei_class != 2 type - e_flags: u32 hex; - e_ehsz: u16; - e_phentsize: u16; - e_phnum: u16; - e_shentsize: u16; - e_shnum: u16; - e_shstrndx: u16; + u8 ei_magic[4] | matches('\x7fELF') str; + u8 ei_class hex; // word size + u8 ei_data hex; // endianess + u8 ei_version; + u8 ei_osabi; + u8 ei_abi_version; + u8 padding[7] nul; + u16 e_type hex; + u16 e_machine hex; + u32 e_version; + select (ei_class) { + 1) struct elf32 elf32; + 2) struct elf64 elf64; + } arch; + u32 e_flags hex; + u16 e_ehsz; + u16 e_phentsize; + u16 e_phnum; + u16 e_shentsize; + u16 e_shnum; + u16 e_shstrndx; }; diff --git a/spec/emz.fspec b/spec/emz.fspec index 0fe02a1..9eb90a4 100644 --- a/spec/emz.fspec +++ b/spec/emz.fspec @@ -1,7 +1,7 @@ struct emz { - header: u8[4] | matches('#EMZ') str; - unknown: u32 hex; // most likely redunancy check (crc32?) - size: u32; - offset: u32; // always 16? - data: u8[$] | compression('deflate', size) hex; + u8 header[4] | matches('#EMZ') str; + u32 unknown hex; // most likely redunancy check (crc32?) + u32 size; + u32 offset; // always 16? + u8 data[$] | compression('deflate', size) hex; }; diff --git a/spec/ftable.fspec b/spec/ftable.fspec index 39fdd26..c051a6a 100644 --- a/spec/ftable.fspec +++ b/spec/ftable.fspec @@ -1,3 +1,3 @@ struct ftable { - id: u16[$] hex; + u16 id[$] hex; }; diff --git a/spec/item.fspec b/spec/item.fspec index c4d1767..31bd3f5 100644 --- a/spec/item.fspec +++ b/spec/item.fspec @@ -15,14 +15,12 @@ struct item { u16 type; u16 resource; u16 targets; - - union data (type) { - 4 => struct weapon weapon; - 5 => struct armor armor; - 7 => struct usable usable; - 12 => struct puppet puppet; - * => struct general general; - }; - + select (type) { + 4) struct weapon weapon; + 5) struct armor armor; + 7) struct usable usable; + 12) struct puppet puppet; + *) struct general general; + } data; struct strings strings; }; diff --git a/spec/model.fspec b/spec/model.fspec index afa8281..aade45c 100644 --- a/spec/model.fspec +++ b/spec/model.fspec @@ -1,6 +1,6 @@ struct texture { u8 type; - u8 name[16] = ascii; + u8 name[16] | encoding('ascii') str; u32 version; u32 width; u32 height; diff --git a/spec/name.fspec b/spec/name.fspec index d4e0f7c..491c8da 100644 --- a/spec/name.fspec +++ b/spec/name.fspec @@ -1,8 +1,8 @@ struct name { - name: u8[28] | encoding('ascii') str; // The encoding actually depends on ROM region - id: u32; + u8 name[28] | encoding('ascii') str; // The encoding actually depends on ROM region + u32 id; }; struct dat { - name: struct name[$]; + struct name name[$]; }; diff --git a/spec/spell.fspec b/spec/spell.fspec index 68aa5fb..8c9c894 100644 --- a/spec/spell.fspec +++ b/spec/spell.fspec @@ -1,22 +1,22 @@ struct spell { - index: u16; - type: u16; // 1-6 for White/Black/Summon/Ninja/Bard/Blue - element: u16; - targets: u16; - skill: u16; - mp_cost: u16; - casting_time: u8; // in quarter of seconds - recast_delay: u8; // in quarter of seconds - level: u8[24] hex; // 1 byte per job, 0xxFF if not learnable, first slot is NONE job so always 0xFF - id: u16; // 0 for "unused" spells; often, but not always, equal to index - unknown: u8; - jp_name: u8[20] | encoding('sjis') str; - en_name: u8[20] | encoding('ascii') str; - jp_description: u8[128] | encoding('sjis') str; - en_description: u8[128] | encoding('ascii') str; - padding: u8[687] nul; + u16 index; + u16 type; // 1-6 for White/Black/Summon/Ninja/Bard/Blue + u16 element; + u16 targets; + u16 skill; + u16 mp_cost; + u8 casting_time; // in quarter of seconds + u8 recast_delay; // in quarter of seconds + u8 level[24] hex; // 1 byte per job, 0xxFF if not learnable, first slot is NONE job so always 0xFF + u16 id; // 0 for "unused" spells; often, but not always, equal to index + u8 unknown; + u8 jp_name[20] | encoding('sjis') str; + u8 en_name[20] | encoding('ascii') str; + u8 jp_description[128] | encoding('sjis') str; + u8 en_description[128] | encoding('ascii') str; + u8 padding[687] nul; }; struct dat { - spell: struct spell[$]; + struct spell spell[$]; }; diff --git a/spec/vtable.fspec b/spec/vtable.fspec index de281b8..a6be4eb 100644 --- a/spec/vtable.fspec +++ b/spec/vtable.fspec @@ -1,3 +1,3 @@ struct vtable { - exist: u8[$] hex; + u8 exist[$] hex; }; diff --git a/vim/filespec.vim b/vim/filespec.vim index 19c9945..1e29d91 100644 --- a/vim/filespec.vim +++ b/vim/filespec.vim @@ -9,10 +9,9 @@ syn keyword fsTodo contained TODO FIXME XXX syn cluster fsCommentGroup contains=fsTodo,fsBadContinuation syn region fsComment start="//" skip="\\$" end="$" keepend contains=@fsCommentGroup,@Spell -syn keyword fsStructure enum struct union -syn keyword fsType s8 s16 s32 s64 -syn keyword fsType u8 u16 u32 u64 -syn keyword fsConstant nul dec hex str +syn keyword fsStructure enum struct select until +syn match fsType "[su][1-9][0-9]*" +syn keyword fsConstant nul dec hex str be le true false syn case ignore syn match fsNumbers display transparent "\<\d\|\.\d" contains=fsNumber,fsFloat,fsOctalError,fsOctal @@ -31,6 +30,7 @@ syn case match syn match fsSpecial display contained "\\\(x\x\+\|\o\{1,3}\|.\|$\)" syn match fsString1 "'[^']*'" contains=fsSpecial syn match fsString2 '"[^"]*"' contains=fsSpecial +syn match fsBinary "b[0-1x]\+" syn match fsBlock "[{}]" syn match fsBracket "[\[\]]" @@ -43,6 +43,7 @@ hi def link fsComment Comment hi def link fsStructure Structure hi def link fsType Type hi def link fsConstant Constant +hi def link fsBinary Number hi def link fsNumber Number hi def link fsOctal Number hi def link fsOctalZero PreProc |