/**
* Set of functions to define file formats.
* @module hedit/format
*/
import formatInternal from 'hedit/private/format';
// Color names to integers map.
const COLORS = {
white: 0,
gray: 1,
blue: 2,
red: 3,
pink: 4,
green: 5,
purple: 6,
orange: 7
};
// Helper function to join two optional strings
function join(a, b) {
if (a && b) {
return a + ' > ' + b;
} else {
return a ? a : b;
}
}
/**
* A `Format` represents the binary structure of a file.
*
* A format is logically divided in **spans**, each of which holds some information
* like an user friendly name and its length. The name is displayed in the statusbar
* at the bottom of the screen to help the user navigate the file.
*
* Some file formats have spans that depend on the actual value of some bytes before them
* (think of a Pascal-style string), so it is needed to establish some kind of dependency
* on those bytes. To accomplish this, you can assign a variable name to a span and read
* its value later when needed. See below for an example of the Pascal string format.
*
* The `Format` class exposes a fluent API to build formats in a single expression,
* and can be combined to create complex trees out of simple smaller formats.
* When you nest formats (using [array()]{@link Format#array}, or [child()]{@link Format#child},
* as well as when using [group()]{@link Format#group}), you can always specify a name:
* this name will be prepended to the name of the inner spans, resulting in names with the following structure:
* ```
* 'Group 1 > Group 2 > A byte'
* ```
*
* You can access this class importing `hedit/format`:
* ```
* import Format from 'hedit/format';
* ```
*
* @example
* // This is a format describing a sequence of two integers of diffent lengths
* const exampleFormat =
* new Format()
* .uint16('A 16 bit integer', 'orange')
* .uint32('A longer 32 bit integer', 'blue');
*
* @example
* // This is a format for a Pascal-like string, i.e. a string with its length prefixed.
* const pascalString =
* new Format()
* .uint8('String length', 'orange', 'len')
* .array('String contents', 'len', 'blue');
* // The line above is a shortcut for:
* // .array('String contents', vars => vars.len, 'blue');
*
* @see For a list of the builtin formats, check [the source](https://github.com/95ulisse/hedit/tree/master/src/js/format).
*/
class Format {
constructor() {
this._segments = [];
}
/**
* Repeates a child format a fixed number of times.
*
* @param {string?} name - Name of this segment.
* @param {integer|function|string} length - How many times to repeat the child format.
* If it is an integer, it represents a fixed number of repetitions.
* If it is a function, it is called with a dictionary of all the available variables
* and is expected to return an integer. If it is a string, it is treated as a
* shortcut for `vars => vars[length]`.
* @param {Format} child - Child format to repeat.
* @return {Format}
*
*//**
*
* Adds a span of raw bytes to the format.
*
* @param {string?} name - Name of this segment.
* @param {integer|function|string} length - How many times to repeat the child format.
* If it is an integer, it represents a fixed number of repetitions.
* If it is a function, it is called with a dictionary of all the available variables
* and is expected to return an integer. If it is a string, it is treated as a
* shortcut for `vars => vars[length]`.
* @param {string} [color=white] - Color of the span.
* @return {Format}
*/
array(name, length, child = 'white') {
const repeat = typeof length === 'string' ? data => 0 + data[length] : length;
if (child instanceof Format) {
// A composite child
this._segments.push({
child: {
*__linearize(proxy, absoffset, basename, variables) {
const n = typeof repeat === 'function' ? repeat(variables) : repeat;
let offset = absoffset;
for (let i = 0; i < n; i++) {
for (let childseg of child.__linearize(proxy, offset, join(basename, name), Object.create(variables))) {
yield childseg;
offset = childseg.to + 1;
}
}
}
}
});
} else if (typeof child === 'string') {
// Shortcut for a simple array of bytes
this._segments.push({
child: {
*__linearize(proxy, absoffset, basename, variables) {
const n = typeof repeat === 'function' ? repeat(variables) : repeat;
if (n > 0) {
yield {
name: join(basename, name),
color: COLORS[child],
from: absoffset,
to: absoffset + n - 1
};
}
}
}
});
}
return this;
}
/**
* Adds a child format to this format.
* This is equivalent to `array(name, 1, c)`.
*
* @param {string} name - Name of this span.
* @param {Format} c - Child format to insert.
* @return {Format}
*
*//**
*
* Adds a child format to this format.
* This is equivalent to `array(null, 1, c)`.
*
* @param {Format} c - Child format to insert.
* @return {Format}
*/
child(name, c) {
if (typeof c === 'undefined') {
c = name;
name = null;
}
return this.array(name, 1, c);
}
/**
* Adds an infinite sequence of the given child format to this format.
* This is equivalent to `array(name, Infinity, c)`.
*
* @param {string} name - Name of this span.
* @param {Format} c - Child format to repeat indefinitely.
* @return {Format}
*
*//**
*
* Adds an infinite sequence of the given child format to this format.
* This is equivalent to `array(null, Infinity, c)`.
*
* @param {Format} c - Child format to repeat indefinitely.
* @return {Format}
*/
sequence(name, c) {
if (typeof c === 'undefined') {
c = name;
name = null;
}
return this.array(name, Infinity, c);
}
/**
* Groups multiple spans under a single name.
* There **must** be a matching call to [endgroup()]{@link Format#endgroup}.
*
* @param {string} name - Group name.
* @return {Format}
*
* @example
* // This generates the following two spans with the following names:
* // Group > A byte
* // Group > Another byte
* new Format()
* .group('Group')
* .uint8('A byte')
* .uint8('Another byte')
* .endgroup();
*/
group(name) {
const childFormat = new Format();
childFormat._parent = this;
this.child(name, childFormat);
return childFormat;
}
/**
* Ends a group started with [group()]{@link Format#group}.
* There **must** be a matching call to [group()]{@link Format#group}.
*
* @return {Format}
*
* @example
* // This generates the following two spans with the following names:
* // Group > A byte
* // Group > Another byte
* new Format()
* .group('Group')
* .uint8('A byte')
* .uint8('Another byte')
* .endgroup();
*/
endgroup() {
const parent = this._parent;
if (!parent) {
throw new Error('Unbalanced group()/endgroup() calls.');
}
delete this._parent;
return parent;
}
*__linearize(proxy, absoffset, basename, variables) {
if (this._parent) {
throw new Error('Unbalanced group()/endgroup() calls.');
}
let offset = absoffset;
// Iterate over all the segments computing the actual absolute offsets
for (let seg of this._segments) {
if (seg.child) {
for (let childseg of seg.child.__linearize(proxy, offset, join(basename, seg.name), Object.create(variables))) {
yield childseg;
offset = childseg.to + 1;
}
} else if (seg.length > 0) {
if (seg.id) {
variables[seg.id] = seg.read(proxy, offset);
}
yield {
name: join(basename, seg.name),
color: COLORS[seg.color],
from: offset,
to: offset + seg.length - 1
};
offset += seg.length;
}
}
}
};
function addCommonMethod(name, length, m, endianess) {
if (endianess) {
// Generate two methods for the little and big endian version
Format.prototype[name + 'le'] = function (name, color = 'white', id) {
this._segments.push({
name,
color,
length,
id,
read(proxy, off) {
return m.call(new DataView(proxy.read(off, length)), 0, true /* Little endian */);
}
});
return this;
};
Format.prototype[name + 'be'] = function (name, color = 'white', id) {
this._segments.push({
name,
color,
length,
id,
read(proxy, off) {
return m.call(new DataView(proxy.read(off, length)), 0, false /* Big endian */);
}
});
return this;
};
// Name without endianess specification defaults to big endian
Format.prototype[name] = Format.prototype[name + 'be'];
} else {
// Generate a single method regardless of the endianess
Format.prototype[name] = function (name, color = 'white', id) {
this._segments.push({
name,
color,
length,
id,
read(proxy, off) {
return m.call(new DataView(proxy.read(off, length)), 0);
}
});
return this;
};
}
}
/**
* Adds a span of 1 byte to the current format.
* The difference between the signed and unsigned versions matters only if reading
* the actual value of the byte.
*
* @name Format#int8
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
addCommonMethod('int8', 1, DataView.prototype.getInt8, false);
/**
* Adds a span of 1 byte to the current format.
* The difference between the signed and unsigned versions matters only if reading
* the actual value of the byte.
*
* @name Format#uint8
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
addCommonMethod('uint8', 1, DataView.prototype.getUint8, false);
/**
* Adds a span of 2 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#int16
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 2 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#int16le
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 2 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#int16be
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 2 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#uint16
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 2 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#uint16le
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 2 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#uint16be
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
addCommonMethod('int16', 2, DataView.prototype.getInt16, true );
addCommonMethod('uint16', 2, DataView.prototype.getUint16, true );
/**
* Adds a span of 4 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#int32
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 4 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#int32le
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 4 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#int32be
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 4 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#uint32
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 4 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#uint32le
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 4 bytes to the current format.
* The difference between the signed, unsigned, little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#uint32be
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
addCommonMethod('int32', 4, DataView.prototype.getInt32, true );
addCommonMethod('uint32', 4, DataView.prototype.getUint32, true );
/**
* Adds a span of 4 bytes to the current format.
* The difference between the little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#float32
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 4 bytes to the current format.
* The difference between the little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#float32le
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 4 bytes to the current format.
* The difference between the little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#float32be
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
addCommonMethod('float32', 4, DataView.prototype.getFloat32, true );
/**
* Adds a span of 8 bytes to the current format.
* The difference between the little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#float64
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 8 bytes to the current format.
* The difference between the little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#float64le
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
/**
* Adds a span of 8 bytes to the current format.
* The difference between the little and big endian
* versions matters only if reading the actual value of the byte.
* If no endianess is specified, the default is big endian.
*
* @name Format#float64be
* @function
* @param {string?} name - Name of the span.
* @param {string} [color=white] - Color of the span.
* @param {string} [id] - Name of the variable in which to store the actual value of this byte.
* @return {Format}
*/
addCommonMethod('float64', 8, DataView.prototype.getFloat64, true );
export { Format as default };
/**
* Registers a new file format.
* @alias module:hedit/format.registerFormat
* @param {string} name - Name of the format. Must be unique.
* @param {object} [guess] - Hint for automatic selection of format on file open.
* @param {string} guess.extension - Use this format for the files matching this extension.
* @param {string} guess.magic - Use this format for the files starting with the given magic.
* @param {object} desc - Description of the file format.
* @throws Throws if the name of the format is not unique or if the format description
* is invalid.
*/
export function registerFormat(name, guess, desc) {
if (typeof desc === 'undefined' && typeof guess === 'object') {
desc = guess;
guess = null;
}
if (!(desc instanceof Format)) {
throw new Error('Only Format objects can be registered.');
}
formatInternal.registerFormat(name, guess, desc);
}