Repository: jneen/parsimmon
Pull request #264 (2018-06-26 01:38:27)

Binary primitive parsers.
This adds a useful array of primitive buffer parsers.  Reference #261. 


Patch:
diff --git a/.travis.yml b/.travis.yml
index 269b82a..88e6ae9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,4 @@ node_js:
   - "8"
   - "7"
   - "6"
-  - "5"
-  - "4"
 after_success: npm run coverage
diff --git a/API.md b/API.md
index 7ec7497..c1f6380 100644
--- a/API.md
+++ b/API.md
@@ -575,6 +575,206 @@ parser.parse(Buffer.from([0x3f]));
 // => { status: true, value: 63 }
 ```
 
+## Parsimmon.buffer(length)
+
+Returns a parser that will consume some of a buffer and present it as a raw buffer for further transformation.  This buffer is cloned, so in case you use a destructive method, it will not corrupt the original input buffer.
+
+```javaScript
+var parser = Parsimmon.Binary.buffer(2).skip(Parsimmon.any);
+parser.parse(Buffer.from([1, 2, 3]));
+// => { status: true, value: <Buffer 01 02> }
+```
+
+## Parsimmon.encodedString(encoding, length)
+
+Parse `length` bytes, and then decode with a particular `encoding`.
+
+```javascript
+var parser = Parsimmon.Binary.encodedString('utf8', 17);
+parser.parse(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x21, 0x20, 0xf0, 0x9f, 0x98, 0x84]));
+// => { status: true, value: 'hello there! 😄' }
+```
+
+## Parsimmon.uint8
+
+Parse an unsigned integer of 1 byte.
+
+```javascript
+var parser = Parsimmon.Binary.uint8;
+parser.parse(Buffer.from([0xFF]));
+// => { status: true, value: 255 }
+```
+
+## Parsimmon.int8
+
+Parse a signed integer of 1 byte.
+
+```javascript
+var parser = Parsimmon.Binary.int8;
+parser.parse(Buffer.from([0xFF]));
+// => { status: true, value: -1 }
+```
+
+## Parsimmon.uintBE(length)
+
+Parse an unsigned integer (big-endian) of length bytes.  Length cannot exceed 6.
+
+```javascript
+var parser = Parsimmon.Binary.uintBE(4);
+parser.parse(Buffer.from([1, 2, 3, 4]));
+// => { status: true, value: 16909060 }
+```
+
+## Parsimmon.intBE(length)
+
+Parse a signed integer (big-endian) of length bytes.  Length cannot exceed 6.
+
+```javascript
+var parser = Parsimmon.Binary.intBE(4);
+parser.parse(Buffer.from([0xff, 2, 3, 4]));
+// => { status: true, value: -16645372 }
+```
+
+## Parsimmon.uintLE(length)
+
+Parse an unsigned integer (little-endian) of length bytes.  Length cannot exceed 6.
+
+```javascript
+var parser = Parsimmon.Binary.uintLE(4);
+parser.parse(Buffer.from([1, 2, 3, 4]));
+// => { status: true, value: 67305985 }
+```
+
+## Parsimmon.intLE(length)
+
+Parse a signed integer (little-endian) of length bytes.  Length cannot exceed 6.
+
+```javascript
+var parser = Parsimmon.Binary.intLE(4);
+parser.parse(Buffer.from([1, 2, 3, 0xff]));
+// => { status: true, value: -16580095 }
+```
+
+## Parsimmon.uint16BE
+
+Parse an unsigned integer (big-endian) of 2 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.uint16BE;
+parser.parse(Buffer.from([0xFF, 0xFE]));
+// => { status: true, value: 65534 }
+```
+
+## Parsimmon.int16BE
+
+Parse a signed integer (big-endian) of 2 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.int16BE;
+parser.parse(Buffer.from([0xFF, 0xFE]));
+// => { status: true, value: -2 }
+```
+
+## Parsimmon.uint16LE
+
+Parse an unsigned integer (little-endian) of 2 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.uint16LE;
+parser.parse(Buffer.from([0xFE, 0xFF]));
+// => { status: true, value: 65534 }
+```
+
+## Parsimmon.int16LE
+
+Parse a signed integer (little-endian) of 2 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.int16LE;
+parser.parse(Buffer.from([0xFE, 0xFF]));
+// => { status: true, value: -2 }
+```
+
+## Parsimmon.uint32BE
+
+Parse an unsigned integer (big-endian) of 4 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.uint32BE;
+parser.parse(Buffer.from([0x00, 0x00, 0x00, 0xFF]));
+// => { status: true, value: 255 }
+```
+
+## Parsimmon.int32BE
+
+Parse an signed integer (big-endian) of 4 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.int32BE;
+parser.parse(Buffer.from([0xFF, 0xFF, 0xFF, 0xFE]));
+// => { status: true, value: -2 }
+```
+
+## Parsimmon.uint32LE
+
+Parse an unsigned integer (little-endian) of 4 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.uint32LE;
+parser.parse(Buffer.from([0xFF, 0x00, 0x00, 0x00]));
+// => { status: true, value: 255 }
+```
+
+## Parsimmon.int32LE
+
+Parse an signed integer (little-endian) of 4 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.int32LE;
+parser.parse(Buffer.from([0xFE, 0xFF, 0xFF, 0xFF]));
+// => { status: true, value: -2 }
+```
+
+## Parsimmon.floatBE
+
+Parse a float (big-endian) of 4 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.floatBE;
+parser.parse(Buffer.from([1, 2, 3, 4]));
+// => { status: true, value: 2.387939260590663e-38 }
+```
+
+## Parsimmon.floatLE
+
+Parse a float (little-endian) of 4 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.floatLE;
+parser.parse(Buffer.from([1, 2, 3, 4]));
+// => { status: true, value: 1.539989614439558e-36 }
+```
+
+## Parsimmon.doubleBE
+
+Parse a double (big-endian) of 8 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.doubleBE;
+parser.parse(Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]));
+// => { status: true, value: 8.20788039913184e-304 }
+```
+
+## Parsimmon.doubleLE
+
+Parse a double (little-endian) of 8 bytes.
+
+```javascript
+var parser = Parsimmon.Binary.doubleLE;
+parser.parse(Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]));
+// => { status: true, value: 5.447603722011605e-270 }
+```
+
 ## 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.
@@ -1087,7 +1287,7 @@ Takes `parser` which returns a function and applies it to the parsed value of `o
 ```javascript
 Parsimmon.digit
   .ap(Parsimmon.digit
-    .map(s => t => 
+    .map(s => t =>
       Number(s) + Number(t)))
         .parse("23");
 // => {status: true, value: 5}
diff --git a/src/parsimmon.js b/src/parsimmon.js
index 05ec002..e3c46a5 100644
--- a/src/parsimmon.js
+++ b/src/parsimmon.js
@@ -210,6 +210,94 @@ function bitSeqObj(namedAlignments) {
   });
 }
 
+function parseBufferFor(other, length) {
+  ensureBuffer();
+  return new Parsimmon(function(input, i) {
+    if (i + length > input.length) {
+      return makeFailure(i, length + " bytes for " + other);
+    }
+    return makeSuccess(i + length, input.slice(i, i + length));
+  });
+}
+
+function parseBuffer(length) {
+  return parseBufferFor("buffer", length).map(function(unsafe) {
+    return Buffer.from(unsafe);
+  });
+}
+
+function encodedString(encoding, length) {
+  return parseBufferFor("string", length).map(function(buff) {
+    return buff.toString(encoding);
+  });
+}
+
+function isInteger(value) {
+  return typeof value === "number" && Math.floor(value) === value;
+}
+
+function assertValidIntegerByteLengthFor(who, length) {
+  if (!isInteger(length) || length < 0 || length > 6) {
+    throw new Error(who + " requires integer length in range [0, 6].");
+  }
+}
+
+function uintBE(length) {
+  assertValidIntegerByteLengthFor("uintBE", length);
+
+  return parseBufferFor("uintBE(" + length + ")", length).map(function(buff) {
+    return buff.readUIntBE(0, length);
+  });
+}
+
+function uintLE(length) {
+  assertValidIntegerByteLengthFor("uintLE", length);
+
+  return parseBufferFor("uintLE(" + length + ")", length).map(function(buff) {
+    return buff.readUIntLE(0, length);
+  });
+}
+
+function intBE(length) {
+  assertValidIntegerByteLengthFor("intBE", length);
+
+  return parseBufferFor("intBE(" + length + ")", length).map(function(buff) {
+    return buff.readIntBE(0, length);
+  });
+}
+
+function intLE(length) {
+  assertValidIntegerByteLengthFor("intLE", length);
+
+  return parseBufferFor("uintLE(" + length + ")", length).map(function(buff) {
+    return buff.readIntLE(0, length);
+  });
+}
+
+function floatBE() {
+  return parseBufferFor("floatBE", 4).map(function(buff) {
+    return buff.readFloatBE(0);
+  });
+}
+
+function floatLE() {
+  return parseBufferFor("floatLE", 4).map(function(buff) {
+    return buff.readFloatLE(0);
+  });
+}
+
+function doubleBE() {
+  return parseBufferFor("doubleBE", 8).map(function(buff) {
+    return buff.readDoubleBE(0);
+  });
+}
+
+function doubleLE() {
+  return parseBufferFor("doubleLE", 8).map(function(buff) {
+    return buff.readDoubleLE(0);
+  });
+}
+
 function toArray(arrLike) {
   return Array.prototype.slice.call(arrLike);
 }
@@ -513,6 +601,17 @@ function formatGot(input, error) {
   }
 
   var lineWithErrorCurrentIndex = lineWithErrorIndex - lineRange.from;
+
+  if (isBuffer(input)) {
+    lastLineNumberLabelLength = (
+      (lineRange.to > 0 ? lineRange.to - 1 : lineRange.to) * 8
+    ).toString(16).length;
+
+    if (lastLineNumberLabelLength < 2) {
+      lastLineNumberLabelLength = 2;
+    }
+  }
+
   var linesWithLineNumbers = reduce(
     function(acc, lineSource, index) {
       var isLineWithError = index === lineWithErrorCurrentIndex;
@@ -1259,7 +1358,29 @@ Parsimmon["fantasy-land/of"] = succeed;
 Parsimmon.Binary = {
   bitSeq: bitSeq,
   bitSeqObj: bitSeqObj,
-  byte: byte
+  byte: byte,
+  buffer: parseBuffer,
+  encodedString: encodedString,
+  uintBE: uintBE,
+  uint8BE: uintBE(1),
+  uint16BE: uintBE(2),
+  uint32BE: uintBE(4),
+  uintLE: uintLE,
+  uint8LE: uintLE(1),
+  uint16LE: uintLE(2),
+  uint32LE: uintLE(4),
+  intBE: intBE,
+  int8BE: intBE(1),
+  int16BE: intBE(2),
+  int32BE: intBE(4),
+  intLE: intLE,
+  int8LE: intLE(1),
+  int16LE: intLE(2),
+  int32LE: intLE(4),
+  floatBE: floatBE(),
+  floatLE: floatLE(),
+  doubleBE: doubleBE(),
+  doubleLE: doubleLE()
 };
 
 module.exports = Parsimmon;


Test patch:
diff --git a/test/core/buffer.test.js b/test/core/buffer.test.js
new file mode 100644
index 0000000..8d1628f
--- /dev/null
+++ b/test/core/buffer.test.js
@@ -0,0 +1,17 @@
+"use strict";
+
+describe("buffer", function() {
+  it("accepts a sublength of a buffer", function() {
+    var b = Buffer.from([0xf, 0xf, 0xf, 0xf]);
+    var p = Parsimmon.Binary.buffer(3).skip(Parsimmon.any);
+    assert.deepEqual(p.tryParse(b), Buffer.from([0xf, 0xf, 0xf]));
+  });
+
+  it("fails if not enough buffer", function() {
+    var b = Buffer.from([0xf, 0xf]);
+    var p = Parsimmon.Binary.buffer(3);
+    assert.throws(function() {
+      p.tryParse(b);
+    });
+  });
+});
diff --git a/test/core/doubleBE.test.js b/test/core/doubleBE.test.js
new file mode 100644
index 0000000..05dae59
--- /dev/null
+++ b/test/core/doubleBE.test.js
@@ -0,0 +1,9 @@
+"use strict";
+
+describe("doubleBE", function() {
+  it("reads a double, big-endian", function() {
+    var b = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
+    var p = Parsimmon.Binary.doubleBE;
+    assert.deepEqual(p.tryParse(b), 8.20788039913184e-304);
+  });
+});
diff --git a/test/core/doubleLE.test.js b/test/core/doubleLE.test.js
new file mode 100644
index 0000000..01bfc0e
--- /dev/null
+++ b/test/core/doubleLE.test.js
@@ -0,0 +1,9 @@
+"use strict";
+
+describe("doubleLE", function() {
+  it("reads a double, little-endian", function() {
+    var b = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
+    var p = Parsimmon.Binary.doubleLE;
+    assert.deepEqual(p.tryParse(b), 5.447603722011605e-270);
+  });
+});
diff --git a/test/core/encodedString.test.js b/test/core/encodedString.test.js
new file mode 100644
index 0000000..7897ba8
--- /dev/null
+++ b/test/core/encodedString.test.js
@@ -0,0 +1,39 @@
+"use strict";
+
+describe("encodedString", function() {
+  it("treats a sublength of a buffer as a string", function() {
+    var b = Buffer.from("hello world", "ascii");
+    var p = Parsimmon.Binary.encodedString("ascii", 11);
+    assert.deepEqual(p.tryParse(b), "hello world");
+  });
+
+  context("utf8", function() {
+    it("treats a sublength of a buffer as a string", function() {
+      var b = Buffer.from("✓ “this test works!”", "utf8");
+      var p = Parsimmon.Binary.encodedString("utf8", 26);
+      assert.deepEqual(p.tryParse(b), "✓ “this test works!”");
+    });
+
+    it("treats a sublength of a buffer as a string", function() {
+      var b = Buffer.from("hello🔥world", "utf8");
+      var p = Parsimmon.Binary.encodedString("utf8", 14);
+      assert.deepEqual(p.tryParse(b), "hello🔥world");
+    });
+  });
+
+  context("utf16le", function() {
+    it("treats a sublength of a buffer as a string", function() {
+      var b = Buffer.from([0xac, 0x20, 0xac, 0x20, 0xac, 0x20]);
+      var p = Parsimmon.Binary.encodedString("utf16le", 6);
+      assert.deepEqual(p.tryParse(b), "€€€");
+    });
+  });
+
+  context("latin1", function() {
+    it("treats a sublength of a buffer as a string", function() {
+      var b = Buffer.from([0xd2, 0x5f, 0xd3]);
+      var p = Parsimmon.Binary.encodedString("latin1", 3);
+      assert.deepEqual(p.tryParse(b), "Ò_Ó");
+    });
+  });
+});
diff --git a/test/core/floatBE.test.js b/test/core/floatBE.test.js
new file mode 100644
index 0000000..0caa619
--- /dev/null
+++ b/test/core/floatBE.test.js
@@ -0,0 +1,9 @@
+"use strict";
+
+describe("floatBE", function() {
+  it("reads a float, big-endian", function() {
+    var b = Buffer.from([1, 2, 3, 4]);
+    var p = Parsimmon.Binary.floatBE;
+    assert.deepEqual(p.tryParse(b), 2.387939260590663e-38);
+  });
+});
diff --git a/test/core/floatLE.test.js b/test/core/floatLE.test.js
new file mode 100644
index 0000000..74f0b49
--- /dev/null
+++ b/test/core/floatLE.test.js
@@ -0,0 +1,9 @@
+"use strict";
+
+describe("floatLE", function() {
+  it("reads a float, little-endian", function() {
+    var b = Buffer.from([1, 2, 3, 4]);
+    var p = Parsimmon.Binary.floatLE;
+    assert.deepEqual(p.tryParse(b), 1.539989614439558e-36);
+  });
+});
diff --git a/test/core/intBE.test.js b/test/core/intBE.test.js
new file mode 100644
index 0000000..78d7cc3
--- /dev/null
+++ b/test/core/intBE.test.js
@@ -0,0 +1,45 @@
+"use strict";
+
+describe("intBE family", function() {
+  context("intBE", function() {
+    it("reads an int, unsigned, big-endian of specific length", function() {
+      var b = Buffer.from([0xff, 0xff]);
+      var p = Parsimmon.Binary.intBE(2);
+      assert.deepEqual(p.tryParse(b), -1);
+    });
+
+    it("disallows bad lengths", function() {
+      assert.throws(function() {
+        return Parsimmon.Binary.intBE(-1);
+      });
+
+      assert.throws(function() {
+        return Parsimmon.Binary.intBE(7);
+      });
+    });
+  });
+
+  context("int8", function() {
+    it("reads an int, signed of 1 byte", function() {
+      var b = Buffer.from([0xff, 0xff]);
+      var p = Parsimmon.Binary.int8BE.skip(Parsimmon.any);
+      assert.deepEqual(p.tryParse(b), -1);
+    });
+  });
+
+  context("int16BE", function() {
+    it("reads an int, signed, big-endian of 2 bytes", function() {
+      var b = Buffer.from([0xff, 0xff]);
+      var p = Parsimmon.Binary.int16BE;
+      assert.deepEqual(p.tryParse(b), -1);
+    });
+  });
+
+  context("int32BE", function() {
+    it("reads an int, signed, big-endian of 4 bytes", function() {
+      var b = Buffer.from([0xff, 0xff, 0xff, 0xff]);
+      var p = Parsimmon.Binary.int32BE;
+      assert.deepEqual(p.tryParse(b), -1);
+    });
+  });
+});
diff --git a/test/core/intLE.test.js b/test/core/intLE.test.js
new file mode 100644
index 0000000..444922a
--- /dev/null
+++ b/test/core/intLE.test.js
@@ -0,0 +1,45 @@
+"use strict";
+
+describe("intLE family", function() {
+  context("intLE", function() {
+    it("reads an int, unsigned, big-endian of specific length", function() {
+      var b = Buffer.from([0xfe, 0xff]);
+      var p = Parsimmon.Binary.intLE(2);
+      assert.deepEqual(p.tryParse(b), -2);
+    });
+
+    it("disallows bad lengths", function() {
+      assert.throws(function() {
+        return Parsimmon.Binary.intLE(-1);
+      });
+
+      assert.throws(function() {
+        return Parsimmon.Binary.intLE(7);
+      });
+    });
+  });
+
+  context("int8", function() {
+    it("reads an int, signed of 1 byte", function() {
+      var b = Buffer.from([0xfe, 0xff]);
+      var p = Parsimmon.Binary.int8LE.skip(Parsimmon.any);
+      assert.deepEqual(p.tryParse(b), -2);
+    });
+  });
+
+  context("int16BE", function() {
+    it("reads an int, signed, big-endian of 2 bytes", function() {
+      var b = Buffer.from([0xfd, 0xff]);
+      var p = Parsimmon.Binary.int16LE;
+      assert.deepEqual(p.tryParse(b), -3);
+    });
+  });
+
+  context("int32BE", function() {
+    it("reads an int, signed, big-endian of 4 bytes", function() {
+      var b = Buffer.from([0xfc, 0xff, 0xff, 0xff]);
+      var p = Parsimmon.Binary.int32LE;
+      assert.deepEqual(p.tryParse(b), -4);
+    });
+  });
+});
diff --git a/test/core/uintBE.test.js b/test/core/uintBE.test.js
new file mode 100644
index 0000000..6238840
--- /dev/null
+++ b/test/core/uintBE.test.js
@@ -0,0 +1,45 @@
+"use strict";
+
+describe("uintBE family", function() {
+  context("uintBE", function() {
+    it("reads an int, unsigned, big-endian of specific length", function() {
+      var b = Buffer.from([0xbe, 0xef]);
+      var p = Parsimmon.Binary.uintBE(2);
+      assert.deepEqual(p.tryParse(b), 0xbeef);
+    });
+
+    it("disallows bad lengths", function() {
+      assert.throws(function() {
+        return Parsimmon.Binary.uintBE(-1);
+      });
+
+      assert.throws(function() {
+        return Parsimmon.Binary.uintBE(7);
+      });
+    });
+  });
+
+  context("uint8", function() {
+    it("reads an int, unsigned of 1 byte", function() {
+      var b = Buffer.from([0xff, 0xff]);
+      var p = Parsimmon.Binary.uint8BE.skip(Parsimmon.any);
+      assert.deepEqual(p.tryParse(b), 0xff);
+    });
+  });
+
+  context("uint16BE", function() {
+    it("reads an int, unsigned, big-endian of 2 bytes", function() {
+      var b = Buffer.from([0xbe, 0xef]);
+      var p = Parsimmon.Binary.uint16BE;
+      assert.deepEqual(p.tryParse(b), 0xbeef);
+    });
+  });
+
+  context("uint32BE", function() {
+    it("reads an int, unsigned, big-endian of 4 bytes", function() {
+      var b = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
+      var p = Parsimmon.Binary.uint32BE;
+      assert.deepEqual(p.tryParse(b), 0xdeadbeef);
+    });
+  });
+});
diff --git a/test/core/uintLE.test.js b/test/core/uintLE.test.js
new file mode 100644
index 0000000..b733553
--- /dev/null
+++ b/test/core/uintLE.test.js
@@ -0,0 +1,37 @@
+"use strict";
+
+describe("uintLE family", function() {
+  context("uintLE", function() {
+    it("reads an int, unsigned, little-endian of specific length", function() {
+      var b = Buffer.from([0xef, 0xbe]);
+      var p = Parsimmon.Binary.uintLE(2);
+      assert.deepEqual(p.tryParse(b), 0xbeef);
+    });
+
+    it("disallows bad lengths", function() {
+      assert.throws(function() {
+        return Parsimmon.Binary.uintLE(-1);
+      });
+
+      assert.throws(function() {
+        return Parsimmon.Binary.uintLE(7);
+      });
+    });
+  });
+
+  context("uint16LE", function() {
+    it("reads an int, unsigned, little-endian of 2 bytes", function() {
+      var b = Buffer.from([0xef, 0xbe]);
+      var p = Parsimmon.Binary.uint16LE;
+      assert.deepEqual(p.tryParse(b), 0xbeef);
+    });
+  });
+
+  context("uint32LE", function() {
+    it("reads an int, unsigned, little-endian of 4 bytes", function() {
+      var b = Buffer.from([0xef, 0xbe, 0xad, 0xde]);
+      var p = Parsimmon.Binary.uint32LE;
+      assert.deepEqual(p.tryParse(b), 0xdeadbeef);
+    });
+  });
+});