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.util;
16  
17  import java.util.Locale;
18  
19  import org.htmlunit.cyberneko.util.FastHashMap;
20  
21  /**
22   * Utility class holding information about the association between MIME types and file extensions.
23   *
24   * @author Marc Guillemot
25   * @author Ronald Brill
26   */
27  public final class MimeType {
28  
29      /** {@code "text/javascript"}. */
30      public static final String TEXT_JAVASCRIPT = "text/javascript";
31      /** {@code "application/octet-stream"}. */
32      public static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
33      /** {@code "application/json"}. */
34      public static final String APPLICATION_JSON = "application/json";
35      /** {@code "application/xhtml+xml"}. */
36      public static final String APPLICATION_XHTML = "application/xhtml+xml";
37      /** {@code "application/xml"}. */
38      public static final String APPLICATION_XML = "application/xml";
39  
40      /** {@code "text/css"}. */
41      public static final String TEXT_CSS = "text/css";
42      /** {@code "text/html"}. */
43      public static final String TEXT_HTML = "text/html";
44      /** {@code "text/xml"}. */
45      public static final String TEXT_XML = "text/xml";
46      /** {@code "text/plain"}. */
47      public static final String TEXT_PLAIN = "text/plain";
48  
49      /** {@code "image/gif"}. */
50      public static final String IMAGE_GIF = "image/gif";
51      /** {@code "image/jpeg"}. */
52      public static final String IMAGE_JPEG = "image/jpeg";
53      /** {@code "image/png"}. */
54      public static final String IMAGE_PNG = "image/png";
55  
56      private static final FastHashMap<String, String> TYPE2EXTENSION = buildMap();
57  
58      /**
59       * A map to avoid lowercase conversion and a check if this is one of
60       * our known MIME types. The value is not used.
61       */
62      private static final FastHashMap<String, Boolean> LOOKUP_MAP = new FastHashMap<>(2 * 16 + 1, 0.7f);
63  
64      static {
65          LOOKUP_MAP.put("application/javascript", true);
66          LOOKUP_MAP.put("application/x-ecmascript", true);
67          LOOKUP_MAP.put("application/x-javascript", true);
68          LOOKUP_MAP.put("text/ecmascript", true);
69          LOOKUP_MAP.put("application/ecmascript", true);
70          LOOKUP_MAP.put("text/javascript1.0", true);
71          LOOKUP_MAP.put("text/javascript1.1", true);
72          LOOKUP_MAP.put("text/javascript1.2", true);
73          LOOKUP_MAP.put("text/javascript1.3", true);
74          LOOKUP_MAP.put("text/javascript1.4", true);
75          LOOKUP_MAP.put("text/javascript1.5", true);
76          LOOKUP_MAP.put("text/jscript", true);
77          LOOKUP_MAP.put("text/livescript", true);
78          LOOKUP_MAP.put("text/x-ecmascript", true);
79          LOOKUP_MAP.put("text/x-javascript", true);
80  
81          // have uppercase ready too, keys() is safe for
82          // concurrent modification
83          for (final String k : LOOKUP_MAP.keys()) {
84              LOOKUP_MAP.put(k.toUpperCase(Locale.ROOT), true);
85          }
86      }
87  
88      /**
89       * Returns whether the given MIME type is a valid JavaScript MIME type according to
90       * <a href="https://www.rfc-editor.org/rfc/rfc9239.html#name-iana-considerations">RFC 9239</a>.
91       *
92       * @param mimeType the type to check
93       * @return {@code true} if the MIME type is a JavaScript MIME type
94       */
95      public static boolean isJavascriptMimeType(final String mimeType) {
96          if (mimeType == null) {
97              return false;
98          }
99          final String mimeTypeLC = StringUtils.toRootLowerCase(mimeType);
100 
101         return TEXT_JAVASCRIPT.equals(mimeTypeLC)
102                 || "application/javascript".equals(mimeTypeLC)
103                 || "application/x-ecmascript".equals(mimeTypeLC)
104                 || "application/x-javascript".equals(mimeTypeLC)
105                 || "text/ecmascript".equals(mimeTypeLC)
106                 || "application/ecmascript".equals(mimeTypeLC)
107                 || "text/javascript1.0".equals(mimeTypeLC)
108                 || "text/javascript1.1".equals(mimeTypeLC)
109                 || "text/javascript1.2".equals(mimeTypeLC)
110                 || "text/javascript1.3".equals(mimeTypeLC)
111                 || "text/javascript1.4".equals(mimeTypeLC)
112                 || "text/javascript1.5".equals(mimeTypeLC)
113                 || "text/jscript".equals(mimeTypeLC)
114                 || "text/livescript".equals(mimeTypeLC)
115                 || "text/x-ecmascript".equals(mimeTypeLC)
116                 || "text/x-javascript".equals(mimeTypeLC);
117     }
118 
119     /**
120      * Returns whether the given MIME type is an obsolete JavaScript MIME type according to
121      * <a href="https://mimesniff.spec.whatwg.org/#javascript-mime-type">MIME Sniffing</a>.
122      *
123      * @param mimeType the type to check
124      * @return {@code true} if the MIME type is an obsolete JavaScript MIME type
125      */
126     public static boolean isObsoleteJavascriptMimeType(final String mimeType) {
127         if (mimeType == null) {
128             return false;
129         }
130 
131         // go a cheap route first
132         if (LOOKUP_MAP.get(mimeType) != null) {
133             return true;
134         }
135 
136         // this is our fallback in case we have not found the usual casing
137         // our target is ASCII, we can lowercase the normal way because
138         // matching some languages with strange rules does not matter, cheaper!
139         final String mimeTypeLC = mimeType.toLowerCase(Locale.ROOT);
140 
141         return "application/javascript".equals(mimeTypeLC)
142                 || "application/ecmascript".equals(mimeTypeLC)
143                 || "application/x-ecmascript".equals(mimeTypeLC)
144                 || "application/x-javascript".equals(mimeTypeLC)
145                 || "text/ecmascript".equals(mimeTypeLC)
146                 || "text/javascript1.0".equals(mimeTypeLC)
147                 || "text/javascript1.1".equals(mimeTypeLC)
148                 || "text/javascript1.2".equals(mimeTypeLC)
149                 || "text/javascript1.3".equals(mimeTypeLC)
150                 || "text/javascript1.4".equals(mimeTypeLC)
151                 || "text/javascript1.5".equals(mimeTypeLC)
152                 || "text/jscript".equals(mimeTypeLC)
153                 || "text/livescript".equals(mimeTypeLC)
154                 || "text/x-ecmascript".equals(mimeTypeLC)
155                 || "text/x-javascript".equals(mimeTypeLC);
156     }
157 
158     private static FastHashMap<String, String> buildMap() {
159         final FastHashMap<String, String> map = new FastHashMap<>(2 * 11 + 1, 0.7f);
160         map.put("application/pdf", "pdf");
161         map.put("application/x-javascript", "js");
162         map.put("image/gif", "gif");
163         map.put("image/jpg", "jpeg");
164         map.put("image/jpeg", "jpeg");
165         map.put("image/png", "png");
166         map.put("image/svg+xml", "svg");
167         map.put(TEXT_CSS, "css");
168         map.put(MimeType.TEXT_HTML, "html");
169         map.put(TEXT_PLAIN, "txt");
170         map.put("image/x-icon", "ico");
171 
172         // have uppercase ready too, keys() is safe for
173         // concurrent modification
174         for (final String k : map.keys()) {
175             map.put(k.toUpperCase(Locale.ROOT), map.get(k));
176         }
177         return map;
178     }
179 
180     /**
181      * Disallow instantiation of this class.
182      */
183     private MimeType() {
184         // Empty.
185     }
186 
187     /**
188      * Returns the preferred file extension for the given content type,
189      * or {@code "unknown"} if none is known.
190      *
191      * @param contentType the MIME type
192      * @return the file extension, or {@code "unknown"} if not recognized
193      */
194     public static String getFileExtension(final String contentType) {
195         if (contentType == null) {
196             return "unknown";
197         }
198 
199         String value = TYPE2EXTENSION.get(contentType);
200         if (value == null) {
201             // fallback
202             final String uppercased = contentType.toLowerCase(Locale.ROOT);
203             value = TYPE2EXTENSION.get(uppercased);
204         }
205 
206         return value == null ? "unknown" : value;
207     }
208 }