1 /**
2  * Struct that represents a MIME type.
3  * Authors:
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  * License:
6  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
7  * Copyright:
8  *  Roman Chistokhodov, 2015-2016
9  */
10 
11 module mime.type;
12 
13 import mime.common;
14 public import mime.magic;
15 public import mime.glob;
16 public import mime.treemagic;
17 
18 import std.typecons : Tuple;
19 
20 private {
21     import std.algorithm.searching : canFind;
22     import std.range;
23 }
24 
25 ///
26 alias Tuple!(string, "namespaceURI", string, "localName") XMLnamespace;
27 
28 /**
29  * Represents single MIME type.
30  */
31 final class MimeType
32 {
33     /**
34      * Create MIME type with name.
35      * Name should be given in the form of media/subtype.
36      */
37     @nogc @safe this(string name) nothrow pure {
38         _name = name;
39     }
40 
41     ///The name of MIME type.
42     @nogc @safe string name() nothrow const pure {
43         return _name;
44     }
45 
46     ///
47     unittest
48     {
49         auto mimeType = new MimeType("text/plain");
50         assert(mimeType.name == "text/plain");
51         mimeType.name = "text/xml";
52         assert(mimeType.name == "text/xml");
53     }
54 
55     ///Set MIME type name.
56     @nogc @safe string name(string typeName) nothrow pure {
57         _name = typeName;
58         return _name;
59     }
60 
61     ///Descriptive comment of MIME type.
62     @nogc @safe string displayName() nothrow const pure {
63         return _displayName;
64     }
65 
66     ///
67     unittest
68     {
69         auto mimeType = new MimeType("text/markdown");
70         mimeType.displayName = "Markdown document";
71         assert(mimeType.displayName == "Markdown document");
72     }
73 
74     ///Set descriptive comment.
75     @nogc @safe string displayName(string comment) nothrow pure {
76         _displayName = comment;
77         return _displayName;
78     }
79 
80     ///Array of MIME glob patterns applied to this MIME type.
81     @nogc @safe const(MimeGlob)[] globs() nothrow const pure {
82         return _globs;
83     }
84 
85     deprecated("Use globs") alias globs patterns;
86 
87     ///Aliases to this MIME type.
88     @nogc @safe const(string)[] aliases() nothrow const pure {
89         return _aliases;
90     }
91 
92     ///First level parents for this MIME type.
93     @nogc @safe const(string)[] parents() nothrow const pure {
94         return _parents;
95     }
96 
97     ///Get XML namespaces associated with this XML-based MIME type.
98     @nogc @safe const(XMLnamespace)[] XMLnamespaces() nothrow const pure {
99         return _XMLnamespaces;
100     }
101 
102     /**
103      * Get icon name.
104      */
105     @nogc @safe string icon() nothrow const pure {
106         return _icon;
107     }
108 
109     ///Set icon name.
110     @nogc @safe string icon(string iconName) nothrow pure {
111         _icon = iconName;
112         return _icon;
113     }
114 
115     /**
116      * Get icon name.
117      * The difference from icon property is that this function provides default icon name if no explicitly set.
118      * The default form is MIME type name with '/' replaces with '-'.
119      * Note: This function will allocate every time it's called if no icon explicitly set.
120      */
121     @safe string getIcon() nothrow const pure {
122         if (_icon.length) {
123             return _icon;
124         } else {
125             return defaultIconName(_name);
126         }
127     }
128 
129     ///
130     unittest
131     {
132         auto mimeType = new MimeType("text/mytype");
133         assert(mimeType.icon.length == 0);
134         assert(mimeType.getIcon() == "text-mytype");
135         mimeType.icon = "mytype";
136         assert(mimeType.getIcon() == "mytype");
137         assert(mimeType.icon == "mytype");
138     }
139 
140     /**
141      * Get generic icon name.
142      * Use this if the icon could not be found.
143      */
144     @nogc @safe string genericIcon() nothrow const pure {
145         return _genericIcon;
146     }
147 
148     ///Set generic icon name.
149     @nogc @safe string genericIcon(string iconName) nothrow pure {
150         _genericIcon = iconName;
151         return _genericIcon;
152     }
153 
154     /**
155      * Get generic icon name.
156      * The difference from genericIcon property is that this function provides default generic icon name if no explicitly set.
157      * The default form is media part of MIME type name with '-x-generic' appended.
158      * Note: This function will allocate every time it's called if no generic icon explicitly set.
159      */
160     @safe string getGenericIcon() nothrow const pure {
161         if (_genericIcon.length) {
162             return _genericIcon;
163         } else {
164             return defaultGenericIconName(_name);
165         }
166     }
167 
168     ///
169     unittest
170     {
171         auto mimeType = new MimeType("text/mytype");
172         assert(mimeType.genericIcon.length == 0);
173         assert(mimeType.getGenericIcon() == "text-x-generic");
174         mimeType.genericIcon = "mytype";
175         assert(mimeType.getGenericIcon() == "mytype");
176         assert(mimeType.genericIcon == "mytype");
177     }
178 
179     ///Add XML namespace.
180     @safe void addXMLnamespace(string namespaceURI, string localName) nothrow pure {
181         addXMLnamespace(XMLnamespace(namespaceURI, localName));
182     }
183 
184     ///ditto
185     @safe void addXMLnamespace(XMLnamespace namespace) nothrow pure {
186         if (!_XMLnamespaces.canFind(namespace))
187             _XMLnamespaces ~= namespace;
188     }
189 
190     ///
191     unittest
192     {
193         auto mimeType = new MimeType("text/html");
194         mimeType.addXMLnamespace("http://www.w3.org/1999/xhtml", "html");
195         assert(mimeType.XMLnamespaces == [XMLnamespace("http://www.w3.org/1999/xhtml", "html")]);
196         mimeType.clearXMLnamespaces();
197         assert(mimeType.XMLnamespaces().empty);
198     }
199 
200     /// Remove all XML namespaces.
201     @safe void clearXMLnamespaces() nothrow pure {
202         _XMLnamespaces = null;
203     }
204 
205     /**
206      * Add alias for this MIME type. Adding a duplicate does nothing.
207      */
208     @safe void addAlias(string alias_) nothrow pure {
209         if (!_aliases.canFind(alias_))
210             _aliases ~= alias_;
211     }
212 
213     ///
214     unittest
215     {
216         auto mimeType = new MimeType("text/html");
217         mimeType.addAlias("application/html");
218         mimeType.addAlias("text/x-html");
219         mimeType.addAlias("application/html");
220         assert(mimeType.aliases == ["application/html", "text/x-html"]);
221         mimeType.clearAliases();
222         assert(mimeType.aliases().empty);
223     }
224 
225     /// Remove all aliases.
226     @safe void clearAliases() nothrow pure {
227         _aliases = null;
228     }
229 
230     /**
231      * Add parent type for this MIME type. Adding a duplicate does nothing.
232      */
233     @safe void addParent(string parent) nothrow pure {
234         if (!_parents.canFind(parent))
235             _parents ~= parent;
236     }
237 
238     ///
239     unittest
240     {
241         auto mimeType = new MimeType("text/html");
242         mimeType.addParent("text/xml");
243         mimeType.addParent("text/plain");
244         mimeType.addParent("text/xml");
245         assert(mimeType.parents == ["text/xml", "text/plain"]);
246         mimeType.clearParents();
247         assert(mimeType.parents().empty);
248     }
249 
250     /// Remove all parents.
251     @safe void clearParents() nothrow pure {
252         _parents = null;
253     }
254 
255     /**
256      * Add glob pattern for this MIME type. Adding a duplicate does nothing.
257      */
258     @safe void addGlob(string pattern, uint weight = defaultGlobWeight, bool cs = false) nothrow pure {
259         addGlob(MimeGlob(pattern, weight, cs));
260     }
261     ///
262     unittest
263     {
264         auto mimeType = new MimeType("image/jpeg");
265         mimeType.addGlob("*.jpg");
266         mimeType.addGlob(MimeGlob("*.jpeg"));
267         mimeType.addGlob("*.jpg");
268         assert(mimeType.globs() == [MimeGlob("*.jpg"), MimeGlob("*.jpeg")]);
269         mimeType.clearGlobs();
270         assert(mimeType.globs().empty);
271     }
272 
273     ///ditto
274     @safe void addGlob(MimeGlob mimeGlob) nothrow pure {
275         if (!_globs.canFind(mimeGlob))
276             _globs ~= mimeGlob;
277     }
278 
279     deprecated("Use addGlob") alias addGlob addPattern;
280 
281     /// Remove all glob patterns.
282     @safe void clearGlobs() nothrow pure {
283         _globs = null;
284     }
285 
286     deprecated("Use clearGlobs") alias clearGlobs clearPatterns;
287 
288     /**
289      * Magic rules for this MIME type.
290      * Returns: Array of $(D mime.magic.MimeMagic).
291      */
292     @nogc @safe auto magics() const nothrow pure {
293         return _magics;
294     }
295 
296     /**
297      * Add magic rule.
298      */
299     @safe void addMagic(MimeMagic magic) nothrow pure {
300         _magics ~= magic;
301     }
302 
303     /**
304      * Remove all magic rules.
305      */
306     @safe void clearMagic() nothrow pure {
307         _magics = null;
308     }
309 
310     /**
311      * Treemagic rules for this MIME type.
312      */
313     @nogc @safe auto treeMagics() const nothrow pure {
314         return _treemagics;
315     }
316 
317     /**
318      * Add treemagic rule.
319      */
320     @safe void addTreeMagic(TreeMagic magic) nothrow pure {
321         _treemagics ~= magic;
322     }
323 
324     /**
325      * Remove all treemagic rules.
326      */
327     @safe void clearTreeMagic() nothrow pure {
328         _treemagics = null;
329     }
330 
331     /**
332      * Create MimeType deep copy.
333      */
334     @safe MimeType clone() nothrow const pure {
335         auto copy = new MimeType(this.name());
336         copy.icon = this.icon();
337         copy.genericIcon = this.genericIcon();
338         copy.displayName = this.displayName();
339         copy.deleteGlobs = this.deleteGlobs;
340         copy.deleteMagic = this.deleteMagic;
341 
342         foreach(namespace; this.XMLnamespaces()) {
343             copy.addXMLnamespace(namespace);
344         }
345 
346         foreach(parent; this.parents()) {
347             copy.addParent(parent);
348         }
349 
350         foreach(aliasName; this.aliases()) {
351             copy.addAlias(aliasName);
352         }
353 
354         foreach(glob; this.globs()) {
355             copy.addGlob(glob);
356         }
357 
358         foreach(magic; this.magics()) {
359             copy.addMagic(magic.clone());
360         }
361 
362         foreach(magic; this.treeMagics()) {
363             copy.addTreeMagic(magic.clone());
364         }
365 
366         return copy;
367     }
368 
369     ///
370     unittest
371     {
372         auto origin = new MimeType("text/xml");
373         origin.icon = "xml";
374         origin.genericIcon = "text";
375         origin.displayName = "XML document";
376         origin.addXMLnamespace(XMLnamespace("http://www.w3.org/1999/xhtml", "html"));
377         origin.addParent("text/plain");
378         origin.addAlias("application/xml");
379         origin.addGlob("*.xml");
380 
381         auto firstMagic = MimeMagic(50);
382         firstMagic.addMatch(MagicMatch(MagicMatch.Type.string_, [0x01, 0x02]));
383         origin.addMagic(firstMagic);
384 
385         auto secondMagic = MimeMagic(60);
386         secondMagic.addMatch(MagicMatch(MagicMatch.Type.string_, [0x03, 0x04]));
387         origin.addMagic(secondMagic);
388 
389         origin.addTreeMagic(TreeMagic(50));
390 
391         auto clone = origin.clone();
392         assert(clone.name() == origin.name());
393         assert(clone.icon() == origin.icon());
394         assert(clone.genericIcon() == origin.genericIcon());
395         assert(clone.XMLnamespaces() == origin.XMLnamespaces());
396         assert(clone.displayName() == origin.displayName());
397         assert(clone.parents() == origin.parents());
398         assert(clone.aliases() == origin.aliases());
399         assert(clone.globs() == origin.globs());
400         assert(clone.magics().length == origin.magics().length);
401 
402         clone.clearTreeMagic();
403         assert(origin.treeMagics().length == 1);
404 
405         origin.addParent("text/markup");
406         assert(origin.parents() == ["text/plain", "text/markup"]);
407         assert(clone.parents() == ["text/plain"]);
408     }
409 
410     /**
411      * Whether to discard globs parsed from a less preferable directory. Used in merges.
412      * See_Also: $(D mime.type.mergeMimeTypes)
413      */
414     @nogc @safe bool deleteGlobs() nothrow const pure
415     {
416         return _deleteGlobs;
417     }
418     ///Setter
419     @nogc @safe bool deleteGlobs(bool clearGlobs) nothrow pure
420     {
421         _deleteGlobs = clearGlobs;
422         return clearGlobs;
423     }
424 
425     /**
426      * Whether to discard magic matches parsed from a less preferable directory. Used in merges.
427      * See_Also: $(D mime.type.mergeMimeTypes)
428      */
429     @nogc @safe bool deleteMagic() nothrow const pure
430     {
431         return _deleteMagic;
432     }
433     ///Setter
434     @nogc @safe bool deleteMagic(bool clearMagic) nothrow pure
435     {
436         _deleteMagic = clearMagic;
437         return clearMagic;
438     }
439 
440 private:
441     string _name;
442     string _icon;
443     string _genericIcon;
444     string[] _aliases;
445     string[] _parents;
446     XMLnamespace[] _XMLnamespaces;
447     MimeGlob[] _globs;
448     MimeMagic[] _magics;
449     TreeMagic[] _treemagics;
450     string _displayName;
451     bool _deleteGlobs;
452     bool _deleteMagic;
453 }
454 
455 private @safe void checkNamesEqual(scope const(MimeType) origin, scope const(MimeType) additive) pure
456 {
457     import std.exception : enforce;
458     enforce(origin.name == additive.name, "Can't merge MIME types with different names");
459 }
460 
461 /**
462  * In-place version of $(D mime.type.mergeMimeTypes)
463  * See_Also: $(D mime.type.mergeMimeTypes)
464  */
465 @safe void mergeMimeTypesInPlace(MimeType origin, const(MimeType) additive) pure
466 {
467     checkNamesEqual(origin, additive);
468     if (additive.displayName.length)
469         origin.displayName = additive.displayName;
470     if (additive.icon.length)
471         origin.icon = additive.icon;
472     if (additive.genericIcon.length)
473         origin.genericIcon = additive.genericIcon;
474     if (additive.deleteGlobs)
475         origin.clearGlobs();
476     if (additive.deleteMagic)
477         origin.clearMagic();
478 
479     foreach(namespace; additive.XMLnamespaces()) {
480         origin.addXMLnamespace(namespace);
481     }
482     foreach(parent; additive.parents()) {
483         origin.addParent(parent);
484     }
485     foreach(aliasName; additive.aliases()) {
486         origin.addAlias(aliasName);
487     }
488     foreach(glob; additive.globs()) {
489         origin.addGlob(glob);
490     }
491     foreach(magic; additive.magics()) {
492         origin.addMagic(magic.clone());
493     }
494     foreach(magic; additive.treeMagics()) {
495         origin.addTreeMagic(magic.clone());
496     }
497 }
498 
499 /**
500  * Merge MIME types definitions parsed from different mime/ directories.
501  * Params:
502  *  origin = MIME type definition that was read from a less preferable mime/ directory.
503  *  additive = MIME type definition override from a more preferable mime/ directory.
504  * Throws: $(B Exception) when trying to merge MIME types with different names.
505  * See_Also: $(D mime.type.mergeMimeTypesInPlace)
506  */
507 @safe MimeType mergeMimeTypes(const(MimeType) origin, const(MimeType) additive) pure
508 {
509     checkNamesEqual(origin, additive);
510     MimeType clone = origin.clone();
511     mergeMimeTypesInPlace(clone, additive);
512     return clone;
513 }
514 
515 ///
516 unittest
517 {
518     import std..string : representation;
519     import std.exception : assertThrown;
520 
521     MimeType mimeType1 = new MimeType("text/plain");
522     MimeType mimeType2 = new MimeType("text/xml");
523 
524     assertThrown(mergeMimeTypes(mimeType1, mimeType2));
525 
526     mimeType1.name = "text/html";
527     mimeType2.name = mimeType1.name;
528 
529     mimeType1.addGlob("*.htm");
530     MimeMagic magic1;
531     magic1.addMatch(MagicMatch(MagicMatch.Type.string_, "<HTML".representation));
532     mimeType1.addMagic(magic1);
533 
534     mimeType2.addAlias("application/html");
535     mimeType2.addParent("text/xml");
536     mimeType2.addXMLnamespace("http://www.w3.org/1999/xhtml", "html");
537     mimeType2.deleteGlobs = true;
538     mimeType2.deleteMagic = true;
539     mimeType2.icon = "application-html";
540     mimeType2.addGlob("*.html");
541     MimeMagic magic2;
542     magic2.addMatch(MagicMatch(MagicMatch.Type.string_, "<html".representation));
543     mimeType2.addMagic(magic2);
544 
545     auto mimeType = mergeMimeTypes(mimeType1, mimeType2);
546     assert(mimeType.name == mimeType1.name);
547     assert(mimeType.aliases == ["application/html"]);
548     assert(mimeType.parents == ["text/xml"]);
549     assert(mimeType.XMLnamespaces == [XMLnamespace("http://www.w3.org/1999/xhtml", "html")]);
550     assert(mimeType.globs == [MimeGlob("*.html")]);
551     assert(mimeType.icon == "application-html");
552     assert(mimeType.magics.length == 1);
553     assert(mimeType.magics[0].matches[0].value == "<html");
554 }