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 +7 -0
- package/lib/libbase64.js +110 -58
- package/package.json +1 -1
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
|
|
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 =
|
|
158
|
+
this.options.lineLength = normalizeLineLength(this.options.lineLength);
|
|
93
159
|
}
|
|
94
160
|
|
|
95
161
|
this.skipStartBytes = Number(this.options.skipStartBytes) || 0;
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
182
|
+
chunk = chunk.subarray(this.skipStartBytes);
|
|
114
183
|
this.skipStartBytes = 0;
|
|
115
184
|
}
|
|
116
185
|
|
|
117
|
-
if (this.
|
|
118
|
-
if (this.outputBytes
|
|
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
|
-
}
|
|
190
|
+
}
|
|
191
|
+
if (this.outputBytes + chunk.length > this.limitOutputBytes) {
|
|
124
192
|
// use partial chunk
|
|
125
|
-
chunk = chunk.
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
229
|
+
return done();
|
|
148
230
|
}
|
|
149
231
|
|
|
150
232
|
this.inputBytes += chunk.length;
|
|
151
233
|
|
|
152
|
-
if (this._remainingBytes
|
|
234
|
+
if (this._remainingBytes) {
|
|
153
235
|
chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length);
|
|
154
|
-
this._remainingBytes =
|
|
236
|
+
this._remainingBytes = null;
|
|
155
237
|
}
|
|
156
238
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
setImmediate(done);
|
|
245
|
+
this._emit(encode(chunk), false);
|
|
246
|
+
done();
|
|
187
247
|
}
|
|
188
248
|
|
|
189
249
|
_flush(done) {
|
|
190
|
-
|
|
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
|
}
|