kde-extraapps/ark/plugins/clirarplugin/cliplugin.cpp
2014-11-18 17:46:34 +00:00

362 lines
14 KiB
C++

/*
* ark -- archiver for the KDE project
*
* Copyright (C) 2009 Harald Hvaal <haraldhv@stud.ntnu.no>
* Copyright (C) 2010-2011,2014 Raphael Kubo da Costa <rakuco@FreeBSD.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "cliplugin.h"
#include "kerfuffle/cliinterface.h"
#include "kerfuffle/kerfuffle_export.h"
#include <KDebug>
#include <QDateTime>
#include <QDir>
#include <QString>
#include <QStringList>
using namespace Kerfuffle;
CliPlugin::CliPlugin(QObject *parent, const QVariantList& args)
: CliInterface(parent, args)
, m_parseState(ParseStateColumnDescription1)
, m_isPasswordProtected(false)
, m_remainingIgnoredSubHeaderLines(0)
, m_remainingIgnoredDetailsLines(0)
, m_isUnrarFree(false)
, m_isUnrarVersion5(false)
{
}
CliPlugin::~CliPlugin()
{
}
// #272281: the proprietary unrar program does not like trailing '/'s
// in directories passed to it when extracting only part of
// the files in an archive.
QString CliPlugin::escapeFileName(const QString &fileName) const
{
if (fileName.endsWith(QLatin1Char('/'))) {
return fileName.left(fileName.length() - 1);
}
return fileName;
}
ParameterList CliPlugin::parameterList() const
{
static ParameterList p;
if (p.isEmpty()) {
p[CaptureProgress] = true;
p[ListProgram] = p[ExtractProgram] = QStringList() << QLatin1String( "unrar" );
p[DeleteProgram] = p[AddProgram] = QStringList() << QLatin1String( "rar" );
p[ListArgs] = QStringList() << QLatin1String( "vt" ) << QLatin1String( "-c-" ) << QLatin1String( "-v" ) << QLatin1String( "$Archive" );
p[ExtractArgs] = QStringList() << QLatin1String( "-kb" ) << QLatin1String( "-p-" )
<< QLatin1String( "$PreservePathSwitch" )
<< QLatin1String( "$PasswordSwitch" )
<< QLatin1String( "$RootNodeSwitch" )
<< QLatin1String( "$Archive" )
<< QLatin1String( "$Files" );
p[PreservePathSwitch] = QStringList() << QLatin1String( "x" ) << QLatin1String( "e" );
p[RootNodeSwitch] = QStringList() << QLatin1String( "-ap$Path" );
p[PasswordSwitch] = QStringList() << QLatin1String( "-p$Password" );
p[DeleteArgs] = QStringList() << QLatin1String( "d" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" );
p[FileExistsExpression] = QLatin1String( "^(.+) already exists. Overwrite it" );
p[FileExistsInput] = QStringList()
<< QLatin1String( "Y" ) //overwrite
<< QLatin1String( "N" ) //skip
<< QLatin1String( "A" ) //overwrite all
<< QLatin1String( "E" ) //autoskip
<< QLatin1String( "Q" ) //cancel
;
p[AddArgs] = QStringList() << QLatin1String( "a" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" );
p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for");
p[WrongPasswordPatterns] = QStringList() << QLatin1String("password incorrect") << QLatin1String("wrong password");
p[ExtractionFailedPatterns] = QStringList() << QLatin1String( "CRC failed" ) << QLatin1String( "Cannot find volume" );
}
return p;
}
bool CliPlugin::readListLine(const QString &line)
{
static const QLatin1String headerString("----------------------");
static const QLatin1String subHeaderString("Data header type: ");
static const QLatin1String columnDescription1String(" Size Packed Ratio Date Time Attr CRC Meth Ver");
static const QLatin1String columnDescription2String(" Host OS Solid Old"); // Only present in unrar-nonfree
if (m_isUnrarVersion5) {
int colonPos = line.indexOf(QLatin1Char(':'));
if (colonPos == -1) {
if (m_entryFileName.isEmpty()) {
return true;
}
ArchiveEntry e;
QString compressionRatio = m_entryDetails.value(QLatin1String("ratio"));
compressionRatio.chop(1); // Remove the '%'
QString time = m_entryDetails.value(QLatin1String("mtime"));
// FIXME unrar 5 beta 8 seems to lack the seconds, or the trailing ,000 is not the milliseconds
QDateTime ts = QDateTime::fromString(time, QLatin1String("yyyy-MM-dd HH:mm,zzz"));
bool isDirectory = m_entryDetails.value(QLatin1String("type")) == QLatin1String("Directory");
if (isDirectory && !m_entryFileName.endsWith(QLatin1Char( '/' ))) {
m_entryFileName += QLatin1Char( '/' );
}
QString compression = m_entryDetails.value(QLatin1String("compression"));
int optionPos = compression.indexOf(QLatin1Char('-'));
if (optionPos != -1) {
e[Method] = compression.mid(optionPos);
e[Version] = compression.left(optionPos).trimmed();
} else {
// no method specified
e[Method].clear();
e[Version] = compression;
}
m_isPasswordProtected = m_entryDetails.value(QLatin1String("flags")).contains(QLatin1String("encrypted"));
e[FileName] = m_entryFileName;
e[InternalID] = m_entryFileName;
e[Size] = m_entryDetails.value(QLatin1String("size"));
e[CompressedSize] = m_entryDetails.value(QLatin1String("packed size"));
e[Ratio] = compressionRatio;
e[Timestamp] = ts;
e[IsDirectory] = isDirectory;
e[Permissions] = m_entryDetails.value(QLatin1String("attributes"));
e[CRC] = m_entryDetails.value(QLatin1String("crc32"));
e[IsPasswordProtected] = m_isPasswordProtected;
kDebug() << "Added entry: " << e;
emit entry(e);
m_entryFileName.clear();
return true;
}
QString key = line.left(colonPos).trimmed().toLower();
QString value = line.mid(colonPos + 2);
if (key == QLatin1String("name")) {
m_entryFileName = value;
m_entryDetails.clear();
return true;
}
// in multivolume archives, the split CRC32 is denoted specially
if (key == QLatin1String("pack-crc32")) {
key = key.mid(5);
}
m_entryDetails.insert(key, value);
return true;
}
switch (m_parseState)
{
case ParseStateColumnDescription1:
if (line.startsWith(QLatin1String("Details:"))) {
m_isUnrarVersion5 = true;
setListEmptyLines(true);
// no previously detected entry
m_entryFileName.clear();
}
if (line.startsWith(columnDescription1String)) {
m_parseState = ParseStateColumnDescription2;
}
break;
case ParseStateColumnDescription2:
// #243273: We need a way to differentiate unrar and unrar-free,
// as their output for the "vt" option is different.
// Currently, we differ them by checking if "vt" produces
// two lines of column names before the header string, as
// only unrar does that (unrar-free always outputs one line
// for column names regardless of how verbose we tell it to
// be).
if (line.startsWith(columnDescription2String)) {
m_parseState = ParseStateHeader;
} else if (line.startsWith(headerString)) {
m_parseState = ParseStateEntryFileName;
m_isUnrarFree = true;
}
break;
case ParseStateHeader:
if (line.startsWith(headerString)) {
m_parseState = ParseStateEntryFileName;
}
break;
case ParseStateEntryFileName:
if (m_remainingIgnoredSubHeaderLines > 0) {
--m_remainingIgnoredSubHeaderLines;
return true;
}
// #242071: The RAR file format has the concept of service headers,
// such as CMT (comments), STM (NTFS alternate data streams)
// and RR (recovery record). These service headers do no
// interest us, and ignoring them seems harmless (at least
// 7zip and WinRAR do not show them either).
if (line.startsWith(subHeaderString)) {
// subHeaderString's length is 18
const QString subHeaderType(line.mid(18));
// XXX: If we ever support archive comments, this code must
// be changed, because the comments will be shown after
// a CMT subheader and will have an arbitrary number of lines
if (subHeaderType == QLatin1String("STM")) {
m_remainingIgnoredSubHeaderLines = 4;
} else {
m_remainingIgnoredSubHeaderLines = 3;
}
kDebug() << "Found a subheader of type" << subHeaderType;
kDebug() << "The next" << m_remainingIgnoredSubHeaderLines
<< "lines will be ignored";
return true;
} else if (line.startsWith(headerString)) {
m_parseState = ParseStateHeader;
return true;
}
m_isPasswordProtected = (line.at(0) == QLatin1Char( '*' ));
// Start from 1 because the first character is either ' ' or '*'
m_entryFileName = QDir::fromNativeSeparators(line.mid(1));
m_parseState = ParseStateEntryDetails;
break;
case ParseStateEntryIgnoredDetails:
if (m_remainingIgnoredDetailsLines > 0) {
--m_remainingIgnoredDetailsLines;
return true;
}
m_parseState = ParseStateEntryFileName;
break;
case ParseStateEntryDetails:
if (line.startsWith(headerString)) {
m_parseState = ParseStateHeader;
return true;
}
const QStringList details = line.split(QLatin1Char( ' ' ),
QString::SkipEmptyParts);
QDateTime ts(QDate::fromString(details.at(3),
QLatin1String("dd-MM-yy")),
QTime::fromString(details.at(4),
QLatin1String("hh:mm")));
// unrar outputs dates with a 2-digit year but QDate takes it as 19??
// let's take 1950 is cut-off; similar to KDateTime
if (ts.date().year() < 1950) {
ts = ts.addYears(100);
}
bool isDirectory = ((details.at(5).at(0) == QLatin1Char( 'd' )) ||
(details.at(5).at(1) == QLatin1Char( 'D' )));
if (isDirectory && !m_entryFileName.endsWith(QLatin1Char( '/' ))) {
m_entryFileName += QLatin1Char( '/' );
}
// If the archive is a multivolume archive, a string indicating
// whether the archive's position in the volume is displayed
// instead of the compression ratio.
QString compressionRatio = details.at(2);
if ((compressionRatio == QLatin1String("<--")) ||
(compressionRatio == QLatin1String("<->")) ||
(compressionRatio == QLatin1String("-->"))) {
compressionRatio = QLatin1Char( '0' );
} else {
compressionRatio.chop(1); // Remove the '%'
}
// TODO:
// - Permissions differ depending on the system the entry was added
// to the archive.
// - unrar reports the ratio as ((compressed size * 100) / size);
// we consider ratio as (100 * ((size - compressed size) / size)).
ArchiveEntry e;
e[FileName] = m_entryFileName;
e[InternalID] = m_entryFileName;
e[Size] = details.at(0);
e[CompressedSize] = details.at(1);
e[Ratio] = compressionRatio;
e[Timestamp] = ts;
e[IsDirectory] = isDirectory;
e[Permissions] = details.at(5);
e[CRC] = details.at(6);
e[Method] = details.at(7);
e[Version] = details.at(8);
e[IsPasswordProtected] = m_isPasswordProtected;
kDebug() << "Added entry: " << e;
// #314297: When RAR 3.x and RAR 4.x list a symlink, they output an
// extra line after the "Host OS/Solid/Old" one mentioning the
// target of the symlink in question. We are not interested in
// this line at the moment, so we just tell the parser to skip
// it.
if (e[Permissions].toString().startsWith(QLatin1Char('l'))) {
m_remainingIgnoredDetailsLines = 1;
} else {
m_remainingIgnoredDetailsLines = 0;
}
emit entry(e);
// #243273: unrar-free does not output the third file entry line,
// skip directly to parsing a new entry.
if (m_isUnrarFree) {
m_parseState = ParseStateEntryFileName;
} else {
m_parseState = ParseStateEntryIgnoredDetails;
}
break;
}
return true;
}
KERFUFFLE_EXPORT_PLUGIN(CliPlugin)
#include "cliplugin.moc"