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 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | using namespace Qt::StringLiterals; |
21 | |
22 | // XML tags in MIME files |
23 | static const char mimeInfoTagC[] = "mime-info" ; |
24 | static const char mimeTypeTagC[] = "mime-type" ; |
25 | static const char mimeTypeAttributeC[] = "type" ; |
26 | static const char subClassTagC[] = "sub-class-of" ; |
27 | static const char [] = "comment" ; |
28 | static const char genericIconTagC[] = "generic-icon" ; |
29 | static const char iconTagC[] = "icon" ; |
30 | static const char nameAttributeC[] = "name" ; |
31 | static const char globTagC[] = "glob" ; |
32 | static const char globDeleteAllTagC[] = "glob-deleteall" ; |
33 | static const char aliasTagC[] = "alias" ; |
34 | static const char patternAttributeC[] = "pattern" ; |
35 | static const char weightAttributeC[] = "weight" ; |
36 | static const char caseSensitiveAttributeC[] = "case-sensitive" ; |
37 | static const char localeAttributeC[] = "xml:lang" ; |
38 | |
39 | static const char magicTagC[] = "magic" ; |
40 | static const char priorityAttributeC[] = "priority" ; |
41 | |
42 | static const char matchTagC[] = "match" ; |
43 | static const char matchValueAttributeC[] = "value" ; |
44 | static const char matchTypeAttributeC[] = "type" ; |
45 | static const char matchOffsetAttributeC[] = "offset" ; |
46 | static 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 | |
77 | QMimeTypeParserBase::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 |
130 | bool 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) |
143 | struct 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 | |
155 | static 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 | |
165 | bool 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 = 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 = ¤tRules.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 | |
317 | QT_END_NAMESPACE |
318 | |