Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qfileselector.cpp
Go to the documentation of this file.
1// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
2// Copyright (C) 2016 Intel Corporation.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:critical reason:file-selection
5
8
9#include <QtCore/QFile>
10#include <QtCore/QDir>
11#include <QtCore/QMutex>
12#include <QtCore/private/qlocking_p.h>
13#include <QtCore/QUrl>
14#include <QtCore/QFileInfo>
15#include <QtCore/QLocale>
16#include <QtCore/QDebug>
17
19
20using namespace Qt::StringLiterals;
21
22//Environment variable to allow tooling full control of file selectors
23static const char env_override[] = "QT_NO_BUILTIN_SELECTORS";
24
26Q_CONSTINIT static QBasicMutex sharedDataMutex;
27
28QFileSelectorPrivate::QFileSelectorPrivate()
29 : QObjectPrivate()
30{
31}
32
33/*!
34 \class QFileSelector
35 \inmodule QtCore
36 \brief QFileSelector provides a convenient way of selecting file variants.
37 \since 5.2
38
39 QFileSelector is a convenience for selecting file variants based on platform or device
40 characteristics. This allows you to develop and deploy one codebase containing all the
41 different variants more easily in some circumstances, such as when the correct variant cannot
42 be determined during the deploy step.
43
44 \section1 Using QFileSelector
45
46 If you always use the same file you do not need to use QFileSelector.
47
48 Consider the following example usage, where you want to use different settings files on
49 different locales. You might select code between locales like this:
50
51 \snippet code/src_corelib_io_qfileselector.cpp 0
52
53 Similarly, if you want to pick a different data file based on target platform,
54 your code might look something like this:
55 \snippet code/src_corelib_io_qfileselector.cpp 1
56
57 QFileSelector provides a convenient alternative to writing such boilerplate code, and in the
58 latter case it allows you to start using an platform-specific configuration without a recompile.
59 QFileSelector also allows for chaining of multiple selectors in a convenient way, for example
60 selecting a different file only on certain combinations of platform and locale. For example, to
61 select based on platform and/or locale, the code is as follows:
62
63 \snippet code/src_corelib_io_qfileselector.cpp 2
64
65 The files to be selected are placed in directories named with a \c'+' and a selector name. In the above
66 example you could have the platform configurations selected by placing them in the following locations:
67 \snippet code/src_corelib_io_qfileselector.cpp 3
68
69 To find selected files, QFileSelector looks in the same directory as the base file. If there are
70 any directories of the form +<selector> with an active selector, QFileSelector will prefer a file
71 with the same file name from that directory over the base file. These directories can be nested to
72 check against multiple selectors, for example:
73 \snippet code/src_corelib_io_qfileselector.cpp 4
74 With those files available, you would select a different file on the android platform,
75 but only if the locale was en_GB.
76
77 For error handling in the case no valid selectors are present, it is recommended to have a default or
78 error-handling file in the base file location even if you expect selectors to be present for all
79 deployments.
80
81 In a future version, some may be marked as deploy-time static and be moved during the
82 deployment step as an optimization. As selectors come with a performance cost, it is
83 recommended to avoid their use in circumstances involving performance-critical code.
84
85 \section1 Adding Selectors
86
87 Selectors normally available are
88 \list
89 \li platform, any of the following strings which match the platform the application is running
90 on (list not exhaustive): android, ios, osx, darwin, mac, macos, linux, qnx, unix, windows.
91 On Linux, if it can be determined, the name of the distribution too, like debian,
92 fedora or opensuse.
93 \li locale, same as QLocale().name().
94 \endlist
95
96 Further selectors will be added from the \c QT_FILE_SELECTORS environment variable, which
97 when set should be a set of comma separated selectors. Note that this variable will only be
98 read once; selectors may not update if the variable changes while the application is running.
99 The initial set of selectors are evaluated only once, on first use.
100
101 You can also add extra selectors at runtime for custom behavior. These will be used in any
102 future calls to select(). If the extra selectors list has been changed, calls to select() will
103 use the new list and may return differently.
104
105 \section1 Conflict Resolution when Multiple Selectors Apply
106
107 When multiple selectors could be applied to the same file, the first matching selector is chosen.
108 The order selectors are checked in are:
109
110 \list 1
111 \li Selectors set via setExtraSelectors(), in the order they are in the list
112 \li Selectors in the \c QT_FILE_SELECTORS environment variable, from left to right
113 \li Locale
114 \li Platform
115 \endlist
116
117 Here is an example involving multiple selectors matching at the same time. It uses platform
118 selectors, plus an extra selector named "admin" is set by the application based on user
119 credentials. The example is sorted so that the lowest matching file would be chosen if all
120 selectors were present:
121
122 \snippet code/src_corelib_io_qfileselector.cpp 5
123
124 Because extra selectors are checked before platform the \c{+admin/background.png} will be chosen
125 on Windows when the admin selector is set, and \c{+windows/background.png} will be chosen on
126 Windows when the admin selector is not set. On Linux, the \c{+admin/+linux/background.png} will be
127 chosen when admin is set, and the \c{+linux/background.png} when it is not.
128
129*/
130
131/*!
132 Create a QFileSelector instance. This instance will have the same static selectors as other
133 QFileSelector instances, but its own set of extra selectors.
134
135 If supplied, it will have the given QObject \a parent.
136*/
137QFileSelector::QFileSelector(QObject *parent)
138 : QObject(*(new QFileSelectorPrivate()), parent)
139{
140}
141
142/*!
143 Destroys this selector instance.
144*/
145QFileSelector::~QFileSelector()
146{
147}
148
149/*!
150 This function returns the selected version of the path, based on the conditions at runtime.
151 If no selectable files are present, returns the original \a filePath.
152
153 If the original file does not exist, the original \a filePath is returned. This means that you
154 must have a base file to fall back on, you cannot have only files in selectable sub-directories.
155
156 See the class overview for the selection algorithm.
157*/
158QString QFileSelector::select(const QString &filePath) const
159{
160 Q_D(const QFileSelector);
161 return d->select(filePath);
162}
163
164static bool isLocalScheme(const QString &file)
165{
166 bool local = file == "qrc"_L1;
167#ifdef Q_OS_ANDROID
168 local |= file == "assets"_L1;
169#endif
170 return local;
171}
172
173/*!
174 This is a convenience version of select operating on QUrl objects. If the scheme is not file or qrc,
175 \a filePath is returned immediately. Otherwise selection is applied to the path of \a filePath
176 and a QUrl is returned with the selected path and other QUrl parts the same as \a filePath.
177
178 See the class overview for the selection algorithm.
179*/
180QUrl QFileSelector::select(const QUrl &filePath) const
181{
182 Q_D(const QFileSelector);
183 if (!isLocalScheme(filePath.scheme()) && !filePath.isLocalFile())
184 return filePath;
185 QUrl ret(filePath);
186 if (isLocalScheme(filePath.scheme())) {
187 auto scheme = ":"_L1;
188#ifdef Q_OS_ANDROID
189 // use other scheme because ":" means "qrc" here
190 if (filePath.scheme() == "assets"_L1)
191 scheme = "assets:"_L1;
192#endif
193
194 QString equivalentPath = scheme + filePath.path();
195 QString selectedPath = d->select(equivalentPath);
196 ret.setPath(selectedPath.remove(0, scheme.size()));
197 } else {
198 // we need to store the original query and fragment, since toLocalFile() will strip it off
199 QString frag;
200 if (ret.hasFragment())
201 frag = ret.fragment();
202 QString query;
203 if (ret.hasQuery())
204 query= ret.query();
205 ret = QUrl::fromLocalFile(d->select(ret.toLocalFile()));
206 if (!frag.isNull())
207 ret.setFragment(frag);
208 if (!query.isNull())
209 ret.setQuery(query);
210 }
211 return ret;
212}
213
214QString QFileSelectorPrivate::selectionHelper(const QString &path, const QString &fileName,
215 const QStringList &selectors, QChar indicator)
216{
217 /* selectionHelper does a depth-first search of possible selected files. Because there is strict
218 selector ordering in the API, we can stop checking as soon as we find the file in a directory
219 which does not contain any other valid selector directories.
220 */
221 Q_ASSERT(path.isEmpty() || path.endsWith(u'/'));
222
223 for (const QString &s : selectors) {
224 QString prospectiveBase = path;
225 if (!indicator.isNull())
226 prospectiveBase += indicator;
227 prospectiveBase += s + u'/';
228 QStringList remainingSelectors = selectors;
229 remainingSelectors.removeAll(s);
230 if (!QDir(prospectiveBase).exists())
231 continue;
232 QString prospectiveFile = selectionHelper(prospectiveBase, fileName, remainingSelectors, indicator);
233 if (!prospectiveFile.isEmpty())
234 return prospectiveFile;
235 }
236
237 // If we reach here there were no successful files found at a lower level in this branch, so we
238 // should check this level as a potential result.
239 if (!QFile::exists(path + fileName))
240 return QString();
241 return path + fileName;
242}
243
244QString QFileSelectorPrivate::select(const QString &filePath) const
245{
246 Q_Q(const QFileSelector);
247 QFileInfo fi(filePath);
248
249 QString pathString;
250 if (auto path = fi.path(); !path.isEmpty())
251 pathString = path.endsWith(u'/') ? path : path + u'/';
252 QString ret = selectionHelper(pathString,
253 fi.fileName(), q->allSelectors());
254
255 if (!ret.isEmpty())
256 return ret;
257 return filePath;
258}
259
260/*!
261 Returns the list of extra selectors which have been added programmatically to this instance.
262*/
263QStringList QFileSelector::extraSelectors() const
264{
265 Q_D(const QFileSelector);
266 return d->extras;
267}
268
269/*!
270 Sets the \a list of extra selectors which have been added programmatically to this instance.
271
272 These selectors have priority over any which have been automatically picked up.
273*/
274void QFileSelector::setExtraSelectors(const QStringList &list)
275{
276 Q_D(QFileSelector);
277 d->extras = list;
278}
279
280/*!
281 Returns the complete, ordered list of selectors used by this instance
282*/
283QStringList QFileSelector::allSelectors() const
284{
285 Q_D(const QFileSelector);
286 const auto locker = qt_scoped_lock(sharedDataMutex);
287 QFileSelectorPrivate::updateSelectors();
288 return d->extras + sharedData->staticSelectors;
289}
290
291void QFileSelectorPrivate::updateSelectors()
292{
293 if (!sharedData->staticSelectors.isEmpty())
294 return; //Already loaded
295
296 QLatin1Char pathSep(',');
297 QStringList envSelectors = QString::fromLatin1(qgetenv("QT_FILE_SELECTORS"))
298 .split(pathSep, Qt::SkipEmptyParts);
299 if (envSelectors.size())
300 sharedData->staticSelectors << envSelectors;
301
302 if (!qEnvironmentVariableIsEmpty(env_override))
303 return;
304
305 sharedData->staticSelectors << sharedData->preloadedStatics; //Potential for static selectors from other modules
306
307 // TODO: Update on locale changed?
308 sharedData->staticSelectors << QLocale().name();
309
310 sharedData->staticSelectors << platformSelectors();
311}
312
313QStringList QFileSelectorPrivate::platformSelectors()
314{
315 // similar, but not identical to QSysInfo::osType
316 QStringList ret;
317#if defined(Q_OS_WIN)
318 ret << QStringLiteral("windows");
319 ret << QSysInfo::kernelType(); // "winnt"
320#elif defined(Q_OS_UNIX)
321 ret << QStringLiteral("unix");
322# if !defined(Q_OS_ANDROID) && !defined(Q_OS_QNX) && !defined(Q_OS_VXWORKS)
323 // we don't want "linux" for Android or two instances of "qnx" for QNX
324 // or two instances of "vxworks" for vxworks
325 ret << QSysInfo::kernelType();
326# endif
327 QString productName = QSysInfo::productType();
328 if (productName != "unknown"_L1)
329 ret << productName; // "opensuse", "fedora", "osx", "ios", "android"
330#endif
331 return ret;
332}
333
334void QFileSelectorPrivate::addStatics(const QStringList &statics)
335{
336 const auto locker = qt_scoped_lock(sharedDataMutex);
337 sharedData->preloadedStatics << statics;
338 sharedData->staticSelectors.clear();
339}
340
341qsizetype QFileSelectorPrivate::removeStatics(const QStringList &statics)
342{
343 const auto locker = qt_scoped_lock(sharedDataMutex);
344 // Clearing staticSelectors ensures that it's repopulated in QFileSelectorPrivate::updateSelectors()
345 sharedData->staticSelectors.clear();
346 return sharedData->preloadedStatics.removeIf([statics](auto &s) {return statics.contains(s, Qt::CaseSensitive);});
347}
348
349QT_END_NAMESPACE
350
351#include "moc_qfileselector.cpp"
static bool isLocalScheme(const QString &file)
static const char env_override[]
Q_GLOBAL_STATIC(QFileSelectorSharedData, sharedData)