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