• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayFrom,
5  MathMax,
6  MathMin,
7  ObjectDefineProperties,
8  ObjectDefineProperty,
9  PromiseResolve,
10  PromiseReject,
11  SafePromisePrototypeFinally,
12  ReflectConstruct,
13  RegExpPrototypeExec,
14  RegExpPrototypeSymbolReplace,
15  StringPrototypeToLowerCase,
16  StringPrototypeSplit,
17  Symbol,
18  SymbolIterator,
19  SymbolToStringTag,
20  Uint8Array,
21} = primordials;
22
23const {
24  createBlob: _createBlob,
25  FixedSizeBlobCopyJob,
26  getDataObject,
27} = internalBinding('blob');
28
29const {
30  TextDecoder,
31  TextEncoder,
32} = require('internal/encoding');
33
34const {
35  makeTransferable,
36  kClone,
37  kDeserialize,
38} = require('internal/worker/js_transferable');
39
40const {
41  isAnyArrayBuffer,
42  isArrayBufferView,
43} = require('internal/util/types');
44
45const {
46  createDeferredPromise,
47  customInspectSymbol: kInspect,
48  kEmptyObject,
49  kEnumerableProperty,
50} = require('internal/util');
51const { inspect } = require('internal/util/inspect');
52
53const {
54  AbortError,
55  codes: {
56    ERR_INVALID_ARG_TYPE,
57    ERR_INVALID_ARG_VALUE,
58    ERR_INVALID_THIS,
59    ERR_BUFFER_TOO_LARGE,
60  },
61} = require('internal/errors');
62
63const {
64  isUint32,
65  validateDictionary,
66} = require('internal/validators');
67
68const kHandle = Symbol('kHandle');
69const kState = Symbol('kState');
70const kIndex = Symbol('kIndex');
71const kType = Symbol('kType');
72const kLength = Symbol('kLength');
73const kArrayBufferPromise = Symbol('kArrayBufferPromise');
74
75const kMaxChunkSize = 65536;
76
77const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
78
79let ReadableStream;
80let URL;
81
82const enc = new TextEncoder();
83let dec;
84
85// Yes, lazy loading is annoying but because of circular
86// references between the url, internal/blob, and buffer
87// modules, lazy loading here makes sure that things work.
88
89function lazyURL(id) {
90  URL ??= require('internal/url').URL;
91  return new URL(id);
92}
93
94function lazyReadableStream(options) {
95  // eslint-disable-next-line no-global-assign
96  ReadableStream ??=
97    require('internal/webstreams/readablestream').ReadableStream;
98  return new ReadableStream(options);
99}
100
101const { EOL } = require('internal/constants');
102
103function isBlob(object) {
104  return object?.[kHandle] !== undefined;
105}
106
107function getSource(source, endings) {
108  if (isBlob(source))
109    return [source.size, source[kHandle]];
110
111  if (isAnyArrayBuffer(source)) {
112    source = new Uint8Array(source);
113  } else if (!isArrayBufferView(source)) {
114    source = `${source}`;
115    if (endings === 'native')
116      source = RegExpPrototypeSymbolReplace(/\n|\r\n/g, source, EOL);
117    source = enc.encode(source);
118  }
119
120  // We copy into a new Uint8Array because the underlying
121  // BackingStores are going to be detached and owned by
122  // the Blob.
123  const { buffer, byteOffset, byteLength } = source;
124  const slice = buffer.slice(byteOffset, byteOffset + byteLength);
125  return [byteLength, new Uint8Array(slice)];
126}
127
128class Blob {
129  /**
130   * @typedef {string|ArrayBuffer|ArrayBufferView|Blob} SourcePart
131   */
132
133  /**
134   * @param {SourcePart[]} [sources]
135   * @param {{
136   *   endings? : string,
137   *   type? : string,
138   * }} [options]
139   * @constructs {Blob}
140   */
141  constructor(sources = [], options) {
142    if (sources === null ||
143        typeof sources[SymbolIterator] !== 'function' ||
144        typeof sources === 'string') {
145      throw new ERR_INVALID_ARG_TYPE('sources', 'a sequence', sources);
146    }
147    validateDictionary(options, 'options');
148    let {
149      type = '',
150      endings = 'transparent',
151    } = options ?? kEmptyObject;
152
153    endings = `${endings}`;
154    if (endings !== 'transparent' && endings !== 'native')
155      throw new ERR_INVALID_ARG_VALUE('options.endings', endings);
156
157    let length = 0;
158    const sources_ = ArrayFrom(sources, (source) => {
159      const { 0: len, 1: src } = getSource(source, endings);
160      length += len;
161      return src;
162    });
163
164    if (!isUint32(length))
165      throw new ERR_BUFFER_TOO_LARGE(0xFFFFFFFF);
166
167    this[kHandle] = _createBlob(sources_, length);
168    this[kLength] = length;
169
170    type = `${type}`;
171    this[kType] = RegExpPrototypeExec(disallowedTypeCharacters, type) !== null ?
172      '' : StringPrototypeToLowerCase(type);
173
174    // eslint-disable-next-line no-constructor-return
175    return makeTransferable(this);
176  }
177
178  [kInspect](depth, options) {
179    if (depth < 0)
180      return this;
181
182    const opts = {
183      ...options,
184      depth: options.depth == null ? null : options.depth - 1,
185    };
186
187    return `Blob ${inspect({
188      size: this.size,
189      type: this.type,
190    }, opts)}`;
191  }
192
193  [kClone]() {
194    const handle = this[kHandle];
195    const type = this[kType];
196    const length = this[kLength];
197    return {
198      data: { handle, type, length },
199      deserializeInfo: 'internal/blob:ClonedBlob',
200    };
201  }
202
203  [kDeserialize]({ handle, type, length }) {
204    this[kHandle] = handle;
205    this[kType] = type;
206    this[kLength] = length;
207  }
208
209  /**
210   * @readonly
211   * @type {string}
212   */
213  get type() {
214    if (!isBlob(this))
215      throw new ERR_INVALID_THIS('Blob');
216    return this[kType];
217  }
218
219  /**
220   * @readonly
221   * @type {number}
222   */
223  get size() {
224    if (!isBlob(this))
225      throw new ERR_INVALID_THIS('Blob');
226    return this[kLength];
227  }
228
229  /**
230   * @param {number} [start]
231   * @param {number} [end]
232   * @param {string} [contentType]
233   * @returns {Blob}
234   */
235  slice(start = 0, end = this[kLength], contentType = '') {
236    if (!isBlob(this))
237      throw new ERR_INVALID_THIS('Blob');
238    if (start < 0) {
239      start = MathMax(this[kLength] + start, 0);
240    } else {
241      start = MathMin(start, this[kLength]);
242    }
243    start |= 0;
244
245    if (end < 0) {
246      end = MathMax(this[kLength] + end, 0);
247    } else {
248      end = MathMin(end, this[kLength]);
249    }
250    end |= 0;
251
252    contentType = `${contentType}`;
253    if (RegExpPrototypeExec(disallowedTypeCharacters, contentType) !== null) {
254      contentType = '';
255    } else {
256      contentType = StringPrototypeToLowerCase(contentType);
257    }
258
259    const span = MathMax(end - start, 0);
260
261    return createBlob(
262      this[kHandle].slice(start, start + span),
263      span,
264      contentType);
265  }
266
267  /**
268   * @returns {Promise<ArrayBuffer>}
269   */
270  arrayBuffer() {
271    if (!isBlob(this))
272      return PromiseReject(new ERR_INVALID_THIS('Blob'));
273
274    // If there's already a promise in flight for the content,
275    // reuse it, but only while it's in flight. After the cached
276    // promise resolves it will be cleared, allowing it to be
277    // garbage collected as soon as possible.
278    if (this[kArrayBufferPromise])
279      return this[kArrayBufferPromise];
280
281    const job = new FixedSizeBlobCopyJob(this[kHandle]);
282
283    const ret = job.run();
284
285    // If the job returns a value immediately, the ArrayBuffer
286    // was generated synchronously and should just be returned
287    // directly.
288    if (ret !== undefined)
289      return PromiseResolve(ret);
290
291    const {
292      promise,
293      resolve,
294      reject,
295    } = createDeferredPromise();
296
297    job.ondone = (err, ab) => {
298      if (err !== undefined)
299        return reject(new AbortError(undefined, { cause: err }));
300      resolve(ab);
301    };
302    this[kArrayBufferPromise] =
303    SafePromisePrototypeFinally(
304      promise,
305      () => this[kArrayBufferPromise] = undefined);
306
307    return this[kArrayBufferPromise];
308  }
309
310  /**
311   * @returns {Promise<string>}
312   */
313  async text() {
314    if (!isBlob(this))
315      throw new ERR_INVALID_THIS('Blob');
316
317    dec ??= new TextDecoder();
318
319    return dec.decode(await this.arrayBuffer());
320  }
321
322  /**
323   * @returns {ReadableStream}
324   */
325  stream() {
326    if (!isBlob(this))
327      throw new ERR_INVALID_THIS('Blob');
328
329    const self = this;
330    return new lazyReadableStream({
331      async start() {
332        this[kState] = await self.arrayBuffer();
333        this[kIndex] = 0;
334      },
335
336      pull(controller) {
337        if (this[kState].byteLength - this[kIndex] <= kMaxChunkSize) {
338          controller.enqueue(new Uint8Array(this[kState], this[kIndex]));
339          controller.close();
340          this[kState] = undefined;
341        } else {
342          controller.enqueue(new Uint8Array(this[kState], this[kIndex], kMaxChunkSize));
343          this[kIndex] += kMaxChunkSize;
344        }
345      },
346    });
347  }
348}
349
350function ClonedBlob() {
351  return makeTransferable(ReflectConstruct(function() {}, [], Blob));
352}
353ClonedBlob.prototype[kDeserialize] = () => {};
354
355function createBlob(handle, length, type = '') {
356  return makeTransferable(ReflectConstruct(function() {
357    this[kHandle] = handle;
358    this[kType] = type;
359    this[kLength] = length;
360  }, [], Blob));
361}
362
363ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
364  __proto__: null,
365  configurable: true,
366  value: 'Blob',
367});
368
369ObjectDefineProperties(Blob.prototype, {
370  size: kEnumerableProperty,
371  type: kEnumerableProperty,
372  slice: kEnumerableProperty,
373  stream: kEnumerableProperty,
374  text: kEnumerableProperty,
375  arrayBuffer: kEnumerableProperty,
376});
377
378function resolveObjectURL(url) {
379  url = `${url}`;
380  try {
381    const parsed = new lazyURL(url);
382
383    const split = StringPrototypeSplit(parsed.pathname, ':');
384
385    if (split.length !== 2)
386      return;
387
388    const {
389      0: base,
390      1: id,
391    } = split;
392
393    if (base !== 'nodedata')
394      return;
395
396    const ret = getDataObject(id);
397
398    if (ret === undefined)
399      return;
400
401    const {
402      0: handle,
403      1: length,
404      2: type,
405    } = ret;
406
407    if (handle !== undefined)
408      return createBlob(handle, length, type);
409  } catch {
410    // If there's an error, it's ignored and nothing is returned
411  }
412}
413
414module.exports = {
415  Blob,
416  ClonedBlob,
417  createBlob,
418  isBlob,
419  kHandle,
420  resolveObjectURL,
421};
422