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