Skip to content

Commit 267a415

Browse files
authored
Merge pull request #27 from golemcloud/formdata-blob
Added support for FormData, Blob and File
2 parents 533ccb3 + ed6c8ca commit 267a415

File tree

9 files changed

+462
-23
lines changed

9 files changed

+462
-23
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ Only if the `http` feature flag is enabled in the generated crate. It depends on
262262
- `fetch`
263263
- `Headers`
264264
- `Response`
265+
- `FormData`
266+
- `Blob`
267+
- `File`
265268

266269
#### Streams
267270

@@ -288,6 +291,14 @@ Implemented by https://github.com/MattiasBuelens/web-streams-polyfill
288291
- `clearInterval`
289292
- `setImmediate`
290293

294+
295+
#### Encoding
296+
297+
- `TextEncoder`
298+
- `TextDecoder`
299+
- `TextDecoderStream`
300+
- `TextEncoderStream`
301+
291302
### Coming from QuickJS
292303

293304
- Global:
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/*! fetch-blob. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
2+
3+
// 64 KiB (same size chrome slice theirs blob into Uint8array's)
4+
const POOL_SIZE = 65536
5+
6+
/**
7+
* @param {(Blob | Uint8Array)[]} parts
8+
* @param {boolean} clone
9+
* @returns {AsyncIterableIterator<Uint8Array>}
10+
*/
11+
async function * toIterator (parts, clone) {
12+
for (const part of parts) {
13+
if (ArrayBuffer.isView(part)) {
14+
if (clone) {
15+
let position = part.byteOffset
16+
const end = part.byteOffset + part.byteLength
17+
while (position !== end) {
18+
const size = Math.min(end - position, POOL_SIZE)
19+
const chunk = part.buffer.slice(position, position + size)
20+
position += chunk.byteLength
21+
yield new Uint8Array(chunk)
22+
}
23+
} else {
24+
yield part
25+
}
26+
} else {
27+
// @ts-ignore TS Think blob.stream() returns a node:stream
28+
yield * part.stream()
29+
}
30+
}
31+
}
32+
33+
const _Blob = class Blob {
34+
/** @type {Array.<(Blob|Uint8Array)>} */
35+
#parts = []
36+
#type = ''
37+
#size = 0
38+
#endings = 'transparent'
39+
40+
/**
41+
* The Blob() constructor returns a new Blob object. The content
42+
* of the blob consists of the concatenation of the values given
43+
* in the parameter array.
44+
*
45+
* @param {*} blobParts
46+
* @param {{ type?: string, endings?: string }} [options]
47+
*/
48+
constructor (blobParts = [], options = {}) {
49+
if (typeof blobParts !== 'object' || blobParts === null) {
50+
throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.')
51+
}
52+
53+
if (typeof blobParts[Symbol.iterator] !== 'function') {
54+
throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.')
55+
}
56+
57+
if (typeof options !== 'object' && typeof options !== 'function') {
58+
throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.')
59+
}
60+
61+
if (options === null) options = {}
62+
63+
const encoder = new TextEncoder()
64+
for (const element of blobParts) {
65+
let part
66+
if (ArrayBuffer.isView(element)) {
67+
part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength))
68+
} else if (element instanceof ArrayBuffer) {
69+
part = new Uint8Array(element.slice(0))
70+
} else if (element instanceof Blob) {
71+
part = element
72+
} else {
73+
part = encoder.encode(`${element}`)
74+
}
75+
76+
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size
77+
// Avoid pushing empty parts into the array to better GC them
78+
if (size) {
79+
this.#size += size
80+
this.#parts.push(part)
81+
}
82+
}
83+
84+
this.#endings = `${options.endings === undefined ? 'transparent' : options.endings}`
85+
const type = options.type === undefined ? '' : String(options.type)
86+
this.#type = /^[\x20-\x7E]*$/.test(type) ? type : ''
87+
}
88+
89+
/**
90+
* The Blob interface's size property returns the
91+
* size of the Blob in bytes.
92+
*/
93+
get size () {
94+
return this.#size
95+
}
96+
97+
/**
98+
* The type property of a Blob object returns the MIME type of the file.
99+
*/
100+
get type () {
101+
return this.#type
102+
}
103+
104+
/**
105+
* The text() method in the Blob interface returns a Promise
106+
* that resolves with a string containing the contents of
107+
* the blob, interpreted as UTF-8.
108+
*
109+
* @return {Promise<string>}
110+
*/
111+
async text () {
112+
// More optimized than using this.arrayBuffer()
113+
// that requires twice as much ram
114+
const decoder = new TextDecoder()
115+
let str = ''
116+
for await (const part of toIterator(this.#parts, false)) {
117+
str += decoder.decode(part, { stream: true })
118+
}
119+
// Remaining
120+
str += decoder.decode()
121+
return str
122+
}
123+
124+
/**
125+
* The arrayBuffer() method in the Blob interface returns a
126+
* Promise that resolves with the contents of the blob as
127+
* binary data contained in an ArrayBuffer.
128+
*
129+
* @return {Promise<ArrayBuffer>}
130+
*/
131+
async arrayBuffer () {
132+
const data = new Uint8Array(this.size)
133+
let offset = 0
134+
for await (const chunk of toIterator(this.#parts, false)) {
135+
data.set(chunk, offset)
136+
offset += chunk.length
137+
}
138+
139+
return data.buffer
140+
}
141+
142+
stream () {
143+
const it = toIterator(this.#parts, true)
144+
145+
return new globalThis.ReadableStream({
146+
// @ts-ignore
147+
type: 'bytes',
148+
async pull (ctrl) {
149+
const chunk = await it.next()
150+
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value)
151+
},
152+
153+
async cancel () {
154+
await it.return()
155+
}
156+
})
157+
}
158+
159+
/**
160+
* The Blob interface's slice() method creates and returns a
161+
* new Blob object which contains data from a subset of the
162+
* blob on which it's called.
163+
*
164+
* @param {number} [start]
165+
* @param {number} [end]
166+
* @param {string} [type]
167+
*/
168+
slice (start = 0, end = this.size, type = '') {
169+
const { size } = this
170+
171+
let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size)
172+
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size)
173+
174+
const span = Math.max(relativeEnd - relativeStart, 0)
175+
const parts = this.#parts
176+
const blobParts = []
177+
let added = 0
178+
179+
for (const part of parts) {
180+
// don't add the overflow to new blobParts
181+
if (added >= span) {
182+
break
183+
}
184+
185+
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size
186+
if (relativeStart && size <= relativeStart) {
187+
// Skip the beginning and change the relative
188+
// start & end position as we skip the unwanted parts
189+
relativeStart -= size
190+
relativeEnd -= size
191+
} else {
192+
let chunk
193+
if (ArrayBuffer.isView(part)) {
194+
chunk = part.subarray(relativeStart, Math.min(size, relativeEnd))
195+
added += chunk.byteLength
196+
} else {
197+
chunk = part.slice(relativeStart, Math.min(size, relativeEnd))
198+
added += chunk.size
199+
}
200+
relativeEnd -= size
201+
blobParts.push(chunk)
202+
relativeStart = 0 // All next sequential parts should start at 0
203+
}
204+
}
205+
206+
const blob = new Blob([], { type: `${type}` })
207+
blob.#size = span
208+
blob.#parts = blobParts
209+
210+
return blob
211+
}
212+
213+
get [Symbol.toStringTag] () {
214+
return 'Blob'
215+
}
216+
217+
static [Symbol.hasInstance] (object) {
218+
return (
219+
object &&
220+
typeof object === 'object' &&
221+
typeof object.constructor === 'function' &&
222+
(
223+
typeof object.stream === 'function' ||
224+
typeof object.arrayBuffer === 'function'
225+
) &&
226+
/^(Blob|File)$/.test(object[Symbol.toStringTag])
227+
)
228+
}
229+
}
230+
231+
Object.defineProperties(_Blob.prototype, {
232+
size: { enumerable: true },
233+
type: { enumerable: true },
234+
slice: { enumerable: true }
235+
})
236+
237+
/** @type {typeof globalThis.Blob} */
238+
export const Blob = _Blob
239+
240+
const _File = class File extends Blob {
241+
#lastModified = 0
242+
#name = ''
243+
244+
/**
245+
* @param {*[]} fileBits
246+
* @param {string} fileName
247+
* @param {{lastModified?: number, type?: string}} options
248+
*/// @ts-ignore
249+
constructor (fileBits, fileName, options = {}) {
250+
if (arguments.length < 2) {
251+
throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`)
252+
}
253+
super(fileBits, options)
254+
255+
if (options === null) options = {}
256+
257+
// Simulate WebIDL type casting for NaN value in lastModified option.
258+
const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified)
259+
if (!Number.isNaN(lastModified)) {
260+
this.#lastModified = lastModified
261+
}
262+
263+
this.#name = String(fileName)
264+
}
265+
266+
get name () {
267+
return this.#name
268+
}
269+
270+
get lastModified () {
271+
return this.#lastModified
272+
}
273+
274+
get [Symbol.toStringTag] () {
275+
return 'File'
276+
}
277+
278+
static [Symbol.hasInstance] (object) {
279+
return !!object && object instanceof Blob &&
280+
/^(File)$/.test(object[Symbol.toStringTag])
281+
}
282+
}
283+
284+
/** @type {typeof globalThis.File} */// @ts-ignore
285+
export const File = _File
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
2+
3+
import { Blob as C, File as F} from '__wasm_rquickjs_builtin/http_blob'
4+
5+
var {toStringTag:t,iterator:i,hasInstance:h}=Symbol,
6+
r=Math.random,
7+
m='append,set,get,getAll,delete,keys,values,entries,forEach,constructor'.split(','),
8+
f=(a,b,c)=>(a+='',/^(Blob|File)$/.test(b && b[t])?[(c=c!==void 0?c+'':b[t]=='File'?b.name:'blob',a),b.name!==c||b[t]=='blob'?new F([b],c,b):b]:[a,b+'']),
9+
e=(c,f)=>(f?c:c.replace(/\r?\n|\r/g,'\r\n')).replace(/\n/g,'%0A').replace(/\r/g,'%0D').replace(/"/g,'%22'),
10+
x=(n, a, e)=>{if(a.length<e){throw new TypeError(`Failed to execute '${n}' on 'FormData': ${e} arguments required, but only ${a.length} present.`)}}
11+
12+
export const File = F
13+
14+
/** @type {typeof globalThis.FormData} */
15+
export const FormData = class FormData {
16+
#d=[];
17+
constructor(...a){if(a.length)throw new TypeError(`Failed to construct 'FormData': parameter 1 is not of type 'HTMLFormElement'.`)}
18+
get [t]() {return 'FormData'}
19+
[i](){return this.entries()}
20+
static [h](o) {return o&&typeof o==='object'&&o[t]==='FormData'&&!m.some(m=>typeof o[m]!='function')}
21+
append(...a){x('append',arguments,2);this.#d.push(f(...a))}
22+
delete(a){x('delete',arguments,1);a+='';this.#d=this.#d.filter(([b])=>b!==a)}
23+
get(a){x('get',arguments,1);a+='';for(var b=this.#d,l=b.length,c=0;c<l;c++)if(b[c][0]===a)return b[c][1];return null}
24+
getAll(a,b){x('getAll',arguments,1);b=[];a+='';this.#d.forEach(c=>c[0]===a&&b.push(c[1]));return b}
25+
has(a){x('has',arguments,1);a+='';return this.#d.some(b=>b[0]===a)}
26+
forEach(a,b){x('forEach',arguments,1);for(var [c,d]of this)a.call(b,d,c,this)}
27+
set(...a){x('set',arguments,2);var b=[],c=!0;a=f(...a);this.#d.forEach(d=>{d[0]===a[0]?c&&(c=!b.push(a)):b.push(d)});c&&b.push(a);this.#d=b}
28+
*entries(){yield*this.#d}
29+
*keys(){for(var[a]of this)yield a}
30+
*values(){for(var[,a]of this)yield a}}
31+
32+
/** @param {FormData} F */
33+
export function formDataToBlob (F,B=C){
34+
var b=`${r()}${r()}`.replace(/\./g, '').slice(-28).padStart(32, '-'),c=[],p=`--${b}\r\nContent-Disposition: form-data; name="`
35+
F.forEach((v,n)=>typeof v=='string'
36+
?c.push(p+e(n)+`"\r\n\r\n${v.replace(/\r(?!\n)|(?<!\r)\n/g, '\r\n')}\r\n`)
37+
:c.push(p+e(n)+`"; filename="${e(v.name, 1)}"\r\nContent-Type: ${v.type||"application/octet-stream"}\r\n\r\n`, v, '\r\n'))
38+
c.push(`--${b}--`)
39+
return new B(c,{type:"multipart/form-data; boundary="+b})}

0 commit comments

Comments
 (0)