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

source code of qtbase/src/corelib/io/qfileselector.cpp