Home Manual Reference Source Test

src/index.js

import CorrodeBase from './base';
import * as utils from './utils';
import { isPlainObject } from 'lodash';

import * as MAPPERS from './map';
import * as ASSERTIONS from './assert';

import { inspect } from 'util';

/**
 * Corrode
 * A batteries-included library for reading your binary data.
 * It helps you converting that blob-mess into readable data.
 *
 * It inherits from {@link CorrodeBase} to make the abstraction between these
 * two components more obvious. CorrodeBase does all the dirty low-level work
 * reading bytes, managing jobs, etc. This class however doesn't manipulate
 * the {@link CorrodeBase#jobs}-array or messes in other ways with the {@link CorrodeBase#jobLoop}.
 *
 * This class builds on loop and tap to provide more complex functionality.
 */
export default class Corrode extends CorrodeBase {

    /**
     * object holding all the user-defined extensions.
     * these will get bound to the corrode-instance on calling the constructor
     * @type {Object<Function>}
     */
    static EXTENSIONS = {};

    /**
     * mappers-object
     * @type {Object<Function>}
     */
    static MAPPERS = MAPPERS;

    /**
     * assertions-object
     * @type {Object<Function>}
     */
    static ASSERTIONS = ASSERTIONS;

    /**
     * initializes mappers, assertions and extensions
     * @param {object} options {@link CorrodeBase#constructor}
     */
    constructor(options){
        super(...arguments);

        // bind ext, map & assert onto this instance
        /** @type {Object<bound Function>} ext {@link Corrode.EXTENSIONS} bound to this instance */
        this.ext = utils.bindObject(Corrode.EXTENSIONS, this);
        /** @type {Object<bound Function>} map {@link Corrode.MAPPERS} bound to this instance */
        this.map = utils.tapBindObject(Corrode.MAPPERS, this);
        /** @type {Object<bound Function>} assert {@link Corrode.ASSERTIONS} bound to this instance */
        this.assert = utils.tapBindObject(Corrode.ASSERTIONS, this);
    }

    /**
     * pushes a repeat-job onto the job-array
     * a repeat-job repeats itself a given number of times and then ends.
     * it's also possible to end the job prematurely or discard it.
     * This is nothing else than a proxy to {@link CorrodeBase#loop}
     * @param {string} [name] name of the new variable-layer. if none is provided, the current layer will be used
     * @param {number} length iteration-count as number or as string referencing a variable from {@link CorrodeBase#vars}
     * @param {function(end: function, discard: function, i: number)} fn called until `end()` is called.
     *        `end(true)` can be used to end and discard the current loop.
     *        `discard()` can be used to reset the current layer ({@link CorrodeBase#options.anonymousLoopDiscardDeep}).
     *        `i` is the current iteration count
     * @return {Corrode} this
     */
    repeat(name, length, fn){
        if(typeof name === 'number' || typeof length === 'function'){
            fn = length;
            length = name;
            name = undefined;
        }

        return this.tap(function(){
            if(typeof length === 'string'){
                length = this.vars[length];
            }

            if(length === 0){
                if(name){
                    this.vars[name] = [];
                }
                return this;
            }

            const loopGuard = function(end, discard, i){
                fn.call(this, end, discard, i);

                if(i >= length - 1){
                    end();
                }
            };

            if(!name){
                return this.loop(loopGuard);
            }

            return this.loop(name, loopGuard);
        });
    }

    /**
     * pushes a terminatedBuffer-job onto the job-array
     * The returned Buffer will be a slice (not a copy) of the underlying {@link CorrodeBase#streamBuffer}
     * internally this method uses the isSeeking-property to prevent flushing of the
     * underlying buffer.
     * @param {string} name name of the buffer-variable
     * @param {number} [terminator=0] uint8-value indicating the end of the buffer
     * @param {boolean} [discardTerminator=true] whether or not to include the terminator in the resulting buffer
     * @return {Corrode} this
     */
    terminatedBuffer(name, terminator = 0, discardTerminator = true){
        let bufferLength = null;
        const loopVar = Symbol.for('terminatedBufferTmp');

        return this
            .tap(function(){
                terminator = typeof terminator === 'string' ? this.vars[terminator] : terminator;
                /** @type {boolean} isSeeking {@link CorrodeBase#isSeeking} */
                this.isSeeking = true;
            })
            .loop(function(end, discard, i){
                this
                    .uint8(loopVar)
                    .tap(function(){
                        if(this.vars[loopVar] === terminator){
                            bufferLength = i + 1;
                            end();
                        }
                    });
            })
            .tap(function(){
                delete this.vars[loopVar];
                this.vars[name] = this.streamBuffer.slice(this.chunkOffset - bufferLength, this.chunkOffset + (discardTerminator ? -1 : 0));
                this.isSeeking = false;
            });
    }

    /**
     * pushes a terminatedString-job onto the job-array
     * Returns a string ranging from the current offset until the given terminator is found
     * @param {string} name name of the string-variable
     * @param {number} [terminator=0] uint8-value indicating the end of the buffer
     * @param {boolean} [discardTerminator=true] whether or not to include the terminator in the resulting buffer
     * @param {string} [encoding=CorrodeBase#options.encoding] encoding encoding used to decode the string, defaults to 'utf8'.
     *                 available encodings can be found here https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings
     * @return {Corrode} this
     */
    terminatedString(name, terminator = 0, discardTerminator = true, encoding = this.options.encoding){
        return this
            .terminatedBuffer(name, terminator, discardTerminator)
            .tap(function(){
                if(!this.vars[name] || this.vars[name].length === 0){
                    // in case nothing is found, return an empty string
                    this.vars[name] = '';
                } else {
                    this.vars[name] = this.vars[name].toString(encoding);
                }
            });
    }

    /**
     * pushes a pointer-job onto the job-array
     * returns an item of an accessable type (object property, array element)
     * being given an accessable type. the offset used to access the accessable type
     * is determined by reading a value of the given type
     * @param {string} name name of the item-variable
     * @param {Object|Array} obj accessable type variable
     * @param {string} type name of the type which should be used to read the index (int8, uint32, etc)
     * @return {Corrode} this
     */
    pointer(name, obj, type = 'int64'){
        return this
            .tap(function(){
                if(typeof obj === 'string'){
                    obj = this.vars[obj];
                }

                this
                    [type](name)
                    .map.abs(name)
                    .map.get(name, obj);
            });
    }

    /**
     * sets {@link CorrodeBase#streamOffset} to a given absolute position
     * like skip, but absolute
     * If you want to set the offset to something lower than the current offset, you should
     * enable {@link CorrodeBase#isSeeking} asap, as otherwise there's no guarantee that the buffer is still available
     * or already flushed.
     * @param {number|string} offset as number or as string referencing a variable from {@link CorrodeBase#vars}
     * @return {Corrode} this
     */
    position(offset){
        return this
            .tap(function(){
                if(typeof offset === 'string'){
                    this.assert.exists(offset);
                    offset = this.vars[offset];
                }

                this.skip(offset - this.streamOffset);
            });
    }

    /**
     * helper utility logging the current vars
     * @return {Corrode} this
     */
    debug(){
        return this.tap(function(){
            console.log(inspect(this.vars, {
                showHidden: false,
                depth: null
            }));
        });
    }

    /**
     * helper utility, parsing a given buffer async
     * @param {Buffer} buffer data
     * @param {function(error: *, data: object)} done callback
     * @return {Corrode} this
     */
    fromBuffer(buffer, done){
        this.end(buffer);
        this.on('finish', () => done(this.vars));
        return this;
    }

    /**
     * adds an extension to corrode.
     * extensions are user-defined functions allowing you to better abstract upon corrode
     * an extension can accept parameters and receives the current available variables.
     * it can either directly return values or read them from the buffer with corrode's functions.
     * @param {string} name of the extension
     * @param {function(...args: *)} fn function receiving arguments given when calling the extension
     * @example <caption>Extension reading values</caption>
     * Corrode.addExtension('foo', function(arg1){ this.uint8(arg1); });
     * (new Corrode()).ext.foo('foo_value', 'arg1');
     * @example <caption>Extension returning values</caption>
     * Corrode.addExtension('bar', function(arg1){ return this.vars[arg1] * this.vars[arg1] });
     * (new Corrode()).ext.bar('bar_value', 'arg1')
     */
    static addExtension(name, fn){
        Corrode.EXTENSIONS[name] = function(name = 'values', ...args){
            return this.tap(name, function(){
                const value = fn.apply(this, args);

                if(typeof value !== 'undefined'){
                    if(this.options.strictObjectMode && this.jobs.length > 0 && value !== this && !isPlainObject(value)){
                        throw new TypeError(`Can't mix immediate returns with later reads on a non-object value (${JSON.stringify(value)}) in strictObjectMode`);
                    }
                    /** @type {Object} vars {@link CorrodeBase#vars} */
                    this.vars = value;
                }
            });
        };

        Corrode.EXTENSIONS[name].orgFn = fn;
    }
}