Skip to content

Commit

Permalink
Merge e943cfc into fb569e0
Browse files Browse the repository at this point in the history
  • Loading branch information
theqabalist committed Mar 9, 2018
2 parents fb569e0 + e943cfc commit 6e9b1a5
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 33 deletions.
37 changes: 37 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,43 @@ 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.

## 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.

180 changes: 178 additions & 2 deletions src/parsimmon.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,161 @@ 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 bitSeq(alignments) {
var totalBits = sum(alignments);
if (totalBits % 8 !== 0) {
throw new Error("Bits do not sum to byte boundary.");
}
var bytes = totalBits / 8;

var tooBigRange = find(function(x) {
return x > 48;
}, alignments);
if (tooBigRange) {
throw new Error(
tooBigRange.toString() +
" 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) {
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 Down Expand Up @@ -240,7 +395,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 @@ -612,6 +767,11 @@ function string(str) {

function byte(b) {
assertNumber(b);
if (b > 0xff) {
throw new Error(
"Value specified to byte constructor 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 +962,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 +1000,21 @@ Parsimmon.whitespace = whitespace;
Parsimmon["fantasy-land/empty"] = empty;
Parsimmon["fantasy-land/of"] = succeed;

function ensureBuffer(f) {
return function() {
if (typeof Buffer === "undefined") {
throw new Error(
"Buffer global does not exist; please consider using https://github.com/feross/buffer if you are running Parsimmon in a browser."
);
}
return f.apply(null, arguments);
};
}

Parsimmon.Binary = {
bitSeq: ensureBuffer(bitSeq),
bitSeqObj: ensureBuffer(bitSeqObj),
byte: ensureBuffer(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
}
}
45 changes: 45 additions & 0 deletions test/core/bitSeq.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use strict";

describe("bitSeq", function() {
it("consumes bits into a sequence from a buffer", function() {
var b = Buffer.from([0xff, 0xff]);
var p = Parsimmon.Binary.bitSeq([3, 5, 5, 3]);
assert.deepEqual(p.parse(b).value, [7, 31, 31, 7]);
});

it("disallows construction of parsers that don't align to byte boundaries", function() {
assert.throws(function() {
Parsimmon.Binary.bitSeq([1, 2]);
});
});

it("fails if requesting too much", function() {
var b = Buffer.from([]);
var p = Parsimmon.Binary.bitSeq([3, 5, 5, 3]);
assert.deepEqual(p.parse(b).expected, ["2 bytes"]);
});

it("throws an exception for too large of a range request", function() {
assert.throws(function() {
Parsimmon.Binary.bitSeq([1, 2, 4, 49]);
});
});

context("Buffer is not present.", function() {
var buff;
before(function() {
buff = global.Buffer;
global.Buffer = undefined;
});

after(function() {
global.Buffer = buff;
});

it("Disallows construction.", function() {
assert.throws(function() {
Parsimmon.Binary.bitSeq(0xf);
}, /buffer global/i);
});
});
});
Loading

0 comments on commit 6e9b1a5

Please sign in to comment.