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 }