mirror of
https://github.com/nodejs/node.git
synced 2025-05-03 07:36:51 +00:00

PR-URL: https://github.com/nodejs/node/pull/43475 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
409 lines
9.1 KiB
JavaScript
409 lines
9.1 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayFrom,
|
|
MathMax,
|
|
MathMin,
|
|
ObjectDefineProperty,
|
|
PromiseResolve,
|
|
PromiseReject,
|
|
SafePromisePrototypeFinally,
|
|
ReflectConstruct,
|
|
RegExpPrototypeExec,
|
|
RegExpPrototypeSymbolReplace,
|
|
StringPrototypeToLowerCase,
|
|
StringPrototypeSplit,
|
|
Symbol,
|
|
SymbolIterator,
|
|
SymbolToStringTag,
|
|
Uint8Array,
|
|
} = primordials;
|
|
|
|
const {
|
|
createBlob: _createBlob,
|
|
FixedSizeBlobCopyJob,
|
|
getDataObject,
|
|
} = internalBinding('blob');
|
|
|
|
const {
|
|
TextDecoder,
|
|
TextEncoder,
|
|
} = require('internal/encoding');
|
|
|
|
const {
|
|
makeTransferable,
|
|
kClone,
|
|
kDeserialize,
|
|
} = require('internal/worker/js_transferable');
|
|
|
|
const {
|
|
isAnyArrayBuffer,
|
|
isArrayBufferView,
|
|
} = require('internal/util/types');
|
|
|
|
const {
|
|
createDeferredPromise,
|
|
customInspectSymbol: kInspect,
|
|
kEmptyObject,
|
|
} = require('internal/util');
|
|
const { inspect } = require('internal/util/inspect');
|
|
|
|
const {
|
|
AbortError,
|
|
codes: {
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_INVALID_THIS,
|
|
ERR_BUFFER_TOO_LARGE,
|
|
}
|
|
} = require('internal/errors');
|
|
|
|
const {
|
|
validateObject,
|
|
isUint32,
|
|
} = require('internal/validators');
|
|
|
|
const kHandle = Symbol('kHandle');
|
|
const kState = Symbol('kState');
|
|
const kIndex = Symbol('kIndex');
|
|
const kType = Symbol('kType');
|
|
const kLength = Symbol('kLength');
|
|
const kArrayBufferPromise = Symbol('kArrayBufferPromise');
|
|
|
|
const kMaxChunkSize = 65536;
|
|
|
|
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
|
|
|
|
let ReadableStream;
|
|
let URL;
|
|
|
|
const enc = new TextEncoder();
|
|
|
|
// Yes, lazy loading is annoying but because of circular
|
|
// references between the url, internal/blob, and buffer
|
|
// modules, lazy loading here makes sure that things work.
|
|
|
|
function lazyURL(id) {
|
|
URL ??= require('internal/url').URL;
|
|
return new URL(id);
|
|
}
|
|
|
|
function lazyReadableStream(options) {
|
|
// eslint-disable-next-line no-global-assign
|
|
ReadableStream ??=
|
|
require('internal/webstreams/readablestream').ReadableStream;
|
|
return new ReadableStream(options);
|
|
}
|
|
|
|
const { EOL } = require('internal/constants');
|
|
|
|
function isBlob(object) {
|
|
return object?.[kHandle] !== undefined;
|
|
}
|
|
|
|
function getSource(source, endings) {
|
|
if (isBlob(source))
|
|
return [source.size, source[kHandle]];
|
|
|
|
if (isAnyArrayBuffer(source)) {
|
|
source = new Uint8Array(source);
|
|
} else if (!isArrayBufferView(source)) {
|
|
source = `${source}`;
|
|
if (endings === 'native')
|
|
source = RegExpPrototypeSymbolReplace(/\n|\r\n/g, source, EOL);
|
|
source = enc.encode(source);
|
|
}
|
|
|
|
// We copy into a new Uint8Array because the underlying
|
|
// BackingStores are going to be detached and owned by
|
|
// the Blob.
|
|
const { buffer, byteOffset, byteLength } = source;
|
|
const slice = buffer.slice(byteOffset, byteOffset + byteLength);
|
|
return [byteLength, new Uint8Array(slice)];
|
|
}
|
|
|
|
class Blob {
|
|
/**
|
|
* @typedef {string|ArrayBuffer|ArrayBufferView|Blob} SourcePart
|
|
*/
|
|
|
|
/**
|
|
* @param {SourcePart[]} [sources]
|
|
* @param {{
|
|
* endings? : string,
|
|
* type? : string,
|
|
* }} [options]
|
|
* @constructs {Blob}
|
|
*/
|
|
constructor(sources = [], options = kEmptyObject) {
|
|
if (sources === null ||
|
|
typeof sources[SymbolIterator] !== 'function' ||
|
|
typeof sources === 'string') {
|
|
throw new ERR_INVALID_ARG_TYPE('sources', 'a sequence', sources);
|
|
}
|
|
validateObject(options, '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, endings);
|
|
length += len;
|
|
return src;
|
|
});
|
|
|
|
if (!isUint32(length))
|
|
throw new ERR_BUFFER_TOO_LARGE(0xFFFFFFFF);
|
|
|
|
this[kHandle] = _createBlob(sources_, length);
|
|
this[kLength] = length;
|
|
|
|
type = `${type}`;
|
|
this[kType] = RegExpPrototypeExec(disallowedTypeCharacters, type) !== null ?
|
|
'' : StringPrototypeToLowerCase(type);
|
|
|
|
// eslint-disable-next-line no-constructor-return
|
|
return makeTransferable(this);
|
|
}
|
|
|
|
[kInspect](depth, options) {
|
|
if (depth < 0)
|
|
return this;
|
|
|
|
const opts = {
|
|
...options,
|
|
depth: options.depth == null ? null : options.depth - 1
|
|
};
|
|
|
|
return `Blob ${inspect({
|
|
size: this.size,
|
|
type: this.type,
|
|
}, opts)}`;
|
|
}
|
|
|
|
[kClone]() {
|
|
const handle = this[kHandle];
|
|
const type = this[kType];
|
|
const length = this[kLength];
|
|
return {
|
|
data: { handle, type, length },
|
|
deserializeInfo: 'internal/blob:ClonedBlob'
|
|
};
|
|
}
|
|
|
|
[kDeserialize]({ handle, type, length }) {
|
|
this[kHandle] = handle;
|
|
this[kType] = type;
|
|
this[kLength] = length;
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {string}
|
|
*/
|
|
get type() {
|
|
if (!isBlob(this))
|
|
throw new ERR_INVALID_THIS('Blob');
|
|
return this[kType];
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {number}
|
|
*/
|
|
get size() {
|
|
if (!isBlob(this))
|
|
throw new ERR_INVALID_THIS('Blob');
|
|
return this[kLength];
|
|
}
|
|
|
|
/**
|
|
* @param {number} [start]
|
|
* @param {number} [end]
|
|
* @param {string} [contentType]
|
|
* @returns {Blob}
|
|
*/
|
|
slice(start = 0, end = this[kLength], contentType = '') {
|
|
if (!isBlob(this))
|
|
throw new ERR_INVALID_THIS('Blob');
|
|
if (start < 0) {
|
|
start = MathMax(this[kLength] + start, 0);
|
|
} else {
|
|
start = MathMin(start, this[kLength]);
|
|
}
|
|
start |= 0;
|
|
|
|
if (end < 0) {
|
|
end = MathMax(this[kLength] + end, 0);
|
|
} else {
|
|
end = MathMin(end, this[kLength]);
|
|
}
|
|
end |= 0;
|
|
|
|
contentType = `${contentType}`;
|
|
if (RegExpPrototypeExec(disallowedTypeCharacters, contentType) !== null) {
|
|
contentType = '';
|
|
} else {
|
|
contentType = StringPrototypeToLowerCase(contentType);
|
|
}
|
|
|
|
const span = MathMax(end - start, 0);
|
|
|
|
return createBlob(
|
|
this[kHandle].slice(start, start + span),
|
|
span,
|
|
contentType);
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<ArrayBuffer>}
|
|
*/
|
|
arrayBuffer() {
|
|
if (!isBlob(this))
|
|
return PromiseReject(new ERR_INVALID_THIS('Blob'));
|
|
|
|
// If there's already a promise in flight for the content,
|
|
// reuse it, but only while it's in flight. After the cached
|
|
// promise resolves it will be cleared, allowing it to be
|
|
// garbage collected as soon as possible.
|
|
if (this[kArrayBufferPromise])
|
|
return this[kArrayBufferPromise];
|
|
|
|
const job = new FixedSizeBlobCopyJob(this[kHandle]);
|
|
|
|
const ret = job.run();
|
|
|
|
// If the job returns a value immediately, the ArrayBuffer
|
|
// was generated synchronously and should just be returned
|
|
// directly.
|
|
if (ret !== undefined)
|
|
return PromiseResolve(ret);
|
|
|
|
const {
|
|
promise,
|
|
resolve,
|
|
reject,
|
|
} = createDeferredPromise();
|
|
|
|
job.ondone = (err, ab) => {
|
|
if (err !== undefined)
|
|
return reject(new AbortError(undefined, { cause: err }));
|
|
resolve(ab);
|
|
};
|
|
this[kArrayBufferPromise] =
|
|
SafePromisePrototypeFinally(
|
|
promise,
|
|
() => this[kArrayBufferPromise] = undefined);
|
|
|
|
return this[kArrayBufferPromise];
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async text() {
|
|
if (!isBlob(this))
|
|
throw new ERR_INVALID_THIS('Blob');
|
|
|
|
const dec = new TextDecoder();
|
|
return dec.decode(await this.arrayBuffer());
|
|
}
|
|
|
|
/**
|
|
* @returns {ReadableStream}
|
|
*/
|
|
stream() {
|
|
if (!isBlob(this))
|
|
throw new ERR_INVALID_THIS('Blob');
|
|
|
|
const self = this;
|
|
return new lazyReadableStream({
|
|
async start() {
|
|
this[kState] = await self.arrayBuffer();
|
|
this[kIndex] = 0;
|
|
},
|
|
|
|
pull(controller) {
|
|
if (this[kState].byteLength - this[kIndex] <= kMaxChunkSize) {
|
|
controller.enqueue(new Uint8Array(this[kState], this[kIndex]));
|
|
controller.close();
|
|
this[kState] = undefined;
|
|
} else {
|
|
controller.enqueue(new Uint8Array(this[kState], this[kIndex], kMaxChunkSize));
|
|
this[kIndex] += kMaxChunkSize;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function ClonedBlob() {
|
|
return makeTransferable(ReflectConstruct(function() {}, [], Blob));
|
|
}
|
|
ClonedBlob.prototype[kDeserialize] = () => {};
|
|
|
|
function createBlob(handle, length, type = '') {
|
|
return makeTransferable(ReflectConstruct(function() {
|
|
this[kHandle] = handle;
|
|
this[kType] = type;
|
|
this[kLength] = length;
|
|
}, [], Blob));
|
|
}
|
|
|
|
ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
|
|
__proto__: null,
|
|
configurable: true,
|
|
value: 'Blob',
|
|
});
|
|
|
|
function resolveObjectURL(url) {
|
|
url = `${url}`;
|
|
try {
|
|
const parsed = new lazyURL(url);
|
|
|
|
const split = StringPrototypeSplit(parsed.pathname, ':');
|
|
|
|
if (split.length !== 2)
|
|
return;
|
|
|
|
const {
|
|
0: base,
|
|
1: id,
|
|
} = split;
|
|
|
|
if (base !== 'nodedata')
|
|
return;
|
|
|
|
const ret = getDataObject(id);
|
|
|
|
if (ret === undefined)
|
|
return;
|
|
|
|
const {
|
|
0: handle,
|
|
1: length,
|
|
2: type,
|
|
} = ret;
|
|
|
|
if (handle !== undefined)
|
|
return createBlob(handle, length, type);
|
|
} catch {
|
|
// If there's an error, it's ignored and nothing is returned
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
Blob,
|
|
ClonedBlob,
|
|
createBlob,
|
|
isBlob,
|
|
kHandle,
|
|
resolveObjectURL,
|
|
};
|