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             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                     // Todo normalize line breaks
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          * {@inheritDoc}
188          */
189         @Override
190         public String getName() {
191             return fileName_;
192         }
193 
194         /**
195          * {@inheritDoc}
196          */
197         @Override
198         public long getLastModified() {
199             return lastModified_;
200         }
201 
202         /**
203          * {@inheritDoc}
204          */
205         @Override
206         public long getSize() {
207             return bytes_.length;
208         }
209 
210         /**
211          * {@inheritDoc}
212          */
213         @Override
214         public String getType(final BrowserVersion browserVersion) {
215             return type_.toLowerCase(Locale.ROOT);
216         }
217 
218         /**
219          * {@inheritDoc}
220          */
221         @Override
222         public String getText() throws IOException {
223             return new String(bytes_, UTF_8);
224         }
225 
226         /**
227          * {@inheritDoc}
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          * {@inheritDoc}
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                 // fall back to default
278             }
279         }
280 
281         return System.currentTimeMillis();
282     }
283 
284     /**
285      * Creates an instance.
286      */
287     public Blob() {
288         super();
289     }
290 
291     /**
292      * Creates an instance.
293      * @param fileBits the bits
294      * @param properties the properties
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      * Ctor.
310      *
311      * @param bytes the bytes
312      * @param contentType the content type
313      */
314     public Blob(final byte[] bytes, final String contentType) {
315         super();
316         setBackend(new InMemoryBackend(bytes, null, contentType, -1));
317     }
318 
319     /**
320      * Returns the {@code size} property.
321      * @return the {@code size} property
322      */
323     @JsxGetter
324     public long getSize() {
325         return getBackend().getSize();
326     }
327 
328     /**
329      * Returns the {@code type} property.
330      * @return the {@code type} property
331      */
332     @JsxGetter
333     public String getType() {
334         return getBackend().getType(getBrowserVersion());
335     }
336 
337     /**
338      * @return a Promise that resolves with an ArrayBuffer containing the
339      *         data in binary form.
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      * @param start An index into the Blob indicating the first byte to include in the new Blob. If you specify
355      *        a negative value, it's treated as an offset from the end of the Blob toward the beginning.
356      *        For example, -10 would be the 10th from last byte in the Blob. The default value is 0.
357      *        If you specify a value for start that is larger than the size of the source Blob,
358      *        the returned Blob has size 0 and contains no data.
359      * @param end An index into the Blob indicating the first byte that will not be included in the
360      *        new Blob (i.e. the byte exactly at this index is not included). If you specify a negative value,
361      *        it's treated as an offset from the end of the Blob toward the beginning.
362      *        For example, -10 would be the 10th from last byte in the Blob. The default value is size.
363      * @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.
364      * @return a new Blob object which contains data from a subset of the blob on which it's called.
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      * @return a ReadableStream which, upon reading, returns the contents of the Blob.
407      */
408     @JsxFunction
409     public ReadableStream stream() {
410         throw new UnsupportedOperationException("Blob.stream() is not yet implemented.");
411     }
412 
413     /**
414      * @return a Promise that resolves with a string containing the
415      *         contents of the blob, interpreted as UTF-8.
416      */
417     @JsxFunction
418     public NativePromise text() {
419         return setupPromise(() -> getBackend().getText());
420     }
421 
422     /**
423      * @return the bytes of this blob
424      */
425     public byte[] getBytes() {
426         return getBackend().getBytes(0, (int) getBackend().getSize());
427     }
428 
429     /**
430      * Sets the specified request with the parameters in this {@code FormData}.
431      * @param webRequest the web request to fill
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      * Delegates the KeyDataPair construction to the backend.
448      * @param name the name
449      * @param fileName the filename
450      * @return the constructed {@link KeyDataPair}
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 }