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 }