/* This file is part of the KDE libraries Copyright (C) 2000-2005 David Faure Copyright (C) 2003 Leo Savernik Moved from ktar.cpp by Roberto Teixeira 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // PATH_MAX 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(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) 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.readLink(); 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 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 ); Q_UNUSED( n ); f.write( array.data(), currentChunkSize ); remainingSize -= currentChunkSize; } f.close(); delete inputDev; } } //////////////////////////////////////////////////////////////////////// //////////////////////// KArchiveDirectory ///////////////////////////////// //////////////////////////////////////////////////////////////////////// class KArchiveDirectoryPrivate { public: ~KArchiveDirectoryPrivate() { qDeleteAll(entries); } QHash 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(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::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 fileList; QMap fileToDir; // placeholders for iterated items QStack dirStack; QStack 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(); foreach(const QString it, dirEntries) { 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( curEntry ); if (curFile) { fileList.append( curFile ); fileToDir.insert( curFile->position(), curDirName ); } } if ( curEntry->isDirectory() && recursiveCopy ) { const KArchiveDirectory *ad = dynamic_cast( 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 foreach(const KArchiveFile *it, fileList) { qint64 pos = it->position(); it->copyTo( fileToDir[pos] ); } }