@ccheever/exact-ibex-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- package/src/window/index.ts +872 -0
package/src/blob/Blob.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Blob implementation for Ibex runtime
|
|
4
|
+
*
|
|
5
|
+
* Implements the WHATWG Blob API for handling binary data.
|
|
6
|
+
* https://w3c.github.io/FileAPI/#blob-section
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ReadableStream, type UnderlyingByteSource } from '../streams';
|
|
10
|
+
|
|
11
|
+
export interface BlobPropertyBag {
|
|
12
|
+
type?: string;
|
|
13
|
+
endings?: 'transparent' | 'native';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Blob represents immutable raw binary data.
|
|
20
|
+
*/
|
|
21
|
+
export class Blob {
|
|
22
|
+
readonly #parts: Uint8Array[];
|
|
23
|
+
readonly #type: string;
|
|
24
|
+
readonly #size: number;
|
|
25
|
+
|
|
26
|
+
constructor(blobParts?: BlobPart[], options?: BlobPropertyBag) {
|
|
27
|
+
this.#parts = [];
|
|
28
|
+
this.#type = normalizeType(options?.type ?? '');
|
|
29
|
+
const endings = options?.endings ?? 'transparent';
|
|
30
|
+
|
|
31
|
+
if (blobParts) {
|
|
32
|
+
const parts = Array.from(blobParts);
|
|
33
|
+
for (let i = 0; i < parts.length; i++) {
|
|
34
|
+
this.#parts.push(convertToBytes(parts[i], endings));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.#size = this.#parts.reduce((acc, part) => acc + part.byteLength, 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The size of the Blob in bytes.
|
|
43
|
+
*/
|
|
44
|
+
get size(): number {
|
|
45
|
+
return this.#size;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The MIME type of the Blob.
|
|
50
|
+
*/
|
|
51
|
+
get type(): string {
|
|
52
|
+
return this.#type;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns a new Blob containing a subset of this Blob's data.
|
|
57
|
+
*/
|
|
58
|
+
slice(start?: number, end?: number, contentType?: string): Blob {
|
|
59
|
+
const size = this.#size;
|
|
60
|
+
|
|
61
|
+
// Normalize start
|
|
62
|
+
let relativeStart: number;
|
|
63
|
+
if (start === undefined) {
|
|
64
|
+
relativeStart = 0;
|
|
65
|
+
} else if (start < 0) {
|
|
66
|
+
relativeStart = Math.max(size + start, 0);
|
|
67
|
+
} else {
|
|
68
|
+
relativeStart = Math.min(start, size);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Normalize end
|
|
72
|
+
let relativeEnd: number;
|
|
73
|
+
if (end === undefined) {
|
|
74
|
+
relativeEnd = size;
|
|
75
|
+
} else if (end < 0) {
|
|
76
|
+
relativeEnd = Math.max(size + end, 0);
|
|
77
|
+
} else {
|
|
78
|
+
relativeEnd = Math.min(end, size);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Calculate span
|
|
82
|
+
const span = Math.max(relativeEnd - relativeStart, 0);
|
|
83
|
+
|
|
84
|
+
if (span === 0) {
|
|
85
|
+
return new Blob([], { type: normalizeType(contentType ?? '') });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract the slice
|
|
89
|
+
const allBytes = this.#concatenateBytes();
|
|
90
|
+
const slicedBytes = allBytes.slice(relativeStart, relativeStart + span);
|
|
91
|
+
|
|
92
|
+
return new Blob([slicedBytes], { type: normalizeType(contentType ?? '') });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns a Promise that resolves with the contents as an ArrayBuffer.
|
|
97
|
+
*/
|
|
98
|
+
async arrayBuffer(): Promise<ArrayBuffer> {
|
|
99
|
+
const bytes = this.#concatenateBytes();
|
|
100
|
+
// Return a copy to ensure immutability
|
|
101
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns a Promise that resolves with the contents as a string.
|
|
106
|
+
*/
|
|
107
|
+
async text(): Promise<string> {
|
|
108
|
+
const bytes = this.#concatenateBytes();
|
|
109
|
+
return new TextDecoder().decode(bytes);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns a Promise that resolves with the parsed JSON contents.
|
|
114
|
+
*/
|
|
115
|
+
async json(): Promise<unknown> {
|
|
116
|
+
const text = await this.text();
|
|
117
|
+
const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
118
|
+
return JSON.parse(normalized);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns a Promise that resolves with parsed application/x-www-form-urlencoded data.
|
|
123
|
+
*/
|
|
124
|
+
async formData(): Promise<FormData> {
|
|
125
|
+
const text = await this.text();
|
|
126
|
+
const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
127
|
+
const params = new URLSearchParams(normalized);
|
|
128
|
+
const form = new FormData();
|
|
129
|
+
params.forEach((value, key) => {
|
|
130
|
+
form.append(key, value);
|
|
131
|
+
});
|
|
132
|
+
return form;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Returns a Promise that resolves with the Blob contents as a Uint8Array.
|
|
137
|
+
*/
|
|
138
|
+
async bytes(): Promise<Uint8Array> {
|
|
139
|
+
return this.#concatenateBytes();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Returns a ReadableStream that can be used to read the Blob's contents.
|
|
144
|
+
*
|
|
145
|
+
* Uses byte stream type (type: 'bytes') so that BYOB readers work.
|
|
146
|
+
*/
|
|
147
|
+
stream(): ReadableStream<Uint8Array> {
|
|
148
|
+
const bytes = this.#concatenateBytes();
|
|
149
|
+
let position = 0;
|
|
150
|
+
const chunkSize = 65536; // 64KB chunks
|
|
151
|
+
|
|
152
|
+
const source: UnderlyingByteSource = {
|
|
153
|
+
type: 'bytes',
|
|
154
|
+
pull(controller) {
|
|
155
|
+
if (position >= bytes.length) {
|
|
156
|
+
controller.close();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const chunk = bytes.slice(position, position + chunkSize);
|
|
160
|
+
position += chunkSize;
|
|
161
|
+
controller.enqueue(chunk);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Prefer the global ReadableStream when available so that
|
|
166
|
+
// `instanceof ReadableStream` checks work against the platform class.
|
|
167
|
+
const RS = (globalThis as any).ReadableStream ?? ReadableStream;
|
|
168
|
+
return new RS(source);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Returns a new Blob with the given bytes appended.
|
|
173
|
+
* Non-standard extension for internal use.
|
|
174
|
+
*/
|
|
175
|
+
#concatenateBytes(): Uint8Array {
|
|
176
|
+
if (this.#parts.length === 0) {
|
|
177
|
+
return new Uint8Array(0);
|
|
178
|
+
}
|
|
179
|
+
if (this.#parts.length === 1) {
|
|
180
|
+
return this.#parts[0];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = new Uint8Array(this.#size);
|
|
184
|
+
let offset = 0;
|
|
185
|
+
for (const part of this.#parts) {
|
|
186
|
+
result.set(part, offset);
|
|
187
|
+
offset += part.byteLength;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns the raw bytes for internal use.
|
|
194
|
+
* Non-standard - used by File and other internal APIs.
|
|
195
|
+
*/
|
|
196
|
+
_getBytes(): Uint8Array {
|
|
197
|
+
return this.#concatenateBytes();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get [Symbol.toStringTag](): string {
|
|
201
|
+
return 'Blob';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalize a MIME type string.
|
|
207
|
+
* Per spec: lowercase, only ASCII printable chars (0x20-0x7E) excluding certain chars.
|
|
208
|
+
*/
|
|
209
|
+
function normalizeType(type: string): string {
|
|
210
|
+
if (!type) return '';
|
|
211
|
+
|
|
212
|
+
// Convert to lowercase
|
|
213
|
+
const lowered = type.toLowerCase();
|
|
214
|
+
|
|
215
|
+
// Check for invalid characters
|
|
216
|
+
for (let i = 0; i < lowered.length; i++) {
|
|
217
|
+
const code = lowered.charCodeAt(i);
|
|
218
|
+
if (code < 0x20 || code > 0x7e) {
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return lowered;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Convert a BlobPart to bytes.
|
|
228
|
+
*/
|
|
229
|
+
function convertToBytes(part: BlobPart, endings: 'transparent' | 'native'): Uint8Array {
|
|
230
|
+
if (typeof part === 'string') {
|
|
231
|
+
let str = part;
|
|
232
|
+
if (endings === 'native') {
|
|
233
|
+
// Convert line endings to platform-native
|
|
234
|
+
// On most platforms this is \n, but could be \r\n on Windows
|
|
235
|
+
const nativeLineEnding = '\n'; // Mobile platforms use \n
|
|
236
|
+
str = str.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
237
|
+
if (nativeLineEnding !== '\n') {
|
|
238
|
+
str = str.replace(/\n/g, nativeLineEnding);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return new TextEncoder().encode(str);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (part instanceof Blob) {
|
|
245
|
+
return part._getBytes();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (part instanceof ArrayBuffer) {
|
|
249
|
+
return new Uint8Array(part);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (ArrayBuffer.isView(part)) {
|
|
253
|
+
return new Uint8Array(part.buffer, part.byteOffset, part.byteLength);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw new TypeError('Invalid blob part type');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default Blob;
|
package/src/blob/File.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File implementation for Ibex runtime
|
|
3
|
+
*
|
|
4
|
+
* Implements the WHATWG File API.
|
|
5
|
+
* https://w3c.github.io/FileAPI/#file-section
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Blob, BlobPart, BlobPropertyBag } from './Blob';
|
|
9
|
+
|
|
10
|
+
export interface FilePropertyBag extends BlobPropertyBag {
|
|
11
|
+
lastModified?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* File represents a file with a name and optional last modified date.
|
|
16
|
+
* Extends Blob with file-specific metadata.
|
|
17
|
+
*/
|
|
18
|
+
export class File extends Blob {
|
|
19
|
+
readonly #name: string;
|
|
20
|
+
readonly #lastModified: number;
|
|
21
|
+
|
|
22
|
+
constructor(fileBits: BlobPart[], fileName: string, options?: FilePropertyBag) {
|
|
23
|
+
super(fileBits, options);
|
|
24
|
+
|
|
25
|
+
// Per spec, convert to string
|
|
26
|
+
this.#name = String(fileName);
|
|
27
|
+
|
|
28
|
+
// Default to current time if not specified
|
|
29
|
+
this.#lastModified = options?.lastModified ?? Date.now();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The name of the file.
|
|
34
|
+
*/
|
|
35
|
+
get name(): string {
|
|
36
|
+
return this.#name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The last modified time of the file, in milliseconds since the Unix epoch.
|
|
41
|
+
*/
|
|
42
|
+
get lastModified(): number {
|
|
43
|
+
return this.#lastModified;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The relative path of the file (empty for files not from a directory picker).
|
|
48
|
+
* This is a legacy property, always returns empty string.
|
|
49
|
+
*/
|
|
50
|
+
get webkitRelativePath(): string {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get [Symbol.toStringTag](): string {
|
|
55
|
+
return 'File';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default File;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormData implementation for Ibex runtime
|
|
3
|
+
*
|
|
4
|
+
* Implements the WHATWG FormData API with multipart/form-data encoding.
|
|
5
|
+
* https://xhr.spec.whatwg.org/#interface-formdata
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Blob } from './Blob';
|
|
9
|
+
import { File } from './File';
|
|
10
|
+
|
|
11
|
+
export type FormDataEntryValue = string | File;
|
|
12
|
+
|
|
13
|
+
interface BlobLikeValue {
|
|
14
|
+
type?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
lastModified?: number;
|
|
17
|
+
_getBytes(): Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of encoding FormData to multipart/form-data
|
|
22
|
+
*/
|
|
23
|
+
export interface MultipartEncodingResult {
|
|
24
|
+
/** The encoded body as bytes */
|
|
25
|
+
body: Uint8Array;
|
|
26
|
+
/** The Content-Type header value including boundary */
|
|
27
|
+
contentType: string;
|
|
28
|
+
/** The boundary string used */
|
|
29
|
+
boundary: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* FormData provides a way to construct a set of key/value pairs
|
|
34
|
+
* representing form fields and their values.
|
|
35
|
+
*/
|
|
36
|
+
export class FormData {
|
|
37
|
+
readonly #entries: Array<[string, FormDataEntryValue]> = [];
|
|
38
|
+
|
|
39
|
+
constructor(form?: unknown) {
|
|
40
|
+
// Note: In a browser, this would accept an HTMLFormElement.
|
|
41
|
+
// In Ibex runtime, we only support creating empty FormData.
|
|
42
|
+
if (form !== undefined && form !== null) {
|
|
43
|
+
throw new TypeError('FormData constructor with form element is not supported');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Appends a new value to an existing key, or adds the key if it doesn't exist.
|
|
49
|
+
*/
|
|
50
|
+
append(name: string, value: string): void;
|
|
51
|
+
append(name: string, value: Blob, filename?: string): void;
|
|
52
|
+
append(name: string, value: string | Blob, filename?: string): void {
|
|
53
|
+
const normalizedName = String(name);
|
|
54
|
+
const entry = normalizeValue(value, filename);
|
|
55
|
+
this.#entries.push([normalizedName, entry]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Deletes all entries with the given name.
|
|
60
|
+
*/
|
|
61
|
+
delete(name: string): void {
|
|
62
|
+
const normalizedName = String(name);
|
|
63
|
+
for (let i = this.#entries.length - 1; i >= 0; i--) {
|
|
64
|
+
if (this.#entries[i][0] === normalizedName) {
|
|
65
|
+
this.#entries.splice(i, 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the first value associated with a given key.
|
|
72
|
+
*/
|
|
73
|
+
get(name: string): FormDataEntryValue | null {
|
|
74
|
+
const normalizedName = String(name);
|
|
75
|
+
for (const [entryName, value] of this.#entries) {
|
|
76
|
+
if (entryName === normalizedName) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns all values associated with a given key.
|
|
85
|
+
*/
|
|
86
|
+
getAll(name: string): FormDataEntryValue[] {
|
|
87
|
+
const normalizedName = String(name);
|
|
88
|
+
const result: FormDataEntryValue[] = [];
|
|
89
|
+
for (const [entryName, value] of this.#entries) {
|
|
90
|
+
if (entryName === normalizedName) {
|
|
91
|
+
result.push(value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns whether a key exists in the FormData.
|
|
99
|
+
*/
|
|
100
|
+
has(name: string): boolean {
|
|
101
|
+
const normalizedName = String(name);
|
|
102
|
+
for (const [entryName] of this.#entries) {
|
|
103
|
+
if (entryName === normalizedName) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sets a key to a value, replacing any existing entries with that key.
|
|
112
|
+
*/
|
|
113
|
+
set(name: string, value: string): void;
|
|
114
|
+
set(name: string, value: Blob, filename?: string): void;
|
|
115
|
+
set(name: string, value: string | Blob, filename?: string): void {
|
|
116
|
+
const normalizedName = String(name);
|
|
117
|
+
const entry = normalizeValue(value, filename);
|
|
118
|
+
|
|
119
|
+
// Find first occurrence and replace
|
|
120
|
+
let found = false;
|
|
121
|
+
for (let i = 0; i < this.#entries.length; i++) {
|
|
122
|
+
if (this.#entries[i][0] === normalizedName) {
|
|
123
|
+
if (!found) {
|
|
124
|
+
this.#entries[i] = [normalizedName, entry];
|
|
125
|
+
found = true;
|
|
126
|
+
} else {
|
|
127
|
+
// Remove subsequent entries
|
|
128
|
+
this.#entries.splice(i, 1);
|
|
129
|
+
i--;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If not found, append
|
|
135
|
+
if (!found) {
|
|
136
|
+
this.#entries.push([normalizedName, entry]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns an iterator of all keys.
|
|
142
|
+
*/
|
|
143
|
+
*keys(): IterableIterator<string> {
|
|
144
|
+
for (const [name] of this.#entries) {
|
|
145
|
+
yield name;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns an iterator of all values.
|
|
151
|
+
*/
|
|
152
|
+
*values(): IterableIterator<FormDataEntryValue> {
|
|
153
|
+
for (const [, value] of this.#entries) {
|
|
154
|
+
yield value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns an iterator of all key/value pairs.
|
|
160
|
+
*/
|
|
161
|
+
*entries(): IterableIterator<[string, FormDataEntryValue]> {
|
|
162
|
+
for (const entry of this.#entries) {
|
|
163
|
+
yield entry;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Calls a function for each key/value pair.
|
|
169
|
+
*/
|
|
170
|
+
forEach(
|
|
171
|
+
callback: (value: FormDataEntryValue, key: string, parent: FormData) => void,
|
|
172
|
+
thisArg?: unknown
|
|
173
|
+
): void {
|
|
174
|
+
// Use indexed loop to avoid the Rolldown for-of → forEach transform which
|
|
175
|
+
// rewrites `this` inside the callback to the global object.
|
|
176
|
+
const entries = this.#entries;
|
|
177
|
+
for (let i = 0; i < entries.length; i++) {
|
|
178
|
+
callback.call(thisArg, entries[i][1], entries[i][0], this);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]> {
|
|
183
|
+
return this.entries();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get [Symbol.toStringTag](): string {
|
|
187
|
+
return 'FormData';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Internal method to get all entries for serialization.
|
|
192
|
+
* Used by fetch() to build multipart/form-data body.
|
|
193
|
+
*/
|
|
194
|
+
_getEntries(): Array<[string, FormDataEntryValue]> {
|
|
195
|
+
return [...this.#entries];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Encode FormData as multipart/form-data.
|
|
200
|
+
*
|
|
201
|
+
* This method is used by fetch() when sending FormData as request body.
|
|
202
|
+
*
|
|
203
|
+
* @returns Object containing body bytes, content-type header, and boundary
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const formData = new FormData();
|
|
207
|
+
* formData.append('name', 'John');
|
|
208
|
+
* formData.append('file', new File(['content'], 'test.txt'));
|
|
209
|
+
* const { body, contentType } = formData._encode();
|
|
210
|
+
* // contentType = 'multipart/form-data; boundary=----ExactFormBoundary...'
|
|
211
|
+
*/
|
|
212
|
+
_encode(): MultipartEncodingResult {
|
|
213
|
+
// Generate a unique boundary
|
|
214
|
+
const boundary = generateBoundary();
|
|
215
|
+
const encoder = new TextEncoder();
|
|
216
|
+
const parts: Uint8Array[] = [];
|
|
217
|
+
const CRLF = '\r\n';
|
|
218
|
+
|
|
219
|
+
for (const [name, value] of this.#entries) {
|
|
220
|
+
// Add boundary
|
|
221
|
+
parts.push(encoder.encode(`--${boundary}${CRLF}`));
|
|
222
|
+
|
|
223
|
+
if (typeof value === 'string') {
|
|
224
|
+
// Text field
|
|
225
|
+
parts.push(encoder.encode(
|
|
226
|
+
`Content-Disposition: form-data; name="${escapeQuotes(name)}"${CRLF}` +
|
|
227
|
+
`${CRLF}` +
|
|
228
|
+
`${value}${CRLF}`
|
|
229
|
+
));
|
|
230
|
+
} else {
|
|
231
|
+
// File field
|
|
232
|
+
const filename = value.name || 'blob';
|
|
233
|
+
const contentType = value.type || 'application/octet-stream';
|
|
234
|
+
|
|
235
|
+
parts.push(encoder.encode(
|
|
236
|
+
`Content-Disposition: form-data; name="${escapeQuotes(name)}"; filename="${escapeQuotes(filename)}"${CRLF}` +
|
|
237
|
+
`Content-Type: ${contentType}${CRLF}` +
|
|
238
|
+
`${CRLF}`
|
|
239
|
+
));
|
|
240
|
+
|
|
241
|
+
// Add file content
|
|
242
|
+
parts.push(value._getBytes());
|
|
243
|
+
parts.push(encoder.encode(CRLF));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add closing boundary
|
|
248
|
+
parts.push(encoder.encode(`--${boundary}--${CRLF}`));
|
|
249
|
+
|
|
250
|
+
// Concatenate all parts
|
|
251
|
+
const totalLength = parts.reduce((acc, part) => acc + part.byteLength, 0);
|
|
252
|
+
const body = new Uint8Array(totalLength);
|
|
253
|
+
let offset = 0;
|
|
254
|
+
for (const part of parts) {
|
|
255
|
+
body.set(part, offset);
|
|
256
|
+
offset += part.byteLength;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
body,
|
|
261
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
262
|
+
boundary,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generate a unique boundary string for multipart encoding.
|
|
269
|
+
*/
|
|
270
|
+
function generateBoundary(): string {
|
|
271
|
+
// Use a combination of timestamp and random values
|
|
272
|
+
const timestamp = Date.now().toString(36);
|
|
273
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
274
|
+
const random2 = Math.random().toString(36).slice(2, 10);
|
|
275
|
+
return `----ExactFormBoundary${timestamp}${random}${random2}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Escape quotes in a string for use in Content-Disposition header.
|
|
280
|
+
*/
|
|
281
|
+
function escapeQuotes(str: string): string {
|
|
282
|
+
return str.replace(/"/g, '\\"').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isBlobLikeValue(value: unknown): value is BlobLikeValue {
|
|
286
|
+
return (
|
|
287
|
+
typeof value === 'object' &&
|
|
288
|
+
value !== null &&
|
|
289
|
+
typeof (value as BlobLikeValue)._getBytes === 'function'
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Normalize a value for storage in FormData.
|
|
295
|
+
* Strings are stored as-is, Blobs are converted to Files.
|
|
296
|
+
*/
|
|
297
|
+
function normalizeValue(value: string | Blob | BlobLikeValue, filename?: string): FormDataEntryValue {
|
|
298
|
+
if (typeof value === 'string') {
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// If it's already a File and no filename override, use as-is
|
|
303
|
+
if (value instanceof File && filename === undefined) {
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!isBlobLikeValue(value)) {
|
|
308
|
+
throw new TypeError("FormData value must be a string, Blob, or File");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Convert Blob to File
|
|
312
|
+
const name = filename ?? (typeof value.name === 'string' ? value.name : 'blob');
|
|
313
|
+
const options = {
|
|
314
|
+
type: value.type,
|
|
315
|
+
lastModified: typeof value.lastModified === 'number' ? value.lastModified : Date.now(),
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Need to extract the bytes from the Blob
|
|
319
|
+
const bytes = value._getBytes();
|
|
320
|
+
return new File([bytes], name, options);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default FormData;
|