kdelibs/kdecore/io/karchive.cpp
2015-04-23 14:50:12 +00:00

859 lines
24 KiB
C++

/* This file is part of the KDE libraries
Copyright (C) 2000-2005 David Faure <faure@kde.org>
Copyright (C) 2003 Leo Savernik <l.savernik@aon.at>
Moved from ktar.cpp by Roberto Teixeira <maragato@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "karchive.h"
#include "klimitediodevice_p.h"
#include <config.h>
#include <kdebug.h>
#include <ksavefile.h>
#include <kde_file.h>
#include <QStack>
#include <QtCore/QMap>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef Q_OS_UNIX
#include <limits.h> // PATH_MAX
#endif
class KArchivePrivate
{
public:
KArchivePrivate()
: rootDir( 0 ),
saveFile( 0 ),
dev ( 0 ),
fileName(),
mode( QIODevice::NotOpen ),
deviceOwned( false )
{}
~KArchivePrivate()
{
delete saveFile;
delete rootDir;
}
void abortWriting();
KArchiveDirectory* rootDir;
KSaveFile* saveFile;
QIODevice * dev;
QString fileName;
QIODevice::OpenMode mode;
bool deviceOwned; // if true, we (KArchive) own dev and must delete it
};
////////////////////////////////////////////////////////////////////////
/////////////////////////// KArchive ///////////////////////////////////
////////////////////////////////////////////////////////////////////////
KArchive::KArchive( const QString& fileName )
: d(new KArchivePrivate)
{
Q_ASSERT( !fileName.isEmpty() );
d->fileName = fileName;
// This constructor leaves the device set to 0.
// This is for the use of KSaveFile, see open().
}
KArchive::KArchive( QIODevice * dev )
: d(new KArchivePrivate)
{
d->dev = dev;
}
KArchive::~KArchive()
{
if ( isOpen() )
close(); // WARNING: won't call the virtual method close in the derived class!!!
delete d;
}
bool KArchive::open( QIODevice::OpenMode mode )
{
Q_ASSERT( mode != QIODevice::NotOpen );
if ( isOpen() )
close();
if ( !d->fileName.isEmpty() )
{
Q_ASSERT( !d->dev );
if ( !createDevice( mode ) )
return false;
}
Q_ASSERT( d->dev );
if ( !d->dev->isOpen() && !d->dev->open( mode ) )
return false;
d->mode = mode;
Q_ASSERT( !d->rootDir );
d->rootDir = 0;
return openArchive( mode );
}
bool KArchive::createDevice( QIODevice::OpenMode mode )
{
switch( mode ) {
case QIODevice::WriteOnly:
if ( !d->fileName.isEmpty() ) {
// The use of KSaveFile can't be done in the ctor (no mode known yet)
//kDebug() << "Writing to a file using KSaveFile";
d->saveFile = new KSaveFile( d->fileName );
if ( !d->saveFile->open() ) {
kWarning() << "KSaveFile creation for " << d->fileName << " failed, " << d->saveFile->errorString();
delete d->saveFile;
d->saveFile = 0;
return false;
}
d->dev = d->saveFile;
Q_ASSERT( d->dev );
}
break;
case QIODevice::ReadOnly:
case QIODevice::ReadWrite:
// ReadWrite mode still uses QFile for now; we'd need to copy to the tempfile, in fact.
if ( !d->fileName.isEmpty() ) {
d->dev = new QFile( d->fileName );
d->deviceOwned = true;
}
break; // continued below
default:
kWarning() << "Unsupported mode " << d->mode;
return false;
}
return true;
}
bool KArchive::close()
{
if ( !isOpen() )
return false; // already closed (return false or true? arguable...)
// moved by holger to allow kzip to write the zip central dir
// to the file in closeArchive()
// DF: added d->dev so that we skip closeArchive if saving aborted.
bool closeSucceeded = true;
if ( d->dev ) {
closeSucceeded = closeArchive();
if ( d->mode == QIODevice::WriteOnly && !closeSucceeded )
d->abortWriting();
}
if ( d->dev )
d->dev->close();
// if d->saveFile is not null then it is equal to d->dev.
if ( d->saveFile ) {
closeSucceeded = d->saveFile->finalize();
delete d->saveFile;
d->saveFile = 0;
} if ( d->deviceOwned ) {
delete d->dev; // we created it ourselves in open()
}
delete d->rootDir;
d->rootDir = 0;
d->mode = QIODevice::NotOpen;
d->dev = 0;
return closeSucceeded;
}
const KArchiveDirectory* KArchive::directory() const
{
// rootDir isn't const so that parsing-on-demand is possible
return const_cast<KArchive *>(this)->rootDir();
}
bool KArchive::addLocalFile( const QString& fileName, const QString& destName )
{
QFileInfo fileInfo( fileName );
if ( !fileInfo.isFile() && !fileInfo.isSymLink() )
{
kWarning() << fileName << "doesn't exist or is not a regular file.";
return false;
}
KDE_struct_stat fi;
if (KDE::lstat(fileName,&fi) == -1) {
kWarning() << "stat'ing" << fileName
<< "failed:" << strerror(errno);
return false;
}
if (fileInfo.isSymLink()) {
QString symLinkTarget;
// Do NOT use fileInfo.readLink() for unix symlinks!
// It returns the -full- path to the target, while we want the target string "as is".
#if defined(Q_OS_UNIX) && !defined(Q_OS_OS2EMX)
const QByteArray encodedFileName = QFile::encodeName(fileName);
QByteArray s;
#if defined(PATH_MAX)
s.resize(PATH_MAX+1);
#else
int path_max = pathconf(encodedFileName.data(), _PC_PATH_MAX);
if (path_max <= 0) {
path_max = 4096;
}
s.resize(path_max);
#endif
int len = readlink(encodedFileName.data(), s.data(), s.size() - 1);
if ( len >= 0 ) {
s[len] = '\0';
symLinkTarget = QFile::decodeName(s);
}
#endif
if (symLinkTarget.isEmpty()) // Mac or Windows
symLinkTarget = fileInfo.symLinkTarget();
return writeSymLink(destName, symLinkTarget, fileInfo.owner(),
fileInfo.group(), fi.st_mode, fi.st_atime, fi.st_mtime,
fi.st_ctime);
}/*end if*/
qint64 size = fileInfo.size();
// the file must be opened before prepareWriting is called, otherwise
// if the opening fails, no content will follow the already written
// header and the tar file is effectively f*cked up
QFile file( fileName );
if ( !file.open( QIODevice::ReadOnly ) )
{
kWarning() << "couldn't open file " << fileName;
return false;
}
if ( !prepareWriting( destName, fileInfo.owner(), fileInfo.group(), size,
fi.st_mode, fi.st_atime, fi.st_mtime, fi.st_ctime ) )
{
kWarning() << " prepareWriting" << destName << "failed";
return false;
}
// Read and write data in chunks to minimize memory usage
QByteArray array;
array.resize( int( qMin( qint64( 1024 * 1024 ), size ) ) );
qint64 n;
qint64 total = 0;
while ( ( n = file.read( array.data(), array.size() ) ) > 0 )
{
if ( !writeData( array.data(), n ) )
{
kWarning() << "writeData failed";
return false;
}
total += n;
}
Q_ASSERT( total == size );
if ( !finishWriting( size ) )
{
kWarning() << "finishWriting failed";
return false;
}
return true;
}
bool KArchive::addLocalDirectory( const QString& path, const QString& destName )
{
QDir dir( path );
if ( !dir.exists() )
return false;
dir.setFilter(dir.filter() | QDir::Hidden);
const QStringList files = dir.entryList();
for ( QStringList::ConstIterator it = files.begin(); it != files.end(); ++it )
{
if ( *it != QLatin1String(".") && *it != QLatin1String("..") )
{
QString fileName = path + QLatin1Char('/') + *it;
// kDebug() << "storing " << fileName;
QString dest = destName.isEmpty() ? *it : (destName + QLatin1Char('/') + *it);
QFileInfo fileInfo( fileName );
if ( fileInfo.isFile() || fileInfo.isSymLink() )
addLocalFile( fileName, dest );
else if ( fileInfo.isDir() )
addLocalDirectory( fileName, dest );
// We omit sockets
}
}
return true;
}
bool KArchive::writeFile( const QString& name, const QString& user,
const QString& group, const char* data, qint64 size,
mode_t perm, time_t atime, time_t mtime, time_t ctime )
{
if ( !prepareWriting( name, user, group, size, perm, atime, mtime, ctime ) )
{
kWarning() << "prepareWriting failed";
return false;
}
// Write data
// Note: if data is 0L, don't call write, it would terminate the KFilterDev
if ( data && size && !writeData( data, size ) )
{
kWarning() << "writeData failed";
return false;
}
if ( !finishWriting( size ) )
{
kWarning() << "finishWriting failed";
return false;
}
return true;
}
bool KArchive::writeData( const char* data, qint64 size )
{
bool ok = device()->write( data, size ) == size;
if ( !ok )
d->abortWriting();
return ok;
}
// The writeDir -> doWriteDir pattern allows to avoid propagating the default
// values into all virtual methods of subclasses, and it allows more extensibility:
// if a new argument is needed, we can add a writeDir overload which stores the
// additional argument in the d pointer, and doWriteDir reimplementations can fetch
// it from there.
bool KArchive::writeDir( const QString& name, const QString& user, const QString& group,
mode_t perm, time_t atime,
time_t mtime, time_t ctime )
{
return doWriteDir( name, user, group, perm | 040000, atime, mtime, ctime );
}
bool KArchive::writeSymLink(const QString &name, const QString &target,
const QString &user, const QString &group,
mode_t perm, time_t atime,
time_t mtime, time_t ctime )
{
return doWriteSymLink( name, target, user, group, perm, atime, mtime, ctime );
}
bool KArchive::prepareWriting( const QString& name, const QString& user,
const QString& group, qint64 size,
mode_t perm, time_t atime,
time_t mtime, time_t ctime )
{
bool ok = doPrepareWriting( name, user, group, size, perm, atime, mtime, ctime );
if ( !ok )
d->abortWriting();
return ok;
}
bool KArchive::finishWriting( qint64 size )
{
return doFinishWriting( size );
}
KArchiveDirectory * KArchive::rootDir()
{
if ( !d->rootDir )
{
//kDebug() << "Making root dir ";
struct passwd* pw = getpwuid( getuid() );
struct group* grp = getgrgid( getgid() );
QString username = pw ? QFile::decodeName(pw->pw_name) : QString::number( getuid() );
QString groupname = grp ? QFile::decodeName(grp->gr_name) : QString::number( getgid() );
d->rootDir = new KArchiveDirectory( this, QLatin1String("/"), (int)(0777 + S_IFDIR), 0, username, groupname, QString() );
}
return d->rootDir;
}
KArchiveDirectory * KArchive::findOrCreate( const QString & path )
{
//kDebug() << path;
if ( path.isEmpty() || path == QLatin1String("/") || path == QLatin1String(".") ) // root dir => found
{
//kDebug() << "returning rootdir";
return rootDir();
}
// Important note : for tar files containing absolute paths
// (i.e. beginning with "/"), this means the leading "/" will
// be removed (no KDirectory for it), which is exactly the way
// the "tar" program works (though it displays a warning about it)
// See also KArchiveDirectory::entry().
// Already created ? => found
const KArchiveEntry* ent = rootDir()->entry( path );
if ( ent )
{
if ( ent->isDirectory() )
//kDebug() << "found it";
return (KArchiveDirectory *) ent;
else
kWarning() << "Found" << path << "but it's not a directory";
}
// Otherwise go up and try again
int pos = path.lastIndexOf( QLatin1Char('/') );
KArchiveDirectory * parent;
QString dirname;
if ( pos == -1 ) // no more slash => create in root dir
{
parent = rootDir();
dirname = path;
}
else
{
QString left = path.left( pos );
dirname = path.mid( pos + 1 );
parent = findOrCreate( left ); // recursive call... until we find an existing dir.
}
//kDebug() << "found parent " << parent->name() << " adding " << dirname << " to ensure " << path;
// Found -> add the missing piece
KArchiveDirectory * e = new KArchiveDirectory( this, dirname, d->rootDir->permissions(),
d->rootDir->date(), d->rootDir->user(),
d->rootDir->group(), QString() );
parent->addEntry( e );
return e; // now a directory to <path> exists
}
void KArchive::setDevice( QIODevice * dev )
{
if ( d->deviceOwned )
delete d->dev;
d->dev = dev;
d->deviceOwned = false;
}
void KArchive::setRootDir( KArchiveDirectory *rootDir )
{
Q_ASSERT( !d->rootDir ); // Call setRootDir only once during parsing please ;)
d->rootDir = rootDir;
}
QIODevice::OpenMode KArchive::mode() const
{
return d->mode;
}
QIODevice * KArchive::device() const
{
return d->dev;
}
bool KArchive::isOpen() const
{
return d->mode != QIODevice::NotOpen;
}
QString KArchive::fileName() const
{
return d->fileName;
}
void KArchivePrivate::abortWriting()
{
if ( saveFile ) {
saveFile->abort();
delete saveFile;
saveFile = 0;
dev = 0;
}
}
////////////////////////////////////////////////////////////////////////
/////////////////////// KArchiveEntry //////////////////////////////////
////////////////////////////////////////////////////////////////////////
class KArchiveEntryPrivate
{
public:
KArchiveEntryPrivate( KArchive* _archive, const QString& _name, int _access,
int _date, const QString& _user, const QString& _group,
const QString& _symlink) :
name(_name),
date(_date),
access(_access),
user(_user),
group(_group),
symlink(_symlink),
archive(_archive)
{}
QString name;
int date;
mode_t access;
QString user;
QString group;
QString symlink;
KArchive* archive;
};
KArchiveEntry::KArchiveEntry( KArchive* t, const QString& name, int access, int date,
const QString& user, const QString& group, const
QString& symlink) :
d(new KArchiveEntryPrivate(t,name,access,date,user,group,symlink))
{
}
KArchiveEntry::~KArchiveEntry()
{
delete d;
}
QDateTime KArchiveEntry::datetime() const
{
QDateTime datetimeobj;
datetimeobj.setTime_t( d->date );
return datetimeobj;
}
int KArchiveEntry::date() const
{
return d->date;
}
QString KArchiveEntry::name() const
{
return d->name;
}
mode_t KArchiveEntry::permissions() const
{
return d->access;
}
QString KArchiveEntry::user() const
{
return d->user;
}
QString KArchiveEntry::group() const
{
return d->group;
}
QString KArchiveEntry::symLinkTarget() const
{
return d->symlink;
}
bool KArchiveEntry::isFile() const
{
return false;
}
bool KArchiveEntry::isDirectory() const
{
return false;
}
KArchive* KArchiveEntry::archive() const
{
return d->archive;
}
////////////////////////////////////////////////////////////////////////
/////////////////////// KArchiveFile ///////////////////////////////////
////////////////////////////////////////////////////////////////////////
class KArchiveFilePrivate
{
public:
KArchiveFilePrivate( qint64 _pos, qint64 _size ) :
pos(_pos),
size(_size)
{}
qint64 pos;
qint64 size;
};
KArchiveFile::KArchiveFile( KArchive* t, const QString& name, int access, int date,
const QString& user, const QString& group,
const QString & symlink,
qint64 pos, qint64 size )
: KArchiveEntry( t, name, access, date, user, group, symlink ),
d( new KArchiveFilePrivate(pos, size) )
{
}
KArchiveFile::~KArchiveFile()
{
delete d;
}
qint64 KArchiveFile::position() const
{
return d->pos;
}
qint64 KArchiveFile::size() const
{
return d->size;
}
void KArchiveFile::setSize( qint64 s )
{
d->size = s;
}
QByteArray KArchiveFile::data() const
{
bool ok = archive()->device()->seek( d->pos );
if (!ok) {
kWarning() << "Failed to sync to" << d->pos << "to read" << name();
}
// Read content
QByteArray arr;
if ( d->size )
{
arr = archive()->device()->read( d->size );
Q_ASSERT( arr.size() == d->size );
}
return arr;
}
QIODevice * KArchiveFile::createDevice() const
{
return new KLimitedIODevice( archive()->device(), d->pos, d->size );
}
bool KArchiveFile::isFile() const
{
return true;
}
void KArchiveFile::copyTo(const QString& dest) const
{
QFile f( dest + QLatin1Char('/') + name() );
if ( f.open( QIODevice::ReadWrite | QIODevice::Truncate ) )
{
QIODevice* inputDev = createDevice();
// Read and write data in chunks to minimize memory usage
const qint64 chunkSize = 1024 * 1024;
qint64 remainingSize = d->size;
QByteArray array;
array.resize( int( qMin( chunkSize, remainingSize ) ) );
while ( remainingSize > 0 ) {
const qint64 currentChunkSize = qMin( chunkSize, remainingSize );
const qint64 n = inputDev->read( array.data(), currentChunkSize );
Q_ASSERT( n == currentChunkSize );
f.write( array.data(), currentChunkSize );
remainingSize -= currentChunkSize;
}
f.close();
delete inputDev;
}
}
////////////////////////////////////////////////////////////////////////
//////////////////////// KArchiveDirectory /////////////////////////////////
////////////////////////////////////////////////////////////////////////
class KArchiveDirectoryPrivate
{
public:
~KArchiveDirectoryPrivate()
{
qDeleteAll(entries);
}
QHash<QString, KArchiveEntry *> entries;
};
KArchiveDirectory::KArchiveDirectory( KArchive* t, const QString& name, int access,
int date,
const QString& user, const QString& group,
const QString &symlink)
: KArchiveEntry( t, name, access, date, user, group, symlink ),
d( new KArchiveDirectoryPrivate )
{
}
KArchiveDirectory::~KArchiveDirectory()
{
delete d;
}
QStringList KArchiveDirectory::entries() const
{
return d->entries.keys();
}
const KArchiveEntry* KArchiveDirectory::entry( const QString& _name ) const
{
QString name = QDir::cleanPath(_name);
int pos = name.indexOf( QLatin1Char('/') );
if ( pos == 0 ) // ouch absolute path (see also KArchive::findOrCreate)
{
if (name.length()>1)
{
name = name.mid( 1 ); // remove leading slash
pos = name.indexOf( QLatin1Char('/') ); // look again
}
else // "/"
return this;
}
// trailing slash ? -> remove
if ( pos != -1 && pos == name.length()-1 )
{
name = name.left( pos );
pos = name.indexOf( QLatin1Char('/') ); // look again
}
if ( pos != -1 )
{
const QString left = name.left(pos);
const QString right = name.mid(pos + 1);
//kDebug() << "left=" << left << "right=" << right;
const KArchiveEntry* e = d->entries.value( left );
if ( !e || !e->isDirectory() )
return 0;
return static_cast<const KArchiveDirectory*>(e)->entry( right );
}
return d->entries.value( name );
}
void KArchiveDirectory::addEntry( KArchiveEntry* entry )
{
if( entry->name().isEmpty() )
return;
if( d->entries.value( entry->name() ) ) {
kWarning() << "directory " << name()
<< "has entry" << entry->name() << "already";
return;
}
d->entries.insert( entry->name(), entry );
}
void KArchiveDirectory::removeEntry( KArchiveEntry* entry )
{
if (!entry) {
return;
}
QHash<QString, KArchiveEntry*>::Iterator it = d->entries.find(entry->name());
// nothing removed?
if (it == d->entries.end()) {
kWarning() << "directory " << name()
<< "has no entry with name " << entry->name();
return;
}
if (it.value() != entry) {
kWarning() << "directory " << name()
<< "has another entry for name " << entry->name();
return;
}
d->entries.erase(it);
}
bool KArchiveDirectory::isDirectory() const
{
return true;
}
static bool sortByPosition( const KArchiveFile* file1, const KArchiveFile* file2 ) {
return file1->position() < file2->position();
}
void KArchiveDirectory::copyTo(const QString& dest, bool recursiveCopy ) const
{
QDir root;
QList<const KArchiveFile*> fileList;
QMap<qint64, QString> fileToDir;
// placeholders for iterated items
QStack<const KArchiveDirectory *> dirStack;
QStack<QString> dirNameStack;
dirStack.push( this ); // init stack at current directory
dirNameStack.push( dest ); // ... with given path
do {
const KArchiveDirectory* curDir = dirStack.pop();
const QString curDirName = dirNameStack.pop();
root.mkdir(curDirName);
const QStringList dirEntries = curDir->entries();
for ( QStringList::const_iterator it = dirEntries.begin(); it != dirEntries.end(); ++it ) {
const KArchiveEntry* curEntry = curDir->entry(*it);
if (!curEntry->symLinkTarget().isEmpty()) {
const QString linkName = curDirName+QLatin1Char('/')+curEntry->name();
#ifdef Q_OS_UNIX
if (!::symlink(curEntry->symLinkTarget().toLocal8Bit(), linkName.toLocal8Bit())) {
kDebug() << "symlink(" << curEntry->symLinkTarget() << ',' << linkName << ") failed:" << strerror(errno);
}
#else
// TODO - how to create symlinks on other platforms?
#endif
} else {
if ( curEntry->isFile() ) {
const KArchiveFile* curFile = dynamic_cast<const KArchiveFile*>( curEntry );
if (curFile) {
fileList.append( curFile );
fileToDir.insert( curFile->position(), curDirName );
}
}
if ( curEntry->isDirectory() && recursiveCopy ) {
const KArchiveDirectory *ad = dynamic_cast<const KArchiveDirectory*>( curEntry );
if (ad) {
dirStack.push( ad );
dirNameStack.push( curDirName + QLatin1Char('/') + curEntry->name() );
}
}
}
}
} while (!dirStack.isEmpty());
qSort( fileList.begin(), fileList.end(), sortByPosition ); // sort on d->pos, so we have a linear access
for ( QList<const KArchiveFile*>::const_iterator it = fileList.constBegin(), end = fileList.constEnd() ;
it != end ; ++it ) {
const KArchiveFile* f = *it;
qint64 pos = f->position();
f->copyTo( fileToDir[pos] );
}
}