1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host.file;
16
17 import static java.nio.charset.StandardCharsets.UTF_8;
18
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.io.Serializable;
22 import java.nio.charset.Charset;
23 import java.util.Locale;
24
25 import org.htmlunit.BrowserVersion;
26 import org.htmlunit.HttpHeader;
27 import org.htmlunit.WebRequest;
28 import org.htmlunit.corejs.javascript.NativeArray;
29 import org.htmlunit.corejs.javascript.NativePromise;
30 import org.htmlunit.corejs.javascript.Scriptable;
31 import org.htmlunit.corejs.javascript.ScriptableObject;
32 import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
33 import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
34 import org.htmlunit.javascript.HtmlUnitScriptable;
35 import org.htmlunit.javascript.JavaScriptEngine;
36 import org.htmlunit.javascript.configuration.JsxClass;
37 import org.htmlunit.javascript.configuration.JsxConstructor;
38 import org.htmlunit.javascript.configuration.JsxFunction;
39 import org.htmlunit.javascript.configuration.JsxGetter;
40 import org.htmlunit.javascript.host.ReadableStream;
41 import org.htmlunit.util.KeyDataPair;
42 import org.htmlunit.util.MimeType;
43 import org.htmlunit.util.StringUtils;
44
45
46
47
48
49
50
51
52 @JsxClass
53 public class Blob extends HtmlUnitScriptable {
54 private static final String OPTIONS_TYPE_NAME = "type";
55
56 private static final String OPTIONS_TYPE_DEFAULT = "";
57 private static final String OPTIONS_LASTMODIFIED = "lastModified";
58
59 private Backend backend_;
60
61
62
63
64 protected abstract static class Backend implements Serializable {
65
66
67
68
69
70
71 abstract String getName();
72
73
74
75
76
77
78 abstract long getLastModified();
79
80
81
82
83
84
85 abstract long getSize();
86
87
88
89
90
91
92
93 abstract String getType(BrowserVersion browserVersion);
94
95
96
97
98
99
100
101 abstract String getText() throws IOException;
102
103
104
105
106
107
108
109
110 abstract byte[] getBytes(int start, int end);
111
112
113
114
115 Backend() {
116
117 }
118
119
120
121
122
123
124
125
126
127 abstract KeyDataPair getKeyDataPair(String name, String fileName, String contentType);
128 }
129
130
131
132
133
134 protected static class InMemoryBackend extends Backend {
135 private final String fileName_;
136 private final String type_;
137 private final long lastModified_;
138 private final byte[] bytes_;
139
140
141
142
143
144
145
146
147
148 protected InMemoryBackend(final byte[] bytes, final String fileName,
149 final String type, final long lastModified) {
150 super();
151 fileName_ = fileName;
152 type_ = type;
153 lastModified_ = lastModified;
154 bytes_ = bytes;
155 }
156
157
158
159
160
161
162
163
164
165
166 protected static InMemoryBackend create(final NativeArray fileBits, final String fileName,
167 final String type, final long lastModified) {
168 if (fileBits == null) {
169 return new InMemoryBackend(new byte[0], fileName, type, lastModified);
170 }
171
172 final ByteArrayOutputStream out = new ByteArrayOutputStream();
173 final long length = fileBits.getLength();
174 for (long i = 0; i < length; i++) {
175 final Object fileBit = fileBits.get(i);
176 if (fileBit instanceof NativeArrayBuffer buffer) {
177 final byte[] bytes = buffer.getBuffer();
178 out.write(bytes, 0, bytes.length);
179 }
180 else if (fileBit instanceof NativeArrayBufferView view) {
181 final byte[] bytes = view.getBuffer().getBuffer();
182 out.write(bytes, 0, bytes.length);
183 }
184 else if (fileBit instanceof Blob blob) {
185 final byte[] bytes = blob.getBackend().getBytes(0, (int) blob.getSize());
186 out.write(bytes, 0, bytes.length);
187 }
188 else {
189 final String bits = JavaScriptEngine.toString(fileBits.get(i));
190
191 final byte[] bytes = bits.getBytes(UTF_8);
192 out.write(bytes, 0, bytes.length);
193 }
194 }
195 return new InMemoryBackend(out.toByteArray(), fileName, type, lastModified);
196 }
197
198
199
200
201 @Override
202 public String getName() {
203 return fileName_;
204 }
205
206
207
208
209 @Override
210 public long getLastModified() {
211 return lastModified_;
212 }
213
214
215
216
217 @Override
218 public long getSize() {
219 return bytes_.length;
220 }
221
222
223
224
225 @Override
226 public String getType(final BrowserVersion browserVersion) {
227 return type_.toLowerCase(Locale.ROOT);
228 }
229
230
231
232
233 @Override
234 public String getText() throws IOException {
235 return new String(bytes_, UTF_8);
236 }
237
238
239
240
241 @Override
242 public byte[] getBytes(final int start, final int end) {
243 final byte[] result = new byte[end - start];
244 System.arraycopy(bytes_, start, result, 0, result.length);
245 return result;
246 }
247
248
249
250
251 @Override
252 public KeyDataPair getKeyDataPair(final String name, final String fileName, final String contentType) {
253 String fname = fileName;
254 if (fname == null) {
255 fname = getName();
256 }
257 final KeyDataPair data = new KeyDataPair(name, null, fname, contentType, (Charset) null);
258 data.setData(bytes_);
259 return data;
260 }
261 }
262
263 protected static String extractFileTypeOrDefault(final ScriptableObject properties) {
264 if (properties == null || JavaScriptEngine.isUndefined(properties)) {
265 return OPTIONS_TYPE_DEFAULT;
266 }
267
268 final Object optionsType = properties.get(OPTIONS_TYPE_NAME, properties);
269 if (optionsType != null && properties != Scriptable.NOT_FOUND
270 && !JavaScriptEngine.isUndefined(optionsType)) {
271 return JavaScriptEngine.toString(optionsType);
272 }
273
274 return OPTIONS_TYPE_DEFAULT;
275 }
276
277 protected static long extractLastModifiedOrDefault(final ScriptableObject properties) {
278 if (properties == null || JavaScriptEngine.isUndefined(properties)) {
279 return System.currentTimeMillis();
280 }
281
282 final Object optionsType = properties.get(OPTIONS_LASTMODIFIED, properties);
283 if (optionsType != null && optionsType != Scriptable.NOT_FOUND
284 && !JavaScriptEngine.isUndefined(optionsType)) {
285 try {
286 return Long.parseLong(JavaScriptEngine.toString(optionsType));
287 }
288 catch (final NumberFormatException ignored) {
289
290 }
291 }
292
293 return System.currentTimeMillis();
294 }
295
296
297
298
299 public Blob() {
300 super();
301 }
302
303
304
305
306
307
308 @JsxConstructor
309 public void jsConstructor(final NativeArray fileBits, final ScriptableObject properties) {
310 NativeArray nativeBits = fileBits;
311 if (JavaScriptEngine.isUndefined(fileBits)) {
312 nativeBits = null;
313 }
314
315 backend_ = InMemoryBackend.create(nativeBits, null,
316 extractFileTypeOrDefault(properties),
317 extractLastModifiedOrDefault(properties));
318 }
319
320
321
322
323
324
325
326 public Blob(final byte[] bytes, final String contentType) {
327 super();
328 setBackend(new InMemoryBackend(bytes, null, contentType, -1));
329 }
330
331
332
333
334
335
336 @JsxGetter
337 public long getSize() {
338 return getBackend().getSize();
339 }
340
341
342
343
344
345
346 @JsxGetter
347 public String getType() {
348 return getBackend().getType(getBrowserVersion());
349 }
350
351
352
353
354
355
356
357
358 @JsxFunction
359 public NativePromise arrayBuffer() {
360 return setupPromise(() -> {
361 final byte[] bytes = getBytes();
362 final NativeArrayBuffer buffer = new NativeArrayBuffer(bytes.length);
363 System.arraycopy(bytes, 0, buffer.getBuffer(), 0, bytes.length);
364 buffer.setParentScope(getParentScope());
365 buffer.setPrototype(ScriptableObject.getClassPrototype(getParentScope(), buffer.getClassName()));
366 return buffer;
367 });
368 }
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385 @JsxFunction
386 public Blob slice(final Object start, final Object end, final Object contentType) {
387 final Blob blob = new Blob();
388 blob.setParentScope(getParentScope());
389 blob.setPrototype(getPrototype(Blob.class));
390
391 final int size = (int) getSize();
392 int usedStart = 0;
393 int usedEnd = size;
394 if (start != null && !JavaScriptEngine.isUndefined(start)) {
395 usedStart = JavaScriptEngine.toInt32(start);
396 if (usedStart < 0) {
397 usedStart = size + usedStart;
398 }
399 usedStart = Math.max(0, usedStart);
400 }
401
402 if (end != null && !JavaScriptEngine.isUndefined(end)) {
403 usedEnd = JavaScriptEngine.toInt32(end);
404 if (usedEnd < 0) {
405 usedEnd = size + usedEnd;
406 }
407 usedEnd = Math.min(size, usedEnd);
408 }
409
410 String usedContentType = "";
411 if (contentType != null && !JavaScriptEngine.isUndefined(contentType)) {
412 usedContentType = JavaScriptEngine.toString(contentType).toLowerCase(Locale.ROOT);
413 }
414
415 if (usedEnd <= usedStart || usedStart >= getSize()) {
416 blob.setBackend(new InMemoryBackend(new byte[0], null, usedContentType, 0L));
417 return blob;
418 }
419
420 blob.setBackend(new InMemoryBackend(getBackend().getBytes(usedStart, usedEnd), null, usedContentType, 0L));
421 return blob;
422 }
423
424
425
426
427
428
429 @JsxFunction
430 public ReadableStream stream() {
431 throw new UnsupportedOperationException("Blob.stream() is not yet implemented.");
432 }
433
434
435
436
437
438
439
440
441 @JsxFunction
442 public NativePromise text() {
443 return setupPromise(() -> getBackend().getText());
444 }
445
446
447
448
449
450
451 public byte[] getBytes() {
452 return getBackend().getBytes(0, (int) getBackend().getSize());
453 }
454
455
456
457
458
459 public void fillRequest(final WebRequest webRequest) {
460 webRequest.setRequestBody(new String(getBytes(), UTF_8));
461
462 final boolean contentTypeDefinedByCaller = webRequest.getAdditionalHeader(HttpHeader.CONTENT_TYPE) != null;
463 if (!contentTypeDefinedByCaller) {
464 final String mimeType = getType();
465 if (StringUtils.isNotBlank(mimeType)) {
466 webRequest.setAdditionalHeader(HttpHeader.CONTENT_TYPE, mimeType);
467 }
468 webRequest.setEncodingType(null);
469 }
470 }
471
472
473
474
475
476
477
478 public KeyDataPair getKeyDataPair(final String name, final String fileName) {
479 String contentType = getType();
480 if (StringUtils.isEmptyOrNull(contentType)) {
481 contentType = MimeType.APPLICATION_OCTET_STREAM;
482 }
483
484 return backend_.getKeyDataPair(name, fileName, contentType);
485 }
486
487 protected Backend getBackend() {
488 return backend_;
489 }
490
491 protected void setBackend(final Backend backend) {
492 backend_ = backend;
493 }
494
495 }