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
28using namespace Qt::StringLiterals;
29
30// Wraps a QLockFile to allow no-op locking when locking is not supported (e.g. QIODevice based backends).
31class AbstractLockFile
32{
33public:
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
45class RealLockFile : public AbstractLockFile
46{
47public:
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
78private:
79 std::unique_ptr<QLockFile> m_lockFile;
80};
81
82class FakeLockFile : public AbstractLockFile
83{
84public:
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
111private:
112 bool m_locked = false;
113};
114
115class KConfigIniBackendAbstractDevice
116{
117public:
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
138class KConfigIniBackendNullDevice : public KConfigIniBackendAbstractDevice
139{
140public:
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
182class KConfigIniBackendPathDevice : public KConfigIniBackendAbstractDevice
183{
184public:
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
371private:
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
386class KConfigIniBackendQIODevice : public KConfigIniBackendAbstractDevice
387{
388public:
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
436private:
437 std::shared_ptr<QIODevice> m_device;
438};
439

source code of kconfig/src/core/kconfiginibackendreader_p.h