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