Part of QUB Online

/* typedefStruct.js -- access slices of a DataView as C-style structs.

Intended for reading and writing binary file formats.

by Christopher Nicolai, 2017

Suppose you have a header file describing some binary file format, e.g.

    #pragma once
    #pragma pack(1)
    
    // This file is always saved in little-endian byte order
    typedef struct {
        char[4] magic;
        i16_t   version;
        i16_t   flags;
        f32_t   value1;
        f64_t   value2;
    } SimpleFile;

You also have an ArrayBuffer with the contents of such a file, e.g.

    fileInput.addEventListener('change', inputEvent => {
        const fileReader = new FileReader;
        fileReader.addEventListener('load', readerEvent => {
            const arraybuffer = readerEvent.target.result;
            // arraybuffer has the contents of file loaded via fileInput (<input type="file">)
        });
        fileReader.readAsArrayBuffer(fileInput.files[0]);
    });

Here's how to read it:

    // define the struct class
    const SimpleFile = typedefStruct([
        ['c[4]', 'magic'],
        ['i16', 'version'],
        ['i16', 'flags'],
        ['f32', 'value1'],
        ['f64', 'value2']
    ], true); // true: little-endian (by default, structs are big-endian (network byte order))
    
    // make a DataView on the arraybuffer
    const dataview = new DataView(arraybuffer);
    
    // make a struct instance operating at offset 0 in the dataview
    const myFile = new SimpleFile(dataview, 0);
    
    // find out the size of the struct:
    const byteLength = myFile.byteLength;
    
    // read one field at a time
    const value1 = myFile.value1;
    
    // or all fields, copied into a plain JS object:
    const myValues = myFile.copy();
    // (so we can release the arraybuffer asap and save memory)
    
    // also, edit fields:
    myFile.value1 = 1.0;
    myFile.magic = 'abcd';

We identify most types by the letter 'i' (integer), 'u' (unsigned) or 'f' (floating-point) followed by the bit width,
e.g. 'i8' or 'f64'. We also define special character types 'c', 'd', and 'e' for utf-8, -16, and -32 respectively, and
'p' for pascal strings. On their own, they are synonyms for the appropriate-sized unsigned int, but with array notation
they are treated specially as strings.

Array notation: follow a typename with dimension in brackets to define a (1D) array, e.g.

    ['i16[8]', 'eightShorts'],
    ['c[8]', 'eightBytesOfUTF8'],
    ['d[8]', 'eightBytesOfUTF16'],
    ['e[8]', 'eightBytesOfUTF32'],
    ['p[8]', 'oneByteOfStringLengthFollowedBySevenAsciiChars']
    ...
    const [a,b,c,d,e,f,g,h] = myFile.eightShorts;
    myFile.eightBytesOfUTF8 = 'testing';

Note that the entire array or string is read or written each time you access it, so batch accesses where possible.

Appending a '*' to the typename treats its value as a 32-bit unsigned pointer. While there is no sensible way to interpret
such pointers, we've included them because they are found in some file headers.

Tip: if someone was careless about (not) declaring struct packing, their compiler may have inserted gaps between fields.
Looking at a hex dump of the file can help locate gaps. They are usually inserted to make the next field start on a multiple
of 4, 8, or 16 bytes. If there are gaps, define extra fields in typedefStruct(), e.g.

    ['i16', 'myShort'],
    ['i16', 'packing1'], // inserted to mirror a gap
    ['i32', 'myInt']

Requires Mozilla StringView (https://developer.mozilla.org/en-US/Add-ons/Code_snippets/StringView) for UTF parsing
(types 'c[]', 'd[]', and 'e[]' only).

*/

const typedefStruct = (() => {
    'use strict';
    
    const getters = {
        c: 'getUint8', // utf8
        d: 'getUint16', // utf16
        e: 'getUint32', // utf32
        p: 'getUint8', // pascal string
        i8: 'getInt8',
        i16: 'getInt16',
        i32: 'getInt32',
        i64: 'getInt64',
        u8: 'getUint8',
        u16: 'getUint16',
        u32: 'getUint32',
        u64: 'getUint64',
        f32: 'getFloat32',
        f64: 'getFloat64',
        v: 'getVoid'
    };
    const setters = {
        c: 'setUint8',
        d: 'setUint16',
        e: 'setUint32',
        p: 'setUint8',
        i8: 'setInt8',
        i16: 'setInt16',
        i32: 'setInt32',
        i64: 'setInt64',
        u8: 'setUint8',
        u16: 'setUint16',
        u32: 'setUint32',
        u64: 'setUint64',
        f32: 'setFloat32',
        f64: 'setFloat64',
        v: 'setVoid'
    };
    
    function addPointerTypes(accessors, depth, sizeof_p) {
        sizeof_p = sizeof_p || 32;
        Object.keys(accessors).forEach(field => {
            accessors[field+'*'] = accessors[field][0]+'etUint'+sizeof_p;
        });
        depth && addPointerTypes(accessors, (depth|0)-1, sizeof_p);
    };
    addPointerTypes(getters, 2);
    addPointerTypes(setters, 2);

    const sizeof = {
        c: 1, d: 2, e: 4, p: 1
    };
    Object.keys(getters).forEach(field => {
        if ( sizeof[field] ) {
            /// already known
        } else if ( field.endsWith('*') ) {
            sizeof[field] = 4;
        } else {
            sizeof[field] = (field.slice(1)|0) >> 3;
        }
    });

    const arrayExpr = /([^[]+)\[(.*)\]/;
                                
    function typedefStruct(spec, littleEndian) {
        // Returns a class, which can be instantiated at an offset into a DataView,
        // whose fields are defined in spec, an array of [typename, fieldname].
        // Each fieldname becomes an instance property, with getters and setters that
        // directly manipulate the DataView contents.
        const le = !! littleEndian;
        return class Struct {
            constructor(dataview, offset) {
                let off = offset;
                for (const [typ, nm] of spec) {
                    let o = off,
                        arrayMatch = typ.match(arrayExpr);
                    if ( arrayMatch ) {
                        let [atyp, adim] = [arrayMatch[1], (arrayMatch[2]|0)];
                        adim = adim|0;
                        const getterName = getters[atyp];
                        const setterName = setters[atyp];
                        const sz = sizeof[atyp];
                        off += adim*sz;
                        if ( ['c', 'd', 'e'].indexOf(atyp) >= 0 ) {
                            const encoding = {c: 'UTF-8', d: 'UTF-16', e: 'UTF-32'}[atyp];
                            const sView = new StringView(dataview.buffer, encoding, o, adim);
                            Object.defineProperty(this, nm, {
                                get: () => sView.toString(),
                                set: s => {
                                    sView.rawData.fill(0);
                                    sView.rawData.set(new TextEncoder(encoding).encode(s));
                                }
                            });
                        } else if ( atyp === 'p' ) {
                            Object.defineProperty(this, nm, {
                                get: () => {
                                    const n = dataview.getUint8(o);
                                    let s = '',
                                        i = 1;
                                    for (; i<=n; ++i) {
                                        s += String.fromCharCode(dataview.getUint8(o+i));
                                    }
                                    return s;
                                },
                                set: s => {
                                    const n = Math.min(sz-1, s.length);
                                    dataview.setUint8(o, n);
                                    let i = 1;
                                    for (; i<=n; ++i) {
                                        dataview.setUint8(o+i, s.charCodeAt(i-1));
                                    }
                                }
                            });
                        } else {
                            Object.defineProperty(this, nm, {
                                get: () => exports.range(adim).map(i => dataview[getterName](o+i*sz, le)),
                                set: arr => arr.forEach((v,i) => dataview[setterName](o+i*sz, v, le))
                            });
                        }
                    } else {
                        off += sizeof[typ];
                        Object.defineProperty(this, nm, {
                            get: () => dataview[getters[typ]](o, le),
                            set: v => dataview[setters[typ]](o, v, le)
                        });
                    }
                }
                this.byteLength = off - offset;
            }
            copy() {
                const c = {};
                for (const [typ, nm] of spec) {
                    c[nm] = this[nm];
                }
                return c;
            }
        };
    }

    return typedefStruct;
})();