Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

buffer: add blob endings option, remove Node.js specific encoding option #39708

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,14 +459,20 @@ multiple worker threads.
### `new buffer.Blob([sources[, options]])`
<!-- YAML
added: v15.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/39708
description: Added the standard `endings` option to replace line-endings,
and removed the non-standard `encoding` option.
-->

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

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

### `blob.arrayBuffer()`
<!-- YAML
Expand Down
52 changes: 40 additions & 12 deletions lib/internal/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
PromiseReject,
SafePromisePrototypeFinally,
ReflectConstruct,
RegExpPrototypeSymbolReplace,
RegExpPrototypeTest,
StringPrototypeToLowerCase,
StringPrototypeSplit,
Expand All @@ -24,7 +25,10 @@ const {
getDataObject,
} = internalBinding('blob');

const { TextDecoder } = require('internal/encoding');
const {
TextDecoder,
TextEncoder,
} = require('internal/encoding');

const {
makeTransferable,
Expand All @@ -48,6 +52,7 @@ const {
AbortError,
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_THIS,
ERR_BUFFER_TOO_LARGE,
}
Expand All @@ -68,10 +73,11 @@ const kMaxChunkSize = 65536;

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

let Buffer;
let ReadableStream;
let URL;
let EOL;

const enc = new TextEncoder();

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

function lazyBuffer() {
Buffer ??= require('buffer').Buffer;
return Buffer;
}

function lazyReadableStream(options) {
ReadableStream ??=
require('internal/webstreams/readablestream').ReadableStream;
return new ReadableStream(options);
}

// TODO(@jasnell): This is annoying but this has to be lazy because
// requiring the 'os' module too early causes building Node.js to
// fail with an unknown reference failure.
function lazyEOL() {
EOL ??= require('os').EOL;
return EOL;
}

function isBlob(object) {
return object?.[kHandle] !== undefined;
}

function getSource(source, encoding) {
function getSource(source, endings) {
if (isBlob(source))
return [source.size, source[kHandle]];

if (isAnyArrayBuffer(source)) {
source = new Uint8Array(source);
} else if (!isArrayBufferView(source)) {
source = lazyBuffer().from(`${source}`, encoding);
source = `${source}`;
if (endings === 'native')
source = RegExpPrototypeSymbolReplace(/\n|\r\n/g, source, lazyEOL());
source = enc.encode(source);
}

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

class Blob {
/**
* @typedef {string|ArrayBuffer|ArrayBufferView|Blob} SourcePart
*
* @param {SourcePart[]} [sources]
* @param {{
* endings? : string,
* type? : string,
* }} [options]
* @returns
*/
constructor(sources = [], options = {}) {
emitExperimentalWarning('buffer.Blob');
if (sources === null ||
Expand All @@ -124,12 +146,18 @@ class Blob {
throw new ERR_INVALID_ARG_TYPE('sources', 'Iterable', sources);
}
validateObject(options, 'options');
const { encoding = 'utf8' } = options;
let { type = '' } = options;
let {
type = '',
endings = 'transparent',
} = options;

endings = `${endings}`;
if (endings !== 'transparent' && endings !== 'native')
throw new ERR_INVALID_ARG_VALUE('options.endings', endings);

let length = 0;
const sources_ = ArrayFrom(sources, (source) => {
const { 0: len, 1: src } = getSource(source, encoding);
const { 0: len, 1: src } = getSource(source, endings);
length += len;
return src;
});
Expand Down
22 changes: 13 additions & 9 deletions test/parallel/test-blob.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Flags: --no-warnings
'use strict';

const common = require('../common');
const assert = require('assert');
const { Blob } = require('buffer');
const { inspect } = require('util');
const { EOL } = require('os');

{
const b = new Blob();
Expand Down Expand Up @@ -45,15 +47,6 @@ assert.throws(() => new Blob({}), {
assert.strictEqual(new Blob([], { type: {} }).type, '[object object]');
}

{
const b = new Blob(['616263'], { encoding: 'hex', type: 'foo' });
assert.strictEqual(b.size, 3);
assert.strictEqual(b.type, 'foo');
b.text().then(common.mustCall((text) => {
assert.strictEqual(text, 'abc');
}));
}

{
const b = new Blob([Buffer.from('abc')]);
assert.strictEqual(b.size, 3);
Expand Down Expand Up @@ -216,3 +209,14 @@ assert.throws(() => new Blob({}), {
res = await reader.read();
assert(res.done);
})().then(common.mustCall());

{
const b = new Blob(['hello\n'], { endings: 'native' });
assert.strictEqual(b.size, EOL.length + 5);

[1, {}, 'foo'].forEach((endings) => {
assert.throws(() => new Blob([], { endings }), {
code: 'ERR_INVALID_ARG_VALUE',
});
});
}