@ldapjs/asn1
Advanced tools
Comparing version 1.2.0 to 2.0.0-rc.1
@@ -13,5 +13,5 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
Reader: Reader, | ||
Reader, | ||
Writer: Writer | ||
Writer | ||
@@ -18,0 +18,0 @@ } |
@@ -1,221 +0,435 @@ | ||
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
'use strict' | ||
const assert = require('assert') | ||
const ASN1 = require('./types') | ||
const errors = require('./errors') | ||
const types = require('./types') | ||
// --- Globals | ||
/** | ||
* Given a buffer of ASN.1 data encoded according to Basic Encoding Rules (BER), | ||
* the reader provides methods for iterating that data and decoding it into | ||
* regular JavaScript types. | ||
*/ | ||
class BerReader { | ||
/** | ||
* The source buffer as it was passed in when creating the instance. | ||
* | ||
* @type {Buffer} | ||
*/ | ||
#buffer | ||
const newInvalidAsn1Error = errors.newInvalidAsn1Error | ||
/** | ||
* The total bytes in the backing buffer. | ||
* | ||
* @type {number} | ||
*/ | ||
#size | ||
// --- API | ||
/** | ||
* An ASN.1 field consists of a tag, a length, and a value. This property | ||
* records the length of the current field. | ||
* | ||
* @type {number} | ||
*/ | ||
#currentFieldLength = 0 | ||
function Reader (data) { | ||
if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data must be a node Buffer') } | ||
/** | ||
* Records the offset in the buffer where the most recent {@link readSequence} | ||
* was invoked. This is used to facilitate slicing of whole sequences from | ||
* the buffer as a new {@link BerReader} instance. | ||
* | ||
* @type {number} | ||
*/ | ||
#currentSequenceStart = 0 | ||
this._buf = data | ||
this._size = data.length | ||
/** | ||
* As the BER buffer is read, this property records the current position | ||
* in the buffer. | ||
* | ||
* @type {number} | ||
*/ | ||
#offset = 0 | ||
// These hold the "current" state | ||
this._len = 0 | ||
this._offset = 0 | ||
} | ||
/** | ||
* @param {Buffer} buffer | ||
*/ | ||
constructor (buffer) { | ||
if (Buffer.isBuffer(buffer) === false) { | ||
throw TypeError('Must supply a Buffer instance to read.') | ||
} | ||
Object.defineProperty(Reader.prototype, Symbol.toStringTag, { value: 'BerReader' }) | ||
this.#buffer = buffer.subarray(0) | ||
this.#size = this.#buffer.length | ||
} | ||
Object.defineProperty(Reader.prototype, 'length', { | ||
enumerable: true, | ||
get: function () { return (this._len) } | ||
}) | ||
get [Symbol.toStringTag] () { return 'BerReader' } | ||
Object.defineProperty(Reader.prototype, 'offset', { | ||
enumerable: true, | ||
get: function () { return (this._offset) } | ||
}) | ||
/** | ||
* Get a buffer that represents the underlying data buffer. | ||
* | ||
* @type {Buffer} | ||
*/ | ||
get buffer () { | ||
return this.#buffer.subarray(0) | ||
} | ||
Object.defineProperty(Reader.prototype, 'remain', { | ||
get: function () { return (this._size - this._offset) } | ||
}) | ||
/** | ||
* The length of the current field being read. | ||
* | ||
* @type {number} | ||
*/ | ||
get length () { | ||
return this.#currentFieldLength | ||
} | ||
Object.defineProperty(Reader.prototype, 'buffer', { | ||
get: function () { return (this._buf.slice(this._offset)) } | ||
}) | ||
/** | ||
* Current read position in the underlying data buffer. | ||
* | ||
* @type {number} | ||
*/ | ||
get offset () { | ||
return this.#offset | ||
} | ||
/** | ||
* Reads a single byte and advances offset; you can pass in `true` to make this | ||
* a "peek" operation (i.e., get the byte, but don't advance the offset). | ||
* | ||
* @param {Boolean} peek true means don't move offset. | ||
* @return {Number} the next byte, null if not enough data. | ||
*/ | ||
Reader.prototype.readByte = function (peek) { | ||
if (this._size - this._offset < 1) { return null } | ||
/** | ||
* The number of bytes remaining in the backing buffer that have not | ||
* been read. | ||
* | ||
* @type {number} | ||
*/ | ||
get remain () { | ||
return this.#size - this.#offset | ||
} | ||
const b = this._buf[this._offset] & 0xff | ||
/** | ||
* Read the next byte in the buffer without advancing the offset. | ||
* | ||
* @return {number | null} The next byte or null if not enough data. | ||
*/ | ||
peek () { | ||
return this.readByte(true) | ||
} | ||
if (!peek) { this._offset += 1 } | ||
/** | ||
* Reads a boolean from the current offset and advances the offset. | ||
* | ||
* @param {number} [tag] The tag number that is expected to be read. | ||
* | ||
* @returns {boolean} True if the tag value represents `true`, otherwise | ||
* `false`. | ||
* | ||
* @throws When there is an error reading the tag. | ||
*/ | ||
readBoolean (tag = types.Boolean) { | ||
const intBuffer = this.readTag(tag) | ||
this.#offset += intBuffer.length | ||
const int = parseIntegerBuffer(intBuffer) | ||
return b | ||
} | ||
return (int !== 0) | ||
} | ||
Reader.prototype.peek = function () { | ||
return this.readByte(true) | ||
} | ||
/** | ||
* Reads a single byte and advances offset; you can pass in `true` to make | ||
* this a "peek" operation (i.e. get the byte, but don't advance the offset). | ||
* | ||
* @param {boolean} [peek=false] `true` means don't move the offset. | ||
* @returns {number | null} The next byte, `null` if not enough data. | ||
*/ | ||
readByte (peek = false) { | ||
if (this.#size - this.#offset < 1) { | ||
return null | ||
} | ||
/** | ||
* Reads a (potentially) variable length off the BER buffer. This call is | ||
* not really meant to be called directly, as callers have to manipulate | ||
* the internal buffer afterwards. | ||
* | ||
* As a result of this call, you can call `Reader.length`, until the | ||
* next thing called that does a readLength. | ||
* | ||
* @return {Number} the amount of offset to advance the buffer. | ||
* @throws {InvalidAsn1Error} on bad ASN.1 | ||
*/ | ||
Reader.prototype.readLength = function (offset) { | ||
if (offset === undefined) { offset = this._offset } | ||
const byte = this.#buffer[this.#offset] & 0xff | ||
if (offset >= this._size) { return null } | ||
if (peek !== true) { | ||
this.#offset += 1 | ||
} | ||
let lenB = this._buf[offset++] & 0xff | ||
if (lenB === null) { return null } | ||
return byte | ||
} | ||
if ((lenB & 0x80) === 0x80) { | ||
lenB &= 0x7f | ||
/** | ||
* Reads an enumeration (integer) from the current offset and advances the | ||
* offset. | ||
* | ||
* @returns {number} The integer represented by the next sequence of bytes | ||
* in the buffer from the current offset. The current offset must be at a | ||
* byte who's value is equal to the ASN.1 enumeration tag. | ||
* | ||
* @throws When there is an error reading the tag. | ||
*/ | ||
readEnumeration () { | ||
const intBuffer = this.readTag(types.Enumeration) | ||
this.#offset += intBuffer.length | ||
if (lenB === 0) { throw newInvalidAsn1Error('Indefinite length not supported') } | ||
return parseIntegerBuffer(intBuffer) | ||
} | ||
if (lenB > 4) { throw newInvalidAsn1Error('encoding too long') } | ||
/** | ||
* Reads an integer from the current offset and advances the offset. | ||
* | ||
* @param {number} [tag] The tag number that is expected to be read. | ||
* | ||
* @returns {number} The integer represented by the next sequence of bytes | ||
* in the buffer from the current offset. The current offset must be at a | ||
* byte who's value is equal to the ASN.1 integer tag. | ||
* | ||
* @throws When there is an error reading the tag. | ||
*/ | ||
readInt (tag = types.Integer) { | ||
const intBuffer = this.readTag(tag) | ||
this.#offset += intBuffer.length | ||
if (this._size - offset < lenB) { return null } | ||
this._len = 0 | ||
for (let i = 0; i < lenB; i++) { this._len = (this._len << 8) + (this._buf[offset++] & 0xff) } | ||
} else { | ||
// Wasn't a variable length | ||
this._len = lenB | ||
return parseIntegerBuffer(intBuffer) | ||
} | ||
return offset | ||
} | ||
/** | ||
* Reads a length value from the BER buffer at the given offset. This | ||
* method is not really meant to be called directly, as callers have to | ||
* manipulate the internal buffer afterwards. | ||
* | ||
* This method does not advance the reader offset. | ||
* | ||
* As a result of this method, the `.length` property can be read for the | ||
* current field until another method invokes `readLength`. | ||
* | ||
* Note: we only support up to 4 bytes to describe the length of a value. | ||
* | ||
* @param {number} [offset] Read a length value starting at the specified | ||
* position in the underlying buffer. | ||
* | ||
* @return {number | null} The position the buffer should be advanced to in | ||
* order for the reader to be at the start of the value for the field. See | ||
* {@link setOffset}. If the offset, or length, exceeds the size of the | ||
* underlying buffer, `null` will be returned. | ||
* | ||
* @throws When an unsupported length value is encountered. | ||
*/ | ||
readLength (offset) { | ||
if (offset === undefined) { offset = this.#offset } | ||
/** | ||
* Parses the next sequence in this BER buffer. | ||
* | ||
* To get the length of the sequence, call `Reader.length`. | ||
* | ||
* @return {Number} the sequence's tag. | ||
*/ | ||
Reader.prototype.readSequence = function (tag) { | ||
const seq = this.peek() | ||
if (seq === null) { return null } | ||
if (tag !== undefined && tag !== seq) { | ||
throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + | ||
': got 0x' + seq.toString(16)) | ||
} | ||
if (offset >= this.#size) { return null } | ||
const o = this.readLength(this._offset + 1) // stored in `length` | ||
if (o === null) { return null } | ||
let lengthByte = this.#buffer[offset++] & 0xff | ||
// TODO: we are commenting this out because it seems to be unreachable. | ||
// It is not clear to me how we can ever check `lenB === null` as `null` | ||
// is a primitive type, and seemingly cannot be represented by a byte. | ||
// If we find that removal of this line does not affect the larger suite | ||
// of ldapjs tests, we should just completely remove it from the code. | ||
/* if (lenB === null) { return null } */ | ||
this._offset = o | ||
return seq | ||
} | ||
if ((lengthByte & 0x80) === 0x80) { | ||
lengthByte &= 0x7f | ||
Reader.prototype.readInt = function () { | ||
return this._readTag(ASN1.Integer) | ||
} | ||
// https://www.rfc-editor.org/rfc/rfc4511.html#section-5.1 prohibits | ||
// indefinite form (0x80). | ||
if (lengthByte === 0) { throw Error('Indefinite length not supported.') } | ||
Reader.prototype.readBoolean = function (tag) { | ||
return (this._readTag(tag || ASN1.Boolean) !== 0) | ||
} | ||
// We only support up to 4 bytes to describe encoding length. So the only | ||
// valid indicators are 0x81, 0x82, 0x83, and 0x84. | ||
if (lengthByte > 4) { throw Error('Encoding too long.') } | ||
Reader.prototype.readEnumeration = function () { | ||
return this._readTag(ASN1.Enumeration) | ||
} | ||
if (this.#size - offset < lengthByte) { return null } | ||
Reader.prototype.readString = function (tag, retbuf) { | ||
if (!tag) { tag = ASN1.OctetString } | ||
this.#currentFieldLength = 0 | ||
for (let i = 0; i < lengthByte; i++) { | ||
this.#currentFieldLength = (this.#currentFieldLength << 8) + | ||
(this.#buffer[offset++] & 0xff) | ||
} | ||
} else { | ||
// Wasn't a variable length | ||
this.#currentFieldLength = lengthByte | ||
} | ||
const b = this.peek() | ||
if (b === null) { return null } | ||
if (b !== tag) { | ||
throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + | ||
': got 0x' + b.toString(16)) | ||
return offset | ||
} | ||
const o = this.readLength(this._offset + 1) // stored in `length` | ||
/** | ||
* At the current offset, read the next tag, length, and value as an | ||
* object identifier (OID) and return the OID string. | ||
* | ||
* @param {number} [tag] The tag number that is expected to be read. | ||
* | ||
* @returns {string | null} Will return `null` if the buffer is an invalid | ||
* length. Otherwise, returns the OID as a string. | ||
*/ | ||
readOID (tag = types.OID) { | ||
// See https://web.archive.org/web/20221008202056/https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN | ||
const oidBuffer = this.readString(tag, true) | ||
if (oidBuffer === null) { return null } | ||
if (o === null) { return null } | ||
const values = [] | ||
let value = 0 | ||
if (this.length > this._size - o) { return null } | ||
for (let i = 0; i < oidBuffer.length; i++) { | ||
const byte = oidBuffer[i] & 0xff | ||
this._offset = o | ||
value <<= 7 | ||
value += byte & 0x7f | ||
if ((byte & 0x80) === 0) { | ||
values.push(value) | ||
value = 0 | ||
} | ||
} | ||
if (this.length === 0) { return retbuf ? Buffer.alloc(0) : '' } | ||
value = values.shift() | ||
values.unshift(value % 40) | ||
values.unshift((value / 40) >> 0) | ||
const str = this._buf.slice(this._offset, this._offset + this.length) | ||
this._offset += this.length | ||
return values.join('.') | ||
} | ||
return retbuf ? str : str.toString('utf8') | ||
} | ||
/** | ||
* At the current buffer offset, read the next tag as a sequence tag, and | ||
* advance the offset to the position of the tag of the first item in the | ||
* sequence. | ||
* | ||
* @param {number} [tag] The tag number that is expected to be read. | ||
* | ||
* @returns {number} The read sequence tag value. Should match the function | ||
* input parameter value. | ||
* | ||
* @throws If the `tag` does not match or if there is an error reading | ||
* the length of the sequence. | ||
*/ | ||
readSequence (tag) { | ||
const foundTag = this.peek() | ||
if (tag !== undefined && tag !== foundTag) { | ||
const expected = tag.toString(16).padStart(2, '0') | ||
const found = foundTag.toString(16).padStart(2, '0') | ||
throw Error(`Expected 0x${expected}: got 0x${found}`) | ||
} | ||
Reader.prototype.readOID = function (tag) { | ||
if (!tag) { tag = ASN1.OID } | ||
this.#currentSequenceStart = this.#offset | ||
const valueOffset = this.readLength(this.#offset + 1) // stored in `length` | ||
if (valueOffset === null) { return null } | ||
const b = this.readString(tag, true) | ||
if (b === null) { return null } | ||
this.#offset = valueOffset | ||
return foundTag | ||
} | ||
const values = [] | ||
let value = 0 | ||
/** | ||
* At the current buffer offset, read the next value as a string and advance | ||
* the offset. | ||
* | ||
* @param {number} [tag] The tag number that is expected to be read. Should | ||
* be `ASN1.String`. | ||
* @param {boolean} [asBuffer=false] When true, the raw buffer will be | ||
* returned. Otherwise a native string. | ||
* | ||
* @returns {string | Buffer | null} Will return `null` if the buffer is | ||
* malformed. | ||
* | ||
* @throws If there is a problem reading the length. | ||
*/ | ||
readString (tag = types.OctetString, asBuffer = false) { | ||
const tagByte = this.peek() | ||
for (let i = 0; i < b.length; i++) { | ||
const byte = b[i] & 0xff | ||
if (tagByte !== tag) { | ||
const expected = tag.toString(16).padStart(2, '0') | ||
const found = tagByte.toString(16).padStart(2, '0') | ||
throw Error(`Expected 0x${expected}: got 0x${found}`) | ||
} | ||
value <<= 7 | ||
value += byte & 0x7f | ||
if ((byte & 0x80) === 0) { | ||
values.push(value) | ||
value = 0 | ||
} | ||
const valueOffset = this.readLength(this.#offset + 1) // stored in `length` | ||
if (valueOffset === null) { return null } | ||
if (this.length > this.#size - valueOffset) { return null } | ||
this.#offset = valueOffset | ||
if (this.length === 0) { return asBuffer ? Buffer.alloc(0) : '' } | ||
const str = this.#buffer.subarray(this.#offset, this.#offset + this.length) | ||
this.#offset += this.length | ||
return asBuffer ? str : str.toString('utf8') | ||
} | ||
value = values.shift() | ||
values.unshift(value % 40) | ||
values.unshift((value / 40) >> 0) | ||
/** | ||
* At the current buffer offset, read the next set of bytes represented | ||
* by the given tag, and return the resulting buffer. For example, if the | ||
* BER represents a sequence with a string "foo", i.e. | ||
* `[0x30, 0x05, 0x04, 0x03, 0x66, 0x6f, 0x6f]`, and the current offset is | ||
* `0`, then the result of `readTag(0x30)` is the buffer | ||
* `[0x04, 0x03, 0x66, 0x6f, 0x6f]`. | ||
* | ||
* @param {number} tag The tag number that is expected to be read. | ||
* | ||
* @returns {Buffer | null} The buffer representing the tag value, or null if | ||
* the buffer is in some way malformed. | ||
* | ||
* @throws When there is an error interpreting the buffer, or the buffer | ||
* is not formed correctly. | ||
*/ | ||
readTag (tag) { | ||
if (tag == null) { | ||
throw Error('Must supply an ASN.1 tag to read.') | ||
} | ||
return values.join('.') | ||
} | ||
const byte = this.peek() | ||
if (byte !== tag) { | ||
const tagString = tag.toString(16).padStart(2, '0') | ||
const byteString = byte.toString(16).padStart(2, '0') | ||
throw Error(`Expected 0x${tagString}: got 0x${byteString}`) | ||
} | ||
Reader.prototype._readTag = function (tag) { | ||
assert.ok(tag !== undefined) | ||
const fieldOffset = this.readLength(this.#offset + 1) // stored in `length` | ||
if (fieldOffset === null) { return null } | ||
const b = this.peek() | ||
if (this.length > this.#size - fieldOffset) { return null } | ||
this.#offset = fieldOffset | ||
if (b === null) { return null } | ||
return this.#buffer.subarray(this.#offset, this.#offset + this.length) | ||
} | ||
if (b !== tag) { | ||
throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + | ||
': got 0x' + b.toString(16)) | ||
/** | ||
* Returns the current sequence as a new {@link BerReader} instance. This | ||
* method relies on {@link readSequence} having been invoked first. If it has | ||
* not been invoked, the returned reader will represent an undefined portion | ||
* of the underlying buffer. | ||
* | ||
* @returns {BerReader} | ||
*/ | ||
sequenceToReader () { | ||
// Represents the number of bytes that constitute the "length" portion | ||
// of the TLV tuple. | ||
const lengthValueLength = this.#offset - this.#currentSequenceStart | ||
const buffer = this.#buffer.subarray( | ||
this.#currentSequenceStart, | ||
this.#currentSequenceStart + (lengthValueLength + this.#currentFieldLength) | ||
) | ||
return new BerReader(buffer) | ||
} | ||
const o = this.readLength(this._offset + 1) // stored in `length` | ||
if (o === null) { return null } | ||
/** | ||
* Set the internal offset to a given position in the underlying buffer. | ||
* This method is to support manual advancement of the reader. | ||
* | ||
* @param {number} position | ||
* | ||
* @throws If the given `position` is not an integer. | ||
*/ | ||
setOffset (position) { | ||
if (Number.isInteger(position) === false) { | ||
throw Error('Must supply an integer position.') | ||
} | ||
this.#offset = position | ||
} | ||
} | ||
if (this.length > 4) { throw newInvalidAsn1Error('Integer too long: ' + this.length) } | ||
if (this.length > this._size - o) { return null } | ||
this._offset = o | ||
const fb = this._buf[this._offset] | ||
/** | ||
* Given a buffer that represents an ingeter TLV, parse it and return it | ||
* as a decimal value. This accounts for signedness. | ||
* | ||
* @param {Buffer} | ||
* | ||
* @returns {number} | ||
*/ | ||
function parseIntegerBuffer (integerBuffer) { | ||
let value = 0 | ||
let i | ||
for (i = 0; i < this.length; i++) { | ||
for (i = 0; i < integerBuffer.length; i++) { | ||
value <<= 8 | ||
value |= (this._buf[this._offset++] & 0xff) | ||
value |= (integerBuffer[i] & 0xff) | ||
} | ||
if ((fb & 0x80) === 0x80 && i !== 4) { value -= (1 << (i * 8)) } | ||
if ((integerBuffer[0] & 0x80) === 0x80 && i !== 4) { value -= (1 << (i * 8)) } | ||
@@ -225,4 +439,2 @@ return value >> 0 | ||
// --- Exported API | ||
module.exports = Reader | ||
module.exports = BerReader |
@@ -1,137 +0,513 @@ | ||
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
'use strict' | ||
const { test } = require('tap') | ||
const tap = require('tap') | ||
const BerReader = require('./reader') | ||
test('load library', function (t) { | ||
t.ok(BerReader) | ||
try { | ||
const reader = new BerReader() | ||
t.equal(reader, null, 'reader') | ||
t.fail('Should have thrown') | ||
} catch (e) { | ||
t.ok(e instanceof TypeError, 'Should have been a type error') | ||
} | ||
t.end() | ||
// A sequence (0x30), 5 bytes (0x05) long, which consists of | ||
// a string (0x04), 3 bytes (0x03) long, representing "foo". | ||
const fooSequence = [0x30, 0x05, 0x04, 0x03, 0x66, 0x6f, 0x6f] | ||
// ClientID certificate request example from | ||
// https://web.archive.org/web/20221008202056/https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN | ||
const microsoftOID = [ | ||
0x06, 0x09, // OID; 9 bytes | ||
0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x15, 0x14, // 1.3.6.1.4.1.311.21.20 | ||
0x31, 0x4a, // Set; 4 bytes | ||
0x30, 0x48, // Sequence; 48 bytes | ||
0x02, 0x01, 0x09, // Integer; 1 bytes; 9 | ||
0x0c, 0x23, // UTF8 String; 23 bytes | ||
0x76, 0x69, 0x63, 0x68, 0x33, 0x64, 0x2e, 0x6a, // vich3d.j | ||
0x64, 0x64, 0x6d, 0x63, 0x73, 0x63, 0x23, 0x6e, // domcsc.n | ||
0x74, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x6d, 0x69, // ttest.mi | ||
0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x23, // crosoft. | ||
0x63, 0x64, 0x6d, // com | ||
0x0c, 0x15, // UTF8 String; 15 bytes | ||
0x4a, 0x44, 0x4f, 0x4d, 0x43, 0x53, 0x43, 0x5c, // JDOMCSC\ | ||
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x69, 0x73, 0x74, // administ | ||
0x72, 0x61, 0x74, 0x6f, 0x72, // rator | ||
0x0c, 0x07, // UTF8 String; 7 bytes | ||
0x63, 0x65, 0x72, 0x74, 0x72, 0x65, 0x71 // certreq | ||
] | ||
tap.test('must supply a buffer', async t => { | ||
const expected = TypeError('Must supply a Buffer instance to read.') | ||
t.throws( | ||
() => new BerReader(), | ||
expected | ||
) | ||
t.throws( | ||
() => new BerReader(''), | ||
expected | ||
) | ||
}) | ||
test('read byte', function (t) { | ||
const reader = new BerReader(Buffer.from([0xde])) | ||
t.ok(reader) | ||
t.equal(reader.readByte(), 0xde, 'wrong value') | ||
t.end() | ||
tap.test('has toStringTag', async t => { | ||
const reader = new BerReader(Buffer.from('foo')) | ||
t.equal(Object.prototype.toString.call(reader), '[object BerReader]') | ||
}) | ||
test('read 1 byte int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x01, 0x03])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), 0x03, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
t.end() | ||
tap.test('buffer property returns buffer', async t => { | ||
const fooBuffer = Buffer.from(fooSequence) | ||
const reader = new BerReader(fooBuffer) | ||
t.equal( | ||
fooBuffer.compare(reader.buffer), | ||
0 | ||
) | ||
}) | ||
test('read 2 byte int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x02, 0x7e, 0xde])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), 0x7ede, 'wrong value') | ||
t.equal(reader.length, 0x02, 'wrong length') | ||
tap.test('peek reads but does not advance', async t => { | ||
const reader = new BerReader(Buffer.from([0xde])) | ||
const byte = reader.peek() | ||
t.equal(byte, 0xde) | ||
t.equal(reader.offset, 0) | ||
}) | ||
tap.test('readBoolean', t => { | ||
t.test('read boolean true', async t => { | ||
const reader = new BerReader(Buffer.from([0x01, 0x01, 0xff])) | ||
t.equal(reader.readBoolean(), true, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
}) | ||
t.test('read boolean false', async t => { | ||
const reader = new BerReader(Buffer.from([0x01, 0x01, 0x00])) | ||
t.equal(reader.readBoolean(), false, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
}) | ||
t.end() | ||
}) | ||
test('read 3 byte int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x03, 0x7e, 0xde, 0x03])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), 0x7ede03, 'wrong value') | ||
t.equal(reader.length, 0x03, 'wrong length') | ||
tap.test('readByte', t => { | ||
t.test('reads a byte and advances offset', async t => { | ||
const reader = new BerReader(Buffer.from([0xde])) | ||
t.equal(reader.offset, 0) | ||
t.equal(reader.readByte(), 0xde) | ||
t.equal(reader.offset, 1) | ||
}) | ||
t.test('returns null if buffer exceeded', async t => { | ||
const reader = new BerReader(Buffer.from([0xde])) | ||
reader.readByte() | ||
t.equal(reader.readByte(), null) | ||
}) | ||
t.test('peek does not advance offset', async t => { | ||
const reader = new BerReader(Buffer.from([0xde])) | ||
const byte = reader.readByte(true) | ||
t.equal(byte, 0xde) | ||
t.equal(reader.offset, 0) | ||
}) | ||
t.end() | ||
}) | ||
test('read 4 byte int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x04, 0x7e, 0xde, 0x03, 0x01])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), 0x7ede0301, 'wrong value') | ||
t.equal(reader.length, 0x04, 'wrong length') | ||
tap.test('readEnumeration', t => { | ||
t.test('read enumeration', async t => { | ||
const reader = new BerReader(Buffer.from([0x0a, 0x01, 0x20])) | ||
t.equal(reader.readEnumeration(), 0x20, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
}) | ||
t.end() | ||
}) | ||
test('read 1 byte negative int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x01, 0xdc])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), -36, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
tap.test('readInt', t => { | ||
t.test('read 1 byte int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x01, 0x03])) | ||
t.equal(reader.readInt(), 0x03, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
}) | ||
t.test('read 2 byte int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x02, 0x7e, 0xde])) | ||
t.equal(reader.readInt(), 0x7ede, 'wrong value') | ||
t.equal(reader.length, 0x02, 'wrong length') | ||
}) | ||
t.test('read 3 byte int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x03, 0x7e, 0xde, 0x03])) | ||
t.equal(reader.readInt(), 0x7ede03, 'wrong value') | ||
t.equal(reader.length, 0x03, 'wrong length') | ||
}) | ||
t.test('read 4 byte int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x04, 0x7e, 0xde, 0x03, 0x01])) | ||
t.equal(reader.readInt(), 0x7ede0301, 'wrong value') | ||
t.equal(reader.length, 0x04, 'wrong length') | ||
}) | ||
t.test('read 1 byte negative int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x01, 0xdc])) | ||
t.equal(reader.readInt(), -36, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
}) | ||
t.test('read 2 byte negative int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x02, 0xc0, 0x4e])) | ||
t.equal(reader.readInt(), -16306, 'wrong value') | ||
t.equal(reader.length, 0x02, 'wrong length') | ||
}) | ||
t.test('read 3 byte negative int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x03, 0xff, 0x00, 0x19])) | ||
t.equal(reader.readInt(), -65511, 'wrong value') | ||
t.equal(reader.length, 0x03, 'wrong length') | ||
}) | ||
t.test('read 4 byte negative int', async t => { | ||
const reader = new BerReader(Buffer.from([0x02, 0x04, 0x91, 0x7c, 0x22, 0x1f])) | ||
t.equal(reader.readInt(), -1854135777, 'wrong value') | ||
t.equal(reader.length, 0x04, 'wrong length') | ||
}) | ||
t.test('read 4 byte negative int (abandon request tag)', async t => { | ||
// Technically, an abandon request shouldn't ever have a negative | ||
// number, but this lets us test the feature completely. | ||
const reader = new BerReader(Buffer.from([0x80, 0x04, 0x91, 0x7c, 0x22, 0x1f])) | ||
t.equal(reader.readInt(0x80), -1854135777, 'wrong value') | ||
t.equal(reader.length, 0x04, 'wrong length') | ||
}) | ||
t.test('correctly advances offset', async t => { | ||
const reader = new BerReader(Buffer.from([ | ||
0x30, 0x06, // sequence; 6 bytes | ||
0x02, 0x04, 0x91, 0x7c, 0x22, 0x1f // integer; 4 bytes | ||
])) | ||
const seqBuffer = reader.readTag(0x30) | ||
t.equal( | ||
Buffer.compare( | ||
seqBuffer, | ||
Buffer.from([0x02, 0x04, 0x91, 0x7c, 0x22, 0x1f] | ||
) | ||
), | ||
0 | ||
) | ||
t.equal(reader.readInt(), -1854135777, 'wrong value') | ||
t.equal(reader.length, 0x04, 'wrong length') | ||
t.equal(reader.offset, 8) | ||
}) | ||
t.end() | ||
}) | ||
test('read 2 byte negative int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x02, 0xc0, 0x4e])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), -16306, 'wrong value') | ||
t.equal(reader.length, 0x02, 'wrong length') | ||
tap.test('readLength', t => { | ||
t.test('reads from specified offset', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
const offset = reader.readLength(1) | ||
t.equal(offset, 2) | ||
t.equal(reader.length, 5) | ||
}) | ||
t.test('returns null if offset exceeds buffer', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
const offset = reader.readLength(10) | ||
t.equal(offset, null) | ||
t.equal(reader.offset, 0) | ||
}) | ||
t.test('reads from current offset', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
const byte = reader.readByte() | ||
t.equal(byte, 0x30) | ||
const offset = reader.readLength() | ||
t.equal(offset, 2) | ||
t.equal(reader.length, 5) | ||
}) | ||
t.test('throws for indefinite length', async t => { | ||
// Buffer would indicate a string of indefinite length. | ||
const reader = new BerReader(Buffer.from([0x04, 0x80])) | ||
t.throws( | ||
() => reader.readLength(1), | ||
Error('Indefinite length not supported.') | ||
) | ||
}) | ||
t.test('throws if length too long', async t => { | ||
// Buffer would indicate a string who's length should be indicated | ||
// by the next 5 bytes (omitted). | ||
const reader = new BerReader(Buffer.from([0x04, 0x85])) | ||
t.throws( | ||
() => reader.readLength(1), | ||
Error('Encoding too long.') | ||
) | ||
}) | ||
t.test('reads a long (integer) from length', async t => { | ||
const reader = new BerReader(Buffer.from([0x81, 0x94])) | ||
const offset = reader.readLength() | ||
t.equal(offset, 2) | ||
t.equal(reader.length, 148) | ||
}) | ||
t.test( | ||
'returns null if long (integer) from length exceeds buffer', | ||
async t => { | ||
const reader = new BerReader(Buffer.from([0x82, 0x03])) | ||
const offset = reader.readLength(0) | ||
t.equal(offset, null) | ||
t.equal(reader.length, 0) | ||
}) | ||
t.end() | ||
}) | ||
test('read 3 byte negative int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x03, 0xff, 0x00, 0x19])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), -65511, 'wrong value') | ||
t.equal(reader.length, 0x03, 'wrong length') | ||
tap.test('readOID', t => { | ||
t.test('returns null for bad buffer', async t => { | ||
const reader = new BerReader(Buffer.from([0x06, 0x03, 0x0a])) | ||
t.equal(reader.readOID(), null) | ||
}) | ||
t.test('reads an OID', async t => { | ||
const input = Buffer.from(microsoftOID.slice(0, 11)) | ||
const reader = new BerReader(input) | ||
t.equal(reader.readOID(), '1.3.6.1.4.1.311.21.20') | ||
}) | ||
t.end() | ||
}) | ||
test('read 4 byte negative int', function (t) { | ||
const reader = new BerReader(Buffer.from([0x02, 0x04, 0x91, 0x7c, 0x22, 0x1f])) | ||
t.ok(reader) | ||
t.equal(reader.readInt(), -1854135777, 'wrong value') | ||
t.equal(reader.length, 0x04, 'wrong length') | ||
tap.test('readSequence', t => { | ||
t.test('throws for tag mismatch', async t => { | ||
const reader = new BerReader(Buffer.from([0x04, 0x00])) | ||
t.throws( | ||
() => reader.readSequence(0x30), | ||
Error('Expected 0x30: got 0x04') | ||
) | ||
}) | ||
t.test('returns null when read length is null', async t => { | ||
const reader = new BerReader(Buffer.from([0x30, 0x84, 0x04, 0x03])) | ||
t.equal(reader.readSequence(), null) | ||
}) | ||
t.test('return read sequence and advances offset', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
const result = reader.readSequence() | ||
t.equal(result, 0x30) | ||
t.equal(reader.offset, 2) | ||
}) | ||
// Original test | ||
t.test('read sequence', async t => { | ||
const reader = new BerReader(Buffer.from([0x30, 0x03, 0x01, 0x01, 0xff])) | ||
t.ok(reader) | ||
t.equal(reader.readSequence(), 0x30, 'wrong value') | ||
t.equal(reader.length, 0x03, 'wrong length') | ||
t.equal(reader.readBoolean(), true, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
}) | ||
t.end() | ||
}) | ||
test('read boolean true', function (t) { | ||
const reader = new BerReader(Buffer.from([0x01, 0x01, 0xff])) | ||
t.ok(reader) | ||
t.equal(reader.readBoolean(), true, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
tap.test('readString', t => { | ||
t.test('throws for tag mismatch', async t => { | ||
const reader = new BerReader(Buffer.from([0x30, 0x00])) | ||
t.throws( | ||
() => reader.readString(), | ||
Error('Expected 0x04: got 0x30') | ||
) | ||
}) | ||
t.test('returns null when read length is null', async t => { | ||
const reader = new BerReader(Buffer.from([0x04, 0x84, 0x03, 0x0a])) | ||
t.equal(reader.readString(), null) | ||
}) | ||
t.test('returns null when value bytes too short', async t => { | ||
const reader = new BerReader(Buffer.from([0x04, 0x03, 0x0a])) | ||
t.equal(reader.readString(), null) | ||
}) | ||
t.test('returns empty buffer for zero length string', async t => { | ||
const reader = new BerReader(Buffer.from([0x04, 0x00])) | ||
const result = reader.readString(0x04, true) | ||
t.type(result, Buffer) | ||
t.equal(Buffer.compare(result, Buffer.alloc(0)), 0) | ||
}) | ||
t.test('returns empty string for zero length string', async t => { | ||
const reader = new BerReader(Buffer.from([0x04, 0x00])) | ||
const result = reader.readString() | ||
t.type(result, 'string') | ||
t.equal(result, '') | ||
}) | ||
t.test('returns string as buffer', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence.slice(2))) | ||
const result = reader.readString(0x04, true) | ||
t.type(result, Buffer) | ||
const expected = Buffer.from(fooSequence.slice(4)) | ||
t.equal(Buffer.compare(result, expected), 0) | ||
}) | ||
t.test('returns string as string', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence.slice(2))) | ||
const result = reader.readString() | ||
t.type(result, 'string') | ||
t.equal(result, 'foo') | ||
}) | ||
// Original test | ||
t.test('read string', async t => { | ||
const dn = 'cn=foo,ou=unit,o=test' | ||
const buf = Buffer.alloc(dn.length + 2) | ||
buf[0] = 0x04 | ||
buf[1] = Buffer.byteLength(dn) | ||
buf.write(dn, 2) | ||
const reader = new BerReader(buf) | ||
t.ok(reader) | ||
t.equal(reader.readString(), dn, 'wrong value') | ||
t.equal(reader.length, dn.length, 'wrong length') | ||
}) | ||
// Orignal test | ||
t.test('long string', async t => { | ||
const buf = Buffer.alloc(256) | ||
const s = | ||
'2;649;CN=Red Hat CS 71GA Demo,O=Red Hat CS 71GA Demo,C=US;' + | ||
'CN=RHCS Agent - admin01,UID=admin01,O=redhat,C=US [1] This is ' + | ||
'Teena Vradmin\'s description.' | ||
buf[0] = 0x04 | ||
buf[1] = 0x81 | ||
buf[2] = 0x94 | ||
buf.write(s, 3) | ||
const ber = new BerReader(buf.subarray(0, 3 + s.length)) | ||
t.equal(ber.readString(), s) | ||
}) | ||
t.end() | ||
}) | ||
test('read boolean false', function (t) { | ||
const reader = new BerReader(Buffer.from([0x01, 0x01, 0x00])) | ||
t.ok(reader) | ||
t.equal(reader.readBoolean(), false, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
tap.test('readTag', t => { | ||
t.test('throws error for null tag', async t => { | ||
const expected = Error('Must supply an ASN.1 tag to read.') | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
t.throws( | ||
() => reader.readTag(), | ||
expected | ||
) | ||
}) | ||
t.test('returns null for null byte tag', { skip: true }) | ||
t.test('throws error for tag mismatch', async t => { | ||
const expected = Error('Expected 0x40: got 0x30') | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
t.throws( | ||
() => reader.readTag(0x40), | ||
expected | ||
) | ||
}) | ||
t.test('returns null if field length is null', async t => { | ||
const reader = new BerReader(Buffer.from([0x05])) | ||
t.equal(reader.readTag(0x05), null) | ||
}) | ||
t.test('returns null if field length greater than available bytes', async t => { | ||
const reader = new BerReader(Buffer.from([0x30, 0x03, 0x04, 0xa0])) | ||
t.equal(reader.readTag(0x30), null) | ||
}) | ||
t.test('returns null if field length greater than available bytes', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
const expected = Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]) | ||
const result = reader.readTag(0x30) | ||
t.equal(Buffer.compare(result, expected), 0) | ||
}) | ||
t.end() | ||
}) | ||
test('read enumeration', function (t) { | ||
const reader = new BerReader(Buffer.from([0x0a, 0x01, 0x20])) | ||
t.ok(reader) | ||
t.equal(reader.readEnumeration(), 0x20, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
tap.test('remain', t => { | ||
t.test('returns the size of the buffer if nothing read', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
t.equal(7, reader.remain) | ||
}) | ||
t.test('returns accurate remaining bytes', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
t.equal(0x30, reader.readSequence()) | ||
t.equal(5, reader.remain) | ||
}) | ||
t.end() | ||
}) | ||
test('read string', function (t) { | ||
const dn = 'cn=foo,ou=unit,o=test' | ||
const buf = Buffer.alloc(dn.length + 2) | ||
buf[0] = 0x04 | ||
buf[1] = Buffer.byteLength(dn) | ||
buf.write(dn, 2) | ||
const reader = new BerReader(buf) | ||
t.ok(reader) | ||
t.equal(reader.readString(), dn, 'wrong value') | ||
t.equal(reader.length, dn.length, 'wrong length') | ||
tap.test('setOffset', t => { | ||
t.test('throws if not an integer', async t => { | ||
const expected = Error('Must supply an integer position.') | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
t.throws( | ||
() => reader.setOffset(1.2), | ||
expected | ||
) | ||
t.throws( | ||
() => reader.setOffset('2'), | ||
expected | ||
) | ||
}) | ||
t.test('sets offset', async t => { | ||
const reader = new BerReader(Buffer.from(fooSequence)) | ||
t.equal(reader.offset, 0) | ||
reader.setOffset(2) | ||
t.equal(reader.offset, 2) | ||
t.equal(reader.peek(), 0x04) | ||
}) | ||
t.end() | ||
}) | ||
test('read sequence', function (t) { | ||
const reader = new BerReader(Buffer.from([0x30, 0x03, 0x01, 0x01, 0xff])) | ||
t.ok(reader) | ||
t.equal(reader.readSequence(), 0x30, 'wrong value') | ||
t.equal(reader.length, 0x03, 'wrong length') | ||
t.equal(reader.readBoolean(), true, 'wrong value') | ||
t.equal(reader.length, 0x01, 'wrong length') | ||
tap.test('sequenceToReader', t => { | ||
t.test('returns new reader with full sequence', async t => { | ||
const multiSequence = [ | ||
0x30, 14, | ||
...fooSequence, | ||
...fooSequence | ||
] | ||
const reader = new BerReader(Buffer.from(multiSequence)) | ||
// Read the intial sequence and verify current position. | ||
t.equal(0x30, reader.readSequence()) | ||
t.equal(2, reader.offset) | ||
// Advance the buffer to the start of the first sub-sequence value. | ||
t.equal(0x30, reader.readSequence()) | ||
t.equal(4, reader.offset) | ||
t.equal(12, reader.remain) | ||
// Get a new reader the consists of the first sub-sequence and verify | ||
// that the original reader's position has not changed. | ||
const fooReader = reader.sequenceToReader() | ||
t.equal(fooReader.remain, 7) | ||
t.equal(fooReader.offset, 0) | ||
t.equal(reader.offset, 4) | ||
t.equal(0x30, fooReader.readSequence()) | ||
t.equal('foo', fooReader.readString()) | ||
// The original reader should advance like normal. | ||
t.equal('foo', reader.readString()) | ||
t.equal(0x30, reader.readSequence()) | ||
t.equal('foo', reader.readString()) | ||
t.equal(0, reader.remain) | ||
t.equal(16, reader.offset) | ||
}) | ||
t.end() | ||
}) | ||
test('anonymous LDAPv3 bind', function (t) { | ||
// Original test | ||
tap.test('anonymous LDAPv3 bind', async t => { | ||
const BIND = Buffer.alloc(14) | ||
@@ -166,18 +542,2 @@ BIND[0] = 0x30 // Sequence | ||
t.equal(null, ber.readByte(), 'Should be out of data') | ||
t.end() | ||
}) | ||
test('long string', function (t) { | ||
const buf = Buffer.alloc(256) | ||
const s = | ||
'2;649;CN=Red Hat CS 71GA Demo,O=Red Hat CS 71GA Demo,C=US;' + | ||
'CN=RHCS Agent - admin01,UID=admin01,O=redhat,C=US [1] This is ' + | ||
'Teena Vradmin\'s description.' | ||
buf[0] = 0x04 | ||
buf[1] = 0x81 | ||
buf[2] = 0x94 | ||
buf.write(s, 3) | ||
const ber = new BerReader(buf.slice(0, 3 + s.length)) | ||
t.equal(ber.readString(), s) | ||
t.end() | ||
}) |
@@ -1,35 +0,36 @@ | ||
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
'use strict' | ||
module.exports = { | ||
EOC: 0, | ||
Boolean: 1, | ||
Integer: 2, | ||
BitString: 3, | ||
OctetString: 4, | ||
Null: 5, | ||
OID: 6, | ||
ObjectDescriptor: 7, | ||
External: 8, | ||
Real: 9, // float | ||
Enumeration: 10, | ||
PDV: 11, | ||
Utf8String: 12, | ||
RelativeOID: 13, | ||
Sequence: 16, | ||
Set: 17, | ||
NumericString: 18, | ||
PrintableString: 19, | ||
T61String: 20, | ||
VideotexString: 21, | ||
IA5String: 22, | ||
UTCTime: 23, | ||
GeneralizedTime: 24, | ||
GraphicString: 25, | ||
VisibleString: 26, | ||
GeneralString: 28, | ||
UniversalString: 29, | ||
CharacterString: 30, | ||
BMPString: 31, | ||
Constructor: 32, | ||
Context: 128 | ||
EOC: 0x0, | ||
Boolean: 0x01, | ||
Integer: 0x02, | ||
BitString: 0x03, | ||
OctetString: 0x04, | ||
Null: 0x05, | ||
OID: 0x06, | ||
ObjectDescriptor: 0x07, | ||
External: 0x08, | ||
Real: 0x09, // float | ||
Enumeration: 0x0a, | ||
PDV: 0x0b, | ||
Utf8String: 0x0c, | ||
RelativeOID: 0x0d, | ||
Sequence: 0x10, | ||
Set: 0x11, | ||
NumericString: 0x12, | ||
PrintableString: 0x13, | ||
T61String: 0x14, | ||
VideotexString: 0x15, | ||
IA5String: 0x16, | ||
UTCTime: 0x17, | ||
GeneralizedTime: 0x18, | ||
GraphicString: 0x19, | ||
VisibleString: 0x1a, | ||
GeneralString: 0x1c, | ||
UniversalString: 0x1d, | ||
CharacterString: 0x1e, | ||
BMPString: 0x1f, | ||
Constructor: 0x20, | ||
LDAPSequence: 0x30, | ||
Context: 0x80 | ||
} |
@@ -1,298 +0,453 @@ | ||
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
'use strict' | ||
const assert = require('assert') | ||
const ASN1 = require('./types') | ||
const errors = require('./errors') | ||
const types = require('./types') | ||
// --- Globals | ||
class BerWriter { | ||
/** | ||
* The source buffer as it was passed in when creating the instance. | ||
* | ||
* @type {Buffer} | ||
*/ | ||
#buffer | ||
const newInvalidAsn1Error = errors.newInvalidAsn1Error | ||
/** | ||
* The total bytes in the backing buffer. | ||
* | ||
* @type {number} | ||
*/ | ||
#size | ||
const DEFAULT_OPTS = { | ||
size: 1024, | ||
growthFactor: 8 | ||
} | ||
/** | ||
* As the BER buffer is written, this property records the current position | ||
* in the buffer. | ||
* | ||
* @type {number} | ||
*/ | ||
#offset = 0 | ||
// --- Helpers | ||
/** | ||
* A list of offsets in the buffer where we need to insert sequence tag and | ||
* length pairs. | ||
*/ | ||
#sequenceOffsets = [] | ||
function merge (from, to) { | ||
assert.ok(from) | ||
assert.equal(typeof (from), 'object') | ||
assert.ok(to) | ||
assert.equal(typeof (to), 'object') | ||
/** | ||
* Coeffecient used when increasing the buffer to accomodate writes that | ||
* exceed the available space left in the buffer. | ||
* | ||
* @type {number} | ||
*/ | ||
#growthFactor | ||
const keys = Object.getOwnPropertyNames(from) | ||
keys.forEach(function (key) { | ||
if (to[key]) { return } | ||
constructor ({ size = 1024, growthFactor = 8 } = {}) { | ||
this.#buffer = Buffer.alloc(size) | ||
this.#size = this.#buffer.length | ||
this.#offset = 0 | ||
this.#growthFactor = growthFactor | ||
} | ||
const value = Object.getOwnPropertyDescriptor(from, key) | ||
Object.defineProperty(to, key, value) | ||
}) | ||
get [Symbol.toStringTag] () { return 'BerWriter' } | ||
return to | ||
} | ||
get buffer () { | ||
// TODO: handle sequence check | ||
// --- API | ||
return this.#buffer.subarray(0, this.#offset) | ||
} | ||
function Writer (options) { | ||
options = merge(DEFAULT_OPTS, options || {}) | ||
/** | ||
* The size of the backing buffer. | ||
* | ||
* @return {number} | ||
*/ | ||
get size () { | ||
return this.#size | ||
} | ||
this._buf = Buffer.alloc(options.size || 1024) | ||
this._size = this._buf.length | ||
this._offset = 0 | ||
this._options = options | ||
/** | ||
* Append a raw buffer to the current writer instance. No validation to | ||
* determine if the buffer represents a valid BER encoding is performed. | ||
* | ||
* @param {Buffer} buffer The buffer to append. If this is not a valid BER | ||
* sequence of data, it will invalidate the BER represented by the `BerWriter`. | ||
* | ||
* @throws If the input is not an instance of Buffer. | ||
*/ | ||
appendBuffer (buffer) { | ||
if (Buffer.isBuffer(buffer) === false) { | ||
throw Error('buffer must be an instance of Buffer') | ||
} | ||
this.#ensureBufferCapacity(buffer.length) | ||
buffer.copy(this.#buffer, this.#offset, 0, buffer.length) | ||
this.#offset += buffer.length | ||
} | ||
// A list of offsets in the buffer where we need to insert | ||
// sequence tag/len pairs. | ||
this._seq = [] | ||
} | ||
/** | ||
* Complete a sequence started with {@link startSequence}. | ||
* | ||
* @throws When the sequence is too long and would exceed the 4 byte | ||
* length descriptor limitation. | ||
*/ | ||
endSequence () { | ||
const sequenceStartOffset = this.#sequenceOffsets.pop() | ||
const start = sequenceStartOffset + 3 | ||
const length = this.#offset - start | ||
Object.defineProperty(Writer.prototype, Symbol.toStringTag, { value: 'BerWriter' }) | ||
if (length <= 0x7f) { | ||
this.#shift(start, length, -2) | ||
this.#buffer[sequenceStartOffset] = length | ||
} else if (length <= 0xff) { | ||
this.#shift(start, length, -1) | ||
this.#buffer[sequenceStartOffset] = 0x81 | ||
this.#buffer[sequenceStartOffset + 1] = length | ||
} else if (length <= 0xffff) { | ||
this.#buffer[sequenceStartOffset] = 0x82 | ||
this.#buffer[sequenceStartOffset + 1] = length >> 8 | ||
this.#buffer[sequenceStartOffset + 2] = length | ||
} else if (length <= 0xffffff) { | ||
this.#shift(start, length, 1) | ||
this.#buffer[sequenceStartOffset] = 0x83 | ||
this.#buffer[sequenceStartOffset + 1] = length >> 16 | ||
this.#buffer[sequenceStartOffset + 2] = length >> 8 | ||
this.#buffer[sequenceStartOffset + 3] = length | ||
} else { | ||
throw Error('sequence too long') | ||
} | ||
} | ||
Object.defineProperty(Writer.prototype, 'buffer', { | ||
get: function () { | ||
if (this._seq.length) { throw newInvalidAsn1Error(this._seq.length + ' unended sequence(s)') } | ||
/** | ||
* Write a sequence tag to the buffer and advance the offset to the starting | ||
* position of the value. Sequences must be completed with a subsequent | ||
* invocation of {@link endSequence}. | ||
* | ||
* @param {number} [tag=0x30] The tag to use for the sequence. | ||
* | ||
* @throws When the tag is not a number. | ||
*/ | ||
startSequence (tag = (types.Sequence | types.Constructor)) { | ||
if (typeof tag !== 'number') { | ||
throw TypeError('tag must be a Number') | ||
} | ||
return (this._buf.slice(0, this._offset)) | ||
this.writeByte(tag) | ||
this.#sequenceOffsets.push(this.#offset) | ||
this.#ensureBufferCapacity(3) | ||
this.#offset += 3 | ||
} | ||
}) | ||
/** | ||
* Append a raw buffer to the current writer instance. No validation to | ||
* determine if the buffer represents a valid BER encoding is performed. | ||
* | ||
* @param {Buffer} buffer The buffer to append. If this is not a valid BER | ||
* sequence of data, it will invalidate the BER represented by the `BerWriter`. | ||
* | ||
* @throws If the input is not an instance of Buffer. | ||
*/ | ||
Writer.prototype.appendBuffer = function appendBuffer (buffer) { | ||
if (Buffer.isBuffer(buffer) === false) { | ||
throw Error('buffer must be an instance of Buffer') | ||
/** | ||
* Write a boolean TLV to the buffer. | ||
* | ||
* @param {boolean} boolValue | ||
* @param {tag} [number=0x01] A custom tag for the boolean. | ||
* | ||
* @throws When a parameter is of the wrong type. | ||
*/ | ||
writeBoolean (boolValue, tag = types.Boolean) { | ||
if (typeof boolValue !== 'boolean') { | ||
throw TypeError('boolValue must be a Boolean') | ||
} | ||
if (typeof tag !== 'number') { | ||
throw TypeError('tag must be a Number') | ||
} | ||
this.#ensureBufferCapacity(3) | ||
this.#buffer[this.#offset++] = tag | ||
this.#buffer[this.#offset++] = 0x01 | ||
this.#buffer[this.#offset++] = boolValue === true ? 0xff : 0x00 | ||
} | ||
for (const b of buffer.values()) { | ||
this.writeByte(b) | ||
} | ||
} | ||
Writer.prototype.writeByte = function (b) { | ||
if (typeof (b) !== 'number') { throw new TypeError('argument must be a Number') } | ||
/** | ||
* Write an arbitrary buffer of data to the backing buffer using the given | ||
* tag. | ||
* | ||
* @param {Buffer} buffer | ||
* @param {number} tag The tag to use for the ASN.1 TLV sequence. | ||
* | ||
* @throws When either input parameter is of the wrong type. | ||
*/ | ||
writeBuffer (buffer, tag) { | ||
if (typeof tag !== 'number') { | ||
throw TypeError('tag must be a Number') | ||
} | ||
if (Buffer.isBuffer(buffer) === false) { | ||
throw TypeError('buffer must be an instance of Buffer') | ||
} | ||
this._ensure(1) | ||
this._buf[this._offset++] = b | ||
} | ||
this.writeByte(tag) | ||
this.writeLength(buffer.length) | ||
this.#ensureBufferCapacity(buffer.length) | ||
buffer.copy(this.#buffer, this.#offset, 0, buffer.length) | ||
this.#offset += buffer.length | ||
} | ||
Writer.prototype.writeInt = function (i, tag) { | ||
if (typeof (i) !== 'number') { throw new TypeError('argument must be a Number') } | ||
if (typeof (tag) !== 'number') { tag = ASN1.Integer } | ||
/** | ||
* Write a single byte to the backing buffer and advance the offset. The | ||
* backing buffer will be automatically expanded to accomodate the new byte | ||
* if no room in the buffer remains. | ||
* | ||
* @param {number} byte The byte to be written. | ||
* | ||
* @throws When the passed in parameter is not a `Number` (aka a byte). | ||
*/ | ||
writeByte (byte) { | ||
if (typeof byte !== 'number') { | ||
throw TypeError('argument must be a Number') | ||
} | ||
let sz = 4 | ||
while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && | ||
(sz > 1)) { | ||
sz-- | ||
i <<= 8 | ||
this.#ensureBufferCapacity(1) | ||
this.#buffer[this.#offset++] = byte | ||
} | ||
if (sz > 4) { throw newInvalidAsn1Error('BER ints cannot be > 0xffffffff') } | ||
this._ensure(2 + sz) | ||
this._buf[this._offset++] = tag | ||
this._buf[this._offset++] = sz | ||
while (sz-- > 0) { | ||
this._buf[this._offset++] = ((i & 0xff000000) >>> 24) | ||
i <<= 8 | ||
/** | ||
* Write an enumeration TLV to the buffer. | ||
* | ||
* @param {number} value | ||
* @param {number} [tag=0x0a] A custom tag for the enumeration. | ||
* | ||
* @throws When a passed in parameter is not of the correct type, or the | ||
* value requires too many bytes (must be <= 4). | ||
*/ | ||
writeEnumeration (value, tag = types.Enumeration) { | ||
if (typeof value !== 'number') { | ||
throw TypeError('value must be a Number') | ||
} | ||
if (typeof tag !== 'number') { | ||
throw TypeError('tag must be a Number') | ||
} | ||
this.writeInt(value, tag) | ||
} | ||
} | ||
Writer.prototype.writeNull = function () { | ||
this.writeByte(ASN1.Null) | ||
this.writeByte(0x00) | ||
} | ||
/** | ||
* Write an, up to 4 byte, integer TLV to the buffer. | ||
* | ||
* @param {number} intToWrite | ||
* @param {number} [tag=0x02] | ||
* | ||
* @throws When either parameter is not of the write type, or if the | ||
* integer consists of too many bytes. | ||
*/ | ||
writeInt (intToWrite, tag = types.Integer) { | ||
if (typeof intToWrite !== 'number') { | ||
throw TypeError('intToWrite must be a Number') | ||
} | ||
if (typeof tag !== 'number') { | ||
throw TypeError('tag must be a Number') | ||
} | ||
Writer.prototype.writeEnumeration = function (i, tag) { | ||
if (typeof (i) !== 'number') { throw new TypeError('argument must be a Number') } | ||
if (typeof (tag) !== 'number') { tag = ASN1.Enumeration } | ||
let intSize = 4 | ||
while ( | ||
( | ||
((intToWrite & 0xff800000) === 0) || | ||
((intToWrite & 0xff800000) === (0xff800000 >> 0)) | ||
) && (intSize > 1) | ||
) { | ||
intSize-- | ||
intToWrite <<= 8 | ||
} | ||
return this.writeInt(i, tag) | ||
} | ||
// TODO: figure out how to cover this in a test. | ||
/* istanbul ignore if: needs test */ | ||
if (intSize > 4) { | ||
throw Error('BER ints cannot be > 0xffffffff') | ||
} | ||
Writer.prototype.writeBoolean = function (b, tag) { | ||
if (typeof (b) !== 'boolean') { throw new TypeError('argument must be a Boolean') } | ||
if (typeof (tag) !== 'number') { tag = ASN1.Boolean } | ||
this.#ensureBufferCapacity(2 + intSize) | ||
this.#buffer[this.#offset++] = tag | ||
this.#buffer[this.#offset++] = intSize | ||
this._ensure(3) | ||
this._buf[this._offset++] = tag | ||
this._buf[this._offset++] = 0x01 | ||
this._buf[this._offset++] = b ? 0xff : 0x00 | ||
} | ||
Writer.prototype.writeString = function (s, tag) { | ||
if (typeof (s) !== 'string') { throw new TypeError('argument must be a string (was: ' + typeof (s) + ')') } | ||
if (typeof (tag) !== 'number') { tag = ASN1.OctetString } | ||
const len = Buffer.byteLength(s) | ||
this.writeByte(tag) | ||
this.writeLength(len) | ||
if (len) { | ||
this._ensure(len) | ||
this._buf.write(s, this._offset) | ||
this._offset += len | ||
while (intSize-- > 0) { | ||
this.#buffer[this.#offset++] = ((intToWrite & 0xff000000) >>> 24) | ||
intToWrite <<= 8 | ||
} | ||
} | ||
} | ||
Writer.prototype.writeBuffer = function (buf, tag) { | ||
if (typeof (tag) !== 'number') { throw new TypeError('tag must be a number') } | ||
if (!Buffer.isBuffer(buf)) { throw new TypeError('argument must be a buffer') } | ||
/** | ||
* Write a set of length bytes to the backing buffer. Per | ||
* https://www.rfc-editor.org/rfc/rfc4511.html#section-5.1, LDAP message | ||
* BERs prohibit greater than 4 byte lengths. Given we are supporing | ||
* the `ldapjs` module, we limit ourselves to 4 byte lengths. | ||
* | ||
* @param {number} len The length value to write to the buffer. | ||
* | ||
* @throws When the length is not a number or requires too many bytes. | ||
*/ | ||
writeLength (len) { | ||
if (typeof len !== 'number') { | ||
throw TypeError('argument must be a Number') | ||
} | ||
this.writeByte(tag) | ||
this.writeLength(buf.length) | ||
this._ensure(buf.length) | ||
buf.copy(this._buf, this._offset, 0, buf.length) | ||
this._offset += buf.length | ||
} | ||
this.#ensureBufferCapacity(4) | ||
Writer.prototype.writeStringArray = function (strings) { | ||
if (Array.isArray(strings) === false) { throw new TypeError('argument must be an Array[String]') } | ||
const self = this | ||
strings.forEach(function (s) { | ||
self.writeString(s) | ||
}) | ||
} | ||
// This is really to solve DER cases, but whatever for now | ||
Writer.prototype.writeOID = function (s, tag) { | ||
if (typeof (s) !== 'string') { throw new TypeError('argument must be a string') } | ||
if (typeof (tag) !== 'number') { tag = ASN1.OID } | ||
if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) { throw new Error('argument is not a valid OID string') } | ||
function encodeOctet (bytes, octet) { | ||
if (octet < 128) { | ||
bytes.push(octet) | ||
} else if (octet < 16384) { | ||
bytes.push((octet >>> 7) | 0x80) | ||
bytes.push(octet & 0x7F) | ||
} else if (octet < 2097152) { | ||
bytes.push((octet >>> 14) | 0x80) | ||
bytes.push(((octet >>> 7) | 0x80) & 0xFF) | ||
bytes.push(octet & 0x7F) | ||
} else if (octet < 268435456) { | ||
bytes.push((octet >>> 21) | 0x80) | ||
bytes.push(((octet >>> 14) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 7) | 0x80) & 0xFF) | ||
bytes.push(octet & 0x7F) | ||
if (len <= 0x7f) { | ||
this.#buffer[this.#offset++] = len | ||
} else if (len <= 0xff) { | ||
this.#buffer[this.#offset++] = 0x81 | ||
this.#buffer[this.#offset++] = len | ||
} else if (len <= 0xffff) { | ||
this.#buffer[this.#offset++] = 0x82 | ||
this.#buffer[this.#offset++] = len >> 8 | ||
this.#buffer[this.#offset++] = len | ||
} else if (len <= 0xffffff) { | ||
this.#buffer[this.#offset++] = 0x83 | ||
this.#buffer[this.#offset++] = len >> 16 | ||
this.#buffer[this.#offset++] = len >> 8 | ||
this.#buffer[this.#offset++] = len | ||
} else { | ||
bytes.push(((octet >>> 28) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 21) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 14) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 7) | 0x80) & 0xFF) | ||
bytes.push(octet & 0x7F) | ||
throw Error('length too long (> 4 bytes)') | ||
} | ||
} | ||
const tmp = s.split('.') | ||
const bytes = [] | ||
bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)) | ||
tmp.slice(2).forEach(function (b) { | ||
encodeOctet(bytes, parseInt(b, 10)) | ||
}) | ||
/** | ||
* Write a NULL tag and value to the buffer. | ||
*/ | ||
writeNull () { | ||
this.writeByte(types.Null) | ||
this.writeByte(0x00) | ||
} | ||
const self = this | ||
this._ensure(2 + bytes.length) | ||
this.writeByte(tag) | ||
this.writeLength(bytes.length) | ||
bytes.forEach(function (b) { | ||
self.writeByte(b) | ||
}) | ||
} | ||
/** | ||
* Given an OID string, e.g. `1.2.840.113549.1.1.1`, split it into | ||
* octets, encode the octets, and write it to the backing buffer. | ||
* | ||
* @param {string} oidString | ||
* @param {number} [tag=0x06] A custom tag to use for the OID. | ||
* | ||
* @throws When the parameters are not of the correct types, or if the | ||
* OID is not in the correct format. | ||
*/ | ||
writeOID (oidString, tag = types.OID) { | ||
if (typeof oidString !== 'string') { | ||
throw TypeError('oidString must be a string') | ||
} | ||
if (typeof tag !== 'number') { | ||
throw TypeError('tag must be a Number') | ||
} | ||
Writer.prototype.writeLength = function (len) { | ||
if (typeof (len) !== 'number') { throw new TypeError('argument must be a Number') } | ||
if (/^([0-9]+\.){3,}[0-9]+$/.test(oidString) === false) { | ||
throw Error('oidString is not a valid OID string') | ||
} | ||
this._ensure(4) | ||
const parts = oidString.split('.') | ||
const bytes = [] | ||
bytes.push(parseInt(parts[0], 10) * 40 + parseInt(parts[1], 10)) | ||
for (const part of parts.slice(2)) { | ||
encodeOctet(bytes, parseInt(part, 10)) | ||
} | ||
if (len <= 0x7f) { | ||
this._buf[this._offset++] = len | ||
} else if (len <= 0xff) { | ||
this._buf[this._offset++] = 0x81 | ||
this._buf[this._offset++] = len | ||
} else if (len <= 0xffff) { | ||
this._buf[this._offset++] = 0x82 | ||
this._buf[this._offset++] = len >> 8 | ||
this._buf[this._offset++] = len | ||
} else if (len <= 0xffffff) { | ||
this._buf[this._offset++] = 0x83 | ||
this._buf[this._offset++] = len >> 16 | ||
this._buf[this._offset++] = len >> 8 | ||
this._buf[this._offset++] = len | ||
} else { | ||
throw newInvalidAsn1Error('Length too long (> 4 bytes)') | ||
this.#ensureBufferCapacity(2 + bytes.length) | ||
this.writeByte(tag) | ||
this.writeLength(bytes.length) | ||
this.appendBuffer(Buffer.from(bytes)) | ||
function encodeOctet (bytes, octet) { | ||
if (octet < 128) { | ||
bytes.push(octet) | ||
} else if (octet < 16_384) { | ||
bytes.push((octet >>> 7) | 0x80) | ||
bytes.push(octet & 0x7F) | ||
} else if (octet < 2_097_152) { | ||
bytes.push((octet >>> 14) | 0x80) | ||
bytes.push(((octet >>> 7) | 0x80) & 0xFF) | ||
bytes.push(octet & 0x7F) | ||
} else if (octet < 268_435_456) { | ||
bytes.push((octet >>> 21) | 0x80) | ||
bytes.push(((octet >>> 14) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 7) | 0x80) & 0xFF) | ||
bytes.push(octet & 0x7F) | ||
} else { | ||
bytes.push(((octet >>> 28) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 21) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 14) | 0x80) & 0xFF) | ||
bytes.push(((octet >>> 7) | 0x80) & 0xFF) | ||
bytes.push(octet & 0x7F) | ||
} | ||
} | ||
} | ||
} | ||
Writer.prototype.startSequence = function (tag) { | ||
if (typeof (tag) !== 'number') { tag = ASN1.Sequence | ASN1.Constructor } | ||
/** | ||
* Write a string TLV to the buffer. | ||
* | ||
* @param {string} stringToWrite | ||
* @param {number} [tag=0x04] The tag to use. | ||
* | ||
* @throws When either input parameter is of the wrong type. | ||
*/ | ||
writeString (stringToWrite, tag = types.OctetString) { | ||
if (typeof stringToWrite !== 'string') { | ||
throw TypeError('stringToWrite must be a string') | ||
} | ||
if (typeof tag !== 'number') { | ||
throw TypeError('tag must be a number') | ||
} | ||
this.writeByte(tag) | ||
this._seq.push(this._offset) | ||
this._ensure(3) | ||
this._offset += 3 | ||
} | ||
const toWriteLength = Buffer.byteLength(stringToWrite) | ||
this.writeByte(tag) | ||
this.writeLength(toWriteLength) | ||
if (toWriteLength > 0) { | ||
this.#ensureBufferCapacity(toWriteLength) | ||
this.#buffer.write(stringToWrite, this.#offset) | ||
this.#offset += toWriteLength | ||
} | ||
} | ||
Writer.prototype.endSequence = function () { | ||
const seq = this._seq.pop() | ||
const start = seq + 3 | ||
const len = this._offset - start | ||
if (len <= 0x7f) { | ||
this._shift(start, len, -2) | ||
this._buf[seq] = len | ||
} else if (len <= 0xff) { | ||
this._shift(start, len, -1) | ||
this._buf[seq] = 0x81 | ||
this._buf[seq + 1] = len | ||
} else if (len <= 0xffff) { | ||
this._buf[seq] = 0x82 | ||
this._buf[seq + 1] = len >> 8 | ||
this._buf[seq + 2] = len | ||
} else if (len <= 0xffffff) { | ||
this._shift(start, len, 1) | ||
this._buf[seq] = 0x83 | ||
this._buf[seq + 1] = len >> 16 | ||
this._buf[seq + 2] = len >> 8 | ||
this._buf[seq + 3] = len | ||
} else { | ||
throw newInvalidAsn1Error('Sequence too long') | ||
/** | ||
* Given a set of strings, write each as a string TLV to the buffer. | ||
* | ||
* @param {string[]} strings | ||
* | ||
* @throws When the input is not an array. | ||
*/ | ||
writeStringArray (strings) { | ||
if (Array.isArray(strings) === false) { | ||
throw TypeError('strings must be an instance of Array') | ||
} | ||
for (const string of strings) { | ||
this.writeString(string) | ||
} | ||
} | ||
} | ||
Writer.prototype._shift = function (start, len, shift) { | ||
assert.ok(start !== undefined) | ||
assert.ok(len !== undefined) | ||
assert.ok(shift) | ||
/** | ||
* Given a number of bytes to be written into the buffer, verify the buffer | ||
* has enough free space. If not, allocate a new buffer, copy the current | ||
* backing buffer into the new buffer, and promote the new buffer to be the | ||
* current backing buffer. | ||
* | ||
* @param {number} numberOfBytesToWrite How many bytes are required to be | ||
* available for writing in the backing buffer. | ||
*/ | ||
#ensureBufferCapacity (numberOfBytesToWrite) { | ||
if (this.#size - this.#offset < numberOfBytesToWrite) { | ||
let newSize = this.#size * this.#growthFactor | ||
if (newSize - this.#offset < numberOfBytesToWrite) { | ||
newSize += numberOfBytesToWrite | ||
} | ||
this._buf.copy(this._buf, start + shift, start, start + len) | ||
this._offset += shift | ||
} | ||
const newBuffer = Buffer.alloc(newSize) | ||
Writer.prototype._ensure = function (len) { | ||
assert.ok(len) | ||
this.#buffer.copy(newBuffer, 0, 0, this.#offset) | ||
this.#buffer = newBuffer | ||
this.#size = newSize | ||
} | ||
} | ||
if (this._size - this._offset < len) { | ||
let sz = this._size * this._options.growthFactor | ||
if (sz - this._offset < len) { sz += len } | ||
const buf = Buffer.alloc(sz) | ||
this._buf.copy(buf, 0, 0, this._offset) | ||
this._buf = buf | ||
this._size = sz | ||
/** | ||
* Shift a region of the buffer indicated by `start` and `length` a number | ||
* of bytes indicated by `shiftAmount`. | ||
* | ||
* @param {number} start The starting position in the buffer for the region | ||
* of bytes to be shifted. | ||
* @param {number} length The number of bytes that constitutes the region | ||
* of the buffer to be shifted. | ||
* @param {number} shiftAmount The number of bytes to shift the region by. | ||
* This may be negative. | ||
*/ | ||
#shift (start, length, shiftAmount) { | ||
// TODO: this leaves garbage behind. We should either zero out the bytes | ||
// left behind, or device a better algorightm that generates a clean | ||
// buffer. | ||
this.#buffer.copy(this.#buffer, start + shiftAmount, start, start + length) | ||
this.#offset += shiftAmount | ||
} | ||
} | ||
// --- Exported API | ||
module.exports = Writer | ||
module.exports = BerWriter |
@@ -1,30 +0,38 @@ | ||
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
'use strict' | ||
const { test } = require('tap') | ||
const tap = require('tap') | ||
const BerWriter = require('./writer') | ||
test('write byte', function (t) { | ||
tap.test('has toStringTag', async t => { | ||
const writer = new BerWriter() | ||
t.equal(Object.prototype.toString.call(writer), '[object BerWriter]') | ||
}) | ||
writer.writeByte(0xC2) | ||
const ber = writer.buffer | ||
tap.test('#ensureBufferCapacity', t => { | ||
t.test('does not change buffer size if unnecessary', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
t.equal(writer.size, 1) | ||
t.ok(ber) | ||
t.equal(ber.length, 1, 'Wrong length') | ||
t.equal(ber[0], 0xC2, 'value wrong') | ||
writer.writeByte(0x01) | ||
t.equal(writer.size, 1) | ||
}) | ||
t.end() | ||
}) | ||
t.test('expands buffer to accomodate write skipping growth factor', async t => { | ||
const writer = new BerWriter({ size: 0 }) | ||
t.equal(writer.size, 0) | ||
test('write 1 byte int', function (t) { | ||
const writer = new BerWriter() | ||
writer.writeByte(0x01) | ||
t.equal(writer.size, 1) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x01])), 0) | ||
}) | ||
writer.writeInt(0x7f) | ||
const ber = writer.buffer | ||
t.test('expands buffer to accomodate write with growth factor', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
t.equal(writer.size, 1) | ||
t.ok(ber) | ||
t.equal(ber.length, 3, 'Wrong length for an int: ' + ber.length) | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong (2) -> ' + ber[0]) | ||
t.equal(ber[1], 0x01, 'length wrong(1) -> ' + ber[1]) | ||
t.equal(ber[2], 0x7f, 'value wrong(3) -> ' + ber[2]) | ||
writer.writeByte(0x01) | ||
writer.writeByte(0x02) | ||
t.equal(writer.size, 8) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x01, 0x02])), 0) | ||
}) | ||
@@ -34,31 +42,39 @@ t.end() | ||
test('write 2 byte int', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('appendBuffer', t => { | ||
t.test('throws if input not a buffer', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.appendBuffer('foo'), | ||
Error('buffer must be an instance of Buffer') | ||
) | ||
}) | ||
writer.writeInt(0x7ffe) | ||
const ber = writer.buffer | ||
t.test('appendBuffer appends a buffer', async t => { | ||
const expected = Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f, 0x66, 0x6f, 0x6f]) | ||
const writer = new BerWriter() | ||
writer.writeString('foo') | ||
writer.appendBuffer(Buffer.from('foo')) | ||
t.equal(Buffer.compare(writer.buffer, expected), 0) | ||
}) | ||
t.ok(ber) | ||
t.equal(ber.length, 4, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x02, 'length wrong') | ||
t.equal(ber[2], 0x7f, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xfe, 'value wrong (byte 2)') | ||
t.end() | ||
}) | ||
test('write 3 byte int', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('startSequence', t => { | ||
t.test('throws if tag not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.startSequence('30'), | ||
Error('tag must be a Number') | ||
) | ||
}) | ||
writer.writeInt(0x7ffffe) | ||
const ber = writer.buffer | ||
t.test('starts a sequence', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
writer.startSequence() | ||
t.equal(writer.size, 8) | ||
t.ok(ber) | ||
t.equal(ber.length, 5, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x03, 'length wrong') | ||
t.equal(ber[2], 0x7f, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xff, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0xfe, 'value wrong (byte 3)') | ||
const expected = Buffer.from([0x30, 0x00, 0x00, 0x00]) | ||
t.equal(Buffer.compare(writer.buffer, expected), 0) | ||
}) | ||
@@ -68,50 +84,180 @@ t.end() | ||
test('write 4 byte int', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('endSequence', t => { | ||
t.test('ends a sequence', async t => { | ||
const writer = new BerWriter({ size: 25 }) | ||
writer.startSequence() | ||
writer.writeString('hello world') | ||
writer.endSequence() | ||
writer.writeInt(0x7ffffffe) | ||
const ber = writer.buffer | ||
const ber = writer.buffer | ||
const expected = Buffer.from([ | ||
0x30, 0x0d, // sequence; 13 bytes | ||
0x04, 0x0b, // string; 11 bytes | ||
0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, // 'hello ' | ||
0x77, 0x6f, 0x72, 0x6c, 0x64 // 'world' | ||
]) | ||
t.equal(Buffer.compare(ber, expected), 0) | ||
}) | ||
t.ok(ber) | ||
t.test('ends sequence of two byte length', async t => { | ||
const value = Buffer.alloc(0x81, 0x01) | ||
const writer = new BerWriter() | ||
t.equal(ber.length, 6, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x04, 'length wrong') | ||
t.equal(ber[2], 0x7f, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xff, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0xff, 'value wrong (byte 3)') | ||
t.equal(ber[5], 0xfe, 'value wrong (byte 4)') | ||
writer.startSequence() | ||
writer.writeBuffer(value, 0x04) | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
t.equal( | ||
Buffer.from([0x30, 0x81, 0x84, 0x04, 0x81, value.length]) | ||
.compare(ber.subarray(0, 6)), | ||
0 | ||
) | ||
}) | ||
t.test('ends sequence of three byte length', async t => { | ||
const value = Buffer.alloc(0xfe, 0x01) | ||
const writer = new BerWriter() | ||
writer.startSequence() | ||
writer.writeBuffer(value, 0x04) | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
t.equal( | ||
Buffer.from([0x30, 0x82, 0x01, 0x01, 0x04, 0x81, value.length]) | ||
.compare(ber.subarray(0, 7)), | ||
0 | ||
) | ||
}) | ||
t.test('ends sequence of four byte length', async t => { | ||
const value = Buffer.alloc(0xaaaaaa, 0x01) | ||
const writer = new BerWriter() | ||
writer.startSequence() | ||
writer.writeBuffer(value, 0x04) | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
t.equal( | ||
Buffer.from([0x30, 0x83, 0xaa, 0xaa, 0xaf, 0x04, 0x83, value.length]) | ||
.compare(ber.subarray(0, 8)), | ||
0 | ||
) | ||
}) | ||
t.test('throws if sequence too long', async t => { | ||
const value = Buffer.alloc(0xaffffff, 0x01) | ||
const writer = new BerWriter() | ||
writer.startSequence() | ||
writer.writeByte(0x04) | ||
// We can't write the length because it is too long. However, this | ||
// still gives us enough data to generate the error we want to generate. | ||
writer.appendBuffer(value) | ||
t.throws( | ||
() => writer.endSequence(), | ||
Error('sequence too long') | ||
) | ||
}) | ||
t.end() | ||
}) | ||
test('write 1 byte negative int', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('writeBoolean', t => { | ||
t.test('throws if input not a boolean', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeBoolean(1), | ||
Error('boolValue must be a Boolean') | ||
) | ||
}) | ||
writer.writeInt(-128) | ||
const ber = writer.buffer | ||
t.test('throws if tag not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeBoolean(true, '5'), | ||
Error('tag must be a Number') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.test('writes true', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
writer.writeBoolean(true) | ||
t.equal(writer.size, 8) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x01, 0x01, 0xff])), 0) | ||
}) | ||
t.equal(ber.length, 3, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x01, 'length wrong') | ||
t.equal(ber[2], 0x80, 'value wrong (byte 1)') | ||
t.test('writes false', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
writer.writeBoolean(false) | ||
t.equal(writer.size, 8) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x01, 0x01, 0x00])), 0) | ||
}) | ||
t.test('writes with custom tag', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
writer.writeBoolean(true, 0xff) | ||
t.equal(writer.size, 8) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0xff, 0x01, 0xff])), 0) | ||
}) | ||
// Original test | ||
t.test('write boolean', async t => { | ||
const writer = new BerWriter() | ||
writer.writeBoolean(true) | ||
writer.writeBoolean(false) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 6, 'Wrong length') | ||
t.equal(ber[0], 0x01, 'tag wrong') | ||
t.equal(ber[1], 0x01, 'length wrong') | ||
t.equal(ber[2], 0xff, 'value wrong') | ||
t.equal(ber[3], 0x01, 'tag wrong') | ||
t.equal(ber[4], 0x01, 'length wrong') | ||
t.equal(ber[5], 0x00, 'value wrong') | ||
}) | ||
t.end() | ||
}) | ||
test('write 2 byte negative int', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('writeBuffer', t => { | ||
t.test('throws if tag not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeBuffer(Buffer.alloc(0), '1'), | ||
Error('tag must be a Number') | ||
) | ||
}) | ||
writer.writeInt(-22400) | ||
const ber = writer.buffer | ||
t.test('throws if buffer not a Buffer', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeBuffer([0x00], 0x01), | ||
Error('buffer must be an instance of Buffer') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.test('write buffer', async t => { | ||
const writer = new BerWriter() | ||
// write some stuff to start with | ||
writer.writeString('hello world') | ||
let ber = writer.buffer | ||
const buf = Buffer.from([0x04, 0x0b, 0x30, 0x09, 0x02, 0x01, 0x0f, 0x01, 0x01, | ||
0xff, 0x01, 0x01, 0xff]) | ||
writer.writeBuffer(buf.subarray(2, buf.length), 0x04) | ||
ber = writer.buffer | ||
t.equal(ber.length, 4, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x02, 'length wrong') | ||
t.equal(ber[2], 0xa8, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0x80, 'value wrong (byte 2)') | ||
t.equal(ber.length, 26, 'wrong length') | ||
t.equal(ber[0], 0x04, 'wrong tag') | ||
t.equal(ber[1], 11, 'wrong length') | ||
t.equal(ber.slice(2, 13).toString('utf8'), 'hello world', 'wrong value') | ||
t.equal(ber[13], buf[0], 'wrong tag') | ||
t.equal(ber[14], buf[1], 'wrong length') | ||
for (let i = 13, j = 0; i < ber.length && j < buf.length; i++, j++) { | ||
t.equal(ber[i], buf[j], 'buffer contents not identical') | ||
} | ||
}) | ||
@@ -121,16 +267,21 @@ t.end() | ||
test('write 3 byte negative int', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('writeByte', t => { | ||
t.test('throws if input not a number', async t => { | ||
const writer = new BerWriter() | ||
t.equal(writer.size, 1024) | ||
writer.writeInt(-481653) | ||
const ber = writer.buffer | ||
t.throws( | ||
() => writer.writeByte('1'), | ||
Error('argument must be a Number') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.test('writes a byte to the backing buffer', async t => { | ||
const writer = new BerWriter() | ||
writer.writeByte(0x01) | ||
t.equal(ber.length, 5, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x03, 'length wrong') | ||
t.equal(ber[2], 0xf8, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xa6, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0x8b, 'value wrong (byte 3)') | ||
const buffer = writer.buffer | ||
t.equal(buffer.length, 1) | ||
t.equal(Buffer.compare(buffer, Buffer.from([0x01])), 0) | ||
}) | ||
@@ -140,17 +291,32 @@ t.end() | ||
test('write 4 byte negative int', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('writeEnumeration', async t => { | ||
t.test('throws if value not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeEnumeration('1'), | ||
Error('value must be a Number') | ||
) | ||
}) | ||
writer.writeInt(-1522904131) | ||
const ber = writer.buffer | ||
t.test('throws if tag not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeEnumeration(1, '1'), | ||
Error('tag must be a Number') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.test('writes an enumeration', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
writer.writeEnumeration(0x01) | ||
t.equal(writer.size, 8) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x0a, 0x01, 0x01])), 0) | ||
}) | ||
t.equal(ber.length, 6, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x04, 'length wrong') | ||
t.equal(ber[2], 0xa5, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0x3a, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0x53, 'value wrong (byte 3)') | ||
t.equal(ber[5], 0xbd, 'value wrong (byte 4)') | ||
t.test('writes an enumeration with custom tag', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
writer.writeEnumeration(0x01, 0xff) | ||
t.equal(writer.size, 8) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0xff, 0x01, 0x01])), 0) | ||
}) | ||
@@ -160,87 +326,249 @@ t.end() | ||
test('write boolean', function (t) { | ||
const writer = new BerWriter() | ||
tap.test('writeInt', t => { | ||
t.test('throws if int not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeInt('1'), | ||
Error('intToWrite must be a Number') | ||
) | ||
}) | ||
writer.writeBoolean(true) | ||
writer.writeBoolean(false) | ||
const ber = writer.buffer | ||
t.test('throws if tag not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeInt(1, '1'), | ||
Error('tag must be a Number') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.equal(ber.length, 6, 'Wrong length') | ||
t.equal(ber[0], 0x01, 'tag wrong') | ||
t.equal(ber[1], 0x01, 'length wrong') | ||
t.equal(ber[2], 0xff, 'value wrong') | ||
t.equal(ber[3], 0x01, 'tag wrong') | ||
t.equal(ber[4], 0x01, 'length wrong') | ||
t.equal(ber[5], 0x00, 'value wrong') | ||
t.test('write 1 byte int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(0x7f) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 3, 'Wrong length for an int: ' + ber.length) | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong (2) -> ' + ber[0]) | ||
t.equal(ber[1], 0x01, 'length wrong(1) -> ' + ber[1]) | ||
t.equal(ber[2], 0x7f, 'value wrong(3) -> ' + ber[2]) | ||
}) | ||
t.test('write 2 byte int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(0x7ffe) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 4, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x02, 'length wrong') | ||
t.equal(ber[2], 0x7f, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xfe, 'value wrong (byte 2)') | ||
}) | ||
t.test('write 3 byte int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(0x7ffffe) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 5, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x03, 'length wrong') | ||
t.equal(ber[2], 0x7f, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xff, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0xfe, 'value wrong (byte 3)') | ||
}) | ||
t.test('write 4 byte int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(0x7ffffffe) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 6, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x04, 'length wrong') | ||
t.equal(ber[2], 0x7f, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xff, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0xff, 'value wrong (byte 3)') | ||
t.equal(ber[5], 0xfe, 'value wrong (byte 4)') | ||
}) | ||
t.test('write 1 byte negative int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(-128) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 3, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x01, 'length wrong') | ||
t.equal(ber[2], 0x80, 'value wrong (byte 1)') | ||
}) | ||
t.test('write 2 byte negative int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(-22400) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 4, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x02, 'length wrong') | ||
t.equal(ber[2], 0xa8, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0x80, 'value wrong (byte 2)') | ||
}) | ||
t.test('write 3 byte negative int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(-481653) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 5, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x03, 'length wrong') | ||
t.equal(ber[2], 0xf8, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0xa6, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0x8b, 'value wrong (byte 3)') | ||
}) | ||
t.test('write 4 byte negative int', async t => { | ||
const writer = new BerWriter() | ||
writer.writeInt(-1522904131) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 6, 'Wrong length for an int') | ||
t.equal(ber[0], 0x02, 'ASN.1 tag wrong') | ||
t.equal(ber[1], 0x04, 'length wrong') | ||
t.equal(ber[2], 0xa5, 'value wrong (byte 1)') | ||
t.equal(ber[3], 0x3a, 'value wrong (byte 2)') | ||
t.equal(ber[4], 0x53, 'value wrong (byte 3)') | ||
t.equal(ber[5], 0xbd, 'value wrong (byte 4)') | ||
}) | ||
t.test('throws for > 4 byte integer', { skip: true }, async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeInt(0xffffffffff), | ||
Error('BER ints cannot be > 0xffffffff') | ||
) | ||
}) | ||
t.end() | ||
}) | ||
test('write string', function (t) { | ||
const writer = new BerWriter() | ||
writer.writeString('hello world') | ||
const ber = writer.buffer | ||
tap.test('writeLength', t => { | ||
t.test('throws if length not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeLength('1'), | ||
Error('argument must be a Number') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.equal(ber.length, 13, 'wrong length') | ||
t.equal(ber[0], 0x04, 'wrong tag') | ||
t.equal(ber[1], 11, 'wrong length') | ||
t.equal(ber.slice(2).toString('utf8'), 'hello world', 'wrong value') | ||
t.test('writes a single byte length', async t => { | ||
const writer = new BerWriter({ size: 4 }) | ||
writer.writeLength(0x7f) | ||
t.equal(writer.buffer.length, 1) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x7f])), 0) | ||
}) | ||
t.test('writes a two byte length', async t => { | ||
const writer = new BerWriter({ size: 4 }) | ||
writer.writeLength(0xff) | ||
t.equal(writer.buffer.length, 2) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x81, 0xff])), 0) | ||
}) | ||
t.test('writes a three byte length', async t => { | ||
const writer = new BerWriter({ size: 4 }) | ||
writer.writeLength(0xffff) | ||
t.equal(writer.buffer.length, 3) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x82, 0xff, 0xff])), 0) | ||
}) | ||
t.test('writes a four byte length', async t => { | ||
const writer = new BerWriter({ size: 4 }) | ||
writer.writeLength(0xffffff) | ||
t.equal(writer.buffer.length, 4) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x83, 0xff, 0xff, 0xff])), 0) | ||
}) | ||
t.test('throw if byte length is too long', async t => { | ||
const writer = new BerWriter({ size: 4 }) | ||
t.throws( | ||
() => writer.writeLength(0xffffffffff), | ||
Error('length too long (> 4 bytes)') | ||
) | ||
}) | ||
t.end() | ||
}) | ||
test('write buffer', function (t) { | ||
const writer = new BerWriter() | ||
// write some stuff to start with | ||
writer.writeString('hello world') | ||
let ber = writer.buffer | ||
const buf = Buffer.from([0x04, 0x0b, 0x30, 0x09, 0x02, 0x01, 0x0f, 0x01, 0x01, | ||
0xff, 0x01, 0x01, 0xff]) | ||
writer.writeBuffer(buf.slice(2, buf.length), 0x04) | ||
ber = writer.buffer | ||
tap.test('writeNull', t => { | ||
t.test('writeNull', async t => { | ||
const writer = new BerWriter({ size: 2 }) | ||
writer.writeNull() | ||
t.equal(writer.size, 2) | ||
t.equal(Buffer.compare(writer.buffer, Buffer.from([0x05, 0x00])), 0) | ||
}) | ||
t.ok(ber) | ||
t.equal(ber.length, 26, 'wrong length') | ||
t.equal(ber[0], 0x04, 'wrong tag') | ||
t.equal(ber[1], 11, 'wrong length') | ||
t.equal(ber.slice(2, 13).toString('utf8'), 'hello world', 'wrong value') | ||
t.equal(ber[13], buf[0], 'wrong tag') | ||
t.equal(ber[14], buf[1], 'wrong length') | ||
for (let i = 13, j = 0; i < ber.length && j < buf.length; i++, j++) { | ||
t.equal(ber[i], buf[j], 'buffer contents not identical') | ||
} | ||
t.end() | ||
}) | ||
test('write string array', function (t) { | ||
const writer = new BerWriter() | ||
writer.writeStringArray(['hello world', 'fubar!']) | ||
const ber = writer.buffer | ||
tap.test('writeOID', t => { | ||
t.test('throws if OID not a string', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeOID(42), | ||
Error('oidString must be a string') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.test('throws if tag not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeOID('1.2.3', '1'), | ||
Error('tag must be a Number') | ||
) | ||
}) | ||
t.equal(ber.length, 21, 'wrong length') | ||
t.equal(ber[0], 0x04, 'wrong tag') | ||
t.equal(ber[1], 11, 'wrong length') | ||
t.equal(ber.slice(2, 13).toString('utf8'), 'hello world', 'wrong value') | ||
t.test('throws if OID not a valid OID string', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeOID('foo'), | ||
Error('oidString is not a valid OID string') | ||
) | ||
}) | ||
t.equal(ber[13], 0x04, 'wrong tag') | ||
t.equal(ber[14], 6, 'wrong length') | ||
t.equal(ber.slice(15).toString('utf8'), 'fubar!', 'wrong value') | ||
t.test('writes an OID', async t => { | ||
const oid = '1.2.840.113549.1.1.1' | ||
const writer = new BerWriter() | ||
writer.writeOID(oid) | ||
t.end() | ||
}) | ||
const expected = Buffer.from([0x06, 0x09, 0x2a, 0x86, | ||
0x48, 0x86, 0xf7, 0x0d, | ||
0x01, 0x01, 0x01]) | ||
const ber = writer.buffer | ||
t.equal(ber.compare(expected), 0) | ||
}) | ||
test('resize internal buffer', function (t) { | ||
const writer = new BerWriter({ size: 2 }) | ||
writer.writeString('hello world') | ||
const ber = writer.buffer | ||
t.test('writes OID covering all octet encodings', async t => { | ||
const oid = '1.2.200.17000.2100100.270100100' | ||
const writer = new BerWriter() | ||
writer.writeOID(oid) | ||
t.ok(ber) | ||
t.equal(ber.length, 13, 'wrong length') | ||
t.equal(ber[0], 0x04, 'wrong tag') | ||
t.equal(ber[1], 11, 'wrong length') | ||
t.equal(ber.slice(2).toString('utf8'), 'hello world', 'wrong value') | ||
const expected = Buffer.from([ | ||
0x06, 0x0f, | ||
0x2a, 0x81, 0x48, 0x81, | ||
0x84, 0x68, 0x81, 0x80, | ||
0x97, 0x04, 0x81, 0x80, | ||
0xe5, 0xcd, 0x04 | ||
]) | ||
const ber = writer.buffer | ||
t.equal(ber.compare(expected), 0) | ||
}) | ||
@@ -250,101 +578,149 @@ t.end() | ||
test('sequence', function (t) { | ||
const writer = new BerWriter({ size: 25 }) | ||
writer.startSequence() | ||
writer.writeString('hello world') | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
tap.test('writeString', t => { | ||
t.test('throws if non-string supplied', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeString(42), | ||
Error('stringToWrite must be a string') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.equal(ber.length, 15, 'wrong length') | ||
t.equal(ber[0], 0x30, 'wrong tag') | ||
t.equal(ber[1], 13, 'wrong length') | ||
t.equal(ber[2], 0x04, 'wrong tag') | ||
t.equal(ber[3], 11, 'wrong length') | ||
t.equal(ber.slice(4).toString('utf8'), 'hello world', 'wrong value') | ||
t.test('throws if tag not a number', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeString('foo', '1'), | ||
Error('tag must be a number') | ||
) | ||
}) | ||
t.end() | ||
}) | ||
t.test('writes an empty string', async t => { | ||
const writer = new BerWriter() | ||
writer.writeString('') | ||
test('nested sequence', function (t) { | ||
const writer = new BerWriter({ size: 25 }) | ||
writer.startSequence() | ||
writer.writeString('hello world') | ||
writer.startSequence() | ||
writer.writeString('hello world') | ||
writer.endSequence() | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
const expected = Buffer.from([0x04, 0x00]) | ||
t.equal(Buffer.compare(writer.buffer, expected), 0) | ||
}) | ||
t.ok(ber) | ||
t.equal(ber.length, 30, 'wrong length') | ||
t.equal(ber[0], 0x30, 'wrong tag') | ||
t.equal(ber[1], 28, 'wrong length') | ||
t.equal(ber[2], 0x04, 'wrong tag') | ||
t.equal(ber[3], 11, 'wrong length') | ||
t.equal(ber.slice(4, 15).toString('utf8'), 'hello world', 'wrong value') | ||
t.equal(ber[15], 0x30, 'wrong tag') | ||
t.equal(ber[16], 13, 'wrong length') | ||
t.equal(ber[17], 0x04, 'wrong tag') | ||
t.equal(ber[18], 11, 'wrong length') | ||
t.equal(ber.slice(19, 30).toString('utf8'), 'hello world', 'wrong value') | ||
t.test('writes a string', async t => { | ||
const writer = new BerWriter({ size: 1 }) | ||
writer.writeString('foo') | ||
const expected = Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]) | ||
t.equal(Buffer.compare(writer.buffer, expected), 0) | ||
t.equal(writer.size, 8) | ||
}) | ||
t.end() | ||
}) | ||
test('LDAP bind message', function (t) { | ||
const dn = 'cn=foo,ou=unit,o=test' | ||
const writer = new BerWriter() | ||
writer.startSequence() | ||
writer.writeInt(3) // msgid = 3 | ||
writer.startSequence(0x60) // ldap bind | ||
writer.writeInt(3) // ldap v3 | ||
writer.writeString(dn) | ||
writer.writeByte(0x80) | ||
writer.writeByte(0x00) | ||
writer.endSequence() | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
tap.test('writeString', t => { | ||
t.test('throws if non-array supplied', async t => { | ||
const writer = new BerWriter() | ||
t.throws( | ||
() => writer.writeStringArray(42), | ||
Error('strings must be an instance of Array') | ||
) | ||
}) | ||
t.ok(ber) | ||
t.equal(ber.length, 35, 'wrong length (buffer)') | ||
t.equal(ber[0], 0x30, 'wrong tag') | ||
t.equal(ber[1], 33, 'wrong length') | ||
t.equal(ber[2], 0x02, 'wrong tag') | ||
t.equal(ber[3], 1, 'wrong length') | ||
t.equal(ber[4], 0x03, 'wrong value') | ||
t.equal(ber[5], 0x60, 'wrong tag') | ||
t.equal(ber[6], 28, 'wrong length') | ||
t.equal(ber[7], 0x02, 'wrong tag') | ||
t.equal(ber[8], 1, 'wrong length') | ||
t.equal(ber[9], 0x03, 'wrong value') | ||
t.equal(ber[10], 0x04, 'wrong tag') | ||
t.equal(ber[11], dn.length, 'wrong length') | ||
t.equal(ber.slice(12, 33).toString('utf8'), dn, 'wrong value') | ||
t.equal(ber[33], 0x80, 'wrong tag') | ||
t.equal(ber[34], 0x00, 'wrong len') | ||
t.test('write string array', async t => { | ||
const writer = new BerWriter() | ||
writer.writeStringArray(['hello world', 'fubar!']) | ||
const ber = writer.buffer | ||
t.equal(ber.length, 21, 'wrong length') | ||
t.equal(ber[0], 0x04, 'wrong tag') | ||
t.equal(ber[1], 11, 'wrong length') | ||
t.equal(ber.subarray(2, 13).toString('utf8'), 'hello world', 'wrong value') | ||
t.equal(ber[13], 0x04, 'wrong tag') | ||
t.equal(ber[14], 6, 'wrong length') | ||
t.equal(ber.subarray(15).toString('utf8'), 'fubar!', 'wrong value') | ||
}) | ||
t.end() | ||
}) | ||
test('Write OID', function (t) { | ||
const oid = '1.2.840.113549.1.1.1' | ||
const writer = new BerWriter() | ||
writer.writeOID(oid) | ||
tap.test('original tests', t => { | ||
t.test('resize internal buffer', async t => { | ||
const writer = new BerWriter({ size: 2 }) | ||
writer.writeString('hello world') | ||
const ber = writer.buffer | ||
const expected = Buffer.from([0x06, 0x09, 0x2a, 0x86, | ||
0x48, 0x86, 0xf7, 0x0d, | ||
0x01, 0x01, 0x01]) | ||
const ber = writer.buffer | ||
t.equal(ber.compare(expected), 0) | ||
t.equal(ber.length, 13, 'wrong length') | ||
t.equal(ber[0], 0x04, 'wrong tag') | ||
t.equal(ber[1], 11, 'wrong length') | ||
t.equal(ber.subarray(2).toString('utf8'), 'hello world', 'wrong value') | ||
}) | ||
t.test('sequence', async t => { | ||
const writer = new BerWriter({ size: 25 }) | ||
writer.startSequence() | ||
writer.writeString('hello world') | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
t.equal(ber.length, 15, 'wrong length') | ||
t.equal(ber[0], 0x30, 'wrong tag') | ||
t.equal(ber[1], 13, 'wrong length') | ||
t.equal(ber[2], 0x04, 'wrong tag') | ||
t.equal(ber[3], 11, 'wrong length') | ||
t.equal(ber.subarray(4).toString('utf8'), 'hello world', 'wrong value') | ||
}) | ||
t.test('nested sequence', async t => { | ||
const writer = new BerWriter({ size: 25 }) | ||
writer.startSequence() | ||
writer.writeString('hello world') | ||
writer.startSequence() | ||
writer.writeString('hello world') | ||
writer.endSequence() | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
t.equal(ber.length, 30, 'wrong length') | ||
t.equal(ber[0], 0x30, 'wrong tag') | ||
t.equal(ber[1], 28, 'wrong length') | ||
t.equal(ber[2], 0x04, 'wrong tag') | ||
t.equal(ber[3], 11, 'wrong length') | ||
t.equal(ber.subarray(4, 15).toString('utf8'), 'hello world', 'wrong value') | ||
t.equal(ber[15], 0x30, 'wrong tag') | ||
t.equal(ber[16], 13, 'wrong length') | ||
t.equal(ber[17], 0x04, 'wrong tag') | ||
t.equal(ber[18], 11, 'wrong length') | ||
t.equal(ber.subarray(19, 30).toString('utf8'), 'hello world', 'wrong value') | ||
}) | ||
t.test('LDAP bind message', async t => { | ||
const dn = 'cn=foo,ou=unit,o=test' | ||
const writer = new BerWriter() | ||
writer.startSequence() | ||
writer.writeInt(3) // msgid = 3 | ||
writer.startSequence(0x60) // ldap bind | ||
writer.writeInt(3) // ldap v3 | ||
writer.writeString(dn) | ||
writer.writeByte(0x80) | ||
writer.writeByte(0x00) | ||
writer.endSequence() | ||
writer.endSequence() | ||
const ber = writer.buffer | ||
t.equal(ber.length, 35, 'wrong length (buffer)') | ||
t.equal(ber[0], 0x30, 'wrong tag') | ||
t.equal(ber[1], 33, 'wrong length') | ||
t.equal(ber[2], 0x02, 'wrong tag') | ||
t.equal(ber[3], 1, 'wrong length') | ||
t.equal(ber[4], 0x03, 'wrong value') | ||
t.equal(ber[5], 0x60, 'wrong tag') | ||
t.equal(ber[6], 28, 'wrong length') | ||
t.equal(ber[7], 0x02, 'wrong tag') | ||
t.equal(ber[8], 1, 'wrong length') | ||
t.equal(ber[9], 0x03, 'wrong value') | ||
t.equal(ber[10], 0x04, 'wrong tag') | ||
t.equal(ber[11], dn.length, 'wrong length') | ||
t.equal(ber.subarray(12, 33).toString('utf8'), dn, 'wrong value') | ||
t.equal(ber[33], 0x80, 'wrong tag') | ||
t.equal(ber[34], 0x00, 'wrong len') | ||
}) | ||
t.end() | ||
}) | ||
test('appendBuffer appends a buffer', async t => { | ||
const expected = Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f, 0x66, 0x6f, 0x6f]) | ||
const writer = new BerWriter() | ||
writer.writeString('foo') | ||
writer.appendBuffer(Buffer.from('foo')) | ||
t.equal(Buffer.compare(writer.buffer, expected), 0) | ||
}) |
@@ -11,3 +11,3 @@ { | ||
"description": "Contains parsers and serializers for ASN.1 (currently BER only)", | ||
"version": "1.2.0", | ||
"version": "2.0.0-rc.1", | ||
"repository": { | ||
@@ -17,14 +17,20 @@ "type": "git", | ||
}, | ||
"main": "lib/index.js", | ||
"main": "index.js", | ||
"devDependencies": { | ||
"@fastify/pre-commit": "^2.0.2", | ||
"standard": "^16.0.4", | ||
"tap": "^16.0.1" | ||
"eslint": "^8.25.0", | ||
"eslint-config-standard": "^17.0.0", | ||
"eslint-plugin-import": "^2.26.0", | ||
"eslint-plugin-n": "^15.3.0", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-promise": "^6.0.1", | ||
"tap": "^16.3.0" | ||
}, | ||
"scripts": { | ||
"test": "tap --no-coverage-report -R terse", | ||
"test:cov": "tap -R terse", | ||
"test:cov:html": "tap -R terse --coverage-report=html", | ||
"test:watch": "tap -w --no-coverage-report -R terse", | ||
"lint": "standard" | ||
"lint": "eslint .", | ||
"lint:ci": "eslint .", | ||
"test": "tap --no-coverage-report", | ||
"test:cov": "tap", | ||
"test:cov:html": "tap --coverage-report=html", | ||
"test:watch": "tap -w --no-coverage-report" | ||
}, | ||
@@ -31,0 +37,0 @@ "license": "MIT", |
@@ -10,11 +10,12 @@ # `@ldapjs/asn1` | ||
var Ber = require('@ldapjs/asn1').Ber; | ||
```js | ||
const { BerReader, BerTypes } = require('@ldapjs/asn1') | ||
const reader = new BerReader(Buffer.from([0x30, 0x03, 0x01, 0x01, 0xff])) | ||
var reader = new Ber.Reader(Buffer.from([0x30, 0x03, 0x01, 0x01, 0xff])); | ||
reader.readSequence() | ||
console.log('Sequence len: ' + reader.length) | ||
if (reader.peek() === BerTypes.Boolean) | ||
console.log(reader.readBoolean()) | ||
``` | ||
reader.readSequence(); | ||
console.log('Sequence len: ' + reader.length); | ||
if (reader.peek() === Ber.Boolean) | ||
console.log(reader.readBoolean()); | ||
### Encoding | ||
@@ -24,15 +25,18 @@ | ||
var Ber = require('@ldapjs/asn1').Ber; | ||
```js | ||
const { BerWriter } = require('@ldapjs/asn1'); | ||
const writer = new BerWriter(); | ||
var writer = new Ber.Writer(); | ||
writer.startSequence(); | ||
writer.writeBoolean(true); | ||
writer.endSequence(); | ||
writer.startSequence(); | ||
writer.writeBoolean(true); | ||
writer.endSequence(); | ||
console.log(writer.buffer); | ||
``` | ||
console.log(writer.buffer); | ||
## Installation | ||
npm install asn1 | ||
```sh | ||
npm install @ldapjs/asn1 | ||
``` | ||
@@ -39,0 +43,0 @@ ## Bugs |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
69340
16
1931
44
8
2
1