1 /**
2  * MIME store implemented around reading of various files in mime/ subfolder.
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, 2015-2016
10  */
11 
12 module mime.stores.files;
13 
14 import mime.common;
15 import mime.store;
16 
17 private {
18     import std.algorithm : map;
19     import std.array : empty;
20     import std.exception;
21     import std.file : isDir, FileException;
22     import std.mmfile;
23     import std.path;
24     import std.range : retro;
25     import std.range.interfaces : inputRangeObject;
26     import std.range.primitives : isInputRange, ElementType;
27     import std.stdio;
28     import std.typecons;
29 
30     import mime.files.aliases;
31     import mime.files.globs;
32     import mime.files.icons;
33     import mime.files.magic;
34     import mime.files.namespaces;
35     import mime.files.subclasses;
36     import mime.files.types;
37     import mime.files.treemagic;
38 }
39 
40 public import mime.files.common;
41 
42 private @trusted auto fileReader(string fileName) {
43     return File(fileName, "r").byLineCopy();
44 }
45 
46 /**
47  * Implementation of $(D mime.store.IMimeStore) interface that uses various files from mime/ subfolder to read MIME types.
48  */
49 final class FilesMimeStore : IMimeStore
50 {
51     ///
52     alias Tuple!(string, "fileName", Exception, "e") FileError;
53 
54     /**
55      * Options to use when reading various shared MIME-info database files.
56      */
57     struct Options
58     {
59         enum : ubyte {
60             skip = 0, ///Don't try to read file.
61             read = 1, ///Try to read file. Give up on any error without throwing it.
62             throwReadError = 2, ///Throw on file reading error.
63             throwParseError = 4, ///Throw on file parsing error.
64             saveErrors = 8, ///Save non-thrown errors to retrieve later via errors method.
65 
66             optional = read | throwParseError,   ///Read file if it's readable. Throw only on malformed contents.
67             required = read | throwReadError | throwParseError,   ///Always try to read file, throw on any error.
68             allowFail = read,  ///Don't throw if file can't be read or has invalid contents.
69         }
70 
71         ubyte types = optional;        ///Options for reading types file.
72         ubyte aliases = optional;      ///Options for reading aliases file.
73         ubyte subclasses = optional;   ///Options for reading subclasses file.
74         ubyte icons = optional;        ///Options for reading icons file.
75         ubyte genericIcons = optional; ///Options for reading generic-icons file.
76         ubyte XMLnamespaces = optional;///Options for reading XMLnamespaces file.
77         ubyte globs2 = optional;       ///Options for reading globs2 file.
78         ubyte globs = optional;        ///Options for reading globs file. Used only if globs2 file could not be read.
79         ubyte magic = skip;            ///Options for reading magic file.
80         ubyte treemagic = skip;        ///Options for reading treemagic file.
81     }
82 
83     private void handleError(Exception e, ubyte option, string fileName)
84     {
85         bool known;
86         if (cast(MimeFileException)e !is null ||
87             cast(MimeMagicFileException)e !is null ||
88             cast(TreeMagicFileException)e !is null)
89         {
90             if (option & Options.throwParseError) {
91                 throw e;
92             }
93             known = true;
94         }
95 
96         if (cast(ErrnoException)e !is null ||
97             cast(FileException)e !is null)
98         {
99             if (option & Options.throwReadError) {
100                 throw e;
101             }
102             known = true;
103         }
104 
105         if (!known) {
106             throw e;
107         }
108 
109         if (option & Options.saveErrors) {
110             _errors ~= FileError(fileName, e);
111         }
112     }
113 
114     /**
115      * Constructor based on MIME paths.
116      * Params:
117      *  mimePaths = Range of paths to base mime/ directories in order from more preferable to less preferable.
118      *  options = Options for file reading and error reporting.
119      * Throws:
120      *  $(D mime.files.common.MimeFileException) if some info file has errors.
121      *  $(D mime.files.magic.MimeMagicFileException) if magic file has errors.
122      *  $(D mime.files.treemagic.TreeMagicFileException) if treemagic file has errors.
123      *  $(B ErrnoException) or $(B FileException) if some important file does not exist or could not be read.
124      * See_Also: $(D mime.paths.mimePaths)
125      */
126     this(Range)(Range mimePaths, Options options = Options.init) if (isInputRange!Range && is(ElementType!Range : string))
127     {
128         foreach(mimePath; mimePaths.retro) {
129             bool dirExists;
130             collectException(mimePath.isDir, dirExists);
131             if (!dirExists) {
132                 continue;
133             }
134 
135             if (options.types & Options.read) {
136                 auto typesPath = buildPath(mimePath, "types");
137                 try {
138                     foreach(line; typesFileReader(fileReader(typesPath))) {
139                         ensureMimeType(line);
140                     }
141                 } catch(Exception e) {
142                     handleError(e, options.types, typesPath);
143                 }
144             }
145 
146             if (options.aliases & Options.read) {
147                 auto aliasesPath = buildPath(mimePath, "aliases");
148                 try {
149                     auto aliases = aliasesFileReader(fileReader(aliasesPath));
150                     foreach(aliasLine; aliases) {
151                         auto mimeType = ensureMimeType(aliasLine.mimeType);
152                         mimeType.addAlias(aliasLine.aliasName);
153                     }
154                 } catch(Exception e) {
155                     handleError(e, options.aliases, aliasesPath);
156                 }
157             }
158 
159             if (options.subclasses & Options.read) {
160                 auto subclassesPath = buildPath(mimePath, "subclasses");
161                 try {
162                     auto subclasses = subclassesFileReader(fileReader(subclassesPath));
163                     foreach(subclassLine; subclasses) {
164                         auto mimeType = ensureMimeType(subclassLine.mimeType);
165                         mimeType.addParent(subclassLine.parent);
166                     }
167                 } catch(Exception e) {
168                     handleError(e, options.subclasses, subclassesPath);
169                 }
170             }
171 
172             if (options.icons & Options.read) {
173                 auto iconsPath = buildPath(mimePath, "icons");
174                 try {
175                     auto icons = iconsFileReader(fileReader(iconsPath));
176                     foreach(iconLine; icons) {
177                         auto mimeType = ensureMimeType(iconLine.mimeType);
178                         mimeType.icon = iconLine.iconName;
179                     }
180                 } catch(Exception e) {
181                     handleError(e, options.icons, iconsPath);
182                 }
183             }
184 
185             if (options.genericIcons & Options.read) {
186                 auto genericIconsPath = buildPath(mimePath, "generic-icons");
187                 try {
188                     auto icons = iconsFileReader(fileReader(genericIconsPath));
189                     foreach(iconLine; icons) {
190                         auto mimeType = ensureMimeType(iconLine.mimeType);
191                         mimeType.genericIcon = iconLine.iconName;
192                     }
193                 } catch(Exception e) {
194                     handleError(e, options.genericIcons, genericIconsPath);
195                 }
196             }
197 
198             if (options.XMLnamespaces & Options.read) {
199                 auto namespacesPath = buildPath(mimePath, "XMLnamespaces");
200                 try {
201                     auto namespaces = namespacesFileReader(fileReader(namespacesPath));
202                     foreach(namespaceLine; namespaces) {
203                         auto mimeType = ensureMimeType(namespaceLine.mimeType);
204                         mimeType.addXMLnamespace(namespaceLine.namespaceUri, namespaceLine.localName);
205                     }
206                 } catch(Exception e) {
207                     handleError(e, options.XMLnamespaces, namespacesPath);
208                 }
209             }
210 
211             bool shouldReadGlobs = false;
212             if (options.globs2 & Options.read) {
213                 auto globs2Path = buildPath(mimePath, "globs2");
214                 try {
215                     setGlobs(globs2FileReader(fileReader(globs2Path)));
216                 } catch(Exception e) {
217                     handleError(e, options.globs2, globs2Path);
218                     shouldReadGlobs = true;
219                 }
220             } else {
221                 shouldReadGlobs = true;
222             }
223 
224             if (shouldReadGlobs && (options.globs & Options.read)) {
225                 auto globsPath = buildPath(mimePath, "globs");
226                 try {
227                     setGlobs(globsFileReader(fileReader(globsPath)));
228                 } catch(Exception e) {
229                     handleError(e, options.globs, globsPath);
230                 }
231             }
232 
233             if (options.magic & Options.read) {
234                 auto magicPath = buildPath(mimePath, "magic");
235                 try {
236                     void sink(MagicEntry t) {
237                         auto mimeType = ensureMimeType(t.mimeType);
238                         if (t.deleteMagic) {
239                             mimeType.clearMagic();
240                         }
241                         if (!t.magic.matches.empty) {
242                             mimeType.addMagic(t.magic);
243                         }
244                     }
245                     auto mmFile = new MmFile(magicPath);
246                     magicFileReader(mmFile[], &sink);
247                 } catch(Exception e) {
248                     handleError(e, options.magic, magicPath);
249                 }
250             }
251 
252             if (options.treemagic & Options.read) {
253                 auto treemagicPath = buildPath(mimePath, "treemagic");
254                 try {
255                     void treeSink(TreeMagicEntry t) {
256                         auto mimeType = ensureMimeType(t.mimeType);
257                         mimeType.addTreeMagic(t.magic);
258                     }
259                     auto mmFile = new MmFile(treemagicPath);
260                     treeMagicFileReader(mmFile[], &treeSink);
261                 } catch(Exception e) {
262                     handleError(e, options.treemagic, treemagicPath);
263                 }
264             }
265         }
266     }
267 
268     unittest
269     {
270         auto mimePaths = ["test/errors"];
271         const skipAll = Options(0,0,0,0,0,0,0,0,0,0);
272 
273         void fileTest(string name, T = MimeFileException)(ubyte opt = Options.required) {
274             Options options = skipAll;
275             mixin("options." ~ name ~ " = opt;");
276             assertThrown!T(new FilesMimeStore(mimePaths, options));
277         }
278 
279         fileTest!("types");
280         fileTest!("aliases");
281         fileTest!("subclasses");
282         fileTest!("genericIcons");
283         fileTest!("icons");
284         fileTest!("XMLnamespaces");
285         fileTest!("globs");
286         fileTest!("globs2", ErrnoException);
287 
288         Options magic = skipAll;
289         magic.magic = Options.required;
290         assertThrown!MimeMagicFileException(new FilesMimeStore(mimePaths, magic));
291 
292         Options treemagic = skipAll;
293         treemagic.treemagic = Options.required;
294         assertThrown!TreeMagicFileException(new FilesMimeStore(mimePaths, treemagic));
295 
296         const opt = Options.allowFail | Options.saveErrors;
297         const all = Options(opt, opt, opt, opt, opt, opt, opt, opt, opt, opt);
298         auto store = new FilesMimeStore(mimePaths, all);
299         assert(store.errors().length == 10);
300     }
301 
302     ///
303     InputRange!(const(MimeType)) byMimeType() {
304         return inputRangeObject(_mimeTypes.byValue().map!(val => cast(const(MimeType))val));
305     }
306 
307     ///
308     Rebindable!(const(MimeType)) mimeType(const char[] name) {
309         return rebindable(mimeTypeImpl(name));
310     }
311 
312     private final const(MimeType) mimeTypeImpl(const char[] name) {
313         MimeType* pmimeType = name in _mimeTypes;
314         if (pmimeType) {
315             return *pmimeType;
316         } else {
317             return null;
318         }
319     }
320 
321     /**
322      * Get errors that were told to not throw but to be saved during parsing.
323      */
324     const(FileError)[] errors() const {
325         return _errors;
326     }
327 
328 private:
329     @trusted MimeType ensureMimeType(const(char)[] name) {
330         MimeType* pmimeType = name in _mimeTypes;
331         if (pmimeType) {
332             return *pmimeType;
333         } else {
334             string mimeName = name.idup;
335             auto mimeType = new MimeType(mimeName);
336             mimeType.icon = defaultIconName(mimeName);
337             mimeType.genericIcon = defaultGenericIconName(mimeName);
338             _mimeTypes[mimeName] = mimeType;
339             return mimeType;
340         }
341     }
342 
343     @trusted void setGlobs(Range)(Range globs) {
344         foreach(globLine; globs) {
345             if (!globLine.pattern.length) {
346                 continue;
347             }
348             auto mimeType = ensureMimeType(globLine.mimeType);
349 
350             if (globLine.pattern.isNoGlobs()) {
351                 mimeType.clearGlobs();
352             } else {
353                 mimeType.addGlob(globLine.pattern, globLine.weight, globLine.caseSensitive);
354             }
355         }
356     }
357 
358     MimeType[const(char)[]] _mimeTypes;
359     FileError[] _errors;
360 }