1 /** 2 * Access to Shared MIME-info database. 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, 2016 9 * See_Also: $(LINK2 https://www.freedesktop.org/wiki/Specifications/shared-mime-info-spec/, Shared MIME database specification) 10 */ 11 12 module mime.database; 13 14 import std.range; 15 import std.typecons; 16 17 import mime.store; 18 import mime.detector; 19 import mime.text; 20 import mime.inode; 21 22 import mime.common : dataSizeToRead; 23 public import mime.type; 24 25 /** 26 * High-level class for accessing Shared MIME-info database. 27 */ 28 final class MimeDatabase 29 { 30 /// Options for $(D mimeTypeForFile) 31 enum Match 32 { 33 globPatterns = 1, /// Match file name against glob patterns. 34 magicRules = 2, /// Match file content against magic rules. With MatchOptions.globPatterns flag it's used only in conflicts. 35 namespaceURI = 4, /// Try to clarify mime type in case it's XML. 36 inodeType = 8, /// Provide inode/* type for files other than regular files. 37 textFallback = 16, /// Provide $(D text/plain) fallback if data seems to be textual. 38 octetStreamFallback = 32, /// Provide $(D application/octet-stream) fallback if data seems to be binary. 39 emptyFileFallback = 64, ///Provide $(B application/x-zerosize) fallback if mime type can't be detected, but data is known to be zero size. 40 all = globPatterns|magicRules|namespaceURI|inodeType|textFallback|octetStreamFallback|emptyFileFallback ///Use all recipes to detect MIME type. 41 } 42 43 /** 44 * Constructor based on MIME paths. 45 * It uses $(D mime.detectors.cache.MimeDetectorFromCache) as MIME type detector and $(D mime.stores.files.FilesMimeStore) as MIME type store. 46 * Params: 47 * mimePaths = Range of paths to base mime directories where needed files will be read. 48 * See_Also: $(D mime.paths.mimePaths), $(D mime.detectors.cache.MimeDetectorFromCache), $(D mime.stores.files.FilesMimeStore) 49 */ 50 this(Range)(Range mimePaths) if (isInputRange!Range && is(ElementType!Range : string)) 51 { 52 import mime.stores.subtypexml; 53 import mime.detectors.cache; 54 55 _store = new MediaSubtypeXmlStore(mimePaths); 56 _detector = new MimeDetectorFromCache(mimePaths); 57 } 58 59 /** 60 * Create MimeDatabase object with given store and detector. 61 */ 62 @safe this(IMimeStore store, IMimeDetector detector) 63 { 64 _store = store; 65 _detector = detector; 66 } 67 68 /** 69 * Get MIME types store used in MimeDatabase instance. 70 */ 71 IMimeStore store() 72 { 73 return _store; 74 } 75 76 /** 77 * Get MIME type detector used in MimeDatabase instance. 78 */ 79 IMimeDetector detector() 80 { 81 return _detector; 82 } 83 84 /** 85 * Get MIME type for given fileName. 86 * See_Also: $(D mimeTypeForFile) 87 */ 88 Rebindable!(const(MimeType)) mimeTypeForFileName(string fileName) 89 { 90 return findExistingAlternative(_detector.mimeTypesForFileName(fileName)); 91 } 92 93 /** 94 * Get MIME type for given data. 95 * Note: This does NOT provide any fallbacks like text/plain, application/octet-stream or application/x-zerosize. 96 * See_Also: $(D mimeTypeForFile) 97 */ 98 Rebindable!(const(MimeType)) mimeTypeForData(const(void)[] data) 99 { 100 return findExistingAlternative(_detector.mimeTypesForData(data)); 101 } 102 103 /** 104 * Get MIME type for file and its data using methods describing in options. 105 * Params: 106 * fileName = Name of file 107 * data = Data chunk read from the file. It's not necessary to read the whole file. 108 * options = Lookup options 109 */ 110 Rebindable!(const(MimeType)) mimeTypeForFile(string fileName, const(void)[] data, Match options = Match.all) 111 { 112 return mimeTypeForFileImpl(fileName, data, options, true); 113 } 114 115 /** 116 * Get MIME type for file using methods describing in options. 117 * File contents will be read automatically if needed. 118 */ 119 Rebindable!(const(MimeType)) mimeTypeForFile(string fileName, Match options = Match.all) 120 { 121 return mimeTypeForFileImpl(fileName, null, options, false); 122 } 123 124 private auto checkIfXml(string fileName, const(void)[] data, const bool dataPassed) 125 { 126 static import std.file; 127 import mime.xml : getXMLnamespaceFromData; 128 if (!dataPassed) 129 { 130 try { 131 data = std.file.read(fileName, dataSizeToRead); 132 } catch(Exception e) { 133 return rebindable(const(MimeType).init); 134 } 135 } 136 string namespaceURI = getXMLnamespaceFromData(cast(const(char)[])data); 137 if (namespaceURI.length) 138 { 139 auto name = _detector.mimeTypeForNamespaceURI(namespaceURI); 140 return mimeType(name, No.resolveAlias); 141 } 142 return rebindable(const(MimeType).init); 143 } 144 145 private auto mimeTypeForFileImpl(string fileName, const(void)[] data, Match options, bool dataPassed) 146 { 147 auto type = mimeTypeForFileImplRef(fileName, data, options, dataPassed); 148 if ((options & Match.namespaceURI) != 0 && type && type.name == "application/xml") 149 { 150 auto xmlType = checkIfXml(fileName, data, dataPassed); 151 if (xmlType) 152 return xmlType; 153 } 154 return type; 155 } 156 157 private auto mimeTypeForFileImplRef(string fileName, ref const(void)[] data, Match options, ref bool dataPassed) 158 { 159 static import std.file; 160 import std.file : getSize; 161 import std.exception : collectException; 162 163 if (data is null && (options & Match.inodeType)) { 164 string inodeType = inodeMimeType(fileName); 165 if (inodeType.length) { 166 return mimeType(inodeType); 167 } 168 } 169 170 const(char[])[] mimeTypes; 171 if (options & Match.globPatterns) { 172 mimeTypes = _detector.mimeTypesForFileName(fileName); 173 if (mimeTypes.length == 1) { 174 auto type = mimeType(mimeTypes[0]); 175 if (type !is null) { 176 return type; 177 } 178 } 179 } 180 181 if ((options & Match.emptyFileFallback) && mimeTypes.length == 0) { 182 if (!dataPassed) { 183 ulong size; 184 auto e = collectException(fileName.getSize, size); 185 if (e is null && size == 0) { 186 return mimeType("application/x-zerosize"); 187 } 188 } else { 189 if (data.length == 0) { 190 return mimeType("application/x-zerosize"); 191 } 192 } 193 } 194 195 if (!dataPassed && (options & (Match.magicRules|Match.textFallback|Match.octetStreamFallback))) { 196 try { 197 data = std.file.read(fileName, dataSizeToRead); 198 } catch(Exception e) { 199 //pass 200 } 201 } 202 203 if (data.length && (options & Match.magicRules)) { 204 auto conflicts = _detector.mimeTypesForData(data); 205 auto type = findExistingAlternative(conflicts); 206 if (type !is null) { 207 return type; 208 } 209 } 210 if (mimeTypes.length) { 211 auto type = findExistingAlternative(mimeTypes); 212 if (type) { 213 return type; 214 } 215 } 216 if (data.length && (options & Match.textFallback) && isTextualData(data)) { 217 return mimeType("text/plain"); 218 } 219 if (data.length && (options & Match.octetStreamFallback)) { 220 return mimeType("application/octet-stream"); 221 } 222 223 return rebindable(const(MimeType).init); 224 } 225 226 private auto findExistingAlternative(const(char[])[] conflicts) 227 { 228 foreach(name; conflicts) { 229 auto type = mimeType(name); 230 if (type !is null) { 231 return type; 232 } 233 } 234 return rebindable(const(MimeType).init); 235 } 236 237 /** 238 * Get mime type by name or alias. 239 * Params: 240 * nameOrAlias = MIME type name or alias. 241 * resolve = Try to resolve alias if could not find MIME type with given name. 242 * Returns: $(D mime.type.MimeType) for given nameOrAlias, resolving alias if needed. Null if no mime type found. 243 */ 244 Rebindable!(const(MimeType)) mimeType(const(char)[] nameOrAlias, Flag!"resolveAlias" resolve = Yes.resolveAlias) 245 { 246 if (nameOrAlias.length == 0) { 247 return rebindable(const(MimeType).init); 248 } 249 auto type = _store.mimeType(nameOrAlias); 250 if (type is null && resolve) { 251 auto resolved = _detector.resolveAlias(nameOrAlias); 252 if (resolved.length) { 253 type = _store.mimeType(resolved); 254 } 255 } 256 return type; 257 } 258 259 private: 260 IMimeStore _store; 261 IMimeDetector _detector; 262 } 263 264 /// 265 unittest 266 { 267 import mime.stores.files; 268 import mime.detectors.cache; 269 270 auto mimePaths = ["./test/mime", "./test/discard", "./test/nonexistent"]; 271 272 alias FilesMimeStore.Options FOptions; 273 FOptions foptions; 274 ubyte opt = FOptions.required; 275 276 foptions.types = opt; 277 foptions.aliases = opt; 278 foptions.subclasses = opt; 279 foptions.icons = opt; 280 foptions.genericIcons = opt; 281 foptions.XMLnamespaces = opt; 282 foptions.globs2 = opt; 283 foptions.globs = opt; 284 foptions.magic = opt; 285 foptions.treemagic = opt; 286 287 auto store = new FilesMimeStore(mimePaths, foptions); 288 assert(!store.byMimeType().empty); 289 auto detector = new MimeDetectorFromCache(mimePaths); 290 291 assert(detector.mimeCaches().length == 2); 292 assert(detector.mimeTypeForFileName("sprite.spr").length); 293 assert(detector.mimeTypeForFileName("model01.mdl").length); 294 assert(detector.mimeTypeForFileName("liblist.gam").length); 295 assert(detector.mimeTypeForFileName("no.exist").empty); 296 assert(detector.mimeTypeForData("IDSP\x02\x00\x00\x00") == "image/x-hlsprite"); 297 assert(detector.resolveAlias("application/nonexistent") is null); 298 299 assert(detector.mimeTypeForNamespaceURI("http://www.w3.org/1999/ent") == "text/x-ent"); 300 assert(detector.mimeTypeForNamespaceURI("nonexistent").empty); 301 302 auto database = new MimeDatabase(store, detector); 303 assert(database.detector() is detector); 304 assert(database.store() is store); 305 306 assert(database.mimeType(string.init) is null); 307 308 auto imageSprite = database.mimeType("image/x-hlsprite"); 309 auto appSprite = database.mimeType("application/x-hlsprite"); 310 assert(database.mimeType("application/x-hlsprite", No.resolveAlias) is null); 311 assert(imageSprite !is null && imageSprite is appSprite); 312 313 assert(database.detector().isSubclassOf("text/x-fgd", "text/plain")); 314 assert(!database.detector().isSubclassOf("text/x-fgd", "application/octet-stream")); 315 316 auto fgdType = database.mimeTypeForFileName("name.fgd"); 317 assert(fgdType !is null); 318 assert(fgdType.name == "text/x-fgd"); 319 320 //testing Match options 321 auto iqm = database.mimeTypeForFile("model.iqm", MimeDatabase.Match.globPatterns); 322 assert(iqm !is null); 323 assert(iqm.name == "application/x-iqm"); 324 325 auto spriteType = database.mimeTypeForFile("sprite.spr", MimeDatabase.Match.globPatterns); 326 assert(spriteType !is null); 327 328 auto sprite32 = database.mimeTypeForFile("sprite.spr", "IDSP\x20\x00\x00\x00", MimeDatabase.Match.magicRules); 329 assert(sprite32 !is null); 330 assert(sprite32.name == "image/x-sprite32"); 331 332 auto zeroType = database.mimeTypeForFile("nonexistent", (void[]).init, MimeDatabase.Match.emptyFileFallback); 333 assert(zeroType !is null); 334 assert(zeroType.name == "application/x-zerosize"); 335 336 zeroType = database.mimeTypeForFile("test/emptyfile", MimeDatabase.Match.emptyFileFallback); 337 assert(zeroType !is null); 338 assert(zeroType.name == "application/x-zerosize"); 339 340 auto textType = database.mimeTypeForFile("test/mime/types", MimeDatabase.Match.textFallback); 341 assert(textType !is null); 342 assert(textType.name == "text/plain"); 343 344 auto dirType = database.mimeTypeForFile("test", MimeDatabase.Match.inodeType); 345 assert(dirType !is null); 346 assert(dirType.name == "inode/directory"); 347 348 auto octetStreamType = database.mimeTypeForFile("test/mime/mime.cache", MimeDatabase.Match.octetStreamFallback); 349 assert(octetStreamType !is null); 350 assert(octetStreamType.name == "application/octet-stream"); 351 352 assert(database.mimeTypeForFile("file.unknown", MimeDatabase.Match.globPatterns) is null); 353 354 //testing data 355 auto hlsprite = database.mimeTypeForData("IDSP\x02\x00\x00\x00"); 356 assert(hlsprite !is null); 357 assert(hlsprite.name == "image/x-hlsprite"); 358 359 auto qsprite = database.mimeTypeForData("IDSP\x01\x00\x00\x00"); 360 assert(qsprite !is null); 361 assert(qsprite.name == "image/x-qsprite"); 362 363 auto q2sprite = database.mimeTypeForData("IDS2"); 364 assert(q2sprite !is null); 365 assert(q2sprite.name == "image/x-q2sprite"); 366 367 //testing case-insensitive suffix 368 auto vpk = database.mimeTypeForFileName("pakdir.vpk"); 369 assert(vpk !is null); 370 assert(vpk.name == "application/vnd.valve.vpk"); 371 372 vpk = database.mimeTypeForFileName("pakdir.VPK"); 373 assert(vpk !is null); 374 assert(vpk.name == "application/vnd.valve.vpk"); 375 376 //testing generic glob 377 auto modelseq = database.mimeTypeForFileName("model01.mdl"); 378 assert(modelseq !is null); 379 assert(modelseq.name == "application/x-hlmdl-sequence"); 380 modelseq = database.mimeTypeForFileName("model01.MDL"); 381 assert(modelseq !is null && modelseq.name == "application/x-hlmdl-sequence"); 382 383 auto generalGlob = database.mimeTypeForFileName("general_test_long_glob"); 384 assert(generalGlob !is null); 385 assert(generalGlob.name == "application/x-general-long-glob"); 386 assert(database.detector.mimeTypeForFileName("general_test_long_glob")); 387 388 assert(!database.mimeTypeForFileName("pak1.PAK")); 389 assert(database.mimeTypeForFileName("pak1.pak")); 390 391 //testing case-sensitive suffix 392 assert(database.mimeTypeForFileName("my.shader")); 393 assert(!database.mimeTypeForFileName("my.SHADER")); 394 395 //testing literal 396 assert(database.mimeTypeForFileName("liblist.gam")); 397 assert(database.mimeTypeForFileName("makefile")); 398 399 //testing discard glob 400 assert(!database.mimeTypeForFileName("GNUmakefile")); 401 assert(!database.detector.mimeTypeForFileName("GNUmakefile")); 402 403 assert(!database.mimeTypeForFileName("file.qvm3")); 404 assert(!database.detector.mimeTypeForFileName("file.qvm3")); 405 406 assert(!database.mimeTypeForFileName("model01.sequence")); 407 assert(!database.detector.mimeTypeForFileName("model01.sequence")); 408 409 //testing discard magic 410 assert(!database.mimeTypeForData("PAK")); 411 assert(!database.detector.mimeTypeForData("PAK")); 412 assert(!database.mimeTypeForFileName("file.qwad")); 413 414 //conflicts 415 assert(database.mimeTypeForFileName("file.jmf")); 416 assert(database.mimeTypeForData("PACK")); 417 418 //xml 419 assert(database.mimeTypeForFileName("file.xml").name == "application/xml"); 420 assert(database.mimeTypeForData("<?xml").name == "application/xml"); 421 assert(database.mimeTypeForFile("file.xml", `<start-element xmlns="http://www.w3.org/1999/ent">`).name == "text/x-ent"); 422 }