1 /**
2  * Common functions and constants to work with MIME types.
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.common;
12 
13 // should be enough for magic rules
14 package enum dataSizeToRead = 1024 * 4;
15 
16 private {
17     import std.typecons;
18     import std.traits;
19     import std.range;
20 }
21 
22 /**
23  * Parse MIME type name into pair of media and subtype strings.
24  * Returns: Tuple of media and subtype strings or pair of empty strings if could not parse name.
25  */
26 @nogc @trusted auto parseMimeTypeName(String)(scope return String name) pure nothrow if (isSomeString!String && is(ElementEncodingType!String : char))
27 {
28     alias Tuple!(String, "media", String, "subtype") MimeTypeName;
29 
30     String media;
31     String subtype;
32 
33     size_t i;
34     for (i=0; i<name.length; ++i) {
35         if (name[i] == '/') {
36             media = name[0..i];
37             subtype = name[i+1..$];
38             break;
39         }
40     }
41 
42     return MimeTypeName(media, subtype);
43 }
44 
45 ///
46 unittest
47 {
48     auto t = parseMimeTypeName("text/plain");
49     assert(t.media == "text" && t.subtype == "plain");
50 
51     t = parseMimeTypeName("not mime type");
52     assert(t.media == string.init && t.subtype == string.init);
53 }
54 
55 private @nogc @trusted bool allSymbolsAreValid(scope const(char)[] name) nothrow pure
56 {
57     import std.ascii : isAlpha, isDigit;
58     for (size_t i=0; i<name.length; ++i) {
59         char c = name[i];
60         if (!(c.isAlpha || c.isDigit || c == '-' || c == '+' || c == '.' || c == '_')) {
61             return false;
62         }
63     }
64     return true;
65 }
66 
67 /**
68  * Check if name is valid MIME type name.
69  */
70 @nogc @safe bool isValidMimeTypeName(scope const(char)[] name) nothrow pure
71 {
72     auto t = parseMimeTypeName(name);
73     return t.media.length && t.subtype.length && allSymbolsAreValid(t.media) && allSymbolsAreValid(t.subtype);
74 }
75 
76 ///
77 unittest
78 {
79     assert( isValidMimeTypeName("text/plain"));
80     assert( isValidMimeTypeName("text/plain2"));
81     assert( isValidMimeTypeName("text/vnd.type"));
82     assert( isValidMimeTypeName("x-scheme-handler/http"));
83     assert(!isValidMimeTypeName("not mime type"));
84     assert(!isValidMimeTypeName("not()/valid"));
85     assert(!isValidMimeTypeName("not/valid{}"));
86     assert(!isValidMimeTypeName("text/"));
87     assert(!isValidMimeTypeName("/plain"));
88     assert(!isValidMimeTypeName("/"));
89 }
90 
91 /**
92  * Default icon for MIME type.
93  * Returns: mimeType with '/' replaces with '-' or null if mimeType is not valid MIME type name.
94  */
95 @safe string defaultIconName(scope string mimeType) nothrow pure
96 {
97     auto t = parseMimeTypeName(mimeType);
98     if (t.media.length && t.subtype.length) {
99         return t.media ~ "-" ~ t.subtype;
100     }
101     return null;
102 }
103 
104 ///
105 unittest
106 {
107     assert(defaultIconName("text/plain") == "text-plain");
108     assert(defaultIconName("not mime type") == string.init);
109 }
110 
111 /**
112  * Default generic icon for MIME type.
113  * Returns: media-x-generic where media is parsed from mimeType or null if mimeType is not valid MIME type name.
114  */
115 @trusted string defaultGenericIconName(scope string mimeType) nothrow pure
116 {
117     auto t = parseMimeTypeName(mimeType);
118     if (t.media.length) {
119         return t.media ~ "-x-generic";
120     }
121     return null;
122 }
123 
124 ///
125 unittest
126 {
127     assert(defaultGenericIconName("image/type") == "image-x-generic");
128     assert(defaultGenericIconName("not mime type") == string.init);
129 }
130 
131 /// Default glob pattern weight to use when it's not explicitly provided.
132 enum uint defaultGlobWeight = 50;
133 /// Maximum glob pattern weight as defined by spec.
134 enum uint maximumGlobWeight = 100;
135 
136 /// Default magic rule priority to use when it's not explicitly provided.
137 enum uint defaultMatchWeight = 50;
138 /// Maximum magic rule priority as defined by spec.
139 enum uint maximumMatchWeight = 100;
140 
141 /**
142  * Check is pattern is __NOGLOBS__. This means glob patterns from the less preferable MIME paths should be ignored.
143  */
144 @nogc @safe bool isNoGlobs(T)(scope const(T)[] pattern) pure nothrow if (is(T == char) || is(T == ubyte) || is(T == byte) || is(T == void)) {
145     return cast(const(ubyte)[])pattern == cast(const(ubyte)[])"__NOGLOBS__";
146 }
147 
148 ///
149 unittest
150 {
151     assert(isNoGlobs("__NOGLOBS__"));
152     assert(!isNoGlobs("someglob"));
153 }
154 
155 /**
156  * Check if value is __NOMAGIC__. This means magic rules from the less preferable MIME paths should be ignored.
157  */
158 @nogc @trusted bool isNoMagic(T)(scope const(T)[] value) pure nothrow if (is(T == char) || is(T == ubyte) || is(T == byte) || is(T == void)) {
159     return cast(const(ubyte)[])value == cast(const(ubyte)[])"__NOMAGIC__";
160 }
161 
162 ///
163 unittest
164 {
165     assert(isNoMagic("__NOMAGIC__"));
166     assert(!isNoMagic("somemagic"));
167 }
168 
169 /**
170  * Get implicit parent type if mimeType. This is text/plain for all text/* types
171  * and application/octet-stream for all streamable types.
172  * Returns: text/plain for text-based types, application/octet-stream for streamable types, null otherwise.
173  * Note: text/plain and application/octet-stream are not considered as parents of their own.
174  */
175 @safe string implicitParent(scope const(char)[] mimeType) nothrow pure
176 {
177     if (mimeType == "text/plain" || mimeType == "application/octet-stream") {
178         return null;
179     }
180 
181     auto t = parseMimeTypeName(mimeType);
182     if (t.media == "text") {
183         return "text/plain";
184     } else if ( t.media == "image" || t.media == "audio" ||
185                 t.media == "video" || t.media == "application")
186     {
187         return "application/octet-stream";
188     }
189     return null;
190 }
191 
192 ///
193 unittest
194 {
195     assert(implicitParent("text/hmtl") == "text/plain");
196     assert(implicitParent("text/plain") == null);
197 
198     assert(implicitParent("image/png") == "application/octet-stream");
199     assert(implicitParent("audio/ogg") == "application/octet-stream");
200     assert(implicitParent("video/mpeg") == "application/octet-stream");
201     assert(implicitParent("application/xml") == "application/octet-stream");
202     assert(implicitParent("application/octet-stream") == null);
203 
204     assert(implicitParent("inode/directory") == null);
205     assert(implicitParent("x-content/unix-software") == null);
206 
207     assert(implicitParent("not a mimetype") == null);
208 }