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/private/qlocking_p.h>
48 #include <QtCore/QUrl>
49 #include <QtCore/QFileInfo>
50 #include <QtCore/QLocale>
51 #include <QtCore/QDebug>
52 
53 QT_BEGIN_NAMESPACE
54 
55 //Environment variable to allow tooling full control of file selectors
56 static const char env_override[] = "QT_NO_BUILTIN_SELECTORS";
57 
58 Q_GLOBAL_STATIC(QFileSelectorSharedData, sharedData);
59 static QBasicMutex sharedDataMutex;
60 
QFileSelectorPrivate()61 QFileSelectorPrivate::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 */
QFileSelector(QObject * parent)170 QFileSelector::QFileSelector(QObject *parent)
171     : QObject(*(new QFileSelectorPrivate()), parent)
172 {
173 }
174 
175 /*!
176   Destroys this selector instance.
177 */
~QFileSelector()178 QFileSelector::~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 */
select(const QString & filePath) const191 QString QFileSelector::select(const QString &filePath) const
192 {
193     Q_D(const QFileSelector);
194     return d->select(filePath);
195 }
196 
isLocalScheme(const QString & file)197 static 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 */
select(const QUrl & filePath) const213 QUrl 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 
selectionHelper(const QString & path,const QString & fileName,const QStringList & selectors,const QChar & indicator)247 QString 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 
select(const QString & filePath) const276 QString 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 */
extraSelectors() const292 QStringList 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 */
setExtraSelectors(const QStringList & list)303 void 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 */
allSelectors() const312 QStringList QFileSelector::allSelectors() const
313 {
314     Q_D(const QFileSelector);
315     const auto locker = qt_scoped_lock(sharedDataMutex);
316     QFileSelectorPrivate::updateSelectors();
317     return d->extras + sharedData->staticSelectors;
318 }
319 
updateSelectors()320 void 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, Qt::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 
platformSelectors()342 QStringList 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 
addStatics(const QStringList & statics)372 void QFileSelectorPrivate::addStatics(const QStringList &statics)
373 {
374     const auto locker = qt_scoped_lock(sharedDataMutex);
375     sharedData->preloadedStatics << statics;
376     sharedData->staticSelectors.clear();
377 }
378 
379 QT_END_NAMESPACE
380 
381 #include "moc_qfileselector.cpp"
382