|
1 | | -// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js |
2 | | -// (MIT licensed) |
3 | | - |
4 | | -const {Readable: ReadableStream} = require('stream'); |
| 1 | +const {Readable} = require('stream'); |
5 | 2 |
|
| 3 | +/** |
| 4 | + * @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>} |
| 5 | + */ |
6 | 6 | const wm = new WeakMap(); |
7 | 7 |
|
| 8 | +async function * read(parts) { |
| 9 | + for (const part of parts) { |
| 10 | + if ('stream' in part) { |
| 11 | + yield * part.stream(); |
| 12 | + } else { |
| 13 | + yield part; |
| 14 | + } |
| 15 | + } |
| 16 | +} |
| 17 | + |
| 18 | +/** |
| 19 | + * @template T |
| 20 | + * @param {T} object |
| 21 | + * @returns {T is Blob} |
| 22 | + */ |
| 23 | +const isBlob = object => { |
| 24 | + return ( |
| 25 | + typeof object === 'object' && |
| 26 | + typeof object.stream === 'function' && |
| 27 | + typeof object.constructor === 'function' && |
| 28 | + /^(Blob|File)$/.test(object[Symbol.toStringTag]) |
| 29 | + ); |
| 30 | +}; |
| 31 | + |
8 | 32 | class Blob { |
| 33 | + /** |
| 34 | + * The Blob() constructor returns a new Blob object. The content |
| 35 | + * of the blob consists of the concatenation of the values given |
| 36 | + * in the parameter array. |
| 37 | + * |
| 38 | + * @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts |
| 39 | + * @param {{ type?: string }} [options] |
| 40 | + */ |
9 | 41 | constructor(blobParts = [], options = {type: ''}) { |
10 | | - const buffers = []; |
11 | 42 | let size = 0; |
12 | 43 |
|
13 | | - blobParts.forEach(element => { |
| 44 | + const parts = blobParts.map(element => { |
14 | 45 | let buffer; |
15 | | - if (element instanceof Buffer) { |
| 46 | + if (Buffer.isBuffer(element)) { |
16 | 47 | buffer = element; |
17 | 48 | } else if (ArrayBuffer.isView(element)) { |
18 | 49 | buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); |
19 | 50 | } else if (element instanceof ArrayBuffer) { |
20 | 51 | buffer = Buffer.from(element); |
21 | | - } else if (element instanceof Blob) { |
22 | | - buffer = wm.get(element).buffer; |
| 52 | + } else if (isBlob(element)) { |
| 53 | + buffer = element; |
23 | 54 | } else { |
24 | 55 | buffer = Buffer.from(typeof element === 'string' ? element : String(element)); |
25 | 56 | } |
26 | 57 |
|
27 | | - size += buffer.length; |
28 | | - buffers.push(buffer); |
| 58 | + size += buffer.length || buffer.size || 0; |
| 59 | + return buffer; |
29 | 60 | }); |
30 | 61 |
|
31 | | - const buffer = Buffer.concat(buffers, size); |
32 | | - |
33 | 62 | const type = options.type === undefined ? '' : String(options.type).toLowerCase(); |
34 | 63 |
|
35 | 64 | wm.set(this, { |
36 | 65 | type: /[^\u0020-\u007E]/.test(type) ? '' : type, |
37 | 66 | size, |
38 | | - buffer |
| 67 | + parts |
39 | 68 | }); |
40 | 69 | } |
41 | 70 |
|
| 71 | + /** |
| 72 | + * The Blob interface's size property returns the |
| 73 | + * size of the Blob in bytes. |
| 74 | + */ |
42 | 75 | get size() { |
43 | 76 | return wm.get(this).size; |
44 | 77 | } |
45 | 78 |
|
| 79 | + /** |
| 80 | + * The type property of a Blob object returns the MIME type of the file. |
| 81 | + */ |
46 | 82 | get type() { |
47 | 83 | return wm.get(this).type; |
48 | 84 | } |
49 | 85 |
|
50 | | - text() { |
51 | | - return Promise.resolve(wm.get(this).buffer.toString()); |
| 86 | + /** |
| 87 | + * The text() method in the Blob interface returns a Promise |
| 88 | + * that resolves with a string containing the contents of |
| 89 | + * the blob, interpreted as UTF-8. |
| 90 | + * |
| 91 | + * @return {Promise<string>} |
| 92 | + */ |
| 93 | + async text() { |
| 94 | + return Buffer.from(await this.arrayBuffer()).toString(); |
52 | 95 | } |
53 | 96 |
|
54 | | - arrayBuffer() { |
55 | | - const buf = wm.get(this).buffer; |
56 | | - const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); |
57 | | - return Promise.resolve(ab); |
58 | | - } |
| 97 | + /** |
| 98 | + * The arrayBuffer() method in the Blob interface returns a |
| 99 | + * Promise that resolves with the contents of the blob as |
| 100 | + * binary data contained in an ArrayBuffer. |
| 101 | + * |
| 102 | + * @return {Promise<ArrayBuffer>} |
| 103 | + */ |
| 104 | + async arrayBuffer() { |
| 105 | + const data = new Uint8Array(this.size); |
| 106 | + let offset = 0; |
| 107 | + for await (const chunk of this.stream()) { |
| 108 | + data.set(chunk, offset); |
| 109 | + offset += chunk.length; |
| 110 | + } |
59 | 111 |
|
60 | | - stream() { |
61 | | - const readable = new ReadableStream(); |
62 | | - readable._read = () => { }; |
63 | | - readable.push(wm.get(this).buffer); |
64 | | - readable.push(null); |
65 | | - return readable; |
| 112 | + return data.buffer; |
66 | 113 | } |
67 | 114 |
|
68 | | - toString() { |
69 | | - return '[object Blob]'; |
| 115 | + /** |
| 116 | + * The Blob interface's stream() method is difference from native |
| 117 | + * and uses node streams instead of whatwg streams. |
| 118 | + * |
| 119 | + * @returns {Readable} Node readable stream |
| 120 | + */ |
| 121 | + stream() { |
| 122 | + return Readable.from(read(wm.get(this).parts)); |
70 | 123 | } |
71 | 124 |
|
72 | | - slice(...args) { |
| 125 | + /** |
| 126 | + * The Blob interface's slice() method creates and returns a |
| 127 | + * new Blob object which contains data from a subset of the |
| 128 | + * blob on which it's called. |
| 129 | + * |
| 130 | + * @param {number} [start] |
| 131 | + * @param {number} [end] |
| 132 | + * @param {string} [contentType] |
| 133 | + */ |
| 134 | + slice(start = 0, end = this.size, type = '') { |
73 | 135 | const {size} = this; |
74 | 136 |
|
75 | | - const start = args[0]; |
76 | | - const end = args[1]; |
77 | | - let relativeStart; |
78 | | - let relativeEnd; |
| 137 | + let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); |
| 138 | + let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); |
79 | 139 |
|
80 | | - if (start === undefined) { |
81 | | - relativeStart = 0; // |
82 | | - } else if (start < 0) { |
83 | | - relativeStart = Math.max(size + start, 0); // |
84 | | - } else { |
85 | | - relativeStart = Math.min(start, size); |
| 140 | + const span = Math.max(relativeEnd - relativeStart, 0); |
| 141 | + const parts = wm.get(this).parts.values(); |
| 142 | + const blobParts = []; |
| 143 | + let added = 0; |
| 144 | + |
| 145 | + for (const part of parts) { |
| 146 | + const size = ArrayBuffer.isView(part) ? part.byteLength : part.size; |
| 147 | + if (relativeStart && size <= relativeStart) { |
| 148 | + // Skip the beginning and change the relative |
| 149 | + // start & end position as we skip the unwanted parts |
| 150 | + relativeStart -= size; |
| 151 | + relativeEnd -= size; |
| 152 | + } else { |
| 153 | + const chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); |
| 154 | + blobParts.push(chunk); |
| 155 | + added += size; |
| 156 | + relativeStart = 0; // All next sequental parts should start at 0 |
| 157 | + |
| 158 | + // don't add the overflow to new blobParts |
| 159 | + if (added >= span) { |
| 160 | + break; |
| 161 | + } |
| 162 | + } |
86 | 163 | } |
87 | 164 |
|
88 | | - if (end === undefined) { |
89 | | - relativeEnd = size; // |
90 | | - } else if (end < 0) { |
91 | | - relativeEnd = Math.max(size + end, 0); // |
92 | | - } else { |
93 | | - relativeEnd = Math.min(end, size); |
94 | | - } |
| 165 | + const blob = new Blob([], {type}); |
| 166 | + Object.assign(wm.get(blob), {size: span, parts: blobParts}); |
95 | 167 |
|
96 | | - const span = Math.max(relativeEnd - relativeStart, 0); |
97 | | - const slicedBuffer = wm.get(this).buffer.slice( |
98 | | - relativeStart, |
99 | | - relativeStart + span |
100 | | - ); |
101 | | - const blob = new Blob([], {type: args[2]}); |
102 | | - const _ = wm.get(blob); |
103 | | - _.buffer = slicedBuffer; |
104 | 168 | return blob; |
105 | 169 | } |
106 | 170 | } |
|
0 commit comments