source: node_modules/undici/lib/fileapi/util.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 11.3 KB
Line 
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}
Note: See TracBrowser for help on using the repository browser.