1 | // Copyright (C) 2018 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qqmlpreviewfileengine.h" |
5 | #include "qqmlpreviewservice.h" |
6 | |
7 | #include <QtCore/qlibraryinfo.h> |
8 | #include <QtCore/qthread.h> |
9 | #include <QtCore/qwaitcondition.h> |
10 | |
11 | #include <cstring> |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | static bool isRelative(const QString &path) |
16 | { |
17 | if (path.isEmpty()) |
18 | return true; |
19 | if (path.at(i: 0) == '/') |
20 | return false; |
21 | if (path.at(i: 0) == ':' && path.size() >= 2 && path.at(i: 1) == '/') |
22 | return false; |
23 | #ifdef Q_OS_WIN |
24 | if (path.length() >= 2 && path.at(1) == ':') |
25 | return false; |
26 | #endif |
27 | return true; |
28 | } |
29 | |
30 | static QString absolutePath(const QString &path) |
31 | { |
32 | return QDir::cleanPath(path: isRelative(path) ? (QDir::currentPath() + '/' + path) : path); |
33 | } |
34 | |
35 | bool isRootPath(const QString &path) |
36 | { |
37 | return QFileSystemEntry::isRootPath(path); |
38 | } |
39 | |
40 | class QQmlPreviewFileEngineIterator : public QAbstractFileEngineIterator |
41 | { |
42 | public: |
43 | QQmlPreviewFileEngineIterator(QDir::Filters filters, const QStringList &filterNames, |
44 | const QStringList &m_entries); |
45 | ~QQmlPreviewFileEngineIterator(); |
46 | |
47 | QString next() override; |
48 | bool hasNext() const override; |
49 | QString currentFileName() const override; |
50 | |
51 | private: |
52 | const QStringList m_entries; |
53 | int m_index; |
54 | }; |
55 | |
56 | QQmlPreviewFileEngineIterator::QQmlPreviewFileEngineIterator(QDir::Filters filters, |
57 | const QStringList &filterNames, |
58 | const QStringList &entries) |
59 | : QAbstractFileEngineIterator(filters, filterNames), m_entries(entries), m_index(0) |
60 | { |
61 | } |
62 | |
63 | QQmlPreviewFileEngineIterator::~QQmlPreviewFileEngineIterator() |
64 | { |
65 | } |
66 | |
67 | QString QQmlPreviewFileEngineIterator::next() |
68 | { |
69 | if (!hasNext()) |
70 | return QString(); |
71 | ++m_index; |
72 | return currentFilePath(); |
73 | } |
74 | |
75 | bool QQmlPreviewFileEngineIterator::hasNext() const |
76 | { |
77 | return m_index < m_entries.size(); |
78 | } |
79 | |
80 | QString QQmlPreviewFileEngineIterator::currentFileName() const |
81 | { |
82 | if (m_index == 0 || m_index > m_entries.size()) |
83 | return QString(); |
84 | return m_entries.at(i: m_index - 1); |
85 | } |
86 | |
87 | QQmlPreviewFileEngine::QQmlPreviewFileEngine(const QString &file, const QString &absolute, |
88 | QQmlPreviewFileLoader *loader) : |
89 | m_name(file), m_absolute(absolute), m_loader(loader) |
90 | { |
91 | load(); |
92 | } |
93 | |
94 | void QQmlPreviewFileEngine::setFileName(const QString &file) |
95 | { |
96 | m_name = file; |
97 | m_absolute = absolutePath(path: file); |
98 | m_fallback.reset(); |
99 | m_contents.close(); |
100 | m_contents.setData(QByteArray()); |
101 | m_entries.clear(); |
102 | load(); |
103 | } |
104 | |
105 | bool QQmlPreviewFileEngine::open(QIODevice::OpenMode flags, |
106 | std::optional<QFile::Permissions> permissions) |
107 | { |
108 | switch (m_result) { |
109 | case QQmlPreviewFileLoader::File: |
110 | return m_contents.open(openMode: flags); |
111 | case QQmlPreviewFileLoader::Directory: |
112 | return false; |
113 | case QQmlPreviewFileLoader::Fallback: |
114 | return m_fallback->open(openMode: flags, permissions); |
115 | default: |
116 | Q_UNREACHABLE_RETURN(false); |
117 | } |
118 | } |
119 | |
120 | bool QQmlPreviewFileEngine::close() |
121 | { |
122 | switch (m_result) { |
123 | case QQmlPreviewFileLoader::Fallback: |
124 | return m_fallback->close(); |
125 | case QQmlPreviewFileLoader::File: |
126 | m_contents.close(); |
127 | return true; |
128 | case QQmlPreviewFileLoader::Directory: |
129 | return false; |
130 | default: |
131 | Q_UNREACHABLE_RETURN(false); |
132 | } |
133 | } |
134 | |
135 | qint64 QQmlPreviewFileEngine::size() const |
136 | { |
137 | return m_fallback ? m_fallback->size() : m_contents.size(); |
138 | } |
139 | |
140 | qint64 QQmlPreviewFileEngine::pos() const |
141 | { |
142 | return m_fallback ? m_fallback->pos() : m_contents.pos(); |
143 | } |
144 | |
145 | bool QQmlPreviewFileEngine::seek(qint64 newPos) |
146 | { |
147 | return m_fallback? m_fallback->seek(pos: newPos) : m_contents.seek(off: newPos); |
148 | } |
149 | |
150 | qint64 QQmlPreviewFileEngine::read(char *data, qint64 maxlen) |
151 | { |
152 | return m_fallback ? m_fallback->read(data, maxlen) : m_contents.read(data, maxlen); |
153 | } |
154 | |
155 | QAbstractFileEngine::FileFlags QQmlPreviewFileEngine::fileFlags( |
156 | QAbstractFileEngine::FileFlags type) const |
157 | { |
158 | if (m_fallback) |
159 | return m_fallback->fileFlags(type); |
160 | |
161 | QAbstractFileEngine::FileFlags ret; |
162 | |
163 | if (type & PermsMask) { |
164 | ret |= QAbstractFileEngine::FileFlags( |
165 | ReadOwnerPerm | ReadUserPerm | ReadGroupPerm | ReadOtherPerm); |
166 | } |
167 | |
168 | if (type & TypesMask) { |
169 | if (m_result == QQmlPreviewFileLoader::Directory) |
170 | ret |= DirectoryType; |
171 | else |
172 | ret |= FileType; |
173 | } |
174 | |
175 | if (type & FlagsMask) { |
176 | ret |= ExistsFlag; |
177 | if (isRootPath(path: m_name)) |
178 | ret |= RootFlag; |
179 | } |
180 | |
181 | return ret; |
182 | } |
183 | |
184 | QString QQmlPreviewFileEngine::fileName(QAbstractFileEngine::FileName file) const |
185 | { |
186 | if (m_fallback) |
187 | return m_fallback->fileName(file); |
188 | |
189 | if (file == BaseName) { |
190 | int slashPos = m_name.lastIndexOf(c: '/'); |
191 | if (slashPos == -1) |
192 | return m_name; |
193 | return m_name.mid(position: slashPos + 1); |
194 | } else if (file == PathName || file == AbsolutePathName) { |
195 | const QString path = (file == AbsolutePathName) ? m_absolute : m_name; |
196 | const int slashPos = path.lastIndexOf(c: '/'); |
197 | if (slashPos == -1) |
198 | return QString(); |
199 | else if (slashPos == 0) |
200 | return "/" ; |
201 | return path.left(n: slashPos); |
202 | } else if (file == CanonicalName || file == CanonicalPathName) { |
203 | if (file == CanonicalPathName) { |
204 | const int slashPos = m_absolute.lastIndexOf(c: '/'); |
205 | if (slashPos != -1) |
206 | return m_absolute.left(n: slashPos); |
207 | } |
208 | return m_absolute; |
209 | } |
210 | return m_name; |
211 | } |
212 | |
213 | uint QQmlPreviewFileEngine::ownerId(QAbstractFileEngine::FileOwner owner) const |
214 | { |
215 | return m_fallback ? m_fallback->ownerId(owner) : static_cast<uint>(-2); |
216 | } |
217 | |
218 | QAbstractFileEngine::Iterator *QQmlPreviewFileEngine::beginEntryList(QDir::Filters filters, |
219 | const QStringList &filterNames) |
220 | { |
221 | return m_fallback ? m_fallback->beginEntryList(filters, filterNames) |
222 | : new QQmlPreviewFileEngineIterator(filters, filterNames, m_entries); |
223 | } |
224 | |
225 | QAbstractFileEngine::Iterator *QQmlPreviewFileEngine::endEntryList() |
226 | { |
227 | return m_fallback ? m_fallback->endEntryList() : nullptr; |
228 | } |
229 | |
230 | bool QQmlPreviewFileEngine::flush() |
231 | { |
232 | return m_fallback ? m_fallback->flush() : true; |
233 | } |
234 | |
235 | bool QQmlPreviewFileEngine::syncToDisk() |
236 | { |
237 | return m_fallback ? m_fallback->syncToDisk() : false; |
238 | } |
239 | |
240 | bool QQmlPreviewFileEngine::isSequential() const |
241 | { |
242 | return m_fallback ? m_fallback->isSequential() : m_contents.isSequential(); |
243 | } |
244 | |
245 | bool QQmlPreviewFileEngine::remove() |
246 | { |
247 | return m_fallback ? m_fallback->remove() : false; |
248 | } |
249 | |
250 | bool QQmlPreviewFileEngine::copy(const QString &newName) |
251 | { |
252 | return m_fallback ? m_fallback->copy(newName) : false; |
253 | } |
254 | |
255 | bool QQmlPreviewFileEngine::rename(const QString &newName) |
256 | { |
257 | return m_fallback ? m_fallback->rename(newName) : false; |
258 | } |
259 | |
260 | bool QQmlPreviewFileEngine::renameOverwrite(const QString &newName) |
261 | { |
262 | return m_fallback ? m_fallback->renameOverwrite(newName) : false; |
263 | } |
264 | |
265 | bool QQmlPreviewFileEngine::link(const QString &newName) |
266 | { |
267 | return m_fallback ? m_fallback->link(newName) : false; |
268 | } |
269 | |
270 | bool QQmlPreviewFileEngine::mkdir(const QString &dirName, bool createParentDirectories, |
271 | std::optional<QFile::Permissions> permissions) const |
272 | { |
273 | return m_fallback ? m_fallback->mkdir(dirName, createParentDirectories, permissions) : false; |
274 | } |
275 | |
276 | bool QQmlPreviewFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const |
277 | { |
278 | return m_fallback ? m_fallback->rmdir(dirName, recurseParentDirectories) : false; |
279 | } |
280 | |
281 | bool QQmlPreviewFileEngine::setSize(qint64 size) |
282 | { |
283 | switch (m_result) { |
284 | case QQmlPreviewFileLoader::Fallback: |
285 | return m_fallback->setSize(size); |
286 | case QQmlPreviewFileLoader::File: |
287 | if (size < 0 || size > std::numeric_limits<int>::max()) |
288 | return false; |
289 | m_contents.buffer().resize(size: static_cast<int>(size)); |
290 | return true; |
291 | case QQmlPreviewFileLoader::Directory: |
292 | return false; |
293 | default: |
294 | Q_UNREACHABLE_RETURN(false); |
295 | } |
296 | } |
297 | |
298 | bool QQmlPreviewFileEngine::caseSensitive() const |
299 | { |
300 | return m_fallback ? m_fallback->caseSensitive() : true; |
301 | } |
302 | |
303 | bool QQmlPreviewFileEngine::isRelativePath() const |
304 | { |
305 | return m_fallback ? m_fallback->isRelativePath() : isRelative(path: m_name); |
306 | } |
307 | |
308 | QStringList QQmlPreviewFileEngine::entryList(QDir::Filters filters, |
309 | const QStringList &filterNames) const |
310 | { |
311 | return m_fallback ? m_fallback->entryList(filters, filterNames) |
312 | : QAbstractFileEngine::entryList(filters, filterNames); |
313 | } |
314 | |
315 | bool QQmlPreviewFileEngine::setPermissions(uint perms) |
316 | { |
317 | return m_fallback ? m_fallback->setPermissions(perms) : false; |
318 | } |
319 | |
320 | QByteArray QQmlPreviewFileEngine::id() const |
321 | { |
322 | return m_fallback ? m_fallback->id() : QByteArray(); |
323 | } |
324 | |
325 | QString QQmlPreviewFileEngine::owner(FileOwner owner) const |
326 | { |
327 | return m_fallback ? m_fallback->owner(owner) : QString(); |
328 | } |
329 | |
330 | QDateTime QQmlPreviewFileEngine::fileTime(FileTime time) const |
331 | { |
332 | // Files we replace are always newer than the ones we had before. This makes the QML engine |
333 | // actually recompile them, rather than pick them from the cache. |
334 | return m_fallback ? m_fallback->fileTime(time) : QDateTime::currentDateTime(); |
335 | } |
336 | |
337 | int QQmlPreviewFileEngine::handle() const |
338 | { |
339 | return m_fallback ? m_fallback->handle() : -1; |
340 | } |
341 | |
342 | qint64 QQmlPreviewFileEngine::readLine(char *data, qint64 maxlen) |
343 | { |
344 | return m_fallback ? m_fallback->readLine(data, maxlen) : m_contents.readLine(data, maxlen); |
345 | } |
346 | |
347 | qint64 QQmlPreviewFileEngine::write(const char *data, qint64 len) |
348 | { |
349 | return m_fallback ? m_fallback->write(data, len) : m_contents.write(data, len); |
350 | } |
351 | |
352 | bool QQmlPreviewFileEngine::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output) |
353 | { |
354 | return m_fallback ? m_fallback->extension(extension, option, output) : false; |
355 | } |
356 | |
357 | bool QQmlPreviewFileEngine::supportsExtension(Extension extension) const |
358 | { |
359 | return m_fallback ? m_fallback->supportsExtension(extension) : false; |
360 | } |
361 | |
362 | void QQmlPreviewFileEngine::load() const |
363 | { |
364 | // We can get here from different threads on different instances of QQmlPreviewFileEngine. |
365 | // However, there is only one loader per QQmlPreviewFileEngineHandler and it is not thread-safe. |
366 | // Its content mutex doesn't help us here because we explicitly wait on it in load(), which |
367 | // causes it to be released. Therefore, lock the load mutex first. |
368 | // This doesn't cause any deadlocks because the only thread that wakes the loader on the content |
369 | // mutex never calls load(). It's the QML debug server thread that handles the debug protocol. |
370 | QMutexLocker loadLocker(m_loader->loadMutex()); |
371 | |
372 | m_result = m_loader->load(file: m_absolute); |
373 | switch (m_result) { |
374 | case QQmlPreviewFileLoader::File: |
375 | m_contents.setData(m_loader->contents()); |
376 | break; |
377 | case QQmlPreviewFileLoader::Directory: |
378 | m_entries = m_loader->entries(); |
379 | break; |
380 | case QQmlPreviewFileLoader::Fallback: |
381 | m_fallback.reset(other: QAbstractFileEngine::create(fileName: m_name)); |
382 | break; |
383 | case QQmlPreviewFileLoader::Unknown: |
384 | Q_UNREACHABLE(); |
385 | break; |
386 | } |
387 | } |
388 | |
389 | QQmlPreviewFileEngineHandler::QQmlPreviewFileEngineHandler(QQmlPreviewFileLoader *loader) |
390 | : m_loader(loader) |
391 | { |
392 | } |
393 | |
394 | QAbstractFileEngine *QQmlPreviewFileEngineHandler::create(const QString &fileName) const |
395 | { |
396 | // Don't load compiled QML/JS over the network |
397 | if (fileName.endsWith(s: ".qmlc" ) || fileName.endsWith(s: ".jsc" ) || isRootPath(path: fileName)) { |
398 | return nullptr; |
399 | } |
400 | |
401 | QString relative = fileName; |
402 | while (relative.endsWith(c: '/')) |
403 | relative.chop(n: 1); |
404 | |
405 | if (relative.isEmpty() || relative == ":" ) |
406 | return nullptr; |
407 | |
408 | const QString absolute = relative.startsWith(c: ':') ? relative : absolutePath(path: relative); |
409 | |
410 | return m_loader->isBlacklisted(file: absolute) |
411 | ? nullptr : new QQmlPreviewFileEngine(relative, absolute, m_loader.data()); |
412 | } |
413 | |
414 | QT_END_NAMESPACE |
415 | |