1 /** 2 * Class for reading mime.cache files. 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.cache; 12 13 import mime.common; 14 15 private { 16 import std.mmfile; 17 18 import std.algorithm; 19 import std.bitmanip; 20 import std.exception; 21 import std.path; 22 import std.range; 23 import std..string; 24 import std.system; 25 import std.traits; 26 import std.typecons; 27 } 28 29 private struct MimeCacheHeader 30 { 31 ushort majorVersion; 32 ushort minorVersion; 33 uint aliasListOffset; 34 uint parentListOffset; 35 uint literalListOffset; 36 uint reverseSuffixTreeOffset; 37 uint globListOffset; 38 uint magicListOffset; 39 uint namespaceListOffset; 40 uint iconsListOffset; 41 uint genericIconsListOffset; 42 } 43 44 ///Alias entry in mime cache. 45 alias Tuple!(const(char)[], "aliasName", const(char)[], "mimeType") AliasEntry; 46 47 ///Other glob than literal or suffix glob pattern. 48 alias Tuple!(const(char)[], "glob", const(char)[], "mimeType", ubyte, "weight", bool, "cs") GlobEntry; 49 ///Literal glob 50 alias Tuple!(const(char)[], "literal", const(char)[], "mimeType", ubyte, "weight", bool, "cs") LiteralEntry; 51 52 ///Icon or generic icon entry in mime cache. 53 alias Tuple!(const(char)[], "mimeType", const(char)[], "iconName") IconEntry; 54 55 ///XML namespace entry in mime cache. 56 alias Tuple!(const(char)[], "namespaceUri", const(char)[], "localName", const(char)[], "mimeType") NamespaceEntry; 57 58 ///Magic match entry in mime cache. 59 alias Tuple!(uint, "weight", const(char)[], "mimeType", uint, "matchletCount", uint, "firstMatchletOffset") MatchEntry; 60 61 ///Magic matchlet entry in mime cache. 62 alias Tuple!(uint, "rangeStart", uint, "rangeLength", 63 uint, "wordSize", uint, "valueLength", 64 const(char)[], "value", const(char)[], "mask", 65 uint, "childrenCount", uint, "firstChildOffset") 66 MatchletEntry; 67 68 private { 69 alias Tuple!(const(char)[], "mimeType", uint, "parentsOffset") ParentEntry; 70 alias Tuple!(ubyte, "weight", bool, "cs") WeightAndCs; 71 } 72 73 /// MIME type alternative found by file name. 74 alias Tuple!(const(char)[], "mimeType", uint, "weight", bool, "cs", const(char)[], "pattern") MimeTypeAlternativeByName; 75 76 /// MIME type alternative found by data. 77 alias Tuple!(const(char)[], "mimeType", uint, "weight") MimeTypeAlternative; 78 79 private @nogc @safe auto parseWeightAndFlags(uint value) nothrow pure { 80 return WeightAndCs(value & 0xFF, (value & 0x100) != 0); 81 } 82 83 /** 84 * Error occured while parsing mime cache. 85 */ 86 class MimeCacheException : Exception 87 { 88 /// 89 this(string msg, string context = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe { 90 super(msg, file, line, next); 91 _context = context; 92 } 93 94 /** 95 * Context where error occured. Usually it's the name of value that could not be read or is invalid. 96 */ 97 @nogc @safe string context() const nothrow { 98 return _context; 99 } 100 private: 101 string _context; 102 } 103 104 /** 105 * Class for reading mime.cache files. Mime cache is mainly optimized for MIME type detection by file name. 106 * 107 * This class is somewhat low level and tricky to use directly. 108 * Also it knows nothing about $(D mime.type.MimeType). 109 * Note: 110 * This class does not try to provide more information than the underlying mime.cache file has. 111 */ 112 final class MimeCache 113 { 114 /** 115 * Read mime cache from given file (usually mime.cache from one of mime paths) 116 * Note: 117 * File will be mapped into memory with MmFile. 118 * Warning: 119 * Strings returned from MimeCache can't be considered permanent 120 * and must be copied with $(B dup) or $(B idup) if their lifetime is longer than this object's one. 121 * Throws: 122 * FileException if could not map file into memory. 123 * $(D MimeCacheException) if provided file is not valid mime cache or unsupported version. 124 */ 125 @trusted this(string fileName) { 126 _mmapped = new MmFile(fileName); 127 this(_mmapped[], fileName, 0); 128 } 129 130 /** 131 * Read mime cache from given data. 132 * Throws: 133 * $(D MimeCacheException) if provided file is not valid mime cache or unsupported version. 134 */ 135 @safe this(immutable(void)[] data, string fileName = null) { 136 this(data, fileName, 0); 137 } 138 139 /** 140 * File name MimeCache was loaded from. 141 */ 142 @nogc @safe string fileName() nothrow const { 143 return _fileName; 144 } 145 146 private @trusted this(const(void)[] data, string fileName, int /* To avoid ambiguity */) { 147 _data = data; 148 _fileName = fileName; 149 150 _header.majorVersion = readValue!ushort(0, "major version"); 151 if (_header.majorVersion != 1) { 152 throw new MimeCacheException("Unsupported mime cache format version or the file is not mime cache", "major version"); 153 } 154 155 _header.minorVersion = readValue!ushort(2, "minor version"); 156 if (_header.minorVersion != 2) { 157 throw new MimeCacheException("Unsupported mime cache format version or the file is not mime cache", "minor version"); 158 } 159 160 _header.aliasListOffset = readValue!uint(4, "alias list offset"); 161 _header.parentListOffset = readValue!uint(8, "parent list offset"); 162 _header.literalListOffset = readValue!uint(12, "literal list offset"); 163 _header.reverseSuffixTreeOffset = readValue!uint(16, "reverse suffix tree offset"); 164 _header.globListOffset = readValue!uint(20, "glob list offset"); 165 _header.magicListOffset = readValue!uint(24, "magic list offset"); 166 _header.namespaceListOffset = readValue!uint(28, "namespace list offset"); 167 _header.iconsListOffset = readValue!uint(32, "icon list offset"); 168 _header.genericIconsListOffset = readValue!uint(36, "generic list offset"); 169 170 if (!aliasesImpl().isSorted!aliasesCmp) { 171 throw new MimeCacheException("aliases must be sorted by alias name"); 172 } 173 174 if (!parentEntriesImpl().isSorted!parentEntryCmp) { 175 throw new MimeCacheException("parent list must be sorted by mime type name"); 176 } 177 178 foreach (parentEntry; parentEntries()) { 179 foreach (parent; parents(parentEntry.parentsOffset)) { 180 //just iterate over all parents to ensure all is ok 181 } 182 } 183 184 if (!literalsImpl().isSorted!literalsCmp) { 185 throw new MimeCacheException("literals must be sorted by literal"); 186 } 187 188 foreach(glob; globs()) { 189 //just iterate over all globs to ensure all is ok 190 } 191 192 if (!commonIconsImpl(_header.iconsListOffset).isSorted!iconsCmp) { 193 throw new MimeCacheException("icon list must be sorted by mime type name"); 194 } 195 196 if (!commonIconsImpl(_header.genericIconsListOffset).isSorted!iconsCmp) { 197 throw new MimeCacheException("generic icons list must be sorted by mime type name"); 198 } 199 200 if (!namespacesImpl().isSorted!namespacesCmp) { 201 throw new MimeCacheException("namespaces must be sorted by namespace uri"); 202 } 203 204 auto rootCount = readValue!uint(_header.reverseSuffixTreeOffset, "root count"); 205 auto firstRootOffset = readValue!uint(_header.reverseSuffixTreeOffset + rootCount.sizeof, "first root offset"); 206 checkSuffixTree(firstRootOffset, rootCount); 207 208 if (!magicMatchesImpl().isSorted!magicMatchesCmp) { 209 throw new MimeCacheException("magic matches must be sorted first by weight (descending) and then by mime type names (ascending)"); 210 } 211 212 foreach(magicMatch; magicMatches()) { 213 checkMatchlets(magicMatch.matchletCount, magicMatch.firstMatchletOffset); 214 } 215 216 if (!magicToDeleteImpl().isSorted!magicToDeleteCmp) { 217 throw new MimeCacheException("magic to delete must be sorted by mime type"); 218 } 219 } 220 221 private void checkMatchlets(uint matchletCount, uint firstMatchletOffset) { 222 foreach(matchlet; magicMatchlets(matchletCount, firstMatchletOffset)) 223 { 224 if (matchlet.childrenCount) { 225 checkMatchlets(matchlet.childrenCount, matchlet.firstChildOffset); 226 } 227 } 228 } 229 230 private void checkSuffixTree(const uint startOffset, const uint count) { 231 for (uint i=0; i<count; ++i) { 232 const size_t offset = startOffset + i * uint.sizeof * 3; 233 auto character = readValue!dchar(offset, "character"); 234 235 if (character) { 236 uint childrenCount = readValue!uint(offset + uint.sizeof, "children count"); 237 uint firstChildOffset = readValue!uint(offset + uint.sizeof*2, "first child offset"); 238 checkSuffixTree(firstChildOffset, childrenCount); 239 } else { 240 uint mimeTypeOffset = readValue!uint(offset + uint.sizeof, "mime type offset"); 241 auto weightAndCs = readValue!uint(offset + uint.sizeof*2, "weight and flags").parseWeightAndFlags; 242 } 243 } 244 } 245 246 /** 247 * MIME type aliases. 248 * Returns: SortedRange of $(D AliasEntry) tuples sorted by aliasName. 249 */ 250 @trusted auto aliases() const { 251 return aliasesImpl().assumeSorted!aliasesCmp; 252 } 253 254 private enum aliasesCmp = "a.aliasName < b.aliasName"; 255 256 private @trusted auto aliasesImpl() const { 257 auto aliasCount = readValue!uint(_header.aliasListOffset, "alias count"); 258 return iota(aliasCount) 259 .map!(i => _header.aliasListOffset + aliasCount.sizeof + i*uint.sizeof*2) 260 .map!(offset => AliasEntry( 261 readString(readValue!uint(offset, "alias offset"), "alias name"), 262 readString(readValue!uint(offset+uint.sizeof, "mime type offset"), "mime type name") 263 )); 264 } 265 266 /** 267 * Resolve MIME type name by aliasName. 268 * Returns: resolved MIME type name or null if could not found any mime type for this aliasName. 269 */ 270 @trusted const(char)[] resolveAlias(scope const(char)[] aliasName) const { 271 auto aliasEntry = aliases().equalRange(AliasEntry(aliasName, null)); 272 return aliasEntry.empty ? null : aliasEntry.front.mimeType; 273 } 274 275 private @trusted auto parents(uint parentsOffset, uint parentCount) const { 276 return iota(parentCount) 277 .map!(i => parentsOffset + parentCount.sizeof + i*uint.sizeof) 278 .map!(offset => readString(readValue!uint(offset, "mime type offset"), "mime type name")); 279 } 280 281 private @trusted auto parents(uint parentsOffset) const { 282 uint parentCount = readValue!uint(parentsOffset, "parent count"); 283 return parents(parentsOffset, parentCount); 284 } 285 286 /** 287 * Get direct parents of given mimeType. 288 * Returns: Range of first level parents for given mimeType. 289 */ 290 @trusted auto parents(scope const(char)[] mimeType) const { 291 auto parentEntry = parentEntries().equalRange(ParentEntry(mimeType, 0)); 292 uint parentsOffset, parentCount; 293 294 if (parentEntry.empty) { 295 parentsOffset = 0; 296 parentCount = 0; 297 } else { 298 parentsOffset = parentEntry.front.parentsOffset; 299 parentCount = readValue!uint(parentsOffset, "parent count"); 300 } 301 return parents(parentsOffset, parentCount); 302 } 303 304 /** 305 * Recursively check if mimeType is subclass of parent. 306 * Note: Mime type is not considered to be subclass of itself. 307 * Returns: true if mimeType is subclass of parent. False otherwise. 308 */ 309 @trusted bool isSubclassOf(scope const(char)[] mimeType, scope const(char)[] parent) const { 310 return isSubclassOfHelper(mimeType, parent, 0); 311 } 312 313 // Just some number to protect from stack overflow or circular subclassing (in specially crafted files). 314 // Could not find any real limit in spec. 315 private static immutable maxSubclassDepth = 8; 316 private @trusted bool isSubclassOfHelper(scope const(char)[] mimeType, scope const(char)[] parent, uint depth) const { 317 if (depth > maxSubclassDepth) { 318 return false; 319 } 320 foreach(candidate; parents(mimeType)) { 321 if (candidate == parent || isSubclassOfHelper(candidate, parent, depth+1)) { 322 return true; 323 } 324 } 325 return false; 326 } 327 328 /** 329 * Glob patterns that are not literal nor suffixes. 330 * Returns: Range of $(D GlobEntry) tuples. 331 */ 332 @trusted auto globs() const { 333 auto globCount = readValue!uint(_header.globListOffset, "glob count"); 334 return iota(globCount) 335 .map!(i => _header.globListOffset + globCount.sizeof + i*uint.sizeof*3) 336 .map!(delegate(offset) { 337 auto glob = readString(readValue!uint(offset, "glob offset"), "glob pattern"); 338 auto mimeType = readString(readValue!uint(offset+uint.sizeof, "mime type offset"), "mime type name"); 339 auto weightAndCs = parseWeightAndFlags(readValue!uint(offset+uint.sizeof*2, "weight and flags")); 340 return GlobEntry(glob, mimeType, weightAndCs.weight, weightAndCs.cs); 341 }); 342 } 343 344 /** 345 * Literal patterns. 346 * Returns: SortedRange of $(D LiteralEntry) tuples sorted by literal. 347 */ 348 @trusted auto literals() const { 349 return literalsImpl.assumeSorted!literalsCmp; 350 } 351 352 private enum literalsCmp = "a.literal < b.literal"; 353 354 private @trusted auto literalsImpl() const { 355 auto literalCount = readValue!uint(_header.literalListOffset, "literal count"); 356 return iota(literalCount) 357 .map!(i => _header.literalListOffset + literalCount.sizeof + i*uint.sizeof*3) 358 .map!(delegate(offset) { 359 auto literal = readString(readValue!uint(offset, "literal offset"), "literal"); 360 auto mimeType = readString(readValue!uint(offset+uint.sizeof, "mime type offset"), "mime type name"); 361 auto weightAndCs = parseWeightAndFlags(readValue!uint(offset+uint.sizeof*2, "weight and flags")); 362 return LiteralEntry(literal, mimeType, weightAndCs.weight, weightAndCs.cs); 363 }); 364 } 365 366 /** 367 * Icons for MIME types. 368 * Returns: SortedRange of $(D IconEntry) tuples sorted by mimeType. 369 */ 370 @trusted auto icons() const { 371 return commonIcons(_header.iconsListOffset); 372 } 373 374 /** 375 * Generic icons for MIME types. 376 * Returns: SortedRange of $(D IconEntry) tuples sorted by mimeType. 377 */ 378 @trusted auto genericIcons() const { 379 return commonIcons(_header.genericIconsListOffset); 380 } 381 382 private enum iconsCmp = "a.mimeType < b.mimeType"; 383 384 /** 385 * XML namespaces for MIME types. 386 * Returns: SortedRange of $(D NamespaceEntry) tuples sorted by namespaceUri. 387 */ 388 @trusted auto namespaces() const { 389 return namespacesImpl().assumeSorted!namespacesCmp; 390 } 391 392 private enum namespacesCmp = "a.namespaceUri < b.namespaceUri"; 393 394 private @trusted auto namespacesImpl() const { 395 auto namespaceCount = readValue!uint(_header.namespaceListOffset, "namespace count"); 396 return iota(namespaceCount) 397 .map!(i => _header.namespaceListOffset + namespaceCount.sizeof + i*uint.sizeof*3) 398 .map!(offset => NamespaceEntry(readString(readValue!uint(offset, "namespace uri offset"), "namespace uri"), 399 readString(readValue!uint(offset+uint.sizeof, "local name offset"), "local name"), 400 readString(readValue!uint(offset+uint.sizeof*2, "mime type offset"), "mime type name"))); 401 } 402 403 @trusted const(char)[] findMimeTypeByNamespaceURI(const(char)[] namespaceURI) const 404 { 405 auto namespaceEntry = namespaces().equalRange(NamespaceEntry(namespaceURI, null, null)); 406 return namespaceEntry.empty ? null : namespaceEntry.front.mimeType; 407 } 408 409 /** 410 * Find icon name for mime type. 411 * Returns: Icon name for given mimeType or null string if not found. 412 */ 413 @trusted const(char)[] findIcon(scope const(char)[] mimeType) const { 414 auto icon = icons().equalRange(IconEntry(mimeType, null)); 415 return icon.empty ? null : icon.front.iconName; 416 } 417 418 /** 419 * Find generic icon name for mime type. 420 * Returns: Generic icon name for given mimeType or null string if not found. 421 */ 422 @trusted const(char)[] findGenericIcon(scope const(char)[] mimeType) const { 423 auto icon = genericIcons().equalRange(IconEntry(mimeType, null)); 424 return icon.empty ? null : icon.front.iconName; 425 } 426 427 private @trusted bool checkMagic(const(MatchletEntry) magicMatchlet, const(char)[] content) const { 428 429 bool check = false; 430 if (magicMatchlet.mask.length == 0 && magicMatchlet.rangeStart + magicMatchlet.value.length <= content.length) { 431 if (magicMatchlet.wordSize == 1) { 432 check = content[magicMatchlet.rangeStart..magicMatchlet.rangeStart + magicMatchlet.value.length] == magicMatchlet.value; 433 }/+ 434 //this is really rare, but should be investigated later 435 else if (magicMatchlet.wordSize != 0 && (magicMatchlet.wordSize % 2 == 0) && (magicMatchlet.valueLength % magicMatchlet.wordSize == 0)) { 436 static if (endian == Endian.littleEndian) { 437 auto byteContent = cast(const(ubyte)[])content; 438 auto byteValue = cast(const(ubyte)[])magicMatchlet.value; 439 440 check = (byteContent[magicMatchlet.rangeStart..magicMatchlet.rangeStart + magicMatchlet.value.length]) 441 .equal(byteValue.chunks(magicMatchlet.wordSize).map!(r => r.retro).joiner); 442 } else { 443 check = content[magicMatchlet.rangeStart..magicMatchlet.rangeStart + magicMatchlet.value.length] == magicMatchlet.value; 444 } 445 }+/ 446 } 447 return check; 448 } 449 450 /** 451 * Find all MIME type alternatives for data matching it against magic rules. 452 * Params: 453 * data = data to check against magic. 454 * Returns: Range of $(D MimeTypeAlternative) tuples matching given data sorted by weight descending. 455 */ 456 @trusted auto findMimeTypesByData(scope const(void)[] data) const 457 { 458 return magicMatches() 459 .filter!(match => testAgainstMatchlets(data, match.matchletCount, match.firstMatchletOffset)) 460 .map!(match => MimeTypeAlternative(match.mimeType, match.weight)); 461 } 462 463 private bool testAgainstMatchlets(scope const(void)[] data, uint matchletCount, uint firstMatchletOffset) const 464 { 465 auto content = cast(const(char)[])data; 466 foreach(matchlet; magicMatchlets(matchletCount, firstMatchletOffset)) 467 { 468 if (checkMagic(matchlet, content)) { 469 if (matchlet.childrenCount) { 470 return testAgainstMatchlets(data, matchlet.childrenCount, matchlet.firstChildOffset); 471 } else { 472 return true; 473 } 474 } 475 } 476 return false; 477 } 478 479 /** 480 * Find all MIME type alternatives for fileName using glob patterns which are not literals or suffices. 481 * Params: 482 * fileName = name to match against glob patterns. 483 * Returns: Range of $(D MimeTypeAlternativeByName) with pattern set to glob pattern matching fileName. 484 */ 485 @trusted auto findMimeTypesByGlob(scope const(char)[] fileName) const { 486 fileName = fileName.baseName; 487 return globs().filter!(delegate(GlobEntry glob) { 488 if (glob.cs) { 489 return globMatch!(std.path.CaseSensitive.yes)(fileName, glob.glob); 490 } else { 491 return globMatch!(std.path.CaseSensitive.no)(fileName, glob.glob); 492 } 493 }).map!(glob => MimeTypeAlternativeByName(glob.mimeType, glob.weight, glob.cs, glob.glob)); 494 } 495 496 /** 497 * Find all MIME type alternatives for fileName using literal patterns like Makefile. 498 * Params: 499 * fileName = name to match against literal patterns. 500 * Returns: Range of $(D MimeTypeAlternativeByName) with pattern set to literal matching fileName. 501 * Note: Depending on whether found literal is case sensitive or not literal can be equal to base fileName or not. 502 */ 503 @trusted auto findMimeTypesByLiteral(scope const(char)[] fileName) const { 504 return findMimeTypesByLiteralHelper(fileName).map!(literal => MimeTypeAlternativeByName(literal.mimeType, literal.weight, literal.cs, literal.literal)); 505 } 506 507 private @trusted auto findMimeTypesByLiteralHelper(scope const(char)[] name) const { 508 name = name.baseName; 509 //Case-sensitive match is always preferred 510 auto csLiteral = literals().equalRange(LiteralEntry(name, null, 0, false)); 511 if (csLiteral.empty) { 512 //Try case-insensitive match. toLower should work for this since all case-insensitive literals in mime.cache are stored in lower form. 513 return literals().equalRange(LiteralEntry(name.toLower, null, 0, false)); 514 } else { 515 return csLiteral; 516 } 517 } 518 519 /** 520 * Find all MIME type alternatives for fileName using suffix patterns like *.cpp. 521 * Due to mime cache format characteristics it uses output range instead of returning the input one. 522 * Params: 523 * fileName = name to match against suffix patterns. 524 * sink = output range where $(D MimeTypeAlternativeByName) objects with pattern set to suffix matching fileName will be put. 525 * Note: pattern property of $(D MimeTypeAlternativeByName) objects will not have leading "*" to avoid allocating. 526 */ 527 @trusted void findMimeTypesBySuffix(OutRange)(scope const(char)[] fileName, OutRange sink) const if (isOutputRange!(OutRange, MimeTypeAlternativeByName)) 528 { 529 auto rootCount = readValue!uint(_header.reverseSuffixTreeOffset, "root count"); 530 auto firstRootOffset = readValue!uint(_header.reverseSuffixTreeOffset + rootCount.sizeof, "first root offset"); 531 532 lookupLeaf(firstRootOffset, rootCount, fileName, fileName, sink); 533 } 534 535 /** 536 * All magic matches in this mime cache. Matches don't include magic rules themselves, but they reference matchlets. 537 * Returns: Range of $(D MatchEntry) tuples. 538 * See_Also: $(D magicMatchlets) 539 */ 540 @trusted auto magicMatches() const { 541 return magicMatchesImpl().assumeSorted!magicMatchesCmp; 542 } 543 544 private @trusted auto magicMatchesImpl() const { 545 auto toReturn = allMagicMatchesImpl(); 546 while(!toReturn.empty && toReturn.front.weight == 0) { 547 toReturn.popFront(); //remove entries meant for magic-deleteall 548 } 549 return toReturn; 550 } 551 552 private auto allMagicMatchesImpl() const { 553 auto matchCount = readValue!uint(_header.magicListOffset, "match count"); 554 auto maxExtent = readValue!uint(_header.magicListOffset + uint.sizeof, "max extent"); //still don't get what is it 555 auto firstMatchOffset = readValue!uint(_header.magicListOffset + uint.sizeof*2, "first match offset"); 556 557 return iota(matchCount) 558 .map!(i => firstMatchOffset + i*uint.sizeof*4) 559 .map!(offset => MatchEntry(readValue!uint(offset, "weight"), 560 readString(readValue!uint(offset+uint.sizeof, "mime type offset"), "mime type name"), 561 readValue!uint(offset+uint.sizeof*2, "matchlet count"), 562 readValue!uint(offset+uint.sizeof*3, "first matchlet offset") 563 )); 564 } 565 566 private enum magicMatchesCmp = "(a.weight > b.weight) || (a.weight == b.weight && a.mimeType < b.mimeType)"; 567 568 /** 569 * One level magic matchlets. 570 * matchletCount and firstMatchletOffset should be taken from $(D MatchEntry) or upper level $(D MatchletEntry). 571 * Returns: Range of $(D MatchletEntry) tuples which are direct descendants of $(D MatchEntry) or another $(D MatchletEntry). 572 * See_Also: $(D magicMatches) 573 */ 574 @trusted auto magicMatchlets(uint matchletCount, uint firstMatchletOffset) const { 575 return iota(matchletCount) 576 .map!(i => firstMatchletOffset + i*uint.sizeof*8) 577 .map!(delegate(offset) { 578 uint rangeStart = readValue!uint(offset, "range start"); 579 uint rangeLength = readValue!uint(offset+uint.sizeof, "range length"); 580 uint wordSize = readValue!uint(offset+uint.sizeof*2, "word size"); 581 uint valueLength = readValue!uint(offset+uint.sizeof*3, "value length"); 582 583 uint valueOffset = readValue!uint(offset+uint.sizeof*4, "value offset"); 584 const(char)[] value = readString(valueOffset, valueLength, "value"); 585 uint maskOffset = readValue!uint(offset+uint.sizeof*5, "mask offset"); 586 const(char)[] mask = maskOffset ? readString(maskOffset, valueLength, "mask") : null; 587 uint childrenCount = readValue!uint(offset+uint.sizeof*6, "children count"); 588 uint firstChildOffset = readValue!uint(offset+uint.sizeof*7, "first child offset"); 589 return MatchletEntry(rangeStart, rangeLength, wordSize, valueLength, value, mask, childrenCount, firstChildOffset); 590 }); 591 } 592 593 /** 594 * Names of mime types which magic rules from the least preferred mime.cache files should be discarded. 595 * Returns: Range of mime type names that were marked as magic-deleteall. 596 */ 597 @trusted auto magicToDelete() const { 598 return magicToDeleteImpl().assumeSorted!magicToDeleteCmp; 599 } 600 601 private @trusted auto magicToDeleteImpl() const { 602 auto allMatches = allMagicMatchesImpl(); 603 auto count = allMatches.countUntil!(m => m.weight != 0); 604 return allMatches.take(count).map!(m => m.mimeType); 605 } 606 607 private enum magicToDeleteCmp = "a < b"; 608 609 private: 610 @trusted void lookupLeaf(OutRange)(const uint startOffset, const uint count, scope const(char[]) originalName, scope const(char[]) name, OutRange sink, scope const(char[]) suffix = null, const bool wasCaseMismatch = false) const { 611 612 for (uint i=0; i<count; ++i) { 613 const size_t offset = startOffset + i * uint.sizeof * 3; 614 auto character = readValue!dchar(offset, "character"); 615 616 if (character) { 617 if (name.length) { 618 dchar back = name.back; 619 if (character.toLower == back.toLower) { 620 uint childrenCount = readValue!uint(offset + uint.sizeof, "children count"); 621 uint firstChildOffset = readValue!uint(offset + uint.sizeof*2, "first child offset"); 622 const(char)[] currentName = name; 623 currentName.popBack(); 624 bool caseMismatch = wasCaseMismatch || character != back; 625 lookupLeaf(firstChildOffset, childrenCount, originalName, currentName, sink, originalName[currentName.length..$], caseMismatch); 626 } 627 } 628 } else { 629 uint mimeTypeOffset = readValue!uint(offset + uint.sizeof, "mime type offset"); 630 auto weightAndCs = readValue!uint(offset + uint.sizeof*2, "weight and flags").parseWeightAndFlags; 631 632 auto mimeTypeEntry = MimeTypeAlternativeByName(readString(mimeTypeOffset, "mime type name"), weightAndCs.weight, weightAndCs.cs, suffix); 633 634 //if case sensitive make sure that file name ends with suffix 635 if (weightAndCs.cs) { 636 if (!wasCaseMismatch) { 637 sink(mimeTypeEntry); 638 } 639 } else { 640 sink(mimeTypeEntry); 641 } 642 } 643 } 644 } 645 646 enum parentEntryCmp = "a.mimeType < b.mimeType"; 647 648 auto parentEntries() const { 649 return parentEntriesImpl().assumeSorted!parentEntryCmp; 650 } 651 652 auto parentEntriesImpl() const { 653 auto parentListCount = readValue!uint(_header.parentListOffset, "parent list count"); 654 return iota(parentListCount) 655 .map!(i => _header.parentListOffset + parentListCount.sizeof + i*uint.sizeof*2) 656 .map!(offset => ParentEntry( 657 readString(readValue!uint(offset, "mime type offset"), "mime type name"), 658 readValue!uint(offset + uint.sizeof, "parents offset") 659 )); 660 } 661 662 @trusted auto commonIcons(uint iconsListOffset) const { 663 return commonIconsImpl(iconsListOffset).assumeSorted!iconsCmp; 664 } 665 666 @trusted auto commonIconsImpl(uint iconsListOffset) const { 667 auto iconCount = readValue!uint(iconsListOffset); 668 return iota(iconCount) 669 .map!(i => iconsListOffset + iconCount.sizeof + i*uint.sizeof*2) 670 .map!(offset => IconEntry( 671 readString(readValue!uint(offset, "mime type offset"), "mime type name"), 672 readString(readValue!uint(offset+uint.sizeof, "icon name offset"), "icon name") 673 )); 674 } 675 676 private @trusted static T readValue(T)(scope const(void)[] data, size_t offset, string context = null) pure 677 { 678 if (data.length >= offset + T.sizeof) { 679 T value = *(cast(const(T)*)data[offset..(offset+T.sizeof)].ptr); 680 static if (endian == Endian.littleEndian) { 681 value = swapEndian(value); 682 } 683 return value; 684 } else { 685 throw new MimeCacheException("Value is out of bounds", context); 686 } 687 } 688 689 unittest 690 { 691 ubyte[] data; 692 data.length = 10; 693 assertThrown!MimeCacheException(readValue!uint(data, 7)); 694 assertThrown!MimeCacheException(readValue!ushort(data, 9)); 695 } 696 697 @trusted T readValue(T)(size_t offset, string context = null) const pure if (isIntegral!T || isSomeChar!T) 698 { 699 return readValue!T(_data, offset, context); 700 } 701 702 private @trusted static auto readString(scope return const(void)[] data, size_t offset, string context = null) pure 703 { 704 if (offset > data.length) { 705 throw new MimeCacheException("Beginning of string is out of bounds", context); 706 } 707 708 auto str = cast(const(char[]))data[offset..data.length]; 709 710 size_t len = 0; 711 while (len < str.length && str[len] != '\0') { 712 ++len; 713 } 714 if (len == str.length) { 715 throw new MimeCacheException("String is not zero terminated", context); 716 } 717 718 return str[0..len]; 719 } 720 721 unittest 722 { 723 ubyte[] data; 724 data.length = 10; 725 data[] = 65; 726 assertThrown!MimeCacheException(readString(data, 11)); 727 assertThrown!MimeCacheException(readString(data, 0)); 728 } 729 730 @trusted auto readString(size_t offset, string context = null) const pure { 731 return readString(_data, offset, context); 732 } 733 734 735 private @trusted static auto readString(scope return const(void)[] data, size_t offset, uint length, string context = null) pure 736 { 737 if (offset + length <= data.length) { 738 return cast(const(char)[])data[offset..offset+length]; 739 } else { 740 throw new MimeCacheException("String is out of bounds", context); 741 } 742 } 743 744 unittest 745 { 746 ubyte[] data; 747 data.length = 10; 748 data[] = 'A'; 749 assertThrown!MimeCacheException(readString(data, 0, 11)); 750 assertThrown!MimeCacheException(readString(data, 1, 10)); 751 } 752 753 @trusted auto readString(size_t offset, uint length, string context = null) const pure { 754 755 return readString(_data, offset, length, context); 756 } 757 758 MmFile _mmapped; 759 const(void)[] _data; 760 MimeCacheHeader _header; 761 string _fileName; 762 } 763 764 unittest 765 { 766 immutable(ubyte)[] data = [0, 1, 0, 15]; 767 auto thrown = collectException!MimeCacheException(new MimeCache(data)); 768 assert(thrown !is null); 769 assert(thrown.context == "minor version"); 770 771 data = [0, 15, 0, 2]; 772 thrown = collectException!MimeCacheException(new MimeCache(data)); 773 assert(thrown !is null); 774 assert(thrown.context == "major version"); 775 776 auto cache = new MimeCache("./test/mime/mime.cache"); 777 assert(cache.fileName() == "./test/mime/mime.cache"); 778 assert(cache.findGenericIcon("application/x-wad") == "package-x-generic"); 779 assert(cache.findIcon("application/vnd.valve.vpk") == "package-x-vpk"); 780 781 assert(cache.isSubclassOf("text/x-fgd", "text/plain")); 782 assert(!cache.isSubclassOf("text/x-fgd", "application/octet-stream")); 783 784 assert(cache.findMimeTypeByNamespaceURI("http://www.w3.org/1999/ent") == "text/x-ent"); 785 } 786