libbase64 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.2](https://github.com/nodemailer/libbase64/compare/v1.3.1...v1.3.2) (2026-10-04)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **encoder:** wrap base64 lines without regular expressions, independent of chunk boundaries ([2c76a48](https://github.com/nodemailer/libbase64/commit/2c76a489f1444600b5645a44c933e159ac8e920f))
9
+
3
10
  ## [1.3.1](https://github.com/nodemailer/libbase64/compare/v1.3.0...v1.3.1) (2026-09-28)
4
11
 
5
12
 
package/lib/libbase64.js CHANGED
@@ -45,6 +45,52 @@ function decode(str) {
45
45
  return Buffer.from(str, 'base64');
46
46
  }
47
47
 
48
+ /**
49
+ * Turns a line length option into a whole number of characters, the default for anything unusable
50
+ */
51
+ function normalizeLineLength(lineLength) {
52
+ lineLength = Math.floor(Number(lineLength));
53
+ return Number.isFinite(lineLength) && lineLength >= 1 ? lineLength : 76;
54
+ }
55
+
56
+ /**
57
+ * Splits the bytes of `src` into lines of `lineLength` bytes, each followed by a line break. With
58
+ * `final` set the last line, which may be shorter, gets no line break; otherwise only complete lines
59
+ * are taken and the rest is left for the caller
60
+ *
61
+ * @param {Buffer} src Bytes to wrap
62
+ * @param {Number} lineLength Line length
63
+ * @param {Boolean} final Whether `src` ends the output
64
+ * @returns {Object} `{ output, rest }`: the wrapped bytes and the number of trailing bytes not taken
65
+ */
66
+ function wrapBuffer(src, lineLength, final) {
67
+ let lines = Math.ceil(src.length / lineLength);
68
+ // the last line waits for more data unless this is the end: whether it gets a line break depends on
69
+ // whether anything follows it
70
+ let complete = Math.max(lines - 1, 0);
71
+ let rest = src.length - complete * lineLength;
72
+
73
+ let output = Buffer.allocUnsafe(complete * (lineLength + 2) + (final ? rest : 0));
74
+ let to = 0;
75
+ for (let from = 0; from < complete * lineLength; from += lineLength) {
76
+ src.copy(output, to, from, from + lineLength);
77
+ to += lineLength;
78
+ output[to++] = 0x0d;
79
+ output[to++] = 0x0a;
80
+ }
81
+ if (final) {
82
+ to += src.copy(output, to, complete * lineLength);
83
+ rest = 0;
84
+ }
85
+
86
+ if (to !== output.length) {
87
+ // never hand out bytes of the unfilled allocation
88
+ throw new Error('Unexpected wrapped length');
89
+ }
90
+
91
+ return { output, rest };
92
+ }
93
+
48
94
  /**
49
95
  * Adds soft line breaks to a base64 string
50
96
  *
@@ -54,12 +100,26 @@ function decode(str) {
54
100
  */
55
101
  function wrap(str, lineLength) {
56
102
  str = (str || '').toString();
57
- lineLength = lineLength || 76;
103
+ lineLength = normalizeLineLength(lineLength);
58
104
 
59
105
  if (str.length <= lineLength) {
60
106
  return str;
61
107
  }
62
108
 
109
+ // eslint-disable-next-line no-control-regex
110
+ if (/[^\u0000-\u00ff]|[\r\n]/.test(str) || str.trim() !== str) {
111
+ // not a plain base64 string, keep the line based behaviour for whatever this is
112
+ return legacyWrap(str, lineLength);
113
+ }
114
+
115
+ return wrapBuffer(Buffer.from(str, 'latin1'), lineLength, true).output.toString('latin1');
116
+ }
117
+
118
+ /**
119
+ * Line wrapping for input that is not a plain base64 string: line breaks already in the input end a
120
+ * line, and surrounding whitespace is trimmed
121
+ */
122
+ function legacyWrap(str, lineLength) {
63
123
  let result = [];
64
124
  let pos = 0;
65
125
  let chunkLength = lineLength * 1024;
@@ -78,9 +138,15 @@ function wrap(str, lineLength) {
78
138
  /**
79
139
  * Creates a transform stream for encoding data to base64 encoding
80
140
  *
141
+ * The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split into
142
+ * chunks: every line but the last one ends with a line break, the last one does not
143
+ *
81
144
  * @constructor
82
145
  * @param {Object} options Stream options
83
146
  * @param {Number} [options.lineLength=76] Maximum lenght for lines, set to false to disable wrapping
147
+ * @param {Number} [options.skipStartBytes] Number of output bytes to drop from the start
148
+ * @param {Number} [options.limitOutputBytes] Maximum number of output bytes to emit
149
+ * @param {String} [options.startPadding] Characters to prepend to the first line before wrapping
84
150
  */
85
151
  class Encoder extends Transform {
86
152
  constructor(options) {
@@ -89,40 +155,42 @@ class Encoder extends Transform {
89
155
  this.options = options || {};
90
156
 
91
157
  if (this.options.lineLength !== false) {
92
- this.options.lineLength = Number(this.options.lineLength) || 76;
158
+ this.options.lineLength = normalizeLineLength(this.options.lineLength);
93
159
  }
94
160
 
95
161
  this.skipStartBytes = Number(this.options.skipStartBytes) || 0;
96
- this.limitOutbutBytes = Number(this.options.limitOutbutBytes) || 0;
162
+ // `limitOutbutBytes` is the name earlier versions read
163
+ this.limitOutputBytes = Number(this.options.limitOutputBytes || this.options.limitOutbutBytes) || 0;
97
164
 
98
- // startPadding can be used together with skipStartBytes
165
+ // encoded characters of the line that is not complete yet. startPadding can be used together
166
+ // with skipStartBytes
99
167
  this._curLine = this.options.startPadding || '';
100
- this._remainingBytes = false;
168
+ // input bytes that do not make up a complete base64 group yet
169
+ this._remainingBytes = null;
101
170
 
102
171
  this.inputBytes = 0;
103
172
  this.outputBytes = 0;
104
173
  }
105
174
 
106
- _writeChunk(chunk /*, isFinal */) {
175
+ _writeChunk(chunk) {
107
176
  if (this.skipStartBytes) {
108
177
  if (chunk.length <= this.skipStartBytes) {
109
178
  this.skipStartBytes -= chunk.length;
110
179
  return;
111
180
  }
112
181
 
113
- chunk = chunk.slice(this.skipStartBytes);
182
+ chunk = chunk.subarray(this.skipStartBytes);
114
183
  this.skipStartBytes = 0;
115
184
  }
116
185
 
117
- if (this.limitOutbutBytes) {
118
- if (this.outputBytes + chunk.length <= this.limitOutbutBytes) {
119
- // ignore, can use entire chunk
120
- } else if (this.outputBytes >= this.limitOutbutBytes) {
186
+ if (this.limitOutputBytes) {
187
+ if (this.outputBytes >= this.limitOutputBytes) {
121
188
  // chunks already processed
122
189
  return;
123
- } else {
190
+ }
191
+ if (this.outputBytes + chunk.length > this.limitOutputBytes) {
124
192
  // use partial chunk
125
- chunk = chunk.slice(0, this.limitOutbutBytes - this.outputBytes);
193
+ chunk = chunk.subarray(0, this.limitOutputBytes - this.outputBytes);
126
194
  }
127
195
  }
128
196
 
@@ -130,12 +198,26 @@ class Encoder extends Transform {
130
198
  this.push(chunk);
131
199
  }
132
200
 
133
- _getWrapped(str, isFinal) {
134
- str = wrap(str, this.options.lineLength);
135
- if (!isFinal && str.length === this.options.lineLength) {
136
- str += '\r\n';
201
+ /**
202
+ * Emits the encoded characters `b64` that follow the current line, keeping what can not be emitted
203
+ * yet as the new current line
204
+ */
205
+ _emit(b64, final) {
206
+ let src = Buffer.from(this._curLine + b64, 'latin1');
207
+ if (!src.length) {
208
+ return;
209
+ }
210
+
211
+ if (!this.options.lineLength) {
212
+ this._curLine = '';
213
+ return this._writeChunk(src);
214
+ }
215
+
216
+ let { output, rest } = wrapBuffer(src, this.options.lineLength, final);
217
+ this._curLine = rest ? src.toString('latin1', src.length - rest) : '';
218
+ if (output.length) {
219
+ this._writeChunk(output);
137
220
  }
138
- return str;
139
221
  }
140
222
 
141
223
  _transform(chunk, encoding, done) {
@@ -144,58 +226,28 @@ class Encoder extends Transform {
144
226
  }
145
227
 
146
228
  if (!chunk || !chunk.length) {
147
- return setImmediate(done);
229
+ return done();
148
230
  }
149
231
 
150
232
  this.inputBytes += chunk.length;
151
233
 
152
- if (this._remainingBytes && this._remainingBytes.length) {
234
+ if (this._remainingBytes) {
153
235
  chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length);
154
- this._remainingBytes = false;
236
+ this._remainingBytes = null;
155
237
  }
156
238
 
157
- if (chunk.length % 3) {
158
- this._remainingBytes = chunk.slice(chunk.length - (chunk.length % 3));
159
- chunk = chunk.slice(0, chunk.length - (chunk.length % 3));
160
- } else {
161
- this._remainingBytes = false;
162
- }
163
-
164
- let b64 = this._curLine + encode(chunk);
165
-
166
- if (this.options.lineLength) {
167
- b64 = this._getWrapped(b64);
168
-
169
- // remove last line as it is still most probably incomplete
170
- let lastLF = b64.lastIndexOf('\n');
171
- if (lastLF < 0) {
172
- this._curLine = b64;
173
- b64 = '';
174
- } else if (lastLF === b64.length - 1) {
175
- this._curLine = '';
176
- } else {
177
- this._curLine = b64.substr(lastLF + 1);
178
- b64 = b64.substr(0, lastLF + 1);
179
- }
239
+ let extra = chunk.length % 3;
240
+ if (extra) {
241
+ this._remainingBytes = chunk.subarray(chunk.length - extra);
242
+ chunk = chunk.subarray(0, chunk.length - extra);
180
243
  }
181
244
 
182
- if (b64) {
183
- this._writeChunk(Buffer.from(b64, 'ascii'), false);
184
- }
185
-
186
- setImmediate(done);
245
+ this._emit(encode(chunk), false);
246
+ done();
187
247
  }
188
248
 
189
249
  _flush(done) {
190
- if (this._remainingBytes && this._remainingBytes.length) {
191
- this._curLine += encode(this._remainingBytes);
192
- }
193
-
194
- if (this._curLine) {
195
- this._curLine = this._getWrapped(this._curLine, true);
196
- this._writeChunk(Buffer.from(this._curLine, 'ascii'), true);
197
- this._curLine = '';
198
- }
250
+ this._emit(this._remainingBytes ? encode(this._remainingBytes) : '', true);
199
251
  done();
200
252
  }
201
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libbase64",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Encode and decode base64 encoded strings",
5
5
  "main": "lib/libbase64.js",
6
6
  "scripts": {