1 /*
2  * Hunt - A refined core library for D programming language.
3  *
4  * Copyright (C) 2018-2019 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.util.MimeTypeUtils;
13 
14 import hunt.util.AcceptMimeType;
15 import hunt.util.MimeType;
16 
17 import hunt.collection;
18 import hunt.logging;
19 
20 import hunt.text.Charset;
21 import hunt.Exceptions;
22 import hunt.text;
23 import hunt.util.Traits;
24 
25 import std.algorithm;
26 import std.array;
27 import std.container.array;
28 import std.conv;
29 import std.file;
30 import std.path;
31 import std.range;
32 import std.stdio;
33 import std.string;
34 import std.uni;
35 
36 
37 /**
38 */
39 class MimeTypeUtils {
40 
41     // private __gshared static ByteBuffer[string] TYPES; // = new ArrayTrie<>(512);
42     private __gshared static Map!(string, string) __dftMimeMap; 
43     private __gshared static Map!(string, string) __inferredEncodings;
44     private __gshared static Map!(string, string) __assumedEncodings;
45 
46 
47     __gshared MimeType[string] CACHE; 
48 
49 
50     shared static this() {
51         __dftMimeMap = new HashMap!(string, string)();
52         __inferredEncodings = new HashMap!(string, string)();
53         __assumedEncodings = new HashMap!(string, string)();
54 
55         foreach (MimeType type ; MimeType.values) {
56             CACHE[type.toString()] = type;
57             // TYPES[type.toString()] = type.asBuffer();
58 
59             auto charset = type.toString().indexOf(";charset=");
60             if (charset > 0) {
61                 string alt = type.toString().replace(";charset=", "; charset=");
62                 CACHE[alt] = type;
63                 // TYPES[alt] = type.asBuffer();
64             }
65 
66             if (type.isCharsetAssumed())
67                 __assumedEncodings.put(type.asString(), type.getCharsetString());
68         }
69 
70         string resourcePath = dirName(thisExePath()) ~ "/resources";
71 
72         string resourceName = buildPath(resourcePath, "mime.properties");
73         loadMimeProperties(resourceName);
74 
75         resourceName = buildPath(resourcePath, "encoding.properties");
76         loadEncodingProperties(resourceName);
77         
78     }
79 
80     private static void loadMimeProperties(string fileName) {
81         if(!exists(fileName)) {
82             version(HUNT_DEBUG) warningf("File does not exist: %s", fileName);
83             return;
84         }
85 
86         void doLoad() {
87             version(HUNT_DEBUG) tracef("loading MIME properties from: %s", fileName);
88             try {
89                 File f = File(fileName, "r");
90                 scope(exit) f.close();
91                 string line;
92                 int count = 0;
93                 while((line = f.readln()) !is null) {
94                     string[] parts = split(line, "=");
95                     if(parts.length < 2) continue;
96 
97                     count++;
98                     string key = parts[0].strip().toLower();
99                     string value = normalizeMimeType(parts[1].strip());
100                     // trace(key, " = ", value);
101                     __dftMimeMap.put(key, value);
102                 }
103 
104                 if (__dftMimeMap.size() == 0) {
105                     warningf("Empty mime types at %s", fileName);
106                 } else if (__dftMimeMap.size() < count) {
107                     warningf("Duplicate or null mime-type extension in resource: %s", fileName);
108                 }            
109             } catch(Exception ex) {
110                 warningf(ex.toString());
111             }
112         }
113 
114         doLoad();
115         // import std.parallelism;
116         // auto t = task(&doLoad);
117         // t.executeInNewThread();
118     }
119 
120     private static void loadEncodingProperties(string fileName) {
121         if(!exists(fileName)) {
122             version(HUNT_DEBUG) warningf("File does not exist: %s", fileName);
123             return;
124         }
125 
126         version(HUNT_DEBUG) tracef("loading MIME properties from: %s", fileName);
127         try {
128             File f = File(fileName, "r");
129             scope(exit) f.close();
130             string line;
131             int count = 0;
132             while((line = f.readln()) !is null) {
133                 string[] parts = split(line, "=");
134                 if(parts.length < 2) continue;
135 
136                 count++;
137                 string t = parts[0].strip();
138                 string charset = parts[1].strip();
139                 version(HUNT_DEBUG) trace(t, " = ", charset);
140                 if(charset.startsWith("-"))
141                     __assumedEncodings.put(t, charset[1..$]);
142                 else
143                     __inferredEncodings.put(t, charset);
144             }
145 
146             if (__inferredEncodings.size() == 0) {
147                 warningf("Empty encodings at %s", fileName);
148             } else if (__inferredEncodings.size() + __inferredEncodings.size() < count) {
149                 warningf("Null or duplicate encodings in resource: %s", fileName);
150             }            
151         } catch(Exception ex) {
152             warningf(ex.toString());
153         }
154     }
155 
156     /**
157      * Constructor.
158      */
159     this() {
160     }
161 
162     Map!(string, string) getMimeMap() {
163         if(_mimeMap is null)
164             _mimeMap = new HashMap!(string, string)();
165         return _mimeMap;
166     }
167 
168     private Map!(string, string) _mimeMap; 
169 
170     /**
171      * @param mimeMap A Map of file extension to mime-type.
172      */
173     void setMimeMap(Map!(string, string) mimeMap) {
174         _mimeMap.clear();
175         if (mimeMap !is null) {
176             foreach (string k, string v ; mimeMap) {
177                 _mimeMap.put(std.uni.toLower(k), normalizeMimeType(v));
178             }
179         }
180     }
181 
182     /**
183      * Get the MIME type by filename extension.
184      * Lookup only the static default mime map.
185      *
186      * @param filename A file name
187      * @return MIME type matching the longest dot extension of the
188      * file name.
189      */
190     static string getDefaultMimeByExtension(string filename) {
191         string type = null;
192 
193         if (filename != null) {
194             ptrdiff_t i = -1;
195             while (type == null) {
196                 i = filename.indexOf(".", i + 1);
197 
198                 if (i < 0 || i >= filename.length)
199                     break;
200 
201                 string ext = std.uni.toLower(filename[i + 1 .. $]);
202                 if (type == null)
203                     type = __dftMimeMap.get(ext);
204             }
205         }
206 
207         if (type == null) {
208             if (type == null)
209                 type = __dftMimeMap.get("*");
210         }
211 
212         return type;
213     }
214 
215     /**
216      * Get the MIME type by filename extension.
217      * Lookup the content and static default mime maps.
218      *
219      * @param filename A file name
220      * @return MIME type matching the longest dot extension of the
221      * file name.
222      */
223     string getMimeByExtension(string filename) {
224         string type = null;
225 
226         if (filename != null) {
227             ptrdiff_t i = -1;
228             while (type == null) {
229                 i = filename.indexOf(".", i + 1);
230 
231                 if (i < 0 || i >= filename.length)
232                     break;
233 
234                 string ext = std.uni.toLower(filename[i + 1 .. $]);
235                 if (_mimeMap !is null)
236                     type = _mimeMap.get(ext);
237                 if (type == null)
238                     type = __dftMimeMap.get(ext);
239             }
240         }
241 
242         if (type == null) {
243             if (_mimeMap !is null)
244                 type = _mimeMap.get("*");
245             if (type == null)
246                 type = __dftMimeMap.get("*");
247         }
248 
249         return type;
250     }
251 
252     /**
253      * Set a mime mapping
254      *
255      * @param extension the extension
256      * @param type      the mime type
257      */
258     void addMimeMapping(string extension, string type) {
259         _mimeMap.put(std.uni.toLower(extension), normalizeMimeType(type));
260     }
261 
262     static Set!string getKnownMimeTypes() {
263         auto hs = new HashSet!(string)();
264         foreach(v ; __dftMimeMap.byValue())
265             hs.add(v);
266         return hs;
267     }
268 
269     private static string normalizeMimeType(string type) {
270         MimeType t = CACHE.get(type, null);
271         if (t !is null)
272             return t.asString();
273 
274         return std.uni.toLower(type);
275     }
276 
277     static string getCharsetFromContentType(string value) {
278         if (value == null)
279             return null;
280         int end = cast(int)value.length;
281         int state = 0;
282         int start = 0;
283         bool quote = false;
284         int i = 0;
285         for (; i < end; i++) {
286             char b = value[i];
287 
288             if (quote && state != 10) {
289                 if ('"' == b)
290                     quote = false;
291                 continue;
292             }
293 
294             if (';' == b && state <= 8) {
295                 state = 1;
296                 continue;
297             }
298 
299             switch (state) {
300                 case 0:
301                     if ('"' == b) {
302                         quote = true;
303                         break;
304                     }
305                     break;
306 
307                 case 1:
308                     if ('c' == b) state = 2;
309                     else if (' ' != b) state = 0;
310                     break;
311                 case 2:
312                     if ('h' == b) state = 3;
313                     else state = 0;
314                     break;
315                 case 3:
316                     if ('a' == b) state = 4;
317                     else state = 0;
318                     break;
319                 case 4:
320                     if ('r' == b) state = 5;
321                     else state = 0;
322                     break;
323                 case 5:
324                     if ('s' == b) state = 6;
325                     else state = 0;
326                     break;
327                 case 6:
328                     if ('e' == b) state = 7;
329                     else state = 0;
330                     break;
331                 case 7:
332                     if ('t' == b) state = 8;
333                     else state = 0;
334                     break;
335 
336                 case 8:
337                     if ('=' == b) state = 9;
338                     else if (' ' != b) state = 0;
339                     break;
340 
341                 case 9:
342                     if (' ' == b)
343                         break;
344                     if ('"' == b) {
345                         quote = true;
346                         start = i + 1;
347                         state = 10;
348                         break;
349                     }
350                     start = i;
351                     state = 10;
352                     break;
353 
354                 case 10:
355                     if (!quote && (';' == b || ' ' == b) ||
356                             (quote && '"' == b))
357                         return StringUtils.normalizeCharset(value, start, i - start);
358                     break;
359 
360                 default: break;
361             }
362         }
363 
364         if (state == 10)
365             return StringUtils.normalizeCharset(value, start, i - start);
366 
367         return null;
368     }
369 
370     /**
371      * Access a mutable map of mime type to the charset inferred from that content type.
372      * An inferred encoding is used by when encoding/decoding a stream and is
373      * explicitly set in any metadata (eg Content-MimeType).
374      *
375      * @return Map of mime type to charset
376      */
377     static Map!(string, string) getInferredEncodings() {
378         return __inferredEncodings;
379     }
380 
381     /**
382      * Access a mutable map of mime type to the charset assumed for that content type.
383      * An assumed encoding is used by when encoding/decoding a stream, but is not
384      * explicitly set in any metadata (eg Content-MimeType).
385      *
386      * @return Map of mime type to charset
387      */
388     static Map!(string, string) getAssumedEncodings() {
389         return __inferredEncodings;
390     }
391 
392     static string getCharsetInferredFromContentType(string contentType) {
393         return __inferredEncodings.get(contentType);
394     }
395 
396     static string getCharsetAssumedFromContentType(string contentType) {
397         return __assumedEncodings.get(contentType);
398     }
399 
400     static string getContentTypeWithoutCharset(string value) {
401         int end = cast(int)value.length;
402         int state = 0;
403         int start = 0;
404         bool quote = false;
405         int i = 0;
406         StringBuilder builder = null;
407         for (; i < end; i++) {
408             char b = value[i];
409 
410             if ('"' == b) {
411                 quote = !quote;
412 
413                 switch (state) {
414                     case 11:
415                         builder.append(b);
416                         break;
417                     case 10:
418                         break;
419                     case 9:
420                         builder = new StringBuilder();
421                         builder.append(value, 0, start + 1);
422                         state = 10;
423                         break;
424                     default:
425                         start = i;
426                         state = 0;
427                 }
428                 continue;
429             }
430 
431             if (quote) {
432                 if (builder !is null && state != 10)
433                     builder.append(b);
434                 continue;
435             }
436 
437             switch (state) {
438                 case 0:
439                     if (';' == b)
440                         state = 1;
441                     else if (' ' != b)
442                         start = i;
443                     break;
444 
445                 case 1:
446                     if ('c' == b) state = 2;
447                     else if (' ' != b) state = 0;
448                     break;
449                 case 2:
450                     if ('h' == b) state = 3;
451                     else state = 0;
452                     break;
453                 case 3:
454                     if ('a' == b) state = 4;
455                     else state = 0;
456                     break;
457                 case 4:
458                     if ('r' == b) state = 5;
459                     else state = 0;
460                     break;
461                 case 5:
462                     if ('s' == b) state = 6;
463                     else state = 0;
464                     break;
465                 case 6:
466                     if ('e' == b) state = 7;
467                     else state = 0;
468                     break;
469                 case 7:
470                     if ('t' == b) state = 8;
471                     else state = 0;
472                     break;
473                 case 8:
474                     if ('=' == b) state = 9;
475                     else if (' ' != b) state = 0;
476                     break;
477 
478                 case 9:
479                     if (' ' == b)
480                         break;
481                     builder = new StringBuilder();
482                     builder.append(value, 0, start + 1);
483                     state = 10;
484                     break;
485 
486                 case 10:
487                     if (';' == b) {
488                         builder.append(b);
489                         state = 11;
490                     }
491                     break;
492 
493                 case 11:
494                     if (' ' != b)
495                         builder.append(b);
496                     break;
497                 
498                 default: break;
499             }
500         }
501         if (builder is null)
502             return value;
503         return builder.toString();
504 
505     }
506 
507     static string getContentTypeMIMEType(string contentType) {
508         if (contentType.empty) 
509             return null;
510 
511         // parsing content-type
512         string[] strings = StringUtils.split(contentType, ";");
513         return strings[0];
514     }
515 
516     static List!string getAcceptMIMETypes(string accept) {
517         if(accept.empty) 
518             new EmptyList!string(); // Collections.emptyList();
519 
520         List!string list = new ArrayList!string();
521         // parsing accept
522         string[] strings = StringUtils.split(accept, ",");
523         foreach (string str ; strings) {
524             string[] s = StringUtils.split(str, ";");
525             list.add(s[0].strip());
526         }
527         return list;
528     }
529 
530     static AcceptMimeType[] parseAcceptMIMETypes(string accept) {
531 
532         if(accept.empty) 
533             return [];
534 
535         string[] arr = StringUtils.split(accept, ",");
536         return apply(arr);
537     }
538 
539     private static AcceptMimeType[] apply(string[] stream) {
540 
541         Array!AcceptMimeType arr;
542 
543         foreach(string s; stream) {
544             string type = strip(s);
545             if(type.empty) continue;
546             string[] mimeTypeAndQuality = StringUtils.split(type, ';');
547             AcceptMimeType acceptMIMEType = new AcceptMimeType();
548             
549             // parse the MIME type
550             string[] mimeType = StringUtils.split(mimeTypeAndQuality[0].strip(), '/');
551             string parentType = mimeType[0].strip();
552             string childType = mimeType[1].strip();
553             acceptMIMEType.setParentType(parentType);
554             acceptMIMEType.setChildType(childType);
555             if (parentType == "*") {
556                 if (childType == "*") {
557                     acceptMIMEType.setMatchType(AcceptMimeMatchType.ALL);
558                 } else {
559                     acceptMIMEType.setMatchType(AcceptMimeMatchType.CHILD);
560                 }
561             } else {
562                 if (childType == "*") {
563                     acceptMIMEType.setMatchType(AcceptMimeMatchType.PARENT);
564                 } else {
565                     acceptMIMEType.setMatchType(AcceptMimeMatchType.EXACT);
566                 }
567             }
568 
569             // parse the quality
570             if (mimeTypeAndQuality.length > 1) {
571                 string q = mimeTypeAndQuality[1];
572                 string[] qualityKV = StringUtils.split(q, '=');
573                 acceptMIMEType.setQuality(to!float(qualityKV[1].strip()));
574             }
575             arr.insertBack(acceptMIMEType);
576         }
577 
578         for(size_t i=0; i<arr.length-1; i++) {
579             for(size_t j=i+1; j<arr.length; j++) {
580                 AcceptMimeType a = arr[i];
581                 AcceptMimeType b = arr[j];
582                 if(b.getQuality() > a.getQuality()) {   // The greater quality is first.
583                     arr[i] = b; arr[j] = a;
584                 }
585             }
586         }
587 
588         return arr.array();
589     }
590 }