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.files; 13 14 import mime.common; 15 import mime.store; 16 17 private { 18 import std.algorithm : map; 19 import std.array : empty; 20 import std.exception; 21 import std.file : isDir, FileException; 22 import std.mmfile; 23 import std.path; 24 import std.range : retro; 25 import std.range.interfaces : inputRangeObject; 26 import std.range.primitives : isInputRange, ElementType; 27 import std.stdio; 28 import std.typecons; 29 30 import mime.files.aliases; 31 import mime.files.globs; 32 import mime.files.icons; 33 import mime.files.magic; 34 import mime.files.namespaces; 35 import mime.files.subclasses; 36 import mime.files.types; 37 import mime.files.treemagic; 38 } 39 40 public import mime.files.common; 41 42 private @trusted auto fileReader(string fileName) { 43 return File(fileName, "r").byLineCopy(); 44 } 45 46 /** 47 * Implementation of $(D mime.store.IMimeStore) interface that uses various files from mime/ subfolder to read MIME types. 48 */ 49 final class FilesMimeStore : IMimeStore 50 { 51 /// 52 alias Tuple!(string, "fileName", Exception, "e") FileError; 53 54 /** 55 * Options to use when reading various shared MIME-info database files. 56 */ 57 struct Options 58 { 59 enum : ubyte { 60 skip = 0, ///Don't try to read file. 61 read = 1, ///Try to read file. Give up on any error without throwing it. 62 throwReadError = 2, ///Throw on file reading error. 63 throwParseError = 4, ///Throw on file parsing error. 64 saveErrors = 8, ///Save non-thrown errors to retrieve later via errors method. 65 66 optional = read | throwParseError, ///Read file if it's readable. Throw only on malformed contents. 67 required = read | throwReadError | throwParseError, ///Always try to read file, throw on any error. 68 allowFail = read, ///Don't throw if file can't be read or has invalid contents. 69 } 70 71 ubyte types = optional; ///Options for reading types file. 72 ubyte aliases = optional; ///Options for reading aliases file. 73 ubyte subclasses = optional; ///Options for reading subclasses file. 74 ubyte icons = optional; ///Options for reading icons file. 75 ubyte genericIcons = optional; ///Options for reading generic-icons file. 76 ubyte XMLnamespaces = optional;///Options for reading XMLnamespaces file. 77 ubyte globs2 = optional; ///Options for reading globs2 file. 78 ubyte globs = optional; ///Options for reading globs file. Used only if globs2 file could not be read. 79 ubyte magic = skip; ///Options for reading magic file. 80 ubyte treemagic = skip; ///Options for reading treemagic file. 81 } 82 83 private void handleError(Exception e, ubyte option, string fileName) 84 { 85 bool known; 86 if (cast(MimeFileException)e !is null || 87 cast(MimeMagicFileException)e !is null || 88 cast(TreeMagicFileException)e !is null) 89 { 90 if (option & Options.throwParseError) { 91 throw e; 92 } 93 known = true; 94 } 95 96 if (cast(ErrnoException)e !is null || 97 cast(FileException)e !is null) 98 { 99 if (option & Options.throwReadError) { 100 throw e; 101 } 102 known = true; 103 } 104 105 if (!known) { 106 throw e; 107 } 108 109 if (option & Options.saveErrors) { 110 _errors ~= FileError(fileName, e); 111 } 112 } 113 114 /** 115 * Constructor based on MIME paths. 116 * Params: 117 * mimePaths = Range of paths to base mime/ directories in order from more preferable to less preferable. 118 * options = Options for file reading and error reporting. 119 * Throws: 120 * $(D mime.files.common.MimeFileException) if some info file has errors. 121 * $(D mime.files.magic.MimeMagicFileException) if magic file has errors. 122 * $(D mime.files.treemagic.TreeMagicFileException) if treemagic file has errors. 123 * $(B ErrnoException) or $(B FileException) if some important file does not exist or could not be read. 124 * See_Also: $(D mime.paths.mimePaths) 125 */ 126 this(Range)(Range mimePaths, Options options = Options.init) if (isInputRange!Range && is(ElementType!Range : string)) 127 { 128 foreach(mimePath; mimePaths.retro) { 129 bool dirExists; 130 collectException(mimePath.isDir, dirExists); 131 if (!dirExists) { 132 continue; 133 } 134 135 if (options.types & Options.read) { 136 auto typesPath = buildPath(mimePath, "types"); 137 try { 138 foreach(line; typesFileReader(fileReader(typesPath))) { 139 ensureMimeType(line); 140 } 141 } catch(Exception e) { 142 handleError(e, options.types, typesPath); 143 } 144 } 145 146 if (options.aliases & Options.read) { 147 auto aliasesPath = buildPath(mimePath, "aliases"); 148 try { 149 auto aliases = aliasesFileReader(fileReader(aliasesPath)); 150 foreach(aliasLine; aliases) { 151 auto mimeType = ensureMimeType(aliasLine.mimeType); 152 mimeType.addAlias(aliasLine.aliasName); 153 } 154 } catch(Exception e) { 155 handleError(e, options.aliases, aliasesPath); 156 } 157 } 158 159 if (options.subclasses & Options.read) { 160 auto subclassesPath = buildPath(mimePath, "subclasses"); 161 try { 162 auto subclasses = subclassesFileReader(fileReader(subclassesPath)); 163 foreach(subclassLine; subclasses) { 164 auto mimeType = ensureMimeType(subclassLine.mimeType); 165 mimeType.addParent(subclassLine.parent); 166 } 167 } catch(Exception e) { 168 handleError(e, options.subclasses, subclassesPath); 169 } 170 } 171 172 if (options.icons & Options.read) { 173 auto iconsPath = buildPath(mimePath, "icons"); 174 try { 175 auto icons = iconsFileReader(fileReader(iconsPath)); 176 foreach(iconLine; icons) { 177 auto mimeType = ensureMimeType(iconLine.mimeType); 178 mimeType.icon = iconLine.iconName; 179 } 180 } catch(Exception e) { 181 handleError(e, options.icons, iconsPath); 182 } 183 } 184 185 if (options.genericIcons & Options.read) { 186 auto genericIconsPath = buildPath(mimePath, "generic-icons"); 187 try { 188 auto icons = iconsFileReader(fileReader(genericIconsPath)); 189 foreach(iconLine; icons) { 190 auto mimeType = ensureMimeType(iconLine.mimeType); 191 mimeType.genericIcon = iconLine.iconName; 192 } 193 } catch(Exception e) { 194 handleError(e, options.genericIcons, genericIconsPath); 195 } 196 } 197 198 if (options.XMLnamespaces & Options.read) { 199 auto namespacesPath = buildPath(mimePath, "XMLnamespaces"); 200 try { 201 auto namespaces = namespacesFileReader(fileReader(namespacesPath)); 202 foreach(namespaceLine; namespaces) { 203 auto mimeType = ensureMimeType(namespaceLine.mimeType); 204 mimeType.addXMLnamespace(namespaceLine.namespaceUri, namespaceLine.localName); 205 } 206 } catch(Exception e) { 207 handleError(e, options.XMLnamespaces, namespacesPath); 208 } 209 } 210 211 bool shouldReadGlobs = false; 212 if (options.globs2 & Options.read) { 213 auto globs2Path = buildPath(mimePath, "globs2"); 214 try { 215 setGlobs(globs2FileReader(fileReader(globs2Path))); 216 } catch(Exception e) { 217 handleError(e, options.globs2, globs2Path); 218 shouldReadGlobs = true; 219 } 220 } else { 221 shouldReadGlobs = true; 222 } 223 224 if (shouldReadGlobs && (options.globs & Options.read)) { 225 auto globsPath = buildPath(mimePath, "globs"); 226 try { 227 setGlobs(globsFileReader(fileReader(globsPath))); 228 } catch(Exception e) { 229 handleError(e, options.globs, globsPath); 230 } 231 } 232 233 if (options.magic & Options.read) { 234 auto magicPath = buildPath(mimePath, "magic"); 235 try { 236 void sink(MagicEntry t) { 237 auto mimeType = ensureMimeType(t.mimeType); 238 if (t.deleteMagic) { 239 mimeType.clearMagic(); 240 } 241 if (!t.magic.matches.empty) { 242 mimeType.addMagic(t.magic); 243 } 244 } 245 auto mmFile = new MmFile(magicPath); 246 magicFileReader(mmFile[], &sink); 247 } catch(Exception e) { 248 handleError(e, options.magic, magicPath); 249 } 250 } 251 252 if (options.treemagic & Options.read) { 253 auto treemagicPath = buildPath(mimePath, "treemagic"); 254 try { 255 void treeSink(TreeMagicEntry t) { 256 auto mimeType = ensureMimeType(t.mimeType); 257 mimeType.addTreeMagic(t.magic); 258 } 259 auto mmFile = new MmFile(treemagicPath); 260 treeMagicFileReader(mmFile[], &treeSink); 261 } catch(Exception e) { 262 handleError(e, options.treemagic, treemagicPath); 263 } 264 } 265 } 266 } 267 268 unittest 269 { 270 auto mimePaths = ["test/errors"]; 271 const skipAll = Options(0,0,0,0,0,0,0,0,0,0); 272 273 void fileTest(string name, T = MimeFileException)(ubyte opt = Options.required) { 274 Options options = skipAll; 275 mixin("options." ~ name ~ " = opt;"); 276 assertThrown!T(new FilesMimeStore(mimePaths, options)); 277 } 278 279 fileTest!("types"); 280 fileTest!("aliases"); 281 fileTest!("subclasses"); 282 fileTest!("genericIcons"); 283 fileTest!("icons"); 284 fileTest!("XMLnamespaces"); 285 fileTest!("globs"); 286 fileTest!("globs2", ErrnoException); 287 288 Options magic = skipAll; 289 magic.magic = Options.required; 290 assertThrown!MimeMagicFileException(new FilesMimeStore(mimePaths, magic)); 291 292 Options treemagic = skipAll; 293 treemagic.treemagic = Options.required; 294 assertThrown!TreeMagicFileException(new FilesMimeStore(mimePaths, treemagic)); 295 296 const opt = Options.allowFail | Options.saveErrors; 297 const all = Options(opt, opt, opt, opt, opt, opt, opt, opt, opt, opt); 298 auto store = new FilesMimeStore(mimePaths, all); 299 assert(store.errors().length == 10); 300 } 301 302 /// 303 InputRange!(const(MimeType)) byMimeType() { 304 return inputRangeObject(_mimeTypes.byValue().map!(val => cast(const(MimeType))val)); 305 } 306 307 /// 308 Rebindable!(const(MimeType)) mimeType(const char[] name) { 309 return rebindable(mimeTypeImpl(name)); 310 } 311 312 private final const(MimeType) mimeTypeImpl(const char[] name) { 313 MimeType* pmimeType = name in _mimeTypes; 314 if (pmimeType) { 315 return *pmimeType; 316 } else { 317 return null; 318 } 319 } 320 321 /** 322 * Get errors that were told to not throw but to be saved during parsing. 323 */ 324 const(FileError)[] errors() const { 325 return _errors; 326 } 327 328 private: 329 @trusted MimeType ensureMimeType(const(char)[] name) { 330 MimeType* pmimeType = name in _mimeTypes; 331 if (pmimeType) { 332 return *pmimeType; 333 } else { 334 string mimeName = name.idup; 335 auto mimeType = new MimeType(mimeName); 336 mimeType.icon = defaultIconName(mimeName); 337 mimeType.genericIcon = defaultGenericIconName(mimeName); 338 _mimeTypes[mimeName] = mimeType; 339 return mimeType; 340 } 341 } 342 343 @trusted void setGlobs(Range)(Range globs) { 344 foreach(globLine; globs) { 345 if (!globLine.pattern.length) { 346 continue; 347 } 348 auto mimeType = ensureMimeType(globLine.mimeType); 349 350 if (globLine.pattern.isNoGlobs()) { 351 mimeType.clearGlobs(); 352 } else { 353 mimeType.addGlob(globLine.pattern, globLine.weight, globLine.caseSensitive); 354 } 355 } 356 } 357 358 MimeType[const(char)[]] _mimeTypes; 359 FileError[] _errors; 360 }