libbase64 1.3.0 → 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/.github/workflows/release.yaml +12 -10
- package/.ncurc.js +7 -1
- package/CHANGELOG.md +14 -0
- package/lib/libbase64.js +137 -58
- package/package.json +4 -4
|
@@ -2,6 +2,9 @@ on:
|
|
|
2
2
|
push:
|
|
3
3
|
branches:
|
|
4
4
|
- master
|
|
5
|
+
# Manual run: publish the version currently on master (used when a release was cut
|
|
6
|
+
# but the publish step failed)
|
|
7
|
+
workflow_dispatch:
|
|
5
8
|
|
|
6
9
|
permissions:
|
|
7
10
|
contents: write
|
|
@@ -15,23 +18,22 @@ jobs:
|
|
|
15
18
|
steps:
|
|
16
19
|
- uses: google-github-actions/release-please-action@v3
|
|
17
20
|
id: release
|
|
21
|
+
if: ${{ github.event_name == 'push' }}
|
|
18
22
|
with:
|
|
19
23
|
release-type: node
|
|
20
24
|
package-name: ${{vars.NPM_MODULE_NAME}}
|
|
21
25
|
pull-request-title-pattern: 'chore${scope}: release ${version} [skip-ci]'
|
|
22
|
-
# The logic below handles the npm publication
|
|
26
|
+
# The logic below handles the npm publication through npm trusted publishing
|
|
27
|
+
# (OIDC, no token secret): it runs when a new release is created, or on a
|
|
28
|
+
# manual dispatch.
|
|
23
29
|
- uses: actions/checkout@v4
|
|
24
|
-
|
|
25
|
-
# a new release is created:
|
|
26
|
-
if: ${{ steps.release.outputs.release_created }}
|
|
30
|
+
if: ${{ steps.release.outputs.release_created || github.event_name == 'workflow_dispatch' }}
|
|
27
31
|
- uses: actions/setup-node@v4
|
|
28
32
|
with:
|
|
29
|
-
node-version:
|
|
33
|
+
node-version: 24
|
|
30
34
|
registry-url: 'https://registry.npmjs.org'
|
|
31
|
-
if: ${{ steps.release.outputs.release_created }}
|
|
35
|
+
if: ${{ steps.release.outputs.release_created || github.event_name == 'workflow_dispatch' }}
|
|
32
36
|
- run: npm ci
|
|
33
|
-
if: ${{ steps.release.outputs.release_created }}
|
|
37
|
+
if: ${{ steps.release.outputs.release_created || github.event_name == 'workflow_dispatch' }}
|
|
34
38
|
- run: npm publish --provenance --access public
|
|
35
|
-
|
|
36
|
-
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
37
|
-
if: ${{ steps.release.outputs.release_created }}
|
|
39
|
+
if: ${{ steps.release.outputs.release_created || github.event_name == 'workflow_dispatch' }}
|
package/.ncurc.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
upgrade: true,
|
|
3
3
|
reject: [
|
|
4
|
+
// 12 is esm only ("type": "module"), and grunt-mocha-test require()s it as a
|
|
5
|
+
// constructor - "Mocha is not a constructor". Node 24 hides this through
|
|
6
|
+
// require(esm); CI on Node 22 does not. Lift it when the runner drops grunt.
|
|
7
|
+
'mocha',
|
|
4
8
|
// 5x is esm only
|
|
5
|
-
'chai'
|
|
9
|
+
'chai',
|
|
10
|
+
// api changes in newer eslint
|
|
11
|
+
'grunt-eslint'
|
|
6
12
|
]
|
|
7
13
|
};
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [1.3.1](https://github.com/nodemailer/libbase64/compare/v1.3.0...v1.3.1) (2026-09-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **decoder:** decode every padded segment instead of stopping at the first padding ([d9ba154](https://github.com/nodemailer/libbase64/commit/d9ba154ba13e45338936a55acec417dfb6ae2e52))
|
|
16
|
+
|
|
3
17
|
## [1.3.0](https://github.com/nodemailer/libbase64/compare/v1.2.1...v1.3.0) (2024-02-23)
|
|
4
18
|
|
|
5
19
|
|
package/lib/libbase64.js
CHANGED
|
@@ -26,9 +26,71 @@ function encode(buffer) {
|
|
|
26
26
|
*/
|
|
27
27
|
function decode(str) {
|
|
28
28
|
str = str || '';
|
|
29
|
+
|
|
30
|
+
// Buffer.from() stops decoding at the first padding char, so input made of several
|
|
31
|
+
// padded segments (eg. every line padded on its own) is decoded segment by segment
|
|
32
|
+
if (typeof str === 'string') {
|
|
33
|
+
let padPos = str.indexOf('=');
|
|
34
|
+
if (padPos >= 0 && /[a-zA-Z0-9+/\-_]/.test(str.substr(padPos))) {
|
|
35
|
+
let parts = [];
|
|
36
|
+
for (let segment of str.split(/[=]+/)) {
|
|
37
|
+
if (segment) {
|
|
38
|
+
parts.push(Buffer.from(segment, 'base64'));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Buffer.concat(parts);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
return Buffer.from(str, 'base64');
|
|
30
46
|
}
|
|
31
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
|
+
|
|
32
94
|
/**
|
|
33
95
|
* Adds soft line breaks to a base64 string
|
|
34
96
|
*
|
|
@@ -38,12 +100,26 @@ function decode(str) {
|
|
|
38
100
|
*/
|
|
39
101
|
function wrap(str, lineLength) {
|
|
40
102
|
str = (str || '').toString();
|
|
41
|
-
lineLength = lineLength
|
|
103
|
+
lineLength = normalizeLineLength(lineLength);
|
|
42
104
|
|
|
43
105
|
if (str.length <= lineLength) {
|
|
44
106
|
return str;
|
|
45
107
|
}
|
|
46
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) {
|
|
47
123
|
let result = [];
|
|
48
124
|
let pos = 0;
|
|
49
125
|
let chunkLength = lineLength * 1024;
|
|
@@ -62,9 +138,15 @@ function wrap(str, lineLength) {
|
|
|
62
138
|
/**
|
|
63
139
|
* Creates a transform stream for encoding data to base64 encoding
|
|
64
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
|
+
*
|
|
65
144
|
* @constructor
|
|
66
145
|
* @param {Object} options Stream options
|
|
67
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
|
|
68
150
|
*/
|
|
69
151
|
class Encoder extends Transform {
|
|
70
152
|
constructor(options) {
|
|
@@ -73,40 +155,42 @@ class Encoder extends Transform {
|
|
|
73
155
|
this.options = options || {};
|
|
74
156
|
|
|
75
157
|
if (this.options.lineLength !== false) {
|
|
76
|
-
this.options.lineLength =
|
|
158
|
+
this.options.lineLength = normalizeLineLength(this.options.lineLength);
|
|
77
159
|
}
|
|
78
160
|
|
|
79
161
|
this.skipStartBytes = Number(this.options.skipStartBytes) || 0;
|
|
80
|
-
|
|
162
|
+
// `limitOutbutBytes` is the name earlier versions read
|
|
163
|
+
this.limitOutputBytes = Number(this.options.limitOutputBytes || this.options.limitOutbutBytes) || 0;
|
|
81
164
|
|
|
82
|
-
// startPadding can be used together
|
|
165
|
+
// encoded characters of the line that is not complete yet. startPadding can be used together
|
|
166
|
+
// with skipStartBytes
|
|
83
167
|
this._curLine = this.options.startPadding || '';
|
|
84
|
-
|
|
168
|
+
// input bytes that do not make up a complete base64 group yet
|
|
169
|
+
this._remainingBytes = null;
|
|
85
170
|
|
|
86
171
|
this.inputBytes = 0;
|
|
87
172
|
this.outputBytes = 0;
|
|
88
173
|
}
|
|
89
174
|
|
|
90
|
-
_writeChunk(chunk
|
|
175
|
+
_writeChunk(chunk) {
|
|
91
176
|
if (this.skipStartBytes) {
|
|
92
177
|
if (chunk.length <= this.skipStartBytes) {
|
|
93
178
|
this.skipStartBytes -= chunk.length;
|
|
94
179
|
return;
|
|
95
180
|
}
|
|
96
181
|
|
|
97
|
-
chunk = chunk.
|
|
182
|
+
chunk = chunk.subarray(this.skipStartBytes);
|
|
98
183
|
this.skipStartBytes = 0;
|
|
99
184
|
}
|
|
100
185
|
|
|
101
|
-
if (this.
|
|
102
|
-
if (this.outputBytes
|
|
103
|
-
// ignore, can use entire chunk
|
|
104
|
-
} else if (this.outputBytes >= this.limitOutbutBytes) {
|
|
186
|
+
if (this.limitOutputBytes) {
|
|
187
|
+
if (this.outputBytes >= this.limitOutputBytes) {
|
|
105
188
|
// chunks already processed
|
|
106
189
|
return;
|
|
107
|
-
}
|
|
190
|
+
}
|
|
191
|
+
if (this.outputBytes + chunk.length > this.limitOutputBytes) {
|
|
108
192
|
// use partial chunk
|
|
109
|
-
chunk = chunk.
|
|
193
|
+
chunk = chunk.subarray(0, this.limitOutputBytes - this.outputBytes);
|
|
110
194
|
}
|
|
111
195
|
}
|
|
112
196
|
|
|
@@ -114,12 +198,26 @@ class Encoder extends Transform {
|
|
|
114
198
|
this.push(chunk);
|
|
115
199
|
}
|
|
116
200
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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);
|
|
121
220
|
}
|
|
122
|
-
return str;
|
|
123
221
|
}
|
|
124
222
|
|
|
125
223
|
_transform(chunk, encoding, done) {
|
|
@@ -128,58 +226,28 @@ class Encoder extends Transform {
|
|
|
128
226
|
}
|
|
129
227
|
|
|
130
228
|
if (!chunk || !chunk.length) {
|
|
131
|
-
return
|
|
229
|
+
return done();
|
|
132
230
|
}
|
|
133
231
|
|
|
134
232
|
this.inputBytes += chunk.length;
|
|
135
233
|
|
|
136
|
-
if (this._remainingBytes
|
|
234
|
+
if (this._remainingBytes) {
|
|
137
235
|
chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length);
|
|
138
|
-
this._remainingBytes =
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (chunk.length % 3) {
|
|
142
|
-
this._remainingBytes = chunk.slice(chunk.length - (chunk.length % 3));
|
|
143
|
-
chunk = chunk.slice(0, chunk.length - (chunk.length % 3));
|
|
144
|
-
} else {
|
|
145
|
-
this._remainingBytes = false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let b64 = this._curLine + encode(chunk);
|
|
149
|
-
|
|
150
|
-
if (this.options.lineLength) {
|
|
151
|
-
b64 = this._getWrapped(b64);
|
|
152
|
-
|
|
153
|
-
// remove last line as it is still most probably incomplete
|
|
154
|
-
let lastLF = b64.lastIndexOf('\n');
|
|
155
|
-
if (lastLF < 0) {
|
|
156
|
-
this._curLine = b64;
|
|
157
|
-
b64 = '';
|
|
158
|
-
} else if (lastLF === b64.length - 1) {
|
|
159
|
-
this._curLine = '';
|
|
160
|
-
} else {
|
|
161
|
-
this._curLine = b64.substr(lastLF + 1);
|
|
162
|
-
b64 = b64.substr(0, lastLF + 1);
|
|
163
|
-
}
|
|
236
|
+
this._remainingBytes = null;
|
|
164
237
|
}
|
|
165
238
|
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
|
168
243
|
}
|
|
169
244
|
|
|
170
|
-
|
|
245
|
+
this._emit(encode(chunk), false);
|
|
246
|
+
done();
|
|
171
247
|
}
|
|
172
248
|
|
|
173
249
|
_flush(done) {
|
|
174
|
-
|
|
175
|
-
this._curLine += encode(this._remainingBytes);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (this._curLine) {
|
|
179
|
-
this._curLine = this._getWrapped(this._curLine, true);
|
|
180
|
-
this._writeChunk(Buffer.from(this._curLine, 'ascii'), true);
|
|
181
|
-
this._curLine = '';
|
|
182
|
-
}
|
|
250
|
+
this._emit(this._remainingBytes ? encode(this._remainingBytes) : '', true);
|
|
183
251
|
done();
|
|
184
252
|
}
|
|
185
253
|
}
|
|
@@ -215,6 +283,15 @@ class Decoder extends Transform {
|
|
|
215
283
|
b64 = b64.replace(/[^a-zA-Z0-9+/=]/g, '');
|
|
216
284
|
}
|
|
217
285
|
|
|
286
|
+
// everything up to the last padding char ends in complete segments, quartets are
|
|
287
|
+
// only counted from there on as a padding run restarts the quartet alignment
|
|
288
|
+
let padded = '';
|
|
289
|
+
let lastPad = b64.lastIndexOf('=');
|
|
290
|
+
if (lastPad >= 0) {
|
|
291
|
+
padded = b64.substr(0, lastPad + 1);
|
|
292
|
+
b64 = b64.substr(lastPad + 1);
|
|
293
|
+
}
|
|
294
|
+
|
|
218
295
|
if (b64.length < 4) {
|
|
219
296
|
this._curLine = b64;
|
|
220
297
|
b64 = '';
|
|
@@ -223,6 +300,8 @@ class Decoder extends Transform {
|
|
|
223
300
|
b64 = b64.substr(0, b64.length - this._curLine.length);
|
|
224
301
|
}
|
|
225
302
|
|
|
303
|
+
b64 = padded + b64;
|
|
304
|
+
|
|
226
305
|
if (b64) {
|
|
227
306
|
let buf = decode(b64);
|
|
228
307
|
this.outputBytes += buf.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libbase64",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Encode and decode base64 encoded strings",
|
|
5
5
|
"main": "lib/libbase64.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"chai": "4.4.1",
|
|
26
26
|
"eslint-config-nodemailer": "1.2.0",
|
|
27
|
-
"eslint-config-prettier": "
|
|
28
|
-
"grunt": "1.6.
|
|
29
|
-
"grunt-cli": "1.
|
|
27
|
+
"eslint-config-prettier": "10.1.8",
|
|
28
|
+
"grunt": "1.6.3",
|
|
29
|
+
"grunt-cli": "1.5.0",
|
|
30
30
|
"grunt-eslint": "24.3.0",
|
|
31
31
|
"grunt-mocha-test": "0.13.3",
|
|
32
32
|
"mocha": "10.3.0"
|