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 for (long i = 0; i < fileBits.getLength(); i++) {
161 final Object fileBit = fileBits.get(i);
162 if (fileBit instanceof NativeArrayBuffer) {
163 final byte[] bytes = ((NativeArrayBuffer) fileBit).getBuffer();
164 out.write(bytes, 0, bytes.length);
165 }
166 else if (fileBit instanceof NativeArrayBufferView) {
167 final byte[] bytes = ((NativeArrayBufferView) fileBit).getBuffer().getBuffer();
168 out.write(bytes, 0, bytes.length);
169 }
170 else if (fileBit instanceof Blob) {
171 final Blob blob = (Blob) fileBit;
172 final byte[] bytes = blob.getBackend().getBytes(0, (int) blob.getSize());
173 out.write(bytes, 0, bytes.length);
174 }
175 else {
176 final String bits = JavaScriptEngine.toString(fileBits.get(i));
177
178 final byte[] bytes = bits.getBytes(UTF_8);
179 out.write(bytes, 0, bytes.length);
180 }
181 }
182 return new InMemoryBackend(out.toByteArray(), fileName, type, lastModified);
183 }
184
185
186
187
188 @Override
189 public String getName() {
190 return fileName_;
191 }
192
193
194
195
196 @Override
197 public long getLastModified() {
198 return lastModified_;
199 }
200
201
202
203
204 @Override
205 public long getSize() {
206 return bytes_.length;
207 }
208
209
210
211
212 @Override
213 public String getType(final BrowserVersion browserVersion) {
214 return type_.toLowerCase(Locale.ROOT);
215 }
216
217
218
219
220 @Override
221 public String getText() throws IOException {
222 return new String(bytes_, UTF_8);
223 }
224
225
226
227
228 @Override
229 public byte[] getBytes(final int start, final int end) {
230 final byte[] result = new byte[end - start];
231 System.arraycopy(bytes_, start, result, 0, result.length);
232 return result;
233 }
234
235
236
237
238 @Override
239 public KeyDataPair getKeyDataPair(final String name, final String fileName, final String contentType) {
240 String fname = fileName;
241 if (fname == null) {
242 fname = getName();
243 }
244 final KeyDataPair data = new KeyDataPair(name, null, fname, contentType, (Charset) null);
245 data.setData(bytes_);
246 return data;
247 }
248 }
249
250 protected static String extractFileTypeOrDefault(final ScriptableObject properties) {
251 if (properties == null || JavaScriptEngine.isUndefined(properties)) {
252 return OPTIONS_TYPE_DEFAULT;
253 }
254
255 final Object optionsType = properties.get(OPTIONS_TYPE_NAME, properties);
256 if (optionsType != null && properties != Scriptable.NOT_FOUND
257 && !JavaScriptEngine.isUndefined(optionsType)) {
258 return JavaScriptEngine.toString(optionsType);
259 }
260
261 return OPTIONS_TYPE_DEFAULT;
262 }
263
264 protected static long extractLastModifiedOrDefault(final ScriptableObject properties) {
265 if (properties == null || JavaScriptEngine.isUndefined(properties)) {
266 return System.currentTimeMillis();
267 }
268
269 final Object optionsType = properties.get(OPTIONS_LASTMODIFIED, properties);
270 if (optionsType != null && optionsType != Scriptable.NOT_FOUND
271 && !JavaScriptEngine.isUndefined(optionsType)) {
272 try {
273 return Long.parseLong(JavaScriptEngine.toString(optionsType));
274 }
275 catch (final NumberFormatException ignored) {
276
277 }
278 }
279
280 return System.currentTimeMillis();
281 }
282
283
284
285
286 public Blob() {
287 super();
288 }
289
290
291
292
293
294
295 @JsxConstructor
296 public void jsConstructor(final NativeArray fileBits, final ScriptableObject properties) {
297 NativeArray nativeBits = fileBits;
298 if (JavaScriptEngine.isUndefined(fileBits)) {
299 nativeBits = null;
300 }
301
302 backend_ = InMemoryBackend.create(nativeBits, null,
303 extractFileTypeOrDefault(properties),
304 extractLastModifiedOrDefault(properties));
305 }
306
307
308
309
310
311
312
313 public Blob(final byte[] bytes, final String contentType) {
314 super();
315 setBackend(new InMemoryBackend(bytes, null, contentType, -1));
316 }
317
318
319
320
321
322 @JsxGetter
323 public long getSize() {
324 return getBackend().getSize();
325 }
326
327
328
329
330
331 @JsxGetter
332 public String getType() {
333 return getBackend().getType(getBrowserVersion());
334 }
335
336
337
338
339
340 @JsxFunction
341 public NativePromise arrayBuffer() {
342 return setupPromise(() -> {
343 final byte[] bytes = getBytes();
344 final NativeArrayBuffer buffer = new NativeArrayBuffer(bytes.length);
345 System.arraycopy(bytes, 0, buffer.getBuffer(), 0, bytes.length);
346 buffer.setParentScope(getParentScope());
347 buffer.setPrototype(ScriptableObject.getClassPrototype(getWindow(), buffer.getClassName()));
348 return buffer;
349 });
350 }
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365 @JsxFunction
366 public Blob slice(final Object start, final Object end, final Object contentType) {
367 final Blob blob = new Blob();
368 blob.setParentScope(getParentScope());
369 blob.setPrototype(getPrototype(Blob.class));
370
371 final int size = (int) getSize();
372 int usedStart = 0;
373 int usedEnd = size;
374 if (start != null && !JavaScriptEngine.isUndefined(start)) {
375 usedStart = JavaScriptEngine.toInt32(start);
376 if (usedStart < 0) {
377 usedStart = size + usedStart;
378 }
379 usedStart = Math.max(0, usedStart);
380 }
381
382 if (end != null && !JavaScriptEngine.isUndefined(end)) {
383 usedEnd = JavaScriptEngine.toInt32(end);
384 if (usedEnd < 0) {
385 usedEnd = size + usedEnd;
386 }
387 usedEnd = Math.min(size, usedEnd);
388 }
389
390 String usedContentType = "";
391 if (contentType != null && !JavaScriptEngine.isUndefined(contentType)) {
392 usedContentType = JavaScriptEngine.toString(contentType).toLowerCase(Locale.ROOT);
393 }
394
395 if (usedEnd <= usedStart || usedStart >= getSize()) {
396 blob.setBackend(new InMemoryBackend(new byte[0], null, usedContentType, 0L));
397 return blob;
398 }
399
400 blob.setBackend(new InMemoryBackend(getBackend().getBytes(usedStart, usedEnd), null, usedContentType, 0L));
401 return blob;
402 }
403
404
405
406
407 @JsxFunction
408 public ReadableStream stream() {
409 throw new UnsupportedOperationException("Blob.stream() is not yet implemented.");
410 }
411
412
413
414
415
416 @JsxFunction
417 public NativePromise text() {
418 return setupPromise(() -> getBackend().getText());
419 }
420
421
422
423
424 public byte[] getBytes() {
425 return getBackend().getBytes(0, (int) getBackend().getSize());
426 }
427
428
429
430
431
432 public void fillRequest(final WebRequest webRequest) {
433 webRequest.setRequestBody(new String(getBytes(), UTF_8));
434
435 final boolean contentTypeDefinedByCaller = webRequest.getAdditionalHeader(HttpHeader.CONTENT_TYPE) != null;
436 if (!contentTypeDefinedByCaller) {
437 final String mimeType = getType();
438 if (StringUtils.isNotBlank(mimeType)) {
439 webRequest.setAdditionalHeader(HttpHeader.CONTENT_TYPE, mimeType);
440 }
441 webRequest.setEncodingType(null);
442 }
443 }
444
445
446
447
448
449
450
451 public KeyDataPair getKeyDataPair(final String name, final String fileName) {
452 String contentType = getType();
453 if (StringUtils.isEmpty(contentType)) {
454 contentType = MimeType.APPLICATION_OCTET_STREAM;
455 }
456
457 return backend_.getKeyDataPair(name, fileName, contentType);
458 }
459
460 protected Backend getBackend() {
461 return backend_;
462 }
463
464 protected void setBackend(final Backend backend) {
465 backend_ = backend;
466 }
467
468 }