diff options
Diffstat (limited to 'src/libs/kdtools/kdsavefile.cpp')
-rw-r--r-- | src/libs/kdtools/kdsavefile.cpp | 521 |
1 files changed, 521 insertions, 0 deletions
diff --git a/src/libs/kdtools/kdsavefile.cpp b/src/libs/kdtools/kdsavefile.cpp new file mode 100644 index 000000000..d866968cf --- /dev/null +++ b/src/libs/kdtools/kdsavefile.cpp @@ -0,0 +1,521 @@ +/**************************************************************************** +** Copyright (C) 2001-2010 Klaralvdalens Datakonsult AB. All rights reserved. +** +** This file is part of the KD Tools library. +** +** Licensees holding valid commercial KD Tools licenses may use this file in +** accordance with the KD Tools Commercial License Agreement provided with +** the Software. +** +** +** This file may be distributed and/or modified under the terms of the +** GNU Lesser General Public License version 2 and version 3 as published by the +** Free Software Foundation and appearing in the file LICENSE.LGPL included. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** Contact info@kdab.com if any conditions of this licensing are not +** clear to you. +** +**********************************************************************/ + +#include "kdsavefile.h" + +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QPointer> +#include <QtCore/QTemporaryFile> + +#ifdef Q_OS_WIN +# include <io.h> +#endif +#include <memory> +#include <sys/stat.h> +#include <sys/types.h> +#include <fcntl.h> + +static int permissionsToMode(QFile::Permissions p, bool *ok) +{ + Q_ASSERT(ok); + int m = 0; +#ifdef Q_OS_WIN + //following qfsfileengine_win.cpp: QFSFileEngine::setPermissions() + if (p & QFile::ReadOwner || p & QFile::ReadUser || p & QFile::ReadGroup || p & QFile::ReadOther) + m |= _S_IREAD; + if (p & QFile::WriteOwner || p & QFile::WriteUser || p & QFile::WriteGroup || p & QFile::WriteOther) + m |= _S_IWRITE; + *ok = m != 0; +#else + if (p & QFile::ReadUser) + m |= S_IRUSR; + if (p & QFile::WriteUser) + m |= S_IWUSR; + if (p & QFile::ExeUser) + m |= S_IXUSR; + if (p & QFile::ReadGroup) + m |= S_IRGRP; + if (p & QFile::WriteGroup) + m |= S_IWGRP; + if (p & QFile::ExeGroup) + m |= S_IXGRP; + if (p & QFile::ReadOther) + m |= S_IROTH; + if (p & QFile::WriteOther) + m |= S_IWOTH; + if (p & QFile::ExeOther) + m |= S_IXOTH; + *ok = true; +#endif + return m; +} + +static bool sync(int fd) +{ +#ifdef Q_OS_WIN + return _commit(fd) == 0; +#else + return fsync(fd) == 0; +#endif +} + +static QString makeAbsolute(const QString &path) +{ + if (QDir::isAbsolutePath(path)) + return path; + return QDir::currentPath() + QLatin1String("/") + path; +} + +static int myOpen(const QString &path, int flags, int mode) +{ +#ifdef Q_OS_WIN + int fd; + _wsopen_s(&fd, reinterpret_cast<const wchar_t *>(path.utf16()), flags, _SH_DENYRW, mode); + return fd; +#else + return open(QFile::encodeName(path).constData(), flags, mode); +#endif +} + +static void myClose(int fd) +{ +#ifdef Q_OS_WIN + _close(fd); +#else + close(fd); +#endif +} + +static bool touchFile(const QString &path, QFile::Permissions p) +{ + bool ok; + const int mode = permissionsToMode(p, &ok); + if (!ok) + return false; + const int fd = myOpen(QDir::toNativeSeparators(path), O_WRONLY|O_CREAT, mode); + if (fd < 0) { + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) + return false; + if (!file.setPermissions(p)) { + QFile::remove(path); + return false; + } + return true; + } + sync(fd); + myClose(fd); + return true; +} + +static QFile *createFile(const QString &path, QIODevice::OpenMode m, QFile::Permissions p, bool *openOk) +{ + Q_ASSERT(openOk); + if (!touchFile(path, p)) + return 0; + std::auto_ptr<QFile> file(new QFile(path)); + *openOk = file->open(m | QIODevice::Append); + if (!*openOk) + QFile::remove(path); // try to remove empty file + return file.release(); +} + +/*! + Generates a temporary file name based on template \a path + \internal + */ +static QString generateTempFileName(const QString &path) +{ + const QString tmp = path + QLatin1String("tmp.dsfdf.%1"); //TODO: use random suffix + int count = 1; + while (QFile::exists(tmp.arg(count))) + ++count; + return tmp.arg(count); +} + +/*! + \class KDSaveFile KDSaveFile + \ingroup core + \brief Secure and robust writing to a file + +*/ + +class KDSaveFile::Private +{ + KDSaveFile *const q; +public: + explicit Private(const QString &fname, KDSaveFile *qq) + : q(qq), +#ifdef Q_OS_WIN + backupExtension(QLatin1String(".bak")), +#else + backupExtension(QLatin1String("~")), +#endif + permissions(QFile::ReadUser|QFile::WriteUser), filename(fname), tmpFile() + { + //TODO respect umask instead of hardcoded default permissions + } + + ~Private() + { + deleteTempFile(); + } + + bool deleteTempFile() + { + if (!tmpFile) + return true; + const QString name = tmpFile->fileName(); + delete tmpFile; + //force a real close by deleting the object, before deleting the actual file. Needed on Windows + QFile tmp(name); + return tmp.remove(); + } + + bool recreateTemporaryFile(QIODevice::OpenMode mode) + { + deleteTempFile(); + bool ok; + tmpFile = createFile(generateTempFileName( filename ), mode, permissions, &ok); + QObject::connect(tmpFile, SIGNAL(aboutToClose()), q, SIGNAL(aboutToClose())); + QObject::connect(tmpFile, SIGNAL(bytesWritten(qint64)), q, SIGNAL(bytesWritten(qint64))); + QObject::connect(tmpFile, SIGNAL(readChannelFinished()), q, SIGNAL(readChannelFinished())); + QObject::connect(tmpFile, SIGNAL(readyRead()), q, SIGNAL(readyRead())); + return ok; + } + + QString generateBackupName() const + { + const QString bf = filename + backupExtension; + if (!QFile::exists(bf)) + return bf; + int count = 1; + while (QFile::exists(bf + QString::number(count))) + ++count; + return bf + QString::number(count); + } + + void propagateErrors() + { + if (!tmpFile) + return; + q->setErrorString(tmpFile->errorString()); + } + + QString backupExtension; + QFile::Permissions permissions; + QString filename; + QPointer<QFile> tmpFile; +}; + + +KDSaveFile::KDSaveFile(const QString &filename, QObject *parent) + : QIODevice(parent), d( new Private(makeAbsolute(filename), this)) +{} + +KDSaveFile::~KDSaveFile() +{ + delete d; +} + +void KDSaveFile::close() +{ + d->deleteTempFile(); + QIODevice::close(); +} + +bool KDSaveFile::open(OpenMode mode) +{ + setOpenMode(QIODevice::NotOpen); + if (mode & QIODevice::Append) { + setErrorString(tr("Append mode not supported.")); + return false; + } + + if ((mode & QIODevice::WriteOnly) == 0){ + setErrorString(tr("Read-only access not supported.")); + return false; + } + + // for some reason this seems to be problematic... let's remove it for now, will break in commit anyhow, if it's serious + //if ( QFile::exists( d->filename ) && !QFileInfo( d->filename ).isWritable() ) { + // setErrorString( tr("The target file %1 exists and is not writable").arg( d->filename ) ); + // return false; + //} + const bool opened = d->recreateTemporaryFile(mode); + if (opened) { + setOpenMode(mode); + } + + //if target file already exists, apply permissions of existing file to temp file + return opened; +} + +bool KDSaveFile::atEnd() +{ + return d->tmpFile ? d->tmpFile->atEnd() : QIODevice::atEnd(); +} + +qint64 KDSaveFile::bytesAvailable() const +{ + return d->tmpFile ? d->tmpFile->bytesAvailable() : QIODevice::bytesAvailable(); +} + +qint64 KDSaveFile::bytesToWrite() const +{ + return d->tmpFile ? d->tmpFile->bytesToWrite() : QIODevice::bytesToWrite(); +} + +bool KDSaveFile::canReadLine() const +{ + return d->tmpFile ? d->tmpFile->canReadLine() : QIODevice::canReadLine(); +} + +bool KDSaveFile::isSequential() const +{ + return d->tmpFile ? d->tmpFile->isSequential() : QIODevice::isSequential(); +} + +qint64 KDSaveFile::pos() const +{ + return d->tmpFile ? d->tmpFile->pos() : QIODevice::pos(); +} + +bool KDSaveFile::reset() +{ + return d->tmpFile ? d->tmpFile->reset() : QIODevice::reset(); +} + +bool KDSaveFile::seek(qint64 pos) +{ + const bool ret = d->tmpFile ? d->tmpFile->seek(pos) : QIODevice::seek(pos); + if (!ret) + d->propagateErrors(); + return ret; +} + +qint64 KDSaveFile::size() const +{ + return d->tmpFile ? d->tmpFile->size() : QIODevice::size(); +} + +bool KDSaveFile::waitForBytesWritten(int msecs) +{ + return d->tmpFile ? d->tmpFile->waitForBytesWritten(msecs) : QIODevice::waitForBytesWritten(msecs); +} + +bool KDSaveFile::waitForReadyRead(int msecs) +{ + return d->tmpFile ? d->tmpFile->waitForReadyRead(msecs) : QIODevice::waitForReadyRead(msecs); +} + +bool KDSaveFile::commit(KDSaveFile::CommitMode mode) +{ + if (!d->tmpFile) + return false; + const QString tmpfname = d->tmpFile->fileName(); + d->tmpFile->flush(); + delete d->tmpFile; + QFile orig(d->filename); + QString backup; + if (orig.exists()) { + backup = d->generateBackupName(); + if (!orig.rename(backup)) { + setErrorString(tr("Could not backup existing file %1: %2").arg( d->filename, orig.errorString())); + QFile tmp(tmpfname); + if (!tmp.remove()) // TODO how to report this error? + qWarning() << "Could not remove temp file" << tmpfname << tmp.errorString(); + if (mode != OverwriteExistingFile) + return false; + } + } + QFile target(tmpfname); + if (!target.rename(d->filename)) { + setErrorString(target.errorString()); + return false; + } + if (mode == OverwriteExistingFile) { + QFile tmp(backup); + const bool removed = !tmp.exists() || tmp.remove(backup); + if (!removed) + qWarning() << "Could not remove the backup: " << tmp.errorString(); + } + + return true; +} + +QString KDSaveFile::fileName() const +{ + return d->filename; +} + +void KDSaveFile::setFileName(const QString &filename) +{ + const QString fn = makeAbsolute(filename); + if (fn == d->filename) + return; + close(); + delete d->tmpFile; + d->filename = fn; +} + +qint64 KDSaveFile::readData(char *data, qint64 maxSize) +{ + if (!d->tmpFile) { + setErrorString(tr("TODO")); + return -1; + } + const qint64 ret = d->tmpFile->read(data, maxSize); + d->propagateErrors(); + return ret; +} + +qint64 KDSaveFile::readLineData(char *data, qint64 maxSize) +{ + if (!d->tmpFile) { + setErrorString(tr("TODO")); + return -1; + } + const qint64 ret = d->tmpFile->readLine(data, maxSize); + d->propagateErrors(); + return ret; +} + +qint64 KDSaveFile::writeData(const char *data, qint64 maxSize) +{ + if (!d->tmpFile) { + setErrorString(tr("TODO")); + return -1; + } + const qint64 ret = d->tmpFile->write(data, maxSize); + d->propagateErrors(); + return ret; +} + +bool KDSaveFile::flush() +{ + return d->tmpFile ? d->tmpFile->flush() : false; +} + +bool KDSaveFile::resize(qint64 sz) +{ + return d->tmpFile ? d->tmpFile->resize(sz) : false; +} + +int KDSaveFile::handle() const +{ + return d->tmpFile ? d->tmpFile->handle() : -1; +} + +QFile::Permissions KDSaveFile::permissions() const +{ + return d->tmpFile ? d->tmpFile->permissions() : d->permissions; +} + +bool KDSaveFile::setPermissions(QFile::Permissions p) +{ + d->permissions = p; + if (d->tmpFile) + return d->tmpFile->setPermissions(p); + return false; +} + +void KDSaveFile::setBackupExtension(const QString &ext) +{ + d->backupExtension = ext; +} + +QString KDSaveFile::backupExtension() const +{ + return d->backupExtension; +} + +/** + * TODO + * + * + */ + +#ifdef KDTOOLSCORE_UNITTESTS + +#include <KDUnitTest/Test> + +KDAB_UNITTEST_SIMPLE( KDSaveFile, "kdcoretools" ) { + //TODO test contents (needs blocking and checked write() ) + { + const QString testfile1 = QLatin1String("kdsavefile-test1"); + QByteArray testData("lalalala"); + KDSaveFile saveFile( testfile1 ); + assertTrue( saveFile.open( QIODevice::WriteOnly ) ); + saveFile.write( testData.constData(), testData.size() ); + assertTrue( saveFile.commit() ); + assertTrue( QFile::exists( testfile1 ) ); + assertTrue( QFile::remove( testfile1 ) ); + } + { + const QString testfile1 = QLatin1String("kdsavefile-test1"); + QByteArray testData("lalalala"); + KDSaveFile saveFile( testfile1 ); + assertTrue( saveFile.open( QIODevice::WriteOnly ) ); + saveFile.write( testData.constData(), testData.size() ); + saveFile.close(); + assertFalse( QFile::exists( testfile1 ) ); + } + { + const QString testfile1 = QLatin1String("kdsavefile-test1"); + QByteArray testData("lalalala"); + KDSaveFile saveFile( testfile1 ); + assertTrue( saveFile.open( QIODevice::WriteOnly ) ); + saveFile.write( testData.constData(), testData.size() ); + assertTrue( saveFile.commit() ); + assertTrue( QFile::exists( testfile1 ) ); + + KDSaveFile sf2( testfile1 ); + sf2.setBackupExtension( QLatin1String(".bak") ); + assertTrue( sf2.open( QIODevice::WriteOnly ) ); + sf2.write( testData.constData(), testData.size() ); + sf2.commit(); //commit in backup mode (default) + const QString backup = testfile1 + sf2.backupExtension(); + assertTrue( QFile::exists( backup ) ); + assertTrue( QFile::remove( backup ) ); + + KDSaveFile sf3( testfile1 ); + sf3.setBackupExtension( QLatin1String(".bak") ); + assertTrue( sf3.open( QIODevice::WriteOnly ) ); + sf3.write( testData.constData(), testData.size() ); + sf3.commit( KDSaveFile::OverwriteExistingFile ); + const QString backup2 = testfile1 + sf3.backupExtension(); + assertFalse( QFile::exists( backup2 ) ); + + assertTrue( QFile::remove( testfile1 ) ); + } + { + const QString testfile1 = QLatin1String("kdsavefile-test1"); + KDSaveFile sf( testfile1 ); + assertFalse( sf.open( QIODevice::ReadOnly ) ); + assertFalse( sf.open( QIODevice::WriteOnly|QIODevice::Append ) ); + assertTrue( sf.open( QIODevice::ReadWrite ) ); + } +} + +#endif // KDTOOLSCORE_UNITTESTS |