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