| 1 | // SPDX-License-Identifier: LGPL-2.0-or-later |
| 2 | // SPDX-FileCopyrightText: 2006, 2007 Thomas Braxton <kde.braxton@gmail.com> |
| 3 | // SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org> |
| 4 | // SPDX-FileCopyrightText: 1997 Matthias Kalle Dalheimer <kalle@kde.org> |
| 5 | // SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org> |
| 6 | |
| 7 | #pragma once |
| 8 | |
| 9 | #include <QDir> |
| 10 | #include <QFile> |
| 11 | #include <QLockFile> |
| 12 | #include <QSaveFile> |
| 13 | |
| 14 | #ifdef Q_OS_ANDROID |
| 15 | #include <QStandardPaths> |
| 16 | #endif |
| 17 | |
| 18 | #include <chrono> |
| 19 | #include <qtpreprocessorsupport.h> |
| 20 | |
| 21 | #ifndef Q_OS_WIN |
| 22 | #include <unistd.h> // getuid |
| 23 | #endif |
| 24 | #include <sys/types.h> // uid_t |
| 25 | |
| 26 | #include "kconfig_core_log_settings.h" |
| 27 | |
| 28 | using namespace Qt::StringLiterals; |
| 29 | |
| 30 | // Wraps a QLockFile to allow no-op locking when locking is not supported (e.g. QIODevice based backends). |
| 31 | class AbstractLockFile |
| 32 | { |
| 33 | public: |
| 34 | AbstractLockFile() = default; |
| 35 | virtual ~AbstractLockFile() = default; |
| 36 | Q_DISABLE_COPY_MOVE(AbstractLockFile) |
| 37 | |
| 38 | [[nodiscard]] virtual bool tryLock(std::chrono::milliseconds timeout = std::chrono::milliseconds::zero()) = 0; |
| 39 | [[nodiscard]] virtual bool isLocked() const = 0; |
| 40 | [[nodiscard]] virtual QString fileName() const = 0; |
| 41 | virtual void unlock() = 0; |
| 42 | [[nodiscard]] virtual QLockFile::LockError error() const = 0; |
| 43 | }; |
| 44 | |
| 45 | class RealLockFile : public AbstractLockFile |
| 46 | { |
| 47 | public: |
| 48 | RealLockFile(std::unique_ptr<QLockFile> &&lockFile) |
| 49 | : m_lockFile(std::move(lockFile)) |
| 50 | { |
| 51 | m_lockFile->setStaleLockTime(std::chrono::seconds{20}); |
| 52 | } |
| 53 | |
| 54 | [[nodiscard]] bool tryLock(std::chrono::milliseconds timeout) override |
| 55 | { |
| 56 | return m_lockFile->tryLock(timeout); |
| 57 | } |
| 58 | |
| 59 | void unlock() override |
| 60 | { |
| 61 | m_lockFile->unlock(); |
| 62 | } |
| 63 | |
| 64 | [[nodiscard]] bool isLocked() const override |
| 65 | { |
| 66 | return m_lockFile->isLocked(); |
| 67 | } |
| 68 | QString fileName() const override |
| 69 | { |
| 70 | return m_lockFile->fileName(); |
| 71 | } |
| 72 | |
| 73 | [[nodiscard]] QLockFile::LockError error() const override |
| 74 | { |
| 75 | return m_lockFile->error(); |
| 76 | } |
| 77 | |
| 78 | private: |
| 79 | std::unique_ptr<QLockFile> m_lockFile; |
| 80 | }; |
| 81 | |
| 82 | class FakeLockFile : public AbstractLockFile |
| 83 | { |
| 84 | public: |
| 85 | [[nodiscard]] bool tryLock(std::chrono::milliseconds timeout) override |
| 86 | { |
| 87 | Q_UNUSED(timeout) |
| 88 | return m_locked = true; |
| 89 | } |
| 90 | |
| 91 | void unlock() override |
| 92 | { |
| 93 | m_locked = false; |
| 94 | } |
| 95 | |
| 96 | [[nodiscard]] bool isLocked() const override |
| 97 | { |
| 98 | return m_locked; |
| 99 | } |
| 100 | |
| 101 | [[nodiscard]] QString fileName() const override |
| 102 | { |
| 103 | return {}; |
| 104 | } |
| 105 | |
| 106 | [[nodiscard]] QLockFile::LockError error() const override |
| 107 | { |
| 108 | return QLockFile::LockError::NoError; |
| 109 | } |
| 110 | |
| 111 | private: |
| 112 | bool m_locked = false; |
| 113 | }; |
| 114 | |
| 115 | class KConfigIniBackendAbstractDevice |
| 116 | { |
| 117 | public: |
| 118 | struct OpenResult { |
| 119 | bool shouldHaveDevice; |
| 120 | std::shared_ptr<QIODevice> device; |
| 121 | }; |
| 122 | |
| 123 | KConfigIniBackendAbstractDevice() = default; |
| 124 | virtual ~KConfigIniBackendAbstractDevice() = default; |
| 125 | Q_DISABLE_COPY_MOVE(KConfigIniBackendAbstractDevice) |
| 126 | |
| 127 | // The local file path for path based devices or a pseudo name for all others. |
| 128 | [[nodiscard]] virtual QString id() const = 0; |
| 129 | [[nodiscard]] virtual bool isDeviceReadable() const = 0; |
| 130 | [[nodiscard]] virtual bool canWriteToDevice() const = 0; |
| 131 | [[nodiscard]] virtual bool writeToDevice(const std::function<void(QIODevice &)> &write) = 0; |
| 132 | [[nodiscard]] virtual OpenResult open() = 0; |
| 133 | [[nodiscard]] virtual std::unique_ptr<AbstractLockFile> lockFile() = 0; |
| 134 | virtual void createEnclosingEntity() = 0; |
| 135 | virtual void setFilePath(const QString &path) = 0; |
| 136 | }; |
| 137 | |
| 138 | class KConfigIniBackendNullDevice : public KConfigIniBackendAbstractDevice |
| 139 | { |
| 140 | public: |
| 141 | [[nodiscard]] QString id() const override |
| 142 | { |
| 143 | return u"((NullDevice))"_s ; |
| 144 | } |
| 145 | |
| 146 | [[nodiscard]] bool isDeviceReadable() const override |
| 147 | { |
| 148 | return false; |
| 149 | } |
| 150 | |
| 151 | [[nodiscard]] bool canWriteToDevice() const override |
| 152 | { |
| 153 | return false; |
| 154 | } |
| 155 | |
| 156 | [[nodiscard]] bool writeToDevice(const std::function<void(QIODevice &)> &write) override |
| 157 | { |
| 158 | Q_UNUSED(write) |
| 159 | return false; |
| 160 | } |
| 161 | |
| 162 | [[nodiscard]] OpenResult open() override |
| 163 | { |
| 164 | return {.shouldHaveDevice = false, .device = nullptr}; |
| 165 | } |
| 166 | |
| 167 | [[nodiscard]] std::unique_ptr<AbstractLockFile> lockFile() override |
| 168 | { |
| 169 | return std::make_unique<FakeLockFile>(); |
| 170 | } |
| 171 | |
| 172 | void createEnclosingEntity() override |
| 173 | { |
| 174 | } |
| 175 | |
| 176 | void setFilePath(const QString &path) override |
| 177 | { |
| 178 | Q_UNUSED(path) |
| 179 | } |
| 180 | }; |
| 181 | |
| 182 | class KConfigIniBackendPathDevice : public KConfigIniBackendAbstractDevice |
| 183 | { |
| 184 | public: |
| 185 | explicit KConfigIniBackendPathDevice(const QString &path) |
| 186 | { |
| 187 | setFilePath(path); |
| 188 | } |
| 189 | |
| 190 | [[nodiscard]] QString id() const override |
| 191 | { |
| 192 | return m_localFilePath; |
| 193 | } |
| 194 | |
| 195 | [[nodiscard]] bool isDeviceReadable() const override |
| 196 | { |
| 197 | return !m_localFilePath.isEmpty(); |
| 198 | } |
| 199 | |
| 200 | [[nodiscard]] bool canWriteToDevice() const override |
| 201 | { |
| 202 | const QString filePath = m_localFilePath; |
| 203 | if (filePath.isEmpty()) { |
| 204 | return false; |
| 205 | } |
| 206 | |
| 207 | QFileInfo file(filePath); |
| 208 | if (file.exists()) { |
| 209 | return file.isWritable(); |
| 210 | } |
| 211 | |
| 212 | // If the file does not exist, check if the deepest existing dir is writable |
| 213 | QFileInfo dir(file.absolutePath()); |
| 214 | while (!dir.exists()) { |
| 215 | QString parent = dir.absolutePath(); // Go up. Can't use cdUp() on non-existing dirs. |
| 216 | if (parent == dir.filePath()) { |
| 217 | // no parent |
| 218 | return false; |
| 219 | } |
| 220 | dir.setFile(parent); |
| 221 | } |
| 222 | return dir.isDir() && dir.isWritable(); |
| 223 | } |
| 224 | |
| 225 | [[nodiscard]] bool writeToDevice(const std::function<void(QIODevice &)> &write) override |
| 226 | { |
| 227 | // check if file exists |
| 228 | QFile::Permissions fileMode = filePath().startsWith(s: u"/etc/xdg/"_s ) ? QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup | QFile::ReadOther // |
| 229 | : QFile::ReadUser | QFile::WriteUser; |
| 230 | |
| 231 | bool createNew = true; |
| 232 | |
| 233 | QFileInfo fi(filePath()); |
| 234 | if (fi.exists()) { |
| 235 | #ifdef Q_OS_WIN |
| 236 | // TODO: getuid does not exist on windows, use GetSecurityInfo and GetTokenInformation instead |
| 237 | createNew = false; |
| 238 | #else |
| 239 | if (fi.ownerId() == ::getuid()) { |
| 240 | // Preserve file mode if file exists and is owned by user. |
| 241 | fileMode = fi.permissions(); |
| 242 | } else { |
| 243 | // File is not owned by user: |
| 244 | // Don't create new file but write to existing file instead. |
| 245 | createNew = false; |
| 246 | } |
| 247 | #endif |
| 248 | } |
| 249 | |
| 250 | if (createNew) { |
| 251 | QSaveFile file(filePath()); |
| 252 | if (!file.open(flags: QIODevice::WriteOnly)) { |
| 253 | #ifdef Q_OS_ANDROID |
| 254 | // HACK: when we are dealing with content:// URIs, QSaveFile has to rely on DirectWrite. |
| 255 | // Otherwise this method returns a false and we're done. |
| 256 | file.setDirectWriteFallback(true); |
| 257 | if (!file.open(QIODevice::WriteOnly)) { |
| 258 | qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString(); |
| 259 | return false; |
| 260 | } |
| 261 | #else |
| 262 | qWarning(catFunc: KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString(); |
| 263 | return false; |
| 264 | #endif |
| 265 | } |
| 266 | |
| 267 | file.setTextModeEnabled(true); // to get eol translation |
| 268 | write(file); |
| 269 | |
| 270 | if (!file.size() && (fileMode == (QFile::ReadUser | QFile::WriteUser))) { |
| 271 | // File is empty and doesn't have special permissions: delete it. |
| 272 | file.cancelWriting(); |
| 273 | |
| 274 | if (fi.exists()) { |
| 275 | // also remove the old file in case it existed. this can happen |
| 276 | // when we delete all the entries in an existing config file. |
| 277 | // if we don't do this, then deletions and revertToDefault's |
| 278 | // will mysteriously fail |
| 279 | QFile::remove(fileName: filePath()); |
| 280 | } |
| 281 | } else { |
| 282 | // Normal case: Close the file |
| 283 | if (file.commit()) { |
| 284 | QFile::setPermissions(filename: filePath(), permissionSpec: fileMode); |
| 285 | return true; |
| 286 | } |
| 287 | // Couldn't write. Disk full? |
| 288 | qCWarning(KCONFIG_CORE_LOG) << "Couldn't write" << filePath() << ". Disk full?" ; |
| 289 | return false; |
| 290 | } |
| 291 | } else { |
| 292 | QFile f(filePath()); |
| 293 | |
| 294 | // Open existing file. *DON'T* create it if it suddenly does not exist! |
| 295 | if (!f.open(flags: QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::ExistingOnly)) { |
| 296 | return false; |
| 297 | } |
| 298 | |
| 299 | f.setTextModeEnabled(true); |
| 300 | write(f); |
| 301 | } |
| 302 | |
| 303 | return true; |
| 304 | } |
| 305 | |
| 306 | [[nodiscard]] OpenResult open() override |
| 307 | { |
| 308 | auto file = std::make_unique<QFile>(args&: m_localFilePath); |
| 309 | if (!file->open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
| 310 | return {.shouldHaveDevice = file->exists(), .device = nullptr}; |
| 311 | } |
| 312 | return {.shouldHaveDevice = true, .device = std::move(file)}; |
| 313 | } |
| 314 | |
| 315 | void createEnclosingEntity() override |
| 316 | { |
| 317 | const QString file = m_localFilePath; |
| 318 | if (file.isEmpty()) { |
| 319 | return; // nothing to do |
| 320 | } |
| 321 | |
| 322 | // Create the containing dir, maybe it wasn't there |
| 323 | if (!QDir().mkpath(dirPath: QFileInfo(file).absolutePath())) { |
| 324 | qWarning(msg: "KConfigIniBackend: Could not create enclosing directory for %s" , qPrintable(file)); |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | void setFilePath(const QString &path) override |
| 329 | { |
| 330 | if (path.isEmpty()) { |
| 331 | return; |
| 332 | } |
| 333 | |
| 334 | Q_ASSERT(QDir::isAbsolutePath(path)); |
| 335 | if (!QDir::isAbsolutePath(path)) { |
| 336 | qWarning() << "setFilePath" << path; |
| 337 | } |
| 338 | |
| 339 | const QFileInfo info(path); |
| 340 | if (info.exists()) { |
| 341 | setLocalFilePath(info.canonicalFilePath()); |
| 342 | return; |
| 343 | } |
| 344 | |
| 345 | if (QString filePath = info.dir().canonicalPath(); !filePath.isEmpty()) { |
| 346 | filePath += QLatin1Char('/') + info.fileName(); |
| 347 | setLocalFilePath(filePath); |
| 348 | } else { |
| 349 | setLocalFilePath(path); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | [[nodiscard]] std::unique_ptr<AbstractLockFile> lockFile() override |
| 354 | { |
| 355 | #ifdef Q_OS_ANDROID |
| 356 | // handle content Uris properly |
| 357 | if (filePath().startsWith(QLatin1String("content://" ))) { |
| 358 | // we can't create file at an arbitrary location, so use internal storage to create one |
| 359 | |
| 360 | // NOTE: filename can be the same, but because this lock is short lived we may never have a collision |
| 361 | return std::make_unique<RealLockFile>(std::make_unique<QLockFile>(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) |
| 362 | + QLatin1String("/" ) + QFileInfo(filePath()).fileName() |
| 363 | + QLatin1String(".lock" ))); |
| 364 | } |
| 365 | return std::make_unique<RealLockFile>(std::make_unique<QLockFile>(filePath() + QLatin1String(".lock" ))); |
| 366 | #else |
| 367 | return std::make_unique<RealLockFile>(args: std::make_unique<QLockFile>(args: filePath() + QLatin1String(".lock" ))); |
| 368 | #endif |
| 369 | } |
| 370 | |
| 371 | private: |
| 372 | /* the absolute path to the object */ |
| 373 | [[nodiscard]] QString filePath() const |
| 374 | { |
| 375 | return m_localFilePath; |
| 376 | } |
| 377 | |
| 378 | void setLocalFilePath(const QString &file) |
| 379 | { |
| 380 | m_localFilePath = file; |
| 381 | } |
| 382 | |
| 383 | QString m_localFilePath; |
| 384 | }; |
| 385 | |
| 386 | class KConfigIniBackendQIODevice : public KConfigIniBackendAbstractDevice |
| 387 | { |
| 388 | public: |
| 389 | KConfigIniBackendQIODevice(const std::shared_ptr<QIODevice> &device) |
| 390 | : m_device(device) |
| 391 | { |
| 392 | } |
| 393 | |
| 394 | [[nodiscard]] QString id() const override |
| 395 | { |
| 396 | return u"((QIODevice))"_s ; |
| 397 | } |
| 398 | |
| 399 | [[nodiscard]] bool isDeviceReadable() const override |
| 400 | { |
| 401 | return m_device->isOpen() && m_device->isReadable(); |
| 402 | } |
| 403 | |
| 404 | [[nodiscard]] bool canWriteToDevice() const override |
| 405 | { |
| 406 | return m_device->isOpen() && m_device->isWritable(); |
| 407 | } |
| 408 | |
| 409 | [[nodiscard]] bool writeToDevice(const std::function<void(QIODevice &)> &write) override |
| 410 | { |
| 411 | m_device->setTextModeEnabled(true); |
| 412 | write(*m_device); |
| 413 | return true; |
| 414 | } |
| 415 | |
| 416 | [[nodiscard]] OpenResult open() override |
| 417 | { |
| 418 | return {.shouldHaveDevice = true, .device = m_device}; |
| 419 | } |
| 420 | |
| 421 | [[nodiscard]] std::unique_ptr<AbstractLockFile> lockFile() override |
| 422 | { |
| 423 | return std::make_unique<FakeLockFile>(); |
| 424 | } |
| 425 | |
| 426 | void createEnclosingEntity() override |
| 427 | { |
| 428 | // nothing to do |
| 429 | } |
| 430 | |
| 431 | void setFilePath([[maybe_unused]] const QString &path) override |
| 432 | { |
| 433 | // nothing to do |
| 434 | } |
| 435 | |
| 436 | private: |
| 437 | std::shared_ptr<QIODevice> m_device; |
| 438 | }; |
| 439 | |