1 /** 2 * Parsing mime/treemagic 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, 2016 9 */ 10 11 module mime.files.treemagic; 12 13 public import mime.treemagic; 14 import mime.common; 15 16 private { 17 import std.algorithm; 18 import std.bitmanip; 19 import std.conv; 20 import std.exception; 21 import std.path; 22 import std.range; 23 import std..string; 24 import std.traits; 25 import std.typecons; 26 import mime.files.common; 27 } 28 29 ///Exception thrown on parse errors while reading treemagic file. 30 final class TreeMagicFileException : Exception 31 { 32 this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe { 33 super(msg, file, line, next); 34 } 35 } 36 37 ///MIME type name and corresponding treemagic. 38 alias Tuple!(immutable(char)[], "mimeType", TreeMagic, "magic") TreeMagicEntry; 39 40 private @trusted TreeMatch parseTreeMatch(ref const(char)[] current, uint myIndent) 41 { 42 enforce(current.length && current[0] == '>', "Expected '>' at the start of match rule"); 43 current = current[1..$]; 44 enforce(current.length && current[0] == '"', "Expected \" before path"); 45 current = current[1..$]; 46 47 auto result = findSplit(current, "\""); 48 enforce(result[1].length, "Could not find \" in the end of path"); 49 50 auto path = result[0]; 51 enforce(path.isValidPath, "Path in treematch must be valid"); 52 current = result[2]; 53 54 enforce(current.length && current[0] == '=', "Expected '=' after path"); 55 current = current[1..$]; 56 57 TreeMatch.Type type; 58 if (current.startsWith("file")) { 59 type = TreeMatch.Type.file; 60 current = current["file".length..$]; 61 } else if (current.startsWith("directory")) { 62 type = TreeMatch.Type.directory; 63 current = current["directory".length..$]; 64 } else if (current.startsWith("link")) { 65 type = TreeMatch.Type.link; 66 current = current["link".length..$]; 67 } else if (current.startsWith("any")) { 68 type = TreeMatch.Type.any; 69 current = current["any".length..$]; 70 } else { 71 throw new Exception("Unknown path type"); 72 } 73 74 auto endResult = findSplit(current, "\n"); 75 enforce(endResult[1].length, "Could not find new line character in the end of treematch section"); 76 77 TreeMatch.Options options; 78 string mimeType; 79 auto optionsStr = endResult[0]; 80 if (optionsStr.length) { 81 enforce(optionsStr[0] == ',', "Comma is expected when options are presented"); 82 optionsStr = optionsStr[1..$]; 83 auto byOption = optionsStr.splitter(","); 84 foreach(option; byOption) { 85 if (option == "executable") { 86 options |= TreeMatch.Options.executable; 87 } else if (option == "match-case") { 88 options |= TreeMatch.Options.matchCase; 89 } else if (option == "non-empty") { 90 options |= TreeMatch.Options.nonEmpty; 91 } else { 92 if (isValidMimeTypeName(option)) { 93 mimeType = option.idup; 94 } else { 95 import std.exception : assumeUnique; 96 throw new Exception(assumeUnique("Unexpected option " ~ option)); 97 } 98 } 99 } 100 } 101 102 current = endResult[2]; 103 104 auto match = TreeMatch(path.idup, type, options, mimeType); 105 106 //read sub rules 107 while (current.length && current[0] != '[') { 108 auto copy = current; 109 uint indent = parseIndent(copy); 110 if (indent > myIndent) { 111 current = copy; 112 TreeMatch submatch = parseTreeMatch(current, indent); 113 match.addSubmatch(submatch); 114 } else { 115 break; 116 } 117 } 118 119 return match; 120 } 121 122 /** 123 * Reads treemagic file contents and push treemagic entries to sink. 124 * Throws: 125 * $(D TreeMagicFileException) on error. 126 */ 127 void treeMagicFileReader(OutRange)(const(void)[] data, OutRange sink) if (isOutputRange!(OutRange, TreeMagicEntry)) 128 { 129 try { 130 enum mimeMagic = "MIME-TreeMagic\0\n"; 131 auto content = cast(const(char)[])data; 132 if (!content.startsWith(mimeMagic)) { 133 throw new Exception("Not treemagic file"); 134 } 135 136 auto current = content[mimeMagic.length..$]; 137 138 while(current.length) { 139 enforce(current[0] == '[', "Expected '[' at the start of magic section"); 140 current = current[1..$]; 141 142 auto result = findSplit(current[0..$], "]\n"); 143 enforce(result[1].length, "Could not find \"]\\n\""); 144 current = result[2]; 145 146 auto sectionResult = findSplit(result[0], ":"); 147 enforce(sectionResult[1].length, "Priority and MIME type must be splitted by ':'"); 148 149 uint priority = parse!uint(sectionResult[0]); 150 auto mimeType = sectionResult[2]; 151 152 auto magic = TreeMagic(priority); 153 154 while (current.length && current[0] != '[') { 155 uint indent = parseIndent(current); 156 157 TreeMatch match = parseTreeMatch(current, indent); 158 magic.addMatch(match); 159 } 160 sink(TreeMagicEntry(mimeType.idup, magic)); 161 } 162 } catch (Exception e) { 163 throw new TreeMagicFileException(e.msg, e.file, e.line, e.next); 164 } 165 } 166 167 /// 168 unittest 169 { 170 auto data = 171 "MIME-TreeMagic\0\n[50:x-content/video-bluray]\n" ~ 172 ">\"BDAV\"=directory,non-empty\n" ~ 173 "1>\"autorun\"=file,executable,match-case\n" ~ 174 "1>\"testlink\"=link\n" ~ 175 ">\"testfile\"=any,application/x-executable,executable\n"; 176 177 void sink(TreeMagicEntry t) { 178 assert(t.mimeType == "x-content/video-bluray"); 179 assert(t.magic.weight == 50); 180 assert(t.magic.matches.length == 2); 181 182 auto submatch = t.magic.matches[0]; 183 assert(submatch.path == "BDAV"); 184 assert(submatch.type == TreeMatch.Type.directory); 185 assert(submatch.nonEmpty); 186 assert(submatch.submatches.length == 2); 187 188 auto otherSubmatch = t.magic.matches[1]; 189 assert(otherSubmatch.path == "testfile"); 190 assert(otherSubmatch.type == TreeMatch.Type.any); 191 assert(otherSubmatch.executable); 192 assert(otherSubmatch.mimeType == "application/x-executable"); 193 194 auto autorun = submatch.submatches[0]; 195 assert(autorun.path == "autorun"); 196 assert(autorun.submatches.length == 0); 197 assert(autorun.type == TreeMatch.Type.file); 198 assert(autorun.executable); 199 assert(autorun.matchCase); 200 201 auto testlink = submatch.submatches[1]; 202 assert(testlink.path == "testlink"); 203 assert(testlink.type == TreeMatch.Type.link); 204 } 205 treeMagicFileReader(data, &sink); 206 207 void emptySink(TreeMagicEntry t) { 208 209 } 210 assertThrown!TreeMagicFileException(treeMagicFileReader("MIME-wrong-magic", &emptySink)); 211 212 data = "MIME-TreeMagic\0\n[50:x-content/video-bluray]\n" ~ 213 ">\"BDAV\"=unknown\n"; 214 assertThrown!TreeMagicFileException(treeMagicFileReader(data, &emptySink)); 215 216 data = "MIME-TreeMagic\0\n[50:x-content/video-bluray]\n" ~ 217 ">\"BDAV\"=directory,unexpected\n"; 218 assertThrown!TreeMagicFileException(treeMagicFileReader(data, &emptySink)); 219 }