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.subtypexml;
13 
14 import mime.common;
15 import mime.store;
16 
17 private {
18     import std.algorithm.iteration : filter, map, joiner;
19     import std.array;
20     import std.exception : assumeUnique, collectException;
21     import std.file : isFile;
22     import std.path : buildPath;
23     import std.range : retro;
24     import std.range.interfaces : inputRangeObject;
25     import std.stdio : File;
26     import std.typecons;
27 
28     import mime.files.types;
29     import mime.xml : readMediaSubtypeFile;
30 }
31 
32 public import mime.files.common;
33 
34 /**
35  * Implementation of $(D mime.store.IMimeStore) interface that uses MEDIA/SUBTYPE.xml files from mime/ subfolder to read MIME types.
36  * It does not read any MIME type definitions at the construction time.
37  * Instead MediaSubtypeXmlStore performs parsing of separate files on demand when calling $(D mimeType) or $(D byMimeType).
38  * All parsed definitions are getting cached to avoid re-parsing on every demand.
39  * See_Also: $(D mime.xml.readMediaSubtypeFile)
40  */
41 final class MediaSubtypeXmlStore : IMimeStore
42 {
43     /**
44      * Params:
45      *  mimePaths = Range of paths to base mime/ directories in order from more preferable to less preferable.
46      */
47     @safe this(const(string)[] mimePaths) nothrow pure
48     {
49         _mimePaths = mimePaths.dup;
50     }
51 
52     /**
53      * Find and parse MEDIA/SUBTYPE.xml file(s) for given MIME type name.
54      * If it finds more then one file for the MIME type, merging operation is performed.
55      * Returns: $(D mime.type.MimeType) object parsed from found xml file(s) or null if no file was found or name is invalid.
56      * Throws: $(D mime.xml.XMLMimeException) on format error or $(B std.file.FileException) on file reading error.
57      * See_Also: $(D mime.type.mergeMimeTypes)
58      */
59     Rebindable!(const(MimeType)) mimeType(const char[] name)
60     {
61         return rebindable(mimeTypeImpl(name));
62     }
63 
64     private const(MimeType) mimeTypeImpl(const char[] name)
65     {
66         if (!isValidMimeTypeName(name))
67             return null;
68         MimeType* pmimeType = name in _mimeTypes;
69         if (pmimeType)
70             return *pmimeType;
71         foreach(mimePath; _mimePaths.retro)
72         {
73             auto subtypePath = buildPath(mimePath, assumeUnique(name ~ ".xml"));
74             bool isFile;
75             collectException(subtypePath.isFile, isFile);
76             if (isFile)
77             {
78                 auto mimeType = readMediaSubtypeFile(subtypePath);
79                 pmimeType = name in _mimeTypes;
80                 if (pmimeType)
81                 {
82                     mergeMimeTypesInPlace(*pmimeType, mimeType);
83                 }
84                 else
85                 {
86                     addIconNames(mimeType);
87                     _mimeTypes[mimeType.name] = mimeType;
88                 }
89             }
90         }
91         pmimeType = name in _mimeTypes;
92         if (pmimeType)
93             return *pmimeType;
94         return null;
95     }
96 
97     /**
98      * Lazily read MIME types objects. The list of MIME types is read from mime/types file, so it must be present.
99      * Returns: Range of $(D mime.type.MimeType) objects.
100      * Throws:
101      *  $(D mime.files.common.MimeFileException) on mime/types file parsing error.
102      *  $(D mime.xml.XMLMimeException) on xml format error.
103      *  $(B std.file.FileException) on file reading error.
104      * Note: The resulted range may contain duplicates, if some MIME type has multiple definitions across base mime paths.
105      *  The duplicates in this case refer to the same object, i.e. $(B is)-equal.
106      */
107     InputRange!(const(MimeType)) byMimeType() {
108         auto typesPaths = _mimePaths.retro.map!(mimePath => buildPath(mimePath, "types")).filter!(delegate(string typesPath) {
109             bool isFile;
110             collectException(typesPath.isFile, isFile);
111             return isFile;
112         });
113 
114         auto typeNames = typesPaths.map!(typesPath => typesFileReader(File(typesPath, "r").byLineCopy())).joiner;
115         auto mimeTypes = typeNames.map!(type => mimeTypeImpl(type)).filter!(mimeType => mimeType !is null);
116         return inputRangeObject(mimeTypes);
117     }
118 
119 private:
120     @safe void addIconNames(MimeType mimeType)
121     {
122         if (!mimeType.icon)
123             mimeType.icon = defaultIconName(mimeType.name);
124         if (!mimeType.genericIcon)
125             mimeType.genericIcon = defaultGenericIconName(mimeType.name);
126     }
127 
128     string[] _mimePaths;
129     MimeType[const(char)[]] _mimeTypes;
130 }
131 
132 unittest
133 {
134     auto mimePaths = ["./test/mime", "./test/discard", "./test/nonexistent"];
135     auto store = new MediaSubtypeXmlStore(mimePaths);
136 
137     assert(store.mimeType("invalid") is null);
138     assert(store.mimeType("application/nonexistent") is null);
139 
140     auto sequenceType = store.mimeType("application/x-hlmdl-sequence");
141     assert(sequenceType !is null);
142     assert(sequenceType.globs == [MimeGlob("*[0123456789][0123456789].mdl", defaultGlobWeight, false)]);
143     assert(sequenceType.genericIcon == "application-x-hlmdl");
144     assert(sequenceType is store.mimeType("application/x-hlmdl-sequence"));
145 
146     auto quakeSprite = store.mimeType("image/x-qsprite");
147     assert(quakeSprite !is null);
148     assert(quakeSprite.aliases == ["application/x-qsprite"]);
149 
150     import std.algorithm.searching : canFind;
151     assert(store.byMimeType.canFind!((const(MimeType) type, string name) { return type.name == name; })("application/x-pak"));
152 }