1'use strict' 2 3const { 4 kState, 5 kError, 6 kResult, 7 kAborted, 8 kLastProgressEventFired 9} = require('./symbols') 10const { ProgressEvent } = require('./progressevent') 11const { getEncoding } = require('./encoding') 12const { DOMException } = require('../fetch/constants') 13const { serializeAMimeType, parseMIMEType } = require('../fetch/dataURL') 14const { types } = require('util') 15const { StringDecoder } = require('string_decoder') 16const { btoa } = require('buffer') 17 18/** @type {PropertyDescriptor} */ 19const staticPropertyDescriptors = { 20 enumerable: true, 21 writable: false, 22 configurable: false 23} 24 25/** 26 * @see https://w3c.github.io/FileAPI/#readOperation 27 * @param {import('./filereader').FileReader} fr 28 * @param {import('buffer').Blob} blob 29 * @param {string} type 30 * @param {string?} encodingName 31 */ 32function readOperation (fr, blob, type, encodingName) { 33 // 1. If fr’s state is "loading", throw an InvalidStateError 34 // DOMException. 35 if (fr[kState] === 'loading') { 36 throw new DOMException('Invalid state', 'InvalidStateError') 37 } 38 39 // 2. Set fr’s state to "loading". 40 fr[kState] = 'loading' 41 42 // 3. Set fr’s result to null. 43 fr[kResult] = null 44 45 // 4. Set fr’s error to null. 46 fr[kError] = null 47 48 // 5. Let stream be the result of calling get stream on blob. 49 /** @type {import('stream/web').ReadableStream} */ 50 const stream = blob.stream() 51 52 // 6. Let reader be the result of getting a reader from stream. 53 const reader = stream.getReader() 54 55 // 7. Let bytes be an empty byte sequence. 56 /** @type {Uint8Array[]} */ 57 const bytes = [] 58 59 // 8. Let chunkPromise be the result of reading a chunk from 60 // stream with reader. 61 let chunkPromise = reader.read() 62 63 // 9. Let isFirstChunk be true. 64 let isFirstChunk = true 65 66 // 10. In parallel, while true: 67 // Note: "In parallel" just means non-blocking 68 // Note 2: readOperation itself cannot be async as double 69 // reading the body would then reject the promise, instead 70 // of throwing an error. 71 ;(async () => { 72 while (!fr[kAborted]) { 73 // 1. Wait for chunkPromise to be fulfilled or rejected. 74 try { 75 const { done, value } = await chunkPromise 76 77 // 2. If chunkPromise is fulfilled, and isFirstChunk is 78 // true, queue a task to fire a progress event called 79 // loadstart at fr. 80 if (isFirstChunk && !fr[kAborted]) { 81 queueMicrotask(() => { 82 fireAProgressEvent('loadstart', fr) 83 }) 84 } 85 86 // 3. Set isFirstChunk to false. 87 isFirstChunk = false 88 89 // 4. If chunkPromise is fulfilled with an object whose 90 // done property is false and whose value property is 91 // a Uint8Array object, run these steps: 92 if (!done && types.isUint8Array(value)) { 93 // 1. Let bs be the byte sequence represented by the 94 // Uint8Array object. 95 96 // 2. Append bs to bytes. 97 bytes.push(value) 98 99 // 3. If roughly 50ms have passed since these steps 100 // were last invoked, queue a task to fire a 101 // progress event called progress at fr. 102 if ( 103 ( 104 fr[kLastProgressEventFired] === undefined || 105 Date.now() - fr[kLastProgressEventFired] >= 50 106 ) && 107 !fr[kAborted] 108 ) { 109 fr[kLastProgressEventFired] = Date.now() 110 queueMicrotask(() => { 111 fireAProgressEvent('progress', fr) 112 }) 113 } 114 115 // 4. Set chunkPromise to the result of reading a 116 // chunk from stream with reader. 117 chunkPromise = reader.read() 118 } else if (done) { 119 // 5. Otherwise, if chunkPromise is fulfilled with an 120 // object whose done property is true, queue a task 121 // to run the following steps and abort this algorithm: 122 queueMicrotask(() => { 123 // 1. Set fr’s state to "done". 124 fr[kState] = 'done' 125 126 // 2. Let result be the result of package data given 127 // bytes, type, blob’s type, and encodingName. 128 try { 129 const result = packageData(bytes, type, blob.type, encodingName) 130 131 // 4. Else: 132 133 if (fr[kAborted]) { 134 return 135 } 136 137 // 1. Set fr’s result to result. 138 fr[kResult] = result 139 140 // 2. Fire a progress event called load at the fr. 141 fireAProgressEvent('load', fr) 142 } catch (error) { 143 // 3. If package data threw an exception error: 144 145 // 1. Set fr’s error to error. 146 fr[kError] = error 147 148 // 2. Fire a progress event called error at fr. 149 fireAProgressEvent('error', fr) 150 } 151 152 // 5. If fr’s state is not "loading", fire a progress 153 // event called loadend at the fr. 154 if (fr[kState] !== 'loading') { 155 fireAProgressEvent('loadend', fr) 156 } 157 }) 158 159 break 160 } 161 } catch (error) { 162 if (fr[kAborted]) { 163 return 164 } 165 166 // 6. Otherwise, if chunkPromise is rejected with an 167 // error error, queue a task to run the following 168 // steps and abort this algorithm: 169 queueMicrotask(() => { 170 // 1. Set fr’s state to "done". 171 fr[kState] = 'done' 172 173 // 2. Set fr’s error to error. 174 fr[kError] = error 175 176 // 3. Fire a progress event called error at fr. 177 fireAProgressEvent('error', fr) 178 179 // 4. If fr’s state is not "loading", fire a progress 180 // event called loadend at fr. 181 if (fr[kState] !== 'loading') { 182 fireAProgressEvent('loadend', fr) 183 } 184 }) 185 186 break 187 } 188 } 189 })() 190} 191 192/** 193 * @see https://w3c.github.io/FileAPI/#fire-a-progress-event 194 * @see https://dom.spec.whatwg.org/#concept-event-fire 195 * @param {string} e The name of the event 196 * @param {import('./filereader').FileReader} reader 197 */ 198function fireAProgressEvent (e, reader) { 199 // The progress event e does not bubble. e.bubbles must be false 200 // The progress event e is NOT cancelable. e.cancelable must be false 201 const event = new ProgressEvent(e, { 202 bubbles: false, 203 cancelable: false 204 }) 205 206 reader.dispatchEvent(event) 207} 208 209/** 210 * @see https://w3c.github.io/FileAPI/#blob-package-data 211 * @param {Uint8Array[]} bytes 212 * @param {string} type 213 * @param {string?} mimeType 214 * @param {string?} encodingName 215 */ 216function packageData (bytes, type, mimeType, encodingName) { 217 // 1. A Blob has an associated package data algorithm, given 218 // bytes, a type, a optional mimeType, and a optional 219 // encodingName, which switches on type and runs the 220 // associated steps: 221 222 switch (type) { 223 case 'DataURL': { 224 // 1. Return bytes as a DataURL [RFC2397] subject to 225 // the considerations below: 226 // * Use mimeType as part of the Data URL if it is 227 // available in keeping with the Data URL 228 // specification [RFC2397]. 229 // * If mimeType is not available return a Data URL 230 // without a media-type. [RFC2397]. 231 232 // https://datatracker.ietf.org/doc/html/rfc2397#section-3 233 // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data 234 // mediatype := [ type "/" subtype ] *( ";" parameter ) 235 // data := *urlchar 236 // parameter := attribute "=" value 237 let dataURL = 'data:' 238 239 const parsed = parseMIMEType(mimeType || 'application/octet-stream') 240 241 if (parsed !== 'failure') { 242 dataURL += serializeAMimeType(parsed) 243 } 244 245 dataURL += ';base64,' 246 247 const decoder = new StringDecoder('latin1') 248 249 for (const chunk of bytes) { 250 dataURL += btoa(decoder.write(chunk)) 251 } 252 253 dataURL += btoa(decoder.end()) 254 255 return dataURL 256 } 257 case 'Text': { 258 // 1. Let encoding be failure 259 let encoding = 'failure' 260 261 // 2. If the encodingName is present, set encoding to the 262 // result of getting an encoding from encodingName. 263 if (encodingName) { 264 encoding = getEncoding(encodingName) 265 } 266 267 // 3. If encoding is failure, and mimeType is present: 268 if (encoding === 'failure' && mimeType) { 269 // 1. Let type be the result of parse a MIME type 270 // given mimeType. 271 const type = parseMIMEType(mimeType) 272 273 // 2. If type is not failure, set encoding to the result 274 // of getting an encoding from type’s parameters["charset"]. 275 if (type !== 'failure') { 276 encoding = getEncoding(type.parameters.get('charset')) 277 } 278 } 279 280 // 4. If encoding is failure, then set encoding to UTF-8. 281 if (encoding === 'failure') { 282 encoding = 'UTF-8' 283 } 284 285 // 5. Decode bytes using fallback encoding encoding, and 286 // return the result. 287 return decode(bytes, encoding) 288 } 289 case 'ArrayBuffer': { 290 // Return a new ArrayBuffer whose contents are bytes. 291 const sequence = combineByteSequences(bytes) 292 293 return sequence.buffer 294 } 295 case 'BinaryString': { 296 // Return bytes as a binary string, in which every byte 297 // is represented by a code unit of equal value [0..255]. 298 let binaryString = '' 299 300 const decoder = new StringDecoder('latin1') 301 302 for (const chunk of bytes) { 303 binaryString += decoder.write(chunk) 304 } 305 306 binaryString += decoder.end() 307 308 return binaryString 309 } 310 } 311} 312 313/** 314 * @see https://encoding.spec.whatwg.org/#decode 315 * @param {Uint8Array[]} ioQueue 316 * @param {string} encoding 317 */ 318function decode (ioQueue, encoding) { 319 const bytes = combineByteSequences(ioQueue) 320 321 // 1. Let BOMEncoding be the result of BOM sniffing ioQueue. 322 const BOMEncoding = BOMSniffing(bytes) 323 324 let slice = 0 325 326 // 2. If BOMEncoding is non-null: 327 if (BOMEncoding !== null) { 328 // 1. Set encoding to BOMEncoding. 329 encoding = BOMEncoding 330 331 // 2. Read three bytes from ioQueue, if BOMEncoding is 332 // UTF-8; otherwise read two bytes. 333 // (Do nothing with those bytes.) 334 slice = BOMEncoding === 'UTF-8' ? 3 : 2 335 } 336 337 // 3. Process a queue with an instance of encoding’s 338 // decoder, ioQueue, output, and "replacement". 339 340 // 4. Return output. 341 342 const sliced = bytes.slice(slice) 343 return new TextDecoder(encoding).decode(sliced) 344} 345 346/** 347 * @see https://encoding.spec.whatwg.org/#bom-sniff 348 * @param {Uint8Array} ioQueue 349 */ 350function BOMSniffing (ioQueue) { 351 // 1. Let BOM be the result of peeking 3 bytes from ioQueue, 352 // converted to a byte sequence. 353 const [a, b, c] = ioQueue 354 355 // 2. For each of the rows in the table below, starting with 356 // the first one and going down, if BOM starts with the 357 // bytes given in the first column, then return the 358 // encoding given in the cell in the second column of that 359 // row. Otherwise, return null. 360 if (a === 0xEF && b === 0xBB && c === 0xBF) { 361 return 'UTF-8' 362 } else if (a === 0xFE && b === 0xFF) { 363 return 'UTF-16BE' 364 } else if (a === 0xFF && b === 0xFE) { 365 return 'UTF-16LE' 366 } 367 368 return null 369} 370 371/** 372 * @param {Uint8Array[]} sequences 373 */ 374function combineByteSequences (sequences) { 375 const size = sequences.reduce((a, b) => { 376 return a + b.byteLength 377 }, 0) 378 379 let offset = 0 380 381 return sequences.reduce((a, b) => { 382 a.set(b, offset) 383 offset += b.byteLength 384 return a 385 }, new Uint8Array(size)) 386} 387 388module.exports = { 389 staticPropertyDescriptors, 390 readOperation, 391 fireAProgressEvent 392} 393