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

source code of qtbase/src/corelib/mimetypes/qmimetypeparser.cpp