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