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