View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
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   * A JavaScript object for {@code Blob}.
47   *
48   * @author Ahmed Ashour
49   * @author Ronald Brill
50   * @author Lai Quang Duong
51   */
52  @JsxClass
53  public class Blob extends HtmlUnitScriptable {
54      private static final String OPTIONS_TYPE_NAME = "type";
55      //default according to https://developer.mozilla.org/en-US/docs/Web/API/File/File
56      private static final String OPTIONS_TYPE_DEFAULT = "";
57      private static final String OPTIONS_LASTMODIFIED = "lastModified";
58  
59      private Backend backend_;
60  
61      /**
62       * The backend used for saving the blob.
63       */
64      protected abstract static class Backend implements Serializable {
65          /**
66           * @return the name
67           */
68          abstract String getName();
69  
70          /**
71           * @return the last modified timestamp as long
72           */
73          abstract long getLastModified();
74  
75          /**
76           * @return the size
77           */
78          abstract long getSize();
79  
80          /**
81           * @param browserVersion the {@link BrowserVersion}
82           * @return the type
83           */
84          abstract String getType(BrowserVersion browserVersion);
85  
86          /**
87           * @return the text
88           * @throws IOException in case of error
89           */
90          abstract String getText() throws IOException;
91  
92          /**
93           * @param start the start position
94           * @param end the end position
95           * @return the bytes
96           */
97          abstract byte[] getBytes(int start, int end);
98  
99          /**
100          * Ctor.
101          */
102         Backend() {
103             // to make it package protected
104         }
105 
106         /**
107          * Returns the KeyDataPair for this Blob/File.
108          *
109          * @param name the name
110          * @param fileName the file name
111          * @param contentType the content type
112          * @return the KeyDataPair to hold the data
113          */
114         abstract KeyDataPair getKeyDataPair(String name, String fileName, String contentType);
115     }
116 
117     /**
118      * Implementation of the {@link Backend} that stores the bytes in memory.
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          * Ctor.
129          *
130          * @param bytes the bytes
131          * @param fileName the name
132          * @param type the type
133          * @param lastModified last modified
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          * Factory method to create an {@link InMemoryBackend} from an {@link NativeArray}.
146          *
147          * @param fileBits the bytes as {@link NativeArray}
148          * @param fileName the name
149          * @param type the type
150          * @param lastModified last modified
151          * @return the new {@link InMemoryBackend}
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                     // Todo normalize line breaks
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          * {@inheritDoc}
187          */
188         @Override
189         public String getName() {
190             return fileName_;
191         }
192 
193         /**
194          * {@inheritDoc}
195          */
196         @Override
197         public long getLastModified() {
198             return lastModified_;
199         }
200 
201         /**
202          * {@inheritDoc}
203          */
204         @Override
205         public long getSize() {
206             return bytes_.length;
207         }
208 
209         /**
210          * {@inheritDoc}
211          */
212         @Override
213         public String getType(final BrowserVersion browserVersion) {
214             return type_.toLowerCase(Locale.ROOT);
215         }
216 
217         /**
218          * {@inheritDoc}
219          */
220         @Override
221         public String getText() throws IOException {
222             return new String(bytes_, UTF_8);
223         }
224 
225         /**
226          * {@inheritDoc}
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          * {@inheritDoc}
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                 // fall back to default
277             }
278         }
279 
280         return System.currentTimeMillis();
281     }
282 
283     /**
284      * Creates an instance.
285      */
286     public Blob() {
287         super();
288     }
289 
290     /**
291      * Creates an instance.
292      * @param fileBits the bits
293      * @param properties the properties
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      * Ctor.
309      *
310      * @param bytes the bytes
311      * @param contentType the content type
312      */
313     public Blob(final byte[] bytes, final String contentType) {
314         super();
315         setBackend(new InMemoryBackend(bytes, null, contentType, -1));
316     }
317 
318     /**
319      * Returns the {@code size} property.
320      * @return the {@code size} property
321      */
322     @JsxGetter
323     public long getSize() {
324         return getBackend().getSize();
325     }
326 
327     /**
328      * Returns the {@code type} property.
329      * @return the {@code type} property
330      */
331     @JsxGetter
332     public String getType() {
333         return getBackend().getType(getBrowserVersion());
334     }
335 
336     /**
337      * @return a Promise that resolves with an ArrayBuffer containing the
338      *         data in binary form.
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      * @param start An index into the Blob indicating the first byte to include in the new Blob. If you specify
354      *        a negative value, it's treated as an offset from the end of the Blob toward the beginning.
355      *        For example, -10 would be the 10th from last byte in the Blob. The default value is 0.
356      *        If you specify a value for start that is larger than the size of the source Blob,
357      *        the returned Blob has size 0 and contains no data.
358      * @param end An index into the Blob indicating the first byte that will not be included in the
359      *        new Blob (i.e. the byte exactly at this index is not included). If you specify a negative value,
360      *        it's treated as an offset from the end of the Blob toward the beginning.
361      *        For example, -10 would be the 10th from last byte in the Blob. The default value is size.
362      * @param contentType The content type to assign to the new Blob; this will be the value of its type property. The default value is an empty string.
363      * @return a new Blob object which contains data from a subset of the blob on which it's called.
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      * @return a ReadableStream which, upon reading, returns the contents of the Blob.
406      */
407     @JsxFunction
408     public ReadableStream stream() {
409         throw new UnsupportedOperationException("Blob.stream() is not yet implemented.");
410     }
411 
412     /**
413      * @return a Promise that resolves with a string containing the
414      *         contents of the blob, interpreted as UTF-8.
415      */
416     @JsxFunction
417     public NativePromise text() {
418         return setupPromise(() -> getBackend().getText());
419     }
420 
421     /**
422      * @return the bytes of this blob
423      */
424     public byte[] getBytes() {
425         return getBackend().getBytes(0, (int) getBackend().getSize());
426     }
427 
428     /**
429      * Sets the specified request with the parameters in this {@code FormData}.
430      * @param webRequest the web request to fill
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      * Delegates the KeyDataPair construction to the backend.
447      * @param name the name
448      * @param fileName the filename
449      * @return the constructed {@link KeyDataPair}
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 }