Binary helper class

9th January 2014

If you are dealing with modern browsers only that support Typed arrays and you are trying to read a binary file then the class below will really make things easier for you:

//a helper for traversing a byte (unsigned int 8bit) array and extracting info
//note: endian is little-endian in js
function ByteReader(arrayBuffer) {
this.arrayBuffer = arrayBuffer;
this.readIndex = 0;

//create typed objects for fast access to the same data rather than converting between different types
this.int8 = new Int8Array(this.arrayBuffer);
this.uint8 = new Uint8Array(this.arrayBuffer);
this.int16 = new Int16Array(this.arrayBuffer);
this.uint16 = new Uint16Array(this.arrayBuffer);
this.int32 = new Int32Array(this.arrayBuffer);
this.uint32 = new Uint32Array(this.arrayBuffer);
this.float32 = new Float32Array(this.arrayBuffer);
this.float64 = new Float64Array(this.arrayBuffer);
}

ByteReader.prototype.hasMore = function() {
return (this.uint8.length > this.readIndex+1);
};

//unlike the others, this does not impact on the next read index
ByteReader.prototype.readBytes = function(offset, length) {
if (length) {
return this.uint8.subarray(offset, offset+length);
} else {
return new Uint8Array();
}
};

ByteReader.prototype.setIndex = function(i) {
this.readIndex = i; //this is always relative to byte order
};

ByteReader.prototype.readInt8 = function() {
var result = this.int8[this.readIndex];
this.readIndex++;
return parseInt(result);
};

ByteReader.prototype.readUint8 = function() {
var result = this.uint8[this.readIndex];
this.readIndex++;
return parseInt(result);
};

ByteReader.prototype.readInt16 = function() {
var result = this.int16[this.readIndex/2];
this.readIndex+=2;
return parseInt(result);
};

ByteReader.prototype.readUint16 = function() {
var result = this.uint16[this.readIndex/2];
this.readIndex+=2;
return parseInt(result);
};

ByteReader.prototype.readInt32 = function() {
var result = this.int32[this.readIndex/4];
this.readIndex+=4;
return parseInt(result);
};

ByteReader.prototype.readUint32 = function() {
var result = this.uint32[this.readIndex/4];
this.readIndex+=4;
return parseInt(result);
};

ByteReader.prototype.readFloat32 = function() {
var result = this.float32[this.readIndex/4];
this.readIndex+=4;
return parseInt(result);
};

ByteReader.prototype.readFloat64 = function() {
var result = this.float64[this.readIndex/8];
this.readIndex+=8;
return parseInt(result);
};

//read two bytes and convert to a string
ByteReader.prototype.readASCII = function(length) {
var result = String.fromCharCode.apply(null, this.uint8.subarray(this.readIndex, this.readIndex+length));
this.readIndex += length;
return result;
};

Use case - partial Doom wad reader

I originally made the class above to assist in the parsing of Doom Wads so I can load them up and render the contents in WebGL. Although the parsing is only partially complete, you can get a feel for how the class helps you read binary files:

//load all of the wad data into usuable objects
//rather than define objects for all types, we just create and load the object structure from the wad into each relevant type
var Wad = function(indentification) {
this.indentification = indentification;
this.lumpInfo = [];
this.lumpHash = {};

this.sprites = {};
this.flats = {};
this.maps = {};
}

Wad.loadUrl = function(url, callback) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", url, true);
xmlhttp.responseType = "arraybuffer";

xmlhttp.onload = function(e) {
if (xmlhttp.response) {
callback(Wad.loadBlob(xmlhttp.response));
}
};

xmlhttp.send(null);
};

Wad.loadBlob = function(arrayBuffer) {
//now parse the wad file like any other file type
var byteReader = new ByteReader(arrayBuffer);

//read the header of the wad file
var wadinfo = new Wad(byteReader.readASCII(4));

var numLumps = byteReader.readUint32();
var infotableofs = byteReader.readUint32();

//read the lump info (directory)
byteReader.setIndex(infotableofs);

var MODE_NORMAL = 0, MODE_SPRITES = 1, MODE_FLATS = 2, MODE_MAP = 3;
var flats, sprites, mode = MODE_NORMAL;

var mapLumps = ["THINGS", "LINEDEFS", "SIDEDEFS", "VERTEXES", "SEGS", "SSECTORS", "NODES", "SECTORS", "REJECT", "BLOCKMAP", "BEHAVIOR"];
var lump, filepos, size, lumpName, lumpData, newLump, mapName;

for (var j=0; j<numLumps; j++) {
lump = arrayBuffer.sub

filepos = byteReader.readUint32();
size = byteReader.readUint32();
lumpName = byteReader.readASCII(8).trim().toUpperCase().replace(/\0+/, ""); //remove the nul character

//may as well just read and extract the data into a hash now rather than later
lumpData = byteReader.readBytes(filepos, size);
newLump = {
name: lumpName,
data: lumpData
};

wadinfo.lumpInfo.push(newLump);

//based on the current mode and the lump name decide what to do with the data
switch (lumpName) {
case "FF_START": case "F_START":
mode = MODE_FLATS;
continue;
case "FF_END": case "F_END":
case "SS_END": case "S_END":
mode = MODE_NORMAL;
continue;
case "SS_START": case "S_START":
mode = MODE_SPRITES;
continue;
}

//handle maps too
if (lumpName.match(/^E[0-9]M[0-9]$/) || lumpName.match(/^MAP[0-9][0-9]$/)) {
mapName = lumpName;
wadinfo.maps[mapName] = {};
mode = MODE_MAP;
continue;
}

if (mode == MODE_MAP && mapLumps.indexOf(lumpName)<0) {
mode = MODE_NORMAL;
}

//parse all of the data in the lumps and load them into object structures that are useful
var lumpDataReader = new ByteReader(lumpData);

switch(lumpName) {
case "BLOCKMAP":
var blockMap = {
header: {
x: lumpDataReader.readUint16(),
y: lumpDataReader.readUint16(),
columns: lumpDataReader.readUint16(),
rows: lumpDataReader.readUint16()
},
blocks: []
};

var numBlocks = blockMap.header.columns * blockMap.header.rows;
var blockIndexes = [];
for (var i=0; i<numBlocks; i++) {
blockIndexes.push(lumpDataReader.readUint16());
}

for (var i=0; i<blockIndexes.length; i++) {
lumpDataReader.setIndex((blockIndexes[i]*2)+1); //skip the first 0x0000 entry

//keep reading the line definition indexes for the current block
var block = [], lineIndex;

//keep reading the line definition indexes until we hit the end token
while ((lineIndex = lumpDataReader.readUint16()) != 0xFFFF) {
block.push(lineIndex);
}

blockMap.blocks.push(block);
}

lumpData = blockMap;

break;
case "VERTEXES":
var vertexes = [];

while(lumpDataReader.hasMore()) {
vertexes.push({
x: lumpDataReader.readUint16(),
y: lumpDataReader.readUint16()
});
}

lumpData = vertexes;
break;
case "SECTORS":
var sectors = [];

while(lumpDataReader.hasMore()) {
sectors.push({
floorheight: lumpDataReader.readUint16(),
ceilingheight: lumpDataReader.readUint16(),
floorpic: lumpDataReader.readASCII(8),
ceilingpic: lumpDataReader.readASCII(8),
lightlevel: lumpDataReader.readUint16(),
special: lumpDataReader.readUint16(),
tag: lumpDataReader.readUint16()
});
}

lumpData = sectors;
break;
case "SIDEDEFS":
var sideDefs = [];

while(lumpDataReader.hasMore()) {
sideDefs.push({
textureoffset: lumpDataReader.readUint16(),
rowoffset: lumpDataReader.readUint16(),
topTexture: lumpDataReader.readASCII(8),
bottomTexture: lumpDataReader.readASCII(8),
midTexture: lumpDataReader.readASCII(8),
sector: lumpDataReader.readUint16()
});
}

lumpData = sideDefs;
break;
case "LINEDEFS":
var lineDefs = [];

while(lumpDataReader.hasMore()) {
lineDefs.push({
v1: lumpDataReader.readUint16(),
v2: lumpDataReader.readUint16(),
flags: lumpDataReader.readUint16(),
special: lumpDataReader.readUint16(),
tag: lumpDataReader.readUint16(),
sidenum: [lumpDataReader.readUint16(), lumpDataReader.readUint16()]
});
}

lumpData = lineDefs;

break;
case "SSECTORS":
var ssectors = [];

while(lumpDataReader.hasMore()) {
ssectors.push({
numsegs: lumpDataReader.readUint16(),
firstseg: lumpDataReader.readUint16()
});
}

lumpData = ssectors;
break;
case "NODES":
var nodes = [];

while(lumpDataReader.hasMore()) {
nodes.push({
x: lumpDataReader.readUint16(),
y: lumpDataReader.readUint16(),
dx: lumpDataReader.readUint16(),
dy: lumpDataReader.readUint16(),
bbox: [
[lumpDataReader.readUint16(), lumpDataReader.readUint16(), lumpDataReader.readUint16(), lumpDataReader.readUint16()],
[lumpDataReader.readUint16(), lumpDataReader.readUint16(), lumpDataReader.readUint16(), lumpDataReader.readUint16()]
],
children: [lumpDataReader.readUint16(), lumpDataReader.readUint16()],
});
}

lumpData = nodes;
break;
case "SEGS":
var segs = [];

while(lumpDataReader.hasMore()) {
segs.push({
v1: lumpDataReader.readUint16(),
v2: lumpDataReader.readUint16(),
angle: lumpDataReader.readUint16(),
linedef: lumpDataReader.readUint16(),
side: lumpDataReader.readUint16(),
offset: lumpDataReader.readUint16()
});
}

lumpData = segs;
break;
case "REJECT":

break;
case "THINGS":
var things = [];

while(lumpDataReader.hasMore()) {
things.push({
x: lumpDataReader.readUint16(),
y: lumpDataReader.readUint16(),
angle: lumpDataReader.readUint16(),
type: lumpDataReader.readUint16(),
options: lumpDataReader.readUint16()
});
}

lumpData = things;
break;
}

switch (mode) {
case MODE_NORMAL:
wadinfo.lumpHash[lumpName] = lumpData;
break;
case MODE_SPRITES:
wadinfo.sprites[lumpName] = lumpData;
break;
case MODE_FLATS:
wadinfo.flats[lumpName] = lumpData;
break;
case MODE_MAP:
wadinfo.maps[mapName][lumpName] = lumpData;
break;
}
}

return wadinfo;
};

Make a comment

Contribute to this article and have your say.