// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later #include "applicationmimeinfo.h" #include "global.h" constexpr std::array desktopSuffix{u8".desktop"}; QStringList getListFiles() noexcept { QStringList files; auto configDirs = getXDGConfigDirs(); auto dataDirs = getXDGDataDirs(); auto desktop = getCurrentDesktop().toLower(); auto appendListFile = [&files, &desktop](const QString &dir) { QFileInfo cur{dir}; if (!cur.exists() or !cur.isDir()) { return; } QDir tmp{cur.absoluteFilePath()}; auto desktopList = tmp.filePath(QString{"%1-mimeapps.list"}.arg(desktop)); if (QFileInfo::exists(desktopList)) { files.append(desktopList); } desktopList = tmp.filePath("mimeapps.list"); if (QFileInfo::exists(desktopList)) { files.append(desktopList); } }; std::for_each(configDirs.cbegin(), configDirs.cend(), appendListFile); std::for_each(dataDirs.cbegin(), dataDirs.cend(), appendListFile); std::reverse(files.begin(), files.end()); return files; } namespace { QString toString(const MimeContent &content) noexcept { QString ret; for (auto it = content.constKeyValueBegin(); it != content.constKeyValueEnd(); ++it) { ret.append(QString{"[%1]\n"}.arg(it->first)); const auto &QStringMap = it->second; for (auto inner = QStringMap.constKeyValueBegin(); inner != QStringMap.constKeyValueEnd(); ++inner) { ret.append(QString{"%1="}.arg(inner->first)); for (const auto &app : inner->second) { ret.append(QString{"%2;"}.arg(app)); } ret.append('\n'); } ret.append('\n'); } return ret; } QString removeDesktopSuffix(const QString &str) noexcept { return str.chopped(desktopSuffix.size() - 1); } } // namespace std::optional MimeFileBase::loadFromFile(const QFileInfo &fileInfo, bool desktopSpec) { bool isWritable{false}; auto filePath = fileInfo.absoluteFilePath(); if (filePath.startsWith(getXDGConfigHome()) or filePath.startsWith(getXDGDataHome())) { isWritable = true; } QFile file{filePath}; if (!file.open(QFile::ReadOnly | QFile::Text | QFile::ExistingOnly)) { qWarning() << "open" << filePath << "failed:" << file.errorString(); return std::nullopt; } QTextStream stream{&file}; stream.setEncoding(QStringConverter::Utf8); MimeContent content; MimeFileParser parser{stream, desktopSpec}; if (auto err = parser.parse(content); err != ParserError::NoError) { qWarning() << "file:" << filePath << "parse failed:" << err; return std::nullopt; } if (content.isEmpty()) { qInfo() << "ignore empty file:" << filePath; return std::nullopt; } return MimeFileBase{fileInfo, std::move(content), desktopSpec, isWritable}; } MimeFileBase::MimeFileBase(const QFileInfo &info, MimeContent &&content, bool desktopSpec, bool writable) : m_desktopSpec(desktopSpec) , m_writable(writable) , m_info(info) , m_content(std::move(content)) { } void MimeFileBase::reload() noexcept { auto newBase = MimeFileBase::loadFromFile(fileInfo(), isDesktopSpecific()); if (!newBase) { qWarning() << "reload" << fileInfo().absoluteFilePath() << "failed, content wouldn't be changed."; return; } m_content = std::move(newBase->m_content); } MimeApps::MimeApps(MimeFileBase &&base) : MimeFileBase(std::move(base)) { } std::optional MimeApps::createMimeApps(const QString &filePath, bool desktopSpec) noexcept { auto baseOpt = MimeFileBase::loadFromFile(QFileInfo{filePath}, desktopSpec); if (!baseOpt) { return std::nullopt; } return MimeApps{std::move(baseOpt).value()}; } void MimeApps::insertToSection(const QString §ion, const QString &mimeType, const QString &appId) noexcept { auto &map = content(); auto targetSection = map.find(section); if (targetSection == map.end()) { targetSection = map.insert(section, {}); } QStringList newApps{QString{appId}.append(desktopSuffix.data())}; auto oldApps = targetSection->find(mimeType); if (oldApps != targetSection->end()) { newApps.append(*oldApps); } targetSection->insert(mimeType, newApps); } void MimeApps::addAssociation(const QString &mimeType, const QString &appId) noexcept { insertToSection(addedAssociations, mimeType, appId); } void MimeApps::removeAssociation(const QString &mimeType, const QString &appId) noexcept { insertToSection(removedAssociations, mimeType, appId); } void MimeApps::setDefaultApplication(const QString &mimeType, const QString &appId) noexcept { auto &map = content(); auto defaultSection = map.find(defaultApplications); if (defaultSection == map.end()) { defaultSection = map.insert(defaultApplications, {}); } defaultSection->insert(mimeType, {QString{appId}.append(desktopSuffix.data())}); } void MimeApps::unsetDefaultApplication(const QString &mimeType) noexcept { auto &map = content(); auto defaultSection = map.find(defaultApplications); if (defaultSection == map.end()) { return; } defaultSection->remove(mimeType); } AppList MimeApps::queryTypes(QString appId) const noexcept { AppList ret; appId.append(desktopSuffix.data()); const auto &lists = content(); if (const auto &adds = lists.constFind(addedAssociations); adds != lists.cend()) { std::for_each(adds->constKeyValueBegin(), adds->constKeyValueEnd(), [&ret, &appId](const auto &it) { if (it.second.contains(appId)) { ret.added.append(it.first); } }); } if (const auto &removes = lists.constFind(removedAssociations); removes != lists.cend()) { std::for_each(removes->constKeyValueBegin(), removes->constKeyValueEnd(), [&ret, &appId](const auto &it) { if (it.second.contains(appId)) { ret.removed.removeOne(it.first); } }); } return ret; } bool MimeApps::writeToFile() const noexcept { auto filePath = fileInfo().absoluteFilePath(); if (!isWritable()) { qDebug() << "shouldn't write file:" << filePath; return false; } QFile file{filePath}; if (!file.open(QFile::ExistingOnly | QFile::Text | QFile::WriteOnly | QFile::Truncate)) { qWarning() << "open" << filePath << "failed:" << file.errorString(); return false; } auto newContent = toString(content()).toLocal8Bit(); auto bytes = file.write(newContent); if (bytes != newContent.size()) { qWarning() << "incomplete content, write:" << bytes << "total:" << newContent.size(); return false; } return true; } QString MimeApps::queryDefaultApp(const QString &type) const noexcept { const auto &map = content(); auto defaultSection = map.find(defaultApplications); if (defaultSection == map.end()) { return {}; } auto defaultApp = defaultSection->find(type); if (defaultApp == defaultSection->end()) { return {}; } const auto &app = defaultApp.value(); if (app.isEmpty()) { return {}; } return removeDesktopSuffix(app.first()); } QStringList MimeCache::queryTypes(QString appId) const noexcept { QStringList ret; appId.append(desktopSuffix.data()); const auto &cache = content()[mimeCache]; for (auto it = cache.constKeyValueBegin(); it != cache.constKeyValueEnd(); ++it) { if (it->second.contains(appId)) { ret.append(it->first); } } return ret; } std::optional MimeCache::createMimeCache(const QString &filePath) noexcept { auto baseOpt = MimeFileBase::loadFromFile(QFileInfo{filePath}, false); if (!baseOpt) { return std::nullopt; } return MimeCache{std::move(baseOpt).value()}; } MimeCache::MimeCache(MimeFileBase &&base) : MimeFileBase(std::move(base)) { } QStringList MimeCache::queryApps(const QString &type) const noexcept { const auto &content = this->content(); auto it = content.constFind(mimeCache); if (it == content.constEnd()) { qDebug() << "this cache is broken, please reload."; return {}; } QStringList ret; if (auto kv = it->constFind(type); kv != it->constEnd()) { const auto &apps = kv.value(); for (const auto &e : apps) { if (!e.endsWith(desktopSuffix.data())) { continue; } ret.append(removeDesktopSuffix(e)); } } return ret; } std::optional MimeInfo::createMimeInfo(const QString &directory) noexcept { MimeInfo ret; auto dirPath = QDir::cleanPath(directory); QDir dir; if (!QFileInfo::exists(dirPath)) { qCritical() << "directory " << dirPath << "doesn't exists."; return std::nullopt; } dir.setPath(dirPath); ret.m_directory = dirPath; QFileInfo cacheFile{dir.filePath("mimeinfo.cache")}; if (cacheFile.exists() and cacheFile.isFile()) { ret.m_cache = MimeCache::createMimeCache(cacheFile.absoluteFilePath()); } QFileInfo desktopAppList{dir.filePath(QString{"%1-mimeapps.list"}.arg(getCurrentDesktop().toLower()))}; if (desktopAppList.exists() and desktopAppList.isFile()) { auto desktopApps = MimeApps::createMimeApps(desktopAppList.absoluteFilePath(), true); if (desktopApps) { ret.m_appsList.emplace_back(std::move(desktopApps).value()); } } QFileInfo appList{dir.filePath("mimeapps.list")}; if (auto userMimeApps = appList.absoluteFilePath(); userMimeApps.startsWith(getXDGConfigHome()) and (!appList.exists() or !appList.isFile())) [[unlikely]] { QFile userFile{userMimeApps}; if (!userFile.open(QFile::WriteOnly | QFile::Text)) { qCritical() << "failed to create user file:" << userMimeApps << userFile.errorString(); } } if (appList.exists() and appList.isFile()) { auto apps = MimeApps::createMimeApps(appList.absoluteFilePath(), false); if (apps) { ret.m_appsList.emplace_back(std::move(apps).value()); } } return ret; } void MimeInfo::reload() noexcept { for (auto &app : m_appsList) { app.reload(); } if (m_cache) { m_cache->reload(); } }