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