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