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 }