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