1/****************************************************************************
2**
3** Copyright (C) 2012 David Faure <faure@kde.org>
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtCore module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QtTest/QtTest>
30#include <qcoreapplication.h>
31#include <qstring.h>
32#include <qtemporaryfile.h>
33#include <qfile.h>
34#include <qdir.h>
35#include <qset.h>
36
37#if defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS)
38#include <unistd.h> // for geteuid
39#endif
40
41#if defined(Q_OS_WIN)
42# include <windows.h>
43#endif
44
45// Restore permissions so that the QTemporaryDir cleanup can happen
46class PermissionRestorer
47{
48 Q_DISABLE_COPY(PermissionRestorer)
49public:
50 explicit PermissionRestorer(const QString& path) : m_path(path) {}
51 ~PermissionRestorer() { restore(); }
52
53 inline void restore()
54 {
55 QFile file(m_path);
56#ifdef Q_OS_UNIX
57 file.setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner));
58#else
59 file.setPermissions(QFile::WriteOwner);
60 file.remove();
61#endif
62 }
63
64private:
65 const QString m_path;
66};
67
68class tst_QSaveFile : public QObject
69{
70 Q_OBJECT
71public slots:
72
73private slots:
74 void transactionalWrite();
75 void retryTransactionalWrite();
76 void textStreamManualFlush();
77 void textStreamAutoFlush();
78 void saveTwice();
79 void transactionalWriteNoPermissionsOnDir_data();
80 void transactionalWriteNoPermissionsOnDir();
81 void transactionalWriteNoPermissionsOnFile();
82 void transactionalWriteCanceled();
83 void transactionalWriteErrorRenaming();
84 void symlink();
85 void directory();
86
87#ifdef Q_OS_WIN
88 void alternateDataStream_data();
89 void alternateDataStream();
90#endif
91};
92
93static inline QByteArray msgCannotOpen(const QFileDevice &f)
94{
95 QString result = QStringLiteral("Cannot open ") + QDir::toNativeSeparators(pathName: f.fileName())
96 + QStringLiteral(": ") + f.errorString();
97 return result.toLocal8Bit();
98}
99
100void tst_QSaveFile::transactionalWrite()
101{
102 QTemporaryDir dir;
103 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
104 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
105 QFile::remove(fileName: targetFile);
106 QSaveFile file(targetFile);
107 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
108 QVERIFY(file.isOpen());
109 QCOMPARE(file.fileName(), targetFile);
110 QVERIFY(!QFile::exists(targetFile));
111
112 QCOMPARE(file.write("Hello"), Q_INT64_C(5));
113 QCOMPARE(file.error(), QFile::NoError);
114 QVERIFY(!QFile::exists(targetFile));
115
116 QVERIFY(file.commit());
117 QVERIFY(QFile::exists(targetFile));
118 QCOMPARE(file.fileName(), targetFile);
119
120 QFile reader(targetFile);
121 QVERIFY(reader.open(QIODevice::ReadOnly));
122 QCOMPARE(QString::fromLatin1(reader.readAll()), QString::fromLatin1("Hello"));
123
124 // check that permissions are the same as for QFile
125 const QString otherFile = dir.path() + QString::fromLatin1(str: "/otherfile");
126 QFile::remove(fileName: otherFile);
127 QFile other(otherFile);
128 other.open(flags: QIODevice::WriteOnly);
129 other.close();
130 QCOMPARE(QFile::permissions(targetFile), QFile::permissions(otherFile));
131}
132
133// QTBUG-77007: Simulate the case of an application with a loop prompting
134// to retry saving on failure. Create a read-only file first (Unix only)
135void tst_QSaveFile::retryTransactionalWrite()
136{
137#ifndef Q_OS_UNIX
138 QSKIP("This test is Unix only");
139#else
140 // root can open the read-only file for writing...
141 if (geteuid() == 0)
142 QSKIP("This test does not work as the root user");
143#endif
144 QTemporaryDir dir;
145 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
146
147 QString targetFile = dir.path() + QLatin1String("/outfile");
148 const QString readOnlyName = targetFile + QLatin1String(".ro");
149 {
150 QFile readOnlyFile(readOnlyName);
151 QVERIFY2(readOnlyFile.open(QIODevice::WriteOnly), msgCannotOpen(readOnlyFile).constData());
152 readOnlyFile.write(data: "Hello");
153 readOnlyFile.close();
154 auto permissions = readOnlyFile.permissions();
155 permissions &= ~(QFileDevice::WriteOwner | QFileDevice::WriteGroup | QFileDevice::WriteUser);
156 QVERIFY(readOnlyFile.setPermissions(permissions));
157 }
158
159 QSaveFile file(readOnlyName);
160 QVERIFY(!file.open(QIODevice::WriteOnly));
161
162 file.setFileName(targetFile);
163 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
164 QVERIFY(file.isOpen());
165 QCOMPARE(file.write("Hello"), Q_INT64_C(5));
166 QCOMPARE(file.error(), QFile::NoError);
167 QVERIFY(file.commit());
168}
169
170void tst_QSaveFile::saveTwice()
171{
172 // Check that we can reuse a QSaveFile object
173 // (and test the case of an existing target file)
174 QTemporaryDir dir;
175 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
176 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
177 QSaveFile file(targetFile);
178 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
179 QCOMPARE(file.write("Hello"), Q_INT64_C(5));
180 QVERIFY2(file.commit(), qPrintable(file.errorString()));
181
182 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
183 QCOMPARE(file.write("World"), Q_INT64_C(5));
184 QVERIFY2(file.commit(), qPrintable(file.errorString()));
185
186 QFile reader(targetFile);
187 QVERIFY2(reader.open(QIODevice::ReadOnly), msgCannotOpen(reader).constData());
188 QCOMPARE(QString::fromLatin1(reader.readAll()), QString::fromLatin1("World"));
189}
190
191void tst_QSaveFile::textStreamManualFlush()
192{
193 QTemporaryDir dir;
194 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
195 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
196 QSaveFile file(targetFile);
197 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
198
199 QTextStream ts(&file);
200 ts << "Manual flush";
201 ts.flush();
202 QCOMPARE(file.error(), QFile::NoError);
203 QVERIFY(!QFile::exists(targetFile));
204
205 QVERIFY(file.commit());
206 QFile reader(targetFile);
207 QVERIFY(reader.open(QIODevice::ReadOnly));
208 QCOMPARE(QString::fromLatin1(reader.readAll().constData()), QString::fromLatin1("Manual flush"));
209 QFile::remove(fileName: targetFile);
210}
211
212void tst_QSaveFile::textStreamAutoFlush()
213{
214 QTemporaryDir dir;
215 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
216 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
217 QSaveFile file(targetFile);
218 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
219
220 QTextStream ts(&file);
221 ts << "Auto-flush.";
222 // no flush
223 QVERIFY(file.commit()); // QIODevice::close will emit aboutToClose, which will flush the stream
224 QFile reader(targetFile);
225 QVERIFY(reader.open(QIODevice::ReadOnly));
226 QCOMPARE(QString::fromLatin1(reader.readAll().constData()), QString::fromLatin1("Auto-flush."));
227 QFile::remove(fileName: targetFile);
228}
229
230void tst_QSaveFile::transactionalWriteNoPermissionsOnDir_data()
231{
232 QTest::addColumn<bool>(name: "directWriteFallback");
233
234 QTest::newRow(dataTag: "default") << false;
235 QTest::newRow(dataTag: "directWriteFallback") << true;
236}
237
238void tst_QSaveFile::transactionalWriteNoPermissionsOnDir()
239{
240#ifdef Q_OS_UNIX
241#if !defined(Q_OS_VXWORKS)
242 if (::geteuid() == 0)
243 QSKIP("Test is not applicable with root privileges");
244#endif
245 QFETCH(bool, directWriteFallback);
246 QTemporaryDir dir;
247 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
248 QVERIFY(QFile(dir.path()).setPermissions(QFile::ReadOwner | QFile::ExeOwner));
249 PermissionRestorer permissionRestorer(dir.path());
250
251 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
252 QSaveFile firstTry(targetFile);
253 QVERIFY(!firstTry.open(QIODevice::WriteOnly));
254 QCOMPARE((int)firstTry.error(), (int)QFile::OpenError);
255 QVERIFY(!firstTry.commit());
256
257 // Now make an existing writable file
258 permissionRestorer.restore();
259 QFile f(targetFile);
260 QVERIFY(f.open(QIODevice::WriteOnly));
261 QCOMPARE(f.write("Hello"), Q_INT64_C(5));
262 f.close();
263
264 // Make the directory non-writable again
265 QVERIFY(QFile(dir.path()).setPermissions(QFile::ReadOwner | QFile::ExeOwner));
266
267 // And write to it again using QSaveFile; only works if directWriteFallback is enabled
268 QSaveFile file(targetFile);
269 file.setDirectWriteFallback(directWriteFallback);
270 QCOMPARE(file.directWriteFallback(), directWriteFallback);
271 if (directWriteFallback) {
272 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
273 QCOMPARE((int)file.error(), (int)QFile::NoError);
274 QCOMPARE(file.write("World"), Q_INT64_C(5));
275 QVERIFY(file.commit());
276
277 QFile reader(targetFile);
278 QVERIFY(reader.open(QIODevice::ReadOnly));
279 QCOMPARE(QString::fromLatin1(reader.readAll()), QString::fromLatin1("World"));
280 reader.close();
281
282 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
283 QCOMPARE((int)file.error(), (int)QFile::NoError);
284 QCOMPARE(file.write("W"), Q_INT64_C(1));
285 file.cancelWriting(); // no effect, as per the documentation
286 QVERIFY(file.commit());
287
288 QVERIFY(reader.open(QIODevice::ReadOnly));
289 QCOMPARE(QString::fromLatin1(reader.readAll()), QString::fromLatin1("W"));
290 } else {
291 QVERIFY(!file.open(QIODevice::WriteOnly));
292 QCOMPARE((int)file.error(), (int)QFile::OpenError);
293 }
294#endif
295}
296
297void tst_QSaveFile::transactionalWriteNoPermissionsOnFile()
298{
299#if defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS)
300 if (::geteuid() == 0)
301 QSKIP("Test is not applicable with root privileges");
302#endif
303 // Setup an existing but readonly file
304 QTemporaryDir dir;
305 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
306 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
307 QFile file(targetFile);
308 PermissionRestorer permissionRestorer(targetFile);
309 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
310 QCOMPARE(file.write("Hello"), Q_INT64_C(5));
311 file.close();
312 file.setPermissions(QFile::ReadOwner);
313 QVERIFY(!file.open(QIODevice::WriteOnly));
314
315 // Try saving into it
316 {
317 QSaveFile saveFile(targetFile);
318 QVERIFY(!saveFile.open(QIODevice::WriteOnly)); // just like QFile
319 }
320 QVERIFY(file.exists());
321}
322
323void tst_QSaveFile::transactionalWriteCanceled()
324{
325 QTemporaryDir dir;
326 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
327 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
328 QFile::remove(fileName: targetFile);
329 QSaveFile file(targetFile);
330 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
331
332 QTextStream ts(&file);
333 ts << "This writing operation will soon be canceled.\n";
334 ts.flush();
335 QCOMPARE(file.error(), QFile::NoError);
336 QVERIFY(!QFile::exists(targetFile));
337
338 // We change our mind, let's abort writing
339 file.cancelWriting();
340
341 QVERIFY(!file.commit());
342
343 QVERIFY(!QFile::exists(targetFile)); // temp file was discarded
344 QCOMPARE(file.fileName(), targetFile);
345}
346
347void tst_QSaveFile::transactionalWriteErrorRenaming()
348{
349#if defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS)
350 if (::geteuid() == 0)
351 QSKIP("Test is not applicable with root privileges");
352#endif
353 QTemporaryDir dir;
354 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
355 const QString targetFile = dir.path() + QString::fromLatin1(str: "/outfile");
356 QSaveFile file(targetFile);
357 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
358 QCOMPARE(file.write("Hello"), qint64(5));
359 QVERIFY(!QFile::exists(targetFile));
360#ifdef Q_OS_UNIX
361 // Make rename() fail for lack of permissions in the directory
362 QFile dirAsFile(dir.path()); // yay, I have to use QFile to change a dir's permissions...
363 QVERIFY(dirAsFile.setPermissions(QFile::Permissions{})); // no permissions
364 PermissionRestorer permissionRestorer(dir.path());
365#else
366 // Windows: Make rename() fail for lack of permissions on an existing target file
367 QFile existingTargetFile(targetFile);
368 QVERIFY2(existingTargetFile.open(QIODevice::WriteOnly), msgCannotOpen(existingTargetFile).constData());
369 QCOMPARE(file.write("Target"), qint64(6));
370 existingTargetFile.close();
371 QVERIFY(existingTargetFile.setPermissions(QFile::ReadOwner));
372 PermissionRestorer permissionRestorer(targetFile);
373#endif
374
375 // The saving should fail.
376 QVERIFY(!file.commit());
377#ifdef Q_OS_UNIX
378 QVERIFY(!QFile::exists(targetFile)); // renaming failed
379#endif
380 QCOMPARE(file.error(), QFile::RenameError);
381}
382
383void tst_QSaveFile::symlink()
384{
385#ifdef Q_OS_UNIX
386 QByteArray someData = "some data";
387 QTemporaryDir dir;
388 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
389
390 const QString targetFile = dir.path() + QLatin1String("/outfile");
391 const QString linkFile = dir.path() + QLatin1String("/linkfile");
392 {
393 QFile file(targetFile);
394 QVERIFY2(file.open(QIODevice::WriteOnly), msgCannotOpen(file).constData());
395 QCOMPARE(file.write("Hello"), Q_INT64_C(5));
396 file.close();
397 }
398
399 QVERIFY(QFile::link(targetFile, linkFile));
400
401 QString canonical = QFileInfo(linkFile).canonicalFilePath();
402 QCOMPARE(canonical, QFileInfo(targetFile).canonicalFilePath());
403
404 // Try saving into it
405 {
406 QSaveFile saveFile(linkFile);
407 QVERIFY(saveFile.open(QIODevice::WriteOnly));
408 QCOMPARE(saveFile.write(someData), someData.size());
409 saveFile.commit();
410
411 //Check that the linkFile is still a link and still has the same canonical path
412 QFileInfo info(linkFile);
413 QVERIFY(info.isSymLink());
414 QCOMPARE(QFileInfo(linkFile).canonicalFilePath(), canonical);
415
416 QFile file(targetFile);
417 QVERIFY2(file.open(QIODevice::ReadOnly), msgCannotOpen(file).constData());
418 QCOMPARE(file.readAll(), someData);
419 file.remove();
420 }
421
422 // Save into a symbolic link that point to a removed file
423 someData = "more stuff";
424 {
425 QSaveFile saveFile(linkFile);
426 QVERIFY(saveFile.open(QIODevice::WriteOnly));
427 QCOMPARE(saveFile.write(someData), someData.size());
428 saveFile.commit();
429
430 QFileInfo info(linkFile);
431 QVERIFY(info.isSymLink());
432 QCOMPARE(QFileInfo(linkFile).canonicalFilePath(), canonical);
433
434 QFile file(targetFile);
435 QVERIFY2(file.open(QIODevice::ReadOnly), msgCannotOpen(file).constData());
436 QCOMPARE(file.readAll(), someData);
437 }
438
439 // link to a link in another directory
440 QTemporaryDir dir2;
441 QVERIFY2(dir2.isValid(), qPrintable(dir2.errorString()));
442
443 const QString linkFile2 = dir2.path() + QLatin1String("/linkfile");
444 QVERIFY(QFile::link(linkFile, linkFile2));
445 QCOMPARE(QFileInfo(linkFile2).canonicalFilePath(), canonical);
446
447
448 someData = "hello everyone";
449
450 {
451 QSaveFile saveFile(linkFile2);
452 QVERIFY(saveFile.open(QIODevice::WriteOnly));
453 QCOMPARE(saveFile.write(someData), someData.size());
454 saveFile.commit();
455
456 QFile file(targetFile);
457 QVERIFY2(file.open(QIODevice::ReadOnly), msgCannotOpen(file).constData());
458 QCOMPARE(file.readAll(), someData);
459 }
460
461 //cyclic link
462 const QString cyclicLink = dir.path() + QLatin1String("/cyclic");
463 QVERIFY(QFile::link(cyclicLink, cyclicLink));
464 {
465 QSaveFile saveFile(cyclicLink);
466 QVERIFY(saveFile.open(QIODevice::WriteOnly));
467 QCOMPARE(saveFile.write(someData), someData.size());
468 saveFile.commit();
469
470 QFile file(cyclicLink);
471 QVERIFY2(file.open(QIODevice::ReadOnly), msgCannotOpen(file).constData());
472 QCOMPARE(file.readAll(), someData);
473 }
474
475 //cyclic link2
476 QVERIFY(QFile::link(cyclicLink + QLatin1Char('1'), cyclicLink + QLatin1Char('2')));
477 QVERIFY(QFile::link(cyclicLink + QLatin1Char('2'), cyclicLink + QLatin1Char('1')));
478
479 {
480 QSaveFile saveFile(cyclicLink + QLatin1Char('1'));
481 QVERIFY(saveFile.open(QIODevice::WriteOnly));
482 QCOMPARE(saveFile.write(someData), someData.size());
483 saveFile.commit();
484
485 // the explicit file becomes a file instead of a link
486 QVERIFY(!QFileInfo(cyclicLink + QLatin1Char('1')).isSymLink());
487 QVERIFY(QFileInfo(cyclicLink + QLatin1Char('2')).isSymLink());
488
489 QFile file(cyclicLink + QLatin1Char('1'));
490 QVERIFY2(file.open(QIODevice::ReadOnly), msgCannotOpen(file).constData());
491 QCOMPARE(file.readAll(), someData);
492 }
493#endif
494}
495
496void tst_QSaveFile::directory()
497{
498 QTemporaryDir dir;
499 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
500
501 const QString subdir = dir.path() + QLatin1String("/subdir");
502 QVERIFY(QDir(dir.path()).mkdir(QStringLiteral("subdir")));
503 {
504 QFile sf(subdir);
505 QVERIFY(!sf.open(QIODevice::WriteOnly));
506 }
507
508#ifdef Q_OS_UNIX
509 //link to a directory
510 const QString linkToDir = dir.path() + QLatin1String("/linkToDir");
511 QVERIFY(QFile::link(subdir, linkToDir));
512
513 {
514 QFile sf(linkToDir);
515 QVERIFY(!sf.open(QIODevice::WriteOnly));
516 }
517#endif
518}
519
520#ifdef Q_OS_WIN
521void tst_QSaveFile::alternateDataStream_data()
522{
523 QTest::addColumn<bool>("directWriteFallback");
524 QTest::addColumn<bool>("success");
525
526 QTest::newRow("default") << false << false;
527 QTest::newRow("directWriteFallback") << true << true;
528}
529
530void tst_QSaveFile::alternateDataStream()
531{
532 QFETCH(bool, directWriteFallback);
533 QFETCH(bool, success);
534 static const char newContent[] = "New content\r\n";
535
536 QTemporaryDir dir;
537 QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
538 QString baseName = dir.path() + QLatin1String("/base");
539 {
540 QFile baseFile(baseName);
541 QVERIFY2(baseFile.open(QIODevice::ReadWrite), qPrintable(baseFile.errorString()));
542 }
543
544 // First, create a file with old content
545 QString adsName = baseName + QLatin1String(":outfile");
546 {
547 QFile targetFile(adsName);
548 if (!targetFile.open(QIODevice::ReadWrite))
549 QSKIP("Failed to ceate ADS file (" + targetFile.errorString().toUtf8()
550 + "). Temp dir is FAT?");
551 targetFile.write("Old content\r\n");
552 }
553
554 // And write to it again using QSaveFile; only works if directWriteFallback is enabled
555 QSaveFile file(adsName);
556 file.setDirectWriteFallback(directWriteFallback);
557 QCOMPARE(file.directWriteFallback(), directWriteFallback);
558
559 if (success) {
560 QVERIFY2(file.open(QIODevice::WriteOnly), qPrintable(file.errorString()));
561 file.write(newContent);
562 QVERIFY2(file.commit(), qPrintable(file.errorString()));
563
564 // check the contents
565 QFile targetFile(adsName);
566 QVERIFY2(targetFile.open(QIODevice::ReadOnly), qPrintable(targetFile.errorString()));
567 QByteArray contents = targetFile.readAll();
568 QCOMPARE(contents, QByteArray(newContent));
569 } else {
570 QVERIFY(!file.open(QIODevice::WriteOnly));
571 }
572}
573#endif
574
575QTEST_MAIN(tst_QSaveFile)
576#include "tst_qsavefile.moc"
577

source code of qtbase/tests/auto/corelib/io/qsavefile/tst_qsavefile.cpp