Skip to content

Commit 5bc31ea

Browse files
jasnelldanielleadams
authored andcommitted
buffer: add endings option, remove Node.js specific encoding option
Signed-off-by: James M Snell <[email protected]> PR-URL: #39708 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent cd9b0bf commit 5bc31ea

File tree

3 files changed

+64
-24
lines changed

3 files changed

+64
-24
lines changed

doc/api/buffer.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,20 @@ multiple worker threads.
459459
### `new buffer.Blob([sources[, options]])`
460460
<!-- YAML
461461
added: v15.7.0
462+
changes:
463+
- version: REPLACEME
464+
pr-url: https://github.com/nodejs/node/pull/39708
465+
description: Added the standard `endings` option to replace line-endings,
466+
and removed the non-standard `encoding` option.
462467
-->
463468

464469
* `sources` {string[]|ArrayBuffer[]|TypedArray[]|DataView[]|Blob[]} An array
465470
of string, {ArrayBuffer}, {TypedArray}, {DataView}, or {Blob} objects, or
466471
any mix of such objects, that will be stored within the `Blob`.
467472
* `options` {Object}
468-
* `encoding` {string} The character encoding to use for string sources.
469-
**Default:** `'utf8'`.
473+
* `endings` {string} One of either `'transparent'` or `'native'`. When set
474+
to `'native'`, line endings in string source parts will be converted to
475+
the platform native line-ending as specified by `require('os').EOL`.
470476
* `type` {string} The Blob content-type. The intent is for `type` to convey
471477
the MIME media type of the data, however no validation of the type format
472478
is performed.
@@ -476,7 +482,9 @@ Creates a new `Blob` object containing a concatenation of the given sources.
476482
{ArrayBuffer}, {TypedArray}, {DataView}, and {Buffer} sources are copied into
477483
the 'Blob' and can therefore be safely modified after the 'Blob' is created.
478484

479-
String sources are also copied into the `Blob`.
485+
String sources are encoded as UTF-8 byte sequences and copied into the Blob.
486+
Unmatched surrogate pairs within each string part will be replaced by Unicode
487+
U+FFFD replacement characters.
480488

481489
### `blob.arrayBuffer()`
482490
<!-- YAML

lib/internal/blob.js

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
PromiseReject,
1010
SafePromisePrototypeFinally,
1111
ReflectConstruct,
12+
RegExpPrototypeSymbolReplace,
1213
RegExpPrototypeTest,
1314
StringPrototypeToLowerCase,
1415
StringPrototypeSplit,
@@ -24,7 +25,10 @@ const {
2425
getDataObject,
2526
} = internalBinding('blob');
2627

27-
const { TextDecoder } = require('internal/encoding');
28+
const {
29+
TextDecoder,
30+
TextEncoder,
31+
} = require('internal/encoding');
2832

2933
const {
3034
makeTransferable,
@@ -48,6 +52,7 @@ const {
4852
AbortError,
4953
codes: {
5054
ERR_INVALID_ARG_TYPE,
55+
ERR_INVALID_ARG_VALUE,
5156
ERR_INVALID_THIS,
5257
ERR_BUFFER_TOO_LARGE,
5358
}
@@ -68,10 +73,11 @@ const kMaxChunkSize = 65536;
6873

6974
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
7075

71-
let Buffer;
7276
let ReadableStream;
7377
let URL;
78+
let EOL;
7479

80+
const enc = new TextEncoder();
7581

7682
// Yes, lazy loading is annoying but because of circular
7783
// references between the url, internal/blob, and buffer
@@ -82,29 +88,35 @@ function lazyURL(id) {
8288
return new URL(id);
8389
}
8490

85-
function lazyBuffer() {
86-
Buffer ??= require('buffer').Buffer;
87-
return Buffer;
88-
}
89-
9091
function lazyReadableStream(options) {
9192
ReadableStream ??=
9293
require('internal/webstreams/readablestream').ReadableStream;
9394
return new ReadableStream(options);
9495
}
9596

97+
// TODO(@jasnell): This is annoying but this has to be lazy because
98+
// requiring the 'os' module too early causes building Node.js to
99+
// fail with an unknown reference failure.
100+
function lazyEOL() {
101+
EOL ??= require('os').EOL;
102+
return EOL;
103+
}
104+
96105
function isBlob(object) {
97106
return object?.[kHandle] !== undefined;
98107
}
99108

100-
function getSource(source, encoding) {
109+
function getSource(source, endings) {
101110
if (isBlob(source))
102111
return [source.size, source[kHandle]];
103112

104113
if (isAnyArrayBuffer(source)) {
105114
source = new Uint8Array(source);
106115
} else if (!isArrayBufferView(source)) {
107-
source = lazyBuffer().from(`${source}`, encoding);
116+
source = `${source}`;
117+
if (endings === 'native')
118+
source = RegExpPrototypeSymbolReplace(/\n|\r\n/g, source, lazyEOL());
119+
source = enc.encode(source);
108120
}
109121

110122
// We copy into a new Uint8Array because the underlying
@@ -116,6 +128,16 @@ function getSource(source, encoding) {
116128
}
117129

118130
class Blob {
131+
/**
132+
* @typedef {string|ArrayBuffer|ArrayBufferView|Blob} SourcePart
133+
*
134+
* @param {SourcePart[]} [sources]
135+
* @param {{
136+
* endings? : string,
137+
* type? : string,
138+
* }} [options]
139+
* @returns
140+
*/
119141
constructor(sources = [], options = {}) {
120142
emitExperimentalWarning('buffer.Blob');
121143
if (sources === null ||
@@ -124,12 +146,18 @@ class Blob {
124146
throw new ERR_INVALID_ARG_TYPE('sources', 'Iterable', sources);
125147
}
126148
validateObject(options, 'options');
127-
const { encoding = 'utf8' } = options;
128-
let { type = '' } = options;
149+
let {
150+
type = '',
151+
endings = 'transparent',
152+
} = options;
153+
154+
endings = `${endings}`;
155+
if (endings !== 'transparent' && endings !== 'native')
156+
throw new ERR_INVALID_ARG_VALUE('options.endings', endings);
129157

130158
let length = 0;
131159
const sources_ = ArrayFrom(sources, (source) => {
132-
const { 0: len, 1: src } = getSource(source, encoding);
160+
const { 0: len, 1: src } = getSource(source, endings);
133161
length += len;
134162
return src;
135163
});

test/parallel/test-blob.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
// Flags: --no-warnings
12
'use strict';
23

34
const common = require('../common');
45
const assert = require('assert');
56
const { Blob } = require('buffer');
67
const { inspect } = require('util');
8+
const { EOL } = require('os');
79

810
{
911
const b = new Blob();
@@ -45,15 +47,6 @@ assert.throws(() => new Blob({}), {
4547
assert.strictEqual(new Blob([], { type: {} }).type, '[object object]');
4648
}
4749

48-
{
49-
const b = new Blob(['616263'], { encoding: 'hex', type: 'foo' });
50-
assert.strictEqual(b.size, 3);
51-
assert.strictEqual(b.type, 'foo');
52-
b.text().then(common.mustCall((text) => {
53-
assert.strictEqual(text, 'abc');
54-
}));
55-
}
56-
5750
{
5851
const b = new Blob([Buffer.from('abc')]);
5952
assert.strictEqual(b.size, 3);
@@ -216,3 +209,14 @@ assert.throws(() => new Blob({}), {
216209
res = await reader.read();
217210
assert(res.done);
218211
})().then(common.mustCall());
212+
213+
{
214+
const b = new Blob(['hello\n'], { endings: 'native' });
215+
assert.strictEqual(b.size, EOL.length + 5);
216+
217+
[1, {}, 'foo'].forEach((endings) => {
218+
assert.throws(() => new Blob([], { endings }), {
219+
code: 'ERR_INVALID_ARG_VALUE',
220+
});
221+
});
222+
}

0 commit comments

Comments
 (0)