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 | #include "qmimeglobpattern_p.h" |
41 | |
42 | #if QT_CONFIG(regularexpression) |
43 | #include <QRegularExpression> |
44 | #endif |
45 | #include <QStringList> |
46 | #include <QDebug> |
47 | |
48 | QT_BEGIN_NAMESPACE |
49 | |
50 | /*! |
51 | \internal |
52 | \class QMimeGlobMatchResult |
53 | \inmodule QtCore |
54 | \brief The QMimeGlobMatchResult class accumulates results from glob matching. |
55 | |
56 | Handles glob weights, and preferring longer matches over shorter matches. |
57 | */ |
58 | |
59 | void QMimeGlobMatchResult::addMatch(const QString &mimeType, int weight, const QString &pattern, int knownSuffixLength) |
60 | { |
61 | if (m_allMatchingMimeTypes.contains(str: mimeType)) |
62 | return; |
63 | // Is this a lower-weight pattern than the last match? Skip this match then. |
64 | if (weight < m_weight) { |
65 | m_allMatchingMimeTypes.append(t: mimeType); |
66 | return; |
67 | } |
68 | bool replace = weight > m_weight; |
69 | if (!replace) { |
70 | // Compare the length of the match |
71 | if (pattern.length() < m_matchingPatternLength) |
72 | return; // too short, ignore |
73 | else if (pattern.length() > m_matchingPatternLength) { |
74 | // longer: clear any previous match (like *.bz2, when pattern is *.tar.bz2) |
75 | replace = true; |
76 | } |
77 | } |
78 | if (replace) { |
79 | m_matchingMimeTypes.clear(); |
80 | // remember the new "longer" length |
81 | m_matchingPatternLength = pattern.length(); |
82 | m_weight = weight; |
83 | } |
84 | if (!m_matchingMimeTypes.contains(str: mimeType)) { |
85 | m_matchingMimeTypes.append(t: mimeType); |
86 | m_allMatchingMimeTypes.append(t: mimeType); |
87 | m_knownSuffixLength = knownSuffixLength; |
88 | } |
89 | } |
90 | |
91 | QMimeGlobPattern::PatternType QMimeGlobPattern::detectPatternType(const QString &pattern) const |
92 | { |
93 | const int patternLength = pattern.length(); |
94 | if (!patternLength) |
95 | return OtherPattern; |
96 | |
97 | const int starCount = pattern.count(c: QLatin1Char('*')); |
98 | const bool hasSquareBracket = pattern.indexOf(c: QLatin1Char('[')) != -1; |
99 | const bool hasQuestionMark = pattern.indexOf(c: QLatin1Char('?')) != -1; |
100 | |
101 | if (!hasSquareBracket && !hasQuestionMark) { |
102 | if (starCount == 1) { |
103 | // Patterns like "*~", "*.extension" |
104 | if (pattern.at(i: 0) == QLatin1Char('*')) |
105 | return SuffixPattern; |
106 | // Patterns like "README*" (well this is currently the only one like that...) |
107 | if (pattern.at(i: patternLength - 1) == QLatin1Char('*')) |
108 | return PrefixPattern; |
109 | } else if (starCount == 0) { |
110 | // Names without any wildcards like "README" |
111 | return LiteralPattern; |
112 | } |
113 | } |
114 | |
115 | if (pattern == QLatin1String("[0-9][0-9][0-9].vdr" )) |
116 | return VdrPattern; |
117 | |
118 | if (pattern == QLatin1String("*.anim[1-9j]" )) |
119 | return AnimPattern; |
120 | |
121 | return OtherPattern; |
122 | } |
123 | |
124 | |
125 | /*! |
126 | \internal |
127 | \class QMimeGlobPattern |
128 | \inmodule QtCore |
129 | \brief The QMimeGlobPattern class contains the glob pattern for file names for MIME type matching. |
130 | |
131 | \sa QMimeType, QMimeDatabase, QMimeMagicRuleMatcher, QMimeMagicRule |
132 | */ |
133 | |
134 | bool QMimeGlobPattern::matchFileName(const QString &inputFileName) const |
135 | { |
136 | // "Applications MUST match globs case-insensitively, except when the case-sensitive |
137 | // attribute is set to true." |
138 | // The constructor takes care of putting case-insensitive patterns in lowercase. |
139 | const QString fileName = m_caseSensitivity == Qt::CaseInsensitive |
140 | ? inputFileName.toLower() : inputFileName; |
141 | |
142 | const int patternLength = m_pattern.length(); |
143 | if (!patternLength) |
144 | return false; |
145 | const int fileNameLength = fileName.length(); |
146 | |
147 | switch (m_patternType) { |
148 | case SuffixPattern: { |
149 | if (fileNameLength + 1 < patternLength) |
150 | return false; |
151 | |
152 | const QChar *c1 = m_pattern.unicode() + patternLength - 1; |
153 | const QChar *c2 = fileName.unicode() + fileNameLength - 1; |
154 | int cnt = 1; |
155 | while (cnt < patternLength && *c1-- == *c2--) |
156 | ++cnt; |
157 | return cnt == patternLength; |
158 | } |
159 | case PrefixPattern: { |
160 | if (fileNameLength + 1 < patternLength) |
161 | return false; |
162 | |
163 | const QChar *c1 = m_pattern.unicode(); |
164 | const QChar *c2 = fileName.unicode(); |
165 | int cnt = 1; |
166 | while (cnt < patternLength && *c1++ == *c2++) |
167 | ++cnt; |
168 | return cnt == patternLength; |
169 | } |
170 | case LiteralPattern: |
171 | return (m_pattern == fileName); |
172 | case VdrPattern: // "[0-9][0-9][0-9].vdr" case |
173 | return fileNameLength == 7 |
174 | && fileName.at(i: 0).isDigit() && fileName.at(i: 1).isDigit() && fileName.at(i: 2).isDigit() |
175 | && QStringView{fileName}.mid(pos: 3, n: 4) == QLatin1String(".vdr" ); |
176 | case AnimPattern: { // "*.anim[1-9j]" case |
177 | if (fileNameLength < 6) |
178 | return false; |
179 | const QChar lastChar = fileName.at(i: fileNameLength - 1); |
180 | const bool lastCharOK = (lastChar.isDigit() && lastChar != QLatin1Char('0')) |
181 | || lastChar == QLatin1Char('j'); |
182 | return lastCharOK && QStringView{fileName}.mid(pos: fileNameLength - 6, n: 5) == QLatin1String(".anim" ); |
183 | } |
184 | case OtherPattern: |
185 | // Other fallback patterns: slow but correct method |
186 | #if QT_CONFIG(regularexpression) |
187 | QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(str: m_pattern)); |
188 | return rx.match(subject: fileName).hasMatch(); |
189 | #else |
190 | return false; |
191 | #endif |
192 | } |
193 | return false; |
194 | } |
195 | |
196 | static bool isSimplePattern(const QString &pattern) |
197 | { |
198 | // starts with "*.", has no other '*' |
199 | return pattern.lastIndexOf(c: QLatin1Char('*')) == 0 |
200 | && pattern.length() > 1 |
201 | && pattern.at(i: 1) == QLatin1Char('.') // (other dots are OK, like *.tar.bz2) |
202 | // and contains no other special character |
203 | && !pattern.contains(c: QLatin1Char('?')) |
204 | && !pattern.contains(c: QLatin1Char('[')) |
205 | ; |
206 | } |
207 | |
208 | static bool isFastPattern(const QString &pattern) |
209 | { |
210 | // starts with "*.", has no other '*' and no other '.' |
211 | return pattern.lastIndexOf(c: QLatin1Char('*')) == 0 |
212 | && pattern.lastIndexOf(c: QLatin1Char('.')) == 1 |
213 | // and contains no other special character |
214 | && !pattern.contains(c: QLatin1Char('?')) |
215 | && !pattern.contains(c: QLatin1Char('[')) |
216 | ; |
217 | } |
218 | |
219 | void QMimeAllGlobPatterns::addGlob(const QMimeGlobPattern &glob) |
220 | { |
221 | const QString &pattern = glob.pattern(); |
222 | Q_ASSERT(!pattern.isEmpty()); |
223 | |
224 | // Store each patterns into either m_fastPatternDict (*.txt, *.html etc. with default weight 50) |
225 | // or for the rest, like core.*, *.tar.bz2, *~, into highWeightPatternOffset (>50) |
226 | // or lowWeightPatternOffset (<=50) |
227 | |
228 | if (glob.weight() == 50 && isFastPattern(pattern) && !glob.isCaseSensitive()) { |
229 | // The bulk of the patterns is *.foo with weight 50 --> those go into the fast patterns hash. |
230 | const QString extension = pattern.mid(position: 2).toLower(); |
231 | QStringList &patterns = m_fastPatterns[extension]; // find or create |
232 | if (!patterns.contains(str: glob.mimeType())) |
233 | patterns.append(t: glob.mimeType()); |
234 | } else { |
235 | if (glob.weight() > 50) { |
236 | if (!m_highWeightGlobs.hasPattern(mimeType: glob.mimeType(), pattern: glob.pattern())) |
237 | m_highWeightGlobs.append(t: glob); |
238 | } else { |
239 | if (!m_lowWeightGlobs.hasPattern(mimeType: glob.mimeType(), pattern: glob.pattern())) |
240 | m_lowWeightGlobs.append(t: glob); |
241 | } |
242 | } |
243 | } |
244 | |
245 | void QMimeAllGlobPatterns::removeMimeType(const QString &mimeType) |
246 | { |
247 | for (auto &x : m_fastPatterns) |
248 | x.removeAll(t: mimeType); |
249 | m_highWeightGlobs.removeMimeType(mimeType); |
250 | m_lowWeightGlobs.removeMimeType(mimeType); |
251 | } |
252 | |
253 | void QMimeGlobPatternList::match(QMimeGlobMatchResult &result, |
254 | const QString &fileName) const |
255 | { |
256 | |
257 | QMimeGlobPatternList::const_iterator it = this->constBegin(); |
258 | const QMimeGlobPatternList::const_iterator endIt = this->constEnd(); |
259 | for (; it != endIt; ++it) { |
260 | const QMimeGlobPattern &glob = *it; |
261 | if (glob.matchFileName(inputFileName: fileName)) { |
262 | const QString pattern = glob.pattern(); |
263 | const int suffixLen = isSimplePattern(pattern) ? pattern.length() - 2 : 0; |
264 | result.addMatch(mimeType: glob.mimeType(), weight: glob.weight(), pattern, knownSuffixLength: suffixLen); |
265 | } |
266 | } |
267 | } |
268 | |
269 | void QMimeAllGlobPatterns::matchingGlobs(const QString &fileName, QMimeGlobMatchResult &result) const |
270 | { |
271 | // First try the high weight matches (>50), if any. |
272 | m_highWeightGlobs.match(result, fileName); |
273 | |
274 | // Now use the "fast patterns" dict, for simple *.foo patterns with weight 50 |
275 | // (which is most of them, so this optimization is definitely worth it) |
276 | const int lastDot = fileName.lastIndexOf(c: QLatin1Char('.')); |
277 | if (lastDot != -1) { // if no '.', skip the extension lookup |
278 | const int ext_len = fileName.length() - lastDot - 1; |
279 | const QString simpleExtension = fileName.right(n: ext_len).toLower(); |
280 | // (toLower because fast patterns are always case-insensitive and saved as lowercase) |
281 | |
282 | const QStringList matchingMimeTypes = m_fastPatterns.value(akey: simpleExtension); |
283 | const QString simplePattern = QLatin1String("*." ) + simpleExtension; |
284 | for (const QString &mime : matchingMimeTypes) |
285 | result.addMatch(mimeType: mime, weight: 50, pattern: simplePattern, knownSuffixLength: simpleExtension.size()); |
286 | // Can't return yet; *.tar.bz2 has to win over *.bz2, so we need the low-weight mimetypes anyway, |
287 | // at least those with weight 50. |
288 | } |
289 | |
290 | // Finally, try the low weight matches (<=50) |
291 | m_lowWeightGlobs.match(result, fileName); |
292 | } |
293 | |
294 | void QMimeAllGlobPatterns::clear() |
295 | { |
296 | m_fastPatterns.clear(); |
297 | m_highWeightGlobs.clear(); |
298 | m_lowWeightGlobs.clear(); |
299 | } |
300 | |
301 | QT_END_NAMESPACE |
302 | |