Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:filesystem-manipulation
++
++ #include "qgeofiletilecacheosm.h"
++ #include <QtLocation/private/qgeotilespec_p.h>
++ #include <QDir>
++ #include <QDirIterator>
++ #include <QPair>
++ #include <QDateTime>
++
++ QT_BEGIN_NAMESPACE
++
++ QGeoFileTileCacheOsm::QGeoFileTileCacheOsm(const QList<QGeoTileProviderOsm *> &providers,
++ const QString &offlineDirectory,
++ const QString &directory, QObject *parent)
++ : QGeoFileTileCache(directory, parent),
++ m_offlineDirectory(offlineDirectory),
++ m_offlineData(false),
++ m_providers(providers)
++ {
++ m_highDpi.resize(providers.size());
++ if (!offlineDirectory.isEmpty()) {
++ m_offlineDirectory = QDir(offlineDirectory);
++ if (m_offlineDirectory.exists())
++ m_offlineData = true;
++ }
++ for (int i = 0; i < providers.size(); i++) {
++ providers[i]->setParent(this);
++ m_highDpi[i] = providers[i]->isHighDpi();
++ connect(providers[i], &QGeoTileProviderOsm::resolutionFinished, this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
++ connect(providers[i], &QGeoTileProviderOsm::resolutionError, this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
++ }
++ }
++
++ QGeoFileTileCacheOsm::~QGeoFileTileCacheOsm()
++ {
++ }
++
++ QSharedPointer<QGeoTileTexture> QGeoFileTileCacheOsm::get(const QGeoTileSpec &spec)
++ {
++ QSharedPointer<QGeoTileTexture> tt = getFromMemory(spec);
++ if (tt)
++ return tt;
++ if ((tt = getFromOfflineStorage(spec)))
++ return tt;
++ return getFromDisk(spec);
++ }
++
++ void QGeoFileTileCacheOsm::onProviderResolutionFinished(const QGeoTileProviderOsm *provider)
++ {
++ clearObsoleteTiles(provider);
++ Q_UNUSED(provider);
++ for (int i = 0; i < m_providers.size(); i++) {
++ if (m_providers[i]->isHighDpi() != m_highDpi[i]) { // e.g., HiDpi was requested but only LoDpi is available
++ int mapId = m_providers[i]->mapType().mapId();
++ m_highDpi[i] = m_providers[i]->isHighDpi();
++
++ // reload cache for mapId i
++ dropTiles(mapId);
++ loadTiles(mapId);
++
++ // send signal to clear scene in all maps created through this provider that use the reloaded tiles
++ emit mapDataUpdated(mapId);
++ }
++ }
++ }
++
++ // On resolution error the provider is removed.
++ // This happens ONLY if there is no enabled hardcoded fallback for the mapId.
++ // Hardcoded fallbacks also have a timestamp, that can get updated with Qt releases.
++ void QGeoFileTileCacheOsm::onProviderResolutionError(const QGeoTileProviderOsm *provider, QNetworkReply::NetworkError error)
++ {
++ Q_UNUSED(error);
++ clearObsoleteTiles(provider); // this still removes tiles who happen to be older than qgeotileproviderosm.cpp defaultTs
++ }
++
++ // init() is always called before the provider resolution starts
++ void QGeoFileTileCacheOsm::init()
++ {
++ if (directory_.isEmpty())
++ directory_ = baseLocationCacheDirectory();
++ QDir::root().mkpath(directory_);
++
++ // find max mapId
++ int max = 0;
++ for (auto p: std::as_const(m_providers))
++ if (p->mapType().mapId() > max)
++ max = p->mapType().mapId();
++ // Create a mapId to maxTimestamp LUT..
++ m_maxMapIdTimestamps.resize(max+1); // initializes to invalid QDateTime
++
++ // .. by finding the newest file in each tileset (tileset = mapId).
++ QDir dir(directory_);
++ QStringList formats;
++ formats << QLatin1String("*.*");
++ const QStringList files = dir.entryList(formats, QDir::Files);
++
++ for (const QString &tileFileName : files) {
++ QGeoTileSpec spec = filenameToTileSpec(tileFileName);
++ if (spec.zoom() == -1)
++ continue;
++ QFileInfo fi(dir.filePath(tileFileName));
++ if (fi.lastModified() > m_maxMapIdTimestamps[spec.mapId()])
++ m_maxMapIdTimestamps[spec.mapId()] = fi.lastModified();
++ }
++
++ // Base class ::init()
++ QGeoFileTileCache::init();
++
++ for (QGeoTileProviderOsm * p: std::as_const(m_providers))
++ clearObsoleteTiles(p);
++ }
++
++ QSharedPointer<QGeoTileTexture> QGeoFileTileCacheOsm::getFromOfflineStorage(const QGeoTileSpec &spec)
++ {
++ if (!m_offlineData)
++ return QSharedPointer<QGeoTileTexture>();
++
++ int providerId = spec.mapId() - 1;
++ if (providerId < 0 || providerId >= m_providers.size())
++ return QSharedPointer<QGeoTileTexture>();
++
++ const QString fileName = tileSpecToFilename(spec, QStringLiteral("*"), providerId);
++ QStringList validTiles = m_offlineDirectory.entryList({fileName});
++ if (!validTiles.size())
++ return QSharedPointer<QGeoTileTexture>();
++
++ QFile file(m_offlineDirectory.absoluteFilePath(validTiles.first()));
++ if (!file.open(QIODevice::ReadOnly))
++ return QSharedPointer<QGeoTileTexture>();
++ QByteArray bytes = file.readAll();
++ file.close();
++
++ QImage image;
++ if (!image.loadFromData(bytes)) {
++ handleError(spec, QLatin1String("Problem with tile image"));
++ return QSharedPointer<QGeoTileTexture>();
++ }
++
++ addToMemoryCache(spec, bytes, QString());
++ return addToTextureCache(spec, image);
++ }
++
++ void QGeoFileTileCacheOsm::dropTiles(int mapId)
++ {
++ {
++ const QList<QGeoTileSpec> keys = textureCache_.keys();
++ for (const QGeoTileSpec &k : keys)
++ if (k.mapId() == mapId)
++ textureCache_.remove(k);
++ }
++
++ {
++ const QList<QGeoTileSpec> keys = memoryCache_.keys();
++ for (const QGeoTileSpec &k : keys)
++ if (k.mapId() == mapId)
++ memoryCache_.remove(k);
++ }
++
++ {
++ const QList<QGeoTileSpec> keys = diskCache_.keys();
++ for (const QGeoTileSpec &k : keys)
++ if (k.mapId() == mapId)
++ diskCache_.remove(k);
++ }
++ }
++
++ void QGeoFileTileCacheOsm::loadTiles(int mapId)
++ {
++ QStringList formats;
++ formats << QLatin1String("*.*");
++
++ QDir dir(directory_);
++ QStringList files = dir.entryList(formats, QDir::Files);
++
++ for (int i = 0; i < files.size(); ++i) {
++ QGeoTileSpec spec = filenameToTileSpec(files.at(i));
++ if (spec.zoom() == -1 || spec.mapId() != mapId)
++ continue;
++ QString filename = dir.filePath(files.at(i));
++ addToDiskCache(spec, filename);
++ }
++ }
++
++ QString QGeoFileTileCacheOsm::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const
++ {
++ int providerId = spec.mapId() - 1;
++ if (providerId < 0 || providerId >= m_providers.size())
++ return QString();
++
++ QDir dir = QDir(directory);
++ return dir.filePath(tileSpecToFilename(spec, format, providerId));
++ }
++
++ QString QGeoFileTileCacheOsm::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, int providerId) const
++ {
++ QString filename = spec.plugin();
++ filename += QLatin1String("-");
++ filename += (m_providers[providerId]->isHighDpi()) ? QLatin1Char('h') : QLatin1Char('l');
++ filename += QLatin1String("-");
++ filename += QString::number(spec.mapId());
++ filename += QLatin1String("-");
++ filename += QString::number(spec.zoom());
++ filename += QLatin1String("-");
++ filename += QString::number(spec.x());
++ filename += QLatin1String("-");
++ filename += QString::number(spec.y());
++
++ //Append version if real version number to ensure backwards compatibility and eviction of old tiles
++ if (spec.version() != -1) {
++ filename += QLatin1String("-");
++ filename += QString::number(spec.version());
++ }
++
++ filename += QLatin1String(".");
++ filename += format;
++ return filename;
++ }
++
++ QGeoTileSpec QGeoFileTileCacheOsm::filenameToTileSpec(const QString &filename) const
++ {
++ QGeoTileSpec emptySpec;
++
++ QStringList parts = filename.split('.');
++
++ if (parts.length() != 2)
++ return emptySpec;
++
++ QString name = parts.at(0);
++ QStringList fields = name.split('-');
++
++ int length = fields.length();
++ if (length != 6 && length != 7)
++ return emptySpec;
++
++ QList<int> numbers;
++
++ bool ok = false;
++ for (int i = 2; i < length; ++i) {
++ ok = false;
++ int value = fields.at(i).toInt(&ok);
++ if (!ok)
++ return emptySpec;
++ numbers.append(value);
++ }
++
++ if (numbers.at(0) > m_providers.size())
++ return emptySpec;
++
++ bool highDpi = m_providers[numbers.at(0) - 1]->isHighDpi();
++ if (highDpi && fields.at(1) != QLatin1Char('h'))
++ return emptySpec;
++ else if (!highDpi && fields.at(1) != QLatin1Char('l'))
++ return emptySpec;
++
++ //File name without version, append default
++ if (numbers.length() < 5)
++ numbers.append(-1);
++
++ return QGeoTileSpec(fields.at(0),
++ numbers.at(0),
++ numbers.at(1),
++ numbers.at(2),
++ numbers.at(3),
++ numbers.at(4));
++ }
++
++ void QGeoFileTileCacheOsm::clearObsoleteTiles(const QGeoTileProviderOsm *p)
++ {
++ // process initialized providers, and connect the others
++
++ if (p->isResolved()) {
++ if (m_maxMapIdTimestamps[p->mapType().mapId()].isValid() && // there are tiles in the cache
++ p->timestamp() > m_maxMapIdTimestamps[p->mapType().mapId()]) { // and they are older than the provider
++ qInfo() << "provider for " << p->mapType().name() << " timestamp: " << p->timestamp()
++ << " -- data last modified: " << m_maxMapIdTimestamps[p->mapType().mapId()] << ". Clearing.";
++ clearMapId(p->mapType().mapId());
++ m_maxMapIdTimestamps[p->mapType().mapId()] = p->timestamp(); // don't do it again.
++ }
++ } else {
++ connect(p, &QGeoTileProviderOsm::resolutionFinished,
++ this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
++ #if 0 // If resolution fails, better not try to remove anything. Beside, on error, resolutionFinished is also emitted.
++ connect(p, &QGeoTileProviderOsm::resolutionError,
++ this, &QGeoFileTileCacheOsm::onProviderResolutionError);
++ #endif
++ }
++ }
++
++ QT_END_NAMESPACE