1 /**
2  * Detecting MIME type of file using MIME cache.
3  *
4  * Authors:
5  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
6  * License:
7  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
8  * Copyright:
9  *  Roman Chistokhodov, 2016
10  */
11 
12 module mime.detectors.cache;
13 
14 import mime.detector;
15 
16 private {
17     import mime.cache;
18 
19     import std.algorithm;
20     import std.array;
21     import std.file;
22     import std.path;
23     import std.range;
24     import std.traits;
25 }
26 
27 /**
28  * Implementation of $(D mime.detector.IMimeDetector) interface using mmappable mime.cache files.
29  */
30 final class MimeDetectorFromCache : IMimeDetector
31 {
32     /**
33      * Constructor based on existing MIME cache objects.
34      * Params:
35      *  mimeCaches = Range of mime.cache.MimeCache objects sorted in order of preference from the mort preferred to the least. All must be non-null.
36      */
37     this(Range)(Range mimeCaches) if (isInputRange!Range && is(ElementType!Range : const(MimeCache)))
38     {
39         _mimeCaches = mimeCaches.array;
40     }
41 
42     /**
43      * Constructor based on MIME paths. It automatically load mime.cache files from given paths.
44      * Params:
45      *  mimePaths = Range of paths to base mime/ directories in order from more preferable to less preferable.
46      * Throws:
47      *  FileException if some existing mime.cache could not be memory mapped.
48      *  $(D mime.cache.MimeCacheException) if some existing mime.cache file is invalid.
49      * See_Also: $(D mime.paths.mimePaths)
50      */
51     this(Range)(Range mimePaths) if (isInputRange!Range && is(ElementType!Range : string))
52     {
53         foreach(mimePath; mimePaths) {
54             string path = buildPath(mimePath, "mime.cache");
55             if (path.exists) {
56                 auto mimeCache = new MimeCache(path);
57                 _mimeCaches ~= mimeCache;
58             }
59         }
60     }
61 
62     const(char)[] mimeTypeForFileName(const(char)[] fileName)
63     {
64         const(char)[] mimeType;
65         uint weight;
66         foreach(i, mimeCache; _mimeCaches) {
67             foreach(alternative; mimeCache.findMimeTypesByLiteral(fileName)) {
68                 if (shouldDiscardGlob(alternative.mimeType, _mimeCaches[0..i])) {
69                     continue;
70                 }
71                 if (mimeType.empty || alternative.weight > weight) {
72                     mimeType = alternative.mimeType;
73                     weight = alternative.weight;
74                 }
75             }
76         }
77         if (mimeType.length) {
78             return mimeType;
79         }
80 
81         size_t lastPatternLength;
82         foreach(i, mimeCache; _mimeCaches) {
83             foreach(alternative; mimeCache.findMimeTypesByGlob(fileName)) {
84                 if (shouldDiscardGlob(alternative.mimeType, _mimeCaches[0..i])) {
85                     continue;
86                 }
87                 if (mimeType.empty ||
88                     weight < alternative.weight ||
89                     (weight == alternative.weight && lastPatternLength < alternative.pattern.length))
90                 {
91                     mimeType = alternative.mimeType;
92                     weight = alternative.weight;
93                     lastPatternLength = alternative.pattern.length;
94                 }
95             }
96         }
97 
98         if (mimeType.length) {
99             return mimeType;
100         }
101 
102         size_t mimeCacheIndex;
103         void exchangeAlternative(MimeTypeAlternativeByName alternative)
104         {
105             if (shouldDiscardGlob(alternative.mimeType, _mimeCaches[0..mimeCacheIndex])) {
106                 return;
107             }
108             if (mimeType.empty ||
109                 weight < alternative.weight ||
110                 (weight == alternative.weight && lastPatternLength < alternative.pattern.length))
111             {
112                 mimeType = alternative.mimeType;
113                 weight = alternative.weight;
114                 lastPatternLength = alternative.pattern.length;
115             }
116         }
117 
118         foreach(mimeCache; _mimeCaches) {
119             mimeCache.findMimeTypesBySuffix(fileName, &exchangeAlternative);
120             mimeCacheIndex++;
121         }
122 
123         return mimeType;
124     }
125 
126     const(char[])[] mimeTypesForFileName(const(char)[] fileName)
127     {
128         const(char)[][] conflicts;
129         const(char)[] mimeType;
130         uint weight;
131 
132         foreach(i, mimeCache; _mimeCaches) {
133             foreach(alternative; mimeCache.findMimeTypesByLiteral(fileName)) {
134                 if (shouldDiscardGlob(alternative.mimeType, _mimeCaches[0..i])) {
135                     continue;
136                 }
137                 if (mimeType.empty || alternative.weight > weight) {
138                     mimeType = alternative.mimeType;
139                     weight = alternative.weight;
140                     conflicts = null;
141                 } else if (weight == alternative.weight &&
142                     mimeType != alternative.mimeType &&
143                     conflicts.find(alternative.mimeType).empty)
144                 {
145                     conflicts ~= alternative.mimeType;
146                 }
147             }
148         }
149 
150         if (mimeType.length) {
151             return mimeType ~ conflicts;
152         }
153 
154         foreach(i, mimeCache; _mimeCaches) {
155             foreach(alternative; mimeCache.findMimeTypesByGlob(fileName)) {
156                 if (shouldDiscardGlob(alternative.mimeType, _mimeCaches[0..i])) {
157                     continue;
158                 }
159                 if (mimeType.empty || weight < alternative.weight) {
160                     mimeType = alternative.mimeType;
161                     weight = alternative.weight;
162                     conflicts = null;
163                 } else if (weight == alternative.weight &&
164                     mimeType != alternative.mimeType &&
165                     conflicts.find(alternative.mimeType).empty)
166                 {
167                     conflicts ~= alternative.mimeType;
168                 }
169             }
170         }
171 
172         if (mimeType.length) {
173             return mimeType ~ conflicts;
174         }
175 
176         size_t mimeCacheIndex;
177         void exchangeAlternative(MimeTypeAlternativeByName alternative)
178         {
179             if (shouldDiscardGlob(alternative.mimeType, _mimeCaches[0..mimeCacheIndex])) {
180                 return;
181             }
182             if (mimeType.empty || weight < alternative.weight) {
183                 mimeType = alternative.mimeType;
184                 weight = alternative.weight;
185                 conflicts = null;
186             } else if (weight == alternative.weight &&
187                 mimeType != alternative.mimeType &&
188                 conflicts.find(alternative.mimeType).empty)
189             {
190                 conflicts ~= alternative.mimeType;
191             }
192         }
193 
194         foreach(mimeCache; _mimeCaches) {
195             mimeCache.findMimeTypesBySuffix(fileName, &exchangeAlternative);
196             mimeCacheIndex++;
197         }
198 
199         if (mimeType.length) {
200             return mimeType ~ conflicts;
201         }
202 
203         return null;
204     }
205 
206     private bool shouldDiscardGlob(const(char)[] mimeType, const(MimeCache)[] mimeCaches)
207     {
208         foreach(mimeCache; mimeCaches) {
209             auto range = mimeCache
210                 .findMimeTypesByLiteral("__NOGLOBS__")
211                 .map!(alternative => alternative.mimeType)
212                 .find(mimeType);
213 
214             if (!range.empty) {
215                 return true;
216             }
217         }
218         return false;
219     }
220 
221     const(char)[] mimeTypeForData(const(void)[] data)
222     {
223         const(char)[] mimeType;
224         uint weight;
225 
226         foreach(i, mimeCache; _mimeCaches) {
227             foreach(alternative; mimeCache.findMimeTypesByData(data)) {
228                 bool shouldDiscard = shouldDiscardMagic(alternative.mimeType, _mimeCaches[0..i]);
229                 if (shouldDiscard) {
230                     continue;
231                 }
232 
233                 if (mimeType.empty || alternative.weight > weight) {
234                     mimeType = alternative.mimeType;
235                     weight = alternative.weight;
236                 }
237                 break;//checking only the first is enough because matches are sorted.
238             }
239         }
240 
241         return mimeType;
242     }
243 
244     const(char[])[] mimeTypesForData(const(void)[] data)
245     {
246         const(char)[][] conflicts;
247         const(char)[] mimeType;
248         uint weight;
249 
250         foreach(i, mimeCache; _mimeCaches) {
251             foreach(alternative; mimeCache.findMimeTypesByData(data)) {
252                 bool shouldDiscard = shouldDiscardMagic(alternative.mimeType, _mimeCaches[0..i]);
253                 if (shouldDiscard) {
254                     continue;
255                 }
256 
257                 if (mimeType.empty || alternative.weight > weight) {
258                     mimeType = alternative.mimeType;
259                     weight = alternative.weight;
260                     conflicts = null;
261                 } else if (weight == alternative.weight &&
262                     mimeType != alternative.mimeType &&
263                     conflicts.find(alternative.mimeType).empty)
264                 {
265                     conflicts ~= alternative.mimeType;
266                 } else if (weight > alternative.weight) {
267                     break; //stop since there're no alternatives with equal or better weight left.
268                 }
269             }
270         }
271 
272         if (mimeType.length) {
273             return mimeType ~ conflicts;
274         }
275         return null;
276     }
277 
278     private bool shouldDiscardMagic(const(char)[] mimeType, const(MimeCache)[] mimeCaches)
279     {
280         foreach(mimeCache; mimeCaches) {
281             if (!mimeCache.magicToDelete().equalRange(mimeType).empty) {
282                 return true;
283             }
284         }
285         return false;
286     }
287 
288     const(char)[] mimeTypeForNamespaceURI(const(char)[] namespaceURI)
289     {
290         foreach(mimeCache; _mimeCaches) {
291             const(char)[] mimeType = mimeCache.findMimeTypeByNamespaceURI(namespaceURI);
292             if (mimeType.length) {
293                 return mimeType;
294             }
295         }
296         return null;
297     }
298 
299     const(char)[] resolveAlias(const(char)[] aliasName)
300     {
301         foreach(mimeCache; _mimeCaches) {
302             const(char)[] mimeType = mimeCache.resolveAlias(aliasName);
303             if (mimeType.length) {
304                 return mimeType;
305             }
306         }
307         return null;
308     }
309 
310     bool isSubclassOf(const(char)[] mimeType, const(char)[] parent)
311     {
312         foreach(mimeCache; _mimeCaches) {
313             if (mimeCache.isSubclassOf(mimeType, parent)) {
314                 return true;
315             }
316         }
317         return false;
318     }
319 
320     /**
321      * Get $(D mime.cache.MimeCache) objects.
322      * Returns: All loaded $(D mime.cache.MimeCache) objects.
323      */
324     const(MimeCache[]) mimeCaches()
325     {
326         return _mimeCaches;
327     }
328 
329 private:
330     const(MimeCache)[] _mimeCaches;
331 }