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