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