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