Repository: jneen/parsimmon
Pull request #231 (2018-03-10 04:57:51)

Buffer support
TODO: Update changelog


Patch:
diff --git a/API.md b/API.md
index afc491d..1285e1c 100644
--- a/API.md
+++ b/API.md
@@ -421,7 +421,46 @@ var parser =
     notChar('b').times(5)
   );
 parser.parse('accccc');
-//=> {status: true, value: ['a', ['c', 'c', 'c', 'c', 'c']]}
+// => { status: true, value: ['a', ['c', 'c', 'c', 'c', 'c']] }
+```
+
+# Binary constructors
+
+The `Parsimmon.Binary` constructors parse binary content using Node.js Buffers. These constructors can be combined with the normal parser combinators such as `Parsimmon.seq`, `Parsimmon.seqObj`, and still have all the same methods as text-based parsers (e.g. `.map`, `.node`, etc.).
+
+## Parsimmon.byte(int)
+
+Returns a parser that yields a byte (as a number) that matches the given input; similar to `Parsimmon.digit` and `Parsimmon.letter`.
+
+```javascript
+var parser = Parsimmon.Binary.byte(0x3f);
+parser.parse(Buffer.from([0x3f]));
+// => { status: true, value: 63 }
+```
+
+## Parsimmon.bitSeq(alignments)
+
+Parse a series of bits that do not have to be byte-aligned and consume them from a Buffer. The maximum number is 48 since more than 48 bits won't fit safely into a JavaScript number without losing precision. Also, the total of all bits in the sequence must be a multiple of 8 since parsing is still done at the byte level.
+
+```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)
+
+Works like `Parsimmon.bitSeq` except each item in the array is either a number of bits or pair (array with length = 2) of name and bits. The bits are parsed in order and put into an object based on the name supplied. If there's no name for the bits, it will be parsed but discarded from the returned value.
+
+```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 } }
 ```
 
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45b3869..6f28a06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## version 1.7.0 (2018-03-10)
+
+* Adds support for binary parsing using Node.js Buffers
+* Adds `Parsimmon.Binary.bitSeq`
+* Adds `Parsimmon.Binary.bitSeqObj`
+* Adds `Parsimmon.Binary.byte`
+
 ## version 1.6.4 (2018-01-01)
 
 * Fixes `parser.many()` to throw an error if it detects an infinite parse loop.
diff --git a/package-lock.json b/package-lock.json
index 3739411..19672c3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "parsimmon",
-  "version": "1.6.2",
+  "version": "1.6.4",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1686,15 +1686,6 @@
           "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
           "dev": true
         },
-        "string_decoder": {
-          "version": "1.0.3",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
-          "dev": true,
-          "requires": {
-            "safe-buffer": "5.1.1"
-          }
-        },
         "string-width": {
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -1706,6 +1697,15 @@
             "strip-ansi": "3.0.1"
           }
         },
+        "string_decoder": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "5.1.1"
+          }
+        },
         "strip-ansi": {
           "version": "3.0.1",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@@ -5013,14 +5013,6 @@
         "xtend": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
       }
     },
-    "string_decoder": {
-      "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz",
-      "integrity": "sha1-8G9BFXtmTYYGn4S9vcmw2KsoFmc=",
-      "dev": true,
-      "requires": {
-        "buffer-shims": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
-      }
-    },
     "string-width": {
       "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
       "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
@@ -5031,6 +5023,14 @@
         "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
       }
     },
+    "string_decoder": {
+      "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz",
+      "integrity": "sha1-8G9BFXtmTYYGn4S9vcmw2KsoFmc=",
+      "dev": true,
+      "requires": {
+        "buffer-shims": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
+      }
+    },
     "stringstream": {
       "version": "0.0.5",
       "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
diff --git a/package.json b/package.json
index 8a9dd34..0c8dfe7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "parsimmon",
-  "version": "1.6.4",
+  "version": "1.7.0",
   "description": "A monadic LL(infinity) parser combinator library",
   "keywords": ["parsing", "parse", "parsers", "parser combinators"],
   "author": "Jeanine Adkisson <jneen at jneen dot net>",
diff --git a/src/parsimmon.js b/src/parsimmon.js
index e604e19..f3ebafa 100644
--- a/src/parsimmon.js
+++ b/src/parsimmon.js
@@ -9,6 +9,209 @@ 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 seenKeys = {};
+  var totalKeys = 0;
+  var fullAlignments = map(function(item) {
+    if (isArray(item)) {
+      var pair = item;
+      if (pair.length !== 2) {
+        throw new Error(
+          "[" +
+            pair.join(", ") +
+            "] should be length 2, got length " +
+            pair.length
+        );
+      }
+      assertString(pair[0]);
+      assertNumber(pair[1]);
+      if (seenKeys[pair[0]]) {
+        throw new Error("duplicate key in bitSeqObj: " + pair[0]);
+      }
+      seenKeys[pair[0]] = true;
+      totalKeys++;
+      return pair;
+    } else {
+      assertNumber(item);
+      return [null, item];
+    }
+  }, namedAlignments);
+  if (totalKeys < 1) {
+    throw new Error(
+      "bitSeqObj expects at least one named pair, got [" +
+        namedAlignments.join(", ") +
+        "]"
+    );
+  }
+  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) {
@@ -19,6 +222,11 @@ function isArray(x) {
   return {}.toString.call(x) === "[object Array]";
 }
 
+function isBuffer(x) {
+  /* global Buffer */
+  return bufferExists() && Buffer.isBuffer(x);
+}
+
 function makeSuccess(index, value) {
   return {
     status: true,
@@ -60,6 +268,13 @@ function mergeReplies(result, last) {
 }
 
 function makeLineColumnIndex(input, i) {
+  if (isBuffer(input)) {
+    return {
+      offset: i,
+      line: -1,
+      column: -1
+    };
+  }
   var lines = input.slice(0, i).split("\n");
   // Note that unlike the character offset, the line and column offsets are
   // 1-based.
@@ -110,6 +325,13 @@ function assertParser(p) {
   }
 }
 
+function get(input, i) {
+  if (typeof input === "string") {
+    return input.charAt(i);
+  }
+  return input[i];
+}
+
 // TODO[ES5]: Switch to Array.isArray eventually.
 function assertArray(x) {
   if (!isArray(x)) {
@@ -164,6 +386,9 @@ function formatGot(input, error) {
   if (i === input.length) {
     return ", got the end of the input";
   }
+  if (isBuffer(input)) {
+    return " at byte " + index.offset;
+  }
   var prefix = i > 0 ? "'..." : "'";
   var suffix = input.length - i > 12 ? "...'" : "'";
   return (
@@ -217,7 +442,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];
@@ -336,8 +561,10 @@ function sepBy1(parser, separator) {
 // -*- Core Parsing Methods -*-
 
 _.parse = function(input) {
-  if (typeof input !== "string") {
-    throw new Error(".parse must be called with a string as its argument");
+  if (typeof input !== "string" && !isBuffer(input)) {
+    throw new Error(
+      ".parse must be called with a string or Buffer as its argument"
+    );
   }
   var result = this.skip(eof)._(input, 0);
   if (result.status) {
@@ -585,6 +812,29 @@ 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);
+    if (head === b) {
+      return makeSuccess(i + 1, head);
+    } else {
+      return makeFailure(i, expected);
+    }
+  });
+}
+
 function regexp(re, group) {
   assertRegexp(re);
   if (arguments.length >= 2) {
@@ -652,11 +902,11 @@ function notFollowedBy(parser) {
 function test(predicate) {
   assertFunction(predicate);
   return Parsimmon(function(input, i) {
-    var char = input.charAt(i);
+    var char = get(input, i);
     if (i < input.length && predicate(char)) {
       return makeSuccess(i + 1, char);
     } else {
-      return makeFailure(i, "a character matching " + predicate);
+      return makeFailure(i, "a character/byte matching " + predicate);
     }
   });
 }
@@ -689,7 +939,7 @@ function takeWhile(predicate) {
 
   return Parsimmon(function(input, i) {
     var j = i;
-    while (j < input.length && predicate(input.charAt(j))) {
+    while (j < input.length && predicate(get(input, j))) {
       j++;
     }
     return makeSuccess(j, input.slice(i, j));
@@ -738,9 +988,9 @@ var index = Parsimmon(function(input, i) {
 
 var any = Parsimmon(function(input, i) {
   if (i >= input.length) {
-    return makeFailure(i, "any character");
+    return makeFailure(i, "any character/byte");
   }
-  return makeSuccess(i + 1, input.charAt(i));
+  return makeSuccess(i + 1, get(input, i));
 });
 
 var all = Parsimmon(function(input, i) {
@@ -802,4 +1052,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;


Test patch:
diff --git a/test/.eslintrc b/test/.eslintrc
index 6b1adcc..7b7df31 100644
--- a/test/.eslintrc
+++ b/test/.eslintrc
@@ -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
   }
 }
diff --git a/test/core/bitSeq.test.js b/test/core/bitSeq.test.js
new file mode 100644
index 0000000..9949d6a
--- /dev/null
+++ b/test/core/bitSeq.test.js
@@ -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]);
+    }, /add up to 3/);
+  });
+
+  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]);
+    }, /49 bit range/);
+  });
+
+  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);
+    });
+  });
+});
diff --git a/test/core/bitSeqObj.test.js b/test/core/bitSeqObj.test.js
new file mode 100644
index 0000000..0d152fa
--- /dev/null
+++ b/test/core/bitSeqObj.test.js
@@ -0,0 +1,88 @@
+"use strict";
+
+describe("bitSeqObj", function() {
+  it("consumes bits into an object from a buffer", function() {
+    var b = Buffer.from([0xff, 0xff]);
+    var p = Parsimmon.Binary.bitSeqObj([
+      ["a", 3],
+      ["b", 5],
+      ["c", 5],
+      ["d", 3]
+    ]);
+    assert.deepEqual(p.parse(b).value, { a: 7, b: 31, c: 31, d: 7 });
+  });
+
+  it("disallows construction of parsers that don't align to byte boundaries", function() {
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([["a", 1], ["b", 2]]);
+    });
+  });
+
+  it("throws when there are zero keys", function() {
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([1, 7]);
+    }, /expects at least one/i);
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([1, 3, 2, 2]);
+    }, /expects at least one/i);
+  });
+
+  it("throws when there are duplicate keys", function() {
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([["a", 1], ["a", 7]]);
+    }, /duplicate/i);
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([["a", 1], ["b", 2], ["a", 5]]);
+    }, /duplicate/i);
+  });
+
+  it("throws you pass the wrong type of argument", function() {
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([[]]);
+    }, /should be length 2/i);
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([[1, 2, 3]]);
+    }, /should be length 2/i);
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([[1, 1]]);
+    }, /not a string/i);
+    assert.throws(function() {
+      Parsimmon.Binary.bitSeqObj([["a", "a"]]);
+    }, /not a number/i);
+  });
+
+  it("fails if requesting too much", function() {
+    var b = Buffer.from([]);
+    var p = Parsimmon.Binary.bitSeqObj([
+      ["a", 3],
+      ["b", 5],
+      ["c", 5],
+      ["d", 3]
+    ]);
+    assert.deepEqual(p.parse(b).expected, ["2 bytes"]);
+  });
+
+  it("ignores unnamed ranges", function() {
+    var b = Buffer.from([0xff, 0xff]);
+    var p = Parsimmon.Binary.bitSeqObj([["a", 3], 5, ["c", 5], ["d", 3]]);
+    assert.deepEqual(p.parse(b).value, { a: 7, c: 31, d: 7 });
+  });
+
+  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.bitSeqObj(0xf);
+      }, /buffer global/i);
+    });
+  });
+});
diff --git a/test/core/byte.test.js b/test/core/byte.test.js
new file mode 100644
index 0000000..f602abc
--- /dev/null
+++ b/test/core/byte.test.js
@@ -0,0 +1,45 @@
+"use strict";
+
+describe("byte", function() {
+  it("matches a buffer byte", function() {
+    var b = Buffer.from([0xf]);
+    var p = Parsimmon.Binary.byte(0xf);
+    assert.ok(p.parse(b).value);
+  });
+
+  it("formats single digit bytes like 0x0f", function() {
+    var b = Buffer.from([0xa]);
+    var p = Parsimmon.Binary.byte(0xf);
+    assert.deepEqual(p.parse(b).expected, ["0x0f"]);
+  });
+
+  it("formats double digit bytes like 0xff", function() {
+    var b = Buffer.from([0x12]);
+    var p = Parsimmon.Binary.byte(0xff);
+    assert.deepEqual(p.parse(b).expected, ["0xff"]);
+  });
+
+  it("disallows larger values than a byte.", function() {
+    assert.throws(function() {
+      Parsimmon.Binary.byte(0xfff);
+    }, /4095=0xfff/);
+  });
+
+  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.byte(0xf);
+      }, /buffer global/i);
+    });
+  });
+});
diff --git a/test/core/parse.test.js b/test/core/parse.test.js
index d4b72a0..2d95cac 100644
--- a/test/core/parse.test.js
+++ b/test/core/parse.test.js
@@ -23,4 +23,13 @@ suite(".parse", function() {
       Parsimmon.of("kaboom").parse();
     });
   });
+
+  context("The input is a buffer.", function() {
+    it("Formats errors correctly.", function() {
+      var parser = Parsimmon.Binary.byte(0);
+      assert.throws(function() {
+        parser.tryParse(Buffer.from([0xf]));
+      }, /at byte/);
+    });
+  });
 });
diff --git a/test/core/test.test.js b/test/core/test.test.js
index f1e5bbe..490ab2d 100644
--- a/test/core/test.test.js
+++ b/test/core/test.test.js
@@ -4,8 +4,14 @@ test("test", function() {
   var parser = Parsimmon.test(function(ch) {
     return ch !== ".";
   });
+  var highBit = Parsimmon.test(function(ch) {
+    return ch | 128;
+  });
   assert.equal(parser.parse("x").value, "x");
   assert.equal(parser.parse(".").status, false);
+  assert.equal(highBit.parse(Buffer.from([255])).status, true);
+  assert.equal(highBit.parse(Buffer.from([0])).status, true);
+  assert.equal(highBit.parse(Buffer.from([127])).status, true);
   assert.throws(function() {
     Parsimmon.test("not a function");
   });