View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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;
16  
17  import java.nio.charset.Charset;
18  import java.util.Arrays;
19  import java.util.Locale;
20  
21  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
22  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
23  import org.htmlunit.cyberneko.xerces.util.StandardEncodingTranslator;
24  import org.htmlunit.javascript.HtmlUnitScriptable;
25  import org.htmlunit.javascript.JavaScriptEngine;
26  import org.htmlunit.javascript.configuration.JsxClass;
27  import org.htmlunit.javascript.configuration.JsxConstructor;
28  import org.htmlunit.javascript.configuration.JsxFunction;
29  import org.htmlunit.javascript.configuration.JsxGetter;
30  import org.htmlunit.util.XUserDefinedCharset;
31  
32  /**
33   * JavaScript host object for {@code TextDecoder}.
34   *
35   * @author Ahmed Ashour
36   * @author Ronald Brill
37   *
38   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder">MDN Documentation</a>
39   */
40  @JsxClass
41  public class TextDecoder extends HtmlUnitScriptable {
42      private String whatwgEncoding_ = "utf-8";
43  
44      /**
45       * Creates an instance of this object.
46       *
47       * @param encodingLabel the encoding label to use; defaults to {@code "utf-8"} if not specified
48       */
49      @JsxConstructor
50      public void jsConstructor(final Object encodingLabel) {
51          if (JavaScriptEngine.isUndefined(encodingLabel)) {
52              return;
53          }
54  
55          String enc = JavaScriptEngine.toString(encodingLabel);
56          enc = enc.trim().toLowerCase(Locale.ROOT);
57          final String whatwgEncoding = StandardEncodingTranslator.ENCODING_FROM_LABEL.get(enc);
58  
59          if (whatwgEncoding == null
60                  || StandardEncodingTranslator.REPLACEMENT.equalsIgnoreCase(whatwgEncoding)) {
61              throw JavaScriptEngine.rangeError("Failed to construct 'TextDecoder': The encoding label provided ('"
62                          + encodingLabel + "') is invalid.");
63          }
64  
65          whatwgEncoding_ = whatwgEncoding;
66      }
67  
68      /**
69       * Returns the encoding label used by this decoder.
70       *
71       * @return the encoding label; defaults to {@code "utf-8"}
72       */
73      @JsxGetter
74      public String getEncoding() {
75          return whatwgEncoding_;
76      }
77  
78      /**
79       * Decodes the given buffer and returns the decoded string.
80       *
81       * @param buffer the buffer to decode
82       * @return the decoded string
83       */
84      @JsxFunction
85      public String decode(final Object buffer) {
86          if (JavaScriptEngine.isUndefined(buffer)) {
87              return "";
88          }
89  
90          if (buffer instanceof NativeArrayBuffer arrayBuffer) {
91              return new String(arrayBuffer.getBuffer(), getEncoding(whatwgEncoding_));
92          }
93  
94          if (buffer instanceof NativeArrayBufferView arrayBufferView) {
95              final NativeArrayBuffer arrayBuffer = arrayBufferView.getBuffer();
96              if (arrayBuffer != null) {
97                  final int byteLength = arrayBufferView.getByteLength();
98                  final int byteOffset = arrayBufferView.getByteOffset();
99                  final byte[] backedBytes = arrayBuffer.getBuffer();
100                 final byte[] bytes = Arrays.copyOfRange(backedBytes, byteOffset, byteOffset + byteLength);
101                 return new String(bytes, getEncoding(whatwgEncoding_));
102             }
103         }
104 
105         throw JavaScriptEngine.typeError("Argument 1 of TextDecoder.decode could not be"
106                                 + " converted to any of: ArrayBufferView, ArrayBuffer.");
107     }
108 
109     private Charset getEncoding(final String encodingLabel) {
110         if (XUserDefinedCharset.NAME.equalsIgnoreCase(encodingLabel)) {
111             return XUserDefinedCharset.INSTANCE;
112         }
113 
114         final String ianaEncoding = StandardEncodingTranslator
115                 .ENCODING_TO_IANA_ENCODING.getOrDefault(encodingLabel, encodingLabel);
116         // Convert our IANA encoding names to Java charset names
117         final String javaEncoding = StandardEncodingTranslator
118                 .IANA_TO_JAVA_ENCODINGS.getOrDefault(ianaEncoding, ianaEncoding);
119 
120         return Charset.forName(javaEncoding);
121     }
122 }