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