cf12defd28
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1063 lines
31 KiB
C++
1063 lines
31 KiB
C++
/* This file is part of the KDE libraries
|
|
SPDX-FileCopyrightText: 2000-2005 David Faure <faure@kde.org>
|
|
SPDX-FileCopyrightText: 2003 Leo Savernik <l.savernik@aon.at>
|
|
|
|
Moved from ktar.cpp by Roberto Teixeira <maragato@kde.org>
|
|
|
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
|
*/
|
|
|
|
#include "karchive.h"
|
|
#include "karchive_p.h"
|
|
#include "klimitediodevice_p.h"
|
|
#include "loggingcategory.h"
|
|
|
|
#include <qplatformdefs.h> // QT_STATBUF, QT_LSTAT
|
|
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QMap>
|
|
#include <QStack>
|
|
|
|
#include <cerrno>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <assert.h>
|
|
|
|
#ifdef Q_OS_UNIX
|
|
#include <grp.h>
|
|
#include <limits.h> // PATH_MAX
|
|
#include <pwd.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h> // DWORD, GetUserNameW
|
|
#endif // Q_OS_WIN
|
|
|
|
#if defined(Q_OS_UNIX)
|
|
#define STAT_METHOD QT_LSTAT
|
|
#else
|
|
#define STAT_METHOD QT_STAT
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
/////////////////// KArchiveDirectoryPrivate ///////////////////////////
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
class KArchiveDirectoryPrivate
|
|
{
|
|
public:
|
|
KArchiveDirectoryPrivate(KArchiveDirectory *parent)
|
|
: q(parent)
|
|
{
|
|
}
|
|
|
|
~KArchiveDirectoryPrivate()
|
|
{
|
|
qDeleteAll(entries);
|
|
}
|
|
|
|
KArchiveDirectoryPrivate(const KArchiveDirectoryPrivate &) = delete;
|
|
KArchiveDirectoryPrivate &operator=(const KArchiveDirectoryPrivate &) = delete;
|
|
|
|
static KArchiveDirectoryPrivate *get(KArchiveDirectory *directory)
|
|
{
|
|
return directory->d;
|
|
}
|
|
|
|
// Returns in containingDirectory the directory that actually contains the returned entry
|
|
const KArchiveEntry *entry(const QString &_name, KArchiveDirectory **containingDirectory) const
|
|
{
|
|
*containingDirectory = q;
|
|
|
|
QString name = QDir::cleanPath(_name);
|
|
int pos = name.indexOf(QLatin1Char('/'));
|
|
if (pos == 0) { // 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 q;
|
|
}
|
|
}
|
|
// 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);
|
|
|
|
// qCDebug(KArchiveLog) << "left=" << left << "right=" << right;
|
|
|
|
KArchiveEntry *e = entries.value(left);
|
|
if (!e || !e->isDirectory()) {
|
|
return nullptr;
|
|
}
|
|
*containingDirectory = static_cast<KArchiveDirectory *>(e);
|
|
return (*containingDirectory)->d->entry(right, containingDirectory);
|
|
}
|
|
|
|
return entries.value(name);
|
|
}
|
|
|
|
KArchiveDirectory *q;
|
|
QHash<QString, KArchiveEntry *> entries;
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////// KArchive ///////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
KArchive::KArchive(const QString &fileName)
|
|
: d(new KArchivePrivate(this))
|
|
{
|
|
if (fileName.isEmpty()) {
|
|
qCWarning(KArchiveLog) << "KArchive: No file name specified";
|
|
}
|
|
d->fileName = fileName;
|
|
// This constructor leaves the device set to 0.
|
|
// This is for the use of QSaveFile, see open().
|
|
}
|
|
|
|
KArchive::KArchive(QIODevice *dev)
|
|
: d(new KArchivePrivate(this))
|
|
{
|
|
if (!dev) {
|
|
qCWarning(KArchiveLog) << "KArchive: Null device specified";
|
|
}
|
|
d->dev = dev;
|
|
}
|
|
|
|
KArchive::~KArchive()
|
|
{
|
|
Q_ASSERT(!isOpen()); // the derived class destructor must have closed already
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (!d->dev) {
|
|
setErrorString(tr("No filename or device was specified"));
|
|
return false;
|
|
}
|
|
|
|
if (!d->dev->isOpen() && !d->dev->open(mode)) {
|
|
setErrorString(tr("Could not open device in mode %1").arg(static_cast<int>(mode)));
|
|
return false;
|
|
}
|
|
|
|
d->mode = mode;
|
|
|
|
Q_ASSERT(!d->rootDir);
|
|
d->rootDir = nullptr;
|
|
|
|
return openArchive(mode);
|
|
}
|
|
|
|
bool KArchive::createDevice(QIODevice::OpenMode mode)
|
|
{
|
|
switch (mode) {
|
|
case QIODevice::WriteOnly:
|
|
if (!d->fileName.isEmpty()) {
|
|
// The use of QSaveFile can't be done in the ctor (no mode known yet)
|
|
// qCDebug(KArchiveLog) << "Writing to a file using QSaveFile";
|
|
d->saveFile = new QSaveFile(d->fileName);
|
|
#ifdef Q_OS_ANDROID
|
|
// we cannot rename on to Android content: URLs
|
|
if (d->fileName.startsWith(QLatin1String("content://"))) {
|
|
d->saveFile->setDirectWriteFallback(true);
|
|
}
|
|
#endif
|
|
if (!d->saveFile->open(QIODevice::WriteOnly)) {
|
|
setErrorString(tr("QSaveFile creation for %1 failed: %2").arg(d->fileName, d->saveFile->errorString()));
|
|
|
|
delete d->saveFile;
|
|
d->saveFile = nullptr;
|
|
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:
|
|
setErrorString(tr("Unsupported mode %1").arg(static_cast<int>(d->mode)));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool KArchive::close()
|
|
{
|
|
if (!isOpen()) {
|
|
setErrorString(tr("Archive already closed"));
|
|
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 != d->saveFile) {
|
|
d->dev->close();
|
|
}
|
|
|
|
// if d->saveFile is not null then it is equal to d->dev.
|
|
if (d->saveFile) {
|
|
closeSucceeded = d->saveFile->commit();
|
|
delete d->saveFile;
|
|
d->saveFile = nullptr;
|
|
}
|
|
if (d->deviceOwned) {
|
|
delete d->dev; // we created it ourselves in open()
|
|
}
|
|
|
|
delete d->rootDir;
|
|
d->rootDir = nullptr;
|
|
d->mode = QIODevice::NotOpen;
|
|
d->dev = nullptr;
|
|
return closeSucceeded;
|
|
}
|
|
|
|
QString KArchive::errorString() const
|
|
{
|
|
return d->errorStr;
|
|
}
|
|
|
|
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()) {
|
|
setErrorString(tr("%1 doesn't exist or is not a regular file.").arg(fileName));
|
|
return false;
|
|
}
|
|
|
|
QT_STATBUF fi;
|
|
if (STAT_METHOD(QFile::encodeName(fileName).constData(), &fi) == -1) {
|
|
setErrorString(tr("Failed accessing the file %1 for adding to the archive. The error was: %2").arg(fileName).arg(QLatin1String{strerror(errno)}));
|
|
return false;
|
|
}
|
|
|
|
if (fileInfo.isSymLink()) {
|
|
QString symLinkTarget;
|
|
// Do NOT use fileInfo.symLinkTarget() 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.constData());
|
|
}
|
|
#endif
|
|
if (symLinkTarget.isEmpty()) { // Mac or Windows
|
|
symLinkTarget = fileInfo.symLinkTarget();
|
|
}
|
|
return writeSymLink(destName,
|
|
symLinkTarget,
|
|
fileInfo.owner(),
|
|
fileInfo.group(),
|
|
fi.st_mode,
|
|
fileInfo.lastRead(),
|
|
fileInfo.lastModified(),
|
|
fileInfo.birthTime());
|
|
} /*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 incorrect
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
setErrorString(tr("Couldn't open file %1: %2").arg(fileName, file.errorString()));
|
|
return false;
|
|
}
|
|
|
|
if (!prepareWriting(destName, fileInfo.owner(), fileInfo.group(), size, fi.st_mode, fileInfo.lastRead(), fileInfo.lastModified(), fileInfo.birthTime())) {
|
|
// qCWarning(KArchiveLog) << " 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)) {
|
|
// qCWarning(KArchiveLog) << "writeData failed";
|
|
return false;
|
|
}
|
|
total += n;
|
|
}
|
|
Q_ASSERT(total == size);
|
|
|
|
if (!finishWriting(size)) {
|
|
// qCWarning(KArchiveLog) << "finishWriting failed";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool KArchive::addLocalDirectory(const QString &path, const QString &destName)
|
|
{
|
|
QDir dir(path);
|
|
if (!dir.exists()) {
|
|
setErrorString(tr("Directory %1 does not exist").arg(path));
|
|
return false;
|
|
}
|
|
dir.setFilter(dir.filter() | QDir::Hidden);
|
|
const QStringList files = dir.entryList();
|
|
for (const QString &file : files) {
|
|
if (file != QLatin1String(".") && file != QLatin1String("..")) {
|
|
const QString fileName = path + QLatin1Char('/') + file;
|
|
// qCDebug(KArchiveLog) << "storing " << fileName;
|
|
const QString dest = destName.isEmpty() ? file : (destName + QLatin1Char('/') + file);
|
|
QFileInfo fileInfo(fileName);
|
|
|
|
if (fileInfo.isFile() || fileInfo.isSymLink()) {
|
|
addLocalFile(fileName, dest);
|
|
} else if (fileInfo.isDir()) {
|
|
// Write directory, so that empty dirs are preserved (and permissions written out, etc.)
|
|
int perms = 0;
|
|
QT_STATBUF fi;
|
|
if (STAT_METHOD(QFile::encodeName(fileName).constData(), &fi) != -1) {
|
|
perms = fi.st_mode;
|
|
}
|
|
writeDir(dest, fileInfo.owner(), fileInfo.group(), perms, fileInfo.lastRead(), fileInfo.lastModified(), fileInfo.birthTime());
|
|
// Recurse
|
|
addLocalDirectory(fileName, dest);
|
|
}
|
|
// We omit sockets
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool KArchive::writeFile(const QString &name,
|
|
QByteArrayView data,
|
|
mode_t perm,
|
|
const QString &user,
|
|
const QString &group,
|
|
const QDateTime &atime,
|
|
const QDateTime &mtime,
|
|
const QDateTime &ctime)
|
|
{
|
|
const qint64 size = data.size();
|
|
if (!prepareWriting(name, user, group, size, perm, atime, mtime, ctime)) {
|
|
// qCWarning(KArchiveLog) << "prepareWriting failed";
|
|
return false;
|
|
}
|
|
|
|
// Write data
|
|
// Note: if data is null, don't call write, it would terminate the KCompressionDevice
|
|
if (data.constData() && size && !writeData(data.constData(), size)) {
|
|
// qCWarning(KArchiveLog) << "writeData failed";
|
|
return false;
|
|
}
|
|
|
|
if (!finishWriting(size)) {
|
|
// qCWarning(KArchiveLog) << "finishWriting failed";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool KArchive::writeData(const char *data, qint64 size)
|
|
{
|
|
return doWriteData(data, size);
|
|
}
|
|
|
|
bool KArchive::writeData(QByteArrayView data)
|
|
{
|
|
return doWriteData(data.constData(), data.size());
|
|
}
|
|
|
|
bool KArchive::doWriteData(const char *data, qint64 size)
|
|
{
|
|
bool ok = device()->write(data, size) == size;
|
|
if (!ok) {
|
|
setErrorString(tr("Writing failed: %1").arg(device()->errorString()));
|
|
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,
|
|
const QDateTime &atime,
|
|
const QDateTime &mtime,
|
|
const QDateTime &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,
|
|
const QDateTime &atime,
|
|
const QDateTime &mtime,
|
|
const QDateTime &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,
|
|
const QDateTime &atime,
|
|
const QDateTime &mtime,
|
|
const QDateTime &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);
|
|
}
|
|
|
|
void KArchive::setErrorString(const QString &errorStr)
|
|
{
|
|
d->errorStr = errorStr;
|
|
}
|
|
|
|
static QString getCurrentUserName()
|
|
{
|
|
#if defined(Q_OS_UNIX)
|
|
struct passwd *pw = getpwuid(getuid());
|
|
return pw ? QFile::decodeName(pw->pw_name) : QString::number(getuid());
|
|
#elif defined(Q_OS_WIN)
|
|
wchar_t buffer[255];
|
|
DWORD size = 255;
|
|
bool ok = GetUserNameW(buffer, &size);
|
|
if (!ok) {
|
|
return QString();
|
|
}
|
|
return QString::fromWCharArray(buffer);
|
|
#else
|
|
return QString();
|
|
#endif
|
|
}
|
|
|
|
static QString getCurrentGroupName()
|
|
{
|
|
#if defined(Q_OS_UNIX)
|
|
struct group *grp = getgrgid(getgid());
|
|
return grp ? QFile::decodeName(grp->gr_name) : QString::number(getgid());
|
|
#elif defined(Q_OS_WIN)
|
|
return QString();
|
|
#else
|
|
return QString();
|
|
#endif
|
|
}
|
|
|
|
KArchiveDirectory *KArchive::rootDir()
|
|
{
|
|
if (!d->rootDir) {
|
|
// qCDebug(KArchiveLog) << "Making root dir ";
|
|
QString username = ::getCurrentUserName();
|
|
QString groupname = ::getCurrentGroupName();
|
|
|
|
d->rootDir = new KArchiveDirectory(this, QStringLiteral("/"), int(0777 + S_IFDIR), QDateTime(), username, groupname, QString());
|
|
}
|
|
return d->rootDir;
|
|
}
|
|
|
|
KArchiveDirectory *KArchive::findOrCreate(const QString &path)
|
|
{
|
|
return d->findOrCreate(path, 0 /*recursionCounter*/);
|
|
}
|
|
|
|
KArchiveDirectory *KArchivePrivate::findOrCreate(const QString &path, int recursionCounter)
|
|
{
|
|
// Check we're not in a path that is ultra deep, this is most probably fine since PATH_MAX on Linux
|
|
// is defined as 4096, so even on /a/a/a/a/a/a 2500 recursions puts us over that limit
|
|
// an ultra deep recursion will make us crash due to not enough stack. Tests show that 1MB stack
|
|
// (default on Linux seems to be 8MB) gives us up to around 4000 recursions
|
|
if (recursionCounter > 2500) {
|
|
qCWarning(KArchiveLog) << "path recursion limit exceeded, bailing out";
|
|
return nullptr;
|
|
}
|
|
// qCDebug(KArchiveLog) << path;
|
|
if (path.isEmpty() || path == QLatin1String("/") || path == QLatin1String(".")) { // root dir => found
|
|
// qCDebug(KArchiveLog) << "returning rootdir";
|
|
return q->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
|
|
KArchiveDirectory *existingEntryParentDirectory;
|
|
const KArchiveEntry *existingEntry = KArchiveDirectoryPrivate::get(q->rootDir())->entry(path, &existingEntryParentDirectory);
|
|
if (existingEntry) {
|
|
if (existingEntry->isDirectory())
|
|
// qCDebug(KArchiveLog) << "found it";
|
|
{
|
|
const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(existingEntry);
|
|
return const_cast<KArchiveDirectory *>(dir);
|
|
} else {
|
|
const KArchiveFile *file = static_cast<const KArchiveFile *>(existingEntry);
|
|
if (file->size() > 0) {
|
|
qCWarning(KArchiveLog) << path << "is normal file, but there are file paths in the archive assuming it is a directory, bailing out";
|
|
return nullptr;
|
|
}
|
|
|
|
qCDebug(KArchiveLog) << path << " is an empty file, assuming it is actually a directory and replacing";
|
|
KArchiveEntry *myEntry = const_cast<KArchiveEntry *>(existingEntry);
|
|
existingEntryParentDirectory->removeEntry(myEntry);
|
|
delete myEntry;
|
|
}
|
|
}
|
|
|
|
// 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 = q->rootDir();
|
|
dirname = path;
|
|
} else {
|
|
QString left = path.left(pos);
|
|
dirname = path.mid(pos + 1);
|
|
parent = findOrCreate(left, recursionCounter + 1); // recursive call... until we find an existing dir.
|
|
}
|
|
|
|
if (!parent) {
|
|
return nullptr;
|
|
}
|
|
|
|
// qCDebug(KArchiveLog) << "found parent " << parent->name() << " adding " << dirname << " to ensure " << path;
|
|
// Found -> add the missing piece
|
|
KArchiveDirectory *e = new KArchiveDirectory(q, dirname, rootDir->permissions(), rootDir->date(), rootDir->user(), rootDir->group(), QString());
|
|
if (parent->addEntryV2(e)) {
|
|
return e; // now a directory to <path> exists
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
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 ;)
|
|
delete d->rootDir; // but if it happens, don't leak
|
|
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->cancelWriting();
|
|
delete saveFile;
|
|
saveFile = nullptr;
|
|
dev = nullptr;
|
|
}
|
|
}
|
|
|
|
// this is a hacky wrapper to check if time_t value is invalid
|
|
QDateTime KArchivePrivate::time_tToDateTime(uint time_t)
|
|
{
|
|
if (time_t == uint(-1)) {
|
|
return QDateTime();
|
|
}
|
|
return QDateTime::fromSecsSinceEpoch(time_t);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
/////////////////////// KArchiveEntry //////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
class KArchiveEntryPrivate
|
|
{
|
|
public:
|
|
KArchiveEntryPrivate(KArchive *_archive,
|
|
const QString &_name,
|
|
int _access,
|
|
const QDateTime &_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;
|
|
QDateTime date;
|
|
mode_t access;
|
|
QString user;
|
|
QString group;
|
|
QString symlink;
|
|
KArchive *archive;
|
|
};
|
|
|
|
KArchiveEntry::KArchiveEntry(KArchive *t,
|
|
const QString &name,
|
|
int access,
|
|
const QDateTime &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::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,
|
|
const QDateTime &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) {
|
|
// qCWarning(KArchiveLog) << "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;
|
|
}
|
|
|
|
static QFileDevice::Permissions withExecutablePerms(QFileDevice::Permissions filePerms, mode_t perms)
|
|
{
|
|
if (perms & 01) {
|
|
filePerms |= QFileDevice::ExeOther;
|
|
}
|
|
|
|
if (perms & 010) {
|
|
filePerms |= QFileDevice::ExeGroup;
|
|
}
|
|
|
|
if (perms & 0100) {
|
|
filePerms |= QFileDevice::ExeOwner;
|
|
}
|
|
|
|
return filePerms;
|
|
}
|
|
|
|
bool KArchiveFile::copyTo(const QString &dest) const
|
|
{
|
|
QFile f(dest + QLatin1Char('/') + name());
|
|
if (f.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
|
|
QIODevice *inputDev = createDevice();
|
|
if (!inputDev) {
|
|
f.remove();
|
|
return false;
|
|
}
|
|
|
|
// 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_UNUSED(n) // except in Q_ASSERT
|
|
Q_ASSERT(n == currentChunkSize);
|
|
f.write(array.data(), currentChunkSize);
|
|
remainingSize -= currentChunkSize;
|
|
}
|
|
f.setPermissions(withExecutablePerms(f.permissions(), permissions()));
|
|
f.close();
|
|
|
|
delete inputDev;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
//////////////////////// KArchiveDirectory /////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
KArchiveDirectory::KArchiveDirectory(KArchive *t,
|
|
const QString &name,
|
|
int access,
|
|
const QDateTime &date,
|
|
const QString &user,
|
|
const QString &group,
|
|
const QString &symlink)
|
|
: KArchiveEntry(t, name, access, date, user, group, symlink)
|
|
, d(new KArchiveDirectoryPrivate(this))
|
|
{
|
|
}
|
|
|
|
KArchiveDirectory::~KArchiveDirectory()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
QStringList KArchiveDirectory::entries() const
|
|
{
|
|
return d->entries.keys();
|
|
}
|
|
|
|
const KArchiveEntry *KArchiveDirectory::entry(const QString &_name) const
|
|
{
|
|
KArchiveDirectory *dummy;
|
|
return d->entry(_name, &dummy);
|
|
}
|
|
|
|
const KArchiveFile *KArchiveDirectory::file(const QString &name) const
|
|
{
|
|
const KArchiveEntry *e = entry(name);
|
|
if (e && e->isFile()) {
|
|
return static_cast<const KArchiveFile *>(e);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void KArchiveDirectory::addEntry(KArchiveEntry *entry)
|
|
{
|
|
addEntryV2(entry);
|
|
}
|
|
|
|
bool KArchiveDirectory::addEntryV2(KArchiveEntry *entry)
|
|
{
|
|
if (d->entries.value(entry->name())) {
|
|
qCWarning(KArchiveLog) << "directory " << name() << "has entry" << entry->name() << "already";
|
|
delete entry;
|
|
return false;
|
|
}
|
|
d->entries.insert(entry->name(), entry);
|
|
return true;
|
|
}
|
|
|
|
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()) {
|
|
qCWarning(KArchiveLog) << "directory " << name() << "has no entry with name " << entry->name();
|
|
return;
|
|
}
|
|
if (it.value() != entry) {
|
|
qCWarning(KArchiveLog) << "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();
|
|
}
|
|
|
|
bool KArchiveDirectory::copyTo(const QString &dest, bool recursiveCopy) const
|
|
{
|
|
QDir root;
|
|
const QString destDir(QDir(dest).absolutePath()); // get directory path without any "." or ".."
|
|
|
|
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(destDir); // ... with given path
|
|
do {
|
|
const KArchiveDirectory *curDir = dirStack.pop();
|
|
|
|
// extract only to specified folder if it is located within archive's extraction folder
|
|
// otherwise put file under root position in extraction folder
|
|
QString curDirName = dirNameStack.pop();
|
|
if (!QDir(curDirName).absolutePath().startsWith(destDir)) {
|
|
qCWarning(KArchiveLog) << "Attempted export into folder" << curDirName << "which is outside of the extraction root folder" << destDir << "."
|
|
<< "Changing export of contained files to extraction root folder.";
|
|
curDirName = destDir;
|
|
}
|
|
|
|
if (!root.mkpath(curDirName)) {
|
|
return false;
|
|
}
|
|
|
|
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()) {
|
|
QString linkName = curDirName + QLatin1Char('/') + curEntry->name();
|
|
// To create a valid link on Windows, linkName must have a .lnk file extension.
|
|
#ifdef Q_OS_WIN
|
|
if (!linkName.endsWith(QLatin1String(".lnk"))) {
|
|
linkName += QLatin1String(".lnk");
|
|
}
|
|
#endif
|
|
QFile symLinkTarget(curEntry->symLinkTarget());
|
|
if (!symLinkTarget.link(linkName)) {
|
|
// qCDebug(KArchiveLog) << "symlink(" << curEntry->symLinkTarget() << ',' << linkName << ") failed:" << strerror(errno);
|
|
}
|
|
} 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());
|
|
|
|
std::sort(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();
|
|
if (!f->copyTo(fileToDir[pos])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void KArchive::virtual_hook(int, void *)
|
|
{
|
|
/*BASE::virtual_hook( id, data )*/;
|
|
}
|
|
|
|
void KArchiveEntry::virtual_hook(int, void *)
|
|
{
|
|
/*BASE::virtual_hook( id, data );*/
|
|
}
|
|
|
|
void KArchiveFile::virtual_hook(int id, void *data)
|
|
{
|
|
KArchiveEntry::virtual_hook(id, data);
|
|
}
|
|
|
|
void KArchiveDirectory::virtual_hook(int id, void *data)
|
|
{
|
|
KArchiveEntry::virtual_hook(id, data);
|
|
}
|