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 Sets the \a permissions the file shall be given on successful commit().
323
324 While being written via QSaveFile the file may have more restrictive
325 permissions.
326*/
327bool QSaveFile::setPermissions(Permissions permissions)
328{
329 Q_D(QSaveFile);
330 d->finalPermissions = permissions;
331 return true;
332}
333
334/*!
335 \reimp
336 Reports the permissions the file shall be given on successful commit().
337*/
338QFileDevice::Permissions QSaveFile::permissions() const
339{
340 if (d_func()->finalPermissions)
341 return *d_func()->finalPermissions;
342 return QFileDevice::permissions();
343}
344
345/*!
346 Commits the changes to disk, if all previous writes were successful.
347
348 It is mandatory to call this at the end of the saving operation, otherwise the file will be
349 discarded.
350
351 If an error happened during writing, deletes the temporary file and returns \c false.
352 Otherwise, renames it to the final fileName and returns \c true on success.
353 Finally, closes the device.
354
355 \sa cancelWriting()
356*/
357bool QSaveFile::commit()
358{
359 Q_D(QSaveFile);
360 if (!d->fileEngine)
361 return false;
362
363 if (!isOpen()) {
364 qWarning("QSaveFile::commit: File (%ls) is not open", qUtf16Printable(fileName()));
365 return false;
366 }
367 if (d->finalPermissions)
368 QFileDevice::setPermissions(*d->finalPermissions); // Records error on failure.
369 QFileDevice::close(); // calls flush()
370
371 const auto &fe = d->fileEngine;
372
373 // Sync to disk if possible. Ignore errors (e.g. not supported).
374 fe->syncToDisk();
375
376 // ensure we act on either a close()/flush() failure or a previous write()
377 // problem
378 if (d->error == QFileDevice::NoError)
379 d->error = d->writeError;
380 d->writeError = QFileDevice::NoError;
381
382 if (d->useTemporaryFile) {
383 if (d->error != QFileDevice::NoError) {
384 fe->remove();
385 return false;
386 }
387 // atomically replace old file with new file
388 // Can't use QFile::rename for that, must use the file engine directly
389 Q_ASSERT(fe);
390 if (!fe->renameOverwrite(d->finalFileName)) {
391 d->setError(fe->error(), fe->errorString());
392 fe->remove();
393 return false;
394 }
395 }
396
397 // Return true if all previous write() calls succeeded and if close(),
398 // flush() and (when relevant) setPermissions() succeeded.
399 return d->error == QFileDevice::NoError;
400}
401
402/*!
403 Cancels writing the new file.
404
405 If the application changes its mind while saving, it can call cancelWriting(),
406 which sets an error code so that commit() will discard the temporary file.
407
408 Alternatively, it can simply make sure not to call commit().
409
410 Further write operations are possible after calling this method, but none
411 of it will have any effect, the written file will be discarded.
412
413 This method has no effect when direct write fallback is used. This is the case
414 when saving over an existing file in a readonly directory: no temporary file can
415 be created, so the existing file is overwritten no matter what, and cancelWriting()
416 cannot do anything about that, the contents of the existing file will be lost.
417
418 \sa commit()
419*/
420void QSaveFile::cancelWriting()
421{
422 Q_D(QSaveFile);
423 if (!isOpen())
424 return;
425 d->setError(QFileDevice::WriteError, QSaveFile::tr("Writing canceled by application"));
426 d->writeError = QFileDevice::WriteError;
427}
428
429/*!
430 \reimp
431*/
432qint64 QSaveFile::writeData(const char *data, qint64 len)
433{
434 Q_D(QSaveFile);
435 if (d->writeError != QFileDevice::NoError)
436 return -1;
437
438 const qint64 ret = QFileDevice::writeData(data, len);
439
440 if (d->error != QFileDevice::NoError)
441 d->writeError = d->error;
442 return ret;
443}
444
445/*!
446 Allows writing over the existing file if necessary.
447
448 QSaveFile creates a temporary file in the same directory as the final
449 file and atomically renames it. However this is not possible if the
450 directory permissions do not allow creating new files.
451 In order to preserve atomicity guarantees, open() fails when it
452 cannot create the temporary file.
453
454 In order to allow users to edit files with write permissions in a
455 directory with restricted permissions, call setDirectWriteFallback() with
456 \a enabled set to true, and the following calls to open() will fallback to
457 opening the existing file directly and writing into it, without the use of
458 a temporary file.
459 This does not have atomicity guarantees, i.e. an application crash or
460 for instance a power failure could lead to a partially-written file on disk.
461 It also means cancelWriting() has no effect, in such a case.
462
463 Typically, to save documents edited by the user, call setDirectWriteFallback(true),
464 and to save application internal files (configuration files, data files, ...), keep
465 the default setting which ensures atomicity.
466
467 \sa directWriteFallback()
468*/
469void QSaveFile::setDirectWriteFallback(bool enabled)
470{
471 Q_D(QSaveFile);
472 d->directWriteFallback = enabled;
473}
474
475/*!
476 Returns \c true if the fallback solution for saving files in read-only
477 directories is enabled.
478
479 \sa setDirectWriteFallback()
480*/
481bool QSaveFile::directWriteFallback() const
482{
483 Q_D(const QSaveFile);
484 return d->directWriteFallback;
485}
486
487QT_END_NAMESPACE
488
489#include "moc_qsavefile.cpp"
490
491#endif // QT_CONFIG(temporaryfile)