Skip to content

Commit

Permalink
Merge pull request #228 from theqabalist/topic/buffer-support
Browse files Browse the repository at this point in the history
Non-byte aligned primitives.
  • Loading branch information
Brian Mock committed Mar 10, 2018
2 parents fb569e0 + 9507048 commit 115dae5
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 35 deletions.
43 changes: 43 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,49 @@ parser.parse('accccc');
//=> {status: true, value: ['a', ['c', 'c', 'c', 'c', 'c']]}
```

# Binary constructors.

The purpose of the following constructors is to allow the consumption of Buffer types in node to allow for attoparsec style consumption of binary input.
As these constructors yield regular values within parsers, they can then be combined in the same fashion as the above string-based constructors to produce
robust binary parsers. These constructors live in the Parsimmon.Binary namespace.

## Parsimmon.byte(int)

Returns a parser that yields a byte that matches the given input. Similar to digit/letter.

```javascript
var parser = Parsimmon.Binary.byte(0xFF);
parser.parse(Buffer.from([0xFF]));
//=> { status: true, value: 255 }
```

## Parsimmon.bitSeq(alignments)

Specify a series of bit alignments that do not have to be byte aligned and consume them from a buffer. The bits must
sum to a byte boundary.

```javascript
var parser = Parsimmon.Binary.bitSeq([3, 5, 5, 3]);
parser.parse(Buffer.from([0x04, 0xFF]));
//=> {status: true, value: [ 0, 4, 31, 7 ]}
```

## Parsimmon.bitSeqObj(namedAlignments)

Specify a series of bit alignments with names that will output an object with those alignments. Very similar to seqObj,
however, but only accepts numeric values. Will discard unnamed alignments.

```javascript
var parser = Parsimmon.Binary.bitSeqObj([
["a", 3],
5,
["b", 5],
["c", 3]
]);
parser.parse(Buffer.from([0x04, 0xFF]));
//=> { status: true, value: { a: 0, b: 31, c: 7 } }
```

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Parser methods
Expand Down
36 changes: 18 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

196 changes: 192 additions & 4 deletions src/parsimmon.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,180 @@ function Parsimmon(action) {

var _ = Parsimmon.prototype;

function times(n, f) {
var i = 0;
for (i; i < n; i++) {
f(i);
}
}

function forEach(f, arr) {
times(arr.length, function(i) {
f(arr[i], i, arr);
});
}

function reduce(f, seed, arr) {
forEach(function(elem, i, arr) {
seed = f(seed, elem, i, arr);
}, arr);
return seed;
}

function map(f, arr) {
return reduce(
function(acc, elem, i, a) {
return acc.concat([f(elem, i, a)]);
},
[],
arr
);
}

function lshiftBuffer(input) {
var asTwoBytes = reduce(
function(a, v, i, b) {
return a.concat(
i === b.length - 1
? Buffer.from([v, 0]).readUInt16BE(0)
: b.readUInt16BE(i)
);
},
[],
input
);
return Buffer.from(
map(function(x) {
return ((x << 1) & 0xffff) >> 8;
}, asTwoBytes)
);
}

function consumeBitsFromBuffer(n, input) {
var state = { v: 0, buf: input };
times(n, function() {
state = {
v: (state.v << 1) | bitPeekBuffer(state.buf),
buf: lshiftBuffer(state.buf)
};
});
return state;
}

function bitPeekBuffer(input) {
return input[0] >> 7;
}

function sum(numArr) {
return reduce(
function(x, y) {
return x + y;
},
0,
numArr
);
}

function find(pred, arr) {
return reduce(
function(found, elem) {
return found || (pred(elem) ? elem : found);
},
null,
arr
);
}

function bufferExists() {
return typeof Buffer !== "undefined";
}

function ensureBuffer() {
if (!bufferExists()) {
throw new Error(
"Buffer global does not exist; please consider using https://github.com/feross/buffer if you are running Parsimmon in a browser."
);
}
}

function bitSeq(alignments) {
ensureBuffer();
var totalBits = sum(alignments);
if (totalBits % 8 !== 0) {
throw new Error(
"The bits [" +
alignments.join(", ") +
"] add up to " +
totalBits +
" which is not an even number of bytes; the total should be divisible by 8"
);
}
var bytes = totalBits / 8;

var tooBigRange = find(function(x) {
return x > 48;
}, alignments);
if (tooBigRange) {
throw new Error(
tooBigRange + " bit range requested exceeds 48 bit (6 byte) Number max."
);
}

return new Parsimmon(function(input, i) {
if (bytes + i > input.length) {
return makeFailure(i, bytes.toString() + " bytes");
}
return makeSuccess(
i + bytes,
reduce(
function(acc, bits) {
var state = consumeBitsFromBuffer(bits, acc.buf);
return {
coll: acc.coll.concat(state.v),
buf: state.buf
};
},
{ coll: [], buf: input },
alignments
).coll
);
});
}

function bitSeqObj(namedAlignments) {
ensureBuffer();
var fullAlignments = map(function(pair) {
return isArray(pair) ? pair : [null, pair];
}, namedAlignments);

var namesOnly = map(function(pair) {
return pair[0];
}, fullAlignments);
var alignmentsOnly = map(function(pair) {
return pair[1];
}, fullAlignments);

return bitSeq(alignmentsOnly).map(function(parsed) {
var namedParsed = map(function(name, i) {
return [name, parsed[i]];
}, namesOnly);

return reduce(
function(obj, kv) {
if (kv[0] !== null) {
obj[kv[0]] = kv[1];
}
return obj;
},
{},
namedParsed
);
});
}

function toArray(arrLike) {
return Array.prototype.slice.call(arrLike);
}
// -*- Helpers -*-

function isParser(obj) {
Expand All @@ -19,10 +193,9 @@ function isArray(x) {
return {}.toString.call(x) === "[object Array]";
}

var hasBuffer = typeof Buffer !== "undefined";
function isBuffer(x) {
/* global Buffer */
return hasBuffer && Buffer.isBuffer(x);
return bufferExists() && Buffer.isBuffer(x);
}

function makeSuccess(index, value) {
Expand Down Expand Up @@ -240,7 +413,7 @@ function seq() {
function seqObj() {
var seenKeys = {};
var totalKeys = 0;
var parsers = [].slice.call(arguments);
var parsers = toArray(arguments);
var numParsers = parsers.length;
for (var j = 0; j < numParsers; j += 1) {
var p = parsers[j];
Expand Down Expand Up @@ -611,7 +784,17 @@ function string(str) {
}

function byte(b) {
ensureBuffer();
assertNumber(b);
if (b > 0xff) {
throw new Error(
"Value specified to byte constructor (" +
b +
"=0x" +
b.toString(16) +
") is larger in value than a single byte."
);
}
var expected = (b > 0xf ? "0x" : "0x0") + b.toString(16);
return Parsimmon(function(input, i) {
var head = get(input, i);
Expand Down Expand Up @@ -802,7 +985,6 @@ var whitespace = regexp(/\s+/).desc("whitespace");
Parsimmon.all = all;
Parsimmon.alt = alt;
Parsimmon.any = any;
Parsimmon.byte = byte;
Parsimmon.createLanguage = createLanguage;
Parsimmon.custom = custom;
Parsimmon.digit = digit;
Expand Down Expand Up @@ -841,4 +1023,10 @@ Parsimmon.whitespace = whitespace;
Parsimmon["fantasy-land/empty"] = empty;
Parsimmon["fantasy-land/of"] = succeed;

Parsimmon.Binary = {
bitSeq: bitSeq,
bitSeqObj: bitSeqObj,
byte: byte
};

module.exports = Parsimmon;
9 changes: 3 additions & 6 deletions test/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
{
"env": {
"node": true
"node": true,
"mocha": true
},
"globals":{
"Parsimmon": true,
"assert": true,
"suite": true,
"setup": true,
"teardown": true,
"test": true
"assert": true
}
}
Loading

0 comments on commit 115dae5

Please sign in to comment.