Schema Reference
A schema tells EByte what a binary format looks like: the fields in order, how wide each one is, and how they depend on each other. This page covers version 1 of the schema format, the one EByte reads. The repeat type needs EByte 0.2.0 or later, derive forms other than count_of an array need 0.3.0, and checksums need 0.4.0. Worked schemas for real formats are in the schema library. The full contract, with every edge case, is schema-v1.md on GitHub.
The File
A schema is a UTF-8 JSON file with exactly four members:
{
"schema_version": 1,
"name": "ebyte_packet",
"endian": "little",
"fields": [ ... ]
}
endian is little or big, and integers inherit it unless they set their own. Names are ASCII letters, digits and underscores, up to 64 characters, starting with a letter or underscore. EByte is strict about the JSON: unknown members, duplicate members, null values and invalid Unicode are all rejected before any binary input is opened.
A schema’s identity is the SHA-256 of its exact bytes, whitespace included. Documents carry that hash, so a document decoded with one schema can’t be encoded with another by accident. Reformatting a schema file changes its identity.
Field Types
Every field has a name and a type.
| Type | Width | In the JSON document |
|---|---|---|
u8, u16, u32, u64 |
1, 2, 4, 8 bytes, unsigned | Decimal string, such as "300" |
i8, i16, i32, i64 |
1, 2, 4, 8 bytes, two’s complement | Decimal string, such as "-2" |
bytes |
length bytes |
Lowercase hex, two characters per byte |
string |
length bytes, encoding is ascii or utf8 |
The exact characters, NULs and spaces kept |
array |
count elements, each an object of fields |
JSON array of objects |
repeat |
Elements of fields until a stop rule, no count |
JSON array of objects |
f32, f64 |
4, 8 bytes, IEEE 754, since 0.6.0 | Shortest exact decimal, "inf", "-inf" or "nan:" and the bits |
bits |
bits from 1 to 64, packed with its neighbours, since 0.6.0 |
Decimal string |
Integers are strings in the document so that 64-bit values never pass through floating point.
An integer can set its own endian. A length or count is either a literal number or a reference to an earlier integer in the same object, written as {"field": "data_size"}. Strings are exact-width: EByte doesn’t trim, pad or look for a terminator. If a format pads a field, declare the padding as its own field and it’ll be kept.
Repeating Blocks
Many formats are a run of blocks with no count anywhere: RIFF chunks, ZIP entries, PNG chunks. A repeat field reads them, with until saying when to stop.
"until": "end_of_input" keeps reading elements while any byte is left. It has to be the last field of the schema and can’t have when.
{"name": "chunks", "type": "repeat", "until": "end_of_input", "fields": [
{"name": "id", "type": "string", "encoding": "ascii", "length": 4},
{"name": "size", "type": "u32"},
{"name": "body", "type": "bytes", "length": {"field": "size"}}
]}
"until": "tag_mismatch" keeps reading while the next bytes equal a tag. The element’s first field is the tag: an unconditional bytes field of 1 to 16 bytes with expect. When the next bytes differ, or the input runs out, the repeat ends and the following fields carry on from there.
{"name": "entries", "type": "repeat", "until": "tag_mismatch", "fields": [
{"name": "signature", "type": "bytes", "length": 4, "expect": "504b0304"},
...
]}
In the document a repeat is an array of objects, like an array, and you can add, remove or reorder elements freely. Every element has to take at least one byte. On encode, EByte refuses output whose bytes right after a tag repeat start with the tag, because reading the file back would take them for another element.
Floats, Bit Fields and Forward Jumps
These arrived in 0.6.0.
Floats. f32 and f64 follow the field’s endian. Decoding writes the shortest decimal that turns back into exactly the same bits, such as "0.1" or "-0", and "nan:7fc00001" for a NaN, with every bit, so even NaN payloads survive. When encoding you can write any ordinary decimal, such as "0.5" or "2.5e-3"; it’s rounded to the nearest float, and a number too big for the type is refused.
Bit fields. Consecutive bits fields share bytes. Their sizes have to add up to whole bytes, at most 8, with nothing else in between. The bytes are read as one big-endian number and the fields take their bits from the most significant end, in order:
{"name": "version", "type": "bits", "bits": 4, "expect": "4"},
{"name": "header_words", "type": "bits", "bits": 4}
A bit field works like an unsigned integer: it can have expect, and other fields can use it as a length, a count or a when test. It can’t have when or derive itself.
Forward jumps. A bytes field can run up to an offset instead of having a length: {"length": {"until_offset": "pixel_offset"}} takes every byte from where the field starts up to the absolute offset in pixel_offset, or a literal number. The bytes are kept, so nothing is lost. If the offset is behind the field, decoding fails, and when encoding the bytes you give must reach the offset exactly.
Checks, Conditions and Derived Counts
expect checks a fixed value. On an integer it’s a decimal string, on a fixed-length bytes field it’s lowercase hex. A magic number is the usual case:
{"name": "magic", "type": "bytes", "length": 2, "expect": "4542"}
A mismatch is an error on decode and on encode. EByte never corrects a value to make it pass.
when makes a field optional. The field is present only if an earlier integer in the same object equals a value:
{"name": "note", "type": "bytes", "length": 2, "when": {"field": "flags", "equals": "1"}}
Equality is the only test. When it’s false, the field takes no bytes and must be absent from the document.
derive lets the encoder work out a count. An unsigned integer marked {"count_of": "records"} is left out of the editable document, and on encode EByte writes the length of the records array into it. The array’s count must point back at that integer. A count field without derive stays in the document, and encode fails if it doesn’t match the array, rather than guessing which one is right.
Since EByte 0.3.0, derive also works out sizes and offsets, and since 0.4.0 checksums. Each one picks exactly one form:
| Form | The value is |
|---|---|
{"size_of": "body"} |
The byte length of body. If body takes its length from this integer, it’s written from body’s value, like count_of. |
{"size_of": {"from": "wave", "to": "chunks"}} |
The bytes from the start of wave to the end of chunks, inclusive. The range may include the integer itself. |
{"offset_of": "items"} |
Where items starts, counted from the first byte of the file. |
{"count_of": "entries"} |
The number of elements in the repeat entries. |
{"checksum": "crc32", "of": {"from": "type", "to": "data"}} |
A checksum of those fields’ bytes, since 0.4.0. of takes one field name or a from/to range. |
Targets are fields in the same object, before or after the integer.
Checksums come in five kinds. The integer’s type has to match the width:
checksum |
Used by | Type | Check value for 123456789 |
|---|---|---|---|
crc32 |
PNG, ZIP, gzip | u32 |
cbf43926 |
adler32 |
zlib | u32 |
091e01de |
crc16_arc |
ARC, LHA | u16 |
bb3d |
crc16_modbus |
Modbus RTU | u16 |
4b37 |
crc16_xmodem |
XMODEM | u16 |
31c3 |
A checksum covers the bytes as stored, after any sizes or offsets inside its range have been written, and its range can’t include itself or another checksum next to it. A stale checksum fails decoding with checksum_mismatch at the checksum field. EByte doesn’t decompress anything, so a checksum over uncompressed data, like a ZIP CRC of a deflated file, can’t be declared.
Size, offset and repeat-count forms can add a constant, written as a decimal string: {"offset_of": "items", "add": "-4"}. These measured values never decide how a file is read, so nothing may use them as a length, a count or a when test. Decoding checks each one against the file and reports constraint_mismatch if the stored number is stale. Encoding writes it after the rest of its object, and fails with value_out_of_range if the result is negative or too big for the integer.
Rules That Keep It Bounded
- References only point backwards, to an earlier integer in the same object that has no
when. The forward links allowed are a derived count or size to the one field it drives, and measured sizes and offsets, which never change how a file is read. - Arrays and repeats can nest up to 8 levels deep. A repeat looks ahead at most 16 bytes.
- There are no expressions, no arithmetic between fields beyond the
deriveforms, no seeking to an offset, no recursion and no plugins. - Decoding must use every byte. Data left over after the last field is reported as
trailing_data, not ignored.
Limits
These are the default ceilings. A program or a command-line flag can lower any of them, but never raise one.
| Limit | Default |
|---|---|
| Schema file size | 1 MiB |
| Field declarations in a schema | 1,024 |
| Array nesting depth | 8 |
| Binary input or output | 16 MiB |
| One bytes or string field | 16 MiB |
| Elements in one array or repeat | 65,536 |
| Values in a document | 100,000 |
| Entries in a trace | 100,000 |
| Document or trace JSON | 64 MiB |
Sizes are checked before EByte reads or allocates for a field, so a header that claims a 2^64-byte field is rejected after reading just that header.
Not in Version 1
Compression, reading backwards to an earlier offset, little-end-first bit numbering, and arithmetic beyond the derive forms. If your format needs one of these, say which: it helps decide what comes next.