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