wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
'use strict'
 
const types = require('./types')
const bufferToHexDump = require('../buffer-to-hex-dump')
 
class BerWriter {
  /**
   * The source buffer as it was passed in when creating the instance.
   *
   * @type {Buffer}
   */
  #buffer
 
  /**
   * The total bytes in the backing buffer.
   *
   * @type {number}
   */
  #size
 
  /**
   * As the BER buffer is written, this property records the current position
   * in the buffer.
   *
   * @type {number}
   */
  #offset = 0
 
  /**
   * A list of offsets in the buffer where we need to insert sequence tag and
   * length pairs.
   */
  #sequenceOffsets = []
 
  /**
   * Coeffecient used when increasing the buffer to accomodate writes that
   * exceed the available space left in the buffer.
   *
   * @type {number}
   */
  #growthFactor
 
  constructor ({ size = 1024, growthFactor = 8 } = {}) {
    this.#buffer = Buffer.alloc(size)
    this.#size = this.#buffer.length
    this.#offset = 0
    this.#growthFactor = growthFactor
  }
 
  get [Symbol.toStringTag] () { return 'BerWriter' }
 
  get buffer () {
    // TODO: handle sequence check
 
    return this.#buffer.subarray(0, this.#offset)
  }
 
  /**
   * The size of the backing buffer.
   *
   * @return {number}
   */
  get size () {
    return this.#size
  }
 
  /**
   * 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
  }
 
  /**
   * 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
 
    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')
    }
  }
 
  /**
   * 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')
    }
 
    this.writeByte(tag)
    this.#sequenceOffsets.push(this.#offset)
    this.#ensureBufferCapacity(3)
    this.#offset += 3
  }
 
  /**
   * @param {HexDumpParams} params The `buffer` parameter will be ignored.
   *
   * @see bufferToHexDump
   */
  toHexDump (params) {
    bufferToHexDump({
      ...params,
      buffer: this.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
  }
 
  /**
   * 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.writeByte(tag)
    this.writeLength(buffer.length)
    this.#ensureBufferCapacity(buffer.length)
    buffer.copy(this.#buffer, this.#offset, 0, buffer.length)
    this.#offset += buffer.length
  }
 
  /**
   * 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')
    }
 
    this.#ensureBufferCapacity(1)
    this.#buffer[this.#offset++] = byte
  }
 
  /**
   * 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)
  }
 
  /**
   * 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')
    }
 
    let intSize = 4
    while (
      (
        ((intToWrite & 0xff800000) === 0) ||
        ((intToWrite & 0xff800000) === (0xff800000 >> 0))
      ) && (intSize > 1)
    ) {
      intSize--
      intToWrite <<= 8
    }
 
    // 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')
    }
 
    this.#ensureBufferCapacity(2 + intSize)
    this.#buffer[this.#offset++] = tag
    this.#buffer[this.#offset++] = intSize
 
    while (intSize-- > 0) {
      this.#buffer[this.#offset++] = ((intToWrite & 0xff000000) >>> 24)
      intToWrite <<= 8
    }
  }
 
  /**
   * 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.#ensureBufferCapacity(4)
 
    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 {
      throw Error('length too long (> 4 bytes)')
    }
  }
 
  /**
   * Write a NULL tag and value to the buffer.
   */
  writeNull () {
    this.writeByte(types.Null)
    this.writeByte(0x00)
  }
 
  /**
   * 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')
    }
 
    if (/^([0-9]+\.){3,}[0-9]+$/.test(oidString) === false) {
      throw Error('oidString is not a valid OID string')
    }
 
    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))
    }
 
    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)
      }
    }
  }
 
  /**
   * 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')
    }
 
    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
    }
  }
 
  /**
   * 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)
    }
  }
 
  /**
   * 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
      }
 
      const newBuffer = Buffer.alloc(newSize)
 
      this.#buffer.copy(newBuffer, 0, 0, this.#offset)
      this.#buffer = newBuffer
      this.#size = newSize
    }
  }
 
  /**
   * 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
  }
}
 
module.exports = BerWriter