Support Alternate Data Streams in QSaveFile

We can't use MoveFile to do atomic commits on an ADS, so QSaveFile needs
to detect when the target name is ADS and then use the direct fallback
mode.

[ChangeLog][QtCore][QSaveFile] Saving to Alternate Data Streams on NTFS
on Windows is now possible, but requires setDirectWriteFallback(true).

Task-number: QTBUG-47379
Change-Id: I81480fdb578d4d43b3fcfffd14d4bc062ae1750d
Reviewed-by: Kai Koehne <kai.koehne@qt.io>
Reviewed-by: David Faure <david.faure@kdab.com>
Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@qt.io>
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Thiago Macieira 2017-07-25 18:08:30 -07:00
parent 9a3ce25f98
commit 66500b9d75
2 changed files with 92 additions and 5 deletions

View File

@ -231,6 +231,37 @@ bool QSaveFile::open(OpenMode mode)
d->finalFileName = existingFile.filePath();
}
auto openDirectly = [&]() {
d->fileEngine = QAbstractFileEngine::create(d->finalFileName);
if (d->fileEngine->open(mode | QIODevice::Unbuffered)) {
d->useTemporaryFile = false;
QFileDevice::open(mode);
return true;
}
return false;
};
#ifdef Q_OS_WIN
// check if it is an Alternate Data Stream
if (d->finalFileName == d->fileName && d->fileName.indexOf(QLatin1Char(':'), 2) > 1) {
// yes, we can't rename onto it...
if (d->directWriteFallback) {
if (openDirectly())
return true;
d->setError(d->fileEngine->error(), d->fileEngine->errorString());
delete d->fileEngine;
d->fileEngine = 0;
} else {
QString msg =
QSaveFile::tr("QSaveFile cannot open '%1' without direct write fallback "
"enabled: path contains an Alternate Data Stream specifier")
.arg(QDir::toNativeSeparators(d->fileName));
d->setError(QFileDevice::OpenError, msg);
}
return false;
}
#endif
d->fileEngine = new QTemporaryFileEngine(&d->finalFileName);
// if the target file exists, we'll copy its permissions below,
// but until then, let's ensure the temporary file is not accessible
@ -243,12 +274,8 @@ bool QSaveFile::open(OpenMode mode)
#ifdef Q_OS_UNIX
if (d->directWriteFallback && err == QFileDevice::OpenError && errno == EACCES) {
delete d->fileEngine;
d->fileEngine = QAbstractFileEngine::create(d->finalFileName);
if (d->fileEngine->open(mode | QIODevice::Unbuffered)) {
d->useTemporaryFile = false;
QFileDevice::open(mode);
if (openDirectly())
return true;
}
err = d->fileEngine->error();
}
#endif

View File

@ -82,6 +82,11 @@ private slots:
void transactionalWriteErrorRenaming();
void symlink();
void directory();
#ifdef Q_OS_WIN
void alternateDataStream_data();
void alternateDataStream();
#endif
};
static inline QByteArray msgCannotOpen(const QFileDevice &f)
@ -474,5 +479,60 @@ void tst_QSaveFile::directory()
#endif
}
#ifdef Q_OS_WIN
void tst_QSaveFile::alternateDataStream_data()
{
QTest::addColumn<bool>("directWriteFallback");
QTest::addColumn<bool>("success");
QTest::newRow("default") << false << false;
QTest::newRow("directWriteFallback") << true << true;
}
void tst_QSaveFile::alternateDataStream()
{
QFETCH(bool, directWriteFallback);
QFETCH(bool, success);
static const char newContent[] = "New content\r\n";
QTemporaryDir dir;
QVERIFY2(dir.isValid(), qPrintable(dir.errorString()));
QString baseName = dir.path() + QLatin1String("/base");
{
QFile baseFile(baseName);
QVERIFY2(baseFile.open(QIODevice::ReadWrite), qPrintable(baseFile.errorString()));
}
// First, create a file with old content
QString adsName = baseName + QLatin1String(":outfile");
{
QFile targetFile(adsName);
if (!targetFile.open(QIODevice::ReadWrite))
QSKIP("Failed to ceate ADS file (" + targetFile.errorString().toUtf8()
+ "). Temp dir is FAT?");
targetFile.write("Old content\r\n");
}
// And write to it again using QSaveFile; only works if directWriteFallback is enabled
QSaveFile file(adsName);
file.setDirectWriteFallback(directWriteFallback);
QCOMPARE(file.directWriteFallback(), directWriteFallback);
if (success) {
QVERIFY2(file.open(QIODevice::WriteOnly), qPrintable(file.errorString()));
file.write(newContent);
QVERIFY2(file.commit(), qPrintable(file.errorString()));
// check the contents
QFile targetFile(adsName);
QVERIFY2(targetFile.open(QIODevice::ReadOnly), qPrintable(targetFile.errorString()));
QByteArray contents = targetFile.readAll();
QCOMPARE(contents, QByteArray(newContent));
} else {
QVERIFY(!file.open(QIODevice::WriteOnly));
}
}
#endif
QTEST_MAIN(tst_QSaveFile)
#include "tst_qsavefile.moc"