Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qsavefile.cpp
Go to the documentation of this file.
1// Copyright (C) 2012 David Faure <faure@kde.org>
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:guaranteed-behavior
4
5#include "qsavefile.h"
6
7#if QT_CONFIG(temporaryfile)
8
9#include "qplatformdefs.h"
10#include "private/qsavefile_p.h"
11#include "qfileinfo.h"
12#include "qabstractfileengine_p.h"
13#include <QtCore/qcoreapplication.h>
14#include "qdebug.h"
15#include "qtemporaryfile.h"
16#include <QtCore/qttranslation.h>
17#include "private/qiodevice_p.h"
18#include "private/qtemporaryfile_p.h"
19#ifdef Q_OS_UNIX
20#include <errno.h>
21#endif
22
23QT_BEGIN_NAMESPACE
24
25using namespace Qt::StringLiterals;
26
27QSaveFilePrivate::QSaveFilePrivate()
28 : writeError(QFileDevice::NoError),
29 useTemporaryFile(true),
30 directWriteFallback(false)
31{
32}
33
34QSaveFilePrivate::~QSaveFilePrivate()
35{
36}
37
38bool QSaveFilePrivate::open(QIODevice::OpenMode mode)
39{
40 writeError = QFileDevice::NoError;
41 if ((mode & (QIODevice::ReadOnly | QIODevice::WriteOnly)) == 0) {
42 qWarning("QSaveFile::open: Open mode not specified");
43 return false;
44 }
45 // In the future we could implement ReadWrite by copying from the existing file to the temp file...
46 // The implications of NewOnly and ExistingOnly when used with QSaveFile need to be considered carefully...
47 if (mode & (QIODevice::ReadOnly | QIODevice::Append | QIODevice::NewOnly
48 | QIODevice::ExistingOnly)) {
49 qWarning("QSaveFile::open: Unsupported open mode 0x%x", uint(mode.toInt()));
50 return false;
51 }
52
53 QFileInfo priorFile(fileName);
54
55 // If the file exists, get its permissions. This is one of the more
56 // comprehensive queries we have to make, allowing QFileInfo to cache as
57 // much as possible early on. On Unix, this is both access() and stat().
58 bool exists = false;
59 QFile::Permissions perms = priorFile.permissions();
60 if (perms) {
61 // if it has permissions, it exists
62 exists = true;
63
64 // If we haven't already been given other permissions to use, save
65 // these. For new files, see below.
66 // These may be overridden later by setPermissions(), of course.
67 if (!finalPermissions)
68 finalPermissions = perms;
69 } else {
70 exists = priorFile.exists();
71 // if exists = true, we exit below with "is not writable"
72 }
73
74 // Check if existing file is writable:
75 if (exists && !perms.testAnyFlag(QFile::WriteUser)) {
76 setError(QFileDevice::WriteError,
77 QSaveFile::tr("Existing file %1 is not writable").arg(fileName));
78 writeError = QFileDevice::WriteError;
79 return false;
80 }
81
82 // Check if it is a directory.
83 if (priorFile.isDir()) {
84 setError(QFileDevice::WriteError, QSaveFile::tr("Filename refers to a directory"));
85 writeError = QFileDevice::WriteError;
86 return false;
87 }
88
89 // Resolve symlinks. Don't use QFileInfo::canonicalFilePath so it still give
90 // the expected target even if the file does not exist
91 finalFileName = fileName;
92 if (priorFile.isSymLink()) {
93 int maxDepth = 128;
94 for (QString target; maxDepth; --maxDepth) {
95 target = priorFile.symLinkTarget();
96 if (target.isEmpty())
97 break;
98 priorFile.setFile(target);
99 }
100 if (maxDepth > 0)
101 finalFileName = priorFile.filePath();
102 }
103
104 auto openDirectly = [this, mode]() {
105 fileEngine = QAbstractFileEngine::create(finalFileName);
106 if (fileEngine->open(mode | QIODevice::Unbuffered)) {
107 useTemporaryFile = false;
108 return true;
109 }
110 return false;
111 };
112
113 const char *directWriteReason = nullptr;
114#ifdef Q_OS_WIN
115 // check if it is an Alternate Data Stream
116 if (finalFileName == fileName && fileName.indexOf(u':', 2) > 1)
117 directWriteReason = QT_TRANSLATE_NOOP("QSaveFile", "target is an Alternate Data Stream");
118#elif defined(Q_OS_ANDROID)
119 // check if it is a content:// URL
120 if (fileName.startsWith("content://"_L1))
121 directWriteReason = QT_TRANSLATE_NOOP("QSaveFile", "target is a content:// virtual file");
122#endif
123 if (
124#if defined(Q_OS_WIN) || defined(Q_OS_ANDROID)
125 !directWriteReason &&
126#endif // Q_OS_WIN || Q_OS_ANDROID
127 priorFile.exists() && !priorFile.isFile()) {
128 directWriteReason = QT_TRANSLATE_NOOP("QSaveFile", "target exists and is not a regular file");
129 }
130 if (directWriteReason) {
131 // yes, we can't rename onto it...
132 if (directWriteFallback) {
133 if (openDirectly())
134 return true;
135 setError(fileEngine->error(), fileEngine->errorString());
136 fileEngine.reset();
137 } else {
138 setError(QFileDevice::OpenError,
139 QSaveFile::tr("QSaveFile cannot open '%1' "
140 "without direct write fallback enabled: %2.")
141 .arg(QDir::toNativeSeparators(fileName),
142 QSaveFile::tr(directWriteReason)));
143 }
144 return false;
145 }
146
147 fileEngine.reset(new QTemporaryFileEngine(&finalFileName,
148 QTemporaryFileEngine::Win32NonShared));
149 // For new files, when other permissions haven't been specified, we want the
150 // same permissions QFile::open() would get us. These depend on vagaries of
151 // the operating system (Unix's umask(), for example) that we don't want to
152 // second guess, so let open() do its thing and then read what it's done
153 // before closing and reopening with 0600 for the real writing.
154 if (!finalPermissions && QTemporaryFileEngine::CreatesWithFileMode) {
155 Q_ASSERT(!priorFile.exists());
156 // Dry-run of what follows, but with different permissions.
157 static_cast<QTemporaryFileEngine *>(fileEngine.get())->initialize(finalFileName, 0666);
158 if (fileEngine->open(mode | QIODevice::Unbuffered)) {
159 finalPermissions = QFileDevicePrivate::permissions();
160 fileEngine->close();
161 }
162 fileEngine->remove();
163 }
164
165 // We'll set the target file's permissions on commit() but, until then,
166 // let's ensure the temporary file is not accessible to a third party.
167 static_cast<QTemporaryFileEngine *>(fileEngine.get())->initialize(finalFileName, 0600);
168 // Same as in QFile: QIODevice provides the buffering, so there's no need to
169 // request it from the file engine.
170 if (!fileEngine->open(mode | QIODevice::Unbuffered)) {
171 QFileDevice::FileError err = fileEngine->error();
172#ifdef Q_OS_UNIX
173 if (directWriteFallback && err == QFileDevice::OpenError && errno == EACCES) {
174 if (openDirectly())
175 return true;
176 err = fileEngine->error();
177 }
178#endif
179 if (err == QFileDevice::UnspecifiedError)
180 err = QFileDevice::OpenError;
181 setError(err, fileEngine->errorString());
182 fileEngine.reset();
183 return false;
184 }
185 useTemporaryFile = true;
186 return true;
187}
188
189QFileDevice::Permissions QSaveFilePrivate::permissions() const
190{
191 if (finalPermissions)
192 return *finalPermissions;
193 return QFileDevicePrivate::permissions();
194}
195
196bool QSaveFilePrivate::setPermissions(QFileDevice::Permissions perms)
197{
198 finalPermissions = perms;
199 return true;
200}
201
202/*!
203 \class QSaveFile
204 \inmodule QtCore
205 \brief The QSaveFile class provides an interface for safely writing to files.
206
207 \ingroup io
208
209 \reentrant
210
211 \since 5.1
212
213 QSaveFile is an I/O device for writing text and binary files, without losing
214 existing data if the writing operation fails.
215
216 While writing, the contents will be written to a temporary file, and if
217 no error happened, commit() will move it to the final file. This ensures that
218 no data at the final file is lost in case an error happens while writing,
219 and no partially-written file is ever present at the final location. Always
220 use QSaveFile when saving entire documents to disk.
221
222 QSaveFile automatically detects errors while writing, such as the full partition
223 situation, where write() cannot write all the bytes. It will remember that
224 an error happened, and will discard the temporary file in commit().
225
226 Much like with QFile, the file is opened with open(). Data is usually read
227 and written using QDataStream or QTextStream, but you can also directly call
228 \l write().
229
230 Unlike QFile, calling close() is not allowed. commit() replaces it. If commit()
231 was not called and the QSaveFile instance is destroyed, the temporary file is
232 discarded.
233
234 To abort saving due to an application error, call cancelWriting(), so that
235 even a call to commit() later on will not save.
236
237 \sa QTextStream, QDataStream, QFileInfo, QDir, QFile, QTemporaryFile
238*/
239
240/*!
241 Constructs a new file object with the given \a parent.
242 You need to call setFileName() before open().
243*/
244QSaveFile::QSaveFile(QObject *parent)
245 : QFileDevice(*new QSaveFilePrivate, parent)
246{
247}
248
249/*!
250 Constructs a new file object with the given \a parent to represent the
251 file with the specified \a name.
252*/
253QSaveFile::QSaveFile(const QString &name, QObject *parent)
254 : QFileDevice(*new QSaveFilePrivate, parent)
255{
256 Q_D(QSaveFile);
257 d->fileName = name;
258}
259
260/*!
261 \fn QSaveFile::QSaveFile(const std::filesystem::path &path, QObject *parent)
262 \since 6.11
263
264 Constructs a new file object with the given \a parent to represent the
265 file with the specified \a path.
266*/
267
268/*!
269 Destroys the file object, discarding the saved contents unless commit() was called.
270*/
271QSaveFile::~QSaveFile()
272{
273 Q_D(QSaveFile);
274 if (isOpen()) {
275 QFileDevice::close();
276 Q_ASSERT(d->fileEngine);
277 d->fileEngine->remove();
278 }
279}
280
281/*!
282 Returns the name set by setFileName() or to the QSaveFile
283 constructor.
284
285 \sa setFileName()
286*/
287QString QSaveFile::fileName() const
288{
289 return d_func()->fileName;
290}
291
292/*!
293 \fn std::filesystem::path QSaveFile::filesystemFileName() const
294 \since 6.11
295 Returns fileName() as \c{std::filesystem::path}.
296*/
297
298/*!
299 Sets the \a name of the file. The name can have no path, a
300 relative path, or an absolute path.
301
302 \sa QFile::setFileName(), fileName()
303*/
304void QSaveFile::setFileName(const QString &name)
305{
306 d_func()->fileName = name;
307}
308
309/*!
310 \fn QSaveFile::setFileName(const std::filesystem::path &name)
311 \since 6.11
312 \overload
313*/
314
315/*!
316 Opens the file using the given \a mode flags.
317
318 Returns \c true if successful; otherwise returns \c false.
319
320 Important: The flags for \a mode must include \l QIODeviceBase::WriteOnly. Other
321 common flags you can use are \l Text and \l Unbuffered. Flags not supported at the
322 moment are \l ReadOnly (and therefore \l ReadWrite), \l Append, \l NewOnly and \l ExistingOnly;
323 they will generate a runtime warning.
324
325 \sa setFileName(), QT_USE_NODISCARD_FILE_OPEN
326*/
327bool QSaveFile::open(OpenMode mode)
328{
329 Q_D(QSaveFile);
330 if (isOpen()) {
331 qWarning("QSaveFile::open: File (%ls) already open", qUtf16Printable(fileName()));
332 return false;
333 }
334 unsetError();
335 if (!d->open(mode))
336 return false;
337 return QFileDevice::open(mode);
338}
339
340/*!
341 \reimp
342 This method has been made private so that it cannot be called, in order to prevent mistakes.
343 In order to finish writing the file, call commit().
344 If instead you want to abort writing, call cancelWriting().
345*/
346void QSaveFile::close()
347{
348 qFatal("QSaveFile::close called");
349}
350
351/*!
352 \fn bool QSaveFile::setPermissions(Permissions permissions)
353 \reimp
354 \since 6.12
355 Sets the \a permissions the file shall be given on successful commit().
356
357 While being written via QSaveFile the file may have more restrictive
358 permissions.
359*/
360
361/*!
362 \fn QFileDevice::Permissions QSaveFile::permissions() const
363 \reimp
364 \since 6.12
365 Reports the permissions the file shall be given on successful commit().
366*/
367
368/*!
369 Commits the changes to disk, if all previous writes were successful.
370
371 It is mandatory to call this at the end of the saving operation, otherwise the file will be
372 discarded.
373
374 If an error happened during writing, deletes the temporary file and returns \c false.
375 Otherwise, renames it to the final fileName and returns \c true on success.
376 Finally, closes the device.
377
378 \sa cancelWriting()
379*/
380bool QSaveFile::commit()
381{
382 Q_D(QSaveFile);
383 if (!d->fileEngine)
384 return false;
385
386 if (!isOpen()) {
387 qWarning("QSaveFile::commit: File (%ls) is not open", qUtf16Printable(fileName()));
388 return false;
389 }
390 if (d->finalPermissions)
391 d->QFileDevicePrivate::setPermissions(*d->finalPermissions); // Records error on failure.
392 QFileDevice::close(); // calls flush()
393
394 const auto &fe = d->fileEngine;
395
396 // Sync to disk if possible. Ignore errors (e.g. not supported).
397 fe->syncToDisk();
398
399 // ensure we act on either a close()/flush() failure or a previous write()
400 // problem
401 if (d->error == QFileDevice::NoError)
402 d->error = d->writeError;
403 d->writeError = QFileDevice::NoError;
404
405 if (d->useTemporaryFile) {
406 if (d->error != QFileDevice::NoError) {
407 fe->remove();
408 return false;
409 }
410 // atomically replace old file with new file
411 // Can't use QFile::rename for that, must use the file engine directly
412 Q_ASSERT(fe);
413 if (!fe->renameOverwrite(d->finalFileName)) {
414 d->setError(fe->error(), fe->errorString());
415 fe->remove();
416 return false;
417 }
418 }
419
420 // Return true if all previous write() calls succeeded and if close(),
421 // flush() and (when relevant) setPermissions() succeeded.
422 return d->error == QFileDevice::NoError;
423}
424
425/*!
426 Cancels writing the new file.
427
428 If the application changes its mind while saving, it can call cancelWriting(),
429 which sets an error code so that commit() will discard the temporary file.
430
431 Alternatively, it can simply make sure not to call commit().
432
433 Further write operations are possible after calling this method, but none
434 of it will have any effect, the written file will be discarded.
435
436 This method has no effect when direct write fallback is used. This is the case
437 when saving over an existing file in a readonly directory: no temporary file can
438 be created, so the existing file is overwritten no matter what, and cancelWriting()
439 cannot do anything about that, the contents of the existing file will be lost.
440
441 \sa commit()
442*/
443void QSaveFile::cancelWriting()
444{
445 Q_D(QSaveFile);
446 if (!isOpen())
447 return;
448 d->setError(QFileDevice::WriteError, QSaveFile::tr("Writing canceled by application"));
449 d->writeError = QFileDevice::WriteError;
450}
451
452/*!
453 \reimp
454*/
455qint64 QSaveFile::writeData(const char *data, qint64 len)
456{
457 Q_D(QSaveFile);
458 if (d->writeError != QFileDevice::NoError)
459 return -1;
460
461 const qint64 ret = QFileDevice::writeData(data, len);
462
463 if (d->error != QFileDevice::NoError)
464 d->writeError = d->error;
465 return ret;
466}
467
468/*!
469 Allows writing over the existing file if necessary.
470
471 QSaveFile creates a temporary file in the same directory as the final
472 file and atomically renames it. However this is not possible if the
473 directory permissions do not allow creating new files.
474 In order to preserve atomicity guarantees, open() fails when it
475 cannot create the temporary file.
476
477 In order to allow users to edit files with write permissions in a
478 directory with restricted permissions, call setDirectWriteFallback() with
479 \a enabled set to true, and the following calls to open() will fallback to
480 opening the existing file directly and writing into it, without the use of
481 a temporary file.
482 This does not have atomicity guarantees, i.e. an application crash or
483 for instance a power failure could lead to a partially-written file on disk.
484 It also means cancelWriting() has no effect, in such a case.
485
486 Typically, to save documents edited by the user, call setDirectWriteFallback(true),
487 and to save application internal files (configuration files, data files, ...), keep
488 the default setting which ensures atomicity.
489
490 \sa directWriteFallback()
491*/
492void QSaveFile::setDirectWriteFallback(bool enabled)
493{
494 Q_D(QSaveFile);
495 d->directWriteFallback = enabled;
496}
497
498/*!
499 Returns \c true if the fallback solution for saving files in read-only
500 directories is enabled.
501
502 \sa setDirectWriteFallback()
503*/
504bool QSaveFile::directWriteFallback() const
505{
506 Q_D(const QSaveFile);
507 return d->directWriteFallback;
508}
509
510QT_END_NAMESPACE
511
512#include "moc_qsavefile.cpp"
513
514#endif // QT_CONFIG(temporaryfile)