1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtCore module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #define QT_NO_CAST_FROM_ASCII |
41 | |
42 | #include "qmimetypeparser_p.h" |
43 | |
44 | #include "qmimetype_p.h" |
45 | #include "qmimemagicrulematcher_p.h" |
46 | |
47 | #include <QtCore/QCoreApplication> |
48 | #include <QtCore/QDebug> |
49 | #include <QtCore/QDir> |
50 | #include <QtCore/QXmlStreamReader> |
51 | #include <QtCore/QXmlStreamWriter> |
52 | #include <QtCore/QStack> |
53 | |
54 | QT_BEGIN_NAMESPACE |
55 | |
56 | // XML tags in MIME files |
57 | static const char mimeInfoTagC[] = "mime-info" ; |
58 | static const char mimeTypeTagC[] = "mime-type" ; |
59 | static const char mimeTypeAttributeC[] = "type" ; |
60 | static const char subClassTagC[] = "sub-class-of" ; |
61 | static const char [] = "comment" ; |
62 | static const char genericIconTagC[] = "generic-icon" ; |
63 | static const char iconTagC[] = "icon" ; |
64 | static const char nameAttributeC[] = "name" ; |
65 | static const char globTagC[] = "glob" ; |
66 | static const char globDeleteAllTagC[] = "glob-deleteall" ; |
67 | static const char aliasTagC[] = "alias" ; |
68 | static const char patternAttributeC[] = "pattern" ; |
69 | static const char weightAttributeC[] = "weight" ; |
70 | static const char caseSensitiveAttributeC[] = "case-sensitive" ; |
71 | static const char localeAttributeC[] = "xml:lang" ; |
72 | |
73 | static const char magicTagC[] = "magic" ; |
74 | static const char priorityAttributeC[] = "priority" ; |
75 | |
76 | static const char matchTagC[] = "match" ; |
77 | static const char matchValueAttributeC[] = "value" ; |
78 | static const char matchTypeAttributeC[] = "type" ; |
79 | static const char matchOffsetAttributeC[] = "offset" ; |
80 | static const char matchMaskAttributeC[] = "mask" ; |
81 | |
82 | /*! |
83 | \class QMimeTypeParser |
84 | \inmodule QtCore |
85 | \internal |
86 | \brief The QMimeTypeParser class parses MIME types, and builds a MIME database hierarchy by adding to QMimeDatabase. |
87 | |
88 | Populates QMimeDataBase |
89 | |
90 | \sa QMimeDatabase, QMimeMagicRuleMatcher, MagicRule, MagicStringRule, MagicByteRule, GlobPattern |
91 | \sa QMimeTypeParser |
92 | */ |
93 | |
94 | /*! |
95 | \class QMimeTypeParserBase |
96 | \inmodule QtCore |
97 | \internal |
98 | \brief The QMimeTypeParserBase class parses for a sequence of <mime-type> in a generic way. |
99 | |
100 | Calls abstract handler function process for QMimeType it finds. |
101 | |
102 | \sa QMimeDatabase, QMimeMagicRuleMatcher, MagicRule, MagicStringRule, MagicByteRule, GlobPattern |
103 | \sa QMimeTypeParser |
104 | */ |
105 | |
106 | /*! |
107 | \fn virtual bool QMimeTypeParserBase::process(const QMimeType &t, QString *errorMessage) = 0; |
108 | Overwrite to process the sequence of parsed data |
109 | */ |
110 | |
111 | QMimeTypeParserBase::ParseState QMimeTypeParserBase::nextState(ParseState currentState, const QStringRef &startElement) |
112 | { |
113 | switch (currentState) { |
114 | case ParseBeginning: |
115 | if (startElement == QLatin1String(mimeInfoTagC)) |
116 | return ParseMimeInfo; |
117 | if (startElement == QLatin1String(mimeTypeTagC)) |
118 | return ParseMimeType; |
119 | return ParseError; |
120 | case ParseMimeInfo: |
121 | return startElement == QLatin1String(mimeTypeTagC) ? ParseMimeType : ParseError; |
122 | case ParseMimeType: |
123 | case ParseComment: |
124 | case ParseGenericIcon: |
125 | case ParseIcon: |
126 | case ParseGlobPattern: |
127 | case ParseGlobDeleteAll: |
128 | case ParseSubClass: |
129 | case ParseAlias: |
130 | case ParseOtherMimeTypeSubTag: |
131 | case ParseMagicMatchRule: |
132 | if (startElement == QLatin1String(mimeTypeTagC)) // Sequence of <mime-type> |
133 | return ParseMimeType; |
134 | if (startElement == QLatin1String(commentTagC )) |
135 | return ParseComment; |
136 | if (startElement == QLatin1String(genericIconTagC)) |
137 | return ParseGenericIcon; |
138 | if (startElement == QLatin1String(iconTagC)) |
139 | return ParseIcon; |
140 | if (startElement == QLatin1String(globTagC)) |
141 | return ParseGlobPattern; |
142 | if (startElement == QLatin1String(globDeleteAllTagC)) |
143 | return ParseGlobDeleteAll; |
144 | if (startElement == QLatin1String(subClassTagC)) |
145 | return ParseSubClass; |
146 | if (startElement == QLatin1String(aliasTagC)) |
147 | return ParseAlias; |
148 | if (startElement == QLatin1String(magicTagC)) |
149 | return ParseMagic; |
150 | if (startElement == QLatin1String(matchTagC)) |
151 | return ParseMagicMatchRule; |
152 | return ParseOtherMimeTypeSubTag; |
153 | case ParseMagic: |
154 | if (startElement == QLatin1String(matchTagC)) |
155 | return ParseMagicMatchRule; |
156 | break; |
157 | case ParseError: |
158 | break; |
159 | } |
160 | return ParseError; |
161 | } |
162 | |
163 | // Parse int number from an (attribute) string |
164 | bool QMimeTypeParserBase::parseNumber(const QStringRef &n, int *target, QString *errorMessage) |
165 | { |
166 | bool ok; |
167 | *target = n.toInt(ok: &ok); |
168 | if (Q_UNLIKELY(!ok)) { |
169 | if (errorMessage) |
170 | *errorMessage = QLatin1String("Not a number '" ) + n + QLatin1String("'." ); |
171 | return false; |
172 | } |
173 | return true; |
174 | } |
175 | |
176 | #ifndef QT_NO_XMLSTREAMREADER |
177 | struct CreateMagicMatchRuleResult { |
178 | QString errorMessage; // must be first |
179 | QMimeMagicRule rule; |
180 | |
181 | CreateMagicMatchRuleResult(const QStringRef &type, const QStringRef &value, const QStringRef &offsets, const QStringRef &mask) |
182 | : errorMessage(), rule(type.toString(), value.toUtf8(), offsets.toString(), mask.toLatin1(), &errorMessage) |
183 | { |
184 | |
185 | } |
186 | }; |
187 | |
188 | static CreateMagicMatchRuleResult createMagicMatchRule(const QXmlStreamAttributes &atts) |
189 | { |
190 | const QStringRef type = atts.value(qualifiedName: QLatin1String(matchTypeAttributeC)); |
191 | const QStringRef value = atts.value(qualifiedName: QLatin1String(matchValueAttributeC)); |
192 | const QStringRef offsets = atts.value(qualifiedName: QLatin1String(matchOffsetAttributeC)); |
193 | const QStringRef mask = atts.value(qualifiedName: QLatin1String(matchMaskAttributeC)); |
194 | return CreateMagicMatchRuleResult(type, value, offsets, mask); |
195 | } |
196 | #endif |
197 | |
198 | bool QMimeTypeParserBase::parse(QIODevice *dev, const QString &fileName, QString *errorMessage) |
199 | { |
200 | #ifdef QT_NO_XMLSTREAMREADER |
201 | Q_UNUSED(dev); |
202 | if (errorMessage) |
203 | *errorMessage = QString::fromLatin1("QXmlStreamReader is not available, cannot parse '%1'." ).arg(fileName); |
204 | return false; |
205 | #else |
206 | QMimeTypePrivate data; |
207 | data.loaded = true; |
208 | int priority = 50; |
209 | QStack<QMimeMagicRule *> currentRules; // stack for the nesting of rules |
210 | QList<QMimeMagicRule> rules; // toplevel rules |
211 | QXmlStreamReader reader(dev); |
212 | ParseState ps = ParseBeginning; |
213 | while (!reader.atEnd()) { |
214 | switch (reader.readNext()) { |
215 | case QXmlStreamReader::StartElement: { |
216 | ps = nextState(currentState: ps, startElement: reader.name()); |
217 | const QXmlStreamAttributes atts = reader.attributes(); |
218 | switch (ps) { |
219 | case ParseMimeType: { // start parsing a MIME type name |
220 | const QString name = atts.value(qualifiedName: QLatin1String(mimeTypeAttributeC)).toString(); |
221 | if (name.isEmpty()) { |
222 | reader.raiseError(QStringLiteral("Missing 'type'-attribute" )); |
223 | } else { |
224 | data.name = name; |
225 | } |
226 | } |
227 | break; |
228 | case ParseGenericIcon: |
229 | data.genericIconName = atts.value(qualifiedName: QLatin1String(nameAttributeC)).toString(); |
230 | break; |
231 | case ParseIcon: |
232 | data.iconName = atts.value(qualifiedName: QLatin1String(nameAttributeC)).toString(); |
233 | break; |
234 | case ParseGlobPattern: { |
235 | const QString pattern = atts.value(qualifiedName: QLatin1String(patternAttributeC)).toString(); |
236 | unsigned weight = atts.value(qualifiedName: QLatin1String(weightAttributeC)).toInt(); |
237 | const bool caseSensitive = atts.value(qualifiedName: QLatin1String(caseSensitiveAttributeC)) == QLatin1String("true" ); |
238 | |
239 | if (weight == 0) |
240 | weight = QMimeGlobPattern::DefaultWeight; |
241 | |
242 | Q_ASSERT(!data.name.isEmpty()); |
243 | const QMimeGlobPattern glob(pattern, data.name, weight, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); |
244 | if (!process(t: glob, errorMessage)) // for actual glob matching |
245 | return false; |
246 | data.addGlobPattern(pattern); // just for QMimeType::globPatterns() |
247 | } |
248 | break; |
249 | case ParseGlobDeleteAll: |
250 | data.globPatterns.clear(); |
251 | break; |
252 | case ParseSubClass: { |
253 | const QString inheritsFrom = atts.value(qualifiedName: QLatin1String(mimeTypeAttributeC)).toString(); |
254 | if (!inheritsFrom.isEmpty()) |
255 | processParent(child: data.name, parent: inheritsFrom); |
256 | } |
257 | break; |
258 | case ParseComment: { |
259 | // comments have locale attributes. |
260 | QString locale = atts.value(qualifiedName: QLatin1String(localeAttributeC)).toString(); |
261 | const QString = reader.readElementText(); |
262 | if (locale.isEmpty()) |
263 | locale = QString::fromLatin1(str: "default" ); |
264 | data.localeComments.insert(akey: locale, avalue: comment); |
265 | } |
266 | break; |
267 | case ParseAlias: { |
268 | const QString alias = atts.value(qualifiedName: QLatin1String(mimeTypeAttributeC)).toString(); |
269 | if (!alias.isEmpty()) |
270 | processAlias(alias, name: data.name); |
271 | } |
272 | break; |
273 | case ParseMagic: { |
274 | priority = 50; |
275 | const QStringRef priorityS = atts.value(qualifiedName: QLatin1String(priorityAttributeC)); |
276 | if (!priorityS.isEmpty()) { |
277 | if (!parseNumber(n: priorityS, target: &priority, errorMessage)) |
278 | return false; |
279 | |
280 | } |
281 | currentRules.clear(); |
282 | //qDebug() << "MAGIC start for mimetype" << data.name; |
283 | } |
284 | break; |
285 | case ParseMagicMatchRule: { |
286 | auto result = createMagicMatchRule(atts); |
287 | if (Q_UNLIKELY(!result.rule.isValid())) |
288 | qWarning(msg: "QMimeDatabase: Error parsing %ls\n%ls" , |
289 | qUtf16Printable(fileName), qUtf16Printable(result.errorMessage)); |
290 | QList<QMimeMagicRule> *ruleList; |
291 | if (currentRules.isEmpty()) |
292 | ruleList = &rules; |
293 | else // nest this rule into the proper parent |
294 | ruleList = ¤tRules.top()->m_subMatches; |
295 | ruleList->append(t: std::move(result.rule)); |
296 | //qDebug() << " MATCH added. Stack size was" << currentRules.size(); |
297 | currentRules.push(t: &ruleList->last()); |
298 | break; |
299 | } |
300 | case ParseError: |
301 | reader.raiseError(message: QLatin1String("Unexpected element <" ) + reader.name() + QLatin1Char('>')); |
302 | break; |
303 | default: |
304 | break; |
305 | } |
306 | } |
307 | break; |
308 | // continue switch QXmlStreamReader::Token... |
309 | case QXmlStreamReader::EndElement: // Finished element |
310 | { |
311 | const QStringRef elementName = reader.name(); |
312 | if (elementName == QLatin1String(mimeTypeTagC)) { |
313 | if (!process(t: QMimeType(data), errorMessage)) |
314 | return false; |
315 | data.clear(); |
316 | } else if (elementName == QLatin1String(matchTagC)) { |
317 | // Closing a <match> tag, pop stack |
318 | currentRules.pop(); |
319 | //qDebug() << " MATCH closed. Stack size is now" << currentRules.size(); |
320 | } else if (elementName == QLatin1String(magicTagC)) { |
321 | //qDebug() << "MAGIC ended, we got" << rules.count() << "rules, with prio" << priority; |
322 | // Finished a <magic> sequence |
323 | QMimeMagicRuleMatcher ruleMatcher(data.name, priority); |
324 | ruleMatcher.addRules(rules); |
325 | processMagicMatcher(matcher: ruleMatcher); |
326 | rules.clear(); |
327 | } |
328 | break; |
329 | } |
330 | default: |
331 | break; |
332 | } |
333 | } |
334 | |
335 | if (Q_UNLIKELY(reader.hasError())) { |
336 | if (errorMessage) { |
337 | *errorMessage = QString::asprintf(format: "An error has been encountered at line %lld of %ls: %ls:" , |
338 | reader.lineNumber(), |
339 | qUtf16Printable(fileName), |
340 | qUtf16Printable(reader.errorString())); |
341 | } |
342 | return false; |
343 | } |
344 | |
345 | return true; |
346 | #endif //QT_NO_XMLSTREAMREADER |
347 | } |
348 | |
349 | QT_END_NAMESPACE |
350 | |